You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@hertzbeat.apache.org by zh...@apache.org on 2026/06/03 08:18:10 UTC

(hertzbeat) branch 2.0.0 created (now cd0b0a4c37)

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

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


      at cd0b0a4c37 feat(web-next): finalize alpha localization cleanup

This branch includes the following new commits:

     new f6ca255355 feat: add web-next observability migration
     new 01c57a50d3 feat(web-next): add shared UI lab foundation
     new 584fca5f0d feat(web-next): add client runtime foundation
     new 388b76446e feat(web-next): restore monitor editor parity
     new d21cadcbe3 feat(web-next): restore monitor list parity
     new 457076998a feat(web-next): restore monitor detail parity
     new a6723173fd feat(web-next): expand runtime i18n catalog
     new f114f1c6d1 feat(web-next): restore shell utility parity
     new e403551b18 feat(web-next): restore overview dashboard parity
     new 0f6c22d9c7 feat(web-next): restore alert workflow parity
     new cae4d75765 feat(web-next): restore collector plugin label parity
     new 2991c30489 feat(web-next): restore settings platform parity
     new c64801e73d feat(web-next): restore passport parity
     new 585626c269 feat(web-next): restore status actions parity
     new 5e3edd3f41 feat(web-next): restore entity workbench parity
     new a571f59693 feat(web-next): restore observability workbench parity
     new cddb70aa56 feat(web-next): close dashboard route catalog parity
     new 95c7899533 feat(web-next): restore bulletin workbench compatibility
     new 435cc244aa feat(web-next): align shared UI primitive parity
     new bf2a36868b feat(web-next): add release runtime readiness gates
     new aac09ca2a3 feat(web-next): close M10 route cutover audit
     new c4d242fa6a feat(web-next): add M10 frontend convergence gate
     new f094cf7a2a feat(backend): harden OTLP ingestion and token scopes
     new 66ae2f6c88 fix(ai): align tools with backend API changes
     new 22d4574d54 fix(collector): include mysql r2dbc runtime
     new 778011d219 fix(remoting): use background task executor
     new 16dd2828b4 feat(manager): add entity workbench backend models
     new 7e2b3582e1 test(web-next): add HertzBeat 2.0 gap and ecosystem gates
     new 43b8d665f3 fix(web-next): align alert setting import lifecycle
     new 202f407d2c fix(web-next): stop topology first-paint overfit
     new eb5c666d8d test(web-next): align topology toolbar fit metadata
     new 81c4bd6576 fix(web-next): remove hand-drawn topology legend marks
     new 0e284d69c3 feat(web-next): expose source-backed topology node legend
     new 72ddbb8020 fix(web-next): scope topology legend to visible graph types
     new 446b18b603 fix(web-next): suppress empty topology legends
     new 1d6164c2d5 fix(web-next): tighten topology toolbar alignment
     new b8f13bffe9 fix(web-next): dedupe topology focus filters
     new 4e969d7bcc fix(web-next): cap topology auto-fit zoom
     new 64f7dd69a2 fix(web-next): dedupe topology header scope summary
     new c0eb3a0814 fix(web-next): source topology node icons from catalog
     new 69b98c9375 fix(web-next): keep topology selection out of render window
     new 23523355b7 fix(web-next): keep topology search input explicit
     new f47ac7311f fix(web-next): surface topology windowed metric table
     new 04aa6d5fdf fix(web-next): bound topology operator zoom
     new 6afe1a7f0e fix(web-next): keep compact topology graphs at 1x
     new 7ff8e34685 fix(web-next): clamp compact topology zoom after centering
     new d068e0a73a Prepare HertzBeat community alpha preview
     new fc5c12431e chore(frontend): retire angular web app
     new 3832485f0f refactor: externalize backend localization messages
     new cd0b0a4c37 feat(web-next): finalize alpha localization cleanup

The 50 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 37/50: fix(web-next): dedupe topology focus filters

Posted by zh...@apache.org.
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 b8f13bffe9c2314adabc4347405a09dd7bf337f3
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 08:30:10 2026 +0800

    fix(web-next): dedupe topology focus filters
---
 web-next/app/topology/page.test.tsx               | 5 ++++-
 web-next/app/ui-lab/page.test.tsx                 | 3 +++
 web-next/packages/hertzbeat-ui/src/index.test.tsx | 6 +++++-
 web-next/packages/hertzbeat-ui/src/index.tsx      | 9 ++++++---
 4 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 7ba725afc8..c15758cea4 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -561,10 +561,13 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-focus-trail-position-contract="document-flow"');
     expect(html).toContain('data-hz-topology-focus-trail-priority="canvas"');
     expect(html).toContain('data-hz-topology-focus-trail-alignment="shared-control-grid"');
-    expect(html).toContain('data-hz-topology-focus-trail-inset="16px"');
+    expect(html).toContain('data-hz-topology-focus-trail-inset="0px"');
     expect(html).toContain('data-hz-topology-focus-trail-control-height="28px"');
     expect(html).toContain('data-hz-topology-focus-trail-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-focus-trail-visual-weight-owner="hertzbeat-ui-focus-trail-visual-weight"');
+    expect(html).toContain('data-hz-topology-focus-filter-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-focus-filter-visibility-owner="hertzbeat-ui-focus-trail-filter-visibility"');
+    expect(html).toContain('data-hz-topology-focus-filter-deduped-by="topology-toolbar"');
     expect(html).toContain('data-hz-topology-focus-trail-chrome="frameless"');
     expect(html).toContain('data-hz-topology-focus-trail-frame="none"');
     expect(html).toContain('data-topology-focus-trail-compactness="graph-dock"');
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index 09959b54d8..dbe95c950a 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2249,6 +2249,9 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-focus-crumb-label-owner="hertzbeat-ui-focus-trail-crumb-label"');
     expect(html).toContain('data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value"');
     expect(html).toContain('data-hz-topology-focus-filters-owner="hertzbeat-ui-focus-trail-filters"');
+    expect(html).toContain('data-hz-topology-focus-filter-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-focus-filter-visibility-owner="hertzbeat-ui-focus-trail-filter-visibility"');
+    expect(html).toContain('data-hz-topology-focus-filter-deduped-by="topology-toolbar"');
     expect(html).toContain('data-hz-topology-focus-filter="environment"');
     expect(html).toContain('data-hz-topology-focus-filter="source"');
     expect(html).toContain('data-hz-topology-focus-filter-owner="hertzbeat-ui-focus-trail-filter"');
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index 8747e1a6ad..f75ebf9007 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -5739,10 +5739,14 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-focus-trail-position-contract="document-flow"');
     expect(html).toContain('data-hz-topology-focus-trail-priority="canvas"');
     expect(html).toContain('data-hz-topology-focus-trail-alignment="shared-control-grid"');
-    expect(html).toContain('data-hz-topology-focus-trail-inset="16px"');
+    expect(html).toContain('data-hz-topology-focus-trail-inset="0px"');
     expect(html).toContain('data-hz-topology-focus-trail-control-height="28px"');
     expect(html).toContain('data-hz-topology-focus-trail-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-focus-trail-visual-weight-owner="hertzbeat-ui-focus-trail-visual-weight"');
+    expect(html).toContain('data-hz-topology-focus-filter-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-focus-filter-visibility-owner="hertzbeat-ui-focus-trail-filter-visibility"');
+    expect(html).toContain('data-hz-topology-focus-filter-deduped-by="topology-toolbar"');
+    expect(html).toContain('sr-only');
     expect(html).toContain('py-1');
     expect(html).toContain('h-6');
     expect(html).not.toContain('absolute');
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index 93e6dde149..91225227d0 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -11843,7 +11843,7 @@ export function HzTopologyFocusTrail({
             ? 'flex min-w-0 flex-wrap items-center gap-1.5 bg-[var(--hz-ui-surface)] text-[12px]'
             : 'grid min-w-0 gap-2 bg-[var(--hz-ui-surface)] text-[12px] lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center',
         isGraphDock && boundary === 'section'
-          ? 'border-t border-[var(--hz-ui-line-soft)] px-4 py-1'
+          ? 'border-t border-[var(--hz-ui-line-soft)] py-1'
           : isRail && boundary === 'section'
           ? 'border-t border-[var(--hz-ui-line-soft)] px-3 py-1'
           : topologyFocusTrailBoundaryClassName[boundary],
@@ -11864,7 +11864,7 @@ export function HzTopologyFocusTrail({
       data-hz-topology-focus-trail-position-contract={isGraphDock ? 'document-flow' : undefined}
       data-hz-topology-focus-trail-priority={isGraphDock ? 'canvas' : undefined}
       data-hz-topology-focus-trail-alignment={isGraphDock ? 'shared-control-grid' : undefined}
-      data-hz-topology-focus-trail-inset={isGraphDock ? '16px' : undefined}
+      data-hz-topology-focus-trail-inset={isGraphDock ? '0px' : undefined}
       data-hz-topology-focus-trail-control-height={isGraphDock ? '28px' : undefined}
       data-hz-topology-focus-trail-visual-weight={isGraphDock ? 'low-interruption' : undefined}
       data-hz-topology-focus-trail-visual-weight-owner={isGraphDock ? 'hertzbeat-ui-focus-trail-visual-weight' : undefined}
@@ -11934,9 +11934,12 @@ export function HzTopologyFocusTrail({
           <div
             className={cn(
               'flex min-w-0 items-center gap-1 overflow-x-auto',
-              isGraphDock ? 'flex-nowrap' : isRail ? 'flex-wrap overflow-x-visible' : 'flex-nowrap'
+              isGraphDock ? 'sr-only flex-nowrap' : isRail ? 'flex-wrap overflow-x-visible' : 'flex-nowrap'
             )}
             data-hz-topology-focus-filters-owner="hertzbeat-ui-focus-trail-filters"
+            data-hz-topology-focus-filter-visibility={isGraphDock ? 'assistive' : 'visible'}
+            data-hz-topology-focus-filter-visibility-owner="hertzbeat-ui-focus-trail-filter-visibility"
+            data-hz-topology-focus-filter-deduped-by={isGraphDock ? 'topology-toolbar' : undefined}
           >
             {filters.map(filter => {
               const { id, label: filterLabel, value, className: filterClassName, ...filterProps } = filter;


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 16/50: feat(web-next): restore observability workbench parity

Posted by zh...@apache.org.
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 a571f59693727fc1ce0a1efca807d3af0e9bae0b
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 01:39:28 2026 +0800

    feat(web-next): restore observability workbench parity
---
 web-next/app/events/page.test.ts                   |   83 +-
 web-next/app/events/page.tsx                       |   13 +-
 web-next/app/exception/[type]/page.test.tsx        |    8 +
 web-next/app/exception/[type]/page.tsx             |    4 +-
 web-next/app/explorer/explorer-page.tsx            |  297 +++++
 web-next/app/explorer/page.test.tsx                |   92 +-
 web-next/app/explorer/page.tsx                     |  226 +---
 .../ingestion/otlp/metrics/otlp-metrics-page.tsx   | 1166 +++++++++++++++++
 web-next/app/ingestion/otlp/metrics/page.test.tsx  |  800 +++++++++++-
 web-next/app/ingestion/otlp/metrics/page.tsx       |  911 +------------
 .../app/ingestion/otlp/metrics/route-state.test.ts |   18 +
 web-next/app/ingestion/otlp/metrics/route-state.ts |   17 +-
 .../app/ingestion/otlp/{page.tsx => otlp-page.tsx} |  211 +--
 web-next/app/ingestion/otlp/page.test.tsx          |   72 +-
 web-next/app/ingestion/otlp/page.tsx               |  693 +---------
 .../app/log/integration/[source]/page.test.tsx     |   16 +
 web-next/app/log/integration/[source]/page.tsx     |    4 +-
 web-next/app/log/manage/log-manage-page.tsx        | 1363 ++++++++++++--------
 web-next/app/log/manage/page.test.tsx              |  399 +++++-
 web-next/app/log/manage/page.tsx                   |   13 +-
 web-next/app/log/manage/route-state.test.ts        |   11 +-
 web-next/app/log/manage/route-state.ts             |   13 +-
 web-next/app/log/stream/page.tsx                   |    2 -
 .../app/trace/manage/page.route-context.test.ts    |    5 +-
 web-next/app/trace/manage/page.test.tsx            |  478 ++++++-
 web-next/app/trace/manage/page.tsx                 |  997 +-------------
 web-next/app/trace/manage/route-state.test.ts      |   11 +-
 web-next/app/trace/manage/route-state.ts           |   16 +-
 .../app/trace/manage/trace-manage-client.test.tsx  |   97 ++
 web-next/app/trace/manage/trace-manage-page.tsx    | 1346 +++++++++++++++++++
 .../log-manage/log-dialog.chrome.test.ts           |   13 +
 .../log-manage/log-related-trace-dialog.test.tsx   |   85 +-
 .../log-manage/log-related-trace-dialog.tsx        |  222 +++-
 .../log-manage/log-stream-detail-dialog.test.tsx   |   34 +-
 .../log-manage/log-stream-detail-dialog.tsx        |  137 +-
 web-next/components/observability/data-table.tsx   |    5 +-
 .../observability/echarts-panel.test.tsx           |   19 +-
 .../components/observability/echarts-panel.tsx     |  141 +-
 .../components/observability/tab-strip.test.tsx    |   19 +
 web-next/components/observability/tab-strip.tsx    |    7 +
 .../observability/time-range-control.test.tsx      |    4 +-
 .../observability/time-range-control.tsx           |   54 +-
 .../components/observability/timeline.test.tsx     |   13 +
 web-next/components/observability/timeline.tsx     |    5 +-
 .../components/observability/waterfall.test.tsx    |    6 +
 web-next/components/observability/waterfall.tsx    |   57 +-
 .../observability/workspace-tab-strip.tsx          |    5 +-
 .../pages/exception-center-surface.test.tsx        |   17 +-
 .../components/pages/exception-center-surface.tsx  |   71 +-
 .../pages/log-integration-redirect-shell.test.tsx  |    7 +-
 web-next/lib/exception-center/view-model.test.ts   |   45 +-
 web-next/lib/exception-center/view-model.ts        |  103 +-
 web-next/lib/explorer-surface/controller.test.ts   |  202 +++
 web-next/lib/explorer-surface/controller.ts        |  232 ++++
 web-next/lib/explorer-surface/view-model.test.ts   |   25 +-
 web-next/lib/explorer-surface/view-model.ts        |   67 +-
 web-next/lib/log-integration/navigation.ts         |    6 +-
 web-next/lib/log-manage/display-mapping.test.ts    |   13 +-
 web-next/lib/log-manage/display-mapping.ts         |   21 +
 web-next/lib/log-manage/query-state.test.ts        |   67 +
 web-next/lib/log-manage/query-state.ts             |   54 +-
 web-next/lib/log-manage/view-model.test.ts         |   12 +
 web-next/lib/log-manage/view-model.ts              |   18 +-
 web-next/lib/otlp-center/controller.test.ts        |   16 +
 web-next/lib/otlp-center/controller.ts             |   17 +-
 web-next/lib/otlp-center/view-model.test.ts        |  119 +-
 web-next/lib/otlp-center/view-model.ts             |  141 +-
 web-next/lib/otlp-metrics/controller.test.ts       |    3 +
 web-next/lib/otlp-metrics/controller.ts            |    5 +-
 web-next/lib/otlp-metrics/view-model.test.ts       |   36 +-
 web-next/lib/otlp-metrics/view-model.ts            |  110 +-
 web-next/lib/signal-route-context.test.ts          |   66 +-
 web-next/lib/signal-route-context.ts               |  132 +-
 web-next/lib/time-context.test.ts                  |  330 ++++-
 web-next/lib/time-context.ts                       |  331 ++++-
 web-next/lib/trace-manage/query-state.test.ts      |   41 +-
 web-next/lib/trace-manage/query-state.ts           |   61 +-
 web-next/lib/trace-manage/view-model.test.ts       |   38 +-
 web-next/lib/trace-manage/view-model.ts            |   88 +-
 79 files changed, 8311 insertions(+), 4391 deletions(-)

diff --git a/web-next/app/events/page.test.ts b/web-next/app/events/page.test.ts
index 6cb4eb0646..7c698e1129 100644
--- a/web-next/app/events/page.test.ts
+++ b/web-next/app/events/page.test.ts
@@ -1,47 +1,50 @@
-import { readFileSync } from 'node:fs';
-import { resolve } from 'node:path';
-import React from 'react';
-import { renderToStaticMarkup } from 'react-dom/server';
-import { describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 
-vi.mock('../log/manage/log-manage-page', () => ({
-  default: ({ forcedView, showViewToggle }: any) =>
-    React.createElement(
-      'main',
-      {
-        'data-log-manage-route': 'otlp-cold-log-workbench',
-        'data-log-manage-style-baseline': 'hertzbeat-cold-matte',
-        'data-forced-view': forcedView,
-        'data-show-view-toggle': String(showViewToggle)
-      },
-      React.createElement('section', { 'data-log-manage-query-bar': 'cold-query-row' }, '日志工作台'),
-      React.createElement('div', { 'data-log-manage-log-list': 'cold-dense-log-list' }, '日志列表'),
-      React.createElement('aside', { 'data-log-manage-detail-panel': 'cold-detail-panel' }, '日志详情')
-    )
+const redirect = vi.fn();
+
+vi.mock('next/navigation', () => ({
+  redirect
 }));
 
 describe('events alias route', () => {
-  it('keeps the events alias on the HertzBeat log workbench without external-product copy', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/events/page.tsx'), 'utf8');
+  beforeEach(() => {
+    redirect.mockReset();
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+  });
+
+  it('redirects events compatibility traffic to the canonical log explorer view', async () => {
     const { default: EventsAliasPage } = await import('./page');
-    const html = renderToStaticMarkup(React.createElement(EventsAliasPage));
 
-    expect(html).toContain('data-log-manage-route="otlp-cold-log-workbench"');
-    expect(html).toContain('data-log-manage-style-baseline="hertzbeat-cold-matte"');
-    expect(html).toContain('data-log-manage-query-bar="cold-query-row"');
-    expect(html).toContain('data-log-manage-log-list="cold-dense-log-list"');
-    expect(html).toContain('data-log-manage-detail-panel="cold-detail-panel"');
-    expect(html).toContain('日志工作台');
-    expect(html).toContain('data-forced-view="explorer"');
-    expect(html).toContain('data-show-view-toggle="false"');
-    expect(source).toContain("from '../log/manage/log-manage-page'");
-    expect(source).toContain('forcedView="explorer"');
-    expect(source).not.toContain('shellHeader');
-    expect(source).not.toContain('SigNoZ');
-    expect(source).not.toContain('Logs Explorer');
-    expect(source).not.toContain('Legacy events traffic');
-    expect(html).not.toContain('signoz-');
-    expect(source).not.toContain('redirect(');
-    expect(source).not.toContain('buildLogCompatRouteUrl');
-  });
+    await expect(EventsAliasPage({ searchParams: Promise.resolve({}) })).rejects.toThrow('redirect:/log/manage?view=list');
+    expect(redirect).toHaveBeenCalledWith('/log/manage?view=list');
+  }, 20000);
+
+  it('preserves log filters and machine context while stripping display-only labels', async () => {
+    const { default: EventsAliasPage } = await import('./page');
+
+    await expect(
+      EventsAliasPage({
+        searchParams: Promise.resolve({
+          content: ' checkout timeout ',
+          traceId: ' trace-123 ',
+          severityNumber: '17',
+          start: '10',
+          end: '20',
+          entityId: '7',
+          entityName: 'Checkout API',
+          returnTo: '/overview?returnLabel=Overview',
+          returnLabel: 'Overview',
+          serviceName: 'checkout',
+          environment: ['prod', 'staging']
+        })
+      })
+    ).rejects.toThrow(
+      'redirect:/log/manage?search=checkout+timeout&traceId=trace-123&severityNumber=17&view=list&start=10&end=20&entityId=7&entityName=Checkout+API&returnTo=%2Foverview&serviceName=checkout&environment=prod'
+    );
+    expect(redirect).toHaveBeenLastCalledWith(
+      '/log/manage?search=checkout+timeout&traceId=trace-123&severityNumber=17&view=list&start=10&end=20&entityId=7&entityName=Checkout+API&returnTo=%2Foverview&serviceName=checkout&environment=prod'
+    );
+  }, 20000);
 });
diff --git a/web-next/app/events/page.tsx b/web-next/app/events/page.tsx
index 32e4dffd9a..aa06b973bf 100644
--- a/web-next/app/events/page.tsx
+++ b/web-next/app/events/page.tsx
@@ -1,6 +1,11 @@
-import React from 'react';
-import LogManagePage from '../log/manage/log-manage-page';
+import { redirect } from 'next/navigation';
+import { buildLogCompatRouteUrlFromSearchParams, type SearchParamsRecord } from '../../lib/log-manage/query-state';
 
-export default function EventsAliasPage() {
-  return <LogManagePage forcedView="explorer" showViewToggle={false} />;
+export default async function EventsAliasPage({
+  searchParams
+}: {
+  searchParams?: Promise<SearchParamsRecord>;
+}) {
+  const resolvedSearchParams = await searchParams;
+  redirect(buildLogCompatRouteUrlFromSearchParams(resolvedSearchParams, { view: 'list' }));
 }
diff --git a/web-next/app/exception/[type]/page.test.tsx b/web-next/app/exception/[type]/page.test.tsx
index 7d8a7063a1..4b705e8a4a 100644
--- a/web-next/app/exception/[type]/page.test.tsx
+++ b/web-next/app/exception/[type]/page.test.tsx
@@ -22,4 +22,12 @@ describe('ExceptionPage', () => {
     expect(html).toContain(`data-exception-type="${type}"`);
     expect(mockSurfaceProps.type).toBe(type);
   });
+
+  it('normalizes unsupported exception route params to the 404 surface type', async () => {
+    const html = renderToStaticMarkup(await ExceptionPage({ params: Promise.resolve({ type: '999' }) }));
+
+    expect(html).toContain('data-exception-center-surface="hertzbeat-exceptions"');
+    expect(html).toContain('data-exception-type="404"');
+    expect(mockSurfaceProps.type).toBe('404');
+  });
 });
diff --git a/web-next/app/exception/[type]/page.tsx b/web-next/app/exception/[type]/page.tsx
index 626eca2044..403f6a1a81 100644
--- a/web-next/app/exception/[type]/page.tsx
+++ b/web-next/app/exception/[type]/page.tsx
@@ -1,8 +1,10 @@
 import React from 'react';
 import { ExceptionCenterSurface } from '../../../components/pages/exception-center-surface';
+import { normalizeExceptionRouteType } from '../../../lib/exception-center/view-model';
 
 export default async function ExceptionPage({ params }: { params: Promise<{ type: string }> }) {
   const { type } = await params;
+  const normalizedType = normalizeExceptionRouteType(type);
 
-  return <ExceptionCenterSurface type={type} />;
+  return <ExceptionCenterSurface type={normalizedType} />;
 }
diff --git a/web-next/app/explorer/explorer-page.tsx b/web-next/app/explorer/explorer-page.tsx
new file mode 100644
index 0000000000..b380e15a2e
--- /dev/null
+++ b/web-next/app/explorer/explorer-page.tsx
@@ -0,0 +1,297 @@
+'use client';
+
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { BellPlus, BarChart3, LayoutDashboard, Play, Save } from 'lucide-react';
+import {
+  HzButton,
+  HzButtonLink,
+  HzDataCellText,
+  HzDataMetaText,
+  HzDataTable,
+  HzExplorerFrame,
+  HzQueryBar,
+  HzSelect,
+  type HzDataColumn
+} from '@hertzbeat/ui';
+import { ClientWorkbench } from '../../components/workbench/client-workbench';
+import { useI18n } from '../../components/providers/i18n-provider';
+import {
+  buildExplorerRouteUrl,
+  loadExplorerReadData,
+  readExplorerQueryState,
+  type ExplorerQueryState,
+  type ExplorerReadData,
+  type ExplorerSignalFilter
+} from '../../lib/explorer-surface/controller';
+import { buildExplorerFilters, type ExplorerResultRow, type ExplorerSignalTone } from '../../lib/explorer-surface/view-model';
+
+type Translator = ReturnType<typeof useI18n>['t'];
+
+function signalToneClass(signalTone: ExplorerSignalTone) {
+  if (signalTone === 'trace') return 'border-[#7a3f55] bg-[#241119] text-[#f18aa6]';
+  if (signalTone === 'log') return 'border-[#3a5674] bg-[#101b29] text-[#9bc5ee]';
+  if (signalTone === 'metric') return 'border-[#315b49] bg-[#0f211b] text-[#8bd8ad]';
+  return 'border-[#303642] bg-[#151821] text-[#d7dce6]';
+}
+
+export default function ExplorerPage() {
+  const { t } = useI18n();
+  const searchParams = useSearchParams();
+  const queryState = useMemo(() => readExplorerQueryState(searchParams), [searchParams]);
+  const load = useCallback(() => loadExplorerReadData(t, undefined, queryState), [queryState, t]);
+  const cacheKey = `explorer:trace-log-read:${queryState.signal}:${queryState.q}`;
+
+  return (
+    <ClientWorkbench load={load} loadingCopy={t('common.workbench.loading.copy')} cacheKey={cacheKey}>
+      {data => <ExplorerWorksurface data={data} t={t} />}
+    </ClientWorkbench>
+  );
+}
+
+function ExplorerWorksurface({ data, t }: { data: ExplorerReadData; t: Translator }) {
+  const router = useRouter();
+  const filters = buildExplorerFilters(t);
+  const rows = data.rows;
+  const activeRow = rows[0] || null;
+  const [draft, setDraft] = useState<ExplorerQueryState>(data.query);
+  useEffect(() => {
+    setDraft(data.query);
+  }, [data.query]);
+  const applyQuery = useCallback(() => {
+    router.replace(buildExplorerRouteUrl(draft));
+  }, [draft, router]);
+  const columns: HzDataColumn<ExplorerResultRow>[] = [
+    {
+      key: 'signal',
+      header: t('explorer.table.signal-type'),
+      width: '112px',
+      render: row => (
+        <span
+          className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold ${signalToneClass(row.signalTone)}`}
+          data-explorer-signal-tone={row.signalTone}
+        >
+          {row.signal}
+        </span>
+      )
+    },
+    {
+      key: 'service',
+      header: t('explorer.table.service'),
+      width: '180px',
+      render: row => <HzDataCellText>{row.service}</HzDataCellText>
+    },
+    {
+      key: 'operation',
+      header: t('explorer.table.operation'),
+      render: row => (
+        <a href={row.href} className="font-medium text-[#9fb4e7] hover:text-[#d7e2ff]">
+          {row.operation}
+        </a>
+      )
+    },
+    {
+      key: 'status',
+      header: t('explorer.table.status'),
+      width: '96px',
+      render: row => <HzDataCellText>{row.status}</HzDataCellText>
+    },
+    {
+      key: 'duration',
+      header: t('explorer.table.duration'),
+      width: '96px',
+      render: row => <HzDataMetaText casing="plain">{row.duration}</HzDataMetaText>
+    },
+    {
+      key: 'time',
+      header: t('explorer.table.time'),
+      width: '176px',
+      render: row => <HzDataMetaText casing="plain">{row.timestamp}</HzDataMetaText>
+    }
+  ];
+
+  return (
+    <main
+      data-explorer-route="otlp-cold-workbench"
+      data-explorer-style-baseline="hertzbeat-cold-matte"
+      data-explorer-api-owner={data.apiOwner}
+      data-explorer-api-state={data.apiState}
+      data-explorer-api-total={rows.length}
+      data-explorer-api-trace-total={data.traceTotal}
+      data-explorer-api-log-total={data.logTotal}
+      data-explorer-api-metric-total={data.metricTotal}
+      data-explorer-query-state={data.query.q || 'none'}
+      data-explorer-signal-filter={data.query.signal}
+      className="min-h-[calc(100vh-56px)] bg-[#07090b] text-[#e8edf5]"
+    >
+      <HzExplorerFrame
+        data-explorer-shared-frame="hertzbeat-ui"
+        className="mx-0 mb-0 mt-0 min-h-[calc(100vh-56px)] sm:mx-0"
+        eyebrow={t('explorer.kicker')}
+        title={t('explorer.title')}
+        description={t('explorer.subtitle')}
+        mainId="explorer-shared-main"
+        mainLabel={t('explorer.title')}
+        filterRailLabel={t('explorer.filters.title')}
+        actions={
+          <>
+            <HzButtonLink href="/explorer?view=saved" intent="secondary" data-explorer-action="save-view">
+              <Save size={14} />
+              {t('explorer.actions.save-view')}
+            </HzButtonLink>
+            <HzButtonLink href="/alert/setting?source=explorer" intent="secondary" data-explorer-action="create-alert">
+              <BellPlus size={14} />
+              {t('explorer.actions.create-alert')}
+            </HzButtonLink>
+            <HzButtonLink href="/dashboard?source=explorer" intent="secondary" data-explorer-action="add-dashboard">
+              <LayoutDashboard size={14} />
+              {t('explorer.actions.add-dashboard')}
+            </HzButtonLink>
+          </>
+        }
+        queryBar={
+          <div data-explorer-query-bar="cold-query-row">
+            <HzQueryBar
+              query={draft.q}
+              onQueryChange={q => setDraft(current => ({ ...current, q }))}
+              inputProps={{ 'aria-label': t('explorer.query.aria'), placeholder: t('explorer.query.placeholder'), 'data-explorer-query-input': 'url-owned' }}
+              actions={
+                <>
+                  <HzSelect
+                    aria-label={t('explorer.signal.aria')}
+                    className="w-[136px]"
+                    value={draft.signal}
+                    onChange={event => setDraft(current => ({ ...current, signal: event.target.value as ExplorerSignalFilter }))}
+                    data-explorer-signal-select="url-owned"
+                    options={[
+                      { value: 'all', label: t('explorer.signal.all') },
+                      { value: 'trace', label: t('explorer.rows.trace.signal') },
+                      { value: 'log', label: t('explorer.rows.log.signal') },
+                      { value: 'metric', label: t('explorer.rows.metric.signal') }
+                    ]}
+                  />
+                  <HzSelect
+                    aria-label={t('explorer.sort.aria')}
+                    className="w-[148px]"
+                    defaultValue="time-desc"
+                    options={[
+                      { value: 'time-desc', label: t('explorer.sort.time-desc') },
+                      { value: 'duration-desc', label: t('explorer.sort.duration-desc') }
+                    ]}
+                  />
+                  <HzButton
+                    type="button"
+                    intent="primary"
+                    onClick={applyQuery}
+                    data-explorer-query-action="run"
+                    data-explorer-query-url={buildExplorerRouteUrl(draft)}
+                  >
+                    <Play size={14} />
+                    {t('explorer.query.run')}
+                  </HzButton>
+                </>
+              }
+            />
+          </div>
+        }
+        filterRail={
+          <aside data-explorer-filter-rail="cold-static-rail" className="bg-[var(--hz-ui-surface)] px-3 py-3">
+            <div className="mb-4 flex items-center justify-between">
+              <div className="text-[13px] font-semibold text-[#e4ebf5]">{t('explorer.filters.title')}</div>
+              <button type="button" className="text-[12px] font-semibold text-[#9aa6b8] hover:text-[#dbe4f0]">
+                {t('explorer.filters.clear')}
+              </button>
+            </div>
+            <div className="space-y-4">
+              {filters.map(filter => (
+                <section key={filter.title} className="space-y-2">
+                  <div className="text-[12px] font-semibold text-[#8d98aa]">{filter.title}</div>
+                  <div className="space-y-1">
+                    {filter.values.map(value => (
+                      <label key={value} className="flex h-7 items-center gap-2 rounded-[3px] px-1 text-[12px] text-[#c7d0df] hover:bg-[#151a22]">
+                        <span className="grid h-4 w-4 place-items-center rounded-[3px] border border-[#4d65c8] bg-[#17213a] text-[10px] font-semibold text-[#b9c8ff]">
+                          ✓
+                        </span>
+                        <span className="min-w-0 truncate">{value}</span>
+                      </label>
+                    ))}
+                  </div>
+                </section>
+              ))}
+            </div>
+          </aside>
+        }
+      >
+        <section data-explorer-chart-band="cold-chart-band" className="border-b border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] px-4 py-4">
+          <div className="mb-3 flex items-center justify-between">
+            <div>
+              <p className="text-[12px] font-semibold text-[#8e99ab]">{t('explorer.chart.title')}</p>
+              <p className="mt-1 text-[13px] text-[#d5dde9]" data-explorer-api-source={`${data.sourceUrls.traces}|${data.sourceUrls.logs}|${data.sourceUrls.metrics}`}>
+                {t('explorer.chart.subtitle')}
+              </p>
+            </div>
+            <div className="flex items-center gap-2 text-[12px] text-[#8d98aa]">
+              <BarChart3 className="h-4 w-4" aria-hidden="true" />
+              {t('explorer.chart.result-count', { count: rows.length })}
+            </div>
+          </div>
+          <div className="flex h-[86px] items-end gap-2 border-t border-[#232a34] pt-3">
+            {[28, 36, 22, 54, 45, 68, 42, 58, 33, 74, 49, 62].map((height, index) => (
+              <div
+                key={`${height}-${index}`}
+                className="min-w-0 flex-1 rounded-[3px] border border-[#2f3b4d] bg-[#182232]"
+                style={{ height: `${height}px` }}
+              />
+            ))}
+          </div>
+        </section>
+
+        <div className="grid min-h-[420px] gap-0 2xl:grid-cols-[minmax(0,1fr)_320px]">
+          <section data-explorer-result-table="cold-dense-table" className="min-w-0 overflow-hidden border-r border-[var(--hz-ui-line-soft)]">
+            <div className="flex h-11 items-center justify-between border-b border-[var(--hz-ui-line-soft)] px-4 text-[12px] text-[#8e99ab]">
+              <span>{t('explorer.results.title')}</span>
+              <span>{t('explorer.results.count', { count: rows.length })}</span>
+            </div>
+            <HzDataTable
+              columns={columns}
+              rows={rows}
+              getRowKey={row => row.key}
+              variant="embedded"
+              data-explorer-result-table-owner="hertzbeat-ui-data-table"
+            />
+            {rows.length === 0 ? (
+              <div data-explorer-api-empty-state="trace-log-bff-query-api" className="border-t border-[var(--hz-ui-line-soft)] px-4 py-8 text-[13px] text-[#8f9bad]">
+                {t('common.no-data')}
+              </div>
+            ) : null}
+          </section>
+
+          <aside data-explorer-detail-panel="cold-detail-panel" className="h-fit bg-[var(--hz-ui-surface)] px-4 py-4">
+            <p className="text-[12px] font-semibold text-[#8d98aa]">{t('explorer.detail.title')}</p>
+            <h2 className="mt-2 text-[18px] font-semibold text-[#f0f4fa]">{activeRow?.service || t('common.no-data')}</h2>
+            <div className="mt-4 space-y-3 text-[12px] text-[#9aa6b8]">
+              <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
+                <span>{t('explorer.detail.signal')}</span>
+                <span className="font-semibold text-[#dbe5f3]">{activeRow?.signal || '-'}</span>
+              </div>
+              <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
+                <span>{t('explorer.detail.status')}</span>
+                <span className="font-semibold text-[#dbe5f3]">{activeRow?.status || '-'}</span>
+              </div>
+              <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
+                <span>{t('explorer.detail.duration')}</span>
+                <span className="font-semibold text-[#dbe5f3]">{activeRow?.duration || '-'}</span>
+              </div>
+            </div>
+            <div className="mt-4 flex flex-col gap-2">
+              <HzButtonLink href="/trace/manage" intent="secondary">{t('explorer.handoff.trace')}</HzButtonLink>
+              <HzButtonLink href="/log/manage" intent="secondary">{t('explorer.handoff.log')}</HzButtonLink>
+              <HzButtonLink href="/ingestion/otlp/metrics" intent="secondary">{t('explorer.handoff.metrics')}</HzButtonLink>
+              <HzButtonLink href="/entities" intent="secondary">{t('explorer.handoff.entities')}</HzButtonLink>
+            </div>
+          </aside>
+        </div>
+      </HzExplorerFrame>
+    </main>
+  );
+}
diff --git a/web-next/app/explorer/page.test.tsx b/web-next/app/explorer/page.test.tsx
index 2c5c0f8f84..e60233d84c 100644
--- a/web-next/app/explorer/page.test.tsx
+++ b/web-next/app/explorer/page.test.tsx
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 vi.mock('next/link', () => ({
   default: ({ href, children, ...props }: any) => (
@@ -12,6 +13,67 @@ vi.mock('next/link', () => ({
   )
 }));
 
+const replaceMock = vi.fn();
+
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    replace: replaceMock
+  }),
+  useSearchParams: () => new URLSearchParams('q=checkout&signal=trace')
+}));
+
+vi.mock('../../components/providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t: createTranslatorMock({ locale: 'zh-CN' })
+  })
+}));
+
+vi.mock('../../components/workbench/client-workbench', () => ({
+  ClientWorkbench: ({ children }: { children: (data: any) => React.ReactNode }) =>
+    children({
+      rows: [
+        {
+          key: 'trace:trace-1',
+          signalKey: 'trace',
+          signalTone: 'trace',
+          href: '/trace/manage?traceId=trace-1',
+          signal: '链路',
+          service: 'checkout',
+          operation: 'POST /checkout',
+          status: '错误',
+          duration: '1.25s',
+          timestamp: '2026/03/30 11:50:57'
+        },
+        {
+          key: 'log:1:trace-1:none:0',
+          signalKey: 'log',
+          signalTone: 'log',
+          href: '/log/manage?traceId=trace-1',
+          signal: '日志',
+          service: 'payment',
+          operation: 'payment failed',
+          status: 'ERROR',
+          duration: '-',
+          timestamp: '2026/03/30 11:50:58'
+        }
+      ],
+      apiState: 'ready',
+      apiOwner: 'trace-log-bff-query-api',
+      query: {
+        q: 'checkout',
+        signal: 'trace'
+      },
+      sourceUrls: {
+        traces: '/traces/list?pageIndex=0&pageSize=8&serviceName=checkout',
+        logs: null,
+        metrics: null
+      },
+      traceTotal: 1,
+      logTotal: 1,
+      metricTotal: 0
+    })
+}));
+
 describe('explorer page', () => {
   it('renders the OTLP cold Workbench explorer baseline in Chinese', async () => {
     const { default: ExplorerPage } = await import('./page');
@@ -19,10 +81,25 @@ describe('explorer page', () => {
 
     expect(html).toContain('data-explorer-route="otlp-cold-workbench"');
     expect(html).toContain('data-explorer-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-explorer-api-owner="trace-log-bff-query-api"');
+    expect(html).toContain('data-explorer-api-state="ready"');
+    expect(html).toContain('data-explorer-api-total="2"');
+    expect(html).toContain('data-explorer-api-metric-total="0"');
+    expect(html).toContain('data-explorer-query-state="checkout"');
+    expect(html).toContain('data-explorer-signal-filter="trace"');
+    expect(html).toContain('data-explorer-api-source="/traces/list?pageIndex=0&amp;pageSize=8&amp;serviceName=checkout|null|null"');
+    expect(html).toContain('data-explorer-shared-frame="hertzbeat-ui"');
+    expect(html).toContain('data-hz-ui="explorer-frame"');
+    expect(html).toContain('data-hz-density="operator-compact"');
     expect(html).toContain('data-explorer-query-bar="cold-query-row"');
     expect(html).toContain('data-explorer-chart-band="cold-chart-band"');
     expect(html).toContain('data-explorer-result-table="cold-dense-table"');
     expect(html).toContain('data-explorer-detail-panel="cold-detail-panel"');
+    expect(html).toContain('data-explorer-result-table-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-explorer-signal-tone="trace"');
+    expect(html).toContain('data-explorer-signal-tone="log"');
+    expect(html).toContain('href="/trace/manage?traceId=trace-1"');
+    expect(html).toContain('href="/log/manage?traceId=trace-1"');
     expect(html).toContain('查询工作台');
     expect(html).toContain('运行查询');
     expect(html).toContain('信号类型');
@@ -32,6 +109,9 @@ describe('explorer page', () => {
     expect(html).toContain('保存视图');
     expect(html).toContain('创建告警');
     expect(html).toContain('加入仪表盘');
+    expect(html).toContain('data-explorer-query-input="url-owned"');
+    expect(html).toContain('data-explorer-signal-select="url-owned"');
+    expect(html).toContain('data-explorer-query-url="/explorer?q=checkout&amp;signal=trace"');
 
     expect(html).not.toContain('signoz-');
     expect(html).not.toContain('Explorer');
@@ -55,11 +135,21 @@ describe('explorer page', () => {
   });
 
   it('does not keep the old placeholder surface owner', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/explorer/page.tsx'), 'utf8');
+    const routeSource = readFileSync(resolve(process.cwd(), 'app/explorer/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/explorer/explorer-page.tsx'), 'utf8');
 
+    expect(routeSource).not.toMatch(/^['"]use client['"]/);
+    expect(routeSource).toContain("import ExplorerPage from './explorer-page'");
     expect(source).not.toContain('OpsSurfacePage');
     expect(source).not.toContain('buildExplorerSurfaceConfig');
+    expect(source).not.toContain('buildExplorerResultRows');
     expect(source).not.toContain('data-explorer-floating-actions');
     expect(source).toContain('data-explorer-route="otlp-cold-workbench"');
+    expect(source).toContain('readExplorerQueryState');
+    expect(source).toContain('buildExplorerRouteUrl');
+    expect(source).toContain('loadExplorerReadData');
+    expect(source).toContain('ClientWorkbench');
+    expect(source).toContain('HzExplorerFrame');
+    expect(source).toContain('HzDataTable');
   });
 });
diff --git a/web-next/app/explorer/page.tsx b/web-next/app/explorer/page.tsx
index cbcd064183..c052038d50 100644
--- a/web-next/app/explorer/page.tsx
+++ b/web-next/app/explorer/page.tsx
@@ -1,226 +1,6 @@
-'use client';
-
 import React from 'react';
-import Link from 'next/link';
-import { BarChart3, BellPlus, Gauge, LayoutDashboard, Play, Save, Search } from 'lucide-react';
-import { Input } from '../../components/ui/input';
-import { Select } from '../../components/ui/select';
-import { buildExplorerFilters, buildExplorerResultRows } from '../../lib/explorer-surface/view-model';
-
-function signalTone(signal: string) {
-  if (signal === '链路') return 'border-[#7a3f55] bg-[#241119] text-[#f18aa6]';
-  if (signal === '日志') return 'border-[#3a5674] bg-[#101b29] text-[#9bc5ee]';
-  if (signal === '指标') return 'border-[#315b49] bg-[#0f211b] text-[#8bd8ad]';
-  return 'border-[#303642] bg-[#151821] text-[#d7dce6]';
-}
-
-const actionClass =
-  'inline-flex h-8 items-center justify-center gap-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe3ee] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] transition-colors hover:border-[#3b4454] hover:bg-[#151821]';
-const primaryActionClass =
-  'inline-flex h-8 items-center justify-center gap-2 rounded-[3px] border border-[#31405c] bg-[#182238] px-4 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] transition-colors hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-white';
-const panelClass = 'rounded-[4px] border border-[#252b35] bg-[#0d1015] shadow-[0_18px_60px_rgba(0,0,0,0.28)]';
-const inputClass =
-  'h-8 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#e6edf7] outline-none transition-colors placeholder:text-[#697386] focus:border-[#4e74f8] focus:ring-2 focus:ring-[rgba(78,116,248,0.12)]';
-
-export default function ExplorerPage() {
-  const filters = buildExplorerFilters();
-  const rows = buildExplorerResultRows();
-  const activeRow = rows[0];
-
-  return (
-    <main
-      data-explorer-route="otlp-cold-workbench"
-      data-explorer-style-baseline="hertzbeat-cold-matte"
-      className="min-h-[calc(100vh-56px)] bg-[#07090b] text-[#e8edf5]"
-    >
-      <div className="mx-auto flex w-full max-w-[1600px] flex-col gap-4 px-6 py-6">
-        <section data-explorer-header="cold-compact-header" className={`${panelClass} px-5 py-4`}>
-          <div className="flex flex-wrap items-end justify-between gap-4">
-            <div className="min-w-0">
-              <p className="mb-2 text-[12px] font-semibold tracking-[0.08em] text-[#8792a5]">可观测</p>
-              <h1 className="text-[30px] font-semibold tracking-normal text-[#f4f7fb]">查询工作台</h1>
-              <p className="mt-3 max-w-[760px] text-[13px] leading-6 text-[#9ca7ba]">
-                统一检索链路、日志和指标结果,围绕查询栏、趋势带、表格和详情面板完成定位。
-              </p>
-            </div>
-            <div data-explorer-action-row="cold-workbench-actions" className="flex flex-wrap items-center gap-2">
-              <Link href="/explorer?view=saved" className={actionClass}>
-                <Save className="h-4 w-4" aria-hidden="true" />
-                保存视图
-              </Link>
-              <Link href="/alert/setting?source=explorer" className={actionClass}>
-                <BellPlus className="h-4 w-4" aria-hidden="true" />
-                创建告警
-              </Link>
-              <Link href="/dashboard?source=explorer" className={actionClass}>
-                <LayoutDashboard className="h-4 w-4" aria-hidden="true" />
-                加入仪表盘
-              </Link>
-            </div>
-          </div>
-        </section>
-
-        <section data-explorer-query-bar="cold-query-row" className={`${panelClass} px-4 py-3`}>
-          <div className="flex flex-wrap items-center gap-2">
-            <div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-[3px] border border-[#3f4450] bg-[#141923] text-[12px] font-semibold text-[#d5dfef]">
-              A
-            </div>
-            <div className="relative min-w-[360px] max-w-[680px] flex-1">
-              <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#7d8798]" aria-hidden="true" />
-              <Input
-                aria-label="查询条件"
-                className={`${inputClass} w-full pl-9 font-mono`}
-                placeholder="输入服务、状态或资源属性过滤条件"
-                defaultValue={'service.name = "checkout" AND status = "错误"'}
-              />
-            </div>
-            <Select aria-label="信号类型" containerClassName="w-[136px]" className="h-8 min-w-0 text-[#d5dce8]" defaultValue="all">
-              <option value="all">全部信号</option>
-              <option value="trace">链路</option>
-              <option value="log">日志</option>
-              <option value="metric">指标</option>
-            </Select>
-            <Select aria-label="排序方式" containerClassName="w-[148px]" className="h-8 min-w-0 text-[#d5dce8]" defaultValue="time-desc">
-              <option value="time-desc">时间倒序</option>
-              <option value="duration-desc">耗时倒序</option>
-            </Select>
-            <button type="button" className={primaryActionClass}>
-              <Play className="h-4 w-4" aria-hidden="true" />
-              运行查询
-            </button>
-          </div>
-        </section>
-
-        <div className="grid min-h-[620px] gap-4 xl:grid-cols-[232px_minmax(0,1fr)]">
-          <aside data-explorer-filter-rail="cold-static-rail" className={`${panelClass} h-fit px-4 py-4`}>
-            <div className="mb-4 flex items-center justify-between">
-              <div className="flex items-center gap-2 text-[13px] font-semibold text-[#e4ebf5]">
-                <Gauge className="h-4 w-4 text-[#8d97a8]" aria-hidden="true" />
-                快速筛选
-              </div>
-              <button type="button" className="text-[12px] font-semibold text-[#9aa6b8] hover:text-[#dbe4f0]">
-                清除
-              </button>
-            </div>
-            <div className="space-y-4">
-              {filters.map(filter => (
-                <section key={filter.title} className="space-y-2">
-                  <div className="text-[12px] font-semibold text-[#8d98aa]">{filter.title}</div>
-                  <div className="space-y-1">
-                    {filter.values.map(value => (
-                      <label key={value} className="flex h-7 items-center gap-2 rounded-[3px] px-1 text-[12px] text-[#c7d0df] hover:bg-[#151a22]">
-                        <span className="grid h-4 w-4 place-items-center rounded-[3px] border border-[#4d65c8] bg-[#17213a] text-[10px] font-semibold text-[#b9c8ff]">
-                          ✓
-                        </span>
-                        <span className="min-w-0 truncate">{value}</span>
-                      </label>
-                    ))}
-                  </div>
-                </section>
-              ))}
-            </div>
-          </aside>
-
-          <section className="min-w-0 space-y-4">
-            <section data-explorer-chart-band="cold-chart-band" className={`${panelClass} px-4 py-4`}>
-              <div className="mb-3 flex items-center justify-between">
-                <div>
-                  <p className="text-[12px] font-semibold text-[#8e99ab]">趋势带</p>
-                  <p className="mt-1 text-[13px] text-[#d5dde9]">近 30 分钟跨信号命中量</p>
-                </div>
-                <div className="flex items-center gap-2 text-[12px] text-[#8d98aa]">
-                  <BarChart3 className="h-4 w-4" aria-hidden="true" />
-                  结果 {rows.length}
-                </div>
-              </div>
-              <div className="flex h-[86px] items-end gap-2 border-t border-[#232a34] pt-3">
-                {[28, 36, 22, 54, 45, 68, 42, 58, 33, 74, 49, 62].map((height, index) => (
-                  <div
-                    key={`${height}-${index}`}
-                    className="min-w-0 flex-1 rounded-[3px] border border-[#2f3b4d] bg-[#182232]"
-                    style={{ height: `${height}px` }}
-                  />
-                ))}
-              </div>
-            </section>
-
-            <div className="grid gap-4 2xl:grid-cols-[minmax(0,1fr)_320px]">
-              <section data-explorer-result-table="cold-dense-table" className={`${panelClass} min-w-0 overflow-hidden`}>
-                <div className="flex h-11 items-center justify-between border-b border-[#252b35] px-4 text-[12px] text-[#8e99ab]">
-                  <span>跨信号结果</span>
-                  <span>{rows.length} 条</span>
-                </div>
-                <table className="w-full border-collapse text-left text-[13px]">
-                  <thead className="border-b border-[#252b35] bg-[#10141b] text-[12px] font-semibold text-[#8f9aab]">
-                    <tr>
-                      <th className="w-[112px] px-4 py-3">信号类型</th>
-                      <th className="w-[180px] px-4 py-3">服务</th>
-                      <th className="px-4 py-3">操作</th>
-                      <th className="w-[96px] px-4 py-3">状态</th>
-                      <th className="w-[96px] px-4 py-3">耗时</th>
-                      <th className="w-[176px] px-4 py-3">时间</th>
-                    </tr>
-                  </thead>
-                  <tbody>
-                    {rows.map(row => (
-                      <tr key={row.key} className="border-b border-[#232a34] last:border-b-0 hover:bg-[#10141b]">
-                        <td className="px-4 py-3">
-                          <span className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold ${signalTone(row.signal)}`}>
-                            {row.signal}
-                          </span>
-                        </td>
-                        <td className="px-4 py-3 font-semibold text-[#e6edf7]">{row.service}</td>
-                        <td className="px-4 py-3">
-                          <Link
-                            href={row.signal === '链路' ? '/trace/manage?serviceName=checkout' : row.signal === '日志' ? '/log/manage?search=payment' : '/ingestion/otlp/metrics?serviceName=frontend'}
-                            className="font-medium text-[#9fb4e7] hover:text-[#d7e2ff]"
-                          >
-                            {row.operation}
-                          </Link>
-                        </td>
-                        <td className="px-4 py-3 font-semibold text-[#e6edf7]">{row.status}</td>
-                        <td className="px-4 py-3 text-[#cbd5e1]">{row.duration}</td>
-                        <td className="px-4 py-3 text-[#cbd5e1]">{row.timestamp}</td>
-                      </tr>
-                    ))}
-                  </tbody>
-                </table>
-              </section>
+import ExplorerPage from './explorer-page';
 
-              <aside data-explorer-detail-panel="cold-detail-panel" className={`${panelClass} h-fit px-4 py-4`}>
-                <p className="text-[12px] font-semibold text-[#8d98aa]">详情面板</p>
-                <h2 className="mt-2 text-[18px] font-semibold text-[#f0f4fa]">{activeRow.service}</h2>
-                <div className="mt-4 space-y-3 text-[12px] text-[#9aa6b8]">
-                  <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
-                    <span>信号</span>
-                    <span className="font-semibold text-[#dbe5f3]">{activeRow.signal}</span>
-                  </div>
-                  <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
-                    <span>状态</span>
-                    <span className="font-semibold text-[#dbe5f3]">{activeRow.status}</span>
-                  </div>
-                  <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
-                    <span>耗时</span>
-                    <span className="font-semibold text-[#dbe5f3]">{activeRow.duration}</span>
-                  </div>
-                </div>
-                <div className="mt-4 flex flex-col gap-2">
-                  <Link href="/trace/manage" className={actionClass}>
-                    查看链路
-                  </Link>
-                  <Link href="/log/manage" className={actionClass}>
-                    查看日志
-                  </Link>
-                  <Link href="/ingestion/otlp/metrics" className={actionClass}>
-                    查看指标
-                  </Link>
-                  <Link href="/entities" className={actionClass}>对象目录</Link>
-                </div>
-              </aside>
-            </div>
-          </section>
-        </div>
-      </div>
-    </main>
-  );
+export default function ExplorerRoutePage() {
+  return <ExplorerPage />;
 }
diff --git a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
new file mode 100644
index 0000000000..b60b7a5b2e
--- /dev/null
+++ b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
@@ -0,0 +1,1166 @@
+'use client';
+
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import Link from 'next/link';
+import { BarChart3, Play, RotateCcw, Search, Workflow } from 'lucide-react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { HzActionGroup, HzAssistiveMarker, HzAttributeDiagnostics, HzButton, HzButtonIcon, HzButtonLink, HzChipGroup, HzCollapsibleSection, HzContextHandoff, HzControlStack, HzDataCellStack, HzDataCellText, HzDataMetaText, HzDataTable, HzDetailRows, HzDisabledActionShell, HzEmptyState, HzInput, HzPanelHeader, HzPanelSection, HzPanelSurface, HzPanelTitleLabel, HzQueryActionGroup, HzSearchFieldFrame, HzSearchFieldIcon, HzSelect, HzSignalWorkbenchShell, HzStateNotice, HzStatCell, HzStatStri [...]
+import { EChartsPanel, type EChartsDataZoomRange } from '@/components/observability/echarts-panel';
+import { buildTimeRangeControlLabels, buildTimeRangePresetLabels, formatEpochMillisDraft, TimeRangeControl } from '@/components/observability/time-range-control';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { apiMessageGet } from '@/lib/api-client';
+import { formatTime } from '@/lib/format';
+import { buildOtlpMetricsConsoleUrl, loadOtlpMetricsConsole, queryStateFromParams, type OtlpMetricsQueryState } from '@/lib/otlp-metrics/controller';
+import { buildSignalEntityContextRows, readEntityIdRouteParam, readEpochMillisRouteParam, readSignalRouteContext, type SignalRouteContext } from '@/lib/signal-route-context';
+import { resolveTimeContextBounds, sanitizeTimeContext, TIME_CONTEXT_PRESETS, type TimeContext } from '@/lib/time-context';
+import {
+  buildConsoleFacts,
+  buildConsoleMetrics,
+  buildContextRows,
+  buildMetricSeriesAttributionDiagnostics,
+  buildMetricSeriesContextRows,
+  buildMetricSeriesEvidenceRows,
+  buildMetricSeriesLinkedRecordRows,
+  buildMetricSeriesRows,
+  buildMetricSeriesViews,
+  buildMetricsChartOption,
+  buildMetricsDataZoomTimeContext,
+  buildMetricTrendBars,
+  buildMetricsExplorerState,
+  buildMetricsHandoffLinks,
+  type OtlpMetricSeriesView
+} from '@/lib/otlp-metrics/view-model';
+import type { OtlpMetricsConsole } from '@/lib/types';
+import { buildOtlpMetricsRoute, hasMetricsDisplayReturnLabel } from './route-state';
+
+function latestSeriesTimestamp(data: OtlpMetricsConsole) {
+  const timestamps = (data.results?.frames || [])
+    .flatMap(frame => frame.data || [])
+    .map(row => Number(row?.[0]))
+    .filter(Number.isFinite);
+  return timestamps.length ? Math.max(...timestamps) : data.stats?.latestObservedAt ?? null;
+}
+
+function routeEpochMillisValue(value?: string) {
+  const epochMillis = readEpochMillisRouteParam(value);
+  return epochMillis ? Number(epochMillis) : undefined;
+}
+
+function routeEntityIdValue(value?: string) {
+  const entityId = readEntityIdRouteParam(value);
+  return entityId ? Number(entityId) : undefined;
+}
+
+function firstRouteText(...values: Array<string | null | undefined>) {
+  return values.find((value): value is string => value != null && value.trim() !== '')?.trim();
+}
+
+function readMetricSeriesLabel(series: OtlpMetricSeriesView, ...keys: string[]) {
+  return firstRouteText(...keys.map(key => series.labels[key]));
+}
+
+function findMetricSeriesForRouteTrace(
+  seriesList: OtlpMetricSeriesView[],
+  query: OtlpMetricsQueryState,
+  routeContext: SignalRouteContext
+) {
+  const traceId = firstRouteText(query.traceId, routeContext.traceId);
+  const spanId = firstRouteText(query.spanId, routeContext.spanId);
+  if (!traceId) return null;
+  return seriesList.find(series => {
+    const seriesTraceId = readMetricSeriesLabel(series, 'traceId', 'trace_id', 'trace.id', 'trace_id_hex');
+    const seriesSpanId = readMetricSeriesLabel(series, 'spanId', 'span_id', 'span.id', 'span_id_hex');
+    if (seriesTraceId !== traceId) return false;
+    return spanId ? seriesSpanId === spanId : true;
+  }) || null;
+}
+
+export default function OtlpMetricsPage() {
+  const { t } = useI18n();
+  const router = useRouter();
+  const searchParams = useSearchParams();
+  const metricsTimeRangeLabels = useMemo(() => buildTimeRangePresetLabels(t), [t]);
+  const metricsTimeRangePresets = useMemo(
+    () => TIME_CONTEXT_PRESETS.map(preset => ({ value: preset.value, label: metricsTimeRangeLabels[preset.value] || preset.value })),
+    [metricsTimeRangeLabels]
+  );
+  const query = useMemo(() => queryStateFromParams(searchParams), [searchParams]);
+  const routeContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
+  const workbenchCacheKey = useMemo(() => buildOtlpMetricsConsoleUrl(query), [query]);
+  const load = useCallback(async (): Promise<OtlpMetricsConsole> => loadOtlpMetricsConsole(apiMessageGet, query), [query]);
+  const initialDraft = useMemo(() => ({
+    query: query.query || '',
+    aggregation: query.aggregation || 'avg',
+    groupBy: query.groupBy || 'service_name',
+    timeRange: query.timeRange || 'last-30m',
+    from: query.from || '',
+    to: query.to || '',
+    start: query.start || '',
+    end: query.end || '',
+    refresh: query.refresh || '',
+    live: query.live || '',
+    tz: query.tz || '',
+    timezone: query.timezone || '',
+    serviceName: query.serviceName || '',
+    serviceNamespace: query.serviceNamespace || '',
+    environment: query.environment || '',
+    traceId: query.traceId || ''
+  }), [
+    query.aggregation,
+    query.environment,
+    query.groupBy,
+    query.query,
+    query.end,
+    query.from,
+    query.live,
+    query.refresh,
+    query.serviceName,
+    query.serviceNamespace,
+    query.start,
+    query.timeRange,
+    query.timezone,
+    query.to,
+    query.traceId,
+    query.tz
+  ]);
+  const [draft, setDraft] = useState(initialDraft);
+  const [selectedSeriesKey, setSelectedSeriesKey] = useState<string | null>(null);
+  const [metricsChartZoomRange, setMetricsChartZoomRange] = useState<EChartsDataZoomRange | null>(null);
+
+  useEffect(() => {
+    if (hasMetricsDisplayReturnLabel(searchParams)) {
+      router.replace(buildOtlpMetricsRoute(query));
+    }
+  }, [query, router, searchParams]);
+
+  useEffect(() => {
+    setDraft(initialDraft);
+  }, [initialDraft]);
+
+  const updateDraftField = useCallback((field: keyof typeof draft, value: string) => {
+    setDraft(previous => ({ ...previous, [field]: value }));
+  }, []);
+
+  const handleMetricsChartZoomChange = useCallback((nextZoom: EChartsDataZoomRange) => {
+    setMetricsChartZoomRange(previous => {
+      if (
+        previous?.start === nextZoom.start &&
+        previous?.end === nextZoom.end &&
+        previous?.startValue === nextZoom.startValue &&
+        previous?.endValue === nextZoom.endValue
+      ) {
+        return previous;
+      }
+      return nextZoom;
+    });
+  }, []);
+
+  const draftTimeContext = useMemo(() => sanitizeTimeContext({
+    timeRange: draft.timeRange || query.timeRange || 'last-30m',
+    from: draft.from || query.from,
+    to: draft.to || query.to,
+    start: draft.start || query.start,
+    end: draft.end || query.end,
+    refresh: draft.refresh || query.refresh,
+    live: draft.live || query.live,
+    tz: draft.tz || query.tz,
+    timezone: draft.timezone || query.timezone
+  }), [
+    draft.end,
+    draft.from,
+    draft.live,
+    draft.refresh,
+    draft.start,
+    draft.timeRange,
+    draft.timezone,
+    draft.to,
+    draft.tz,
+    query.end,
+    query.from,
+    query.live,
+    query.refresh,
+    query.start,
+    query.timeRange,
+    query.timezone,
+    query.to,
+    query.tz
+  ]);
+
+  const applyMetricsQuery = useCallback((nextTimeContext?: TimeContext) => {
+    const timeContext = sanitizeTimeContext({
+      timeRange: nextTimeContext?.timeRange || draft.timeRange || query.timeRange || 'last-30m',
+      from: nextTimeContext?.from || draft.from || query.from,
+      to: nextTimeContext?.to || draft.to || query.to,
+      start: nextTimeContext?.start || draft.start || query.start,
+      end: nextTimeContext?.end || draft.end || query.end,
+      refresh: nextTimeContext?.refresh || draft.refresh || query.refresh,
+      live: nextTimeContext?.live || draft.live || query.live,
+      tz: nextTimeContext?.tz || draft.tz || query.tz,
+      timezone: nextTimeContext?.timezone || draft.timezone || query.timezone
+    });
+    const bounds = resolveTimeContextBounds(timeContext);
+    const hasExpressionDraft = Boolean(timeContext.from && timeContext.to);
+    const hasAbsoluteDraft = Boolean(timeContext.start && timeContext.end);
+    router.replace(buildOtlpMetricsRoute({
+      ...query,
+      query: draft.query.trim() || undefined,
+      aggregation: draft.aggregation,
+      groupBy: draft.groupBy,
+      timeRange: timeContext.timeRange || 'last-30m',
+      from: hasExpressionDraft ? timeContext.from : undefined,
+      to: hasExpressionDraft ? timeContext.to : undefined,
+      serviceName: draft.serviceName.trim() || undefined,
+      serviceNamespace: draft.serviceNamespace.trim() || undefined,
+      environment: draft.environment.trim() || undefined,
+      traceId: draft.traceId.trim() || undefined,
+      start: hasExpressionDraft ? undefined : bounds?.start || (!hasAbsoluteDraft ? query.start : undefined),
+      end: hasExpressionDraft ? undefined : bounds?.end || (!hasAbsoluteDraft ? query.end : undefined),
+      refresh: timeContext.refresh,
+      live: timeContext.live,
+      tz: hasExpressionDraft ? undefined : timeContext.tz,
+      timezone: hasExpressionDraft ? timeContext.timezone || timeContext.tz : timeContext.timezone
+    }));
+  }, [draft, query, router]);
+
+  const applyMetricsTimeContext = useCallback((timeContext: TimeContext) => {
+    const sanitized = sanitizeTimeContext(timeContext);
+    setDraft(previous => ({
+      ...previous,
+      timeRange: sanitized.timeRange || previous.timeRange || 'last-30m',
+      from: sanitized.from || '',
+      to: sanitized.to || '',
+      start: sanitized.start || '',
+      end: sanitized.end || '',
+      refresh: sanitized.refresh || '',
+      live: sanitized.live || '',
+      tz: sanitized.tz || '',
+      timezone: sanitized.timezone || ''
+    }));
+    applyMetricsQuery(sanitized);
+  }, [applyMetricsQuery]);
+
+  return (
+    <ClientWorkbench load={load} loadingCopy={t('otlp.metrics.loading')} cacheKey={workbenchCacheKey}>
+      {data => {
+        const queryEntityId = routeEntityIdValue(query.entityId);
+        const queryEntityIdText = readEntityIdRouteParam(query.entityId);
+        const queryStart = routeEpochMillisValue(query.start);
+        const queryEnd = routeEpochMillisValue(query.end);
+        const queryStartText = readEpochMillisRouteParam(query.start);
+        const queryEndText = readEpochMillisRouteParam(query.end);
+        const mergedData: OtlpMetricsConsole = {
+          ...data,
+          context: {
+            ...data.context,
+            entityId: data.context?.entityId ?? queryEntityId,
+            entityName: data.context?.entityName || query.entityName,
+            serviceName: data.context?.serviceName || query.serviceName,
+            serviceNamespace: data.context?.serviceNamespace || query.serviceNamespace,
+            environment: data.context?.environment || query.environment,
+            start: data.context?.start ?? queryStart,
+            end: data.context?.end ?? queryEnd
+          }
+        };
+        const workbenchState = buildMetricsExplorerState(mergedData, t);
+        const metricSeries = buildMetricSeriesViews(mergedData, t);
+        const hasMetricSeries = metricSeries.length > 0;
+        const seriesRows = buildMetricSeriesRows(metricSeries, t);
+        const selectedMetricSeries =
+          metricSeries.find(series => series.key === selectedSeriesKey) ||
+          findMetricSeriesForRouteTrace(metricSeries, query, routeContext) ||
+          metricSeries[0] ||
+          null;
+        const selectedMetricSeriesIndex = selectedMetricSeries ? metricSeries.findIndex(series => series.key === selectedMetricSeries.key) : -1;
+        const selectedSeriesContextRows = buildMetricSeriesContextRows(selectedMetricSeries, t);
+        const selectedSeriesEvidenceRows = buildMetricSeriesEvidenceRows(selectedMetricSeries, formatTime, t);
+        const selectedSeriesContextDetailRows = selectedSeriesContextRows.map(row => ({
+          key: row.label,
+          title: row.label,
+          copy: row.value,
+          meta: row.meta
+        }));
+        const selectedSeriesEvidenceDetailRows = selectedSeriesEvidenceRows.map(row => ({
+          key: row.label,
+          title: row.label,
+          copy: row.value,
+          meta: row.meta
+        }));
+        const attributionDiagnostics = buildMetricSeriesAttributionDiagnostics(selectedMetricSeries, t);
+        const attributionDiagnosticRows = attributionDiagnostics.map(row => ({
+          key: row.key,
+          label: row.label,
+          value: row.value,
+          meta: row.meta,
+          state: row.state,
+          stateLabel: row.state === 'present' ? '已提供' : '缺失',
+          tone: row.state === 'present' ? 'success' as const : 'critical' as const,
+          rowProps: {
+            'data-otlp-metrics-attribution-diagnostic-state': row.state
+          } as React.HTMLAttributes<HTMLDivElement>
+        }));
+        const trendSourceSeries = selectedMetricSeries
+          ? [selectedMetricSeries, ...metricSeries.filter(series => series.key !== selectedMetricSeries.key)]
+          : metricSeries;
+        const trendBars = buildMetricTrendBars(trendSourceSeries, formatTime);
+        const metricsChartOption = buildMetricsChartOption(metricSeries);
+        const metricsChartZoomContext = buildMetricsDataZoomTimeContext(
+          metricSeries,
+          metricsChartZoomRange,
+          draftTimeContext.timeRange || query.timeRange || 'last-30m'
+        );
+        const metricsChartZoomBounds = metricsChartZoomContext ? resolveTimeContextBounds(metricsChartZoomContext) : null;
+        const metricsChartZoomDraftLabel = metricsChartZoomBounds?.start && metricsChartZoomBounds?.end
+          ? `${formatEpochMillisDraft(metricsChartZoomBounds.start)} → ${formatEpochMillisDraft(metricsChartZoomBounds.end)}`
+          : '';
+        const canApplyMetricsChartZoom = Boolean(metricsChartZoomContext && metricsChartZoomBounds && metricsChartZoomDraftLabel);
+        const facts = buildConsoleFacts(mergedData, t, formatTime);
+        const metrics = buildConsoleMetrics(mergedData, t);
+        const contextRows = buildContextRows(mergedData, t, formatTime);
+        const metricsDetailContextRows = contextRows.map(row => ({
+          key: row.title,
+          title: row.title,
+          copy: row.copy,
+          meta: row.meta
+        }));
+        const handoffLinks = buildMetricsHandoffLinks(mergedData, query, routeContext, selectedMetricSeries);
+        const linkedRecordRows = buildMetricSeriesLinkedRecordRows(selectedMetricSeries, handoffLinks, t);
+        const linkedRecordHandoffTargets = linkedRecordRows.map(row => ({
+          id: row.key,
+          label: <span data-otlp-metrics-linked-record-action={row.key}>{row.label}</span>,
+          description: row.meta,
+          meta: row.value,
+          href: row.href,
+          tone: row.key === 'alerts' ? ('critical' as const) : row.key === 'traces' ? ('info' as const) : ('neutral' as const)
+        }));
+        const missingEntityHandoffTitle = t('otlp.metrics.handoff.entity-disabled');
+        const canOpenEntity = handoffLinks.entityHref.startsWith('/entities/');
+        const entityContextRows = buildSignalEntityContextRows(routeContext, {
+          entityId: queryEntityIdText || (mergedData.context?.entityId != null ? String(mergedData.context.entityId) : undefined),
+          entityName: query.entityName || mergedData.context?.entityName,
+          serviceName: mergedData.context?.serviceName || query.serviceName,
+          serviceNamespace: mergedData.context?.serviceNamespace || query.serviceNamespace,
+          environment: mergedData.context?.environment || query.environment,
+          start: queryStartText || (mergedData.context?.start != null ? String(mergedData.context.start) : undefined),
+          end: queryEndText || (mergedData.context?.end != null ? String(mergedData.context.end) : undefined),
+          source: routeContext.source || 'OTLP'
+        });
+        const entityContextDetailRows = entityContextRows.map(row => ({
+          key: row.label,
+          title: row.label,
+          copy: row.value,
+          meta: row.meta
+        }));
+        const serviceGroupLabel = t('otlp.metrics.group.service');
+        const namespaceGroupLabel = t('otlp.metrics.group.namespace');
+        const environmentGroupLabel = t('otlp.metrics.group.environment');
+        const currentGroupLabel = draft.groupBy === 'service_namespace'
+          ? namespaceGroupLabel
+          : draft.groupBy === 'deployment_environment_name'
+            ? environmentGroupLabel
+            : serviceGroupLabel;
+        const headerContextPills = [
+          { label: t('otlp.metrics.field.service'), value: firstRouteText(mergedData.context?.serviceName, query.serviceName, draft.serviceName) },
+          { label: t('otlp.metrics.field.namespace'), value: firstRouteText(mergedData.context?.serviceNamespace, query.serviceNamespace, draft.serviceNamespace) },
+          { label: t('otlp.metrics.field.environment'), value: firstRouteText(mergedData.context?.environment, query.environment, draft.environment) },
+          {
+            label: t('otlp.metrics.field.group-by'),
+            value: currentGroupLabel
+          }
+        ].filter((pill): pill is { label: string; value: string } => Boolean(pill.value));
+        const seriesSetScopeRows = [
+          { label: t('otlp.metrics.scope.service'), value: firstRouteText(mergedData.context?.serviceName, query.serviceName, draft.serviceName) || t('otlp.metrics.scope.all-services') },
+          { label: t('otlp.metrics.field.namespace'), value: firstRouteText(mergedData.context?.serviceNamespace, query.serviceNamespace, draft.serviceNamespace) || t('otlp.metrics.scope.all-namespaces') },
+          { label: t('otlp.metrics.field.environment'), value: firstRouteText(mergedData.context?.environment, query.environment, draft.environment) || t('otlp.metrics.scope.all-environments') },
+          {
+            label: t('otlp.metrics.field.group-by'),
+            value: currentGroupLabel
+          },
+          { label: t('otlp.metrics.scope.series'), value: t('otlp.metrics.scope.series-count', { count: seriesRows.length }) }
+        ];
+        const latestObservedAt = latestSeriesTimestamp(mergedData);
+        const metricSeriesTableRows = seriesRows.map((row, index) => ({
+          ...row,
+          rowKey: metricSeries[index]?.key || `${row.title}-${index}`,
+          pointCount: metricSeries[index]?.points.length ?? 0,
+          series: metricSeries[index]
+        }));
+        const firstSeries = (selectedMetricSeriesIndex >= 0 ? seriesRows[selectedMetricSeriesIndex] : undefined) ?? seriesRows[0] ?? {
+          title: mergedData.query || t('otlp.metrics.query.unselected'),
+          copy: mergedData.context?.serviceName || routeContext.serviceName || '-',
+          meta: '-'
+        };
+
+        return (
+          <HzSignalWorkbenchShell
+            data-otlp-metrics-route="otlp-cold-metrics-workbench"
+            data-otlp-metrics-shell-owner="hertzbeat-ui-signal-workbench-shell"
+            data-otlp-metrics-style-baseline="hertzbeat-cold-matte"
+            data-otlp-metrics-page-shell="flat-direct-stack"
+            data-otlp-metrics-page-shell-layer="removed"
+            data-otlp-metrics-page-stack="direct-panels"
+            layout="metrics-workbench"
+          >
+              <HzPanelSurface
+                data-otlp-metrics-header="cold-compact-header"
+                data-otlp-metrics-header-owner="hertzbeat-ui-panel-surface"
+                data-otlp-metrics-header-layout="compact-title-with-aligned-toolbar"
+                data-otlp-metrics-header-actions="removed"
+                padding="query"
+              >
+                <HzWorkbenchLayout
+                  as="div"
+                  variant="metrics-header"
+                  data-otlp-metrics-header-layout-frame="compact-title-with-aligned-toolbar"
+                  data-otlp-metrics-header-layout-frame-owner="hertzbeat-ui-workbench-layout"
+                >
+                  <HzWorkbenchHeaderCopy
+                    density="compact"
+                    eyebrow={t('otlp.metrics.header.kicker')}
+                    title={t('otlp.metrics.title')}
+                    data-otlp-metrics-title-block="operator-context"
+                    data-otlp-metrics-title-block-owner="hertzbeat-ui-workbench-header-copy"
+                  >
+                    {headerContextPills.length ? (
+                      <HzChipGroup
+                        density="compact"
+                        spacing="top-3"
+                        data-otlp-metrics-header-context-strip="applied-query-facts"
+                        data-otlp-metrics-header-context-strip-owner="hertzbeat-ui-toolbar-chips"
+                      >
+                        {headerContextPills.map(pill => (
+                          <HzStatusBadge
+                            key={pill.label}
+                            tone="neutral"
+                            size="sm"
+                            layout="context-pill"
+                            label={pill.label}
+                            value={pill.value}
+                            data-otlp-metrics-header-context-pill="applied-query-fact"
+                            data-otlp-metrics-header-context-pill-owner="hertzbeat-ui-status-badge"
+                          />
+                        ))}
+                      </HzChipGroup>
+                    ) : null}
+                  </HzWorkbenchHeaderCopy>
+                  <HzWorkbenchLayout
+                    as="div"
+                    variant="header-toolbar-slot"
+                    data-otlp-metrics-header-topbar="time-context"
+                    data-otlp-metrics-header-topbar-owner="hertzbeat-ui-workbench-layout"
+                  >
+                    <HzWorkbenchLayout
+                      as="div"
+                      variant="time-toolbar"
+                      data-otlp-metrics-time-toolbar="top-right-corner"
+                      data-otlp-metrics-time-toolbar-owner="hertzbeat-ui-workbench-layout"
+                      data-otlp-metrics-toolbar-stack="same-width-time-actions"
+                    >
+                      {routeContext.returnTo ? (
+                        <HzActionGroup
+                          data-otlp-metrics-return-action-group="header"
+                          data-otlp-metrics-return-action-group-owner="hertzbeat-ui-action-group"
+                          layout="full-end"
+                        >
+                          <HzButtonLink
+                            component={Link}
+                            data-otlp-metrics-return-action="true"
+                            data-otlp-metrics-header-action="return-source"
+                            href={routeContext.returnTo}
+                            size="md"
+                          >
+                            <HzButtonIcon
+                              icon={Workflow}
+                              data-otlp-metrics-header-action-icon="return-source"
+                              data-otlp-metrics-header-action-icon-owner="hertzbeat-ui-button-icon"
+                            />
+                            {t('otlp.metrics.route.action.return-source')}
+                          </HzButtonLink>
+                        </HzActionGroup>
+                      ) : null}
+                      <HzControlStack
+                        layout="end-inline"
+                        data-otlp-metrics-time-control="shared-time-context-control"
+                        data-otlp-metrics-time-control-owner="hertzbeat-ui-control-stack"
+                        data-otlp-metrics-time-control-placement="top-right"
+                        data-otlp-metrics-time-control-visual="narrow-top-right-rail"
+                        data-otlp-metrics-time-control-fit="no-clipping"
+                      >
+                        <TimeRangeControl
+                          value={draftTimeContext}
+                          labels={buildTimeRangeControlLabels(t)}
+                          onApply={applyMetricsTimeContext}
+                          onRefresh={() => applyMetricsQuery(draftTimeContext)}
+                          onReset={() => applyMetricsTimeContext({ timeRange: 'last-30m' })}
+                          presets={metricsTimeRangePresets}
+                          showAbsoluteFields
+                          variant="narrow-rail"
+                          data-otlp-metrics-time-range-control-owner="hertzbeat-shared-time-range-control"
+                          presetSelectProps={{ 'data-otlp-metrics-time-range-select': 'true' }}
+                          presetOptionDataAttribute="data-otlp-metrics-time-range-preset"
+                          refreshActionProps={{ 'data-otlp-metrics-time-refresh-action': 'true' }}
+                        />
+                      </HzControlStack>
+                    </HzWorkbenchLayout>
+                  </HzWorkbenchLayout>
+                </HzWorkbenchLayout>
+              </HzPanelSurface>
+
+              <HzPanelSurface
+                data-otlp-metrics-query-bar="cold-query-row"
+                data-otlp-metrics-query-bar-owner="hertzbeat-ui-panel-surface"
+                padding="query"
+              >
+                <HzControlStack
+                  layout="inline-wrap"
+                  data-otlp-metrics-query-control-stack="shared-inline-controls"
+                  data-otlp-metrics-query-control-stack-owner="hertzbeat-ui-control-stack"
+                >
+                  <HzSearchFieldFrame
+                    data-otlp-metrics-query-search-frame="shared-search-field-frame"
+                    data-otlp-metrics-query-search-frame-owner="hertzbeat-ui-search-field-frame"
+                    width="metrics-query"
+                    icon={(
+                      <HzSearchFieldIcon
+                        icon={Search}
+                        data-otlp-metrics-query-search-icon="metric-query"
+                        data-otlp-metrics-query-search-icon-owner="hertzbeat-ui-search-field-icon"
+                      />
+                    )}
+                  >
+                    <HzInput
+                      data-otlp-metrics-query-input="true"
+                      data-otlp-metrics-query-input-owner="hertzbeat-ui-input"
+                      aria-label={t('otlp.metrics.query.aria')}
+                      value={draft.query || mergedData.query || ''}
+                      onChange={event => updateDraftField('query', event.target.value)}
+                      onInput={event => updateDraftField('query', event.currentTarget.value)}
+                      placeholder="http.server.duration"
+                      inset="search-icon"
+                      width="metrics-query-expression"
+                    />
+                  </HzSearchFieldFrame>
+                  <HzSelect
+                    data-otlp-metrics-aggregation-select="true"
+                    data-otlp-metrics-aggregation-select-owner="hertzbeat-ui-select"
+                    aria-label={t('otlp.metrics.aggregation.aria')}
+                    value={draft.aggregation}
+                    onChange={event => updateDraftField('aggregation', event.target.value)}
+                    width="metrics-aggregation"
+                    triggerClassName="text-[#d5dce8]"
+                    options={[
+                      { value: 'avg', label: t('otlp.metrics.aggregation.avg') },
+                      { value: 'sum', label: t('otlp.metrics.aggregation.sum') },
+                      { value: 'max', label: t('otlp.metrics.aggregation.max') },
+                      { value: 'min', label: t('otlp.metrics.aggregation.min') }
+                    ]}
+                    optionDataAttributes={option => ({
+                      'data-otlp-metrics-aggregation-option': option.value
+                    })}
+                  />
+                  <HzSelect
+                    data-otlp-metrics-group-by-select="true"
+                    data-otlp-metrics-group-by-select-owner="hertzbeat-ui-select"
+                    aria-label={t('otlp.metrics.field.group-by')}
+                    value={draft.groupBy}
+                    onChange={event => updateDraftField('groupBy', event.target.value)}
+                    width="metrics-group-by"
+                    triggerClassName="text-[#d5dce8]"
+                    options={[
+                      { value: 'service_name', label: serviceGroupLabel },
+                      { value: 'service_namespace', label: namespaceGroupLabel },
+                      { value: 'deployment_environment_name', label: environmentGroupLabel }
+                    ]}
+                    optionDataAttributes={option => ({
+                      'data-otlp-metrics-group-by-option': option.value
+                    })}
+                  />
+                  <HzQueryActionGroup
+                    data-otlp-metrics-query-action-group="shared-query-action-group"
+                    data-otlp-metrics-query-action-group-owner="hertzbeat-ui-query-action-group"
+                  >
+                    <HzButton data-otlp-metrics-run-query-action="true" intent="primary" size="md" onClick={applyMetricsQuery}>
+                      <HzButtonIcon
+                        icon={Play}
+                        data-otlp-metrics-query-action-icon="run"
+                        data-otlp-metrics-query-action-icon-owner="hertzbeat-ui-button-icon"
+                      />
+                      {t('otlp.metrics.query.run')}
+                    </HzButton>
+                    <HzButtonLink component={Link} href="/ingestion/otlp/metrics" size="md" data-otlp-metrics-reset-action="true">
+                      <HzButtonIcon
+                        icon={RotateCcw}
+                        data-otlp-metrics-query-action-icon="reset"
+                        data-otlp-metrics-query-action-icon-owner="hertzbeat-ui-button-icon"
+                      />
+                      {t('common.reset')}
+                    </HzButtonLink>
+                  </HzQueryActionGroup>
+                </HzControlStack>
+                <HzControlStack
+                  layout="inline-wrap"
+                  spacing="top-2"
+                  data-otlp-metrics-context-control-stack="shared-inline-controls"
+                  data-otlp-metrics-context-control-stack-owner="hertzbeat-ui-control-stack"
+                >
+                  <HzInput
+                    data-otlp-metrics-service-input="true"
+                    data-otlp-metrics-context-input="service-name"
+                    data-otlp-metrics-context-input-owner="hertzbeat-ui-input"
+                    aria-label={t('otlp.metrics.field.service-name')}
+                    value={draft.serviceName || mergedData.context?.serviceName || ''}
+                    onChange={event => updateDraftField('serviceName', event.target.value)}
+                    onInput={event => updateDraftField('serviceName', event.currentTarget.value)}
+                    placeholder={t('otlp.metrics.field.service-name')}
+                    width="metrics-context"
+                  />
+                  <HzInput
+                    data-otlp-metrics-namespace-input="true"
+                    data-otlp-metrics-context-input="namespace"
+                    data-otlp-metrics-context-input-owner="hertzbeat-ui-input"
+                    aria-label={t('otlp.metrics.field.namespace')}
+                    value={draft.serviceNamespace || mergedData.context?.serviceNamespace || ''}
+                    onChange={event => updateDraftField('serviceNamespace', event.target.value)}
+                    onInput={event => updateDraftField('serviceNamespace', event.currentTarget.value)}
+                    placeholder={t('otlp.metrics.field.namespace')}
+                    width="metrics-context"
+                  />
+                  <HzInput
+                    data-otlp-metrics-environment-input="true"
+                    data-otlp-metrics-context-input="environment"
+                    data-otlp-metrics-context-input-owner="hertzbeat-ui-input"
+                    aria-label={t('otlp.metrics.field.environment')}
+                    value={draft.environment || mergedData.context?.environment || ''}
+                    onChange={event => updateDraftField('environment', event.target.value)}
+                    onInput={event => updateDraftField('environment', event.currentTarget.value)}
+                    placeholder={t('otlp.metrics.field.environment')}
+                    width="metrics-context-compact"
+                  />
+                  <HzInput
+                    data-otlp-metrics-trace-id-input="true"
+                    data-otlp-metrics-context-input="trace-id"
+                    data-otlp-metrics-context-input-owner="hertzbeat-ui-input"
+                    aria-label={t('otlp.metrics.field.trace-id')}
+                    value={draft.traceId}
+                    onChange={event => updateDraftField('traceId', event.target.value)}
+                    onInput={event => updateDraftField('traceId', event.currentTarget.value)}
+                    placeholder={t('otlp.metrics.field.trace-id')}
+                    width="metrics-trace-id"
+                  />
+                </HzControlStack>
+              </HzPanelSurface>
+
+              <HzPanelSurface
+                data-otlp-metrics-chart-band="cold-chart-band"
+                data-otlp-metrics-chart-band-owner="hertzbeat-ui-panel-surface"
+                padding="chart"
+              >
+                <HzWorkbenchLayout
+                  as="div"
+                  variant="chart-stack"
+                  data-otlp-metrics-chart-layout="wide-series-chart-with-compact-meta"
+                  data-otlp-metrics-chart-layout-owner="hertzbeat-ui-workbench-layout"
+                  data-otlp-metrics-series-mode="entity-series-set"
+                >
+                  <HzPanelSurface
+                    data-otlp-metrics-chart-panel="series-set-trend"
+                    data-otlp-metrics-chart-panel-owner="hertzbeat-ui-panel-surface"
+                    data-otlp-metrics-chart-panel-variant-owner="hertzbeat-ui-panel-surface"
+                    padding="chart-inner"
+                    variant="chart-inner"
+                    data-otlp-metrics-chart-datazoom-state="local-observation"
+                    data-otlp-metrics-chart-datazoom-preserve="preserved"
+                  >
+                    <HzWorkbenchLayout
+                      as="div"
+                      variant="metrics-chart-toolbar"
+                      data-otlp-metrics-chart-header-layout="trend-toolbar"
+                      data-otlp-metrics-chart-header-layout-owner="hertzbeat-ui-workbench-layout"
+                    >
+                      <HzPanelTitleLabel
+                        icon={BarChart3}
+                        data-otlp-metrics-chart-title-label="shared-panel-title-label"
+                        data-otlp-metrics-chart-title-label-owner="hertzbeat-ui-panel-title-label"
+                      >
+                        趋势带
+                      </HzPanelTitleLabel>
+                      <HzActionGroup
+                        layout="end-wrap"
+                        data-otlp-metrics-chart-toolbar-actions="shared-toolbar-actions"
+                        data-otlp-metrics-chart-toolbar-actions-owner="hertzbeat-ui-action-group"
+                      >
+                        {hasMetricSeries ? (
+                          <HzChipGroup
+                            data-otlp-metrics-chart-meta-row="compact-real-facts"
+                            data-otlp-metrics-chart-meta-row-owner="hertzbeat-ui-toolbar-chips"
+                            align="end"
+                            density="compact"
+                          >
+                            {facts.map(fact => (
+                              <HzStatusBadge
+                                key={fact.label}
+                                data-otlp-metrics-chart-meta-fact="true"
+                                data-otlp-metrics-chart-meta-fact-owner="hertzbeat-ui-status-badge"
+                                tone="neutral"
+                                size="sm"
+                                layout="metric-fact"
+                                label={fact.label}
+                                value={fact.value}
+                              />
+                            ))}
+                          </HzChipGroup>
+                        ) : null}
+                        {metricsChartZoomDraftLabel ? (
+                          <HzStatusBadge
+                            data-otlp-metrics-chart-zoom-draft="pending-query-time"
+                            data-otlp-metrics-chart-zoom-draft-owner="hertzbeat-ui-status-badge"
+                            data-otlp-metrics-chart-zoom-draft-state="pending"
+                            tone="info"
+                            size="sm"
+                            layout="zoom-draft"
+                            label={t('time.context.zoom.draft')}
+                            value={metricsChartZoomDraftLabel}
+                            valueFont="mono"
+                          />
+                        ) : null}
+                        {hasMetricSeries ? (
+                          <HzButton
+                            type="button"
+                            intent="ghost"
+                            size="sm"
+                            data-otlp-metrics-chart-zoom-apply="local-to-query-time"
+                            data-otlp-metrics-chart-zoom-apply-owner="hertzbeat-ui-button"
+                            data-otlp-metrics-chart-zoom-apply-state={canApplyMetricsChartZoom ? 'ready' : 'idle'}
+                            disabled={!canApplyMetricsChartZoom}
+                            onClick={() => {
+                              if (!metricsChartZoomContext) return;
+                              setMetricsChartZoomRange(null);
+                              applyMetricsTimeContext(metricsChartZoomContext);
+                            }}
+                          >
+                            {t('time.context.zoom.apply')}
+                          </HzButton>
+                        ) : null}
+                        <HzStatusBadge
+                          data-otlp-metrics-chart-series-count="toolbar-count"
+                          data-otlp-metrics-chart-series-count-owner="hertzbeat-ui-status-badge"
+                          tone="neutral"
+                          size="xs"
+                        >
+                            {workbenchState.seriesCountLabel}
+                          </HzStatusBadge>
+                      </HzActionGroup>
+                    </HzWorkbenchLayout>
+                    {hasMetricSeries ? (
+                      <EChartsPanel
+                        option={metricsChartOption}
+                        height={300}
+                        edge="metrics-chart"
+                        data-otlp-metrics-echarts-edge="shared-metrics-chart"
+                        data-otlp-metrics-echarts-edge-owner="hertzbeat-ui-echarts-panel"
+                        tone="operator"
+                        preserveDataZoom
+                        onDataZoomChange={nextZoom => {
+                          handleMetricsChartZoomChange(nextZoom);
+                          const nextZoomContext = buildMetricsDataZoomTimeContext(
+                            metricSeries,
+                            nextZoom,
+                            draftTimeContext.timeRange || query.timeRange || 'last-30m'
+                          );
+                          if (!nextZoomContext) return;
+                          setDraft(previous => ({
+                            ...previous,
+                            timeRange: nextZoomContext.timeRange || previous.timeRange || 'last-30m',
+                            from: nextZoomContext.from || '',
+                            to: nextZoomContext.to || '',
+                            start: nextZoomContext.start || '',
+                            end: nextZoomContext.end || '',
+                            refresh: nextZoomContext.refresh || previous.refresh || draftTimeContext.refresh || '',
+                            live: nextZoomContext.live || previous.live || draftTimeContext.live || '',
+                            tz: nextZoomContext.tz || previous.tz || draftTimeContext.tz || '',
+                            timezone: nextZoomContext.timezone || previous.timezone || draftTimeContext.timezone || ''
+                          }));
+                        }}
+                      />
+                    ) : (
+                      <HzTrendFrame
+                        data-otlp-metrics-trend-frame="shared-compact-bars"
+                        data-otlp-metrics-trend-frame-owner="hertzbeat-ui-trend-frame"
+                      >
+                        {trendBars.length ? (
+                          trendBars.map(series => (
+                            <HzTrendBar
+                              key={series.key}
+                              data-otlp-metrics-trend-bar={'real-series-point'}
+                              data-otlp-metrics-trend-bar-owner="hertzbeat-ui-trend-bar"
+                              heightPct={series.heightPct}
+                              title={`${series.seriesName} · ${series.label} · ${series.valueLabel}`}
+                            />
+                          ))
+                        ) : (
+                          <HzStateNotice
+                            data-otlp-metrics-trend-empty="no-real-series"
+                            data-otlp-metrics-trend-empty-owner="hertzbeat-ui-state-notice"
+                            data-otlp-metrics-empty-state="honest-no-series"
+                            data-otlp-metrics-empty-state-context="applied-query-visible"
+                            tone="info"
+                            variant="hint"
+                            frame="trend-empty"
+                            title="暂无指标趋势"
+                            description="运行查询后展示真实采样点。"
+                          />
+                        )}
+                      </HzTrendFrame>
+                    )}
+                    {!hasMetricSeries ? null : (
+                      <HzDataMetaText
+                        data-otlp-metrics-trend-sample-helper="real-sample-count"
+                        data-otlp-metrics-trend-sample-helper-owner="hertzbeat-ui-data-meta-text"
+                        display="block"
+                        casing="plain"
+                        spacing="trend-helper"
+                      >
+                        {trendBars.length ? `${trendBars.length} 个采样点` : '-'}
+                      </HzDataMetaText>
+                    )}
+                  </HzPanelSurface>
+                </HzWorkbenchLayout>
+              </HzPanelSurface>
+
+              <HzWorkbenchLayout
+                data-otlp-metrics-workbench-grid="series-detail-split"
+                data-otlp-metrics-workbench-grid-owner="hertzbeat-ui-workbench-layout"
+                variant={hasMetricSeries ? 'metrics-series-detail' : 'metrics-series-only'}
+              >
+                <HzPanelSurface
+                  data-otlp-metrics-series-table="cold-dense-metric-list"
+                  data-otlp-metrics-series-table-mode="service-entity-series-set"
+                  data-otlp-metrics-series-table-density="primary-scan"
+                  data-otlp-metrics-series-table-owner="hertzbeat-ui-data-table"
+                  data-otlp-metrics-series-table-panel-owner="hertzbeat-ui-panel-surface"
+                  clip
+                >
+                  <HzPanelHeader
+                    data-otlp-metrics-series-table-header="shared-panel-header"
+                    data-otlp-metrics-series-table-header-owner="hertzbeat-ui-panel-header"
+                    chrome="transparent-topless"
+                    title="序列集合"
+                    meta={
+                      <HzStatusBadge
+                        data-otlp-metrics-series-table-summary="result-count"
+                        data-otlp-metrics-series-table-summary-owner="hertzbeat-ui-status-badge"
+                        tone="neutral"
+                        size="xs"
+                      >
+                        {seriesRows.length} 条
+                      </HzStatusBadge>
+                    }
+                  />
+                  <HzPanelSection
+                    data-otlp-metrics-series-set-summary-section="shared-panel-section"
+                    data-otlp-metrics-series-set-summary-section-owner="hertzbeat-ui-panel-section"
+                  >
+                    <HzStatStrip
+                      columns={4}
+                      frame="panel-inset"
+                      spacing="compact"
+                      data-otlp-metrics-series-set-summary="service-entity-scope"
+                      data-otlp-metrics-series-set-summary-owner="hertzbeat-ui-stat-strip"
+                      data-otlp-metrics-series-set-summary-strip="shared-stat-strip"
+                      data-otlp-metrics-series-set-summary-strip-owner="hertzbeat-ui-stat-strip"
+                    >
+                      {seriesSetScopeRows.map(row => (
+                        <HzStatCell
+                          key={row.label}
+                          data-otlp-metrics-series-set-summary-cell={row.label}
+                          data-otlp-metrics-series-set-summary-cell-owner="hertzbeat-ui-stat-cell"
+                          label={row.label}
+                          value={row.value}
+                          variant="tile"
+                          density="compact"
+                          frame="inset"
+                        />
+                      ))}
+                    </HzStatStrip>
+                  </HzPanelSection>
+                  <HzDataTable
+                    data-otlp-metrics-series-data-table="shared-data-table"
+                    data-otlp-metrics-series-data-table-owner="hertzbeat-ui-data-table"
+                    variant="embedded"
+                    rows={metricSeriesTableRows}
+                    getRowKey={row => row.rowKey}
+                    selectedRowKey={selectedMetricSeries?.key}
+                    onRowClick={row => {
+                      if (row.series) setSelectedSeriesKey(row.series.key);
+                    }}
+                    getRowProps={row => ({
+                      'data-otlp-metrics-series-row': 'selectable-series',
+                      'data-otlp-metrics-series-row-selected': selectedMetricSeries?.key === row.series?.key ? 'true' : 'false'
+                    })}
+                    emptyLabel={
+                      <HzEmptyState
+                        data-otlp-metrics-series-table-empty-state="shared-empty-state"
+                        data-otlp-metrics-series-table-empty-state-owner="hertzbeat-ui-empty-state"
+                        data-otlp-metrics-empty-guidance="operator-no-data-guidance"
+                        title={workbenchState.emptyTitle}
+                        description={workbenchState.noMetricsTitle}
+                        layout="table-panel"
+                      />
+                    }
+                    columns={[
+                      {
+                        key: 'name',
+                        header: '指标名称',
+                        render: row => <HzDataCellText variant="title" display="block">{row.title}</HzDataCellText>
+                      },
+                      {
+                        key: 'service',
+                        header: '服务',
+                        render: row => <HzDataCellText variant="value">{row.copy}</HzDataCellText>
+                      },
+                      {
+                        key: 'entity',
+                        header: '实体',
+                        render: row => (
+                          <HzDataCellStack
+                            display="block"
+                            width="metrics-entity"
+                            data-otlp-metrics-series-entity="true"
+                            data-otlp-metrics-series-entity-owner="hertzbeat-ui-data-cell-stack"
+                          >
+                            <HzDataCellText
+                              variant="value"
+                              display="block"
+                              tone="strong"
+                              weight="semibold"
+                              data-otlp-metrics-series-entity-label-owner="hertzbeat-ui-data-cell-text"
+                            >
+                              {row.entityLabel}
+                            </HzDataCellText>
+                            <HzDataCellText
+                              variant="meta"
+                              display="block"
+                              spacing="stack-tight"
+                              casing="plain"
+                              tone={row.entityState === 'present' ? 'success' : 'muted'}
+                              data-otlp-metrics-series-entity-meta-owner="hertzbeat-ui-data-cell-text"
+                              data-otlp-metrics-series-entity-state={row.entityState}
+                            >
+                              {row.entityMeta}
+                            </HzDataCellText>
+                          </HzDataCellStack>
+                        )
+                      },
+                      {
+                        key: 'latest',
+                        header: '最新值',
+                        render: row => <HzDataCellText variant="value" font="mono" tone="bright" data-otlp-metrics-series-latest-owner="hertzbeat-ui-data-cell-text">{row.meta}</HzDataCellText>
+                      },
+                      {
+                        key: 'points',
+                        header: '采样点',
+                        render: row => <HzDataCellText variant="value">{row.pointCount}</HzDataCellText>
+                      },
+                      {
+                        key: 'time',
+                        header: '最近时间',
+                        render: () => <HzDataCellText variant="timestamp">{formatTime(latestObservedAt)}</HzDataCellText>
+                      }
+                    ]}
+                  />
+                </HzPanelSurface>
+
+                {hasMetricSeries && selectedMetricSeries ? (
+                <HzPanelSurface
+                  data-otlp-metrics-detail-panel="cold-detail-panel"
+                  data-otlp-metrics-detail-panel-owner="hertzbeat-ui-panel-surface"
+                  data-otlp-metrics-detail-panel-priority="secondary-inspector"
+                  data-otlp-metrics-detail-panel-stickiness-owner="hertzbeat-ui-panel-surface"
+                  clip
+                  stickiness="top-4"
+                >
+                  <HzPanelHeader
+                    data-otlp-metrics-detail-panel-header="shared-panel-header"
+                    data-otlp-metrics-detail-panel-header-owner="hertzbeat-ui-panel-header"
+                    chrome="transparent"
+                    eyebrow="详情面板"
+                    title={firstSeries.title}
+                    subtitle={workbenchState.chartLabel}
+                  />
+                  <HzPanelSection
+                    divider="none"
+                    data-otlp-metrics-detail-panel-body="compact-evidence-stack"
+                    data-otlp-metrics-detail-panel-body-owner="hertzbeat-ui-panel-section"
+                  >
+                    <HzStatStrip
+                      columns={3}
+                      frame="panel-solid"
+                      data-otlp-metrics-detail-summary-stats="shared-stat-strip"
+                      data-otlp-metrics-detail-summary-stats-owner="hertzbeat-ui-stat-strip"
+                    >
+                      {metrics.map(metric => (
+                        <HzStatCell
+                          key={metric.label}
+                          data-otlp-metrics-detail-summary-stat={metric.label}
+                          data-otlp-metrics-detail-summary-stat-owner="hertzbeat-ui-stat-cell"
+                          label={metric.label}
+                          value={metric.value}
+                          variant="tile"
+                          density="compact"
+                          frame="flush"
+                        />
+                      ))}
+                    </HzStatStrip>
+                    <HzDetailRows
+                      data-otlp-metrics-detail-context-rows="shared-detail-rows"
+                      data-otlp-metrics-detail-context-rows-owner="hertzbeat-ui-detail-rows"
+                      offset="top"
+                      heading="查询上下文"
+                      rows={metricsDetailContextRows}
+                    />
+                    {selectedSeriesContextRows.length > 0 ? (
+                      <HzDetailRows
+                        data-otlp-metrics-selected-series-context="selected-series-attribution"
+                        data-otlp-metrics-selected-series-context-owner="hertzbeat-ui-detail-rows"
+                        aria-label="当前选中序列 关联实体 当前服务 采集模板 当前环境"
+                        boundary="top"
+                        heading="当前选中序列"
+                        rows={selectedSeriesContextDetailRows}
+                      />
+                    ) : null}
+                    {selectedSeriesEvidenceRows.length > 0 ? (
+                      <HzDetailRows
+                        data-otlp-metrics-selected-series-evidence="real-sample-evidence"
+                        data-otlp-metrics-selected-series-evidence-owner="hertzbeat-ui-detail-rows"
+                        aria-label="指标证据 采样点 最新值 值域 采样窗口 关联链路"
+                        boundary="top"
+                        heading="指标证据"
+                        rows={selectedSeriesEvidenceDetailRows}
+                      />
+                    ) : null}
+                  </HzPanelSection>
+                  <HzPanelSection
+                    divider="top"
+                    data-otlp-metrics-handoff-action-section="shared-panel-section"
+                    data-otlp-metrics-handoff-action-section-owner="hertzbeat-ui-panel-section"
+                  >
+                    <HzActionGroup
+                      layout="grid-2"
+                      data-otlp-metrics-handoff-actions="compact-context-actions"
+                      data-otlp-metrics-handoff-actions-owner="hertzbeat-ui-action-group"
+                    >
+                      {canOpenEntity ? (
+                        <HzButtonLink component={Link} href={handoffLinks.entityHref} size="md" layout="full" data-otlp-metrics-entity-action="true">
+                          实体详情
+                        </HzButtonLink>
+                      ) : (
+                        <HzDisabledActionShell
+                          title={missingEntityHandoffTitle}
+                          data-otlp-metrics-entity-action-disabled-shell-owner="hertzbeat-ui-disabled-action-shell"
+                          layout="full"
+                        >
+                          <HzButton
+                            data-otlp-metrics-entity-action-disabled="missing-entity-id"
+                            size="md"
+                            layout="full"
+                            disabled
+                            title={missingEntityHandoffTitle}
+                            aria-label={missingEntityHandoffTitle}
+                          >
+                            实体详情
+                          </HzButton>
+                        </HzDisabledActionShell>
+                      )}
+                      <HzButtonLink component={Link} href={handoffLinks.alertHandlingHref} size="md" layout="full" data-otlp-metrics-alert-handling-action="true">
+                        告警处理
+                      </HzButtonLink>
+                      <HzButtonLink component={Link} href={handoffLinks.logsHref} size="md" layout="full" data-otlp-metrics-logs-action="true">
+                        查看日志
+                      </HzButtonLink>
+                      <HzButtonLink component={Link} href={handoffLinks.tracesHref} size="md" layout="full" data-otlp-metrics-traces-action="true">
+                        查看链路
+                      </HzButtonLink>
+                      <HzButtonLink component={Link} href={handoffLinks.entitiesHref} size="md" layout="full" data-otlp-metrics-entities-action="true">
+                        对象目录
+                      </HzButtonLink>
+                    </HzActionGroup>
+                  </HzPanelSection>
+                  <HzPanelSection
+                    divider="top"
+                    spacing="stack-2"
+                    data-otlp-metrics-secondary-context="collapsible-evidence-diagnostics"
+                    data-otlp-metrics-secondary-context-owner="hertzbeat-ui-panel-section"
+                  >
+                    {linkedRecordRows.length > 0 ? (
+                      <HzCollapsibleSection
+                        data-otlp-metrics-linked-record-summary="log-trace-alert-links"
+                        data-otlp-metrics-linked-record-summary-panel="collapsible"
+                        data-otlp-metrics-linked-record-summary-owner="hertzbeat-ui-collapsible-section"
+                        aria-label="关联记录 历史日志 链路瀑布图 告警处理"
+                        title="关联记录"
+                        meta="日志 / 链路 / 告警"
+                        surface="inset"
+                      >
+                        <div
+                          data-otlp-metrics-linked-record-handoff="shared-context-handoff"
+                          data-otlp-metrics-linked-record-handoff-owner="hertzbeat-ui-context-handoff"
+                        >
+                          <HzContextHandoff
+                            title="证据跳转"
+                            context="日志 / 链路 / 告警"
+                            targets={linkedRecordHandoffTargets}
+                            frame="flush"
+                          />
+                        </div>
+                      </HzCollapsibleSection>
+                    ) : null}
+                    {attributionDiagnostics.length > 0 ? (
+                      <HzCollapsibleSection
+                        data-otlp-metrics-attribution-diagnostics-panel="collapsible"
+                        data-otlp-metrics-attribution-diagnostics-panel-owner="hertzbeat-ui-collapsible-section"
+                        aria-label="归因诊断 hertzbeat.entity_id hertzbeat.entity_name hertzbeat.workspace_id hertzbeat.collector hertzbeat.template"
+                        title="归因诊断"
+                        meta="hertzbeat.*"
+                        surface="inset"
+                      >
+                        <HzAttributeDiagnostics
+                          data-otlp-metrics-attribution-diagnostics="hertzbeat-attribute-diagnostics"
+                          data-otlp-metrics-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"
+                          title="归因诊断"
+                          namespaceLabel="hertzbeat.*"
+                          rows={attributionDiagnosticRows}
+                          frame="embedded"
+                        />
+                      </HzCollapsibleSection>
+                    ) : null}
+                    <HzCollapsibleSection
+                      data-otlp-metrics-entity-context="hertzbeat-signal-entity-context"
+                      data-otlp-metrics-entity-context-panel="collapsible"
+                      data-otlp-metrics-entity-context-panel-owner="hertzbeat-ui-collapsible-section"
+                      aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"
+                      title="实体上下文"
+                      meta="当前窗口"
+                      surface="inset"
+                    >
+                      <HzDetailRows
+                        data-otlp-metrics-entity-context-owner="hertzbeat-ui-detail-rows"
+                        heading="实体上下文"
+                        padding="compact-y"
+                        rows={entityContextDetailRows}
+                      />
+                    </HzCollapsibleSection>
+                  </HzPanelSection>
+                </HzPanelSurface>
+                ) : (
+                  <HzAssistiveMarker
+                    data-otlp-metrics-detail-panel-empty="suppressed-until-real-series"
+                    data-otlp-metrics-detail-panel-empty-owner="hertzbeat-ui-assistive-marker"
+                  />
+                )}
+              </HzWorkbenchLayout>
+          </HzSignalWorkbenchShell>
+        );
+      }}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/ingestion/otlp/metrics/page.test.tsx b/web-next/app/ingestion/otlp/metrics/page.test.tsx
index e4b12343b6..781de4a004 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -7,6 +7,7 @@ import { createRoot, type Root } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../../test/i18n-test-helper';
+import { SUPPLEMENTAL_MESSAGES } from '../../../../lib/i18n-runtime-messages';
 
 (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
 
@@ -46,7 +47,7 @@ vi.mock('next/navigation', () => ({
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock()
+    t: createTranslatorMock({ overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] })
   })
 }));
 
@@ -70,10 +71,12 @@ vi.mock('@/components/ui/select', () => ({
 }));
 
 vi.mock('@/components/observability/echarts-panel', () => ({
-  EChartsPanel: ({ onDataZoomChange, preserveDataZoom }: any) => (
+  EChartsPanel: ({ edge, onDataZoomChange, preserveDataZoom, ...props }: any) => (
     <button
+      {...props}
       type="button"
       data-echarts-panel="otlp-metrics"
+      data-echarts-panel-edge={edge}
       data-echarts-panel-preserve-datazoom={preserveDataZoom ? 'true' : 'false'}
       onClick={() => onDataZoomChange?.({ start: 25, end: 75 })}
     >
@@ -92,6 +95,16 @@ vi.mock('@/lib/format', () => ({
 
 vi.mock('@/lib/otlp-metrics/controller', () => ({
   loadOtlpMetricsConsole,
+  buildOtlpMetricsConsoleUrl: (query: Record<string, unknown> = {}) => {
+    const params = new URLSearchParams();
+    Object.entries(query).forEach(([key, value]) => {
+      if (value !== undefined && value !== null && value !== '') {
+        params.set(key, String(value));
+      }
+    });
+    const search = params.toString();
+    return `/api/otlp/v1/metrics${search ? `?${search}` : ''}`;
+  },
   queryStateFromParams: (params: { get(key: string): string | null }) => ({
     query: params.get('query') || undefined,
     aggregation: params.get('aggregation') || undefined,
@@ -107,11 +120,14 @@ vi.mock('@/lib/otlp-metrics/controller', () => ({
     serviceName: params.get('serviceName') || undefined,
     serviceNamespace: params.get('serviceNamespace') || undefined,
     environment: params.get('environment') || undefined,
+    from: params.get('from') || undefined,
+    to: params.get('to') || undefined,
     start: params.get('start') || undefined,
     end: params.get('end') || undefined,
     refresh: params.get('refresh') || undefined,
     live: params.get('live') || undefined,
-    tz: params.get('tz') || undefined
+    tz: params.get('tz') || undefined,
+    timezone: params.get('timezone') || undefined
   })
 }));
 
@@ -138,7 +154,10 @@ vi.mock('@/lib/signal-route-context', () => ({
     serviceName: mockState.searchParams.get('serviceName') || undefined,
     serviceNamespace: mockState.searchParams.get('serviceNamespace') || undefined,
     environment: mockState.searchParams.get('environment') || undefined,
+    from: mockState.searchParams.get('from') || undefined,
+    to: mockState.searchParams.get('to') || undefined,
     timeRange: mockState.searchParams.get('timeRange') || undefined,
+    timezone: mockState.searchParams.get('timezone') || undefined,
     source: mockState.searchParams.get('source') || undefined,
     collector: mockState.searchParams.get('collector') || undefined,
     template: mockState.searchParams.get('template') || undefined,
@@ -240,8 +259,8 @@ vi.mock('@/lib/otlp-metrics/view-model', () => ({
     zoomRange
       ? {
           timeRange: fallbackTimeRange || 'last-30m',
-          start: '1500',
-          end: '2500'
+          from: '1970-01-01 08:00:01',
+          to: '1970-01-01 08:00:02'
         }
       : null,
   buildMetricSeriesRows: () => mockState.metricSeries.map(series => ({
@@ -322,19 +341,100 @@ afterEach(() => {
 });
 
 describe('otlp metrics page', () => {
+  it('keeps the metrics header and query toolbar copy behind i18n keys', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
+    const slice = source.slice(
+      source.indexOf('const serviceGroupLabel'),
+      source.indexOf('data-otlp-metrics-chart-band="cold-chart-band"')
+    );
+
+    expect(slice).not.toMatch(/[\u4e00-\u9fff]/);
+    expect(slice).toContain("t('otlp.metrics.header.kicker')");
+    expect(slice).toContain("t('otlp.metrics.query.run')");
+    expect(slice).toContain("t('otlp.metrics.group.service')");
+    expect(slice).toContain("t('otlp.metrics.scope.all-services')");
+  });
+
   it('keeps metrics on the OTLP cold Workbench owner instead of the old external-product explorer stack', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
 
     expect(source).toContain('data-otlp-metrics-route="otlp-cold-metrics-workbench"');
+    expect(source).toContain('HzSignalWorkbenchShell');
+    expect(source).toContain('data-otlp-metrics-shell-owner="hertzbeat-ui-signal-workbench-shell"');
+    expect(source).toContain('layout="metrics-workbench"');
+    expect(source).not.toContain('className="flex min-h-[calc(100vh-56px)] flex-col gap-3 bg-[#07090b] px-3 pb-3 pt-0 text-[#e8edf5]"');
     expect(source).toContain('data-otlp-metrics-style-baseline="hertzbeat-cold-matte"');
     expect(source).toContain('data-otlp-metrics-query-bar="cold-query-row"');
+    expect(source).toContain('data-otlp-metrics-query-bar-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('padding="query"');
+    expect(source).toContain('data-otlp-metrics-query-control-stack="shared-inline-controls"');
+    expect(source).toContain('data-otlp-metrics-query-control-stack-owner="hertzbeat-ui-control-stack"');
+    expect(source).toContain('data-otlp-metrics-context-control-stack="shared-inline-controls"');
+    expect(source).toContain('data-otlp-metrics-context-control-stack-owner="hertzbeat-ui-control-stack"');
+    expect(source).toContain('layout="inline-wrap"');
+    expect(source).toContain('spacing="top-2"');
+    expect(source).not.toContain('className="flex flex-wrap items-center gap-2"');
+    expect(source).not.toContain('className="mt-2 flex flex-wrap items-center gap-2"');
+    expect(source).not.toContain('data-otlp-metrics-context-control-stack-owner="hertzbeat-ui-control-stack"\n                  className="mt-2"');
+    expect(source).toContain('HzSearchFieldFrame');
+    expect(source).toContain('HzSearchFieldIcon');
+    expect(source).toContain('HzInput');
     expect(source).toContain('data-otlp-metrics-query-input="true"');
+    expect(source).toContain('data-otlp-metrics-query-search-frame="shared-search-field-frame"');
+    expect(source).toContain('data-otlp-metrics-query-search-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(source).toContain('width="metrics-query"');
+    expect(source).toContain('data-otlp-metrics-query-search-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(source).toContain('data-otlp-metrics-query-input-owner="hertzbeat-ui-input"');
+    expect(source).toContain('inset="search-icon"');
+    expect(source).toContain('width="metrics-query-expression"');
+    expect(source).not.toContain('<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4');
+    expect(source).not.toContain('className={`${inputClass} w-full pl-9 font-mono`}');
+    expect(source).not.toContain('className="w-full font-mono"');
+    expect(source).not.toContain('className="min-w-[320px] max-w-[560px] flex-1"');
     expect(source).toContain('data-otlp-metrics-service-input="true"');
     expect(source).toContain('data-otlp-metrics-namespace-input="true"');
     expect(source).toContain('data-otlp-metrics-environment-input="true"');
+    expect(source).toContain('data-otlp-metrics-context-input="service-name"');
+    expect(source).toContain('data-otlp-metrics-context-input="namespace"');
+    expect(source).toContain('data-otlp-metrics-context-input="environment"');
+    expect(source).toContain('data-otlp-metrics-context-input="trace-id"');
+    expect(source).toContain('data-otlp-metrics-context-input-owner="hertzbeat-ui-input"');
+    expect(source).toContain('width="metrics-context"');
+    expect(source).toContain('width="metrics-context-compact"');
+    expect(source).toContain('width="metrics-trace-id"');
+    expect(source).not.toContain('className="w-[220px]"');
+    expect(source).not.toContain('className="w-[160px]"');
+    expect(source).not.toContain('className="min-w-[220px] max-w-[360px] flex-1 font-mono"');
+    expect(source).not.toContain("import { Input } from '@/components/ui/input'");
+    expect(source).not.toContain('const inputClass =');
+    expect(source).not.toContain('className={`${inputClass}');
     expect(source).toContain('data-otlp-metrics-aggregation-select="true"');
+    expect(source).toContain('data-otlp-metrics-aggregation-select-owner="hertzbeat-ui-select"');
+    expect(source).toContain('width="metrics-aggregation"');
+    expect(source).toContain('data-otlp-metrics-aggregation-option');
     expect(source).toContain('data-otlp-metrics-group-by-select="true"');
+    expect(source).toContain('data-otlp-metrics-group-by-select-owner="hertzbeat-ui-select"');
+    expect(source).toContain('width="metrics-group-by"');
+    expect(source).toContain('data-otlp-metrics-group-by-option');
+    expect(source).toContain('HzSelect');
+    expect(source).not.toContain("import { Select } from '@/components/ui/select'");
+    expect(source).not.toContain('containerClassName="w-[124px]"');
+    expect(source).not.toContain('containerClassName="w-[132px]"');
+    expect(source).not.toContain('className="w-[124px]"');
+    expect(source).not.toContain('className="w-[132px]"');
+    expect(source).toContain('HzQueryActionGroup');
+    expect(source).toContain('HzButton');
+    expect(source).toContain('data-otlp-metrics-query-action-group="shared-query-action-group"');
+    expect(source).toContain('data-otlp-metrics-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(source).toContain('data-otlp-metrics-query-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(source).toContain('data-otlp-metrics-reset-action="true"');
+    expect(source).not.toContain('const primaryActionClass =');
+    expect(source).not.toContain('className={primaryActionClass}');
+    expect(source).not.toContain('<Play className="h-4 w-4" aria-hidden="true" />');
+    expect(source).not.toContain('<RotateCcw className="h-4 w-4" aria-hidden="true" />');
+    expect(source).not.toContain('<section data-otlp-metrics-query-bar="cold-query-row" className={`${panelClass} px-4 py-3`}>');
     expect(source).toContain('data-otlp-metrics-header-topbar="time-context"');
+    expect(source).toContain('data-otlp-metrics-header-topbar-owner="hertzbeat-ui-workbench-layout"');
     expect(source).toContain('data-otlp-metrics-page-shell="flat-direct-stack"');
     expect(source).toContain('data-otlp-metrics-page-shell-layer="removed"');
     expect(source).toContain('data-otlp-metrics-page-stack="direct-panels"');
@@ -344,12 +444,41 @@ describe('otlp metrics page', () => {
     expect(source).not.toContain('className="flex w-full max-w-none flex-col gap-3 px-3 py-3"');
     expect(source).not.toContain('w-[calc(100%+2rem)]');
     expect(source).not.toContain('-mx-4');
-    expect(source).toContain('className="flex min-h-[calc(100vh-56px)] flex-col gap-3 bg-[#07090b] px-3 pb-3 pt-0 text-[#e8edf5]"');
-    expect(source).toContain('pt-0');
+    expect(source).not.toContain('className="flex min-h-[calc(100vh-56px)] flex-col gap-3 bg-[#07090b] px-3 pb-3 pt-0 text-[#e8edf5]"');
+    expect(source).toContain('HzSignalWorkbenchShell');
+    expect(source).toContain('data-otlp-metrics-shell-owner="hertzbeat-ui-signal-workbench-shell"');
+    expect(source).toContain('layout="metrics-workbench"');
+    expect(source).toContain('data-otlp-metrics-header="cold-compact-header"');
+    expect(source).toContain('data-otlp-metrics-header-owner="hertzbeat-ui-panel-surface"');
     expect(source).toContain('data-otlp-metrics-header-layout="compact-title-with-aligned-toolbar"');
+    expect(source).toContain('data-otlp-metrics-header-layout-frame="compact-title-with-aligned-toolbar"');
+    expect(source).toContain('data-otlp-metrics-header-layout-frame-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="metrics-header"');
+    expect(source).not.toContain('className="grid min-w-0 gap-3 xl:grid-cols-[minmax(280px,1fr)_minmax(780px,auto)] xl:items-start"');
     expect(source).toContain('data-otlp-metrics-title-block="operator-context"');
+    expect(source).toContain('data-otlp-metrics-title-block-owner="hertzbeat-ui-workbench-header-copy"');
+    expect(source).toContain('HzWorkbenchHeaderCopy');
+    expect(source).toContain('density="compact"');
+    expect(source).toContain("eyebrow={t('otlp.metrics.header.kicker')}");
+    expect(source).toContain("title={t('otlp.metrics.title')}");
     expect(source).toContain('data-otlp-metrics-header-context-strip="applied-query-facts"');
+    expect(source).toContain('data-otlp-metrics-header-context-strip-owner="hertzbeat-ui-toolbar-chips"');
+    expect(source).toContain('spacing="top-3"');
+    expect(source).toContain('data-otlp-metrics-header-context-pill="applied-query-fact"');
+    expect(source).toContain('data-otlp-metrics-header-context-pill-owner="hertzbeat-ui-status-badge"');
     expect(source).toContain('headerContextPills.map');
+    expect(source).toContain('<HzChipGroup');
+    expect(source).toContain('<HzStatusBadge');
+    expect(source).toContain('layout="context-pill"');
+    expect(source).toContain('label={pill.label}');
+    expect(source).toContain('value={pill.value}');
+    expect(source).not.toContain('data-otlp-metrics-header-context-strip-owner="hertzbeat-ui-toolbar-chips"\n                        className="mt-3"');
+    expect(source).not.toContain("<p className=\"mb-2 text-[12px] font-semibold tracking-[0.08em] text-[#8792a5]\">{t('otlp.metrics.header.kicker')}</p>");
+    expect(source).not.toContain("<h1 className=\"text-[26px] font-semibold tracking-normal text-[#f4f7fb]\">{t('otlp.metrics.title')}</h1>");
+    expect(source).not.toContain('className="inline-flex h-7 max-w-[220px] items-center gap-2 rounded-[3px] border border-[#252b35] bg-[#10141b] px-2.5 text-[11px]"');
+    expect(source).not.toContain('className="max-w-[220px] gap-2"');
+    expect(source).not.toContain('<span className="shrink-0 font-semibold text-[#7f8a9d]">{pill.label}</span>');
+    expect(source).not.toContain('<span className="truncate font-semibold text-[#dbe5f3]">{pill.value}</span>');
     expect(source).not.toContain('data-otlp-metrics-header-copy="operator-query-copy"');
     expect(source).not.toContain('查询指标序列,按服务、命名空间、环境或标签分组。');
     expect(source).not.toContain('按当前查询条件展示');
@@ -357,12 +486,34 @@ describe('otlp metrics page', () => {
     expect(source).not.toContain('按 OTLP 上下文、时间范围和指标表达式查看序列,并把采集集群、监控模板和阈值规则接到同一条排障链路里。');
     expect(source).not.toContain('xl:grid-cols-[minmax(0,1fr)_auto]');
     expect(source).toContain('data-otlp-metrics-time-toolbar="top-right-corner"');
+    expect(source).toContain('data-otlp-metrics-time-toolbar-owner="hertzbeat-ui-workbench-layout"');
     expect(source).toContain('data-otlp-metrics-toolbar-stack="same-width-time-actions"');
+    expect(source).toContain('variant="header-toolbar-slot"');
+    expect(source).toContain('variant="time-toolbar"');
+    expect(source).not.toContain('data-otlp-metrics-toolbar-stack="same-width-time-actions"\n                      className="justify-end"');
+    expect(source).toContain('HzActionGroup');
+    expect(source).toContain('HzButtonLink');
+    expect(source).toContain('HzButtonIcon');
+    expect(source).toContain('data-otlp-metrics-return-action-group-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-otlp-metrics-return-action="true"');
+    expect(source).toContain('data-otlp-metrics-header-action="return-source"');
+    expect(source).toContain('data-otlp-metrics-header-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(source).toContain("t('otlp.metrics.route.action.return-source')");
     expect(source).not.toContain('className="ml-auto flex min-w-[360px] flex-col items-end gap-2"');
+    expect(source).not.toContain('className="flex min-w-0 justify-end xl:justify-self-end"');
+    expect(source).not.toContain('className="flex w-full max-w-[1120px] flex-wrap justify-end gap-2"');
+    expect(source).not.toContain('className="flex w-full justify-end"');
     expect(source).toContain('data-otlp-metrics-time-control-placement="top-right"');
+    expect(source).toContain('data-otlp-metrics-time-control-owner="hertzbeat-ui-control-stack"');
+    expect(source).toContain('HzControlStack');
+    expect(source).toContain('layout="end-inline"');
+    expect(source).toContain('data-otlp-metrics-time-range-control-owner="hertzbeat-shared-time-range-control"');
+    expect(source).not.toContain('variant="narrow-rail"\n                          className="justify-end"');
     expect(source).toContain('data-otlp-metrics-time-control-visual="narrow-top-right-rail"');
     expect(source).toContain('data-otlp-metrics-time-control-fit="no-clipping"');
     expect(source).toContain('data-otlp-metrics-header-actions="removed"');
+    expect(source).not.toContain('const panelClass =');
+    expect(source).not.toContain('className={`${panelClass} px-4 py-3`}');
     expect(source).not.toContain("'border-[var(--ops-primary)] px-3'");
     expect(source).not.toContain('data-otlp-metrics-action-row="aligned-context-actions"');
     expect(source).not.toContain('返回 OTLP 接入');
@@ -385,31 +536,85 @@ describe('otlp metrics page', () => {
     expect(source).toContain('sanitizeTimeContext');
     expect(source).toContain('resolveTimeContextBounds(timeContext)');
     expect(source).toContain('data-otlp-metrics-chart-band="cold-chart-band"');
+    expect(source).toContain('data-otlp-metrics-chart-band-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('padding="chart"');
     expect(source).toContain('data-otlp-metrics-chart-layout="wide-series-chart-with-compact-meta"');
+    expect(source).toContain('data-otlp-metrics-chart-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="chart-stack"');
+    expect(source).not.toContain('className="grid items-start gap-3"');
     expect(source).toContain('data-otlp-metrics-series-mode="entity-series-set"');
+    expect(source).toContain('data-otlp-metrics-chart-header-layout="trend-toolbar"');
+    expect(source).toContain('data-otlp-metrics-chart-header-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="metrics-chart-toolbar"');
+    expect(source).not.toContain('className="mb-3 text-[12px] text-[#8792a5]"');
+    expect(source).toContain('data-otlp-metrics-chart-title-label="shared-panel-title-label"');
+    expect(source).toContain('data-otlp-metrics-chart-title-label-owner="hertzbeat-ui-panel-title-label"');
+    expect(source).toContain('data-otlp-metrics-chart-toolbar-actions="shared-toolbar-actions"');
+    expect(source).toContain('data-otlp-metrics-chart-toolbar-actions-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('layout="end-wrap"');
+    expect(source).not.toContain('<span className="inline-flex min-w-0 flex-wrap items-center justify-end gap-2">');
+    expect(source).not.toContain('<span className="inline-flex items-center gap-2 font-semibold text-[#c6cfdd]">');
+    expect(source).not.toContain('<BarChart3 className="h-4 w-4" aria-hidden="true" />');
     expect(source).toContain('const hasMetricSeries = metricSeries.length > 0');
     expect(source).toContain('{hasMetricSeries ? (');
     expect(source).toContain('data-otlp-metrics-chart-meta-row="compact-real-facts"');
+    expect(source).toContain('data-otlp-metrics-chart-meta-row-owner="hertzbeat-ui-toolbar-chips"');
+    expect(source).toContain('align="end"');
+    expect(source).not.toContain('data-otlp-metrics-chart-meta-row-owner="hertzbeat-ui-toolbar-chips"\n                            density="compact"\n                            className="justify-end"');
     expect(source).toContain('data-otlp-metrics-chart-meta-fact="true"');
+    expect(source).toContain('data-otlp-metrics-chart-meta-fact-owner="hertzbeat-ui-status-badge"');
+    expect(source).toContain('layout="metric-fact"');
+    expect(source).toContain('label={fact.label}');
+    expect(source).toContain('value={fact.value}');
+    expect(source).not.toContain('<span className="truncate text-[10px] font-semibold text-[#7f8a9d]">{fact.label}</span>');
+    expect(source).not.toContain('<span className="truncate text-[11px] font-semibold text-[#cfd8e6]">{fact.value}</span>');
     expect(source).toContain('facts.map');
+    expect(source).toContain('HzChipGroup');
     expect(source).not.toContain('data-otlp-metrics-facts-grid="inline-summary-strip"');
     expect(source).toContain('data-otlp-metrics-empty-state="honest-no-series"');
     expect(source).toContain('data-otlp-metrics-empty-state-context="applied-query-visible"');
     expect(source).toContain('data-otlp-metrics-chart-panel="series-set-trend"');
+    expect(source).toContain('data-otlp-metrics-chart-panel-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('data-otlp-metrics-chart-panel-variant-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('padding="chart-inner"');
+    expect(source).toContain('variant="chart-inner"');
+    expect(source).toContain('HzPanelSurface');
+    expect(source).not.toContain('className="bg-[#10141b] px-3 py-3 shadow-none"');
+    expect(source).not.toContain('className="mb-3 flex flex-wrap items-center justify-between gap-2 text-[12px] text-[#8792a5]"');
+    expect(source).not.toContain('className="min-w-0 rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3"');
+    expect(source).not.toContain('data-otlp-metrics-chart-band="cold-chart-band" className={`${panelClass} px-4 py-4`}');
     expect(source).not.toContain('data-otlp-metrics-chart-layout="fact-stack-with-wide-trend"');
     expect(source).not.toContain('data-otlp-metrics-facts-grid="compact-summary-stack"');
     expect(source).not.toContain('className="grid items-start gap-3 xl:grid-cols-[320px_minmax(0,1fr)]"');
     expect(source).not.toContain('repeat(4,minmax(0,160px))_minmax(0,1fr)');
+    expect(source).not.toContain('className="inline-flex h-6 min-w-0 max-w-[180px] items-center gap-1 rounded-[3px] border border-[#252b35] bg-[#0d1015] px-2"');
     expect(source).toContain('data-otlp-metrics-chart-datazoom-state="local-observation"');
     expect(source).toContain('data-otlp-metrics-chart-datazoom-preserve="preserved"');
     expect(source).toContain('data-otlp-metrics-chart-zoom-apply="local-to-query-time"');
+    expect(source).toContain('data-otlp-metrics-chart-zoom-apply-owner="hertzbeat-ui-button"');
     expect(source).toContain('data-otlp-metrics-chart-zoom-apply-state={');
+    expect(source).not.toContain('className="h-7 px-2"');
     expect(source).toContain('metricsChartZoomDraftLabel');
     expect(source).toContain('data-otlp-metrics-chart-zoom-draft="pending-query-time"');
+    expect(source).toContain('data-otlp-metrics-chart-zoom-draft-owner="hertzbeat-ui-status-badge"');
     expect(source).toContain('data-otlp-metrics-chart-zoom-draft-state="pending"');
+    expect(source).toContain('layout="zoom-draft"');
+    expect(source).toContain("label={t('time.context.zoom.draft')}");
+    expect(source).toContain('value={metricsChartZoomDraftLabel}');
+    expect(source).toContain('valueFont="mono"');
+    expect(source).not.toContain("<span className=\"shrink-0 text-[#7f8a9d]\">{t('time.context.zoom.draft')}</span>");
+    expect(source).not.toContain('<span className="min-w-0 truncate font-mono">{metricsChartZoomDraftLabel}</span>');
+    expect(source).toContain('data-otlp-metrics-chart-series-count="toolbar-count"');
+    expect(source).toContain('data-otlp-metrics-chart-series-count-owner="hertzbeat-ui-status-badge"');
+    expect(source).not.toContain('className="inline-flex h-7 items-center justify-center rounded-[3px] border border-[#2b3039] bg-transparent px-2 text-[11px] font-semibold text-[#9ca7ba] transition-colors enabled:hover:border-[#3b4454] enabled:hover:bg-[#151821] enabled:hover:text-[#e6edf7] disabled:cursor-not-allowed disabled:opacity-45"');
+    expect(source).not.toContain('className="inline-flex h-7 max-w-[340px] items-center gap-1.5 overflow-hidden rounded-[3px] border border-[#344052] bg-[#121823] px-2 text-[11px] font-semibold text-[#cfd8e6]"');
     expect(source).toContain('formatEpochMillisDraft');
     expect(source).toContain('buildMetricsChartOption(metricSeries)');
     expect(source).toContain('height={300}');
+    expect(source).toContain('edge="metrics-chart"');
+    expect(source).toContain('data-otlp-metrics-echarts-edge="shared-metrics-chart"');
+    expect(source).toContain('data-otlp-metrics-echarts-edge-owner="hertzbeat-ui-echarts-panel"');
+    expect(source).not.toContain('className="rounded-none border-x-0 border-y border-[#252b35] bg-transparent"');
     expect(source).toContain('buildMetricsDataZoomTimeContext(');
     expect(source).toContain('preserveDataZoom');
     expect(source).toContain('handleMetricsChartZoomChange(nextZoom);');
@@ -417,26 +622,136 @@ describe('otlp metrics page', () => {
     expect(source).toContain('buildMetricTrendBars');
     expect(source).toContain('data-otlp-metrics-trend-bar={');
     expect(source).toContain("'real-series-point'");
+    expect(source).toContain('data-otlp-metrics-trend-bar-owner="hertzbeat-ui-trend-bar"');
+    expect(source).toContain('heightPct={series.heightPct}');
+    expect(source).toContain('HzTrendBar');
+    expect(source).not.toContain('className="min-w-0 flex-1 rounded-t-[3px] border border-[#2f3b4d] bg-[#182232]"');
+    expect(source).not.toContain('style={{ height: `${series.heightPct}%` }}');
+    expect(source).toContain('HzDataMetaText');
+    expect(source).toContain('data-otlp-metrics-trend-sample-helper="real-sample-count"');
+    expect(source).toContain('data-otlp-metrics-trend-sample-helper-owner="hertzbeat-ui-data-meta-text"');
+    expect(source).toContain('spacing="trend-helper"');
+    expect(source).not.toContain('<div className="mt-2 text-[11px] text-[#6d7788]">');
     expect(source).toContain('data-otlp-metrics-trend-empty="no-real-series"');
+    expect(source).toContain('data-otlp-metrics-trend-empty-owner="hertzbeat-ui-state-notice"');
+    expect(source).toContain('frame="trend-empty"');
+    expect(source).toContain('data-otlp-metrics-trend-frame="shared-compact-bars"');
+    expect(source).toContain('data-otlp-metrics-trend-frame-owner="hertzbeat-ui-trend-frame"');
+    expect(source).not.toContain('<div className="flex h-16 items-end gap-1.5">');
+    expect(source).not.toContain('className="flex h-full min-w-0 flex-1 flex-col justify-center border-dashed border-[#2a303a] bg-[#0c1016] text-center"');
+    expect(source).toContain('HzStateNotice');
     expect(source).not.toContain('empty-series-placeholder');
     expect(source).not.toContain('Array.from({ length: 10 }');
     expect(source).toContain('const [selectedSeriesKey, setSelectedSeriesKey]');
     expect(source).toContain('selectedMetricSeries');
     expect(source).toContain('buildMetricsHandoffLinks(mergedData, query, routeContext, selectedMetricSeries)');
-    expect(source).toContain('data-otlp-metrics-series-row="selectable-series"');
-    expect(source).toContain('data-otlp-metrics-series-row-selected={isSelectedSeries ?');
+    expect(source).toContain('metricSeriesTableRows');
+    expect(source).toContain('HzDataTable');
+    expect(source).toContain('HzDataCellText');
+    expect(source).toContain('HzEmptyState');
+    expect(source).toContain('data-otlp-metrics-workbench-grid-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain("variant={hasMetricSeries ? 'metrics-series-detail' : 'metrics-series-only'}");
+    expect(source).not.toContain("className={hasMetricSeries ? 'grid items-start gap-4 xl:grid-cols-[minmax(0,1fr)_320px]' : 'grid items-start gap-4'}");
+    expect(source).toContain('data-otlp-metrics-series-table-owner="hertzbeat-ui-data-table"');
+    expect(source).toContain('data-otlp-metrics-series-table-panel-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('data-otlp-metrics-series-table-header="shared-panel-header"');
+    expect(source).toContain('data-otlp-metrics-series-table-header-owner="hertzbeat-ui-panel-header"');
+    expect(source).toContain('chrome="transparent-topless"');
+    expect(source).not.toContain('className="border-x-0 border-t-0 bg-transparent"');
+    expect(source).toContain('title="序列集合"');
+    expect(source).toContain('data-otlp-metrics-series-table-summary-owner="hertzbeat-ui-status-badge"');
+    expect(source).toContain('data-otlp-metrics-series-table-empty-state="shared-empty-state"');
+    expect(source).toContain('data-otlp-metrics-series-table-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(source).toContain('data-otlp-metrics-series-set-summary-owner="hertzbeat-ui-stat-strip"');
+    expect(source).toContain('data-otlp-metrics-series-set-summary-section="shared-panel-section"');
+    expect(source).toContain('data-otlp-metrics-series-set-summary-section-owner="hertzbeat-ui-panel-section"');
+    expect(source).toContain('data-otlp-metrics-series-set-summary-strip="shared-stat-strip"');
+    expect(source).toContain('data-otlp-metrics-series-set-summary-strip-owner="hertzbeat-ui-stat-strip"');
+    expect(source).toContain('frame="panel-inset"');
+    expect(source).toContain('spacing="compact"');
+    expect(source).toContain('data-otlp-metrics-series-set-summary-cell={row.label}');
+    expect(source).toContain('data-otlp-metrics-series-set-summary-cell-owner="hertzbeat-ui-stat-cell"');
+    expect(source).toContain('frame="inset"');
+    expect(source).not.toContain('className="border-[#252b35] bg-[#0d1015]"');
+    expect(source).not.toContain('<div className="border-b border-[#252b35] px-4 py-3">');
+    expect(source).not.toContain('className="rounded-[3px] border border-[#252b35] bg-[#10141b] p-1"');
+    expect(source).not.toContain('className="gap-1"');
+    expect(source).toContain('data-otlp-metrics-series-data-table="shared-data-table"');
+    expect(source).toContain('data-otlp-metrics-series-data-table-owner="hertzbeat-ui-data-table"');
+    expect(source).toContain("'data-otlp-metrics-series-row': 'selectable-series'");
+    expect(source).toContain("'data-otlp-metrics-series-row-selected': selectedMetricSeries?.key === row.series?.key ? 'true' : 'false'");
     expect(source).toContain('data-otlp-metrics-series-entity="true"');
-    expect(source).toContain('setSelectedSeriesKey(series.key)');
+    expect(source).toContain('data-otlp-metrics-series-entity-owner="hertzbeat-ui-data-cell-stack"');
+    expect(source).toContain('width="metrics-entity"');
+    expect(source).toContain('data-otlp-metrics-series-entity-label-owner="hertzbeat-ui-data-cell-text"');
+    expect(source).toContain('data-otlp-metrics-series-entity-meta-owner="hertzbeat-ui-data-cell-text"');
+    expect(source).toContain('data-otlp-metrics-series-entity-state={row.entityState}');
+    expect(source).not.toContain('<span data-otlp-metrics-series-entity="true" className="block min-w-[140px]">');
+    expect(source).toContain('tone="strong"');
+    expect(source).toContain('weight="semibold"');
+    expect(source).toContain('casing="plain"');
+    expect(source).toContain("tone={row.entityState === 'present' ? 'success' : 'muted'}");
+    expect(source).toContain('data-otlp-metrics-series-latest-owner="hertzbeat-ui-data-cell-text"');
+    expect(source).toContain('font="mono"');
+    expect(source).toContain('tone="bright"');
+    expect(source).not.toContain('className="font-semibold text-[#dbe5f3]"');
+    expect(source).not.toContain("className={row.entityState === 'present' ? 'normal-case tracking-normal text-[#75c795]' : 'normal-case tracking-normal text-[#8b94a4]'}");
+    expect(source).not.toContain('className="font-mono text-[#e6edf7]"');
+    expect(source).toContain('setSelectedSeriesKey(row.series.key)');
     expect(source).toContain('buildMetricSeriesContextRows(selectedMetricSeries, t)');
-    expect(source).toContain('buildMetricSeriesEvidenceRows(selectedMetricSeries, formatTime)');
-    expect(source).toContain('buildMetricSeriesLinkedRecordRows(selectedMetricSeries, handoffLinks)');
+    expect(source).toContain('buildMetricSeriesEvidenceRows(selectedMetricSeries, formatTime, t)');
+    expect(source).toContain('buildMetricSeriesLinkedRecordRows(selectedMetricSeries, handoffLinks, t)');
+    expect(source).toContain('linkedRecordHandoffTargets');
     expect(source).toContain('buildMetricSeriesAttributionDiagnostics(selectedMetricSeries, t)');
+    expect(source).toContain('HzCollapsibleSection');
+    expect(source).toContain('HzContextHandoff');
+    expect(source).toContain('HzDetailRows');
+    expect(source).toContain('HzPanelHeader');
+    expect(source).toContain('HzStatStrip');
+    expect(source).toContain('HzStatCell');
+    expect(source).toContain('HzAttributeDiagnostics');
+    expect(source).toContain('metricsDetailContextRows');
+    expect(source).toContain('data-otlp-metrics-detail-panel-header="shared-panel-header"');
+    expect(source).toContain('data-otlp-metrics-detail-panel-header-owner="hertzbeat-ui-panel-header"');
+    expect(source).toContain('chrome="transparent"');
+    expect(source).not.toContain('className="bg-transparent"');
+    expect(source).toContain('data-otlp-metrics-detail-panel-stickiness-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('stickiness="top-4"');
+    expect(source).not.toContain('className="xl:sticky xl:top-4 xl:self-start"');
+    expect(source).toContain('data-otlp-metrics-detail-summary-stats="shared-stat-strip"');
+    expect(source).toContain('data-otlp-metrics-detail-summary-stats-owner="hertzbeat-ui-stat-strip"');
+    expect(source).toContain('data-otlp-metrics-detail-summary-stat-owner="hertzbeat-ui-stat-cell"');
+    expect(source).toContain('frame="panel-solid"');
+    expect(source).toContain('frame="flush"');
+    expect(source).not.toContain('className="rounded-[3px] border border-[#252b35] bg-[#252b35] p-1"');
+    expect(source).not.toContain('density="compact"\n                          className="border-0"');
+    expect(source).toContain('data-otlp-metrics-detail-context-rows="shared-detail-rows"');
+    expect(source).toContain('data-otlp-metrics-detail-context-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('offset="top"');
+    expect(source).not.toContain('data-otlp-metrics-detail-context-rows-owner="hertzbeat-ui-detail-rows"\n                      className="mt-3"');
+    expect(source).toContain('selectedSeriesContextDetailRows');
+    expect(source).toContain('selectedSeriesEvidenceDetailRows');
+    expect(source).toContain('entityContextDetailRows');
     expect(source).toContain('data-otlp-metrics-selected-series-context="selected-series-attribution"');
+    expect(source).toContain('data-otlp-metrics-selected-series-context-owner="hertzbeat-ui-detail-rows"');
     expect(source).toContain('data-otlp-metrics-selected-series-evidence="real-sample-evidence"');
+    expect(source).toContain('data-otlp-metrics-selected-series-evidence-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('boundary="top"');
+    expect(source).not.toContain('className="mt-3 border-t border-[#252b35] pt-3"');
     expect(source).toContain('data-otlp-metrics-linked-record-summary="log-trace-alert-links"');
+    expect(source).toContain('data-otlp-metrics-linked-record-summary-owner="hertzbeat-ui-collapsible-section"');
+    expect(source).toContain('surface="inset"');
     expect(source).toContain('data-otlp-metrics-linked-record-action={row.key}');
+    expect(source).toContain('data-otlp-metrics-linked-record-handoff="shared-context-handoff"');
+    expect(source).toContain('data-otlp-metrics-linked-record-handoff-owner="hertzbeat-ui-context-handoff"');
+    expect(source).toContain('frame="flush"');
+    expect(source).not.toContain('className="border-0"');
     expect(source).toContain('data-otlp-metrics-attribution-diagnostics="hertzbeat-attribute-diagnostics"');
-    expect(source).toContain('data-otlp-metrics-attribution-diagnostic-state={row.state}');
+    expect(source).toContain('data-otlp-metrics-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(source).toContain('frame="embedded"');
+    expect(source).not.toContain('className="rounded-none border-0 bg-transparent"');
+    expect(source).toContain('attributionDiagnosticRows');
+    expect(source).toContain("'data-otlp-metrics-attribution-diagnostic-state': row.state");
     expect(source).toContain('当前选中序列');
     expect(source).toContain('指标证据');
     expect(source).toContain('关联记录');
@@ -446,33 +761,91 @@ describe('otlp metrics page', () => {
     expect(source).toContain('归因诊断');
     expect(source).toContain('hertzbeat.entity_id');
     expect(source).toContain('采集模板');
+    expect(source).not.toContain('<p className="text-[11px] font-semibold text-[#8792a5]">当前选中序列</p>');
+    expect(source).not.toContain('<p className="text-[11px] font-semibold text-[#8792a5]">指标证据</p>');
+    expect(source).not.toContain('grid grid-cols-2 gap-px overflow-hidden rounded-[3px] border border-[#252b35] bg-[#252b35]');
+    expect(source).not.toContain('className="col-span-2 min-w-0 bg-[#10141b] px-3 py-2"');
+    expect(source).not.toContain('className="grid grid-cols-[minmax(0,1fr)_52px] gap-2 text-[11px]"');
+    expect(source).not.toContain("row.state === 'present' ? '已提供' : '缺失'}</span>");
+    expect(source).not.toContain('className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]"');
+    expect(source).not.toContain('hover:border-[#344052] hover:bg-[#121823]');
+    expect(source).not.toContain('<table className="w-full border-collapse text-left text-[12px]">');
+    expect(source).not.toContain('className={`cursor-pointer border-b border-[#1f2530] last:border-b-0');
+    expect(source).not.toContain('className="mt-3 grid gap-px overflow-hidden rounded-[3px] border border-[#252b35] bg-[#252b35] sm:grid-cols-2 xl:grid-cols-5"');
+    expect(source).not.toContain('className="min-w-0 bg-[#10141b] px-2.5 py-2"');
+    expect(source).not.toContain('className="mx-auto flex max-w-[280px] flex-col items-center"');
+    expect(source).not.toContain('<Database className="h-8 w-8 text-[#7d8798]" aria-hidden="true" />');
+    expect(source).not.toContain('<summary className="flex cursor-pointer list-none items-center justify-between gap-2 px-2.5 py-2 text-[11px] font-semibold text-[#8792a5]">');
+    expect(source).not.toContain('className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#9ca7ba]"');
+    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#8792a5]">详情面板</p>');
+    expect(source).not.toContain('<h2 className="mt-2 truncate text-[18px] font-semibold text-[#f2f6fb]">{firstSeries.title}</h2>');
+    expect(source).not.toContain('className="flex h-full min-w-0 flex-1 items-center justify-center rounded-[3px] border border-dashed border-[#2a303a] bg-[#0c1016] px-3 text-center"');
+    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#a7b0bf]">暂无指标趋势</p>');
+    expect(source).not.toContain('<span>{workbenchState.seriesCountLabel}</span>');
     expect(source).toContain('data-otlp-metrics-workbench-grid="series-detail-split"');
-    expect(source).toContain("'grid items-start gap-4 xl:grid-cols-[minmax(0,1fr)_320px]'");
+    expect(source).toContain('data-otlp-metrics-workbench-grid-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain("variant={hasMetricSeries ? 'metrics-series-detail' : 'metrics-series-only'}");
+    expect(source).not.toContain("'grid items-start gap-4 xl:grid-cols-[minmax(0,1fr)_320px]'");
     expect(source).not.toContain('xl:grid-cols-[minmax(0,1fr)_360px]');
     expect(source).not.toContain('2xl:grid-cols-[minmax(0,1fr)_330px]');
     expect(source).toContain('data-otlp-metrics-series-table="cold-dense-metric-list"');
     expect(source).toContain('data-otlp-metrics-series-table-mode="service-entity-series-set"');
     expect(source).toContain('data-otlp-metrics-series-table-density="primary-scan"');
+    expect(source).toContain('data-otlp-metrics-series-table-panel-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('data-otlp-metrics-series-table-summary-owner="hertzbeat-ui-status-badge"');
+    expect(source).not.toContain('className={`${panelClass} min-w-0 overflow-hidden`}');
+    expect(source).toContain('data-otlp-metrics-series-set-summary-owner="hertzbeat-ui-stat-strip"');
     expect(source).toContain('data-otlp-metrics-series-set-summary="service-entity-scope"');
     expect(source).toContain('seriesSetScopeRows.map');
     expect(source).toContain('序列集合');
+    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#8792a5]">序列集合</p>');
+    expect(source).not.toContain('className="flex min-w-0 items-center justify-between gap-3"');
     expect(source).not.toContain('最近序列');
     expect(source).toContain('data-otlp-metrics-detail-panel="cold-detail-panel"');
+    expect(source).toContain('data-otlp-metrics-detail-panel-owner="hertzbeat-ui-panel-surface"');
     expect(source).toContain('data-otlp-metrics-detail-panel-priority="secondary-inspector"');
+    expect(source).toContain('data-otlp-metrics-detail-panel-header-owner="hertzbeat-ui-panel-header"');
     expect(source).toContain('data-otlp-metrics-detail-panel-body="compact-evidence-stack"');
+    expect(source).toContain('data-otlp-metrics-detail-panel-body-owner="hertzbeat-ui-panel-section"');
+    expect(source).toContain('divider="none"');
+    expect(source).not.toContain('<div data-otlp-metrics-detail-panel-body="compact-evidence-stack" className="px-4 py-3">');
     expect(source).toContain('{hasMetricSeries && selectedMetricSeries ? (');
     expect(source).toContain('data-otlp-metrics-detail-panel-empty="suppressed-until-real-series"');
+    expect(source).toContain('data-otlp-metrics-detail-panel-empty-owner="hertzbeat-ui-assistive-marker"');
+    expect(source).toContain('HzAssistiveMarker');
+    expect(source).not.toContain('<span data-otlp-metrics-detail-panel-empty="suppressed-until-real-series" className="sr-only" />');
+    expect(source).not.toContain('className={`${panelClass} min-w-0 overflow-hidden xl:sticky xl:top-4 xl:self-start`}');
     expect(source).toContain('data-otlp-metrics-secondary-context="collapsible-evidence-diagnostics"');
+    expect(source).toContain('data-otlp-metrics-secondary-context-owner="hertzbeat-ui-panel-section"');
+    expect(source).toContain('spacing="stack-2"');
+    expect(source).not.toContain('<div data-otlp-metrics-secondary-context="collapsible-evidence-diagnostics" className="space-y-2 border-t border-[#252b35] px-4 py-3">');
     expect(source).toContain('data-otlp-metrics-linked-record-summary-panel="collapsible"');
     expect(source).toContain('data-otlp-metrics-attribution-diagnostics-panel="collapsible"');
+    expect(source).toContain('data-otlp-metrics-attribution-diagnostics-panel-owner="hertzbeat-ui-collapsible-section"');
     expect(source).toContain('data-otlp-metrics-entity-context-panel="collapsible"');
-    expect(source).toContain('<details');
+    expect(source).toContain('data-otlp-metrics-entity-context-panel-owner="hertzbeat-ui-collapsible-section"');
+    expect(source).toContain('data-otlp-metrics-entity-context-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('padding="compact-y"');
+    expect(source).not.toContain('className="py-2"');
+    expect(source.match(/className="border-\[#252b35\] bg-\[#0d1015\]"/g) ?? []).toHaveLength(0);
+    expect(source).not.toContain('<details');
     expect(source).toContain('data-otlp-metrics-handoff-actions="compact-context-actions"');
+    expect(source).toContain('data-otlp-metrics-handoff-action-section="shared-panel-section"');
+    expect(source).toContain('data-otlp-metrics-handoff-action-section-owner="hertzbeat-ui-panel-section"');
+    expect(source).toContain('data-otlp-metrics-handoff-actions-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('layout="grid-2"');
+    expect(source).toContain('layout="full"');
+    expect(source).not.toContain('data-otlp-metrics-entity-action="true" className="w-full px-2"');
+    expect(source).not.toContain('data-otlp-metrics-alert-handling-action="true" className="w-full px-2"');
+    expect(source).not.toContain('data-otlp-metrics-logs-action="true" className="w-full px-2"');
+    expect(source).not.toContain('data-otlp-metrics-traces-action="true" className="w-full px-2"');
+    expect(source).not.toContain('data-otlp-metrics-entities-action="true" className="w-full px-2"');
+    expect(source).not.toContain('className="grid grid-cols-2 gap-2 border-t border-[#252b35] px-4 py-3"');
     expect(source).toContain('data-otlp-metrics-entity-context="hertzbeat-signal-entity-context"');
     expect(source).toContain('aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"');
     expect(source).not.toContain('data-otlp-metrics-hertzbeat-loop="collector-template-alert-loop"');
-    expect(source).toContain('指标工作台');
-    expect(source).toContain('运行查询');
+    expect(source).toContain("t('otlp.metrics.title')");
+    expect(source).toContain("t('otlp.metrics.query.run')");
     expect(source).not.toContain('data-otlp-metrics-action-row="aligned-context-actions"');
     expect(source).not.toContain('返回 OTLP 接入');
     expect(source).not.toContain('href="/setting/collector"');
@@ -494,8 +867,20 @@ describe('otlp metrics page', () => {
     expect(source).toContain('data-otlp-metrics-alert-handling-action="true"');
     expect(source).toContain('data-otlp-metrics-logs-action="true"');
     expect(source).toContain('data-otlp-metrics-traces-action="true"');
+    expect(source).toContain('data-otlp-metrics-entities-action="true"');
+    expect(source).toContain('HzButtonLink');
+    expect(source).toContain('HzDisabledActionShell');
+    expect(source).toContain('data-otlp-metrics-entity-action-disabled-shell-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(source).toContain('layout="full"');
+    expect(source).not.toContain('data-otlp-metrics-entity-action-disabled-shell-owner="hertzbeat-ui-disabled-action-shell"\n                          className="w-full"');
+    expect(source).not.toContain('const actionClass =');
+    expect(source).not.toContain('const disabledActionClass =');
+    expect(source).not.toContain('className={`${actionClass}');
+    expect(source).not.toContain('className={`${disabledActionClass}');
     expect(source).toContain("const missingEntityHandoffTitle = t('otlp.metrics.handoff.entity-disabled')");
     expect(source).toContain('data-otlp-metrics-entity-action-disabled="missing-entity-id"');
+    expect(source).toContain('size="md"\n                            layout="full"\n                            disabled');
+    expect(source).not.toContain('data-otlp-metrics-entity-action-disabled="missing-entity-id"\n                            size="md"\n                            className="w-full px-2"');
     expect(source).toContain('title={missingEntityHandoffTitle}');
     expect(source).toContain('buildOtlpMetricsRoute');
     expect(source).not.toContain('告警规则');
@@ -520,7 +905,7 @@ describe('otlp metrics page', () => {
   });
 
   it('keeps route-level actions on the cold-matte palette instead of bright blue demo buttons', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
 
     expect(source).toContain('data-otlp-metrics-run-query-action="true"');
     expect(source).not.toContain('border-[#4f6df0]');
@@ -529,7 +914,7 @@ describe('otlp metrics page', () => {
   });
 
   it('does not directly coerce query route primitives with Number in page context', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
 
     expect(source).not.toContain('Number(query.start)');
     expect(source).not.toContain('Number(query.end)');
@@ -542,6 +927,13 @@ describe('otlp metrics page', () => {
     const html = renderToStaticMarkup(<OtlpMetricsPage />);
 
     expect(html).toContain('data-otlp-metrics-route="otlp-cold-metrics-workbench"');
+    expect(html).toContain('data-hz-ui="signal-workbench-shell"');
+    expect(html).toContain('data-hz-signal-workbench-shell-owner="hertzbeat-ui-signal-workbench-shell"');
+    expect(html).toContain('data-hz-signal-workbench-shell-layout="metrics-workbench"');
+    expect(html).toContain('data-otlp-metrics-shell-owner="hertzbeat-ui-signal-workbench-shell"');
+    expect(html).toContain('data-hz-signal-workbench-shell-content="true"');
+    expect(html).toContain('data-hz-signal-workbench-shell-content-layout="metrics-workbench"');
+    expect(html).toContain('class="contents"');
     expect(html).toContain('data-otlp-metrics-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-otlp-metrics-page-shell="flat-direct-stack"');
     expect(html).toContain('data-otlp-metrics-page-shell-layer="removed"');
@@ -550,16 +942,83 @@ describe('otlp metrics page', () => {
     expect(html).not.toContain('data-otlp-metrics-page-shell-gutter="edge-to-edge-viewport"');
     expect(html).not.toContain('data-otlp-metrics-page-shell-top="flush-under-app-bar"');
     expect(html).toContain('data-otlp-metrics-query-bar="cold-query-row"');
+    expect(html).toContain('data-otlp-metrics-query-bar-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="query"');
+    expect(html).toContain('data-otlp-metrics-query-control-stack="shared-inline-controls"');
+    expect(html).toContain('data-otlp-metrics-context-control-stack="shared-inline-controls"');
+    expect(html).toContain('data-hz-control-stack-layout="inline-wrap"');
+    expect(html).toContain('data-hz-control-stack-spacing="top-2"');
     expect(html).toContain('data-otlp-metrics-time-control="shared-time-context-control"');
+    expect(html).toContain('data-otlp-metrics-time-control-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-otlp-metrics-time-range-control-owner="hertzbeat-shared-time-range-control"');
+    expect(html).toContain('data-hz-ui="control-stack"');
+    expect(html).toContain('data-hz-control-stack-layout="end-inline"');
     expect(html).toContain('data-otlp-metrics-time-control-placement="top-right"');
     expect(html).toContain('data-otlp-metrics-time-control-visual="narrow-top-right-rail"');
     expect(html).toContain('data-otlp-metrics-time-control-fit="no-clipping"');
     expect(html).toContain('data-otlp-metrics-header-topbar="time-context"');
+    expect(html).toContain('data-otlp-metrics-header-topbar-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="header-toolbar-slot"');
+    expect(html).toContain('data-otlp-metrics-header-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="query"');
     expect(html).toContain('data-otlp-metrics-header-layout="compact-title-with-aligned-toolbar"');
+    expect(html).toContain('data-otlp-metrics-header-layout-frame="compact-title-with-aligned-toolbar"');
+    expect(html).toContain('data-otlp-metrics-header-layout-frame-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="metrics-header"');
     expect(html).toContain('data-otlp-metrics-title-block="operator-context"');
+    expect(html).toContain('data-otlp-metrics-title-block-owner="hertzbeat-ui-workbench-header-copy"');
+    expect(html).toContain('data-hz-ui="workbench-header-copy"');
+    expect(html).toContain('data-hz-workbench-header-copy-density="compact"');
     expect(html).toContain('data-otlp-metrics-header-context-strip="applied-query-facts"');
+    expect(html).toContain('data-otlp-metrics-header-context-strip-owner="hertzbeat-ui-toolbar-chips"');
+    expect(html).toContain('data-otlp-metrics-header-context-pill-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-status-badge-layout="context-pill"');
+    expect(html).toContain('data-hz-status-badge-part="label"');
+    expect(html).toContain('data-hz-status-badge-part="value"');
+    expect(html).toContain('data-hz-ui="chip-group"');
+    expect(html).toContain('data-hz-chip-group-align="start"');
+    expect(html).toContain('data-hz-chip-group-density="compact"');
+    expect(html).toContain('data-hz-chip-group-spacing="top-3"');
     expect(html).not.toContain('data-otlp-metrics-header-copy="operator-query-copy"');
     expect(html).toContain('data-otlp-metrics-toolbar-stack="same-width-time-actions"');
+    expect(html).toContain('data-otlp-metrics-query-search-frame="shared-search-field-frame"');
+    expect(html).toContain('data-otlp-metrics-query-search-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-width="metrics-query"');
+    expect(html).toContain('relative min-w-[320px] max-w-[560px] flex-1');
+    expect(html).toContain('data-otlp-metrics-query-search-icon="metric-query"');
+    expect(html).toContain('data-otlp-metrics-query-search-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-hz-search-field-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-otlp-metrics-query-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-inset="search-icon"');
+    expect(html).toContain('data-hz-input-width="metrics-query-expression"');
+    expect(html).toContain('w-full font-mono');
+    expect(html).toContain('data-otlp-metrics-context-input="service-name"');
+    expect(html).toContain('data-otlp-metrics-context-input="namespace"');
+    expect(html).toContain('data-otlp-metrics-context-input="environment"');
+    expect(html).toContain('data-otlp-metrics-context-input="trace-id"');
+    expect(html).toContain('data-otlp-metrics-context-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-width="metrics-context"');
+    expect(html).toContain('data-hz-input-width="metrics-context-compact"');
+    expect(html).toContain('data-hz-input-width="metrics-trace-id"');
+    expect(html).toContain('min-w-[220px] max-w-[360px] flex-1 font-mono');
+    expect(html).toContain('data-hz-input-inset="none"');
+    expect(html).toContain('data-otlp-metrics-aggregation-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-otlp-metrics-group-by-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui="select"');
+    expect(html).toContain('data-hz-select-width="metrics-aggregation"');
+    expect(html).toContain('data-hz-select-width="metrics-group-by"');
+    expect(html).toContain('data-hz-ui="select-trigger"');
+    expect(html).toContain('data-otlp-metrics-query-action-group="shared-query-action-group"');
+    expect(html).toContain('data-otlp-metrics-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-query-action-group-kind="run-reset"');
+    expect(html).toContain('data-otlp-metrics-query-action-icon="run"');
+    expect(html).toContain('data-otlp-metrics-query-action-icon="reset"');
+    expect(html).toContain('data-otlp-metrics-query-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-otlp-metrics-reset-action="true"');
+    expect(html).toContain('data-hz-ui="button"');
+    expect(html).toContain('data-hz-ui="button-link"');
     expect(html).toContain('data-time-range-control="hertzbeat-shared"');
     expect(html).toContain('data-time-range-control-visual="grafana-like-narrow-rail"');
     expect(html).toContain('data-time-range-control-layout="nowrap-top-right-rail"');
@@ -586,13 +1045,43 @@ describe('otlp metrics page', () => {
     expect(applyButton).not.toContain('border-[var(--ops-primary)]');
     expect(html).toContain('data-otlp-metrics-chart-band="cold-chart-band"');
     expect(html).toContain('data-otlp-metrics-chart-layout="wide-series-chart-with-compact-meta"');
+    expect(html).toContain('data-otlp-metrics-chart-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="chart-stack"');
     expect(html).toContain('data-otlp-metrics-series-mode="entity-series-set"');
     expect(html).not.toContain('data-otlp-metrics-chart-meta-row="compact-real-facts"');
     expect(html).not.toContain('data-otlp-metrics-facts-grid="inline-summary-strip"');
     expect(html).toContain('data-otlp-metrics-chart-panel="series-set-trend"');
+    expect(html).toContain('data-otlp-metrics-chart-band-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="chart"');
+    expect(html).toContain('data-otlp-metrics-chart-panel-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-ui="panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-density="operator-compact"');
+    expect(html).toContain('data-otlp-metrics-chart-panel-variant-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="chart-inner"');
+    expect(html).toContain('data-hz-panel-surface-variant="chart-inner"');
+    expect(html).toContain('data-otlp-metrics-chart-header-layout="trend-toolbar"');
+    expect(html).toContain('data-otlp-metrics-chart-header-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui="workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="metrics-chart-toolbar"');
+    expect(html).toContain('mb-3 gap-2 text-[12px] text-[#8792a5] sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center');
+    expect(html).toContain('data-otlp-metrics-chart-title-label="shared-panel-title-label"');
+    expect(html).toContain('data-otlp-metrics-chart-title-label-owner="hertzbeat-ui-panel-title-label"');
+    expect(html).toContain('data-hz-ui="panel-title-label"');
+    expect(html).toContain('data-hz-panel-title-label-density="operator-compact"');
     expect(html).toContain('data-otlp-metrics-chart-datazoom-state="local-observation"');
     expect(html).toContain('data-otlp-metrics-chart-datazoom-preserve="preserved"');
+    expect(html).toContain('data-otlp-metrics-chart-series-count="toolbar-count"');
+    expect(html).toContain('data-otlp-metrics-chart-series-count-owner="hertzbeat-ui-status-badge"');
     expect(html).toContain('data-otlp-metrics-trend-empty="no-real-series"');
+    expect(html).toContain('data-otlp-metrics-trend-empty-owner="hertzbeat-ui-state-notice"');
+    expect(html).toContain('data-otlp-metrics-trend-frame="shared-compact-bars"');
+    expect(html).toContain('data-otlp-metrics-trend-frame-owner="hertzbeat-ui-trend-frame"');
+    expect(html).toContain('data-hz-ui="trend-frame"');
+    expect(html).toContain('data-hz-trend-frame-density="operator-compact"');
+    expect(html).toContain('data-hz-ui="state-notice"');
+    expect(html).toContain('data-hz-state-variant="hint"');
+    expect(html).toContain('data-hz-state-frame="trend-empty"');
+    expect(html).toContain('data-hz-state-hint-owner="hertzbeat-ui-state-notice"');
     expect(html).toContain('暂无指标趋势');
     expect(html).toContain('运行查询后展示真实采样点。');
     expect(html).toContain('data-otlp-metrics-empty-guidance="operator-no-data-guidance"');
@@ -601,12 +1090,50 @@ describe('otlp metrics page', () => {
     expect(html).toContain('确认时间范围、实体归因、采集器和监控模板后再查看指标。');
     expect(html).not.toContain('data-otlp-metrics-trend-bar="empty-series-placeholder"');
     expect(html).toContain('data-otlp-metrics-workbench-grid="series-detail-split"');
+    expect(html).toContain('data-otlp-metrics-workbench-grid-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui="workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="metrics-series-only"');
     expect(html).toContain('data-otlp-metrics-series-table="cold-dense-metric-list"');
     expect(html).toContain('data-otlp-metrics-series-table-mode="service-entity-series-set"');
     expect(html).toContain('data-otlp-metrics-series-table-density="primary-scan"');
+    expect(html).toContain('data-otlp-metrics-series-table-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-otlp-metrics-series-table-panel-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-ui="panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-clip="true"');
+    expect(html).toContain('data-otlp-metrics-series-table-header="shared-panel-header"');
+    expect(html).toContain('data-otlp-metrics-series-table-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-ui="panel-header"');
+    expect(html).toContain('data-hz-panel-header-density="operator-compact"');
+    expect(html).toContain('data-hz-panel-header-chrome="transparent-topless"');
+    expect(html).toContain('data-otlp-metrics-series-table-summary-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-ui="status-badge"');
+    expect(html).toContain('data-hz-status-size="xs"');
+    expect(html).toContain('data-otlp-metrics-series-set-summary-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-otlp-metrics-series-set-summary-section="shared-panel-section"');
+    expect(html).toContain('data-otlp-metrics-series-set-summary-section-owner="hertzbeat-ui-panel-section"');
+    expect(html).toContain('data-hz-panel-section-owner="hertzbeat-ui-panel-section"');
+    expect(html).toContain('data-hz-panel-section-padding="summary"');
+    expect(html).toContain('data-otlp-metrics-series-set-summary-strip="shared-stat-strip"');
+    expect(html).toContain('data-otlp-metrics-series-set-summary-strip-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-hz-stat-strip-frame="panel-inset"');
+    expect(html).toContain('data-hz-stat-strip-spacing="compact"');
+    expect(html).toContain('data-otlp-metrics-series-set-summary-cell-owner="hertzbeat-ui-stat-cell"');
+    expect(html).toContain('data-hz-ui="stat-strip"');
+    expect(html).toContain('data-hz-ui="stat-cell"');
+    expect(html).toContain('data-otlp-metrics-series-data-table="shared-data-table"');
+    expect(html).toContain('data-otlp-metrics-series-data-table-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).toContain('data-hz-data-table-variant="embedded"');
+    expect(html).toContain('data-otlp-metrics-series-table-empty-state="shared-empty-state"');
+    expect(html).toContain('data-otlp-metrics-series-table-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(html).toContain('data-hz-ui="empty-state"');
+    expect(html).toContain('data-hz-empty-state-layout="table-panel"');
     expect(html).toContain('data-otlp-metrics-series-set-summary="service-entity-scope"');
     expect(html).not.toContain('data-otlp-metrics-detail-panel="cold-detail-panel"');
     expect(html).toContain('data-otlp-metrics-detail-panel-empty="suppressed-until-real-series"');
+    expect(html).toContain('data-otlp-metrics-detail-panel-empty-owner="hertzbeat-ui-assistive-marker"');
+    expect(html).toContain('data-hz-ui="assistive-marker"');
+    expect(html).toContain('data-hz-assistive-marker-visibility="sr-only"');
     expect(html).not.toContain('data-otlp-metrics-detail-panel-body="compact-evidence-stack"');
     expect(html).not.toContain('data-otlp-metrics-secondary-context="collapsible-evidence-diagnostics"');
     expect(html).not.toContain('data-otlp-metrics-entity-context-panel="collapsible"');
@@ -666,6 +1193,24 @@ describe('otlp metrics page', () => {
     expect(html).not.toContain('1 个查询');
   });
 
+  it('renders a shared return action when metrics inherit a trace return path', async () => {
+    mockState.searchParams = new URLSearchParams(
+      'traceId=trace-123&spanId=span-456&serviceName=checkout&returnTo=%2Ftrace%2Fmanage%3FtraceId%3Dtrace-123%26spanId%3Dspan-456%26serviceName%3Dcheckout'
+    );
+    const { default: OtlpMetricsPage } = await import('./page');
+
+    const html = renderToStaticMarkup(<OtlpMetricsPage />);
+
+    expect(html).toContain('data-otlp-metrics-return-action-group-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-action-group-layout="full-end"');
+    expect(html).toContain('data-otlp-metrics-return-action="true"');
+    expect(html).toContain('data-otlp-metrics-header-action="return-source"');
+    expect(html).toContain('data-otlp-metrics-header-action-icon="return-source"');
+    expect(html).toContain('data-otlp-metrics-header-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('href="/trace/manage?traceId=trace-123&amp;spanId=span-456&amp;serviceName=checkout"');
+    expect(html).toContain('返回来源');
+  });
+
   it('does not show fake zero metric cards or a selected-series inspector when no real series exists', async () => {
     mockState.metricSeries = [];
 
@@ -674,7 +1219,12 @@ describe('otlp metrics page', () => {
 
     expect(html).toContain('data-otlp-metrics-empty-state="honest-no-series"');
     expect(html).toContain('data-otlp-metrics-empty-state-context="applied-query-visible"');
+    expect(html).toContain('data-otlp-metrics-trend-empty-owner="hertzbeat-ui-state-notice"');
+    expect(html).toContain('data-hz-state-frame="trend-empty"');
+    expect(html).toContain('data-otlp-metrics-trend-frame-owner="hertzbeat-ui-trend-frame"');
     expect(html).toContain('data-otlp-metrics-detail-panel-empty="suppressed-until-real-series"');
+    expect(html).toContain('data-otlp-metrics-detail-panel-empty-owner="hertzbeat-ui-assistive-marker"');
+    expect(html).toContain('data-hz-ui="assistive-marker"');
     expect(html).not.toContain('data-otlp-metrics-facts-grid="inline-summary-strip"');
     expect(html).not.toContain('data-otlp-metrics-detail-panel="cold-detail-panel"');
     expect(html).not.toContain('data-otlp-metrics-detail-panel-body="compact-evidence-stack"');
@@ -704,17 +1254,35 @@ describe('otlp metrics page', () => {
     const html = renderToStaticMarkup(<OtlpMetricsPage />);
 
     expect(html).toContain('data-echarts-panel="otlp-metrics"');
+    expect(html).toContain('data-echarts-panel-edge="metrics-chart"');
+    expect(html).toContain('data-otlp-metrics-echarts-edge="shared-metrics-chart"');
+    expect(html).toContain('data-otlp-metrics-echarts-edge-owner="hertzbeat-ui-echarts-panel"');
     expect(html).toContain('data-echarts-panel-preserve-datazoom="true"');
     expect(html).toContain('data-otlp-metrics-chart-datazoom-state="local-observation"');
     expect(html).toContain('data-otlp-metrics-chart-datazoom-preserve="preserved"');
     expect(html).toContain('data-otlp-metrics-chart-zoom-apply="local-to-query-time"');
+    expect(html).toContain('data-otlp-metrics-chart-zoom-apply-owner="hertzbeat-ui-button"');
     expect(html).toContain('data-otlp-metrics-chart-zoom-apply-state="idle"');
+    expect(html).toContain('data-hz-ui="button"');
+    expect(html).toContain('data-hz-control-height="28"');
     expect(html).not.toContain('data-otlp-metrics-chart-zoom-draft="pending-query-time"');
     expect(html).toContain('data-otlp-metrics-chart-meta-row="compact-real-facts"');
+    expect(html).toContain('data-otlp-metrics-chart-meta-row-owner="hertzbeat-ui-toolbar-chips"');
+    expect(html).toContain('data-otlp-metrics-chart-toolbar-actions="shared-toolbar-actions"');
+    expect(html).toContain('data-otlp-metrics-chart-toolbar-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-ui="action-group"');
+    expect(html).toContain('data-hz-action-group-layout="end-wrap"');
     expect(html).toContain('data-otlp-metrics-chart-meta-fact="true"');
+    expect(html).toContain('data-otlp-metrics-chart-meta-fact-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-ui="chip-group"');
+    expect(html).toContain('data-hz-chip-group-density="compact"');
+    expect(html).toContain('data-hz-ui="status-badge"');
+    expect(html).toContain('data-hz-status-badge-layout="metric-fact"');
+    expect(html).toContain('data-hz-status-badge-part="label"');
+    expect(html).toContain('data-hz-status-badge-part="value"');
     expect(html).not.toContain('data-otlp-metrics-facts-grid="inline-summary-strip"');
     expect(html).toContain('disabled=""');
-    expect(html).toContain('Apply as query time');
+    expect(html).toContain('应用为查询时间');
   });
 
   it('applies OTLP metric chart zoom to the route only after the explicit apply action', async () => {
@@ -746,6 +1314,8 @@ describe('otlp metrics page', () => {
     let applyAction = interactionContainer.querySelector('[data-otlp-metrics-chart-zoom-apply="local-to-query-time"]') as HTMLButtonElement | null;
     const chart = interactionContainer.querySelector('[data-echarts-panel="otlp-metrics"]') as HTMLButtonElement | null;
     expect(applyAction).not.toBeNull();
+    expect(applyAction?.getAttribute('data-otlp-metrics-chart-zoom-apply-owner')).toBe('hertzbeat-ui-button');
+    expect(applyAction?.getAttribute('data-hz-ui')).toBe('button');
     expect(applyAction?.disabled).toBe(true);
     expect(chart).not.toBeNull();
 
@@ -758,16 +1328,24 @@ describe('otlp metrics page', () => {
     const startInput = interactionContainer.querySelector('[data-time-range-start-input="true"]') as HTMLInputElement | null;
     const endInput = interactionContainer.querySelector('[data-time-range-end-input="true"]') as HTMLInputElement | null;
     expect(applyAction?.disabled).toBe(false);
+    expect(applyAction?.getAttribute('data-otlp-metrics-chart-zoom-apply-owner')).toBe('hertzbeat-ui-button');
+    expect(applyAction?.getAttribute('data-hz-ui')).toBe('button');
     expect(applyAction?.getAttribute('data-otlp-metrics-chart-zoom-apply-state')).toBe('ready');
     const zoomDraft = interactionContainer.querySelector('[data-otlp-metrics-chart-zoom-draft="pending-query-time"]') as HTMLElement | null;
     expect(zoomDraft).not.toBeNull();
+    expect(zoomDraft?.getAttribute('data-otlp-metrics-chart-zoom-draft-owner')).toBe('hertzbeat-ui-status-badge');
+    expect(zoomDraft?.getAttribute('data-hz-ui')).toBe('status-badge');
+    expect(zoomDraft?.getAttribute('data-hz-status-tone')).toBe('info');
+    expect(zoomDraft?.getAttribute('data-hz-status-badge-layout')).toBe('zoom-draft');
     expect(zoomDraft?.getAttribute('data-otlp-metrics-chart-zoom-draft-state')).toBe('pending');
-    expect(zoomDraft?.textContent).toContain('1970-01-01 08:00:01.500');
-    expect(zoomDraft?.textContent).toContain('1970-01-01 08:00:02.500');
+    expect(zoomDraft?.querySelector('[data-hz-status-badge-part="label"]')).not.toBeNull();
+    expect(zoomDraft?.querySelector('[data-hz-status-badge-part="value"]')).not.toBeNull();
+    expect(zoomDraft?.textContent).toContain('1970-01-01 08:00:01');
+    expect(zoomDraft?.textContent).toContain('1970-01-01 08:00:02');
     expect(startInput?.getAttribute('data-time-range-absolute-input-format')).toBe('local-datetime');
     expect(endInput?.getAttribute('data-time-range-absolute-input-format')).toBe('local-datetime');
-    expect(startInput?.value).toBe('1970-01-01 08:00:01');
-    expect(endInput?.value).toBe('1970-01-01 08:00:02');
+    expect(startInput?.value).toBe('');
+    expect(endInput?.value).toBe('');
     expect(mockState.replace).not.toHaveBeenCalled();
 
     await act(async () => {
@@ -780,38 +1358,62 @@ describe('otlp metrics page', () => {
     expect(href).toContain('/ingestion/otlp/metrics?');
     expect(href).toContain('query=http_requests_total');
     expect(href).toContain('timeRange=last-1h');
-    expect(href).toContain('start=1500');
-    expect(href).toContain('end=2500');
-    expect(href).toContain('refresh=30');
+    expect(href).toContain('from=1970-01-01+08%3A00%3A01');
+    expect(href).toContain('to=1970-01-01+08%3A00%3A02');
+    expect(href).not.toContain('start=1500');
+    expect(href).not.toContain('end=2500');
+    expect(href).not.toContain('refresh=30');
     expect(href).toContain('live=false');
-    expect(href).toContain('tz=Asia%2FShanghai');
+    expect(href).toContain('timezone=Asia%2FShanghai');
+    expect(href).not.toContain('tz=Asia%2FShanghai');
   });
 
   it('keeps the metrics top-right toolbar on the shared time control contract', async () => {
     mockState.searchParams = new URLSearchParams('timeRange=last-6h&refresh=30&live=true&tz=Asia%2FShanghai');
-    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
 
     const { default: OtlpMetricsPage } = await import('./page');
     const html = renderToStaticMarkup(<OtlpMetricsPage />);
 
     expect(source).toContain('data-otlp-metrics-time-control-placement="top-right"');
     expect(source).toContain('data-otlp-metrics-time-toolbar="top-right-corner"');
+    expect(source).toContain('data-otlp-metrics-time-toolbar-owner="hertzbeat-ui-workbench-layout"');
     expect(source).toContain('data-otlp-metrics-header-topbar="time-context"');
+    expect(source).toContain('data-otlp-metrics-header-topbar-owner="hertzbeat-ui-workbench-layout"');
     expect(source).toContain('data-otlp-metrics-toolbar-stack="same-width-time-actions"');
+    expect(source).toContain('variant="header-toolbar-slot"');
+    expect(source).toContain('variant="time-toolbar"');
     expect(source).toContain('data-otlp-metrics-time-control-visual="narrow-top-right-rail"');
     expect(source).toContain('data-otlp-metrics-time-control-fit="no-clipping"');
     expect(source).toContain('variant="narrow-rail"');
     expect(source).toContain('data-otlp-metrics-title-block="operator-context"');
+    expect(source).toContain('data-otlp-metrics-title-block-owner="hertzbeat-ui-workbench-header-copy"');
     expect(source).not.toContain('xl:grid-cols-[minmax(0,1fr)_auto]');
     expect(source).not.toContain('data-otlp-metrics-time-control-layout="compact-rail"');
     expect(source).not.toContain('data-otlp-metrics-time-control="shared-time-context-control" className="min-w-[520px] flex-1"');
+    expect(source).not.toContain('className="flex min-w-0 justify-end xl:justify-self-end"');
+    expect(source).not.toContain('className="flex w-full max-w-[1120px] flex-wrap justify-end gap-2"');
+    expect(source).not.toContain('className="flex w-full justify-end"');
+    expect(source).toContain('data-otlp-metrics-time-control-owner="hertzbeat-ui-control-stack"');
+    expect(source).toContain('data-otlp-metrics-time-range-control-owner="hertzbeat-shared-time-range-control"');
+    expect(source).toContain('layout="end-inline"');
+    expect(source).not.toContain('variant="narrow-rail"\n                          className="justify-end"');
     expect(source.indexOf('data-otlp-metrics-time-control="shared-time-context-control"')).toBeLessThan(
       source.indexOf('data-otlp-metrics-query-bar="cold-query-row"')
     );
     expect(html).toContain('data-otlp-metrics-time-control="shared-time-context-control"');
+    expect(html).toContain('data-otlp-metrics-time-control-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-otlp-metrics-time-range-control-owner="hertzbeat-shared-time-range-control"');
+    expect(html).toContain('data-hz-ui="control-stack"');
+    expect(html).toContain('data-hz-control-stack-layout="end-inline"');
     expect(html).toContain('data-otlp-metrics-time-control-placement="top-right"');
     expect(html).toContain('data-otlp-metrics-time-toolbar="top-right-corner"');
+    expect(html).toContain('data-otlp-metrics-time-toolbar-owner="hertzbeat-ui-workbench-layout"');
     expect(html).toContain('data-otlp-metrics-toolbar-stack="same-width-time-actions"');
+    expect(html).toContain('data-otlp-metrics-header-topbar-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="header-toolbar-slot"');
+    expect(html).toContain('data-hz-workbench-layout-variant="time-toolbar"');
+    expect(html).toContain('grid min-w-0 ml-auto w-full max-w-[1120px] justify-end gap-2 xl:w-auto');
     expect(html).toContain('data-otlp-metrics-time-control-visual="narrow-top-right-rail"');
     expect(html).toContain('data-otlp-metrics-time-control-fit="no-clipping"');
     expect(html).toContain('data-time-range-control="hertzbeat-shared"');
@@ -853,8 +1455,8 @@ describe('otlp metrics page', () => {
     const serviceInput = interactionContainer.querySelector('[data-otlp-metrics-service-input="true"]') as HTMLInputElement | null;
     const namespaceInput = interactionContainer.querySelector('[data-otlp-metrics-namespace-input="true"]') as HTMLInputElement | null;
     const environmentInput = interactionContainer.querySelector('[data-otlp-metrics-environment-input="true"]') as HTMLInputElement | null;
-    const aggregationSelect = interactionContainer.querySelector('[data-otlp-metrics-aggregation-select="true"]') as HTMLSelectElement | null;
-    const groupBySelect = interactionContainer.querySelector('[data-otlp-metrics-group-by-select="true"]') as HTMLSelectElement | null;
+    const aggregationSelect = interactionContainer.querySelector('[data-otlp-metrics-aggregation-select="true"]') as HTMLElement | null;
+    const groupBySelect = interactionContainer.querySelector('[data-otlp-metrics-group-by-select="true"]') as HTMLElement | null;
     const timeRangeSelect = interactionContainer.querySelector('[data-otlp-metrics-time-range-select="true"]') as HTMLSelectElement | null;
     const timeApplyAction = interactionContainer.querySelector('[data-time-range-apply-action="true"]') as HTMLButtonElement | null;
     const runAction = interactionContainer.querySelector('[data-otlp-metrics-run-query-action="true"]') as HTMLButtonElement | null;
@@ -875,15 +1477,31 @@ describe('otlp metrics page', () => {
       namespaceInput!.dispatchEvent(new Event('input', { bubbles: true }));
       environmentInput!.value = 'prod';
       environmentInput!.dispatchEvent(new Event('input', { bubbles: true }));
-      aggregationSelect!.value = 'sum';
-      aggregationSelect!.dispatchEvent(new Event('change', { bubbles: true }));
-      groupBySelect!.value = 'service_name';
-      groupBySelect!.dispatchEvent(new Event('change', { bubbles: true }));
       timeRangeSelect!.value = 'last-1h';
       timeRangeSelect!.dispatchEvent(new Event('change', { bubbles: true }));
       await Promise.resolve();
     });
 
+    await act(async () => {
+      aggregationSelect!.querySelector('[data-hz-ui="select-trigger"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      aggregationSelect!.querySelector('[data-otlp-metrics-aggregation-option="sum"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      groupBySelect!.querySelector('[data-hz-ui="select-trigger"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      groupBySelect!.querySelector('[data-otlp-metrics-group-by-option="service_name"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
     await act(async () => {
       timeApplyAction?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
       await Promise.resolve();
@@ -951,7 +1569,7 @@ describe('otlp metrics page', () => {
     expect(href).toContain('timeRange=last-45m');
     expect(href).toContain('start=1712730900000');
     expect(href).toContain('end=1712733600000');
-    expect(href).toContain('refresh=30');
+    expect(href).not.toContain('refresh=30');
     expect(href).toContain('live=false');
     expect(href).toContain('tz=Asia%2FShanghai');
     nowSpy.mockRestore();
@@ -1072,6 +1690,20 @@ describe('otlp metrics page', () => {
     const initialEntityCells = Array.from(interactionContainer.querySelectorAll('[data-otlp-metrics-series-entity="true"]')) as HTMLElement[];
     expect(initialEntityCells[0]?.textContent).toContain('Checkout API');
     expect(initialEntityCells[0]?.textContent).toContain('entityId 7');
+    expect(initialEntityCells[0]?.getAttribute('data-hz-ui')).toBe('data-cell-stack');
+    expect(initialEntityCells[0]?.getAttribute('data-otlp-metrics-series-entity-owner')).toBe('hertzbeat-ui-data-cell-stack');
+    expect(initialEntityCells[0]?.getAttribute('data-hz-data-cell-stack-width')).toBe('metrics-entity');
+    expect(initialEntityCells[0]?.className).toContain('min-w-[140px]');
+    const initialEntityLabel = initialEntityCells[0]?.querySelector('[data-otlp-metrics-series-entity-label-owner="hertzbeat-ui-data-cell-text"]') as HTMLElement | null;
+    const initialEntityMeta = initialEntityCells[0]?.querySelector('[data-otlp-metrics-series-entity-meta-owner="hertzbeat-ui-data-cell-text"]') as HTMLElement | null;
+    const initialLatestValue = interactionContainer.querySelector('[data-otlp-metrics-series-latest-owner="hertzbeat-ui-data-cell-text"]') as HTMLElement | null;
+    expect(initialEntityLabel?.getAttribute('data-hz-data-cell-tone')).toBe('strong');
+    expect(initialEntityLabel?.getAttribute('data-hz-data-cell-weight')).toBe('semibold');
+    expect(initialEntityMeta?.getAttribute('data-hz-data-cell-tone')).toBe('success');
+    expect(initialEntityMeta?.getAttribute('data-hz-data-cell-casing')).toBe('plain');
+    expect(initialEntityMeta?.getAttribute('data-otlp-metrics-series-entity-state')).toBe('present');
+    expect(initialLatestValue?.getAttribute('data-hz-data-cell-tone')).toBe('bright');
+    expect(initialLatestValue?.getAttribute('data-hz-data-cell-font')).toBe('mono');
 
     await act(async () => {
       rows[1]?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
@@ -1085,16 +1717,81 @@ describe('otlp metrics page', () => {
     expect(interactionContainer.textContent).toContain('fastapi');
     const evidencePanel = interactionContainer.querySelector('[data-otlp-metrics-selected-series-evidence="real-sample-evidence"]') as HTMLElement | null;
     expect(evidencePanel).not.toBeNull();
+    expect(evidencePanel?.getAttribute('data-otlp-metrics-selected-series-evidence-owner')).toBe('hertzbeat-ui-detail-rows');
+    expect(evidencePanel?.getAttribute('data-hz-detail-rows-owner')).toBe('hertzbeat-ui-detail-rows');
+    expect(evidencePanel?.getAttribute('data-hz-detail-rows-boundary')).toBe('top');
     expect(evidencePanel?.textContent).toContain('指标证据');
     expect(evidencePanel?.textContent).toContain('采样窗口');
+    const detailBody = interactionContainer.querySelector('[data-otlp-metrics-detail-panel-body="compact-evidence-stack"]') as HTMLElement | null;
+    expect(detailBody).not.toBeNull();
+    expect(detailBody?.getAttribute('data-otlp-metrics-detail-panel-body-owner')).toBe('hertzbeat-ui-panel-section');
+    expect(detailBody?.getAttribute('data-hz-panel-section-owner')).toBe('hertzbeat-ui-panel-section');
+    expect(detailBody?.getAttribute('data-hz-panel-section-divider')).toBe('none');
     expect(evidencePanel?.textContent).toContain('真实采样时间');
     expect(evidencePanel?.textContent).toContain('关联链路');
     expect(evidencePanel?.textContent).toContain('trace-inventory');
+    const sampleHelper = interactionContainer.querySelector('[data-otlp-metrics-trend-sample-helper="real-sample-count"]') as HTMLElement | null;
+    expect(sampleHelper).not.toBeNull();
+    expect(sampleHelper?.getAttribute('data-otlp-metrics-trend-sample-helper-owner')).toBe('hertzbeat-ui-data-meta-text');
+    expect(sampleHelper?.getAttribute('data-hz-ui')).toBe('data-meta-text');
+    expect(sampleHelper?.getAttribute('data-hz-data-meta-spacing')).toBe('trend-helper');
+    expect(sampleHelper?.textContent).toContain('-');
+    const selectedContextPanel = interactionContainer.querySelector('[data-otlp-metrics-selected-series-context="selected-series-attribution"]') as HTMLElement | null;
+    expect(selectedContextPanel?.getAttribute('data-otlp-metrics-selected-series-context-owner')).toBe('hertzbeat-ui-detail-rows');
+    expect(selectedContextPanel?.getAttribute('data-hz-detail-rows-owner')).toBe('hertzbeat-ui-detail-rows');
+    expect(selectedContextPanel?.getAttribute('data-hz-detail-rows-boundary')).toBe('top');
+    expect(selectedContextPanel?.textContent).toContain('当前选中序列');
+    expect(selectedContextPanel?.textContent).toContain('fastapi');
+    const detailPanelHeader = interactionContainer.querySelector('[data-otlp-metrics-detail-panel-header="shared-panel-header"]') as HTMLElement | null;
+    const detailPanel = interactionContainer.querySelector('[data-otlp-metrics-detail-panel="cold-detail-panel"]') as HTMLElement | null;
+    const detailGrid = interactionContainer.querySelector('[data-otlp-metrics-workbench-grid="series-detail-split"]') as HTMLElement | null;
+    expect(detailGrid?.getAttribute('data-otlp-metrics-workbench-grid-owner')).toBe('hertzbeat-ui-workbench-layout');
+    expect(detailGrid?.getAttribute('data-hz-ui')).toBe('workbench-layout');
+    expect(detailGrid?.getAttribute('data-hz-workbench-layout-variant')).toBe('metrics-series-detail');
+    expect(detailPanel?.getAttribute('data-otlp-metrics-detail-panel-owner')).toBe('hertzbeat-ui-panel-surface');
+    expect(detailPanel?.getAttribute('data-hz-ui')).toBe('panel-surface');
+    expect(detailPanel?.getAttribute('data-hz-panel-surface-clip')).toBe('true');
+    expect(detailPanel?.getAttribute('data-otlp-metrics-detail-panel-stickiness-owner')).toBe('hertzbeat-ui-panel-surface');
+    expect(detailPanel?.getAttribute('data-hz-panel-surface-stickiness')).toBe('top-4');
+    expect(detailPanelHeader?.getAttribute('data-otlp-metrics-detail-panel-header-owner')).toBe('hertzbeat-ui-panel-header');
+    expect(detailPanelHeader?.getAttribute('data-hz-ui')).toBe('panel-header');
+    expect(detailPanelHeader?.getAttribute('data-hz-panel-header-owner')).toBe('hertzbeat-ui-panel-header');
+    expect(detailPanelHeader?.getAttribute('data-hz-panel-header-chrome')).toBe('transparent');
+    expect(detailPanelHeader?.textContent).toContain('详情面板');
+    expect(detailPanelHeader?.textContent).toContain('db.client.duration');
+    const detailSummaryStats = interactionContainer.querySelector('[data-otlp-metrics-detail-summary-stats="shared-stat-strip"]') as HTMLElement | null;
+    expect(detailSummaryStats?.getAttribute('data-otlp-metrics-detail-summary-stats-owner')).toBe('hertzbeat-ui-stat-strip');
+    expect(detailSummaryStats?.getAttribute('data-hz-stat-strip-owner')).toBe('hertzbeat-ui-stat-strip');
+    expect(detailSummaryStats?.getAttribute('data-hz-stat-strip-frame')).toBe('panel-solid');
+    expect(detailSummaryStats?.querySelectorAll('[data-otlp-metrics-detail-summary-stat-owner="hertzbeat-ui-stat-cell"]')).toHaveLength(3);
+    expect(detailSummaryStats?.querySelector('[data-otlp-metrics-detail-summary-stat-owner="hertzbeat-ui-stat-cell"]')?.getAttribute('data-hz-stat-frame')).toBe('flush');
+    const detailContextRows = interactionContainer.querySelector('[data-otlp-metrics-detail-context-rows="shared-detail-rows"]') as HTMLElement | null;
+    expect(detailContextRows?.getAttribute('data-otlp-metrics-detail-context-rows-owner')).toBe('hertzbeat-ui-detail-rows');
+    expect(detailContextRows?.getAttribute('data-hz-detail-rows-owner')).toBe('hertzbeat-ui-detail-rows');
+    expect(detailContextRows?.getAttribute('data-hz-detail-rows-offset')).toBe('top');
+    expect(detailContextRows?.textContent).toContain('查询上下文');
+    const entityContextRows = interactionContainer.querySelector('[data-otlp-metrics-entity-context="hertzbeat-signal-entity-context"] [data-otlp-metrics-entity-context-owner="hertzbeat-ui-detail-rows"]') as HTMLElement | null;
+    expect(entityContextRows?.getAttribute('data-hz-detail-rows-owner')).toBe('hertzbeat-ui-detail-rows');
+    expect(entityContextRows?.getAttribute('data-hz-detail-rows-padding')).toBe('compact-y');
+    expect(entityContextRows?.textContent).toContain('实体上下文');
 
     const logHref = (interactionContainer.querySelector('[data-otlp-metrics-logs-action="true"]') as HTMLAnchorElement | null)?.href;
     const traceHref = (interactionContainer.querySelector('[data-otlp-metrics-traces-action="true"]') as HTMLAnchorElement | null)?.href;
     const entityHref = (interactionContainer.querySelector('[data-otlp-metrics-entity-action="true"]') as HTMLAnchorElement | null)?.href;
     const alertHref = (interactionContainer.querySelector('[data-otlp-metrics-alert-handling-action="true"]') as HTMLAnchorElement | null)?.href;
+    const handoffSection = interactionContainer.querySelector('[data-otlp-metrics-handoff-action-section="shared-panel-section"]') as HTMLElement | null;
+    const handoffGroup = interactionContainer.querySelector('[data-otlp-metrics-handoff-actions="compact-context-actions"]') as HTMLElement | null;
+
+    expect(handoffSection?.getAttribute('data-otlp-metrics-handoff-action-section-owner')).toBe('hertzbeat-ui-panel-section');
+    expect(handoffSection?.getAttribute('data-hz-ui')).toBe('panel-section');
+    expect(handoffSection?.getAttribute('data-hz-panel-section-divider')).toBe('top');
+    expect(handoffGroup?.getAttribute('data-otlp-metrics-handoff-actions-owner')).toBe('hertzbeat-ui-action-group');
+    expect(handoffGroup?.getAttribute('data-hz-action-group-owner')).toBe('hertzbeat-ui-action-group');
+    expect(handoffGroup?.getAttribute('data-hz-action-group-layout')).toBe('grid-2');
+    expect(handoffGroup?.querySelectorAll('[data-hz-ui="button-link"]')).toHaveLength(5);
+    handoffGroup?.querySelectorAll('[data-hz-ui="button-link"]').forEach(link => {
+      expect(link.getAttribute('data-hz-button-link-layout')).toBe('full');
+    });
 
     const logParams = new URL(logHref || '', 'http://localhost').searchParams;
     expect(logParams.get('view')).toBe('list');
@@ -1199,8 +1896,25 @@ describe('otlp metrics page', () => {
     const { default: OtlpMetricsPage } = await import('./page');
     const html = renderToStaticMarkup(<OtlpMetricsPage />);
 
+    expect(html).toContain('data-otlp-metrics-secondary-context="collapsible-evidence-diagnostics"');
+    expect(html).toContain('data-otlp-metrics-secondary-context-owner="hertzbeat-ui-panel-section"');
+    expect(html).toContain('data-hz-ui="panel-section"');
+    expect(html).toContain('data-hz-panel-section-divider="top"');
+    expect(html).toContain('data-hz-panel-section-spacing="stack-2"');
     expect(html).toContain('data-otlp-metrics-linked-record-summary="log-trace-alert-links"');
+    expect(html).toContain('data-otlp-metrics-linked-record-summary-owner="hertzbeat-ui-collapsible-section"');
+    expect(html).toContain('data-hz-ui="collapsible-section"');
+    expect(html).toContain('data-hz-collapsible-owner="hertzbeat-ui-collapsible-section"');
+    expect(html).toContain('data-hz-collapsible-surface="inset"');
+    expect(html).toContain('data-hz-collapsible-summary-owner="hertzbeat-ui-collapsible-section"');
+    expect(html).toContain('data-hz-collapsible-body-owner="hertzbeat-ui-collapsible-section"');
+    expect(html).toContain('data-otlp-metrics-linked-record-handoff="shared-context-handoff"');
+    expect(html).toContain('data-otlp-metrics-linked-record-handoff-owner="hertzbeat-ui-context-handoff"');
+    expect(html).toContain('data-hz-ui="context-handoff"');
+    expect(html).toContain('data-hz-context-handoff-owner="hertzbeat-ui-context-handoff"');
+    expect(html).toContain('data-hz-context-handoff-frame="flush"');
     expect(html).toContain('关联记录');
+    expect(html).toContain('证据跳转');
     expect(html).toContain('历史日志');
     expect(html).toContain('按链路查看');
     expect(html).toContain('历史日志会定位到当前 span');
@@ -1242,7 +1956,15 @@ describe('otlp metrics page', () => {
 
     expect(messages).toContain("'otlp.metrics.handoff.entity-disabled'");
     expect(html).toContain('data-otlp-metrics-entity-action-disabled="missing-entity-id"');
+    expect(html).toContain('data-otlp-metrics-entity-action-disabled-shell-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(html).toContain('data-hz-disabled-action-shell-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(html).toContain('data-hz-button-layout="full"');
     expect(html).toContain('data-otlp-metrics-attribution-diagnostics="hertzbeat-attribute-diagnostics"');
+    expect(html).toContain('data-otlp-metrics-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(html).toContain('data-hz-attribute-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(html).toContain('data-hz-attribute-diagnostics-frame="embedded"');
+    expect(html).toContain('data-hz-attribute-diagnostic-row="true"');
+    expect(html).toContain('data-hz-attribute-diagnostic-badge="true"');
     expect(html).toContain('归因诊断');
     expect(html).toContain('hertzbeat.entity_id');
     expect(html).toContain('缺少实体 ID,实体详情会保持禁用');
@@ -1250,8 +1972,8 @@ describe('otlp metrics page', () => {
     expect(html).toContain('collector-local');
     expect(html).toContain('hertzbeat.template');
     expect(html).toContain('jvm');
-    expect(html).toContain('title="otlp.metrics.handoff.entity-disabled"');
-    expect(html).toContain('aria-label="otlp.metrics.handoff.entity-disabled"');
+    expect(html).toContain('title="缺少实体 ID,暂时不能打开实体详情。"');
+    expect(html).toContain('aria-label="缺少实体 ID,暂时不能打开实体详情。"');
     expect(html).not.toContain('data-otlp-metrics-entity-action="true" href="/entities/');
     expect(html).toContain('/entities?search=self-monitor');
   });
diff --git a/web-next/app/ingestion/otlp/metrics/page.tsx b/web-next/app/ingestion/otlp/metrics/page.tsx
index 93e2ca54bf..20e6541e43 100644
--- a/web-next/app/ingestion/otlp/metrics/page.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.tsx
@@ -1,909 +1,6 @@
-'use client';
+import React from 'react';
+import OtlpMetricsPage from './otlp-metrics-page';
 
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import Link from 'next/link';
-import { BarChart3, Database, Play, RotateCcw, Search } from 'lucide-react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { EChartsPanel, type EChartsDataZoomRange } from '@/components/observability/echarts-panel';
-import { buildTimeRangeControlLabels, formatEpochMillisDraft, TimeRangeControl } from '@/components/observability/time-range-control';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { Input } from '@/components/ui/input';
-import { Select } from '@/components/ui/select';
-import { apiMessageGet } from '@/lib/api-client';
-import { formatTime } from '@/lib/format';
-import { loadOtlpMetricsConsole, queryStateFromParams, type OtlpMetricsQueryState } from '@/lib/otlp-metrics/controller';
-import { buildSignalEntityContextRows, readEntityIdRouteParam, readEpochMillisRouteParam, readSignalRouteContext, type SignalRouteContext } from '@/lib/signal-route-context';
-import { resolveTimeContextBounds, sanitizeTimeContext, TIME_CONTEXT_PRESETS, type TimeContext } from '@/lib/time-context';
-import {
-  buildConsoleFacts,
-  buildConsoleMetrics,
-  buildContextRows,
-  buildMetricSeriesAttributionDiagnostics,
-  buildMetricSeriesContextRows,
-  buildMetricSeriesEvidenceRows,
-  buildMetricSeriesLinkedRecordRows,
-  buildMetricSeriesRows,
-  buildMetricSeriesViews,
-  buildMetricsChartOption,
-  buildMetricsDataZoomTimeContext,
-  buildMetricTrendBars,
-  buildMetricsExplorerState,
-  buildMetricsHandoffLinks,
-  type OtlpMetricSeriesView
-} from '@/lib/otlp-metrics/view-model';
-import type { OtlpMetricsConsole } from '@/lib/types';
-import { buildOtlpMetricsRoute, hasMetricsDisplayReturnLabel } from './route-state';
-
-const actionClass =
-  'inline-flex h-8 items-center justify-center gap-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe3ee] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] transition-colors hover:border-[#3b4454] hover:bg-[#151821]';
-const primaryActionClass =
-  'inline-flex h-8 items-center justify-center gap-2 rounded-[3px] border border-[#3b4454] bg-[#18202c] px-4 text-[12px] font-semibold text-[#f2f6fb] shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] transition-colors hover:bg-[#202938]';
-const disabledActionClass =
-  'inline-flex h-8 cursor-not-allowed items-center justify-center gap-2 rounded-[3px] border border-[#242a34] bg-[#0b0e13] px-3 text-[12px] font-semibold text-[#687386] opacity-80 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]';
-const panelClass = 'rounded-[4px] border border-[#252b35] bg-[#0d1015] shadow-[0_18px_60px_rgba(0,0,0,0.28)]';
-const inputClass =
-  'h-8 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#e6edf7] outline-none transition-colors placeholder:text-[#697386] focus:border-[#4e74f8] focus:ring-2 focus:ring-[rgba(78,116,248,0.12)]';
-
-function latestSeriesTimestamp(data: OtlpMetricsConsole) {
-  const timestamps = (data.results?.frames || [])
-    .flatMap(frame => frame.data || [])
-    .map(row => Number(row?.[0]))
-    .filter(Number.isFinite);
-  return timestamps.length ? Math.max(...timestamps) : data.stats?.latestObservedAt ?? null;
-}
-
-function routeEpochMillisValue(value?: string) {
-  const epochMillis = readEpochMillisRouteParam(value);
-  return epochMillis ? Number(epochMillis) : undefined;
-}
-
-function routeEntityIdValue(value?: string) {
-  const entityId = readEntityIdRouteParam(value);
-  return entityId ? Number(entityId) : undefined;
-}
-
-const metricsTimeRangePresets = TIME_CONTEXT_PRESETS.map(preset => ({ value: preset.value, label: preset.label }));
-
-function firstRouteText(...values: Array<string | null | undefined>) {
-  return values.find((value): value is string => value != null && value.trim() !== '')?.trim();
-}
-
-function readMetricSeriesLabel(series: OtlpMetricSeriesView, ...keys: string[]) {
-  return firstRouteText(...keys.map(key => series.labels[key]));
-}
-
-function findMetricSeriesForRouteTrace(
-  seriesList: OtlpMetricSeriesView[],
-  query: OtlpMetricsQueryState,
-  routeContext: SignalRouteContext
-) {
-  const traceId = firstRouteText(query.traceId, routeContext.traceId);
-  const spanId = firstRouteText(query.spanId, routeContext.spanId);
-  if (!traceId) return null;
-  return seriesList.find(series => {
-    const seriesTraceId = readMetricSeriesLabel(series, 'traceId', 'trace_id', 'trace.id', 'trace_id_hex');
-    const seriesSpanId = readMetricSeriesLabel(series, 'spanId', 'span_id', 'span.id', 'span_id_hex');
-    if (seriesTraceId !== traceId) return false;
-    return spanId ? seriesSpanId === spanId : true;
-  }) || null;
-}
-
-export default function OtlpMetricsPage() {
-  const { t } = useI18n();
-  const router = useRouter();
-  const searchParams = useSearchParams();
-  const query = useMemo(() => queryStateFromParams(searchParams), [searchParams]);
-  const routeContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
-  const load = useCallback(async (): Promise<OtlpMetricsConsole> => loadOtlpMetricsConsole(apiMessageGet, query), [query]);
-  const initialDraft = useMemo(() => ({
-    query: query.query || '',
-    aggregation: query.aggregation || 'avg',
-    groupBy: query.groupBy || 'service_name',
-    timeRange: query.timeRange || 'last-30m',
-    start: query.start || '',
-    end: query.end || '',
-    refresh: query.refresh || '',
-    live: query.live || '',
-    tz: query.tz || '',
-    serviceName: query.serviceName || '',
-    serviceNamespace: query.serviceNamespace || '',
-    environment: query.environment || '',
-    traceId: query.traceId || ''
-  }), [
-    query.aggregation,
-    query.environment,
-    query.groupBy,
-    query.query,
-    query.end,
-    query.live,
-    query.refresh,
-    query.serviceName,
-    query.serviceNamespace,
-    query.start,
-    query.timeRange,
-    query.traceId,
-    query.tz
-  ]);
-  const [draft, setDraft] = useState(initialDraft);
-  const [selectedSeriesKey, setSelectedSeriesKey] = useState<string | null>(null);
-  const [metricsChartZoomRange, setMetricsChartZoomRange] = useState<EChartsDataZoomRange | null>(null);
-
-  useEffect(() => {
-    if (hasMetricsDisplayReturnLabel(searchParams)) {
-      router.replace(buildOtlpMetricsRoute(query));
-    }
-  }, [query, router, searchParams]);
-
-  useEffect(() => {
-    setDraft(initialDraft);
-  }, [initialDraft]);
-
-  const updateDraftField = useCallback((field: keyof typeof draft, value: string) => {
-    setDraft(previous => ({ ...previous, [field]: value }));
-  }, []);
-
-  const handleMetricsChartZoomChange = useCallback((nextZoom: EChartsDataZoomRange) => {
-    setMetricsChartZoomRange(previous => {
-      if (
-        previous?.start === nextZoom.start &&
-        previous?.end === nextZoom.end &&
-        previous?.startValue === nextZoom.startValue &&
-        previous?.endValue === nextZoom.endValue
-      ) {
-        return previous;
-      }
-      return nextZoom;
-    });
-  }, []);
-
-  const draftTimeContext = useMemo(() => sanitizeTimeContext({
-    timeRange: draft.timeRange || query.timeRange || 'last-30m',
-    start: draft.start || query.start,
-    end: draft.end || query.end,
-    refresh: draft.refresh || query.refresh,
-    live: draft.live || query.live,
-    tz: draft.tz || query.tz
-  }), [
-    draft.end,
-    draft.live,
-    draft.refresh,
-    draft.start,
-    draft.timeRange,
-    draft.tz,
-    query.end,
-    query.live,
-    query.refresh,
-    query.start,
-    query.timeRange,
-    query.tz
-  ]);
-
-  const applyMetricsQuery = useCallback((nextTimeContext?: TimeContext) => {
-    const timeContext = sanitizeTimeContext({
-      timeRange: nextTimeContext?.timeRange || draft.timeRange || query.timeRange || 'last-30m',
-      start: nextTimeContext?.start || draft.start || query.start,
-      end: nextTimeContext?.end || draft.end || query.end,
-      refresh: nextTimeContext?.refresh || draft.refresh || query.refresh,
-      live: nextTimeContext?.live || draft.live || query.live,
-      tz: nextTimeContext?.tz || draft.tz || query.tz
-    });
-    const bounds = resolveTimeContextBounds(timeContext);
-    const hasAbsoluteDraft = Boolean(timeContext.start && timeContext.end);
-    router.replace(buildOtlpMetricsRoute({
-      ...query,
-      query: draft.query.trim() || undefined,
-      aggregation: draft.aggregation,
-      groupBy: draft.groupBy,
-      timeRange: timeContext.timeRange || 'last-30m',
-      serviceName: draft.serviceName.trim() || undefined,
-      serviceNamespace: draft.serviceNamespace.trim() || undefined,
-      environment: draft.environment.trim() || undefined,
-      traceId: draft.traceId.trim() || undefined,
-      start: bounds?.start || (!hasAbsoluteDraft ? query.start : undefined),
-      end: bounds?.end || (!hasAbsoluteDraft ? query.end : undefined),
-      refresh: timeContext.refresh,
-      live: timeContext.live,
-      tz: timeContext.tz
-    }));
-  }, [draft, query, router]);
-
-  const applyMetricsTimeContext = useCallback((timeContext: TimeContext) => {
-    const sanitized = sanitizeTimeContext(timeContext);
-    setDraft(previous => ({
-      ...previous,
-      timeRange: sanitized.timeRange || previous.timeRange || 'last-30m',
-      start: sanitized.start || '',
-      end: sanitized.end || '',
-      refresh: sanitized.refresh || '',
-      live: sanitized.live || '',
-      tz: sanitized.tz || ''
-    }));
-    applyMetricsQuery(sanitized);
-  }, [applyMetricsQuery]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('otlp.metrics.loading')}>
-      {data => {
-        const queryEntityId = routeEntityIdValue(query.entityId);
-        const queryEntityIdText = readEntityIdRouteParam(query.entityId);
-        const queryStart = routeEpochMillisValue(query.start);
-        const queryEnd = routeEpochMillisValue(query.end);
-        const queryStartText = readEpochMillisRouteParam(query.start);
-        const queryEndText = readEpochMillisRouteParam(query.end);
-        const mergedData: OtlpMetricsConsole = {
-          ...data,
-          context: {
-            ...data.context,
-            entityId: data.context?.entityId ?? queryEntityId,
-            entityName: data.context?.entityName || query.entityName,
-            serviceName: data.context?.serviceName || query.serviceName,
-            serviceNamespace: data.context?.serviceNamespace || query.serviceNamespace,
-            environment: data.context?.environment || query.environment,
-            start: data.context?.start ?? queryStart,
-            end: data.context?.end ?? queryEnd
-          }
-        };
-        const workbenchState = buildMetricsExplorerState(mergedData);
-        const metricSeries = buildMetricSeriesViews(mergedData, t);
-        const hasMetricSeries = metricSeries.length > 0;
-        const seriesRows = buildMetricSeriesRows(metricSeries, t);
-        const selectedMetricSeries =
-          metricSeries.find(series => series.key === selectedSeriesKey) ||
-          findMetricSeriesForRouteTrace(metricSeries, query, routeContext) ||
-          metricSeries[0] ||
-          null;
-        const selectedMetricSeriesIndex = selectedMetricSeries ? metricSeries.findIndex(series => series.key === selectedMetricSeries.key) : -1;
-        const selectedSeriesContextRows = buildMetricSeriesContextRows(selectedMetricSeries, t);
-        const selectedSeriesEvidenceRows = buildMetricSeriesEvidenceRows(selectedMetricSeries, formatTime);
-        const attributionDiagnostics = buildMetricSeriesAttributionDiagnostics(selectedMetricSeries, t);
-        const trendSourceSeries = selectedMetricSeries
-          ? [selectedMetricSeries, ...metricSeries.filter(series => series.key !== selectedMetricSeries.key)]
-          : metricSeries;
-        const trendBars = buildMetricTrendBars(trendSourceSeries, formatTime);
-        const metricsChartOption = buildMetricsChartOption(metricSeries);
-        const metricsChartZoomContext = buildMetricsDataZoomTimeContext(
-          metricSeries,
-          metricsChartZoomRange,
-          draftTimeContext.timeRange || query.timeRange || 'last-30m'
-        );
-        const metricsChartZoomDraftLabel = metricsChartZoomContext?.start && metricsChartZoomContext?.end
-          ? `${formatEpochMillisDraft(metricsChartZoomContext.start)} → ${formatEpochMillisDraft(metricsChartZoomContext.end)}`
-          : '';
-        const canApplyMetricsChartZoom = Boolean(metricsChartZoomContext && metricsChartZoomDraftLabel);
-        const facts = buildConsoleFacts(mergedData, t, formatTime);
-        const metrics = buildConsoleMetrics(mergedData, t);
-        const contextRows = buildContextRows(mergedData, t, formatTime);
-        const handoffLinks = buildMetricsHandoffLinks(mergedData, query, routeContext, selectedMetricSeries);
-        const linkedRecordRows = buildMetricSeriesLinkedRecordRows(selectedMetricSeries, handoffLinks);
-        const missingEntityHandoffTitle = t('otlp.metrics.handoff.entity-disabled');
-        const canOpenEntity = handoffLinks.entityHref.startsWith('/entities/');
-        const entityContextRows = buildSignalEntityContextRows(routeContext, {
-          entityId: queryEntityIdText || (mergedData.context?.entityId != null ? String(mergedData.context.entityId) : undefined),
-          entityName: query.entityName || mergedData.context?.entityName,
-          serviceName: mergedData.context?.serviceName || query.serviceName,
-          serviceNamespace: mergedData.context?.serviceNamespace || query.serviceNamespace,
-          environment: mergedData.context?.environment || query.environment,
-          start: queryStartText || (mergedData.context?.start != null ? String(mergedData.context.start) : undefined),
-          end: queryEndText || (mergedData.context?.end != null ? String(mergedData.context.end) : undefined),
-          source: routeContext.source || 'OTLP'
-        });
-        const headerContextPills = [
-          { label: '服务', value: firstRouteText(mergedData.context?.serviceName, query.serviceName, draft.serviceName) },
-          { label: '命名空间', value: firstRouteText(mergedData.context?.serviceNamespace, query.serviceNamespace, draft.serviceNamespace) },
-          { label: '环境', value: firstRouteText(mergedData.context?.environment, query.environment, draft.environment) },
-          {
-            label: '分组',
-            value:
-              draft.groupBy === 'service_namespace'
-                ? '命名空间'
-                : draft.groupBy === 'deployment_environment_name'
-                  ? '环境'
-              : '服务'
-          }
-        ].filter((pill): pill is { label: string; value: string } => Boolean(pill.value));
-        const seriesSetScopeRows = [
-          { label: '服务范围', value: firstRouteText(mergedData.context?.serviceName, query.serviceName, draft.serviceName) || '全部服务' },
-          { label: '命名空间', value: firstRouteText(mergedData.context?.serviceNamespace, query.serviceNamespace, draft.serviceNamespace) || '全部命名空间' },
-          { label: '环境', value: firstRouteText(mergedData.context?.environment, query.environment, draft.environment) || '全部环境' },
-          {
-            label: '分组',
-            value:
-              draft.groupBy === 'service_namespace'
-                ? '命名空间'
-                : draft.groupBy === 'deployment_environment_name'
-                  ? '环境'
-                  : '服务'
-          },
-          { label: '序列', value: `${seriesRows.length} 条` }
-        ];
-        const latestObservedAt = latestSeriesTimestamp(mergedData);
-        const firstSeries = (selectedMetricSeriesIndex >= 0 ? seriesRows[selectedMetricSeriesIndex] : undefined) ?? seriesRows[0] ?? {
-          title: mergedData.query || '未选择指标',
-          copy: mergedData.context?.serviceName || routeContext.serviceName || '-',
-          meta: '-'
-        };
-
-        return (
-          <main
-            data-otlp-metrics-route="otlp-cold-metrics-workbench"
-            data-otlp-metrics-style-baseline="hertzbeat-cold-matte"
-            data-otlp-metrics-page-shell="flat-direct-stack"
-            data-otlp-metrics-page-shell-layer="removed"
-            data-otlp-metrics-page-stack="direct-panels"
-            className="flex min-h-[calc(100vh-56px)] flex-col gap-3 bg-[#07090b] px-3 pb-3 pt-0 text-[#e8edf5]"
-          >
-              <section
-                data-otlp-metrics-header="cold-compact-header"
-                data-otlp-metrics-header-layout="compact-title-with-aligned-toolbar"
-                data-otlp-metrics-header-actions="removed"
-                className={`${panelClass} px-4 py-3`}
-              >
-                <div className="grid min-w-0 gap-3 xl:grid-cols-[minmax(280px,1fr)_minmax(780px,auto)] xl:items-start">
-                  <div data-otlp-metrics-title-block="operator-context" className="min-w-0">
-                    <p className="mb-2 text-[12px] font-semibold tracking-[0.08em] text-[#8792a5]">可观测 / 指标</p>
-                    <h1 className="text-[26px] font-semibold tracking-normal text-[#f4f7fb]">指标工作台</h1>
-                    {headerContextPills.length ? (
-                      <div data-otlp-metrics-header-context-strip="applied-query-facts" className="mt-3 flex flex-wrap gap-2">
-                        {headerContextPills.map(pill => (
-                          <span
-                            key={pill.label}
-                            className="inline-flex h-7 max-w-[220px] items-center gap-2 rounded-[3px] border border-[#252b35] bg-[#10141b] px-2.5 text-[11px]"
-                          >
-                            <span className="shrink-0 font-semibold text-[#7f8a9d]">{pill.label}</span>
-                            <span className="truncate font-semibold text-[#dbe5f3]">{pill.value}</span>
-                          </span>
-                        ))}
-                      </div>
-                    ) : null}
-                  </div>
-                  <div data-otlp-metrics-header-topbar="time-context" className="flex min-w-0 justify-end xl:justify-self-end">
-                    <div
-                      data-otlp-metrics-time-toolbar="top-right-corner"
-                      data-otlp-metrics-toolbar-stack="same-width-time-actions"
-                      className="flex w-full max-w-[1120px] justify-end"
-                    >
-                      <div
-                        data-otlp-metrics-time-control="shared-time-context-control"
-                        data-otlp-metrics-time-control-placement="top-right"
-                        data-otlp-metrics-time-control-visual="narrow-top-right-rail"
-                        data-otlp-metrics-time-control-fit="no-clipping"
-                        className="flex w-full justify-end"
-                      >
-                        <TimeRangeControl
-                          value={draftTimeContext}
-                          labels={buildTimeRangeControlLabels(t)}
-                          onApply={applyMetricsTimeContext}
-                          onRefresh={() => applyMetricsQuery(draftTimeContext)}
-                          onReset={() => applyMetricsTimeContext({ timeRange: 'last-30m' })}
-                          presets={metricsTimeRangePresets}
-                          showAbsoluteFields
-                          variant="narrow-rail"
-                          className="justify-end"
-                          presetSelectProps={{ 'data-otlp-metrics-time-range-select': 'true' }}
-                          presetOptionDataAttribute="data-otlp-metrics-time-range-preset"
-                          refreshActionProps={{ 'data-otlp-metrics-time-refresh-action': 'true' }}
-                        />
-                      </div>
-                    </div>
-                  </div>
-                </div>
-              </section>
-
-              <section data-otlp-metrics-query-bar="cold-query-row" className={`${panelClass} px-4 py-3`}>
-                <div className="flex flex-wrap items-center gap-2">
-                  <div className="relative min-w-[320px] max-w-[560px] flex-1">
-                    <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#7d8798]" aria-hidden="true" />
-                    <Input
-                      data-otlp-metrics-query-input="true"
-                      aria-label="指标查询"
-                      value={draft.query || mergedData.query || ''}
-                      onChange={event => updateDraftField('query', event.target.value)}
-                      onInput={event => updateDraftField('query', event.currentTarget.value)}
-                      placeholder="http.server.duration"
-                      className={`${inputClass} w-full pl-9 font-mono`}
-                    />
-                  </div>
-                  <Select
-                    data-otlp-metrics-aggregation-select="true"
-                    aria-label="聚合方式"
-                    value={draft.aggregation}
-                    onChange={event => updateDraftField('aggregation', event.target.value)}
-                    containerClassName="w-[124px]"
-                    className="h-8 min-w-0 text-[#d5dce8]"
-                  >
-                    <option value="avg">平均</option>
-                    <option value="sum">求和</option>
-                    <option value="max">最大</option>
-                    <option value="min">最小</option>
-                  </Select>
-                  <Select
-                    data-otlp-metrics-group-by-select="true"
-                    aria-label="分组"
-                    value={draft.groupBy}
-                    onChange={event => updateDraftField('groupBy', event.target.value)}
-                    containerClassName="w-[132px]"
-                    className="h-8 min-w-0 text-[#d5dce8]"
-                  >
-                    <option value="service_name">服务</option>
-                    <option value="service_namespace">命名空间</option>
-                    <option value="deployment_environment_name">环境</option>
-                  </Select>
-                  <button data-otlp-metrics-run-query-action="true" type="button" className={primaryActionClass} onClick={applyMetricsQuery}>
-                    <Play className="h-4 w-4" aria-hidden="true" />
-                    运行查询
-                  </button>
-                  <Link href="/ingestion/otlp/metrics" className={actionClass}>
-                    <RotateCcw className="h-4 w-4" aria-hidden="true" />
-                    重置
-                  </Link>
-                </div>
-                <div className="mt-2 flex flex-wrap items-center gap-2">
-                  <Input
-                    data-otlp-metrics-service-input="true"
-                    aria-label="服务名称"
-                    value={draft.serviceName || mergedData.context?.serviceName || ''}
-                    onChange={event => updateDraftField('serviceName', event.target.value)}
-                    onInput={event => updateDraftField('serviceName', event.currentTarget.value)}
-                    placeholder="服务名称"
-                    className={`${inputClass} w-[220px]`}
-                  />
-                  <Input
-                    data-otlp-metrics-namespace-input="true"
-                    aria-label="命名空间"
-                    value={draft.serviceNamespace || mergedData.context?.serviceNamespace || ''}
-                    onChange={event => updateDraftField('serviceNamespace', event.target.value)}
-                    onInput={event => updateDraftField('serviceNamespace', event.currentTarget.value)}
-                    placeholder="命名空间"
-                    className={`${inputClass} w-[220px]`}
-                  />
-                  <Input
-                    data-otlp-metrics-environment-input="true"
-                    aria-label="环境"
-                    value={draft.environment || mergedData.context?.environment || ''}
-                    onChange={event => updateDraftField('environment', event.target.value)}
-                    onInput={event => updateDraftField('environment', event.currentTarget.value)}
-                    placeholder="环境"
-                    className={`${inputClass} w-[160px]`}
-                  />
-                  <Input
-                    data-otlp-metrics-trace-id-input="true"
-                    aria-label="链路 ID"
-                    value={draft.traceId}
-                    onChange={event => updateDraftField('traceId', event.target.value)}
-                    onInput={event => updateDraftField('traceId', event.currentTarget.value)}
-                    placeholder="链路 ID"
-                    className={`${inputClass} min-w-[220px] max-w-[360px] flex-1 font-mono`}
-                  />
-                </div>
-              </section>
-
-              <section data-otlp-metrics-chart-band="cold-chart-band" className={`${panelClass} px-4 py-4`}>
-                <div
-                  data-otlp-metrics-chart-layout="wide-series-chart-with-compact-meta"
-                  data-otlp-metrics-series-mode="entity-series-set"
-                  className="grid items-start gap-3"
-                >
-                  <div
-                    data-otlp-metrics-chart-panel="series-set-trend"
-                    className="min-w-0 rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3"
-                    data-otlp-metrics-chart-datazoom-state="local-observation"
-                    data-otlp-metrics-chart-datazoom-preserve="preserved"
-                  >
-                    <div className="mb-3 flex flex-wrap items-center justify-between gap-2 text-[12px] text-[#8792a5]">
-                      <span className="inline-flex items-center gap-2 font-semibold text-[#c6cfdd]">
-                        <BarChart3 className="h-4 w-4" aria-hidden="true" />
-                        趋势带
-                      </span>
-                      <span className="inline-flex min-w-0 flex-wrap items-center justify-end gap-2">
-                        {hasMetricSeries ? (
-                          <span data-otlp-metrics-chart-meta-row="compact-real-facts" className="inline-flex min-w-0 flex-wrap items-center justify-end gap-1.5">
-                            {facts.map(fact => (
-                              <span
-                                key={fact.label}
-                                data-otlp-metrics-chart-meta-fact="true"
-                                className="inline-flex h-6 min-w-0 max-w-[180px] items-center gap-1 rounded-[3px] border border-[#252b35] bg-[#0d1015] px-2"
-                              >
-                                <span className="truncate text-[10px] font-semibold text-[#7f8a9d]">{fact.label}</span>
-                                <span className="truncate text-[11px] font-semibold text-[#cfd8e6]">{fact.value}</span>
-                              </span>
-                            ))}
-                          </span>
-                        ) : null}
-                        {metricsChartZoomDraftLabel ? (
-                          <span
-                            data-otlp-metrics-chart-zoom-draft="pending-query-time"
-                            data-otlp-metrics-chart-zoom-draft-state="pending"
-                            className="inline-flex h-7 max-w-[340px] items-center gap-1.5 overflow-hidden rounded-[3px] border border-[#344052] bg-[#121823] px-2 text-[11px] font-semibold text-[#cfd8e6]"
-                          >
-                            <span className="shrink-0 text-[#7f8a9d]">{t('time.context.zoom.draft')}</span>
-                            <span className="min-w-0 truncate font-mono">{metricsChartZoomDraftLabel}</span>
-                          </span>
-                        ) : null}
-                        {hasMetricSeries ? (
-                          <button
-                            type="button"
-                            className="inline-flex h-7 items-center justify-center rounded-[3px] border border-[#2b3039] bg-transparent px-2 text-[11px] font-semibold text-[#9ca7ba] transition-colors enabled:hover:border-[#3b4454] enabled:hover:bg-[#151821] enabled:hover:text-[#e6edf7] disabled:cursor-not-allowed disabled:opacity-45"
-                            data-otlp-metrics-chart-zoom-apply="local-to-query-time"
-                            data-otlp-metrics-chart-zoom-apply-state={canApplyMetricsChartZoom ? 'ready' : 'idle'}
-                            disabled={!canApplyMetricsChartZoom}
-                            onClick={() => {
-                              if (!metricsChartZoomContext) return;
-                              setMetricsChartZoomRange(null);
-                              applyMetricsTimeContext(metricsChartZoomContext);
-                            }}
-                          >
-                            {t('time.context.zoom.apply')}
-                          </button>
-                        ) : null}
-                        <span>{workbenchState.seriesCountLabel}</span>
-                      </span>
-                    </div>
-                    {hasMetricSeries ? (
-                      <EChartsPanel
-                        option={metricsChartOption}
-                        height={300}
-                        className="rounded-none border-x-0 border-y border-[#252b35] bg-transparent"
-                        tone="operator"
-                        preserveDataZoom
-                        onDataZoomChange={nextZoom => {
-                          handleMetricsChartZoomChange(nextZoom);
-                          const nextZoomContext = buildMetricsDataZoomTimeContext(
-                            metricSeries,
-                            nextZoom,
-                            draftTimeContext.timeRange || query.timeRange || 'last-30m'
-                          );
-                          if (!nextZoomContext) return;
-                          setDraft(previous => ({
-                            ...previous,
-                            timeRange: nextZoomContext.timeRange || previous.timeRange || 'last-30m',
-                            start: nextZoomContext.start || '',
-                            end: nextZoomContext.end || '',
-                            refresh: nextZoomContext.refresh || previous.refresh || draftTimeContext.refresh || '',
-                            live: nextZoomContext.live || previous.live || draftTimeContext.live || '',
-                            tz: nextZoomContext.tz || previous.tz || draftTimeContext.tz || ''
-                          }));
-                        }}
-                      />
-                    ) : (
-                      <div className="flex h-16 items-end gap-1.5">
-                        {trendBars.length ? (
-                          trendBars.map(series => (
-                            <span
-                              key={series.key}
-                              data-otlp-metrics-trend-bar={'real-series-point'}
-                              className="min-w-0 flex-1 rounded-t-[3px] border border-[#2f3b4d] bg-[#182232]"
-                              style={{ height: `${series.heightPct}%` }}
-                              title={`${series.seriesName} · ${series.label} · ${series.valueLabel}`}
-                            />
-                          ))
-                        ) : (
-                          <div
-                            data-otlp-metrics-trend-empty="no-real-series"
-                            data-otlp-metrics-empty-state="honest-no-series"
-                            data-otlp-metrics-empty-state-context="applied-query-visible"
-                            className="flex h-full min-w-0 flex-1 items-center justify-center rounded-[3px] border border-dashed border-[#2a303a] bg-[#0c1016] px-3 text-center"
-                          >
-                            <div className="min-w-0">
-                              <p className="text-[12px] font-semibold text-[#a7b0bf]">暂无指标趋势</p>
-                              <p className="mt-1 text-[11px] leading-4 text-[#657083]">运行查询后展示真实采样点。</p>
-                            </div>
-                          </div>
-                        )}
-                      </div>
-                    )}
-                    {!hasMetricSeries ? null : (
-                      <div className="mt-2 text-[11px] text-[#6d7788]">
-                        {trendBars.length ? `${trendBars.length} 个采样点` : '-'}
-                      </div>
-                    )}
-                  </div>
-                </div>
-              </section>
-
-              <section data-otlp-metrics-workbench-grid="series-detail-split" className={hasMetricSeries ? 'grid items-start gap-4 xl:grid-cols-[minmax(0,1fr)_320px]' : 'grid items-start gap-4'}>
-                <div
-                  data-otlp-metrics-series-table="cold-dense-metric-list"
-                  data-otlp-metrics-series-table-mode="service-entity-series-set"
-                  data-otlp-metrics-series-table-density="primary-scan"
-                  className={`${panelClass} min-w-0 overflow-hidden`}
-                >
-                  <div className="border-b border-[#252b35] px-4 py-3">
-                    <div className="flex min-w-0 items-center justify-between gap-3">
-                      <p className="text-[12px] font-semibold text-[#8792a5]">序列集合</p>
-                      <span
-                        data-otlp-metrics-series-table-summary="result-count"
-                        className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#9ca7ba]"
-                      >
-                        {seriesRows.length} 条
-                      </span>
-                    </div>
-                    <div
-                      data-otlp-metrics-series-set-summary="service-entity-scope"
-                      className="mt-3 grid gap-px overflow-hidden rounded-[3px] border border-[#252b35] bg-[#252b35] sm:grid-cols-2 xl:grid-cols-5"
-                    >
-                      {seriesSetScopeRows.map(row => (
-                        <div key={row.label} className="min-w-0 bg-[#10141b] px-2.5 py-2">
-                          <p className="truncate text-[10px] font-semibold text-[#7f8a9d]">{row.label}</p>
-                          <p className="mt-1 truncate text-[12px] font-semibold text-[#e6edf7]">{row.value}</p>
-                        </div>
-                      ))}
-                    </div>
-                  </div>
-                  <table className="w-full border-collapse text-left text-[12px]">
-                    <thead className="border-b border-[#252b35] bg-[#10141b] text-[#8792a5]">
-                      <tr>
-                        <th className="px-4 py-3 font-semibold">指标名称</th>
-                        <th className="px-4 py-3 font-semibold">服务</th>
-                        <th className="px-4 py-3 font-semibold">实体</th>
-                        <th className="px-4 py-3 font-semibold">最新值</th>
-                        <th className="px-4 py-3 font-semibold">采样点</th>
-                        <th className="px-4 py-3 font-semibold">最近时间</th>
-                      </tr>
-                    </thead>
-                    <tbody>
-                      {seriesRows.length ? (
-                        seriesRows.map((row, index) => {
-                          const series = metricSeries[index];
-                          const isSelectedSeries = selectedMetricSeries?.key === series?.key;
-                          return (
-                            <tr
-                              key={`${row.title}-${index}`}
-                              data-otlp-metrics-series-row="selectable-series"
-                              data-otlp-metrics-series-row-selected={isSelectedSeries ? 'true' : 'false'}
-                              className={`cursor-pointer border-b border-[#1f2530] last:border-b-0 ${isSelectedSeries ? 'bg-[#131b28]' : 'hover:bg-[#111721]'}`}
-                              tabIndex={0}
-                              onClick={() => {
-                                if (series) setSelectedSeriesKey(series.key);
-                              }}
-                              onKeyDown={event => {
-                                if ((event.key === 'Enter' || event.key === ' ') && series) {
-                                  event.preventDefault();
-                                  setSelectedSeriesKey(series.key);
-                                }
-                              }}
-                            >
-                            <td className="max-w-[320px] truncate px-4 py-3 font-mono text-[12px] font-semibold text-[#edf3fb]">{row.title}</td>
-                            <td className="px-4 py-3 text-[#c8d2df]">{row.copy}</td>
-                            <td data-otlp-metrics-series-entity="true" className="min-w-[140px] px-4 py-3">
-                              <span className="block truncate font-semibold text-[#dbe5f3]">{row.entityLabel}</span>
-                              <span
-                                className={`mt-1 block truncate text-[11px] ${
-                                  row.entityState === 'present' ? 'text-[#75c795]' : 'text-[#8b94a4]'
-                                }`}
-                              >
-                                {row.entityMeta}
-                              </span>
-                            </td>
-                            <td className="px-4 py-3 font-mono text-[#e6edf7]">{row.meta}</td>
-                            <td className="px-4 py-3 text-[#c8d2df]">{metricSeries[index]?.points.length ?? 0}</td>
-                            <td className="px-4 py-3 font-mono text-[11px] text-[#8792a5]">{formatTime(latestObservedAt)}</td>
-                            </tr>
-                          );
-                        })
-                      ) : (
-                        <tr>
-                          <td colSpan={6} className="h-[260px] text-center">
-                            <div className="mx-auto flex max-w-[280px] flex-col items-center">
-                              <Database className="h-8 w-8 text-[#7d8798]" aria-hidden="true" />
-                              <p className="mt-3 text-[14px] font-semibold text-[#edf3fb]">{workbenchState.emptyTitle}</p>
-                              <p
-                                data-otlp-metrics-empty-guidance="operator-no-data-guidance"
-                                className="mt-2 text-[12px] leading-5 text-[#7f8a9d]"
-                              >
-                                {workbenchState.noMetricsTitle}
-                              </p>
-                            </div>
-                          </td>
-                        </tr>
-                      )}
-                    </tbody>
-                  </table>
-                </div>
-
-                {hasMetricSeries && selectedMetricSeries ? (
-                <aside
-                  data-otlp-metrics-detail-panel="cold-detail-panel"
-                  data-otlp-metrics-detail-panel-priority="secondary-inspector"
-                  className={`${panelClass} min-w-0 overflow-hidden xl:sticky xl:top-4 xl:self-start`}
-                >
-                  <div className="border-b border-[#252b35] px-4 py-3">
-                    <p className="text-[12px] font-semibold text-[#8792a5]">详情面板</p>
-                    <h2 className="mt-2 truncate text-[18px] font-semibold text-[#f2f6fb]">{firstSeries.title}</h2>
-                    <p className="mt-1 truncate text-[11px] text-[#6d7788]">{workbenchState.chartLabel}</p>
-                  </div>
-                  <div data-otlp-metrics-detail-panel-body="compact-evidence-stack" className="px-4 py-3">
-                    <div className="grid grid-cols-2 gap-px overflow-hidden rounded-[3px] border border-[#252b35] bg-[#252b35]">
-                      {metrics.map(metric => (
-                        <div key={metric.label} className="min-w-0 bg-[#10141b] px-3 py-2">
-                          <p className="truncate text-[11px] font-semibold text-[#8792a5]">{metric.label}</p>
-                          <p className="mt-1 truncate text-[13px] font-semibold text-[#e6edf7]">{metric.value}</p>
-                        </div>
-                      ))}
-                      {contextRows.map(row => (
-                        <div key={row.title} className="col-span-2 min-w-0 bg-[#10141b] px-3 py-2">
-                          <p className="truncate text-[11px] font-semibold text-[#8792a5]">{row.title}</p>
-                          <p className="mt-1 truncate text-[13px] font-semibold text-[#e6edf7]">{row.copy}</p>
-                          <p className="mt-1 truncate text-[11px] text-[#6d7788]">{row.meta}</p>
-                        </div>
-                      ))}
-                    </div>
-                    {selectedSeriesContextRows.length > 0 ? (
-                      <div
-                        data-otlp-metrics-selected-series-context="selected-series-attribution"
-                        aria-label="当前选中序列 关联实体 当前服务 采集模板 当前环境"
-                        className="mt-3 border-t border-[#252b35] pt-3"
-                      >
-                        <div className="mb-2 flex items-center justify-between gap-2">
-                          <p className="text-[11px] font-semibold text-[#8792a5]">当前选中序列</p>
-                          <span className="text-[11px] font-semibold text-[#6d7788]">采集模板</span>
-                        </div>
-                        <div className="space-y-2">
-                          {selectedSeriesContextRows.map(row => (
-                            <div key={row.label} className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]">
-                              <span className="text-[#7f8a9d]">{row.label}</span>
-                              <span className="min-w-0">
-                                <span className="block truncate font-semibold text-[#e6edf7]">{row.value}</span>
-                                <span className="block truncate text-[#6d7788]">{row.meta}</span>
-                              </span>
-                            </div>
-                          ))}
-                        </div>
-                      </div>
-                    ) : null}
-                    {selectedSeriesEvidenceRows.length > 0 ? (
-                      <div
-                        data-otlp-metrics-selected-series-evidence="real-sample-evidence"
-                        aria-label="指标证据 采样点 最新值 值域 采样窗口 关联链路"
-                        className="mt-3 border-t border-[#252b35] pt-3"
-                      >
-                        <div className="mb-2 flex items-center justify-between gap-2">
-                          <p className="text-[11px] font-semibold text-[#8792a5]">指标证据</p>
-                          <span className="text-[11px] font-semibold text-[#6d7788]">真实采样</span>
-                        </div>
-                        <div className="space-y-2">
-                          {selectedSeriesEvidenceRows.map(row => (
-                            <div key={row.label} className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]">
-                              <span className="text-[#7f8a9d]">{row.label}</span>
-                              <span className="min-w-0">
-                                <span className="block truncate font-semibold text-[#e6edf7]">{row.value}</span>
-                                <span className="block truncate text-[#6d7788]">{row.meta}</span>
-                              </span>
-                            </div>
-                          ))}
-                        </div>
-                      </div>
-                    ) : null}
-                  </div>
-                  <div data-otlp-metrics-handoff-actions="compact-context-actions" className="grid grid-cols-2 gap-2 border-t border-[#252b35] px-4 py-3">
-                    {canOpenEntity ? (
-                      <Link href={handoffLinks.entityHref} data-otlp-metrics-entity-action="true" className={`${actionClass} w-full px-2`}>
-                        实体详情
-                      </Link>
-                    ) : (
-                      <span className="inline-flex" title={missingEntityHandoffTitle}>
-                        <button
-                          data-otlp-metrics-entity-action-disabled="missing-entity-id"
-                          type="button"
-                          className={`${disabledActionClass} w-full px-2`}
-                          disabled
-                          title={missingEntityHandoffTitle}
-                          aria-label={missingEntityHandoffTitle}
-                        >
-                          实体详情
-                        </button>
-                      </span>
-                    )}
-                    <Link href={handoffLinks.alertHandlingHref} data-otlp-metrics-alert-handling-action="true" className={`${actionClass} w-full px-2`}>
-                      告警处理
-                    </Link>
-                    <Link href={handoffLinks.logsHref} data-otlp-metrics-logs-action="true" className={`${actionClass} w-full px-2`}>
-                      查看日志
-                    </Link>
-                    <Link href={handoffLinks.tracesHref} data-otlp-metrics-traces-action="true" className={`${actionClass} w-full px-2`}>
-                      查看链路
-                    </Link>
-                    <Link href={handoffLinks.entitiesHref} className={`${actionClass} w-full px-2`}>
-                      对象目录
-                    </Link>
-                  </div>
-                  <div data-otlp-metrics-secondary-context="collapsible-evidence-diagnostics" className="space-y-2 border-t border-[#252b35] px-4 py-3">
-                    {linkedRecordRows.length > 0 ? (
-                      <details
-                        data-otlp-metrics-linked-record-summary="log-trace-alert-links"
-                        data-otlp-metrics-linked-record-summary-panel="collapsible"
-                        aria-label="关联记录 历史日志 链路瀑布图 告警处理"
-                        className="rounded-[3px] border border-[#252b35] bg-[#0d1015]"
-                      >
-                        <summary className="flex cursor-pointer list-none items-center justify-between gap-2 px-2.5 py-2 text-[11px] font-semibold text-[#8792a5]">
-                          <span>关联记录</span>
-                          <span className="text-[#6d7788]">日志 / 链路 / 告警</span>
-                        </summary>
-                        <div className="space-y-2 border-t border-[#252b35] p-2">
-                          {linkedRecordRows.map(row => (
-                            <Link
-                              key={row.key}
-                              href={row.href}
-                              data-otlp-metrics-linked-record-action={row.key}
-                              className="block rounded-[3px] border border-[#252b35] bg-[#0b0f15] px-2.5 py-2 transition-colors hover:border-[#344052] hover:bg-[#121823]"
-                            >
-                              <span className="flex min-w-0 items-center justify-between gap-2 text-[11px]">
-                                <span className="truncate text-[#7f8a9d]">{row.label}</span>
-                                <span className="shrink-0 font-semibold text-[#e6edf7]">{row.value}</span>
-                              </span>
-                              <span className="mt-1 block truncate text-[11px] text-[#6d7788]">{row.meta}</span>
-                            </Link>
-                          ))}
-                        </div>
-                      </details>
-                    ) : null}
-                    {attributionDiagnostics.length > 0 ? (
-                      <details
-                        data-otlp-metrics-attribution-diagnostics="hertzbeat-attribute-diagnostics"
-                        data-otlp-metrics-attribution-diagnostics-panel="collapsible"
-                        aria-label="归因诊断 hertzbeat.entity_id hertzbeat.entity_name hertzbeat.workspace_id hertzbeat.collector hertzbeat.template"
-                        className="rounded-[3px] border border-[#252b35] bg-[#0d1015]"
-                      >
-                        <summary className="flex cursor-pointer list-none items-center justify-between gap-2 px-2.5 py-2 text-[11px] font-semibold text-[#8792a5]">
-                          <span>归因诊断</span>
-                          <span className="text-[#6d7788]">hertzbeat.*</span>
-                        </summary>
-                        <div className="space-y-2 border-t border-[#252b35] p-2">
-                          {attributionDiagnostics.map(row => (
-                            <div
-                              key={row.key}
-                              data-otlp-metrics-attribution-diagnostic-state={row.state}
-                              className="grid grid-cols-[minmax(0,1fr)_52px] gap-2 text-[11px]"
-                            >
-                              <span className="min-w-0">
-                                <span className="block truncate font-mono font-semibold text-[#e6edf7]">{row.label}</span>
-                                <span className="block truncate text-[#6d7788]">{row.value} · {row.meta}</span>
-                              </span>
-                              <span
-                                className={`inline-flex h-5 items-center justify-center rounded-[3px] border px-1.5 font-semibold ${
-                                  row.state === 'present'
-                                    ? 'border-[#24543b] bg-[#0d241a] text-[#76d69b]'
-                                    : 'border-[#4a3140] bg-[#24111a] text-[#d68aa6]'
-                                }`}
-                              >
-                                {row.state === 'present' ? '已提供' : '缺失'}
-                              </span>
-                            </div>
-                          ))}
-                        </div>
-                      </details>
-                    ) : null}
-                    <details
-                      data-otlp-metrics-entity-context="hertzbeat-signal-entity-context"
-                      data-otlp-metrics-entity-context-panel="collapsible"
-                      aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"
-                      className="rounded-[3px] border border-[#252b35] bg-[#0d1015]"
-                    >
-                      <summary className="flex cursor-pointer list-none items-center justify-between gap-2 px-2.5 py-2 text-[11px] font-semibold text-[#8792a5]">
-                        <span>实体上下文</span>
-                        <span className="text-[#6d7788]">当前窗口</span>
-                      </summary>
-                      <div className="space-y-2 border-t border-[#252b35] p-2">
-                        {entityContextRows.map(row => (
-                          <div key={row.label} className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]">
-                            <span className="text-[#7f8a9d]">{row.label}</span>
-                            <span className="min-w-0">
-                              <span className="block truncate font-semibold text-[#e6edf7]">{row.value}</span>
-                              <span className="block truncate text-[#6d7788]">{row.meta}</span>
-                            </span>
-                          </div>
-                        ))}
-                      </div>
-                    </details>
-                  </div>
-                </aside>
-                ) : (
-                  <span data-otlp-metrics-detail-panel-empty="suppressed-until-real-series" className="sr-only" />
-                )}
-              </section>
-          </main>
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function OtlpMetricsRoutePage() {
+  return <OtlpMetricsPage />;
 }
diff --git a/web-next/app/ingestion/otlp/metrics/route-state.test.ts b/web-next/app/ingestion/otlp/metrics/route-state.test.ts
index 085115d2af..a9044ac7f5 100644
--- a/web-next/app/ingestion/otlp/metrics/route-state.test.ts
+++ b/web-next/app/ingestion/otlp/metrics/route-state.test.ts
@@ -88,4 +88,22 @@ describe('otlp metrics route state', () => {
       '/ingestion/otlp/metrics?timeRange=last-6h&serviceName=checkout&start=1712730000000&end=1712751600000&refresh=30&live=false&tz=Asia%2FShanghai'
     );
   });
+
+  it('keeps readable expression time windows in metrics workbench URLs', () => {
+    const route = buildOtlpMetricsRoute({
+      serviceName: 'checkout',
+      timeRange: 'last-1h',
+      from: '2026-05-17 15:30:00',
+      to: '2026-05-17 16:30:00',
+      timezone: 'Asia/Shanghai',
+      start: '1779003000000',
+      end: '1779006600000'
+    });
+
+    expect(route).toBe(
+      '/ingestion/otlp/metrics?timeRange=last-1h&from=2026-05-17+15%3A30%3A00&to=2026-05-17+16%3A30%3A00&serviceName=checkout&timezone=Asia%2FShanghai'
+    );
+    expect(route).not.toContain('start=');
+    expect(route).not.toContain('end=');
+  });
 });
diff --git a/web-next/app/ingestion/otlp/metrics/route-state.ts b/web-next/app/ingestion/otlp/metrics/route-state.ts
index 945e0df745..35ea0a16db 100644
--- a/web-next/app/ingestion/otlp/metrics/route-state.ts
+++ b/web-next/app/ingestion/otlp/metrics/route-state.ts
@@ -29,6 +29,8 @@ const METRICS_ROUTE_KEYS: Array<keyof OtlpMetricsQueryState> = [
   'aggregation',
   'groupBy',
   'timeRange',
+  'from',
+  'to',
   'serviceName',
   'serviceNamespace',
   'environment',
@@ -40,6 +42,7 @@ const METRICS_ROUTE_KEYS: Array<keyof OtlpMetricsQueryState> = [
   'refresh',
   'live',
   'tz',
+  'timezone',
   'codeRepo',
   'codeProvider',
   'codePath',
@@ -59,7 +62,7 @@ function appendRouteValue(params: URLSearchParams, key: keyof OtlpMetricsQuerySt
     if (entityId) params.set(key, entityId);
     return;
   }
-  if (key === 'timeRange' || key === 'start' || key === 'end' || key === 'refresh' || key === 'live' || key === 'tz') {
+  if (key === 'timeRange' || key === 'from' || key === 'to' || key === 'start' || key === 'end' || key === 'refresh' || key === 'live' || key === 'tz' || key === 'timezone') {
     const timeValue = normalizeTimeContextValue(key, value);
     if (timeValue) params.set(key, timeValue);
     return;
@@ -83,6 +86,16 @@ export function hasMetricsUnsanitizedTimeBounds(searchParams: SearchParamReader)
 
 export function buildOtlpMetricsRoute(query: OtlpMetricsQueryState) {
   const params = new URLSearchParams();
-  METRICS_ROUTE_KEYS.forEach(key => appendRouteValue(params, key, query[key]));
+  const expressionFrom = normalizeTimeContextValue('from', query.from);
+  const expressionTo = normalizeTimeContextValue('to', query.to);
+  const expressionOwnsRoute = Boolean(expressionFrom && expressionTo);
+  METRICS_ROUTE_KEYS.forEach(key => {
+    if (expressionOwnsRoute && (key === 'start' || key === 'end')) return;
+    if (expressionOwnsRoute && key === 'tz') {
+      if (!query.timezone) appendRouteValue(params, 'timezone', query.tz);
+      return;
+    }
+    appendRouteValue(params, key, query[key]);
+  });
   return params.toString() ? `/ingestion/otlp/metrics?${params.toString()}` : '/ingestion/otlp/metrics';
 }
diff --git a/web-next/app/ingestion/otlp/page.tsx b/web-next/app/ingestion/otlp/otlp-page.tsx
similarity index 71%
copy from web-next/app/ingestion/otlp/page.tsx
copy to web-next/app/ingestion/otlp/otlp-page.tsx
index 71b73d682b..94953c9795 100644
--- a/web-next/app/ingestion/otlp/page.tsx
+++ b/web-next/app/ingestion/otlp/otlp-page.tsx
@@ -22,8 +22,13 @@ import { ClientWorkbench } from '@/components/workbench/client-workbench';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { Button } from '@/components/ui/button';
 import { apiMessageGet } from '@/lib/api-client';
-import { loadOtlpPageData } from '@/lib/otlp-center/controller';
-import { buildCollectionLoopLinks, buildReadinessRows, buildSelfCheckRows } from '@/lib/otlp-center/view-model';
+import { OTLP_BINDINGS_URL, OTLP_GUIDE_URL, OTLP_OVERVIEW_URL, loadOtlpPageData } from '@/lib/otlp-center/controller';
+import {
+  buildCollectionLoopLinks,
+  buildReadinessRows,
+  buildSelfCheckRows,
+  buildUnboundCandidateRows
+} from '@/lib/otlp-center/view-model';
 import type { OtlpEntityBindingSummary, OtlpIngestionGuide, OtlpIngestionOverview } from '@/lib/types';
 import { SearchRow } from '../../../components/ui/search-row';
 import { coldOpsCatalogVisual } from '../../../lib/cold-ops-visual';
@@ -259,12 +264,16 @@ export default function OtlpPage() {
   const { t } = useI18n();
   const searchParams = useSearchParams();
   const [sourceSearch, setSourceSearch] = useState('');
+  const otlpCenterCacheKey = React.useMemo(
+    () => ['otlp-center', OTLP_OVERVIEW_URL, OTLP_GUIDE_URL, OTLP_BINDINGS_URL].join(':'),
+    []
+  );
   const load = useCallback(async (): Promise<OtlpPageData> => {
     return loadOtlpPageData(apiMessageGet);
   }, []);
 
   return (
-    <ClientWorkbench load={load} loadingCopy={t('otlp.loading')}>
+    <ClientWorkbench load={load} loadingCopy={t('otlp.loading')} cacheKey={otlpCenterCacheKey}>
       {data => {
         const routeContext = readSignalRouteContext(searchParams);
         const rawSearch = searchParams.toString();
@@ -278,12 +287,12 @@ export default function OtlpPage() {
         const sourceSections: SourceSection[] = [
           {
             key: 'quickstart',
-            label: '快速开始',
+            label: t('otlp.source.section.quickstart'),
             items: [
               {
                 key: 'demo-data',
-                label: '5 分钟内接入演示数据',
-                description: '写入一条演示 OTLP 数据流,并立即查看工作台。',
+                label: t('otlp.source.item.demo-data.label'),
+                description: t('otlp.source.item.demo-data.description'),
                 href: withSearch('/overview'),
                 icon: Zap
               }
@@ -291,54 +300,54 @@ export default function OtlpPage() {
           },
           {
             key: 'migrate',
-            label: '现有接入',
+            label: t('otlp.source.section.migrate'),
             items: [
               {
                 key: 'open-telemetry',
-                label: '已有 OpenTelemetry',
-                description: '复用现有 OpenTelemetry Collector 或 SDK 管道。',
+                label: t('otlp.source.item.open-telemetry.label'),
+                description: t('otlp.source.item.open-telemetry.description'),
                 href: withSearch('/entities/discovery'),
                 icon: Network
               },
               {
                 key: 'grafana',
                 label: 'Grafana',
-                description: '将 Grafana 侧的信号接入 HertzBeat。',
+                description: t('otlp.source.item.grafana.description'),
                 href: withSearch('/ingestion/otlp/metrics'),
                 icon: Activity
               },
               {
                 key: 'elk',
                 label: 'ELK',
-                description: '把日志源路由到日志工作台。',
+                description: t('otlp.source.item.elk.description'),
                 href: withSearch('/log/manage'),
                 icon: Layers3
               },
               {
                 key: 'new-relic',
                 label: 'New Relic',
-                description: '把服务遥测映射到对象目录。',
+                description: t('otlp.source.item.new-relic.description'),
                 href: withSearch('/entities'),
                 icon: Boxes
               },
               {
                 key: 'commercial-observability',
-                label: '已有可观测平台',
-                description: '接入服务、日志和指标后,在对象目录和排障工作台继续处理。',
+                label: t('otlp.source.item.commercial-observability.label'),
+                description: t('otlp.source.item.commercial-observability.description'),
                 href: withSearch('/entities/discovery'),
                 icon: Activity
               },
               {
                 key: 'honeycomb',
                 label: 'Honeycomb',
-                description: '把链路数据接入链路工作台,并关联服务与日志。',
+                description: t('otlp.source.item.honeycomb.description'),
                 href: withSearch('/explorer'),
                 icon: Network
               },
               {
                 key: 'self-hosted-observability',
-                label: '自托管 OTLP 管道',
-                description: '复用内网或离线环境中的 OTLP Collector 管道。',
+                label: t('otlp.source.item.self-hosted-observability.label'),
+                description: t('otlp.source.item.self-hosted-observability.description'),
                 href: withSearch('/ingestion/otlp'),
                 icon: Server
               }
@@ -346,77 +355,78 @@ export default function OtlpPage() {
           },
           {
             key: 'apm-traces',
-            label: 'APM / 链路',
+            label: t('otlp.source.section.apm-traces'),
             items: [
-              { key: 'java', label: 'Java', description: 'Java OpenTelemetry SDK 配置。', href: withSearch('/trace/manage'), icon: Code2 },
-              { key: 'python', label: 'Python', description: 'Python OpenTelemetry SDK 配置。', href: withSearch('/trace/manage'), icon: Braces },
-              { key: 'nodejs', label: 'JavaScript', description: 'Node.js 与浏览器链路配置。', href: withSearch('/trace/manage'), icon: Globe2 },
-              { key: 'golang', label: 'Golang', description: 'Go 服务链路配置。', href: withSearch('/trace/manage'), icon: Network },
-              { key: 'dotnet', label: '.NET', description: '.NET OpenTelemetry 埋点配置。', href: withSearch('/trace/manage'), icon: Code2 },
-              { key: 'deno', label: 'Deno', description: 'Deno 运行时链路配置。', href: withSearch('/trace/manage'), icon: Braces },
-              { key: 'php', label: 'PHP', description: 'PHP 应用链路配置。', href: withSearch('/trace/manage'), icon: Code2 },
-              { key: 'ruby', label: 'Ruby', description: 'Ruby 服务链路配置。', href: withSearch('/trace/manage'), icon: Braces },
-              { key: 'rust', label: 'Rust', description: 'Rust OpenTelemetry 配置。', href: withSearch('/trace/manage'), icon: Code2 },
-              { key: 'nginx-tracing', label: 'Nginx 链路', description: '追踪反向代理和边缘服务调用。', href: withSearch('/trace/manage'), icon: Server },
-              { key: 'external-api', label: '外部 API 监控', description: '跟踪外部 API 延迟和错误。', href: withSearch('/trace/manage'), icon: Server },
-              { key: 'web-vitals', label: 'Web Vitals 链路', description: '前端链路和核心体验指标配置。', href: withSearch('/trace/manage'), icon: Activity },
-              { key: 'temporal', label: 'Temporal', description: '追踪 Temporal 工作流和活动。', href: withSearch('/trace/manage'), icon: Layers3 }
+              { key: 'java', label: 'Java', description: t('otlp.source.item.java.description'), href: withSearch('/trace/manage'), icon: Code2 },
+              { key: 'python', label: 'Python', description: t('otlp.source.item.python.description'), href: withSearch('/trace/manage'), icon: Braces },
+              { key: 'nodejs', label: 'JavaScript', description: t('otlp.source.item.nodejs.description'), href: withSearch('/trace/manage'), icon: Globe2 },
+              { key: 'golang', label: 'Golang', description: t('otlp.source.item.golang.description'), href: withSearch('/trace/manage'), icon: Network },
+              { key: 'dotnet', label: '.NET', description: t('otlp.source.item.dotnet.description'), href: withSearch('/trace/manage'), icon: Code2 },
+              { key: 'deno', label: 'Deno', description: t('otlp.source.item.deno.description'), href: withSearch('/trace/manage'), icon: Braces },
+              { key: 'php', label: 'PHP', description: t('otlp.source.item.php.description'), href: withSearch('/trace/manage'), icon: Code2 },
+              { key: 'ruby', label: 'Ruby', description: t('otlp.source.item.ruby.description'), href: withSearch('/trace/manage'), icon: Braces },
+              { key: 'rust', label: 'Rust', description: t('otlp.source.item.rust.description'), href: withSearch('/trace/manage'), icon: Code2 },
+              { key: 'nginx-tracing', label: t('otlp.source.item.nginx-tracing.label'), description: t('otlp.source.item.nginx-tracing.description'), href: withSearch('/trace/manage'), icon: Server },
+              { key: 'external-api', label: t('otlp.source.item.external-api.label'), description: t('otlp.source.item.external-api.description'), href: withSearch('/trace/manage'), icon: Server },
+              { key: 'web-vitals', label: t('otlp.source.item.web-vitals.label'), description: t('otlp.source.item.web-vitals.description'), href: withSearch('/trace/manage'), icon: Activity },
+              { key: 'temporal', label: 'Temporal', description: t('otlp.source.item.temporal.description'), href: withSearch('/trace/manage'), icon: Layers3 }
             ]
           },
           {
             key: 'logs',
-            label: '日志',
+            label: t('otlp.source.section.logs'),
             items: [
               {
                 key: 'kubernetes-pod-logs',
-                label: 'Kubernetes Pod 日志',
-                description: '通过 OTLP 采集 Kubernetes Pod 日志。',
+                label: t('otlp.source.item.kubernetes-pod-logs.label'),
+                description: t('otlp.source.item.kubernetes-pod-logs.description'),
                 href: withSearch('/log/manage'),
                 icon: Container
               },
               {
                 key: 'docker-container-logs',
-                label: 'Docker 容器日志',
-                description: '把容器日志转发到 HertzBeat。',
+                label: t('otlp.source.item.docker-container-logs.label'),
+                description: t('otlp.source.item.docker-container-logs.description'),
                 href: withSearch('/log/manage'),
                 icon: Boxes
               },
-              { key: 'fluentbit', label: 'FluentBit', description: '使用 Fluent Bit 发送日志。', href: withSearch('/log/manage'), icon: FileText },
-              { key: 'fluentd', label: 'FluentD', description: '使用 FluentD 发送日志。', href: withSearch('/log/manage'), icon: FileText },
-              { key: 'syslog', label: 'Syslog', description: '规范化 Syslog 数据流。', href: withSearch('/log/manage'), icon: FileText },
-              { key: 'systemd-logs', label: 'Systemd 日志', description: '采集 systemd journal 日志。', href: withSearch('/log/manage'), icon: Server },
-              { key: 'cloudflare-logs', label: 'Cloudflare 日志', description: '转发 Cloudflare 边缘日志。', href: withSearch('/log/manage'), icon: Globe2 },
-              { key: 'vercel-logs', label: 'Vercel 日志', description: '转发 Vercel 部署日志。', href: withSearch('/log/manage'), icon: Globe2 },
-              { key: 'heroku-logs', label: 'Heroku 日志', description: '转发 Heroku 应用日志。', href: withSearch('/log/manage'), icon: Server },
-              { key: 'http-logs', label: 'HTTP 日志上报', description: '通过 HTTP 推送应用日志。', href: withSearch('/log/manage'), icon: Network },
-              { key: 'deno-logs', label: 'Deno 日志', description: '采集 Deno 运行时日志。', href: withSearch('/log/manage'), icon: Braces }
+              { key: 'fluentbit', label: 'FluentBit', description: t('otlp.source.item.fluentbit.description'), href: withSearch('/log/manage'), icon: FileText },
+              { key: 'fluentd', label: 'FluentD', description: t('otlp.source.item.fluentd.description'), href: withSearch('/log/manage'), icon: FileText },
+              { key: 'syslog', label: 'Syslog', description: t('otlp.source.item.syslog.description'), href: withSearch('/log/manage'), icon: FileText },
+              { key: 'systemd-logs', label: t('otlp.source.item.systemd-logs.label'), description: t('otlp.source.item.systemd-logs.description'), href: withSearch('/log/manage'), icon: Server },
+              { key: 'cloudflare-logs', label: t('otlp.source.item.cloudflare-logs.label'), description: t('otlp.source.item.cloudflare-logs.description'), href: withSearch('/log/manage'), icon: Globe2 },
+              { key: 'vercel-logs', label: t('otlp.source.item.vercel-logs.label'), description: t('otlp.source.item.vercel-logs.description'), href: withSearch('/log/manage'), icon: Globe2 },
+              { key: 'heroku-logs', label: t('otlp.source.item.heroku-logs.label'), description: t('otlp.source.item.heroku-logs.description'), href: withSearch('/log/manage'), icon: Server },
+              { key: 'http-logs', label: t('otlp.source.item.http-logs.label'), description: t('otlp.source.item.http-logs.description'), href: withSearch('/log/manage'), icon: Network },
+              { key: 'deno-logs', label: t('otlp.source.item.deno-logs.label'), description: t('otlp.source.item.deno-logs.description'), href: withSearch('/log/manage'), icon: Braces }
             ]
           },
           {
             key: 'metrics',
-            label: '指标',
+            label: t('otlp.source.section.metrics'),
             items: [
-              { key: 'prometheus', label: 'Prometheus', description: '把 Prometheus 指标桥接到 OTLP。', href: withSearch('/ingestion/otlp/metrics'), icon: Activity },
-              { key: 'otel-metrics', label: 'OpenTelemetry 指标', description: '检查 OTLP 指标数据流。', href: withSearch('/ingestion/otlp/metrics'), icon: Database },
-              { key: 'host-metrics', label: '主机指标', description: '采集主机和进程指标。', href: withSearch('/ingestion/otlp/metrics'), icon: Server },
-              { key: 'jmx-metrics', label: 'JMX 指标', description: '采集 JVM 和 JMX 指标。', href: withSearch('/ingestion/otlp/metrics'), icon: Database },
-              { key: 'nginx-metrics', label: 'Nginx 指标', description: '采集 Nginx 服务指标。', href: withSearch('/ingestion/otlp/metrics'), icon: Server }
+              { key: 'prometheus', label: 'Prometheus', description: t('otlp.source.item.prometheus.description'), href: withSearch('/ingestion/otlp/metrics'), icon: Activity },
+              { key: 'otel-metrics', label: t('otlp.source.item.otel-metrics.label'), description: t('otlp.source.item.otel-metrics.description'), href: withSearch('/ingestion/otlp/metrics'), icon: Database },
+              { key: 'host-metrics', label: t('otlp.source.item.host-metrics.label'), description: t('otlp.source.item.host-metrics.description'), href: withSearch('/ingestion/otlp/metrics'), icon: Server },
+              { key: 'jmx-metrics', label: t('otlp.source.item.jmx-metrics.label'), description: t('otlp.source.item.jmx-metrics.description'), href: withSearch('/ingestion/otlp/metrics'), icon: Database },
+              { key: 'nginx-metrics', label: t('otlp.source.item.nginx-metrics.label'), description: t('otlp.source.item.nginx-metrics.description'), href: withSearch('/ingestion/otlp/metrics'), icon: Server }
             ]
           }
         ];
         const filteredSourceSections = filterOtlpSourceSections(sourceSections, sourceSearch);
         const filterSourceItems = filteredSourceSections.map(section => [section.label, section.items.length] as const);
         const filterTotal = filterSourceItems.reduce((total, [, count]) => total + count, 0);
-        const filterItems = [['全部', filterTotal] as const, ...filterSourceItems];
+        const filterItems = [[t('otlp.source.filter.all'), filterTotal] as const, ...filterSourceItems];
         const signalRibbons = [
-          { label: 'OTLP 接入', value: `${activeSignals}/3`, detail: '活跃信号' },
-          { label: '指标工作台', value: metricCount, detail: '指标通道' },
-          { label: '日志工作台', value: logCount, detail: '日志通道' },
-          { label: '链路工作台', value: traceCount, detail: '链路通道' }
+          { label: t('otlp.ribbon.intake.label'), value: `${activeSignals}/3`, detail: t('otlp.ribbon.intake.detail') },
+          { label: t('otlp.ribbon.metrics.label'), value: metricCount, detail: t('otlp.ribbon.metrics.detail') },
+          { label: t('otlp.ribbon.logs.label'), value: logCount, detail: t('otlp.ribbon.logs.detail') },
+          { label: t('otlp.ribbon.traces.label'), value: traceCount, detail: t('otlp.ribbon.traces.detail') }
         ];
-        const readinessRows = buildReadinessRows(data.overview, data.bindings, formatTime);
+        const readinessRows = buildReadinessRows(data.overview, data.bindings, formatTime, t);
         const selfCheckRows = buildSelfCheckRows(data.overview.readinessChecks);
-        const collectionLoopLinks = buildCollectionLoopLinks();
+        const unboundCandidateRows = buildUnboundCandidateRows(data.bindings.recentUnboundCandidates);
+        const collectionLoopLinks = buildCollectionLoopLinks(t);
         return (
           <main
             className={coldOpsVisual.canvas.root}
@@ -440,17 +450,17 @@ export default function OtlpPage() {
               <div className={coldOpsVisual.stepper.grid}>
                 <div className={coldOpsVisual.stepper.itemCompact}>
                   <span className={coldOpsVisual.stepper.activeBadge}>1</span>
-                  <span className={coldOpsVisual.stepper.activeLabel}>选择 OTLP 数据源</span>
+                  <span className={coldOpsVisual.stepper.activeLabel}>{t('otlp.stepper.source')}</span>
                   <span className={coldOpsVisual.stepper.connector} />
                 </div>
                 <div className={coldOpsVisual.stepper.item}>
                   <span className={coldOpsVisual.stepper.idleBadge}>2</span>
-                  <span>配置协议与令牌</span>
+                  <span>{t('otlp.stepper.protocol')}</span>
                   <span className={coldOpsVisual.stepper.connector} />
                 </div>
                 <div className={coldOpsVisual.stepper.item}>
                   <span className={coldOpsVisual.stepper.idleBadge}>3</span>
-                  <span>进入信号工作台</span>
+                  <span>{t('otlp.stepper.workbench')}</span>
                 </div>
               </div>
             </section>
@@ -465,9 +475,9 @@ export default function OtlpPage() {
                     style={coldOpsVisual.canvas.panelMeshStyle}
                   />
                   <div className="relative">
-                    <h1 className="text-[34px] font-semibold leading-tight text-white">OTLP 协议接入</h1>
+                    <h1 className="text-[34px] font-semibold leading-tight text-white">{t('otlp.hero.title')}</h1>
                     <p className="mt-3 max-w-[700px] text-[14px] leading-6 text-[#aeb6c2]">
-                      选择 OpenTelemetry/OTLP 数据源,配置指标、日志或链路的接入路径,并绑定到 HertzBeat 对象。
+                      {t('otlp.hero.copy')}
                     </p>
                     <div
                       className={coldOpsVisual.button.row}
@@ -477,18 +487,18 @@ export default function OtlpPage() {
                       {showEntityReturn ? (
                         <Link href={entityReturnHref}>
                           <Button className={coldOpsVisual.button.compact} size="sm" variant="subtle">
-                            返回对象
+                            {t('otlp.action.return-entity')}
                           </Button>
                         </Link>
                       ) : null}
                       <Link href="/setting/settings/token">
-                        <Button className={coldOpsVisual.button.compact} size="sm" variant="subtle">管理令牌</Button>
+                        <Button className={coldOpsVisual.button.compact} size="sm" variant="subtle">{t('otlp.manage-token')}</Button>
                       </Link>
                       <Link href="/entities">
-                        <Button className={coldOpsVisual.button.compact} size="sm" variant="subtle">查看实体</Button>
+                        <Button className={coldOpsVisual.button.compact} size="sm" variant="subtle">{t('otlp.action.view-entities')}</Button>
                       </Link>
                       <Link href="/entities/discovery">
-                        <Button className={coldOpsVisual.button.primaryCompact} size="sm" variant="subtle">进入遥测发现</Button>
+                        <Button className={coldOpsVisual.button.primaryCompact} size="sm" variant="subtle">{t('otlp.open-discovery')}</Button>
                       </Link>
                     </div>
                   </div>
@@ -521,11 +531,11 @@ export default function OtlpPage() {
               >
                 <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
                   <div>
-                    <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">采集体系</p>
-                    <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">现有接入闭环</h2>
+                    <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">{t('otlp.section.collection-loop.kicker')}</p>
+                    <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">{t('otlp.section.collection-loop.title')}</h2>
                   </div>
                   <p className="max-w-[600px] text-[12px] leading-5 text-[#8e97a5]">
-                    OTLP 接入只是采集体系的一条路径;传统监控、采集器、模板、服务发现和对象目录继续作为统一运维入口。
+                    {t('otlp.section.collection-loop.copy')}
                   </p>
                 </div>
                 <div className="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
@@ -555,11 +565,11 @@ export default function OtlpPage() {
               >
                 <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
                   <div>
-                    <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">接入自检</p>
-                    <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">真实状态</h2>
+                    <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">{t('otlp.section.readiness.kicker')}</p>
+                    <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">{t('otlp.section.readiness.title')}</h2>
                   </div>
                   <p className="max-w-[540px] text-[12px] leading-5 text-[#8e97a5]">
-                    展示已确认的三信号、实体归因和最近上报;未接入的健康项不显示为空指标。
+                    {t('otlp.section.readiness.copy')}
                   </p>
                 </div>
                 <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
@@ -581,6 +591,45 @@ export default function OtlpPage() {
                 </div>
               </section>
 
+              {unboundCandidateRows.length > 0 ? (
+                <section
+                  className="mb-5 rounded-[4px] border border-[#252b34] bg-[#0f1217] p-4"
+                  data-otlp-entity-candidates="telemetry-derived"
+                  data-otlp-entity-candidate-count={unboundCandidateRows.length}
+                >
+                  <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
+                    <div>
+                      <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">{t('otlp.section.candidates.kicker')}</p>
+                      <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">{t('otlp.section.candidates.title')}</h2>
+                    </div>
+                    <p className="max-w-[540px] text-[12px] leading-5 text-[#8e97a5]">
+                      {t('otlp.section.candidates.copy')}
+                    </p>
+                  </div>
+                  <div className="grid gap-2 lg:grid-cols-2">
+                    {unboundCandidateRows.map(row => (
+                      <Link
+                        key={row.key}
+                        href={row.href}
+                        className="group min-w-0 rounded-[3px] border border-[#2a303a] bg-[#101319] px-3 py-2 transition hover:border-[#4e74f8] hover:bg-[#151b28]"
+                        data-otlp-entity-candidate-row={row.key}
+                        data-otlp-entity-candidate-signals={row.signals.join(',')}
+                        data-otlp-entity-candidate-identities={row.canonicalIdentitySummary}
+                      >
+                        <div className="flex items-center justify-between gap-3">
+                          <h3 className="truncate text-[13px] font-semibold text-[#f6f8fb]">{row.title}</h3>
+                          <span className="shrink-0 rounded-[3px] border border-[#323946] bg-[#121720] px-2 py-0.5 text-[10px] font-semibold text-[#8d97a6]">
+                            {t('otlp.section.candidates.pending')}
+                          </span>
+                        </div>
+                        <p className="mt-1 truncate text-[12px] leading-5 text-[#9ba5b4]">{row.copy}</p>
+                        <p className="mt-0.5 truncate text-[11px] text-[#6f7784]">{row.meta}</p>
+                      </Link>
+                    ))}
+                  </div>
+                </section>
+              ) : null}
+
               {selfCheckRows.length > 0 ? (
                 <section
                   className="mb-5 rounded-[4px] border border-[#252b34] bg-[#0f1217] p-4"
@@ -589,11 +638,11 @@ export default function OtlpPage() {
                 >
                   <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
                     <div>
-                      <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">运行自检</p>
-                      <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">Collector、存储和查询</h2>
+                      <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">{t('otlp.section.self-check.kicker')}</p>
+                      <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">{t('otlp.section.self-check.title')}</h2>
 	                    </div>
 	                    <p className="max-w-[560px] text-[12px] leading-5 text-[#8e97a5]">
-	                      检查采集节点、历史存储、查询服务和 GreptimeDB 连接状态。
+	                      {t('otlp.section.self-check.copy')}
 	                    </p>
 	                  </div>
                   <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
@@ -624,9 +673,9 @@ export default function OtlpPage() {
                     data-otlp-center-search-mode={coldOpsVisual.search.mode}
                     data-otlp-center-filtered-total={filterTotal}
                     value={sourceSearch}
-                    placeholder="搜索数据源、云服务、SDK 或运行时"
-                    searchLabel="筛选"
-                    clearLabel="清除"
+                    placeholder={t('otlp.source.search.placeholder')}
+                    searchLabel={t('otlp.source.search.label')}
+                    clearLabel={t('common.clear')}
                     inputWidthClassName="w-[360px]"
                     onValueChange={setSourceSearch}
                     onSearch={() => setSourceSearch(value => value)}
@@ -655,7 +704,7 @@ export default function OtlpPage() {
                         className={coldOpsVisual.sourceCard.empty}
                         data-otlp-center-search-empty="true"
                       >
-                        未找到匹配的数据源
+                        {t('otlp.source.search.empty')}
                       </div>
                     ) : null}
                   </div>
@@ -669,7 +718,7 @@ export default function OtlpPage() {
                   data-otlp-center-filter-line-alignment="fixed-count-column"
                   data-otlp-center-filter-total={filterTotal}
                 >
-                  <h2 className={coldOpsVisual.filter.title}>筛选</h2>
+                  <h2 className={coldOpsVisual.filter.title}>{t('otlp.source.filter.title')}</h2>
                   <div className={coldOpsVisual.filter.list}>
                     {filterItems.map(([label, count], index) => (
                       <div key={label} className={coldOpsVisual.filter.item}>
diff --git a/web-next/app/ingestion/otlp/page.test.tsx b/web-next/app/ingestion/otlp/page.test.tsx
index c3da0e4886..7b8aa82c62 100644
--- a/web-next/app/ingestion/otlp/page.test.tsx
+++ b/web-next/app/ingestion/otlp/page.test.tsx
@@ -2,6 +2,7 @@ import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import { SUPPLEMENTAL_MESSAGES } from '../../../lib/i18n-runtime-messages';
 
 vi.mock('next/link', () => ({
   default: ({ href, children, ...props }: any) => <a href={href} {...props}>{children}</a>
@@ -17,7 +18,7 @@ vi.mock('next/navigation', () => ({
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
     locale: 'zh-CN',
-    t: createTranslatorMock()
+    t: createTranslatorMock({ overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] })
   })
 }));
 
@@ -57,6 +58,9 @@ vi.mock('@/lib/format', () => ({
 }));
 
 vi.mock('@/lib/otlp-center/controller', () => ({
+  OTLP_BINDINGS_URL: '/ingestion/otlp/bindings',
+  OTLP_GUIDE_URL: '/ingestion/otlp/guide',
+  OTLP_OVERVIEW_URL: '/ingestion/otlp/overview',
   loadOtlpPageData: vi.fn()
 }));
 
@@ -84,6 +88,15 @@ vi.mock('@/lib/otlp-center/view-model', () => ({
     meta: check.detail,
     tone: check.status
   })),
+  buildUnboundCandidateRows: (items: any[] = []) => items.map(item => ({
+    key: `${item.primaryIdentityKey}:${item.primaryIdentityValue}:${item.namespace}:${item.environment}`,
+    title: item.suggestedName,
+    copy: `${item.primaryIdentityKey} = ${item.primaryIdentityValue}`,
+    meta: `${item.namespace} · ${item.environment} · ${item.signals.join(', ')}`,
+    href: `/entities/discovery?identityKey=${item.primaryIdentityKey}&identityValue=${item.primaryIdentityValue}&serviceName=${item.suggestedName}&serviceNamespace=${item.namespace}&environment=${item.environment}`,
+    signals: item.signals,
+    canonicalIdentitySummary: Object.entries(item.canonicalIdentities).map(([key, value]) => `${key}=${value}`).join(';')
+  })),
   buildProtocolOptions: () => [{ key: 'http', label: 'HTTP' }, { key: 'grpc', label: 'gRPC' }],
   buildSignalRows: (signals: any[]) => signals.map(signal => ({ title: signal.signal, copy: signal.summary || '-', meta: String(signal.totalCount || 0) })),
   filterGuideRowsByProtocol: (signals: any[]) => signals.map(item => ({ title: item.signal, copy: item.summary || '-', meta: item.endpoint || '-' })),
@@ -95,8 +108,9 @@ vi.mock('@/lib/setting-token/controller', () => ({
 }));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
-  ClientWorkbench: ({ children }: { children: (data: any) => React.ReactNode }) =>
-    children({
+  ClientWorkbench: ({ children, cacheKey }: { children: (data: any) => React.ReactNode; cacheKey?: string }) => (
+    <div data-client-workbench-cache-key={cacheKey}>
+      {children({
       overview: {
         activeSignalCount: 3,
         recentServiceCount: 2,
@@ -121,14 +135,43 @@ vi.mock('@/components/workbench/client-workbench', () => ({
       },
       bindings: {
         recentBoundEntities: [{ entityId: 1, name: 'checkout', primaryIdentityKey: 'service.name', primaryIdentityValue: 'checkout', monitorBindCount: 1 }],
+        recentUnboundCandidates: [{
+          suggestedName: 'billing',
+          suggestedType: 'service',
+          namespace: 'commerce',
+          environment: 'prod',
+          primaryIdentityKey: 'service.name',
+          primaryIdentityValue: 'billing',
+          signals: ['logs', 'metrics'],
+          canonicalIdentities: {
+            'service.name': 'billing',
+            'service.namespace': 'commerce',
+            'deployment.environment.name': 'prod'
+          },
+          latestObservedAt: 1712730000000
+        }],
         recentIdentitySamples: [{ signal: 'metrics', key: 'service.name', value: 'checkout' }]
       }
-    })
+      })}
+    </div>
+  )
 }));
 
 describe('otlp page', () => {
+  it('keeps OTLP source catalog and page chrome copy behind i18n keys', async () => {
+    const source = await import('node:fs/promises')
+      .then(fs => fs.readFile(new URL('./otlp-page.tsx', import.meta.url), 'utf8'));
+
+    expect(source).not.toMatch(/[\u4e00-\u9fff]/);
+    expect(source).toContain("t('otlp.source.section.quickstart')");
+    expect(source).toContain("t('otlp.source.item.demo-data.label')");
+    expect(source).toContain("t('otlp.hero.title')");
+    expect(source).toContain("t('otlp.section.collection-loop.title')");
+    expect(source).toContain("t('otlp.source.search.placeholder')");
+  });
+
   it('filters source catalog by explicit names instead of generic OpenTelemetry description text', async () => {
-    const { filterOtlpSourceSections } = await import('./page');
+    const { filterOtlpSourceSections } = await import('./otlp-page');
     const icon = (() => null) as any;
     const sections = [
       {
@@ -164,10 +207,10 @@ describe('otlp page', () => {
 
     expect(filteredKeys).toEqual(['open-telemetry']);
     expect(filteredKeys).not.toContain('java');
-  });
+  }, 15000);
 
   it('keeps single-letter source search at card-token level instead of matching whole sections', async () => {
-    const { filterOtlpSourceSections } = await import('./page');
+    const { filterOtlpSourceSections } = await import('./otlp-page');
     const icon = (() => null) as any;
     const sections = [
       {
@@ -197,6 +240,7 @@ describe('otlp page', () => {
     const html = renderToStaticMarkup(<OtlpPage />);
 
     expect(html).toContain('data-workspace-shell="true"');
+    expect(html).toContain('data-client-workbench-cache-key="otlp-center:/ingestion/otlp/overview:/ingestion/otlp/guide:/ingestion/otlp/bindings"');
     expect(html).toContain('data-otlp-center-route="hertzbeat-intake-cortex"');
     expect(html).toContain('data-otlp-center-visual-system="hertzbeat-native-avant-garde"');
     expect(html).toContain('data-otlp-visual-contract="cold-ops-catalog-v1"');
@@ -233,6 +277,16 @@ describe('otlp page', () => {
     expect(html).toContain('data-otlp-self-check-row="storage"');
     expect(html).toContain('data-otlp-self-check-row="query"');
     expect(html).toContain('data-otlp-self-check-row="greptime"');
+    expect(html).toContain('data-otlp-entity-candidates="telemetry-derived"');
+    expect(html).toContain('data-otlp-entity-candidate-count="1"');
+    expect(html).toContain('data-otlp-entity-candidate-row="service.name:billing:commerce:prod"');
+    expect(html).toContain('data-otlp-entity-candidate-signals="logs,metrics"');
+    expect(html).toContain('data-otlp-entity-candidate-identities="service.name=billing;service.namespace=commerce;deployment.environment.name=prod"');
+    expect(html).toContain('候选实体');
+    expect(html).toContain('billing');
+    expect(html).toContain('service.name = billing');
+    expect(html).toContain('commerce · prod · logs, metrics');
+    expect(html).toContain('href="/entities/discovery?identityKey=service.name&amp;identityValue=billing&amp;serviceName=billing&amp;serviceNamespace=commerce&amp;environment=prod"');
     expect(html).toContain('运行自检');
     expect(html).toContain('Collector 集群');
     expect(html).toContain('历史存储');
@@ -431,7 +485,7 @@ describe('otlp page', () => {
   }, 15000);
 
   it('keeps the source catalog filtering as an instant inline result transform', async () => {
-    const { filterOtlpSourceSections } = await import('./page');
+    const { filterOtlpSourceSections } = await import('./otlp-page');
     const filtered = filterOtlpSourceSections(
       [
         {
@@ -495,7 +549,7 @@ describe('otlp page', () => {
 
   it('renders OTLP readiness from real overview fields instead of fake quality counters', async () => {
     const source = await import('node:fs/promises')
-      .then(fs => fs.readFile(new URL('./page.tsx', import.meta.url), 'utf8'));
+      .then(fs => fs.readFile(new URL('./otlp-page.tsx', import.meta.url), 'utf8'));
 
     expect(source).toContain('buildReadinessRows');
     expect(source).toContain('buildSelfCheckRows');
diff --git a/web-next/app/ingestion/otlp/page.tsx b/web-next/app/ingestion/otlp/page.tsx
index 71b73d682b..1d18e7e4d9 100644
--- a/web-next/app/ingestion/otlp/page.tsx
+++ b/web-next/app/ingestion/otlp/page.tsx
@@ -1,692 +1,7 @@
-'use client';
+import React from 'react';
 
-import Link from 'next/link';
-import { useSearchParams } from 'next/navigation';
-import React, { useCallback, useState } from 'react';
-import {
-  Activity,
-  Boxes,
-  Braces,
-  Code2,
-  Container,
-  Database,
-  FileText,
-  Globe2,
-  Layers3,
-  Network,
-  Server,
-  Zap,
-  type LucideIcon
-} from 'lucide-react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { Button } from '@/components/ui/button';
-import { apiMessageGet } from '@/lib/api-client';
-import { loadOtlpPageData } from '@/lib/otlp-center/controller';
-import { buildCollectionLoopLinks, buildReadinessRows, buildSelfCheckRows } from '@/lib/otlp-center/view-model';
-import type { OtlpEntityBindingSummary, OtlpIngestionGuide, OtlpIngestionOverview } from '@/lib/types';
-import { SearchRow } from '../../../components/ui/search-row';
-import { coldOpsCatalogVisual } from '../../../lib/cold-ops-visual';
-import { formatTime } from '../../../lib/format';
-import { readSignalRouteContext } from '../../../lib/signal-route-context';
-import { buildEntityWorkspaceHref } from '../../../lib/workspace-navigation';
+import OtlpPage from './otlp-page';
 
-type OtlpPageData = {
-  overview: OtlpIngestionOverview;
-  guide: OtlpIngestionGuide;
-  bindings: OtlpEntityBindingSummary;
-};
-
-type SourceItem = {
-  key: string;
-  label: string;
-  description: string;
-  href: string;
-  icon: LucideIcon;
-  searchAliases?: string[];
-};
-
-type SourceSection = {
-  key: string;
-  label: string;
-  items: SourceItem[];
-};
-
-const coldOpsVisual = coldOpsCatalogVisual;
-const sourceCardClass = coldOpsVisual.sourceCard.className;
-
-const sourceBrandMarks: Record<
-  string,
-  {
-    label: string;
-    background: string;
-    color: string;
-    border?: string;
-    art?: 'ops-mesh' | 'grafana' | 'elk' | 'new-relic' | 'honeycomb' | 'self-hosted-pipeline' | 'java' | 'kubernetes' | 'docker';
-  }
-> = {
-  'open-telemetry': { label: 'OT', background: '#050816', color: '#f6c244', border: '#2d3445' },
-  grafana: { label: 'G', background: '#1f1309', color: '#ff8b2b', border: '#3f2611', art: 'grafana' },
-  elk: { label: 'EL', background: '#111318', color: '#2ec7c9', border: '#2b303a', art: 'elk' },
-  'new-relic': { label: 'NR', background: '#06241f', color: '#00e0a4', border: '#0f4c3f', art: 'new-relic' },
-  'commercial-observability': { label: 'OM', background: '#15171d', color: '#f8fafc', border: '#2d313c', art: 'ops-mesh' },
-  honeycomb: { label: 'HC', background: '#231807', color: '#ffbf3c', border: '#5c4110', art: 'honeycomb' },
-  'self-hosted-observability': { label: 'SH', background: '#2a130f', color: '#ff6b4a', border: '#5f271b', art: 'self-hosted-pipeline' },
-  java: { label: 'J', background: '#fff7ed', color: '#dc2626', border: '#fed7aa', art: 'java' },
-  python: { label: 'Py', background: '#0f2137', color: '#ffd95a', border: '#264b74' },
-  nodejs: { label: 'JS', background: '#2d2a08', color: '#f7df1e', border: '#5b5310' },
-  golang: { label: 'Go', background: '#062735', color: '#67e8f9', border: '#155e75' },
-  dotnet: { label: '.N', background: '#241149', color: '#c4b5fd', border: '#4c1d95' },
-  deno: { label: 'De', background: '#111827', color: '#f9fafb', border: '#374151' },
-  php: { label: 'php', background: '#1e1f3d', color: '#a5b4fc', border: '#3730a3' },
-  ruby: { label: 'Rb', background: '#351113', color: '#fb7185', border: '#7f1d1d' },
-  rust: { label: 'Rs', background: '#2b1d12', color: '#fdba74', border: '#7c2d12' },
-  'kubernetes-pod-logs': { label: 'K8s', background: '#0b3b8d', color: '#dbeafe', border: '#2563eb', art: 'kubernetes' },
-  'docker-container-logs': { label: 'Dc', background: '#062840', color: '#38bdf8', border: '#075985', art: 'docker' },
-  'cloudflare-logs': { label: 'CF', background: '#331707', color: '#fb923c', border: '#7c2d12' },
-  'vercel-logs': { label: '▲', background: '#f8fafc', color: '#111827', border: '#e5e7eb' },
-  prometheus: { label: 'Pr', background: '#33170b', color: '#ff7a45', border: '#7c2d12' }
-};
-
-function SourceBrandArt({ art }: { art: NonNullable<(typeof sourceBrandMarks)[string]['art']> }) {
-  if (art === 'ops-mesh') {
-    return (
-      <span className="relative block size-4" aria-hidden="true">
-        <span className="absolute left-[2px] top-[2px] size-[5px] rounded-[1px] border border-white/80" />
-        <span className="absolute right-[2px] top-[2px] size-[5px] rounded-[1px] border border-white/80" />
-        <span className="absolute bottom-[2px] left-[5px] size-[5px] rounded-[1px] border border-white/80" />
-        <span className="absolute left-[6px] top-[7px] h-px w-[5px] bg-white/80" />
-        <span className="absolute left-[4px] top-[7px] h-[5px] w-px rotate-45 bg-white/80" />
-      </span>
-    );
-  }
-
-  if (art === 'grafana') {
-    return (
-      <span className="relative block size-4" aria-hidden="true">
-        <span className="absolute inset-[2px] rounded-full border-2 border-[#ff7a1a]" />
-        <span className="absolute left-[5px] top-[5px] size-[5px] rounded-full bg-[#ff7a1a]" />
-        <span className="absolute right-[1px] top-[2px] size-[3px] rounded-full bg-[#ffb36b]" />
-      </span>
-    );
-  }
-
-  if (art === 'elk') {
-    return (
-      <span className="grid size-4 grid-cols-2 gap-[2px]" aria-hidden="true">
-        <span className="rounded-full bg-[#00bfb3]" />
-        <span className="rounded-full bg-[#f04e98]" />
-        <span className="rounded-full bg-[#f9c74f]" />
-        <span className="rounded-full bg-[#5b8def]" />
-      </span>
-    );
-  }
-
-  if (art === 'new-relic') {
-    return (
-      <span className="relative block size-4" aria-hidden="true">
-        <span className="absolute inset-[1px] bg-[#00d68f]" style={{ clipPath: 'polygon(50% 0, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%)' }} />
-        <span className="absolute inset-[5px] rounded-[2px] bg-[#06241f]" />
-      </span>
-    );
-  }
-
-  if (art === 'honeycomb') {
-    return (
-      <span className="relative block size-4" aria-hidden="true">
-        <span className="absolute left-[1px] top-[3px] size-[5px] rotate-45 rounded-[1px] bg-[#f59e0b]" />
-        <span className="absolute left-[6px] top-[1px] size-[5px] rotate-45 rounded-[1px] bg-[#fbbf24]" />
-        <span className="absolute left-[8px] top-[8px] size-[5px] rotate-45 rounded-[1px] bg-[#d97706]" />
-      </span>
-    );
-  }
-
-  if (art === 'self-hosted-pipeline') {
-    return (
-      <span className="relative block size-4" aria-hidden="true">
-        <span className="absolute left-[1px] top-[3px] h-[10px] w-[4px] rounded-[1px] border border-white/80" />
-        <span className="absolute left-[6px] top-[1px] h-[14px] w-[4px] rounded-[1px] border border-white/70" />
-        <span className="absolute right-[1px] top-[4px] h-[8px] w-[4px] rounded-[1px] border border-white/60" />
-        <span className="absolute left-[4px] top-[7px] h-px w-[8px] bg-white/70" />
-      </span>
-    );
-  }
-
-  if (art === 'java') {
-    return (
-      <span className="relative flex size-4 items-end justify-center text-[7px] font-black leading-none text-[#d71920]" aria-hidden="true">
-        <span className="absolute left-[5px] top-[1px] h-[5px] w-[5px] rounded-full border-t border-[#2b6cb0]" />
-        <span>Java</span>
-      </span>
-    );
-  }
-
-  if (art === 'kubernetes') {
-    return (
-      <span className="relative flex size-4 items-center justify-center" aria-hidden="true">
-        <span className="absolute inset-[1px] bg-[#326ce5]" style={{ clipPath: 'polygon(50% 0, 86% 14%, 100% 50%, 86% 86%, 50% 100%, 14% 86%, 0 50%, 14% 14%)' }} />
-        <span className="relative text-[9px] font-black leading-none text-white">✳</span>
-      </span>
-    );
-  }
-
-  return (
-    <span className="relative block size-4" aria-hidden="true">
-      <span className="absolute bottom-[4px] left-[2px] h-[4px] w-[11px] rounded-[1px] bg-[#1d9bf0]" />
-      <span className="absolute bottom-[8px] left-[3px] h-[3px] w-[3px] rounded-[1px] bg-[#38bdf8]" />
-      <span className="absolute bottom-[8px] left-[7px] h-[3px] w-[3px] rounded-[1px] bg-[#38bdf8]" />
-      <span className="absolute bottom-[11px] left-[5px] h-[2px] w-[5px] rounded-[1px] bg-[#7dd3fc]" />
-    </span>
-  );
-}
-
-function SourceBrandMark({ itemKey, brandMark }: { itemKey: string; brandMark: (typeof sourceBrandMarks)[string] }) {
-  return (
-    <span
-      className={coldOpsVisual.sourceCard.mark}
-      data-otlp-center-brand-logo={itemKey}
-      data-otlp-center-brand-art={brandMark.art ? itemKey : undefined}
-      style={{
-        background: brandMark.background,
-        borderColor: brandMark.border ?? 'transparent',
-        color: brandMark.color
-      }}
-    >
-      {brandMark.art ? <SourceBrandArt art={brandMark.art} /> : brandMark.label}
-    </span>
-  );
-}
-
-function SourceCard({ item }: { item: SourceItem }) {
-  const Icon = item.icon;
-  const brandMark = sourceBrandMarks[item.key];
-
-  return (
-    <Link
-      className={sourceCardClass}
-      data-otlp-center-source-card={item.key}
-      data-otlp-center-source-card-density="nav-scale-compact"
-      href={item.href}
-      title={item.description}
-    >
-      {brandMark ? (
-        <SourceBrandMark itemKey={item.key} brandMark={brandMark} />
-      ) : (
-        <span className={coldOpsVisual.sourceCard.icon}>
-          <Icon className="size-3" aria-hidden="true" />
-        </span>
-      )}
-      <span className={coldOpsVisual.sourceCard.label}>{item.label}</span>
-    </Link>
-  );
-}
-
-function readinessToneClass(tone: 'success' | 'warning' | 'danger' | 'neutral') {
-  if (tone === 'success') return 'bg-[#2dd46f]';
-  if (tone === 'warning') return 'bg-[#f5b84b]';
-  if (tone === 'danger') return 'bg-[#f06464]';
-  return 'bg-[#8b93a2]';
-}
-
-function tokenizeSourceSearchText(value: string) {
-  return value.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
-}
-
-function sourceItemMatchesSearch(item: SourceItem, sourceSearch: string) {
-  const searchTokens = tokenizeSourceSearchText(sourceSearch);
-  if (searchTokens.length === 0) return true;
-
-  const sourceTokens = [item.label, item.key, ...(item.searchAliases ?? [])]
-    .flatMap(tokenizeSourceSearchText);
-
-  return searchTokens.every(searchToken =>
-    sourceTokens.some(sourceToken => sourceToken.startsWith(searchToken))
-  );
-}
-
-export function filterOtlpSourceSections(sourceSections: SourceSection[], sourceSearch: string) {
-  if (tokenizeSourceSearchText(sourceSearch).length === 0) return sourceSections;
-
-  return sourceSections
-    .map(section => ({
-      ...section,
-      items: section.items.filter(item => sourceItemMatchesSearch(item, sourceSearch))
-    }))
-    .filter(section => section.items.length > 0);
-}
-
-export default function OtlpPage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const [sourceSearch, setSourceSearch] = useState('');
-  const load = useCallback(async (): Promise<OtlpPageData> => {
-    return loadOtlpPageData(apiMessageGet);
-  }, []);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('otlp.loading')}>
-      {data => {
-        const routeContext = readSignalRouteContext(searchParams);
-        const rawSearch = searchParams.toString();
-        const withSearch = (path: string) => (rawSearch ? `${path}?${rawSearch}` : path);
-        const entityReturnHref = buildEntityWorkspaceHref(routeContext);
-        const showEntityReturn = Boolean(routeContext.returnTo || routeContext.entityId);
-        const activeSignals = data.overview.activeSignalCount ?? 0;
-        const logCount = data.overview.logs?.totalCount ?? 0;
-        const metricCount = data.overview.metrics?.totalCount ?? 0;
-        const traceCount = data.overview.traces?.totalCount ?? 0;
-        const sourceSections: SourceSection[] = [
-          {
-            key: 'quickstart',
-            label: '快速开始',
-            items: [
-              {
-                key: 'demo-data',
-                label: '5 分钟内接入演示数据',
-                description: '写入一条演示 OTLP 数据流,并立即查看工作台。',
-                href: withSearch('/overview'),
-                icon: Zap
-              }
-            ]
-          },
-          {
-            key: 'migrate',
-            label: '现有接入',
-            items: [
-              {
-                key: 'open-telemetry',
-                label: '已有 OpenTelemetry',
-                description: '复用现有 OpenTelemetry Collector 或 SDK 管道。',
-                href: withSearch('/entities/discovery'),
-                icon: Network
-              },
-              {
-                key: 'grafana',
-                label: 'Grafana',
-                description: '将 Grafana 侧的信号接入 HertzBeat。',
-                href: withSearch('/ingestion/otlp/metrics'),
-                icon: Activity
-              },
-              {
-                key: 'elk',
-                label: 'ELK',
-                description: '把日志源路由到日志工作台。',
-                href: withSearch('/log/manage'),
-                icon: Layers3
-              },
-              {
-                key: 'new-relic',
-                label: 'New Relic',
-                description: '把服务遥测映射到对象目录。',
-                href: withSearch('/entities'),
-                icon: Boxes
-              },
-              {
-                key: 'commercial-observability',
-                label: '已有可观测平台',
-                description: '接入服务、日志和指标后,在对象目录和排障工作台继续处理。',
-                href: withSearch('/entities/discovery'),
-                icon: Activity
-              },
-              {
-                key: 'honeycomb',
-                label: 'Honeycomb',
-                description: '把链路数据接入链路工作台,并关联服务与日志。',
-                href: withSearch('/explorer'),
-                icon: Network
-              },
-              {
-                key: 'self-hosted-observability',
-                label: '自托管 OTLP 管道',
-                description: '复用内网或离线环境中的 OTLP Collector 管道。',
-                href: withSearch('/ingestion/otlp'),
-                icon: Server
-              }
-            ]
-          },
-          {
-            key: 'apm-traces',
-            label: 'APM / 链路',
-            items: [
-              { key: 'java', label: 'Java', description: 'Java OpenTelemetry SDK 配置。', href: withSearch('/trace/manage'), icon: Code2 },
-              { key: 'python', label: 'Python', description: 'Python OpenTelemetry SDK 配置。', href: withSearch('/trace/manage'), icon: Braces },
-              { key: 'nodejs', label: 'JavaScript', description: 'Node.js 与浏览器链路配置。', href: withSearch('/trace/manage'), icon: Globe2 },
-              { key: 'golang', label: 'Golang', description: 'Go 服务链路配置。', href: withSearch('/trace/manage'), icon: Network },
-              { key: 'dotnet', label: '.NET', description: '.NET OpenTelemetry 埋点配置。', href: withSearch('/trace/manage'), icon: Code2 },
-              { key: 'deno', label: 'Deno', description: 'Deno 运行时链路配置。', href: withSearch('/trace/manage'), icon: Braces },
-              { key: 'php', label: 'PHP', description: 'PHP 应用链路配置。', href: withSearch('/trace/manage'), icon: Code2 },
-              { key: 'ruby', label: 'Ruby', description: 'Ruby 服务链路配置。', href: withSearch('/trace/manage'), icon: Braces },
-              { key: 'rust', label: 'Rust', description: 'Rust OpenTelemetry 配置。', href: withSearch('/trace/manage'), icon: Code2 },
-              { key: 'nginx-tracing', label: 'Nginx 链路', description: '追踪反向代理和边缘服务调用。', href: withSearch('/trace/manage'), icon: Server },
-              { key: 'external-api', label: '外部 API 监控', description: '跟踪外部 API 延迟和错误。', href: withSearch('/trace/manage'), icon: Server },
-              { key: 'web-vitals', label: 'Web Vitals 链路', description: '前端链路和核心体验指标配置。', href: withSearch('/trace/manage'), icon: Activity },
-              { key: 'temporal', label: 'Temporal', description: '追踪 Temporal 工作流和活动。', href: withSearch('/trace/manage'), icon: Layers3 }
-            ]
-          },
-          {
-            key: 'logs',
-            label: '日志',
-            items: [
-              {
-                key: 'kubernetes-pod-logs',
-                label: 'Kubernetes Pod 日志',
-                description: '通过 OTLP 采集 Kubernetes Pod 日志。',
-                href: withSearch('/log/manage'),
-                icon: Container
-              },
-              {
-                key: 'docker-container-logs',
-                label: 'Docker 容器日志',
-                description: '把容器日志转发到 HertzBeat。',
-                href: withSearch('/log/manage'),
-                icon: Boxes
-              },
-              { key: 'fluentbit', label: 'FluentBit', description: '使用 Fluent Bit 发送日志。', href: withSearch('/log/manage'), icon: FileText },
-              { key: 'fluentd', label: 'FluentD', description: '使用 FluentD 发送日志。', href: withSearch('/log/manage'), icon: FileText },
-              { key: 'syslog', label: 'Syslog', description: '规范化 Syslog 数据流。', href: withSearch('/log/manage'), icon: FileText },
-              { key: 'systemd-logs', label: 'Systemd 日志', description: '采集 systemd journal 日志。', href: withSearch('/log/manage'), icon: Server },
-              { key: 'cloudflare-logs', label: 'Cloudflare 日志', description: '转发 Cloudflare 边缘日志。', href: withSearch('/log/manage'), icon: Globe2 },
-              { key: 'vercel-logs', label: 'Vercel 日志', description: '转发 Vercel 部署日志。', href: withSearch('/log/manage'), icon: Globe2 },
-              { key: 'heroku-logs', label: 'Heroku 日志', description: '转发 Heroku 应用日志。', href: withSearch('/log/manage'), icon: Server },
-              { key: 'http-logs', label: 'HTTP 日志上报', description: '通过 HTTP 推送应用日志。', href: withSearch('/log/manage'), icon: Network },
-              { key: 'deno-logs', label: 'Deno 日志', description: '采集 Deno 运行时日志。', href: withSearch('/log/manage'), icon: Braces }
-            ]
-          },
-          {
-            key: 'metrics',
-            label: '指标',
-            items: [
-              { key: 'prometheus', label: 'Prometheus', description: '把 Prometheus 指标桥接到 OTLP。', href: withSearch('/ingestion/otlp/metrics'), icon: Activity },
-              { key: 'otel-metrics', label: 'OpenTelemetry 指标', description: '检查 OTLP 指标数据流。', href: withSearch('/ingestion/otlp/metrics'), icon: Database },
-              { key: 'host-metrics', label: '主机指标', description: '采集主机和进程指标。', href: withSearch('/ingestion/otlp/metrics'), icon: Server },
-              { key: 'jmx-metrics', label: 'JMX 指标', description: '采集 JVM 和 JMX 指标。', href: withSearch('/ingestion/otlp/metrics'), icon: Database },
-              { key: 'nginx-metrics', label: 'Nginx 指标', description: '采集 Nginx 服务指标。', href: withSearch('/ingestion/otlp/metrics'), icon: Server }
-            ]
-          }
-        ];
-        const filteredSourceSections = filterOtlpSourceSections(sourceSections, sourceSearch);
-        const filterSourceItems = filteredSourceSections.map(section => [section.label, section.items.length] as const);
-        const filterTotal = filterSourceItems.reduce((total, [, count]) => total + count, 0);
-        const filterItems = [['全部', filterTotal] as const, ...filterSourceItems];
-        const signalRibbons = [
-          { label: 'OTLP 接入', value: `${activeSignals}/3`, detail: '活跃信号' },
-          { label: '指标工作台', value: metricCount, detail: '指标通道' },
-          { label: '日志工作台', value: logCount, detail: '日志通道' },
-          { label: '链路工作台', value: traceCount, detail: '链路通道' }
-        ];
-        const readinessRows = buildReadinessRows(data.overview, data.bindings, formatTime);
-        const selfCheckRows = buildSelfCheckRows(data.overview.readinessChecks);
-        const collectionLoopLinks = buildCollectionLoopLinks();
-        return (
-          <main
-            className={coldOpsVisual.canvas.root}
-            data-workspace-shell="true"
-            data-otlp-center-route="hertzbeat-intake-cortex"
-            data-otlp-center-visual-system={coldOpsVisual.visualSystem}
-            data-otlp-visual-contract={coldOpsVisual.contract}
-            data-otlp-visual-radius={coldOpsVisual.radius.contract}
-            data-otlp-visual-rail={coldOpsVisual.layout.railWidth}
-            data-otlp-center-tone={coldOpsVisual.tone}
-            data-otlp-center-catalog-canvas={coldOpsVisual.canvasName}
-            style={coldOpsVisual.canvas.backgroundStyle}
-          >
-            <section
-              className={coldOpsVisual.stepper.shell}
-              data-otlp-center-stepper="hertzbeat-intake-steps"
-              data-otlp-center-stepper-align="centered-860-balanced"
-              data-otlp-center-stepper-phase="source-selection"
-            >
-              <div className={coldOpsVisual.stepper.divider} aria-hidden="true" />
-              <div className={coldOpsVisual.stepper.grid}>
-                <div className={coldOpsVisual.stepper.itemCompact}>
-                  <span className={coldOpsVisual.stepper.activeBadge}>1</span>
-                  <span className={coldOpsVisual.stepper.activeLabel}>选择 OTLP 数据源</span>
-                  <span className={coldOpsVisual.stepper.connector} />
-                </div>
-                <div className={coldOpsVisual.stepper.item}>
-                  <span className={coldOpsVisual.stepper.idleBadge}>2</span>
-                  <span>配置协议与令牌</span>
-                  <span className={coldOpsVisual.stepper.connector} />
-                </div>
-                <div className={coldOpsVisual.stepper.item}>
-                  <span className={coldOpsVisual.stepper.idleBadge}>3</span>
-                  <span>进入信号工作台</span>
-                </div>
-              </div>
-            </section>
-
-            <section className={coldOpsVisual.layout.pageSection}>
-              <div className={coldOpsVisual.layout.pageDivider} aria-hidden="true" />
-              <div className={coldOpsVisual.layout.heroGrid} data-otlp-center-hero="hertzbeat-intake-cortex">
-                <div className={coldOpsVisual.panel.hero}>
-                  <div
-                    className={coldOpsVisual.canvas.panelMesh}
-                    aria-hidden="true"
-                    style={coldOpsVisual.canvas.panelMeshStyle}
-                  />
-                  <div className="relative">
-                    <h1 className="text-[34px] font-semibold leading-tight text-white">OTLP 协议接入</h1>
-                    <p className="mt-3 max-w-[700px] text-[14px] leading-6 text-[#aeb6c2]">
-                      选择 OpenTelemetry/OTLP 数据源,配置指标、日志或链路的接入路径,并绑定到 HertzBeat 对象。
-                    </p>
-                    <div
-                      className={coldOpsVisual.button.row}
-                      data-otlp-center-action-bar="standard-action-row"
-                      data-otlp-center-action-size={coldOpsVisual.button.sizeContract}
-                    >
-                      {showEntityReturn ? (
-                        <Link href={entityReturnHref}>
-                          <Button className={coldOpsVisual.button.compact} size="sm" variant="subtle">
-                            返回对象
-                          </Button>
-                        </Link>
-                      ) : null}
-                      <Link href="/setting/settings/token">
-                        <Button className={coldOpsVisual.button.compact} size="sm" variant="subtle">管理令牌</Button>
-                      </Link>
-                      <Link href="/entities">
-                        <Button className={coldOpsVisual.button.compact} size="sm" variant="subtle">查看实体</Button>
-                      </Link>
-                      <Link href="/entities/discovery">
-                        <Button className={coldOpsVisual.button.primaryCompact} size="sm" variant="subtle">进入遥测发现</Button>
-                      </Link>
-                    </div>
-                  </div>
-                </div>
-                <div className="flex flex-wrap items-center gap-2">
-                  <div
-                    className={coldOpsVisual.signal.band}
-                    data-otlp-center-signal-band="hertzbeat-signal-ribbons"
-                    data-otlp-center-signal-band-layout="single-layer"
-                    data-otlp-center-signal-value-scale="restrained-17"
-                  >
-                    {signalRibbons.map((item, index) => (
-                      <div key={item.label} className={coldOpsVisual.signal.ribbon}>
-                        <div>
-                          <p className={coldOpsVisual.signal.label}>{item.label}</p>
-                          <p className={coldOpsVisual.signal.detail}>{item.detail}</p>
-                        </div>
-                        <span className={index === 0 ? coldOpsVisual.signal.activeValue : coldOpsVisual.signal.value}>
-                          {item.value}
-                        </span>
-                      </div>
-                    ))}
-                  </div>
-                </div>
-              </div>
-
-              <section
-                className="mb-5 rounded-[4px] border border-[#252b34] bg-[#0f1217] p-4"
-                data-otlp-collection-loop="existing-ingestion-system"
-              >
-                <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
-                  <div>
-                    <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">采集体系</p>
-                    <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">现有接入闭环</h2>
-                  </div>
-                  <p className="max-w-[600px] text-[12px] leading-5 text-[#8e97a5]">
-                    OTLP 接入只是采集体系的一条路径;传统监控、采集器、模板、服务发现和对象目录继续作为统一运维入口。
-                  </p>
-                </div>
-                <div className="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
-                  {collectionLoopLinks.map(link => (
-                    <Link
-                      key={link.key}
-                      href={link.href}
-                      className="group min-w-0 rounded-[3px] border border-[#2a303a] bg-[#101319] px-3 py-2 transition hover:border-[#4e74f8] hover:bg-[#151b28]"
-                      data-otlp-collection-loop-link={link.key}
-                    >
-                      <div className="flex items-center justify-between gap-3">
-                        <h3 className="truncate text-[13px] font-semibold text-[#f6f8fb]">{link.title}</h3>
-                        <span className="shrink-0 rounded-[3px] border border-[#323946] bg-[#121720] px-2 py-0.5 text-[10px] font-semibold text-[#8d97a6]">
-                          {link.meta}
-                        </span>
-                      </div>
-                      <p className="mt-1 line-clamp-2 text-[12px] leading-5 text-[#8993a3]">{link.copy}</p>
-                    </Link>
-                  ))}
-                </div>
-              </section>
-
-              <section
-                className="mb-5 rounded-[4px] border border-[#252b34] bg-[#0f1217] p-4"
-                data-otlp-readiness-status="overview-backed"
-                data-otlp-readiness-scope="signals-entity-latest"
-              >
-                <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
-                  <div>
-                    <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">接入自检</p>
-                    <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">真实状态</h2>
-                  </div>
-                  <p className="max-w-[540px] text-[12px] leading-5 text-[#8e97a5]">
-                    展示已确认的三信号、实体归因和最近上报;未接入的健康项不显示为空指标。
-                  </p>
-                </div>
-                <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
-                  {readinessRows.map(row => (
-                    <div
-                      key={row.key}
-                      className="min-w-0 rounded-[3px] border border-[#2a303a] bg-[#101319] px-3 py-2"
-                      data-otlp-readiness-row={row.key}
-                      data-otlp-readiness-tone={row.tone}
-                    >
-                      <div className="flex items-center gap-2">
-                        <span className={`size-1.5 shrink-0 rounded-full ${readinessToneClass(row.tone)}`} aria-hidden="true" />
-                        <p className="truncate text-[11px] font-semibold text-[#a8b0bd]">{row.title}</p>
-                      </div>
-                      <p className="mt-1 truncate text-[16px] font-semibold tabular-nums text-[#f8fafc]">{row.copy}</p>
-                      <p className="mt-0.5 truncate text-[11px] text-[#6f7784]">{row.meta}</p>
-                    </div>
-                  ))}
-                </div>
-              </section>
-
-              {selfCheckRows.length > 0 ? (
-                <section
-                  className="mb-5 rounded-[4px] border border-[#252b34] bg-[#0f1217] p-4"
-                  data-otlp-self-check-status="backend-backed"
-                  data-otlp-self-check-scope="collector-storage-query-greptime"
-                >
-                  <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
-                    <div>
-                      <p className="text-[11px] font-semibold tracking-[0.14em] text-[#7f8794]">运行自检</p>
-                      <h2 className="mt-1 text-[15px] font-semibold text-[#f3f6fa]">Collector、存储和查询</h2>
-	                    </div>
-	                    <p className="max-w-[560px] text-[12px] leading-5 text-[#8e97a5]">
-	                      检查采集节点、历史存储、查询服务和 GreptimeDB 连接状态。
-	                    </p>
-	                  </div>
-                  <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
-                    {selfCheckRows.map(row => (
-                      <div
-                        key={row.key}
-                        className="min-w-0 rounded-[3px] border border-[#2a303a] bg-[#101319] px-3 py-2"
-                        data-otlp-self-check-row={row.key}
-                        data-otlp-self-check-tone={row.tone}
-                      >
-                        <div className="flex items-center gap-2">
-                          <span className={`size-1.5 shrink-0 rounded-full ${readinessToneClass(row.tone)}`} aria-hidden="true" />
-                          <p className="truncate text-[11px] font-semibold text-[#a8b0bd]">{row.title}</p>
-                        </div>
-                        <p className="mt-1 truncate text-[16px] font-semibold tabular-nums text-[#f8fafc]">{row.copy}</p>
-                        <p className="mt-0.5 truncate text-[11px] text-[#6f7784]">{row.meta}</p>
-                      </div>
-                    ))}
-                  </div>
-                </section>
-              ) : null}
-
-              <div className={coldOpsVisual.layout.railGrid} data-otlp-center-rail-grid="shared-340">
-                <section className="min-w-0" data-otlp-center-source-grid="hertzbeat-source-catalog">
-                  <SearchRow
-                    data-otlp-center-search-row="hertzbeat-catalog-filter"
-                    data-otlp-center-search-owner="shared-search-row"
-                    data-otlp-center-search-mode={coldOpsVisual.search.mode}
-                    data-otlp-center-filtered-total={filterTotal}
-                    value={sourceSearch}
-                    placeholder="搜索数据源、云服务、SDK 或运行时"
-                    searchLabel="筛选"
-                    clearLabel="清除"
-                    inputWidthClassName="w-[360px]"
-                    onValueChange={setSourceSearch}
-                    onSearch={() => setSourceSearch(value => value)}
-                    onClear={() => setSourceSearch('')}
-                  />
-
-                  <div className={coldOpsVisual.sourceCard.stack}>
-                    {filteredSourceSections.map(section => (
-                      <section key={section.key} className={coldOpsVisual.sourceCard.section} data-otlp-center-source-section={section.key}>
-                        <h2 className={coldOpsVisual.sourceCard.heading}>
-                          {section.label} ({section.items.length})
-                        </h2>
-                        <div
-                          className={coldOpsVisual.sourceCard.grid}
-                          data-otlp-center-grid-density="hertzbeat-dense-catalog"
-                          data-otlp-center-grid-span={coldOpsVisual.sourceCard.gridSpan}
-                        >
-                          {section.items.map(item => (
-                            <SourceCard key={item.key} item={item} />
-                          ))}
-                        </div>
-                      </section>
-                    ))}
-                    {filteredSourceSections.length === 0 ? (
-                      <div
-                        className={coldOpsVisual.sourceCard.empty}
-                        data-otlp-center-search-empty="true"
-                      >
-                        未找到匹配的数据源
-                      </div>
-                    ) : null}
-                  </div>
-                </section>
-
-                <aside
-                  className={coldOpsVisual.panel.rail}
-                  data-otlp-center-filter-rail="hertzbeat-prism-filters"
-                  data-otlp-center-filter-rail-mode={coldOpsVisual.panel.railMode}
-                  data-otlp-center-filter-rail-stickiness="static-flow"
-                  data-otlp-center-filter-line-alignment="fixed-count-column"
-                  data-otlp-center-filter-total={filterTotal}
-                >
-                  <h2 className={coldOpsVisual.filter.title}>筛选</h2>
-                  <div className={coldOpsVisual.filter.list}>
-                    {filterItems.map(([label, count], index) => (
-                      <div key={label} className={coldOpsVisual.filter.item}>
-                        <span className={index === 0 ? coldOpsVisual.filter.activeText : coldOpsVisual.filter.idleText}>{label}</span>
-                        <span className={coldOpsVisual.filter.dash} />
-                        <span className={coldOpsVisual.filter.count}>
-                          {count}
-                        </span>
-                      </div>
-                    ))}
-                  </div>
-                </aside>
-              </div>
-            </section>
-          </main>
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function OtlpRoutePage() {
+  return <OtlpPage />;
 }
diff --git a/web-next/app/log/integration/[source]/page.test.tsx b/web-next/app/log/integration/[source]/page.test.tsx
index e8b7fd9830..a9d1d2dd19 100644
--- a/web-next/app/log/integration/[source]/page.test.tsx
+++ b/web-next/app/log/integration/[source]/page.test.tsx
@@ -23,6 +23,22 @@ describe('log integration source alias route', () => {
     expect(redirect).toHaveBeenCalledWith('/ingestion/otlp?signal=logs');
   });
 
+  it('does not seed source aliases into discarded log search filters', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: LogIntegrationSourcePage } = await import('./page');
+
+    await expect(
+      LogIntegrationSourcePage({
+        params: Promise.resolve({ source: 'fluentbit' }),
+        searchParams: Promise.resolve({})
+      })
+    ).rejects.toThrow('redirect:/ingestion/otlp?signal=logs');
+    expect(redirect).toHaveBeenLastCalledWith('/ingestion/otlp?signal=logs');
+  });
+
   it('keeps route context and drops source fallback when redirecting a bookmarked source alias', async () => {
     redirect.mockImplementation((target: string) => {
       throw new Error(`redirect:${target}`);
diff --git a/web-next/app/log/integration/[source]/page.tsx b/web-next/app/log/integration/[source]/page.tsx
index bbf9e915b0..22cd4089dd 100644
--- a/web-next/app/log/integration/[source]/page.tsx
+++ b/web-next/app/log/integration/[source]/page.tsx
@@ -2,15 +2,13 @@ import { redirect } from 'next/navigation';
 import { buildLogIntegrationIngestionHref, createSearchParamReader, type SearchParamsRecord } from '../../../../lib/log-integration/navigation';
 
 export default async function LogIntegrationSourceAliasPage({
-  params,
   searchParams
 }: {
   params: Promise<{ source: string }>;
   searchParams?: Promise<SearchParamsRecord>;
 }) {
-  const resolved = await params;
   const resolvedSearchParams = await searchParams;
-  const searchParamReader = createSearchParamReader(resolvedSearchParams, resolved.source);
+  const searchParamReader = createSearchParamReader(resolvedSearchParams);
 
   redirect(buildLogIntegrationIngestionHref(searchParamReader));
 }
diff --git a/web-next/app/log/manage/log-manage-page.tsx b/web-next/app/log/manage/log-manage-page.tsx
index 07064fb346..925a42a503 100644
--- a/web-next/app/log/manage/log-manage-page.tsx
+++ b/web-next/app/log/manage/log-manage-page.tsx
@@ -19,23 +19,21 @@ import {
   WifiOff,
   Workflow
 } from 'lucide-react';
-import { useRouter, useSearchParams } from 'next/navigation';
+import { useRouter } from 'next/navigation';
+import { HzActionGroup, HzAttributeDiagnostics, HzButton, HzButtonIcon, HzButtonLink, HzControlStack, HzDataTable, HzDetailAside, HzDetailBodyStack, HzDetailRows, HzEmptyState, HzInput, HzLogStreamLiveRow, HzPanelHeader, HzPanelSurface, HzScrollViewport, HzSearchFieldFrame, HzSearchFieldIcon, HzSelect, HzSignalTrendBars, HzStateNotice, HzStatCell, HzStatusBadge, HzWorkbenchLayout, type HzStatusTone } from '@hertzbeat/ui';
 import { LogRelatedTraceDialog } from '../../../components/log-manage/log-related-trace-dialog';
 import { LogStreamDetailDialog } from '../../../components/log-manage/log-stream-detail-dialog';
 import { buildTimeRangeControlLabels, TimeRangeControl } from '@/components/observability/time-range-control';
 import { useI18n } from '@/components/providers/i18n-provider';
-import { Input } from '@/components/ui/input';
-import { Select } from '@/components/ui/select';
 import { ClientWorkbench } from '@/components/workbench/client-workbench';
 import { apiMessageGet } from '@/lib/api-client';
 import { bodyText, formatTime } from '@/lib/format';
-import { severityLabel } from '@/lib/log-manage/display-mapping';
+import { logSeverityTone, severityLabel, type LogSeverityTone } from '@/lib/log-manage/display-mapping';
 import {
   buildLogStreamUrl,
   buildLogUrls,
-  queryStateFromParams,
   resolveBrowserLogStreamUrl,
-  resolveLogWorkbenchView,
+  type LogManageRouteState,
   type LogQueryState,
   type LogWorkbenchView
 } from '@/lib/log-manage/query-state';
@@ -58,7 +56,7 @@ import {
   buildSelectedLogRows,
   type LogAttributionDiagnostic
 } from '@/lib/log-manage/view-model';
-import { buildSignalEntityContextRows, readSignalRouteContext, type SignalRouteContext } from '@/lib/signal-route-context';
+import { buildSignalEntityContextRows, type SignalRouteContext } from '@/lib/signal-route-context';
 import { loadTraceDetailBundle } from '../../../lib/trace-manage/controller';
 import { buildSelectedSpanFacts, buildTraceWaterfallRows } from '../../../lib/trace-manage/view-model';
 import { resolveAppliedTimeContext, sanitizeTimeContext, type TimeContext } from '@/lib/time-context';
@@ -71,6 +69,10 @@ type LogManageData = {
   trend: LogTrendStats;
   coverage: LogTraceCoverage;
   query: LogQueryState;
+  loadStatus?: {
+    state: 'degraded';
+    message: string;
+  };
 };
 
 type BackendLogOverview = LogOverview & {
@@ -92,26 +94,30 @@ const EMPTY_QUERY: LogQueryState = {
   severityText: ''
 };
 
+const EMPTY_LOG_MANAGE_ROUTE_STATE: LogManageRouteState = {
+  initialQuery: EMPTY_QUERY,
+  currentView: 'stream',
+  routeContext: {},
+  shouldCleanUrl: false
+};
+
 const quickSeverityFilters = ['ERROR', 'WARN', 'INFO', 'DEBUG'];
 const maxStreamEntries = 10000;
 const maxPendingStreamEntries = 1000;
+const LOG_MANAGE_SETTLED_CACHE_TTL_MS = 10_000;
+const LOG_MANAGE_API_TIMEOUT_MS = 3_500;
+
+function logSeverityStatusTone(severityTone: LogSeverityTone): HzStatusTone {
+  if (severityTone === 'danger') return 'critical';
+  if (severityTone === 'warning') return 'warning';
+  if (severityTone === 'success') return 'success';
+  return 'neutral';
+}
 
-const actionClass =
-  'inline-flex h-8 items-center justify-center gap-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe3ee] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] transition-colors hover:border-[#3b4454] hover:bg-[#151821]';
-const primaryActionClass =
-  'inline-flex h-8 items-center justify-center gap-2 rounded-[3px] border border-[#3b4454] bg-[#18202c] px-4 text-[12px] font-semibold text-[#f2f6fb] shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] transition-colors hover:bg-[#202938]';
-const disabledActionClass =
-  'inline-flex h-8 cursor-not-allowed items-center justify-center gap-2 rounded-[3px] border border-[#242a34] bg-[#0b0e13] px-3 text-[12px] font-semibold text-[#687386] opacity-80 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]';
-const panelClass = 'rounded-[4px] border border-[#252b35] bg-[#0d1015] shadow-[0_18px_60px_rgba(0,0,0,0.28)]';
-const inputClass =
-  'h-8 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#e6edf7] outline-none transition-colors placeholder:text-[#697386] focus:border-[#4e74f8] focus:ring-2 focus:ring-[rgba(78,116,248,0.12)]';
-
-function severityTone(severity: string) {
-  const normalized = severity.toUpperCase();
-  if (normalized.includes('ERROR') || normalized.includes('FATAL')) return 'border-[#70424a] bg-[#231116] text-[#f39aa8]';
-  if (normalized.includes('WARN')) return 'border-[#6f5730] bg-[#21190d] text-[#efca83]';
-  if (normalized.includes('INFO')) return 'border-[#315b49] bg-[#0f211b] text-[#8bd8ad]';
-  return 'border-[#303642] bg-[#151821] text-[#d7dce6]';
+function logStreamStatusBadgeTone(status: 'connecting' | 'connected' | 'disconnected'): HzStatusTone {
+  if (status === 'connected') return 'success';
+  if (status === 'connecting') return 'warning';
+  return 'critical';
 }
 
 function updateDraftField(field: keyof LogQueryState, value: string) {
@@ -129,6 +135,64 @@ function normalizeLogOverview(overview: BackendLogOverview): LogOverview {
   };
 }
 
+function emptyLogManageData(query: LogQueryState, message: string): LogManageData {
+  return {
+    overview: {
+      totalLogs: 0,
+      errorLogs: 0,
+      distinctTraceCount: 0,
+      latestObservedAt: null,
+      hasActiveLog: false
+    },
+    list: {
+      content: [],
+      totalElements: 0,
+      pageIndex: 0,
+      pageSize: 8
+    },
+    trend: {
+      hourlyStats: {}
+    },
+    coverage: {
+      traceCoverage: {
+        withBothTraceAndSpan: 0,
+        withTrace: 0,
+        withoutTrace: 0,
+        withSpan: 0
+      }
+    },
+    query,
+    loadStatus: {
+      state: 'degraded',
+      message
+    }
+  };
+}
+
+function describeLogManageLoadFailure(error: unknown) {
+  return error instanceof Error && error.message ? error.message : 'Log API request failed';
+}
+
+async function apiMessageGetWithTimeout<T>(path: string, timeoutMs = LOG_MANAGE_API_TIMEOUT_MS): Promise<T> {
+  const controller = new AbortController();
+  let timeoutId: ReturnType<typeof setTimeout> | undefined;
+  const timeout = new Promise<never>((_, reject) => {
+    timeoutId = setTimeout(() => {
+      controller.abort();
+      reject(new Error(`Log API request timed out after ${timeoutMs}ms`));
+    }, timeoutMs);
+  });
+
+  try {
+    return await Promise.race([
+      apiMessageGet<T>(path, { signal: controller.signal }),
+      timeout
+    ]);
+  } finally {
+    if (timeoutId) clearTimeout(timeoutId);
+  }
+}
+
 type StreamLogItem = {
   key: string;
   entry: LogEntry;
@@ -150,6 +214,8 @@ type RelatedTracePreviewState = {
   contextLog: LogEntry | null;
 };
 
+type LogManageTranslate = ReturnType<typeof useI18n>['t'];
+
 function readLogAttribute(source: Record<string, unknown> | undefined, key: string) {
   const value = source?.[key];
   return typeof value === 'string' ? value.trim() || undefined : undefined;
@@ -171,79 +237,86 @@ function LogEntityDetailAction({
   href,
   canOpen,
   missingEntityHandoffTitle,
+  label,
+  actionScope = 'detail',
   showIcon = false
 }: {
   href: string;
   canOpen: boolean;
   missingEntityHandoffTitle: string;
+  label: string;
+  actionScope?: 'header' | 'stream-detail' | 'detail';
   showIcon?: boolean;
 }) {
-  const icon = showIcon ? <Workflow className="h-4 w-4" aria-hidden="true" /> : null;
+  const icon = showIcon ? (
+    <HzButtonIcon
+      icon={Workflow}
+      data-log-manage-header-action-icon={actionScope === 'header' ? 'entity' : undefined}
+      data-log-manage-header-action-icon-owner={actionScope === 'header' ? 'hertzbeat-ui-button-icon' : undefined}
+    />
+  ) : null;
+  const actionScopeProps =
+    actionScope === 'header'
+      ? { 'data-log-manage-header-action': 'entity' }
+      : actionScope === 'stream-detail'
+        ? { 'data-log-manage-stream-detail-action': 'entity' }
+        : { 'data-log-manage-detail-action': 'entity' };
 
   if (canOpen) {
     return (
-      <Link data-log-manage-entity-action="true" href={href} className={actionClass}>
+      <HzButtonLink component={Link} data-log-manage-entity-action="true" href={href} size="md" {...actionScopeProps}>
         {icon}
-        实体详情
-      </Link>
+        {label}
+      </HzButtonLink>
     );
   }
 
   return (
     <span className="inline-flex" title={missingEntityHandoffTitle}>
-      <button
+      <HzButton
         data-log-manage-entity-action="true"
         data-log-manage-entity-action-disabled="missing-entity-id"
-        type="button"
-        className={disabledActionClass}
+        size="md"
         disabled
         title={missingEntityHandoffTitle}
         aria-label={missingEntityHandoffTitle}
+        {...actionScopeProps}
       >
         {icon}
-        实体详情
-      </button>
+        {label}
+      </HzButton>
     </span>
   );
 }
 
-function LogAttributionDiagnosticsPanel({ rows }: { rows: LogAttributionDiagnostic[] }) {
+function LogAttributionDiagnosticsPanel({ rows, t }: { rows: LogAttributionDiagnostic[]; t: LogManageTranslate }) {
   if (rows.length === 0) return null;
 
   return (
-    <div
+    <HzAttributeDiagnostics
       data-log-manage-selected-attribution-diagnostics="hertzbeat-attribute-diagnostics"
-      aria-label="归因诊断 hertzbeat.entity_id hertzbeat.collector hertzbeat.template"
+      data-log-manage-selected-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"
+      aria-label={t('log.manage.selected.attribution.aria')}
       className="mt-4 rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3"
-    >
-      <div className="mb-2 flex items-center justify-between gap-2">
-        <p className="text-[11px] font-semibold text-[#8792a5]">归因诊断</p>
-        <span className="text-[11px] font-semibold text-[#6d7788]">hertzbeat.*</span>
-      </div>
-      <div className="space-y-2">
-        {rows.map(row => (
-          <div
-            key={row.key}
-            data-log-manage-selected-attribution-diagnostic-state={row.state}
-            className="grid grid-cols-[minmax(0,1fr)_52px] gap-2 text-[11px]"
-          >
-            <span className="min-w-0">
-              <span className="block truncate font-mono font-semibold text-[#e6edf7]">{row.label}</span>
-              <span className="block truncate text-[#6d7788]">{row.value} · {row.meta}</span>
-            </span>
-            <span
-              className={`inline-flex h-5 items-center justify-center rounded-[3px] border px-1.5 font-semibold ${
-                row.state === 'present'
-                  ? 'border-[rgba(96,181,134,0.36)] bg-[rgba(96,181,134,0.1)] text-[rgba(120,220,160,0.95)]'
-                  : 'border-[rgba(216,111,91,0.36)] bg-[rgba(216,111,91,0.1)] text-[rgba(244,154,168,0.95)]'
-              }`}
-            >
-              {row.state === 'present' ? '已提供' : '缺失'}
-            </span>
-          </div>
-        ))}
-      </div>
-    </div>
+      title={t('log.manage.selected.attribution.title')}
+      namespaceLabel="hertzbeat.*"
+      rows={rows.map(row => ({
+        key: row.key,
+        label: row.label,
+        value: row.value,
+        meta: row.meta,
+        state: row.state,
+        stateLabel: row.state === 'present'
+          ? t('log.manage.selected.attribution.state.present')
+          : t('log.manage.selected.attribution.state.missing'),
+        tone: row.state === 'present' ? 'success' : 'critical',
+        rowProps: { 'data-log-manage-selected-attribution-diagnostic-state': row.state },
+        badgeProps: {
+          'data-log-manage-attribution-diagnostic-badge-owner': 'hertzbeat-ui-status-badge',
+          'data-log-manage-attribution-diagnostic-badge-state': row.state
+        }
+      }))}
+    />
   );
 }
 
@@ -282,14 +355,14 @@ function stringifyLogEntry(entry: LogEntry | null) {
   }
 }
 
-function resolveLogDetailTitle(entry: LogEntry | null) {
-  if (!entry) return '日志详情';
+function resolveLogDetailTitle(entry: LogEntry | null, t: LogManageTranslate) {
+  if (!entry) return t('log.manage.stream.detail.title');
   const text = bodyText(entry.body);
-  return text.length > 96 ? `${text.slice(0, 96)}...` : text || '日志详情';
+  return text.length > 96 ? `${text.slice(0, 96)}...` : text || t('log.manage.stream.detail.title');
 }
 
-function resolveLogDetailSubtitle(entry: LogEntry | null) {
-  if (!entry) return '未选择日志';
+function resolveLogDetailSubtitle(entry: LogEntry | null, t: LogManageTranslate) {
+  if (!entry) return t('log.manage.stream.selected.none');
   return (
     readLogAttribute(entry.resource, 'service.name') ||
     readLogAttribute(entry.attributes, 'service.name') ||
@@ -298,10 +371,10 @@ function resolveLogDetailSubtitle(entry: LogEntry | null) {
   );
 }
 
-function streamStatusCopy(status: 'connecting' | 'connected' | 'disconnected') {
-  if (status === 'connected') return '已连接';
-  if (status === 'connecting') return '连接中';
-  return '未连接';
+function streamStatusCopy(status: 'connecting' | 'connected' | 'disconnected', t: LogManageTranslate) {
+  if (status === 'connected') return t('log.manage.stream.status.connected');
+  if (status === 'connecting') return t('log.manage.stream.status.connecting');
+  return t('log.manage.stream.status.disconnected');
 }
 
 function formatTraceDurationNanos(value?: number | null) {
@@ -321,18 +394,6 @@ function buildTraceTimelineTicks(durationNanos?: number | null) {
   }));
 }
 
-function tracePreviewTranslator(t: (key: string, params?: Record<string, string | number | null | undefined>) => string) {
-  return (key: string, params?: Record<string, string | number | null | undefined>) => {
-    const tracePreviewMessages: Record<string, string> = {
-      'trace.manage.empty-selected-span.title': '选择一个跨度',
-      'trace.manage.empty-selected-span.copy': '选择左侧跨度后查看服务、状态和事件证据。',
-      'trace.manage.selected-span.service-namespace': '服务与命名空间',
-      'trace.manage.trace-state-empty': '暂无链路状态'
-    };
-    return tracePreviewMessages[key] || t(key, params);
-  };
-}
-
 function mergeTraceDetailSpans(detail: TraceDetail, spans: TraceDetail['spans']): TraceDetail {
   return {
     ...detail,
@@ -340,11 +401,6 @@ function mergeTraceDetailSpans(detail: TraceDetail, spans: TraceDetail['spans'])
   };
 }
 
-function hasDisplayReturnLabel(searchParams: { get(name: string): string | null }) {
-  const returnTo = searchParams.get('returnTo') || '';
-  return Boolean(searchParams.get('returnLabel') || returnTo.includes('returnLabel='));
-}
-
 function LogManageExplorer({
   data,
   query,
@@ -380,11 +436,14 @@ function LogManageExplorer({
     formatTime,
     severityLabel
   });
+  const logEntryByRowKey = useMemo(
+    () => new Map(rows.map((row, index) => [row.key, data.list.content?.[index] ?? null])),
+    [data.list.content, rows]
+  );
   const selectedSeverity = draft.severityText.trim().toUpperCase();
   const latestObservedAt = data.overview.latestObservedAt ? formatTime(data.overview.latestObservedAt) : '-';
   const traceCoverage = data.coverage.traceCoverage?.withBothTraceAndSpan ?? 0;
   const trendBars = Object.entries(data.trend.hourlyStats || {}).slice(-12);
-  const maxTrend = Math.max(...trendBars.map(([, count]) => Number(count) || 0), 1);
   const activeRow = rows[0];
   const activeEntry = data.list.content?.[0] ?? null;
   const handoffLinks = buildLogHandoffLinks(activeEntry, routeContext, {
@@ -396,10 +455,10 @@ function LogManageExplorer({
   const activeEnvironment = activeEntry?.resource?.['deployment.environment.name']?.toString();
   const activeAttributionDiagnostics = buildLogAttributionDiagnostics(activeEntry, t);
   const activeLogEvidenceRows = [
-    ['日志时间', activeRow?.timestamp || '-', '日志写入时间'],
-    ['日志级别', activeRow?.severity || '-', selectedSeverity || '当前筛选'],
-    ['正文摘要', activeRow?.message || '-', '日志正文'],
-    ['最近上报', latestObservedAt, '工作台最新日志']
+    [t('log.manage.evidence.time.title'), activeRow?.timestamp || '-', t('log.manage.evidence.time.meta')],
+    [t('log.manage.evidence.severity.title'), activeRow?.severity || '-', selectedSeverity || t('log.manage.evidence.severity.current-filter')],
+    [t('log.manage.evidence.body.title'), activeRow?.message || '-', t('log.manage.evidence.body.meta')],
+    [t('log.manage.evidence.latest.title'), latestObservedAt, t('log.manage.evidence.latest.meta')]
   ];
   const entityContextRows = buildSignalEntityContextRows(routeContext, {
     serviceName: activeServiceName,
@@ -496,28 +555,28 @@ function LogManageExplorer({
   const relatedTraceSelectedFacts = buildSelectedSpanFacts(
     relatedTraceSelectedSpan,
     relatedTraceDetail,
-    tracePreviewTranslator(t),
+    t,
     formatTraceDurationNanos
   );
   const relatedTraceStageFacts = relatedTraceDetail
     ? [
         {
-          label: '当前跨度',
+          label: t('log.manage.related-trace.fact.current-span'),
           value: relatedTraceSelectedSpan?.spanName || relatedTraceSelectedSpan?.spanId || relatedTraceDetail.rootSpanName || relatedTraceDetail.traceId,
           tone: 'accent' as const
         },
         {
-          label: '错误跨度',
+          label: t('log.manage.related-trace.fact.error-spans'),
           value: String(relatedTraceDetail.errorSpanCount || relatedTraceRows.filter(row => row.tone === 'danger').length),
           tone: (relatedTraceDetail.errorSpanCount || relatedTraceRows.filter(row => row.tone === 'danger').length) > 0 ? ('error' as const) : ('default' as const)
         },
         {
-          label: '事件',
+          label: t('log.manage.related-trace.fact.events'),
           value: String(relatedTraceEventCount),
           tone: 'default' as const
         },
         {
-          label: '关联',
+          label: t('log.manage.related-trace.fact.links'),
           value: String((relatedTraceSelectedSpan?.links || []).length),
           tone: 'default' as const
         }
@@ -742,7 +801,7 @@ function LogManageExplorer({
       setRelatedTracePreview({
         open: true,
         loading: false,
-        error: '链路预览暂时不可用,请稍后重试或打开完整链路工作台。',
+        error: t('log.manage.related-trace.error'),
         detail: null,
         selectedSpanId: entry.spanId || null,
         selectedEventKey: null,
@@ -778,88 +837,108 @@ function LogManageExplorer({
 
   const renderViewSwitch = () =>
     showViewToggle ? (
-      <section data-log-manage-view-switch="stream-history" className={`${panelClass} px-3 py-3`}>
-        <div className="flex flex-wrap items-center justify-between gap-2">
-          <div className="text-[12px] font-semibold text-[#8792a5]">日志视图</div>
-          <div className="ml-auto flex flex-wrap items-center justify-end gap-2">
-            <button
-              type="button"
+      <HzPanelSurface
+        data-log-manage-view-switch="stream-history"
+        data-log-manage-panel-surface="view-switch"
+        data-log-manage-view-switch-panel-surface-owner="hertzbeat-ui-panel-surface"
+        padding="view-switch"
+      >
+        <HzWorkbenchLayout
+          as="div"
+          variant="view-switch"
+          data-log-manage-view-switch-layout="shared-view-switch"
+          data-log-manage-view-switch-layout-owner="hertzbeat-ui-workbench-layout"
+        >
+          <div className="text-[12px] font-semibold text-[#8792a5]">{t('log.manage.stream.view-switch.title')}</div>
+          <HzActionGroup
+            layout="end-wrap"
+            data-log-manage-view-toggle-group="shared-action-group"
+            data-log-manage-view-toggle-group-owner="hertzbeat-ui-action-group"
+          >
+            <HzButton
+              data-log-manage-view-toggle-control="shared-hz-button"
               data-log-manage-view-option="stream"
               data-log-manage-view-active={currentView === 'stream'}
+              aria-pressed={currentView === 'stream'}
+              intent={currentView === 'stream' ? 'primary' : 'secondary'}
+              size="md"
               onClick={() => switchView('stream')}
-              className={currentView === 'stream' ? primaryActionClass : actionClass}
             >
               <Wifi className="h-4 w-4" aria-hidden="true" />
-              日志流
-            </button>
-            <button
-              type="button"
+              {t('log.manage.stream.view.stream')}
+            </HzButton>
+            <HzButton
+              data-log-manage-view-toggle-control="shared-hz-button"
               data-log-manage-view-option="list"
               data-log-manage-view-active={currentView === 'list'}
+              aria-pressed={currentView === 'list'}
+              intent={currentView === 'list' ? 'primary' : 'secondary'}
+              size="md"
               onClick={() => switchView('list')}
-              className={currentView === 'list' ? primaryActionClass : actionClass}
             >
               <ScrollText className="h-4 w-4" aria-hidden="true" />
-              历史检索
-            </button>
-          </div>
-        </div>
-      </section>
+              {t('log.manage.stream.view.history')}
+            </HzButton>
+          </HzActionGroup>
+        </HzWorkbenchLayout>
+      </HzPanelSurface>
     ) : null;
 
   const renderDetailDialog = () => (
     <LogStreamDetailDialog
       open={detailLog != null}
       onClose={() => setDetailSelection(null)}
-      title={detailSelection?.source === 'stream' ? '日志流详情' : '日志详情'}
-      subtitle={resolveLogDetailTitle(detailLog)}
+      title={detailSelection?.source === 'stream' ? t('log.manage.stream.detail.stream-title') : t('log.manage.stream.detail.title')}
+      subtitle={resolveLogDetailTitle(detailLog, t)}
       traceId={detailLog?.traceId}
       selectionState={detailSelection?.selectionState}
+      warning={detailSelection?.selectionState === 'detached' ? t('log.manage.stream.detail.detached-warning') : undefined}
       facts={detailFacts}
       attributionDiagnostics={detailAttributionDiagnostics}
       badges={['JSON']}
       metaItems={[
-        resolveLogDetailSubtitle(detailLog),
+        resolveLogDetailSubtitle(detailLog, t),
         ...(detailLog?.traceId ? [`traceId · ${detailLog.traceId}`] : [])
       ]}
       actions={
         <div className="flex flex-wrap gap-2">
           {detailLog?.traceId ? (
-            <button
+            <HzButton
+              data-log-manage-detail-dialog-action="trace"
               data-log-manage-results-open-trace-action="true"
-              type="button"
-              className={actionClass}
+              size="md"
               onClick={() => openRelatedTracePreview(detailLog)}
             >
-              查看链路
-            </button>
+              {t('log.manage.stream.action.view-trace')}
+            </HzButton>
           ) : (
             <span className="inline-flex" title={missingTraceHandoffTitle}>
-              <button
+              <HzButton
+                data-log-manage-detail-dialog-action="trace"
                 data-log-manage-results-open-trace-action="true"
                 data-log-manage-results-open-trace-action-disabled="missing-trace-id"
-                type="button"
-                className={disabledActionClass}
+                size="md"
                 disabled
                 title={missingTraceHandoffTitle}
                 aria-label={missingTraceHandoffTitle}
               >
-                查看链路
-              </button>
+                {t('log.manage.stream.action.view-trace')}
+              </HzButton>
             </span>
           )}
-          <Link href={detailHandoffLinks.metricsHref} className={actionClass}>
-            查看指标
-          </Link>
+          <HzButtonLink component={Link} data-log-manage-detail-dialog-action="metrics" href={detailHandoffLinks.metricsHref} size="md">
+            {t('log.manage.stream.action.view-metrics')}
+          </HzButtonLink>
           <LogEntityDetailAction
             href={detailHandoffLinks.entityHref}
             canOpen={canOpenDetailEntity}
             missingEntityHandoffTitle={missingEntityHandoffTitle}
+            label={t('log.manage.route.action.entity')}
           />
           {detailCodeHref ? (
-            <a href={detailCodeHref} target="_blank" rel="noreferrer" className={actionClass}>
-              查看代码
-            </a>
+            <HzButtonLink data-log-manage-detail-dialog-action="code" href={detailCodeHref} target="_blank" rel="noreferrer" size="md">
+              {t('log.manage.stream.action.view-code')}
+            </HzButtonLink>
           ) : null}
         </div>
       }
@@ -872,8 +951,8 @@ function LogManageExplorer({
     <LogRelatedTraceDialog
       open={relatedTracePreview.open}
       onClose={closeRelatedTracePreview}
-      title={relatedTraceSelectedSpan?.spanName || relatedTraceDetail?.rootSpanName || relatedTraceDetail?.traceId || '关联链路预览'}
-      subtitle={relatedTraceDetail?.serviceName || resolveLogDetailSubtitle(relatedTraceContextLog)}
+      title={relatedTraceSelectedSpan?.spanName || relatedTraceDetail?.rootSpanName || relatedTraceDetail?.traceId || t('log.manage.related-trace.fallback-title')}
+      subtitle={relatedTraceDetail?.serviceName || resolveLogDetailSubtitle(relatedTraceContextLog, t)}
       loading={relatedTracePreview.loading}
       error={relatedTracePreview.error}
       rows={relatedTraceRows}
@@ -883,41 +962,47 @@ function LogManageExplorer({
       onSelectEvent={(eventKey, rowKey) => setRelatedTracePreview(previous => ({ ...previous, selectedSpanId: rowKey || previous.selectedSpanId, selectedEventKey: eventKey }))}
       stageMeta={[
         ...(relatedTraceDetail?.traceId ? [relatedTraceDetail.traceId] : []),
-        relatedTraceRows.length > 0 ? `${relatedTraceRows.length} 个跨度` : '',
+        relatedTraceRows.length > 0 ? t('log.manage.related-trace.spans-count', { count: relatedTraceRows.length }) : '',
         ...(relatedTraceDetail?.durationNanos ? [formatTraceDurationNanos(relatedTraceDetail.durationNanos)] : [])
       ].filter(Boolean)}
-      badges={relatedTraceDetail ? ['链路预览'] : []}
+      badges={relatedTraceDetail ? [t('log.manage.related-trace.badge')] : []}
       metaItems={relatedTraceDetail ? [`traceId · ${relatedTraceDetail.traceId}`] : []}
       stageFacts={relatedTraceStageFacts}
       timelineTicks={buildTraceTimelineTicks(relatedTraceDetail?.durationNanos)}
       selectedFacts={relatedTraceSelectedFacts}
       headerAction={
         relatedTraceWorkspaceEntry?.traceId ? (
-          <Link data-log-related-trace-open-workspace-action="true" href={relatedTraceWorkspaceHref} className={actionClass}>
-            查看完整链路
-          </Link>
+          <HzButtonLink
+            component={Link}
+            data-log-related-trace-open-workspace-action="true"
+            data-log-related-trace-detail-action="workspace"
+            href={relatedTraceWorkspaceHref}
+            size="md"
+          >
+            {t('log.manage.related-trace.open-workspace')}
+          </HzButtonLink>
         ) : (
           <span className="inline-flex" title={missingFullTraceHandoffTitle}>
-            <button
+            <HzButton
               data-log-related-trace-open-workspace-action-disabled="missing-trace-id"
-              type="button"
-              className={disabledActionClass}
+              data-log-related-trace-detail-action="workspace"
+              size="md"
               disabled
               title={missingFullTraceHandoffTitle}
               aria-label={missingFullTraceHandoffTitle}
             >
-              查看完整链路
-            </button>
+              {t('log.manage.related-trace.open-workspace')}
+            </HzButton>
           </span>
         )
       }
-      emptyTitle="暂无关联链路"
-      emptyCopy="该日志没有可预览的链路详情。"
-      loadingTitle="正在加载链路预览"
-      loadingCopy="正在从当前日志上下文加载链路预览。"
-      spanLabel="跨度"
-      durationLabel="耗时"
-      timelineLabel="时间轴"
+      emptyTitle={t('log.manage.related-trace.empty-title')}
+      emptyCopy={t('log.manage.related-trace.empty-copy')}
+      loadingTitle={t('log.manage.related-trace.loading-title')}
+      loadingCopy={t('log.manage.related-trace.loading-copy')}
+      spanLabel={t('log.manage.related-trace.span-label')}
+      durationLabel={t('log.manage.related-trace.duration-label')}
+      timelineLabel={t('log.manage.related-trace.timeline-label')}
     />
   );
 
@@ -926,76 +1011,87 @@ function LogManageExplorer({
       ? buildSelectedLogRows(selectedStreamEntry, t, bodyText, formatTime, severityLabel)
       : [];
     const pauseVisible = shouldShowStreamPauseOverlay({ isPaused: isStreamPaused, itemCount: streamItems.length });
+    const streamStageHeaderActions = (
+      <>
+        <HzStatusBadge
+          data-log-manage-stream-status={streamStatus}
+          data-log-manage-stream-status-badge-owner="hertzbeat-ui-status-badge"
+          tone={logStreamStatusBadgeTone(streamStatus)}
+          size="md"
+        >
+          {streamStatus === 'connected' ? <Wifi className="h-4 w-4" aria-hidden="true" /> : <WifiOff className="h-4 w-4" aria-hidden="true" />}
+          {streamStatusCopy(streamStatus, t)}
+        </HzStatusBadge>
+        <HzStatusBadge data-log-manage-stream-count-badge-owner="hertzbeat-ui-status-badge" tone="neutral" size="md">
+          {t('log.manage.stream.count', { count: streamItems.length })}
+        </HzStatusBadge>
+        <HzStatusBadge
+          data-log-manage-stream-live-chip={isStreamPaused ? 'paused' : 'live'}
+          data-log-manage-stream-live-badge-owner="hertzbeat-ui-status-badge"
+          tone={isStreamPaused ? 'warning' : 'success'}
+          size="md"
+        >
+          {isStreamPaused ? t('log.manage.stream.state.paused') : t('log.manage.stream.state.live')}
+        </HzStatusBadge>
+        <HzButton
+          data-log-manage-reconnect-action="true"
+          data-log-manage-stream-control="reconnect"
+          size="md"
+          onClick={() => setStreamReconnectNonce(value => value + 1)}
+        >
+          <RotateCcw className="h-4 w-4" aria-hidden="true" />
+          {t('log.manage.stream.action.reconnect')}
+        </HzButton>
+        <HzButton data-log-manage-stream-control="pause-toggle" size="md" onClick={() => setIsStreamPaused(value => !value)}>
+          {isStreamPaused ? <PlayCircle className="h-4 w-4" aria-hidden="true" /> : <PauseCircle className="h-4 w-4" aria-hidden="true" />}
+          {isStreamPaused ? t('log.manage.stream.action.resume') : t('log.manage.stream.action.pause')}
+        </HzButton>
+        <HzButton data-log-manage-stream-control="clear" size="md" onClick={clearStream}>
+          <Trash2 className="h-4 w-4" aria-hidden="true" />
+          {t('log.manage.stream.action.clear')}
+        </HzButton>
+      </>
+    );
 
     return (
-      <section
+      <HzPanelSurface
         data-log-manage-stream-stage="hertzbeat-live-log-stream"
+        data-log-manage-panel-surface="stream-stage"
         data-log-manage-stream-live-state={isStreamPaused ? 'paused' : 'live'}
-        className={`${panelClass} overflow-hidden`}
+        data-log-manage-stream-stage-panel-surface-owner="hertzbeat-ui-panel-surface"
+        clip
       >
-        <div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#252b35] px-4 py-3">
-          <div className="min-w-0">
-            <p className="text-[12px] font-semibold text-[#8792a5]">日志流</p>
-            <h2 className="mt-1 text-[18px] font-semibold text-[#f0f4fa]">实时日志流</h2>
-          </div>
-          <div className="flex flex-wrap items-center gap-2">
-            <span
-              data-log-manage-stream-status={streamStatus}
-              className={`inline-flex h-8 items-center gap-2 rounded-[3px] border px-3 text-[12px] font-semibold ${
-                streamStatus === 'connected'
-                  ? 'border-[#315b49] bg-[#0d1b17] text-[#8bd8ad]'
-                  : streamStatus === 'connecting'
-                    ? 'border-[#6f5730] bg-[#1d170d] text-[#efca83]'
-                    : 'border-[#70424a] bg-[#1d1115] text-[#f39aa8]'
-              }`}
-            >
-              {streamStatus === 'connected' ? <Wifi className="h-4 w-4" aria-hidden="true" /> : <WifiOff className="h-4 w-4" aria-hidden="true" />}
-              {streamStatusCopy(streamStatus)}
-            </span>
-            <span className="inline-flex h-8 items-center rounded-[3px] border border-[#2b3039] px-3 text-[12px] font-semibold text-[#dbe3ee]">
-              {streamItems.length} 条日志
-            </span>
-            <span
-              data-log-manage-stream-live-chip={isStreamPaused ? 'paused' : 'live'}
-              className={`inline-flex h-8 items-center rounded-[3px] border px-3 text-[12px] font-semibold ${
-                isStreamPaused ? 'border-[#6f5730] bg-[#1d170d] text-[#efca83]' : 'border-[#315b49] bg-[#0d1b17] text-[#8bd8ad]'
-              }`}
-            >
-              {isStreamPaused ? '已暂停' : '实时'}
-            </span>
-            <button
-              data-log-manage-reconnect-action="true"
-              type="button"
-              className={actionClass}
-              onClick={() => setStreamReconnectNonce(value => value + 1)}
-            >
-              <RotateCcw className="h-4 w-4" aria-hidden="true" />
-              重连
-            </button>
-            <button type="button" className={actionClass} onClick={() => setIsStreamPaused(value => !value)}>
-              {isStreamPaused ? <PlayCircle className="h-4 w-4" aria-hidden="true" /> : <PauseCircle className="h-4 w-4" aria-hidden="true" />}
-              {isStreamPaused ? '继续' : '暂停'}
-            </button>
-            <button type="button" className={actionClass} onClick={clearStream}>
-              <Trash2 className="h-4 w-4" aria-hidden="true" />
-              清空
-            </button>
-          </div>
-        </div>
+        <HzPanelHeader
+          data-log-manage-stream-stage-header-owner="hertzbeat-ui-panel-header"
+          eyebrow={t('log.manage.stream.stage.kicker')}
+          title={t('log.manage.stream.stage.title')}
+          actions={streamStageHeaderActions}
+        />
         {pauseVisible ? (
-          <div className="border-b border-[#2b3039] bg-[#17140b] px-4 py-2 text-[12px] font-semibold text-[#efca83]">
-            日志流已暂停,新的日志会暂时留在采集链路中,继续后重新连接。
-          </div>
+          <HzStateNotice
+            data-log-manage-stream-pause-notice-owner="hertzbeat-ui-state-notice"
+            data-log-manage-stream-pause-notice="paused-buffer-visible"
+            tone="warning"
+            title={t('log.manage.stream.paused-copy')}
+            variant="embedded"
+            className="border-b border-t-0 border-[#2b3039] bg-[#17140b]"
+          />
         ) : null}
-        <div className="grid min-h-[520px] lg:grid-cols-[minmax(0,1fr)_320px]">
-          <div
+        <HzWorkbenchLayout
+          as="div"
+          variant="stream-stage"
+          data-log-manage-stream-stage-layout="shared-stream-stage"
+          data-log-manage-stream-stage-layout-owner="hertzbeat-ui-workbench-layout"
+        >
+          <HzScrollViewport
             ref={streamViewportRef}
+            variant="log-stream"
             data-log-manage-stream-viewport="virtualized-log-stream"
+            data-log-manage-stream-viewport-owner="hertzbeat-ui-scroll-viewport"
             data-log-manage-stream-window={`${streamWindow.startIndex}:${streamWindow.endIndex}`}
             data-log-manage-stream-row-height={STREAM_VIEWPORT_ROW_HEIGHT}
             data-log-manage-stream-retention={`${streamItems.length}/${maxStreamEntries}`}
             onScroll={event => setStreamViewport(readStreamViewportState(event.currentTarget))}
-            className="hb-scrollbar max-h-[620px] overflow-auto"
           >
             {streamItems.length > 0 ? (
               <div className="divide-y divide-[#252b35]">
@@ -1005,24 +1101,28 @@ function LogManageExplorer({
                   const entryTraceId = entry.traceId || '-';
                   const selected = item.key === selectedStreamKey;
                   const rowIndex = streamWindow.startIndex + offset;
+                  const streamSeverity = severityLabel(entry);
                   return (
-                    <button
+                    <HzLogStreamLiveRow
                       key={item.key}
-                      type="button"
+                      selected={selected}
                       data-log-manage-stream-row="true"
+                      data-log-manage-stream-row-owner="hertzbeat-ui-log-stream-row"
                       data-log-manage-stream-row-index={rowIndex}
                       data-log-manage-stream-row-style="compact-live-row"
                       data-log-manage-stream-trace-id={entryTraceId}
                       data-log-manage-stream-selected={selected}
                       onClick={() => selectStreamItem(item)}
                       style={{ height: STREAM_VIEWPORT_ROW_HEIGHT }}
-                      className={`grid w-full grid-cols-[58px_minmax(0,112px)_minmax(0,1fr)] items-center gap-3 px-4 text-left transition-colors hover:bg-[#10141b] sm:grid-cols-[64px_156px_minmax(0,1fr)_180px] lg:grid-cols-[72px_176px_minmax(0,1fr)_220px] ${
-                        selected ? 'bg-[#111927]' : 'bg-transparent'
-                      }`}
                     >
-                      <span className={`inline-flex h-5 min-w-[50px] items-center justify-center self-center rounded-[3px] border px-2 text-[11px] font-semibold ${severityTone(severityLabel(entry))}`}>
-                        {severityLabel(entry)}
-                      </span>
+                      <HzStatusBadge
+                        data-log-manage-stream-severity-tone={logSeverityTone(streamSeverity)}
+                        data-log-manage-stream-severity-badge-owner="hertzbeat-ui-status-badge"
+                        tone={logSeverityStatusTone(logSeverityTone(streamSeverity))}
+                        className="min-w-[50px] justify-center self-center"
+                      >
+                        {streamSeverity}
+                      </HzStatusBadge>
                       <span className="truncate font-mono text-[11px] text-[#7f8a9d]">
                         {formatTime(entry.timeUnixNano ? entry.timeUnixNano / 1_000_000 : null)}
                       </span>
@@ -1031,77 +1131,98 @@ function LogManageExplorer({
                         <span className="truncate">{readLogAttribute(entry.resource, 'service.name') || 'unknown-service'}</span>
                         <span className="min-w-[72px] truncate font-mono">traceId · {entryTraceId}</span>
                       </span>
-                    </button>
+                    </HzLogStreamLiveRow>
                   );
                 })}
                 {streamWindow.bottomSpacerHeight > 0 ? <div aria-hidden="true" style={{ height: streamWindow.bottomSpacerHeight }} /> : null}
               </div>
             ) : (
-              <div data-log-manage-stream-empty-state="true" className="flex h-[520px] items-center justify-center text-center text-[13px] text-[#d5d8e1]">
-                <div>
-                  <div className="mx-auto grid h-11 w-11 place-items-center rounded-[4px] border border-[#2b3039] bg-[#101217]">
-                    <Wifi className="h-5 w-5 text-[#9aa6b8]" aria-hidden="true" />
-                  </div>
-                  <p className="mt-3 font-semibold">等待实时日志</p>
-                  <p className="mt-2 text-[#8f9bad]">保持连接后,Collector 和 OTLP 写入的日志会实时进入这里。</p>
-                </div>
-              </div>
+              <HzEmptyState
+                data-log-manage-stream-empty-state="true"
+                data-log-manage-stream-empty-owner="hertzbeat-ui-empty-state"
+                title={t('log.manage.stream.empty.title')}
+                description={t('log.manage.stream.empty.copy')}
+                className="h-[520px] border-y-0 text-left"
+              />
             )}
-          </div>
-          <aside className="border-l border-[#252b35] bg-[#0b0e13] px-4 py-4">
-            <div className="flex items-center justify-between gap-2">
-              <p className="text-[12px] font-semibold text-[#8792a5]">选中日志</p>
-              {streamSelection.detached ? <span className="text-[11px] font-semibold text-[#efca83]">已脱离缓冲</span> : null}
-            </div>
-            <h3 className="mt-2 line-clamp-2 text-[16px] font-semibold text-[#f0f4fa]">
-              {selectedStreamEntry ? resolveLogDetailTitle(selectedStreamEntry) : '未选择日志'}
-            </h3>
-            <div className="mt-4 space-y-2">
+          </HzScrollViewport>
+          <HzDetailAside
+            data-log-manage-stream-selected-aside="shared-detail-aside"
+            data-log-manage-stream-selected-aside-owner="hertzbeat-ui-detail-aside"
+          >
+            <HzPanelHeader
+              data-log-manage-stream-selected-header-owner="hertzbeat-ui-panel-header"
+              eyebrow={t('log.manage.stream.selected.title')}
+              title={selectedStreamEntry ? resolveLogDetailTitle(selectedStreamEntry, t) : t('log.manage.stream.selected.none')}
+              meta={streamSelection.detached ? (
+                <HzStatusBadge
+                  data-log-manage-stream-selected-detached-badge-owner="hertzbeat-ui-status-badge"
+                  tone="warning"
+                  size="xs"
+                >
+                  {t('log.manage.stream.selected.detached')}
+                </HzStatusBadge>
+              ) : null}
+              className="-mx-4 -mt-4 border-x-0 border-t-0 bg-transparent"
+            />
+            <HzDetailBodyStack
+              data-log-manage-stream-selected-body="shared-detail-body-stack"
+              data-log-manage-stream-selected-body-owner="hertzbeat-ui-detail-body-stack"
+            >
               {selectedStreamRows.length > 0 ? (
-                selectedStreamRows.map(row => (
-                  <div key={row.title} className="rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-2">
-                    <p className="truncate text-[11px] font-semibold text-[#8792a5]">{row.title}</p>
-                    <p className="mt-1 truncate text-[12px] font-semibold text-[#e6edf7]">{row.copy}</p>
-                    <p className="mt-1 truncate text-[11px] text-[#6d7788]">{row.meta}</p>
-                  </div>
-                ))
+                <HzDetailRows
+                  data-log-manage-stream-selected-detail-owner="hertzbeat-ui-detail-rows"
+                  rows={selectedStreamRows}
+                />
               ) : (
-                <p className="text-[12px] text-[#7f8a9d]">点击任意日志查看完整内容、链路和实体上下文。</p>
+                <HzStateNotice
+                  data-log-manage-stream-selected-helper-owner="hertzbeat-ui-state-notice"
+                  data-log-manage-stream-selected-helper="selected-empty"
+                  tone="info"
+                  title={t('log.manage.stream.selected.helper')}
+                  variant="hint"
+                />
               )}
-            </div>
-            <LogAttributionDiagnosticsPanel rows={selectedStreamAttributionDiagnostics} />
-            <div className="mt-4 flex flex-col gap-2">
-              <button
-                type="button"
-                className={actionClass}
+            </HzDetailBodyStack>
+            <LogAttributionDiagnosticsPanel rows={selectedStreamAttributionDiagnostics} t={t} />
+            <HzControlStack
+              data-log-manage-stream-detail-action-stack="shared-control-stack"
+              data-log-manage-stream-detail-action-stack-owner="hertzbeat-ui-control-stack"
+              className="mt-4"
+            >
+              <HzButton
+                data-log-manage-stream-detail-action="view-log"
+                size="md"
                 disabled={!selectedStreamEntry}
                 onClick={() => openLogDetails(selectedStreamEntry, 'stream', streamSelection.detached ? 'detached' : 'attached')}
               >
                 <Eye className="h-4 w-4" aria-hidden="true" />
-                查看日志
-              </button>
-              <button
+                {t('log.manage.stream.action.view-log')}
+              </HzButton>
+              <HzButton
                 data-log-manage-results-open-trace-action="true"
                 data-log-manage-open-log-detail-before-trace="true"
-                type="button"
-                className={selectedStreamEntry?.traceId ? actionClass : disabledActionClass}
+                data-log-manage-stream-detail-action="view-trace"
+                size="md"
                 disabled={!selectedStreamEntry?.traceId}
                 title={!selectedStreamEntry?.traceId ? missingTraceHandoffTitle : undefined}
                 aria-label={!selectedStreamEntry?.traceId ? missingTraceHandoffTitle : undefined}
                 data-log-manage-results-open-trace-action-disabled={!selectedStreamEntry?.traceId ? 'missing-trace-id' : undefined}
                 onClick={() => openTraceDrilldownFromLog(selectedStreamEntry, 'stream', streamSelection.detached ? 'detached' : 'attached')}
               >
-                查看链路
-              </button>
+                {t('log.manage.stream.action.view-trace')}
+              </HzButton>
               <LogEntityDetailAction
                 href={streamHandoffLinks.entityHref}
                 canOpen={canOpenStreamEntity}
                 missingEntityHandoffTitle={missingEntityHandoffTitle}
+                label={t('log.manage.route.action.entity')}
+                actionScope="stream-detail"
               />
-            </div>
-          </aside>
-        </div>
-      </section>
+            </HzControlStack>
+          </HzDetailAside>
+        </HzWorkbenchLayout>
+      </HzPanelSurface>
     );
   };
 
@@ -1112,13 +1233,18 @@ function LogManageExplorer({
       className="min-h-[calc(100vh-56px)] bg-[#07090b] text-[#e8edf5]"
     >
       <div className="mx-auto flex w-full max-w-[1600px] flex-col gap-4 px-6 py-6">
-        <section data-log-manage-header="cold-compact-header" className={`${panelClass} px-5 py-4`}>
+        <HzPanelSurface
+          data-log-manage-header="cold-compact-header"
+          data-log-manage-panel-surface="header"
+          data-log-manage-header-padding-owner="hertzbeat-ui-panel-surface"
+          padding="header"
+        >
           <div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-start">
             <div className="min-w-0">
-              <p className="mb-2 text-[12px] font-semibold tracking-[0.08em] text-[#8792a5]">可观测</p>
-              <h1 className="text-[30px] font-semibold tracking-normal text-[#f4f7fb]">日志工作台</h1>
+              <p className="mb-2 text-[12px] font-semibold tracking-[0.08em] text-[#8792a5]">{t('log.manage.route.kicker')}</p>
+              <h1 className="text-[30px] font-semibold tracking-normal text-[#f4f7fb]">{t('log.manage.route.title')}</h1>
               <p className="mt-3 max-w-[780px] text-[13px] leading-6 text-[#9ca7ba]">
-                围绕采集来源、实体、链路和告警处理筛选日志,把实时流、历史检索和上下文排查放在同一工作台。
+                {t('log.manage.route.subtitle')}
               </p>
             </div>
             <div
@@ -1146,321 +1272,410 @@ function LogManageExplorer({
                   refreshActionProps={{ 'data-log-manage-time-refresh-action': 'true' }}
                 />
               </div>
-              <div data-log-manage-action-row="cold-workbench-actions" className="flex flex-wrap items-center justify-end gap-2">
-                <Link href={handoffLinks.intakeHref} className={actionClass}>
-                  <Workflow className="h-4 w-4" aria-hidden="true" />
-                  返回 OTLP 接入
-                </Link>
-                <Link href="/setting/collector" className={actionClass}>
-                  <Server className="h-4 w-4" aria-hidden="true" />
-                  采集集群
-                </Link>
+              <HzActionGroup
+                data-log-manage-action-row="cold-workbench-actions"
+                data-log-manage-action-row-owner="hertzbeat-ui-action-group"
+                data-log-manage-action-row-layout-owner="hertzbeat-ui-action-group"
+                layout="full-end"
+              >
+                {routeContext.returnTo ? (
+                  <HzButtonLink
+                    component={Link}
+                    data-log-manage-return-action="true"
+                    data-log-manage-header-action="return-source"
+                    href={routeContext.returnTo}
+                    size="md"
+                  >
+                    <HzButtonIcon
+                      icon={Workflow}
+                      data-log-manage-header-action-icon="return-source"
+                      data-log-manage-header-action-icon-owner="hertzbeat-ui-button-icon"
+                    />
+                    {t('log.manage.route.action.return-source')}
+                  </HzButtonLink>
+                ) : null}
+                <HzButtonLink component={Link} href={handoffLinks.intakeHref} size="md" data-log-manage-header-action="intake">
+                  <HzButtonIcon
+                    icon={Workflow}
+                    data-log-manage-header-action-icon="intake"
+                    data-log-manage-header-action-icon-owner="hertzbeat-ui-button-icon"
+                  />
+                  {t('log.manage.route.action.intake')}
+                </HzButtonLink>
+                <HzButtonLink component={Link} href="/setting/collector" size="md" data-log-manage-header-action="collector">
+                  <HzButtonIcon
+                    icon={Server}
+                    data-log-manage-header-action-icon="collector"
+                    data-log-manage-header-action-icon-owner="hertzbeat-ui-button-icon"
+                  />
+                  {t('log.manage.route.action.collector')}
+                </HzButtonLink>
                 <LogEntityDetailAction
                   href={handoffLinks.entityHref}
                   canOpen={canOpenActiveEntity}
                   missingEntityHandoffTitle={missingEntityHandoffTitle}
+                  label={t('log.manage.route.action.entity')}
+                  actionScope="header"
                   showIcon
                 />
-                <Link href={handoffLinks.alertHandlingHref} className={actionClass}>
-                  <BellRing className="h-4 w-4" aria-hidden="true" />
-                  告警处理
-                </Link>
-                <Link href="/setting/define" className={actionClass}>
-                  <ListChecks className="h-4 w-4" aria-hidden="true" />
-                  监控模板
-                </Link>
-              </div>
+                <HzButtonLink component={Link} href={handoffLinks.alertHandlingHref} size="md" data-log-manage-header-action="alerts">
+                  <HzButtonIcon
+                    icon={BellRing}
+                    data-log-manage-header-action-icon="alerts"
+                    data-log-manage-header-action-icon-owner="hertzbeat-ui-button-icon"
+                  />
+                  {t('log.manage.route.action.alerts')}
+                </HzButtonLink>
+                <HzButtonLink component={Link} href="/setting/define" size="md" data-log-manage-header-action="templates">
+                  <HzButtonIcon
+                    icon={ListChecks}
+                    data-log-manage-header-action-icon="templates"
+                    data-log-manage-header-action-icon-owner="hertzbeat-ui-button-icon"
+                  />
+                  {t('log.manage.route.action.templates')}
+                </HzButtonLink>
+              </HzActionGroup>
             </div>
           </div>
-        </section>
+        </HzPanelSurface>
 
         {renderViewSwitch()}
 
-        <section data-log-manage-query-bar="cold-query-row" className={`${panelClass} px-4 py-3`}>
+        <HzPanelSurface
+          data-log-manage-query-bar="cold-query-row"
+          data-log-manage-panel-surface="query"
+          data-log-manage-panel-surface-padding-owner="hertzbeat-ui-panel-surface"
+          padding="query"
+        >
           <div className="flex flex-wrap items-center gap-2">
-            <div className="relative min-w-[320px] max-w-[560px] flex-1">
-              <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#7d8798]" aria-hidden="true" />
-              <Input
-                aria-label="日志查询"
+            <HzSearchFieldFrame
+              width="log-query"
+              data-log-manage-query-search-frame="shared-search-field-frame"
+              data-log-manage-query-search-frame-owner="hertzbeat-ui-search-field-frame"
+              icon={(
+                <HzSearchFieldIcon
+                  icon={Search}
+                  data-log-manage-query-search-icon="service"
+                  data-log-manage-query-search-icon-owner="hertzbeat-ui-search-field-icon"
+                />
+              )}
+            >
+              <HzInput
+                aria-label={t('log.manage.query.search.aria')}
                 value={draft.search}
                 onChange={event => setDraft(updateDraftField('search', event.target.value))}
-                placeholder='service.name = "checkout"'
-                className={`${inputClass} w-full pl-9 font-mono`}
+                placeholder={t('log.manage.query.search.placeholder')}
+                inset="search-icon"
+                width="log-query-expression"
+                data-log-manage-query-search-input="true"
+                data-log-manage-query-search-input-owner="hertzbeat-ui-input"
               />
-            </div>
-            <Select aria-label="严重级别" value={draft.severityText || 'all'} onChange={event => applySeverity(event.target.value === 'all' ? '' : event.target.value)} containerClassName="w-[132px]" className="h-8 min-w-0 text-[#d5dce8]">
-              <option value="all">全部级别</option>
-              {quickSeverityFilters.map(severity => (
-                <option key={severity} value={severity}>
-                  {severity}
-                </option>
-              ))}
-            </Select>
-            <button data-log-manage-run-query-action="true" type="button" className={primaryActionClass} onClick={() => applyQuery()}>
+            </HzSearchFieldFrame>
+            <HzSelect
+              aria-label={t('log.manage.query.severity.aria')}
+              value={draft.severityText || 'all'}
+              onChange={event => applySeverity(event.target.value === 'all' ? '' : event.target.value)}
+              width="log-severity"
+              triggerTone="signal-query"
+              data-log-manage-query-severity-select="shared-log-severity-select"
+              data-log-manage-query-severity-select-owner="hertzbeat-ui-select"
+              options={[
+                { value: 'all', label: t('log.manage.query.severity.all') },
+                ...quickSeverityFilters.map(severity => ({ value: severity, label: severity }))
+              ]}
+            />
+            <HzButton data-log-manage-run-query-action="true" intent="primary" size="md" onClick={() => applyQuery()}>
               <Play className="h-4 w-4" aria-hidden="true" />
-              {currentView === 'stream' ? '应用到日志流' : '运行查询'}
-            </button>
-            <button type="button" className={actionClass} onClick={resetQuery}>
+              {currentView === 'stream' ? t('log.manage.query.run.stream') : t('log.manage.query.run.history')}
+            </HzButton>
+            <HzButton data-log-manage-reset-action="true" intent="secondary" size="md" onClick={resetQuery}>
               <RotateCcw className="h-4 w-4" aria-hidden="true" />
-              重置
-            </button>
+              {t('log.manage.query.reset')}
+            </HzButton>
           </div>
           <div className="mt-2 flex flex-wrap items-center gap-2">
-            <Input
-              aria-label="链路 ID"
+            <HzInput
+              aria-label={t('log.manage.query.trace-id')}
               value={draft.traceId}
               onChange={event => setDraft(updateDraftField('traceId', event.target.value))}
-              placeholder="链路 ID"
-              className={`${inputClass} w-[220px] font-mono`}
+              placeholder={t('log.manage.query.trace-id')}
+              width="log-query-token"
+              data-log-manage-query-token-input="trace-id"
+              data-log-manage-query-token-input-owner="hertzbeat-ui-input"
             />
-            <Input
-              aria-label="跨度 ID"
+            <HzInput
+              aria-label={t('log.manage.query.span-id')}
               value={draft.spanId}
               onChange={event => setDraft(updateDraftField('spanId', event.target.value))}
-              placeholder="跨度 ID"
-              className={`${inputClass} w-[220px] font-mono`}
+              placeholder={t('log.manage.query.span-id')}
+              width="log-query-token"
+              data-log-manage-query-token-input="span-id"
+              data-log-manage-query-token-input-owner="hertzbeat-ui-input"
             />
-            <Input
-              aria-label="日志正文"
+            <HzInput
+              aria-label={t('log.manage.query.body.aria')}
               value={draft.logContent}
               onChange={event => setDraft(updateDraftField('logContent', event.target.value))}
-              placeholder="搜索日志正文"
-              className={`${inputClass} min-w-[280px] max-w-[520px] flex-1`}
+              placeholder={t('log.manage.query.body.placeholder')}
+              width="log-query-body"
+              data-log-manage-query-body-input="shared-log-body-input"
+              data-log-manage-query-body-input-owner="hertzbeat-ui-input"
             />
           </div>
-        </section>
+        </HzPanelSurface>
+
+        {data.loadStatus?.state === 'degraded' ? (
+          <HzStateNotice
+            data-log-manage-api-degraded="true"
+            data-log-manage-api-degraded-owner="hertzbeat-ui-state-notice"
+            tone="warning"
+            title={t('log.manage.api.degraded.title')}
+            description={t('log.manage.api.degraded.copy')}
+            meta={data.loadStatus.message}
+          />
+        ) : null}
 
         {currentView === 'stream' ? renderStreamStage() : null}
 
         {currentView === 'list' ? (
-        <section data-log-manage-chart-band="cold-chart-band" className={`${panelClass} px-4 py-4`}>
+        <HzPanelSurface
+          data-log-manage-chart-band="cold-chart-band"
+          data-log-manage-panel-surface="chart"
+          data-log-manage-chart-padding-owner="hertzbeat-ui-panel-surface"
+          padding="chart"
+        >
           <div className="grid gap-3 lg:grid-cols-[repeat(4,minmax(0,160px))_minmax(0,1fr)]">
             {[
-              ['日志总数', data.overview.totalLogs],
-              ['错误日志', data.overview.errorLogs],
-              ['链路关联', traceCoverage],
-              ['最近时间', latestObservedAt]
-            ].map(([label, value]) => (
-              <div key={String(label)} className="rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3">
-                <p className="text-[12px] font-semibold text-[#8792a5]">{label}</p>
-                <p className="mt-2 truncate text-[18px] font-semibold text-[#f2f6fb]">{value}</p>
-              </div>
+              { id: 'total', label: t('log.manage.summary.total'), value: data.overview.totalLogs },
+              { id: 'errors', label: t('log.manage.summary.errors'), value: data.overview.errorLogs },
+              { id: 'trace-coverage', label: t('log.manage.summary.trace-coverage'), value: traceCoverage },
+              { id: 'latest', label: t('log.manage.summary.latest'), value: latestObservedAt }
+            ].map(item => (
+              <HzStatCell
+                key={item.id}
+                data-log-manage-summary-stat-owner="hertzbeat-ui-stat-cell"
+                data-log-manage-summary-stat={item.id}
+                label={item.label}
+                value={item.value}
+                variant="tile"
+              />
             ))}
-            <div className="min-w-0 rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3">
-              <div className="mb-3 flex items-center justify-between text-[12px] text-[#8792a5]">
-                <span className="font-semibold">趋势带</span>
-                <span className="inline-flex items-center gap-1">
+            <HzSignalTrendBars
+              data-log-manage-signal-trend-owner="hertzbeat-ui-signal-trend-bars"
+              title={t('log.manage.trend.title')}
+              meta={(
+                <>
                   <BarChart3 className="h-4 w-4" aria-hidden="true" />
-                  {trendBars.length || 0} 点
-                </span>
-              </div>
-              <div className="flex h-16 items-end gap-2">
-                {(trendBars.length ? trendBars : [['-', 0]]).map(([hour, count]) => (
-                  <div key={hour} className="min-w-0 flex-1">
-                    <div
-                      className="rounded-[3px] border border-[#2f3b4d] bg-[#182232]"
-                      style={{ height: `${Math.max(10, (Number(count) / maxTrend) * 64)}px` }}
-                      title={`${hour}: ${count}`}
-                    />
-                  </div>
-                ))}
-              </div>
-            </div>
+                  {t('log.manage.trend.points', { count: trendBars.length || 0 })}
+                </>
+              )}
+              bars={trendBars.map(([hour, count]) => ({
+                id: hour,
+                label: hour,
+                value: Number(count) || 0,
+                tone: 'info' as const,
+                title: `${hour}: ${count}`
+              }))}
+            />
           </div>
-        </section>
+        </HzPanelSurface>
         ) : null}
 
         {currentView === 'list' ? (
         <div className="grid gap-4 2xl:grid-cols-[minmax(0,1fr)_320px]">
-          <section data-log-manage-log-list="cold-dense-log-list" className={`${panelClass} min-w-0 overflow-hidden`}>
-            <div className="flex h-11 items-center justify-between border-b border-[#252b35] px-4 text-[12px] text-[#8e99ab]">
-              <span className="inline-flex items-center gap-2">
-                <ScrollText className="h-4 w-4" aria-hidden="true" />
-                最近日志
-              </span>
-              <span>{rows.length} 条</span>
-            </div>
-            {rows.length ? (
-              <table className="w-full border-collapse text-left text-[13px]">
-                <thead className="border-b border-[#252b35] bg-[#10141b] text-[12px] font-semibold text-[#8f9aab]">
-                  <tr>
-                    <th className="w-[176px] px-4 py-3">时间</th>
-                    <th className="w-[116px] px-4 py-3">严重级别</th>
-                    <th className="w-[180px] px-4 py-3">服务</th>
-                    <th className="px-4 py-3">日志正文</th>
-                    <th className="w-[220px] px-4 py-3">链路</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  {rows.map((row, index) => {
-                    const entry = data.list.content?.[index] ?? null;
-                    return (
-                      <tr
-                        key={row.key}
-                        data-log-manage-row-detail-action="true"
-                        role="button"
-                        tabIndex={0}
-                        onClick={() => openLogDetails(entry, 'history')}
-                        onKeyDown={event => {
-                          if (event.key === 'Enter' || event.key === ' ') {
-                            event.preventDefault();
-                            openLogDetails(entry, 'history');
-                          }
-                        }}
-                        className="cursor-pointer border-b border-[#232a34] last:border-b-0 hover:bg-[#10141b]"
-                      >
-                        <td className="px-4 py-3 font-mono text-[12px] text-[#cbd5e1]">{row.timestamp}</td>
-                        <td className="px-4 py-3">
-                          <span className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold ${severityTone(row.severity)}`}>
-                            {row.severity}
-                          </span>
-                        </td>
-                        <td className="px-4 py-3 font-semibold text-[#e6edf7]">{row.service}</td>
-                        <td className="min-w-0 px-4 py-3">
-                          <span className="block truncate font-mono text-[12px] text-[#e8edf5]">{row.message}</span>
-                          <span className="mt-1 inline-flex items-center gap-1 text-[11px] font-semibold text-[#7f8a9d]">
-                            <Eye className="h-3.5 w-3.5" aria-hidden="true" />
-                            查看日志
-                          </span>
-                        </td>
-                        <td className="px-4 py-3 font-mono text-[11px] text-[#9aa6b8]">
-                          <button
-                            data-log-manage-row-trace-detail-action="true"
-                            type="button"
-                            disabled={row.traceId === '-'}
-                            title={row.traceId === '-' ? missingTraceHandoffTitle : undefined}
-                            aria-label={row.traceId === '-' ? missingTraceHandoffTitle : undefined}
-                            data-log-manage-row-trace-detail-action-disabled={row.traceId === '-' ? 'missing-trace-id' : undefined}
-                            onClick={event => {
-                              event.stopPropagation();
-                              openLogDetails(entry, 'history');
-                            }}
-                            className="text-left hover:text-[#d7e2ff] disabled:cursor-not-allowed disabled:text-[#5d6674]"
-                          >
-                            {row.traceId}
-                          </button>
-                        </td>
-                      </tr>
-                    );
-                  })}
-                </tbody>
-              </table>
-            ) : (
-              <div className="flex h-[360px] items-center justify-center text-center text-[13px] text-[#d5d8e1]">
-                <div>
-                  <div className="mx-auto grid h-11 w-11 place-items-center rounded-[4px] border border-[#2b3039] bg-[#101217]">
-                    <ScrollText className="h-5 w-5 text-[#9aa6b8]" aria-hidden="true" />
-                  </div>
-                  <p className="mt-3 font-semibold">暂无日志</p>
-                  <p data-log-manage-empty-guidance="operator-no-data-guidance" className="mt-2 text-[#8f9bad]">
-                    确认时间范围、实体归因、采集器和监控模板后再查看日志。
-                  </p>
-                </div>
-              </div>
-            )}
-          </section>
-
-          <aside data-log-manage-detail-panel="cold-detail-panel" className={`${panelClass} h-fit px-4 py-4`}>
-            <p className="text-[12px] font-semibold text-[#8d98aa]">详情面板</p>
-            <h2 className="mt-2 truncate text-[18px] font-semibold text-[#f0f4fa]">{activeRow?.service || '未选择日志'}</h2>
-            <div className="mt-4 space-y-3 text-[12px] text-[#9aa6b8]">
-              <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
-                <span>严重级别</span>
-                <span className="font-semibold text-[#dbe5f3]">{activeRow?.severity || '-'}</span>
-              </div>
-              <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
-                <span>链路 ID</span>
-                <span className="max-w-[190px] truncate font-mono font-semibold text-[#dbe5f3]">{activeRow?.traceId || '-'}</span>
-              </div>
-              <div className="flex items-center justify-between border-b border-dashed border-[#2c3441] pb-2">
-                <span>跨度 ID</span>
-                <span className="max-w-[190px] truncate font-mono font-semibold text-[#dbe5f3]">{activeRow?.spanId || '-'}</span>
-              </div>
-            </div>
-            <div
-              data-log-manage-selected-evidence="selected-log-evidence"
-              aria-label="日志证据 日志时间 日志级别 正文摘要 最近上报"
-              className="mt-4 rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3"
-            >
-              <div className="mb-2 flex items-center justify-between gap-2">
-                <p className="text-[11px] font-semibold text-[#8792a5]">日志证据</p>
-                <span className="text-[11px] font-semibold text-[#6d7788]">当前选中</span>
-              </div>
-              <div className="space-y-2">
-                {activeLogEvidenceRows.map(([label, value, meta]) => (
-                  <div key={label} className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]">
-                    <span className="text-[#7f8a9d]">{label}</span>
-                    <span className="min-w-0">
-                      <span className="block truncate font-semibold text-[#e6edf7]">{value}</span>
-                      <span className="block truncate text-[#6d7788]">{meta}</span>
-                    </span>
-                  </div>
-                ))}
-              </div>
-            </div>
-            <div
-              data-log-manage-entity-context="hertzbeat-signal-entity-context"
-              aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"
-              className="mt-4 rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3"
-            >
-              <div className="mb-2 flex items-center gap-2">
-                <p className="text-[11px] font-semibold text-[#8792a5]">实体上下文</p>
-              </div>
-              <div className="space-y-2">
-                {entityContextRows.map(row => (
-                  <div key={row.label} className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]">
-                    <span className="text-[#7f8a9d]">{row.label}</span>
+          <HzPanelSurface
+            data-log-manage-log-list="cold-dense-log-list"
+            data-log-manage-panel-surface="table"
+            data-log-manage-row-control-owner="shared-hz-button"
+            className="overflow-hidden"
+          >
+            <HzPanelHeader
+              data-log-manage-table-header-owner="hertzbeat-ui-panel-header"
+              title={(
+                <>
+                  <ScrollText className="h-4 w-4" aria-hidden="true" />
+                  {t('log.manage.list.title')}
+                </>
+              )}
+              meta={<HzStatusBadge data-log-manage-table-count-badge-owner="hertzbeat-ui-status-badge" tone="neutral" size="xs">{t('log.manage.list.count', { count: rows.length })}</HzStatusBadge>}
+            />
+            <HzDataTable
+              data-log-manage-table-chrome-owner="hertzbeat-ui-data-table"
+              variant="embedded"
+              rows={rows}
+              getRowKey={row => row.key}
+              selectedRowKey={activeRow?.key}
+              onRowClick={row => openLogDetails(logEntryByRowKey.get(row.key) ?? null, 'history')}
+              getRowProps={() => ({ 'data-log-manage-row-detail-action': 'true' })}
+              emptyLabel={(
+              <HzEmptyState
+                data-log-manage-empty-guidance="operator-no-data-guidance"
+                data-log-manage-empty-state-owner="hertzbeat-ui-empty-state"
+                title={t('log.manage.empty.title')}
+                description={t('log.manage.empty.copy')}
+                className="h-[360px] border-y-0 text-left"
+              />
+              )}
+              columns={[
+                {
+                  key: 'time',
+                  header: t('log.manage.list.column.time'),
+                  width: '176px',
+                  render: row => <span className="font-mono text-[12px] text-[#cbd5e1]">{row.timestamp}</span>
+                },
+                {
+                  key: 'severity',
+                  header: t('log.manage.list.column.severity'),
+                  width: '116px',
+                  render: row => (
+                    <HzStatusBadge
+                      data-log-manage-severity-tone={row.severityTone}
+                      data-log-manage-severity-badge-owner="hertzbeat-ui-status-badge"
+                      tone={logSeverityStatusTone(row.severityTone)}
+                    >
+                      {row.severity}
+                    </HzStatusBadge>
+                  )
+                },
+                {
+                  key: 'service',
+                  header: t('log.manage.list.column.service'),
+                  width: '180px',
+                  render: row => <span className="font-semibold text-[#e6edf7]">{row.service}</span>
+                },
+                {
+                  key: 'body',
+                  header: t('log.manage.list.column.body'),
+                  render: row => (
                     <span className="min-w-0">
-                      <span className="block truncate font-semibold text-[#e6edf7]">{row.value}</span>
-                      <span className="block truncate text-[#6d7788]">{row.meta}</span>
+                      <span className="block truncate font-mono text-[12px] text-[#e8edf5]">{row.message}</span>
+                      <span className="mt-1 inline-flex items-center gap-1 text-[11px] font-semibold text-[#7f8a9d]">
+                        <Eye className="h-3.5 w-3.5" aria-hidden="true" />
+                        {t('log.manage.stream.action.view-log')}
+                      </span>
                     </span>
-                  </div>
-                ))}
-              </div>
-            </div>
-            <LogAttributionDiagnosticsPanel rows={activeAttributionDiagnostics} />
-            <div className="mt-4 flex flex-col gap-2">
-              <p
-                data-log-manage-alert-context-hint="entity-trace-alert-handoff"
-                className="rounded-[3px] border border-[#252b35] bg-[#10141b] px-2.5 py-2 text-[11px] leading-4 text-[#8792a5]"
-              >
-                按当前实体、服务和已带入的链路上下文查看相关告警
-              </p>
-              <p
-                data-log-manage-signal-handoff-hint="log-trace-metric-context"
-                className="rounded-[3px] border border-[#252b35] bg-[#10141b] px-2.5 py-2 text-[11px] leading-4 text-[#8792a5]"
-              >
-                当前日志的链路、跨度和服务上下文会带入链路与指标工作台
-              </p>
-              <LogEntityDetailAction
-                href={handoffLinks.entityHref}
-                canOpen={canOpenActiveEntity}
-                missingEntityHandoffTitle={missingEntityHandoffTitle}
+                  )
+                },
+                {
+                  key: 'trace',
+                  header: t('log.manage.list.column.trace'),
+                  width: '220px',
+                  render: row => (
+                    <HzButton
+                      data-log-manage-row-trace-detail-action="true"
+                      data-log-manage-row-control="shared-hz-button"
+                      intent="ghost"
+                      size="xs"
+                      disabled={row.traceId === '-'}
+                      title={row.traceId === '-' ? missingTraceHandoffTitle : undefined}
+                      aria-label={row.traceId === '-' ? missingTraceHandoffTitle : undefined}
+                      data-log-manage-row-trace-detail-action-disabled={row.traceId === '-' ? 'missing-trace-id' : undefined}
+                      onClick={() => openLogDetails(logEntryByRowKey.get(row.key) ?? null, 'history')}
+                      className="max-w-[180px] justify-start truncate font-mono"
+                    >
+                      {row.traceId}
+                    </HzButton>
+                  )
+                }
+              ]}
+            />
+          </HzPanelSurface>
+
+          <HzPanelSurface data-log-manage-detail-panel="cold-detail-panel" data-log-manage-panel-surface="detail" className="h-fit overflow-hidden">
+            <HzPanelHeader
+              data-log-manage-detail-header-owner="hertzbeat-ui-panel-header"
+              eyebrow={t('log.manage.detail.title')}
+              title={(
+                <>
+                  <Server className="h-4 w-4" aria-hidden="true" />
+                  {activeRow?.service || t('log.manage.stream.selected.none')}
+                </>
+              )}
+            />
+            <div className="px-4 py-4">
+              <HzDetailRows
+                data-log-manage-detail-facts-owner="hertzbeat-ui-detail-rows"
+                rows={[
+                  { key: 'severity', title: t('log.manage.detail.severity'), copy: activeRow?.severity || '-' },
+                  { key: 'trace-id', title: t('log.manage.detail.trace-id'), copy: activeRow?.traceId || '-' },
+                  { key: 'span-id', title: t('log.manage.detail.span-id'), copy: activeRow?.spanId || '-' }
+                ]}
               />
-              <Link href={handoffLinks.alertHandlingHref} className={actionClass}>
-                告警处理
-              </Link>
-              <button
-                data-log-manage-results-open-trace-action="true"
-                data-log-manage-open-log-detail-before-trace="true"
-                type="button"
-                className={activeEntry?.traceId ? actionClass : disabledActionClass}
-                disabled={!activeEntry?.traceId}
-                title={!activeEntry?.traceId ? missingTraceHandoffTitle : undefined}
-                aria-label={!activeEntry?.traceId ? missingTraceHandoffTitle : undefined}
-                data-log-manage-results-open-trace-action-disabled={!activeEntry?.traceId ? 'missing-trace-id' : undefined}
-                onClick={() => openTraceDrilldownFromLog(activeEntry, 'history')}
+              <HzDetailRows
+                data-log-manage-selected-evidence="selected-log-evidence"
+                data-log-manage-selected-evidence-owner="hertzbeat-ui-detail-rows"
+                aria-label={t('log.manage.evidence.aria')}
+                className="mt-4"
+                heading={t('log.manage.evidence.title')}
+                rows={activeLogEvidenceRows.map(([label, value, meta]) => ({
+                  key: String(label),
+                  title: label,
+                  copy: value,
+                  meta
+                }))}
+              />
+              <HzDetailRows
+                data-log-manage-entity-context="hertzbeat-signal-entity-context"
+                data-log-manage-entity-context-owner="hertzbeat-ui-detail-rows"
+                aria-label={t('log.manage.context.entity.aria')}
+                className="mt-4"
+                heading={t('log.manage.context.entity.title')}
+                rows={entityContextRows.map(row => ({
+                  key: row.label,
+                  title: row.label,
+                  copy: row.value,
+                  meta: row.meta
+                }))}
+              />
+              <LogAttributionDiagnosticsPanel rows={activeAttributionDiagnostics} t={t} />
+              <HzControlStack
+                data-log-manage-history-detail-action-stack="shared-control-stack"
+                data-log-manage-history-detail-action-stack-owner="hertzbeat-ui-control-stack"
+                className="mt-4"
               >
-                查看链路
-              </button>
-              <Link href={handoffLinks.metricsHref} className={actionClass}>
-                查看指标
-              </Link>
-              <Link href={handoffLinks.entitiesHref} className={actionClass}>
-                对象目录
-              </Link>
+                <HzStateNotice
+                  data-log-manage-alert-context-hint="entity-trace-alert-handoff"
+                  data-log-manage-handoff-hint-owner="hertzbeat-ui-state-notice"
+                  title={t('log.manage.handoff.alert-hint')}
+                  variant="hint"
+                />
+                <HzStateNotice
+                  data-log-manage-signal-handoff-hint="log-trace-metric-context"
+                  data-log-manage-handoff-hint-owner="hertzbeat-ui-state-notice"
+                  title={t('log.manage.handoff.signal-hint')}
+                  variant="hint"
+                />
+                <LogEntityDetailAction
+                  href={handoffLinks.entityHref}
+                  canOpen={canOpenActiveEntity}
+                  missingEntityHandoffTitle={missingEntityHandoffTitle}
+                  label={t('log.manage.route.action.entity')}
+                />
+                <HzButtonLink component={Link} data-log-manage-history-detail-action="alerts" href={handoffLinks.alertHandlingHref} size="md">
+                  {t('log.manage.handoff.alerts')}
+                </HzButtonLink>
+                <HzButton
+                  data-log-manage-history-detail-action="traces"
+                  data-log-manage-results-open-trace-action="true"
+                  data-log-manage-open-log-detail-before-trace="true"
+                  size="md"
+                  disabled={!activeEntry?.traceId}
+                  title={!activeEntry?.traceId ? missingTraceHandoffTitle : undefined}
+                  aria-label={!activeEntry?.traceId ? missingTraceHandoffTitle : undefined}
+                  data-log-manage-results-open-trace-action-disabled={!activeEntry?.traceId ? 'missing-trace-id' : undefined}
+                  onClick={() => openTraceDrilldownFromLog(activeEntry, 'history')}
+                >
+                  {t('log.manage.handoff.traces')}
+                </HzButton>
+                <HzButtonLink component={Link} data-log-manage-history-detail-action="metrics" href={handoffLinks.metricsHref} size="md">
+                  {t('log.manage.handoff.metrics')}
+                </HzButtonLink>
+                <HzButtonLink component={Link} data-log-manage-history-detail-action="entities" href={handoffLinks.entitiesHref} size="md">
+                  {t('log.manage.handoff.entities')}
+                </HzButtonLink>
+              </HzControlStack>
             </div>
-          </aside>
+          </HzPanelSurface>
         </div>
         ) : null}
         {renderDetailDialog()}
@@ -1471,16 +1686,17 @@ function LogManageExplorer({
 }
 
 export default function LogManagePage({
+  initialRouteState,
   forcedView,
   showViewToggle = true
-}: LogManagePageProps = {}) {
-  const searchParams = useSearchParams();
+}: LogManagePageProps & { initialRouteState?: LogManageRouteState } = {}) {
+  const logManageRouteState = initialRouteState ?? EMPTY_LOG_MANAGE_ROUTE_STATE;
   const router = useRouter();
-  const [draft, setDraft] = useState<LogQueryState>(() => queryStateFromParams(searchParams));
-  const [query, setQuery] = useState<LogQueryState>(() => queryStateFromParams(searchParams));
+  const [draft, setDraft] = useState<LogQueryState>(() => logManageRouteState.initialQuery);
+  const [query, setQuery] = useState<LogQueryState>(() => logManageRouteState.initialQuery);
   const { t } = useI18n();
-  const currentView = forcedView ?? resolveLogWorkbenchView(searchParams);
-  const routeContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
+  const currentView = forcedView ?? logManageRouteState.currentView;
+  const routeContext = logManageRouteState.routeContext;
   const logTimeContext = useMemo(() => sanitizeTimeContext({
     timeRange: routeContext.timeRange || 'last-30m',
     start: routeContext.start,
@@ -1489,41 +1705,62 @@ export default function LogManagePage({
     live: routeContext.live || 'true',
     tz: routeContext.tz
   }), [routeContext]);
+  const logUrls = useMemo(() => buildLogUrls(query, routeContext), [query, routeContext]);
+  const logManageCacheKey = useMemo(
+    () => [
+      'log-manage',
+      currentView,
+      logUrls.overviewUrl,
+      logUrls.listUrl,
+      logUrls.trendUrl,
+      logUrls.coverageUrl
+    ].join('|'),
+    [currentView, logUrls]
+  );
 
   const buildRouteWithContext = useCallback(
-    (nextQuery: LogQueryState, view: LogWorkbenchView) => buildLogManageRoute(searchParams, nextQuery, view),
-    [searchParams]
+    (nextQuery: LogQueryState, view: LogWorkbenchView) => buildLogManageRoute(routeContext, nextQuery, view),
+    [routeContext]
   );
 
   useEffect(() => {
-    if (hasDisplayReturnLabel(searchParams)) {
-      router.replace(buildLogManageRoute(searchParams, query, currentView));
+    if (logManageRouteState.shouldCleanUrl) {
+      router.replace(buildLogManageRoute(routeContext, query, currentView));
     }
-  }, [currentView, query, router, searchParams]);
+  }, [currentView, logManageRouteState.shouldCleanUrl, query, routeContext, router]);
 
   const applyLogTimeContext = useCallback((timeContext: TimeContext) => {
     const appliedContext = resolveAppliedTimeContext(timeContext, logTimeContext);
-    router.replace(buildLogManageRoute(searchParams, query, currentView, appliedContext));
-  }, [currentView, logTimeContext, query, router, searchParams]);
+    router.replace(buildLogManageRoute(routeContext, query, currentView, appliedContext));
+  }, [currentView, logTimeContext, query, routeContext, router]);
 
   const load = useCallback(async (): Promise<LogManageData> => {
-    const { listUrl, overviewUrl, trendUrl, coverageUrl } = buildLogUrls(query, routeContext);
-    const [overview, list, trend, coverage] = await Promise.all([
-      apiMessageGet<BackendLogOverview>(overviewUrl),
-      apiMessageGet<PageResult<LogEntry>>(listUrl),
-      apiMessageGet<LogTrendStats>(trendUrl),
-      apiMessageGet<LogTraceCoverage>(coverageUrl)
-    ]);
-    return { overview: normalizeLogOverview(overview), list, trend, coverage, query };
-  }, [query, routeContext]);
+    const { listUrl, overviewUrl, trendUrl, coverageUrl } = logUrls;
+    try {
+      const [overview, list, trend, coverage] = await Promise.all([
+        apiMessageGetWithTimeout<BackendLogOverview>(overviewUrl),
+        apiMessageGetWithTimeout<PageResult<LogEntry>>(listUrl),
+        apiMessageGetWithTimeout<LogTrendStats>(trendUrl),
+        apiMessageGetWithTimeout<LogTraceCoverage>(coverageUrl)
+      ]);
+      return { overview: normalizeLogOverview(overview), list, trend, coverage, query };
+    } catch (error) {
+      return emptyLogManageData(query, describeLogManageLoadFailure(error));
+    }
+  }, [logUrls, query]);
 
   return (
-    <ClientWorkbench load={load} loadingCopy={t('log.manage.loading')}>
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('log.manage.loading')}
+      cacheKey={logManageCacheKey}
+      cacheSettledTtlMs={LOG_MANAGE_SETTLED_CACHE_TTL_MS}
+    >
       {data => {
         const resetQuery = () => {
           setDraft(EMPTY_QUERY);
           setQuery(EMPTY_QUERY);
-          router.replace(buildResetLogManageRoute(searchParams, currentView));
+          router.replace(buildResetLogManageRoute(routeContext, currentView));
         };
 
         const applyQuery = (override?: LogQueryState) => {
@@ -1550,7 +1787,7 @@ export default function LogManagePage({
             routeContext={routeContext}
             timeContext={logTimeContext}
             applyTimeContext={applyLogTimeContext}
-            currentLogReturnHref={buildLogManageRoute(searchParams, query, currentView)}
+            currentLogReturnHref={buildLogManageRoute(routeContext, query, currentView)}
           />
         );
       }}
diff --git a/web-next/app/log/manage/page.test.tsx b/web-next/app/log/manage/page.test.tsx
index d44ce3c943..4436d13bb6 100644
--- a/web-next/app/log/manage/page.test.tsx
+++ b/web-next/app/log/manage/page.test.tsx
@@ -7,10 +7,12 @@ import { createRoot, type Root } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import LogManagePage from './log-manage-page';
+import type { LogManageRouteState, LogQueryState, LogWorkbenchView } from '@/lib/log-manage/query-state';
 
 (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
 
-const apiMessageGet = vi.fn();
+const apiMessageGet = vi.hoisted(() => vi.fn());
 
 const mockState = vi.hoisted(() => ({
   searchParams: new URLSearchParams(),
@@ -80,14 +82,35 @@ vi.mock('next/navigation', () => ({
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock()
+    t: createTranslatorMock({ locale: 'zh-CN' })
   })
 }));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
-  ClientWorkbench: ({ children, load }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown> }) => {
+  ClientWorkbench: ({
+    children,
+    load,
+    cacheKey,
+    cacheSettledTtlMs,
+    loadingCopy
+  }: {
+    children: (data: any) => React.ReactNode;
+    load: () => Promise<unknown>;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
+    loadingCopy?: string;
+  }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return (
+      <div
+        data-client-workbench="true"
+        data-cache-key={cacheKey}
+        data-cache-settled-ttl={cacheSettledTtlMs}
+        data-loading-copy={loadingCopy}
+      >
+        {children(mockState.renderData)}
+      </div>
+    );
   }
 }));
 
@@ -113,6 +136,13 @@ vi.mock('@/lib/format', () => ({
 }));
 
 vi.mock('@/lib/log-manage/display-mapping', () => ({
+  logSeverityTone: (severity?: string | number | null) => {
+    const normalized = String(severity ?? '').toUpperCase();
+    if (normalized.includes('ERROR') || normalized.includes('FATAL')) return 'danger';
+    if (normalized.includes('WARN')) return 'warning';
+    if (normalized.includes('INFO')) return 'success';
+    return 'neutral';
+  },
   severityLabel: (entry: any) => entry.severityText || 'LOG'
 }));
 
@@ -251,6 +281,7 @@ vi.mock('@/lib/log-manage/view-model', () => ({
       message: String(entry.body),
       service: entry.resource?.['service.name'] || '-',
       severity: entry.severityText || 'LOG',
+      severityTone: entry.severityText === 'ERROR' ? 'danger' : entry.severityText === 'WARN' ? 'warning' : 'neutral',
       traceId: entry.traceId || '-',
       spanId: entry.spanId || '-'
     })),
@@ -278,19 +309,69 @@ vi.mock('@/lib/log-manage/view-model', () => ({
 }));
 
 vi.mock('./route-state', () => ({
-  buildLogManageRoute: (_searchParams: any, query: any) => {
+  buildLogManageRoute: (routeContext: any, query: any, view?: string) => {
     const params = new URLSearchParams();
     if (query.search) params.set('search', query.search);
     if (query.traceId) params.set('traceId', query.traceId);
     if (query.spanId) params.set('spanId', query.spanId);
-    const view = _searchParams.get('view');
     if (view) params.set('view', view);
-    params.set('source', _searchParams.get('source') || 'otlp');
+    params.set('source', routeContext.source || 'otlp');
     return params.toString() ? `/log/manage?${params.toString()}` : '/log/manage';
   },
   buildResetLogManageRoute: () => '/log/manage'
 }));
 
+function buildLogManageRouteState(): LogManageRouteState {
+  const query: LogQueryState = {
+    search: mockState.searchParams.get('search') || mockState.searchParams.get('content') || '',
+    logContent: mockState.searchParams.get('logContent') || '',
+    traceId: mockState.searchParams.get('traceId') || '',
+    spanId: mockState.searchParams.get('spanId') || '',
+    severityNumber: mockState.searchParams.get('severityNumber') || '',
+    severityText: mockState.searchParams.get('severityText') || ''
+  };
+  const requestedView = mockState.searchParams.get('view');
+  const currentView: LogWorkbenchView =
+    requestedView === 'list' || requestedView === 'history'
+      ? 'list'
+      : requestedView === 'stream'
+        ? 'stream'
+        : query.search || query.traceId || query.spanId || query.severityNumber || query.severityText
+          ? 'list'
+          : 'stream';
+
+  return {
+    initialQuery: query,
+    currentView,
+    routeContext: {
+      entityId: mockState.searchParams.get('entityId') || undefined,
+      entityName: mockState.searchParams.get('entityName') || undefined,
+      returnTo: mockState.searchParams.get('returnTo') || undefined,
+      serviceName: mockState.searchParams.get('serviceName') || undefined,
+      serviceNamespace: mockState.searchParams.get('serviceNamespace') || undefined,
+      environment: mockState.searchParams.get('environment') || undefined,
+      start: mockState.searchParams.get('start') || undefined,
+      end: mockState.searchParams.get('end') || undefined,
+      timeRange: mockState.searchParams.get('timeRange') || undefined,
+      refresh: mockState.searchParams.get('refresh') || undefined,
+      live: mockState.searchParams.get('live') || undefined,
+      tz: mockState.searchParams.get('tz') || undefined,
+      source: mockState.searchParams.get('source') || undefined,
+      traceId: mockState.searchParams.get('traceId') || undefined,
+      spanId: mockState.searchParams.get('spanId') || undefined
+    },
+    shouldCleanUrl: Boolean(mockState.searchParams.get('returnLabel') || mockState.searchParams.get('returnTo')?.includes('returnLabel='))
+  };
+}
+
+function renderLogManagePage(initialRouteState = buildLogManageRouteState()) {
+  return renderToStaticMarkup(<LogManagePage initialRouteState={initialRouteState} />);
+}
+
+function renderInteractiveLogManagePage(initialRouteState = buildLogManageRouteState()) {
+  interactionRoot?.render(<LogManagePage initialRouteState={initialRouteState} />);
+}
+
 beforeEach(() => {
   mockState.searchParams = new URLSearchParams();
   mockState.replace.mockReset();
@@ -315,39 +396,113 @@ afterEach(() => {
 describe('log manage page', () => {
   it('keeps log manage on the OTLP cold Workbench owner instead of the old external-product shell', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
+    const messagesSource = readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8');
 
     expect(source).toContain('data-log-manage-route="otlp-cold-log-workbench"');
     expect(source).toContain('data-log-manage-style-baseline="hertzbeat-cold-matte"');
+    expect(source).toContain('data-log-manage-header-padding-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('padding="header"');
+    expect(source).not.toContain('data-log-manage-header="cold-compact-header" data-log-manage-panel-surface="header" className="px-5 py-4"');
     expect(source).toContain('data-log-manage-query-bar="cold-query-row"');
+    expect(source).toContain('data-log-manage-panel-surface-padding-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('padding="query"');
+    expect(source).not.toContain('data-log-manage-query-bar="cold-query-row" data-log-manage-panel-surface="query" className="px-4 py-3"');
+    expect(source).toContain('data-log-manage-api-degraded="true"');
+    expect(source).toContain('data-log-manage-api-degraded-owner="hertzbeat-ui-state-notice"');
+    expect(messagesSource).toContain("'log.manage.api.degraded.title':");
+    expect(messagesSource).toContain("'log.manage.api.degraded.copy':");
     expect(source).toContain('data-log-manage-chart-band="cold-chart-band"');
+    expect(source).toContain('data-log-manage-chart-padding-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('padding="chart"');
+    expect(source).not.toContain('data-log-manage-chart-band="cold-chart-band" data-log-manage-panel-surface="chart" className="px-4 py-4"');
     expect(source).toContain('data-log-manage-log-list="cold-dense-log-list"');
+    expect(source).toContain('data-log-manage-table-chrome-owner="hertzbeat-ui-data-table"');
+    expect(source).toContain('HzDataTable');
+    expect(source).toContain('HzPanelHeader');
+    expect(source).toContain('data-log-manage-table-header-owner="hertzbeat-ui-panel-header"');
+    expect(source).toContain('data-log-manage-table-count-badge-owner="hertzbeat-ui-status-badge"');
     expect(source).toContain('data-log-manage-detail-panel="cold-detail-panel"');
+    expect(source).toContain('data-log-manage-detail-header-owner="hertzbeat-ui-panel-header"');
+    expect(source).toContain("eyebrow={t('log.manage.detail.title')}");
     expect(source).not.toContain('data-log-manage-hertzbeat-loop="collector-template-alert-loop"');
     expect(source).toContain('data-log-manage-entity-context="hertzbeat-signal-entity-context"');
-    expect(source).toContain('aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"');
+    expect(source).toContain('data-log-manage-entity-context-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('data-log-manage-selected-evidence-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('data-log-manage-detail-facts-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('data-log-manage-stream-selected-detail-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('data-log-manage-stream-detail-action-stack="shared-control-stack"');
+    expect(source).toContain('data-log-manage-stream-detail-action-stack-owner="hertzbeat-ui-control-stack"');
+    expect(source).toContain('data-log-manage-history-detail-action-stack="shared-control-stack"');
+    expect(source).toContain('data-log-manage-history-detail-action-stack-owner="hertzbeat-ui-control-stack"');
+    expect(source).not.toContain('className="mt-4 flex flex-col gap-2"');
+    expect(source).toContain("aria-label={t('log.manage.context.entity.aria')}");
     expect(source).not.toContain('data-log-manage-intake-quality="logs-collector-quality"');
     expect(source).not.toContain('data-log-manage-intake-quality-row={row.key}');
     expect(source).not.toContain('buildLogIntakeQualityRows');
     expect(source).toContain('data-log-manage-view-switch="stream-history"');
+    expect(source).toContain('data-log-manage-view-switch-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('padding="view-switch"');
+    expect(source).toContain('data-log-manage-view-switch-layout="shared-view-switch"');
+    expect(source).toContain('data-log-manage-view-switch-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="view-switch"');
+    expect(source).toContain('data-log-manage-view-toggle-group="shared-action-group"');
+    expect(source).toContain('data-log-manage-view-toggle-group-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('layout="end-wrap"');
+    expect(source).not.toContain('className="flex flex-wrap items-center justify-between gap-2"');
+    expect(source).not.toContain('className="ml-auto flex flex-wrap items-center justify-end gap-2"');
+    expect(source).not.toContain('data-log-manage-panel-surface="view-switch" className="px-3 py-3"');
     expect(source).toContain('data-log-manage-stream-stage="hertzbeat-live-log-stream"');
+    expect(source).toContain('data-log-manage-stream-stage-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(source).toContain('clip');
+    expect(source).toContain('data-log-manage-stream-stage-layout="shared-stream-stage"');
+    expect(source).toContain('data-log-manage-stream-stage-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="stream-stage"');
+    expect(source).not.toContain('className="grid min-h-[520px] lg:grid-cols-[minmax(0,1fr)_320px]"');
+    expect(source).not.toContain(`data-log-manage-stream-live-state={isStreamPaused ? 'paused' : 'live'}
+        className="overflow-hidden"`);
+    expect(source).toContain('data-log-manage-stream-stage-header-owner="hertzbeat-ui-panel-header"');
+    expect(source).toContain('data-log-manage-stream-selected-header-owner="hertzbeat-ui-panel-header"');
+    expect(source).toContain('data-log-manage-stream-pause-notice-owner="hertzbeat-ui-state-notice"');
+    expect(source).toContain('data-log-manage-stream-selected-helper-owner="hertzbeat-ui-state-notice"');
+    expect(source).toContain("warning={detailSelection?.selectionState === 'detached' ? t('log.manage.stream.detail.detached-warning') : undefined}");
+    expect(source).toContain("eyebrow={t('log.manage.stream.stage.kicker')}");
+    expect(source).toContain("eyebrow={t('log.manage.stream.selected.title')}");
     expect(source).toContain('data-log-manage-reconnect-action="true"');
-    expect(source).toContain('data-log-manage-row-detail-action="true"');
+    expect(source).toContain("'data-log-manage-row-detail-action': 'true'");
     expect(source).toContain('data-log-manage-row-trace-detail-action="true"');
     expect(source).toContain('LogStreamDetailDialog');
     expect(source).toContain('buildLogAttributionDiagnostics(detailLog, t)');
     expect(source).toContain('attributionDiagnostics={detailAttributionDiagnostics}');
-    expect(source).toContain('实体上下文');
+    expect(source).toContain('HzAttributeDiagnostics');
+    expect(source).toContain('data-log-manage-selected-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(source).toContain("t('log.manage.context.entity.title')");
     expect(source).not.toContain('HertzBeat 采集闭环');
     expect(source).not.toContain('告警闭环');
     expect(source).not.toContain('日志排查可返回接入总览、采集集群、监控模板和告警处理上下文。');
-    expect(source).toContain('采集集群');
-    expect(source).toContain('监控模板');
-    expect(source).toContain('采集来源');
-    expect(source).toContain('实体详情');
-    expect(source).toContain('告警处理');
+    [
+      'log.manage.route.action.collector',
+      'log.manage.route.action.templates',
+      'log.manage.route.action.return-source',
+      'log.manage.route.action.entity',
+      'log.manage.route.action.alerts',
+      'log.manage.context.entity.aria'
+    ].forEach(key => {
+      expect(source).toContain(`t('${key}`);
+      expect(messagesSource).toContain(`'${key}'`);
+    });
     expect(source).toContain('href={handoffLinks.alertHandlingHref}');
+    expect(source).toContain('data-log-manage-action-row-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-log-manage-action-row-layout-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-log-manage-return-action="true"');
+    expect(source).toContain('data-log-manage-header-action="return-source"');
+    expect(source).toContain('data-log-manage-header-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(source).toContain('layout="full-end"');
+    expect(source).not.toContain('data-log-manage-action-row="cold-workbench-actions" className="flex flex-wrap items-center justify-end gap-2"');
+    expect(source).not.toContain('data-log-manage-header-action="collector">\n                  <Server className="h-4 w-4"');
+    expect(source).not.toContain('data-log-manage-header-action="templates">\n                  <ListChecks className="h-4 w-4"');
+    expect(source).not.toContain('data-log-manage-header-action="alerts">\n                  <BellRing className="h-4 w-4"');
     expect(source).toContain('data-log-manage-alert-context-hint="entity-trace-alert-handoff"');
-    expect(source).toContain('按当前实体、服务和已带入的链路上下文查看相关告警');
+    expect(source).toContain("t('log.manage.handoff.alert-hint')");
     expect(source).not.toContain('告警规则');
     expect(source).not.toContain('signoz-');
     expect(source).not.toContain('data-log-manage-floating-actions');
@@ -367,7 +522,8 @@ describe('log manage page', () => {
     expect(source).not.toContain('SummaryMetricGrid');
     expect(source).not.toContain('DrawerSection');
     expect(source).not.toContain('VirtualList');
-    expect(source).not.toContain('DataTable');
+    expect(source).not.toContain('<DataTable');
+    expect(source).not.toContain('import { DataTable');
   });
 
   it('keeps log route-level actions on the cold-matte palette instead of bright blue demo buttons', () => {
@@ -379,6 +535,23 @@ describe('log manage page', () => {
     expect(source).not.toContain('hover:bg-[#5574f4]');
   });
 
+  it('renders a shared return action when logs inherit a trace return path', () => {
+    mockState.searchParams = new URLSearchParams(
+      'view=list&traceId=trace-123&spanId=span-456&serviceName=checkout&returnTo=%2Ftrace%2Fmanage%3FtraceId%3Dtrace-123%26spanId%3Dspan-456%26serviceName%3Dcheckout'
+    );
+
+    const html = renderLogManagePage();
+
+    expect(html).toContain('data-log-manage-action-row-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-action-group-layout="full-end"');
+    expect(html).toContain('data-log-manage-return-action="true"');
+    expect(html).toContain('data-log-manage-header-action="return-source"');
+    expect(html).toContain('data-log-manage-header-action-icon="return-source"');
+    expect(html).toContain('data-log-manage-header-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('href="/trace/manage?traceId=trace-123&amp;spanId=span-456&amp;serviceName=checkout"');
+    expect(html).toContain('返回来源');
+  });
+
   it('keeps the Angular log-to-trace drilldown contract as drawer preview before route navigation', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
 
@@ -389,10 +562,10 @@ describe('log manage page', () => {
     expect(source).toContain('openTraceDrilldownFromLog');
     expect(source).toContain('data-log-manage-row-trace-detail-action="true"');
     expect(source).toContain('data-log-manage-open-log-detail-before-trace="true"');
-    expect(source).toContain("openLogDetails(entry, 'history');");
+    expect(source).toContain("onRowClick={row => openLogDetails(logEntryByRowKey.get(row.key) ?? null, 'history')}");
     expect(source).toContain('data-log-related-trace-open-workspace-action="true"');
     expect(source).toContain('data-log-manage-results-open-trace-action="true"');
-    expect(source).toContain('查看完整链路');
+    expect(source).toContain("t('log.manage.related-trace.open-workspace')");
     expect(source).not.toContain('data-log-manage-row-trace-link="context-preserved"');
     expect(source).not.toContain('data-log-manage-row-trace-preview-action="true"');
     expect(source).not.toContain('href={row.traceId !== \'-\' ? rowHandoffLinks.traceHref : \'/trace/manage\'}');
@@ -402,9 +575,9 @@ describe('log manage page', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
 
     expect(source).toContain('buildSelectedSpanFacts');
-    expect(source).toContain('`${relatedTraceRows.length} 个跨度`');
-    expect(source).toContain("badges={relatedTraceDetail ? ['链路预览'] : []}");
-    expect(source).toContain("label: '关联'");
+    expect(source).toContain("t('log.manage.related-trace.spans-count'");
+    expect(source).toContain("badges={relatedTraceDetail ? [t('log.manage.related-trace.badge')] : []}");
+    expect(source).toContain("label: t('log.manage.related-trace.fact.links')");
     expect(source).not.toContain('`${relatedTraceRows.length} spans`');
     expect(source).not.toContain("badges={relatedTraceDetail ? ['SPAN'] : []}");
     expect(source).not.toContain("label: 'Links'");
@@ -442,8 +615,7 @@ describe('log manage page', () => {
     ];
 
     try {
-      const { default: LogManagePage } = await import('./page');
-      const html = renderToStaticMarkup(<LogManagePage />);
+      const html = renderLogManagePage();
 
       expect(source).toContain("const missingEntityHandoffTitle = t('log.manage.handoff.entity-disabled')");
       expect(source).toContain('data-log-manage-entity-action-disabled="missing-entity-id"');
@@ -456,18 +628,35 @@ describe('log manage page', () => {
     } finally {
       mockState.renderData.list.content = originalContent;
     }
-  }, 15000);
+  }, 30000);
 
   it('renders the in-place log stream when the Angular stream view is requested', async () => {
     mockState.searchParams = new URLSearchParams('view=stream&traceId=trace-123&spanId=span-456&severityText=ERROR');
-
-    const { default: LogManagePage } = await import('./page');
-    const html = renderToStaticMarkup(<LogManagePage />);
+    const html = renderLogManagePage();
 
     expect(html).toContain('data-log-manage-view-switch="stream-history"');
+    expect(html).toContain('data-log-manage-view-switch-layout="shared-view-switch"');
+    expect(html).toContain('data-log-manage-view-switch-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="view-switch"');
+    expect(html).toContain('data-log-manage-view-toggle-group="shared-action-group"');
+    expect(html).toContain('data-log-manage-view-toggle-group-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-action-group-layout="end-wrap"');
     expect(html).toContain('data-log-manage-stream-stage="hertzbeat-live-log-stream"');
+    expect(html).toContain('data-log-manage-stream-stage-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-clip="true"');
+    expect(html).toContain('data-log-manage-stream-stage-layout="shared-stream-stage"');
+    expect(html).toContain('data-log-manage-stream-stage-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="stream-stage"');
+    expect(html).toContain('data-log-manage-stream-stage-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-log-manage-stream-selected-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-log-manage-stream-detail-action-stack="shared-control-stack"');
+    expect(html).toContain('data-log-manage-stream-detail-action-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-control-stack-layout="stack"');
+    expect(html).toContain('data-hz-panel-header-eyebrow="true"');
+    expect(html).not.toContain('data-log-manage-stream-selected-helper-owner="hertzbeat-ui-state-notice"');
     expect(html).toContain('data-log-manage-reconnect-action="true"');
     expect(html).toContain('data-log-manage-stream-row="true"');
+    expect(html).toContain('data-log-manage-stream-severity-tone="danger"');
     expect(html).toContain('checkout timeout');
     expect(html).not.toContain('data-log-manage-stream-empty-state="true"');
     expect(html).toContain('日志流');
@@ -479,7 +668,7 @@ describe('log manage page', () => {
     expect(html).not.toContain('运行查询');
     expect(html).not.toContain('/api/logs/sse/subscribe');
     expect(html).not.toContain('data-log-manage-log-list="cold-dense-log-list"');
-  });
+  }, 30000);
 
   it('keeps live stream rows keyed by a monotonic sequence without surfacing normal retention as backpressure', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
@@ -493,9 +682,14 @@ describe('log manage page', () => {
 
   it('keeps no-log empty guidance operator-facing instead of generic storage copy', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
+    const messagesSource = readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8');
 
     expect(source).toContain('data-log-manage-empty-guidance="operator-no-data-guidance"');
-    expect(source).toContain('确认时间范围、实体归因、采集器和监控模板后再查看日志。');
+    expect(source).toContain('data-log-manage-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(source).toContain('data-log-manage-stream-empty-owner="hertzbeat-ui-empty-state"');
+    expect(source).toContain('HzEmptyState');
+    expect(source).toContain("t('log.manage.empty.copy')");
+    expect(messagesSource).toContain("'log.manage.empty.copy': '确认时间范围、实体归因、采集器和监控模板后再查看日志。'");
     expect(source).not.toContain('日志写入后会在这里按时间倒序展示。');
   });
 
@@ -509,15 +703,31 @@ describe('log manage page', () => {
     expect(source).toContain('requestAnimationFrame(flushPendingStreamItems)');
     expect(source).toContain('resolveBrowserLogStreamUrl(buildLogStreamUrl(query, routeContext))');
     expect(source).toContain("useState(() => routeContext.live === 'false')");
-    expect(source).toContain('const { listUrl, overviewUrl, trendUrl, coverageUrl } = buildLogUrls(query, routeContext);');
+    expect(source).toContain('const logUrls = useMemo(() => buildLogUrls(query, routeContext), [query, routeContext]);');
+    expect(source).toContain('const { listUrl, overviewUrl, trendUrl, coverageUrl } = logUrls;');
     expect(source).toContain('resolveStreamWindow');
     expect(source).toContain('readStreamViewportState');
     expect(source).toContain('STREAM_VIEWPORT_ROW_HEIGHT');
     expect(source).toContain('anchorIndex: streamViewport.isPinnedToLatest ? null : selectedStreamIndex');
+    expect(source).toContain('HzScrollViewport');
+    expect(source).toContain('HzLogStreamLiveRow');
+    expect(source).toContain('HzDetailAside');
+    expect(source).toContain('HzDetailBodyStack');
     expect(source).toContain('data-log-manage-stream-viewport="virtualized-log-stream"');
+    expect(source).toContain('data-log-manage-stream-viewport-owner="hertzbeat-ui-scroll-viewport"');
+    expect(source).toContain('variant="log-stream"');
+    expect(source).not.toContain('className="hb-scrollbar max-h-[620px] overflow-auto"');
     expect(source).toContain('data-log-manage-stream-window');
+    expect(source).toContain('data-log-manage-stream-row-owner="hertzbeat-ui-log-stream-row"');
     expect(source).toContain('data-log-manage-stream-row-style="compact-live-row"');
+    expect(source).toContain('data-log-manage-stream-selected-aside="shared-detail-aside"');
+    expect(source).toContain('data-log-manage-stream-selected-aside-owner="hertzbeat-ui-detail-aside"');
+    expect(source).toContain('data-log-manage-stream-selected-body="shared-detail-body-stack"');
+    expect(source).toContain('data-log-manage-stream-selected-body-owner="hertzbeat-ui-detail-body-stack"');
     expect(source).toContain('visibleStreamItems.map');
+    expect(source).not.toContain('className={`grid w-full grid-cols-[58px_minmax(0,112px)_minmax(0,1fr)]');
+    expect(source).not.toContain('<aside className="border-l border-[#252b35] bg-[#0b0e13] px-4 py-4">');
+    expect(source).not.toContain('<div className="mt-4 space-y-2">');
     expect(source).not.toContain('日志流过快');
     expect(source).not.toContain('前端已隐藏');
     expect(source).not.toContain('shouldShowStreamBackpressureNotice');
@@ -529,15 +739,23 @@ describe('log manage page', () => {
       'view=stream&timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=Asia%2FShanghai&serviceName=checkout'
     );
     const source = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
-
-    const { default: LogManagePage } = await import('./page');
-    const html = renderToStaticMarkup(<LogManagePage />);
+    const html = renderLogManagePage();
 
     expect(source).toContain("useEffect(() => {\n    setIsStreamPaused(routeContext.live === 'false');\n  }, [routeContext.live]);");
     expect(source).toContain("if (isStreamPaused) {\n      setStreamStatus('disconnected');\n      return undefined;\n    }");
     expect(source).toContain("data-log-manage-stream-live-state={isStreamPaused ? 'paused' : 'live'}");
     expect(html).toContain('data-log-manage-stream-stage="hertzbeat-live-log-stream"');
     expect(html).toContain('data-log-manage-stream-live-state="paused"');
+    expect(html).toContain('data-log-manage-stream-stage-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-log-manage-stream-selected-aside="shared-detail-aside"');
+    expect(html).toContain('data-log-manage-stream-selected-aside-owner="hertzbeat-ui-detail-aside"');
+    expect(html).toContain('data-hz-ui="detail-aside"');
+    expect(html).toContain('data-log-manage-stream-selected-body="shared-detail-body-stack"');
+    expect(html).toContain('data-log-manage-stream-selected-body-owner="hertzbeat-ui-detail-body-stack"');
+    expect(html).toContain('data-hz-ui="detail-body-stack"');
+    expect(html).toContain('data-log-manage-stream-pause-notice-owner="hertzbeat-ui-state-notice"');
+    expect(html).toContain('data-log-manage-stream-pause-notice="paused-buffer-visible"');
+    expect(html).toContain('data-hz-ui="state-notice"');
     expect(html).toContain('已暂停');
     expect(html).toContain('继续');
   }, 15000);
@@ -547,14 +765,12 @@ describe('log manage page', () => {
       'view=list&timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=Asia%2FShanghai&serviceName=checkout'
     );
     const source = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
-
-    const { default: LogManagePage } = await import('./page');
-    const html = renderToStaticMarkup(<LogManagePage />);
+    const html = renderLogManagePage();
 
     expect(source).toContain("import { buildTimeRangeControlLabels, TimeRangeControl } from '@/components/observability/time-range-control';");
     expect(source).toContain('labels={buildTimeRangeControlLabels(t)}');
     expect(source).toContain("live: routeContext.live || 'true'");
-    expect(source).toContain('buildLogManageRoute(searchParams, query, currentView, appliedContext)');
+    expect(source).toContain('buildLogManageRoute(routeContext, query, currentView, appliedContext)');
     expect(source).toContain('data-log-manage-time-control="shared-time-context-control"');
     expect(source).toContain('data-log-manage-time-control-placement="top-right"');
     expect(source).toContain('data-log-manage-time-control-visual="narrow-top-right-rail"');
@@ -587,19 +803,51 @@ describe('log manage page', () => {
 
   it('renders the cold query row, trend band, dense log list, and detail handoffs', async () => {
     mockState.searchParams = new URLSearchParams('view=list');
-
-    const { default: LogManagePage } = await import('./page');
-    const html = renderToStaticMarkup(<LogManagePage />);
+    const html = renderLogManagePage();
 
     expect(html).toContain('data-log-manage-route="otlp-cold-log-workbench"');
+    expect(html).toContain('data-loading-copy="正在加载日志工作台"');
     expect(html).toContain('data-log-manage-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-log-manage-query-bar="cold-query-row"');
+    expect(html).toContain('data-log-manage-panel-surface-padding-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="query"');
+    expect(html).toContain('data-log-manage-query-search-frame="shared-search-field-frame"');
+    expect(html).toContain('data-log-manage-query-search-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-width="log-query"');
+    expect(html).toContain('data-log-manage-query-search-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-log-manage-query-search-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-inset="search-icon"');
+    expect(html).toContain('data-hz-input-width="log-query-expression"');
+    expect(html).toContain('data-log-manage-query-severity-select="shared-log-severity-select"');
+    expect(html).toContain('data-log-manage-query-severity-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-select-width="log-severity"');
+    expect(html).toContain('data-hz-select-trigger-tone="signal-query"');
+    expect(html).toContain('data-log-manage-query-token-input="trace-id"');
+    expect(html).toContain('data-log-manage-query-token-input="span-id"');
+    expect(html).toContain('data-log-manage-query-token-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-width="log-query-token"');
+    expect(html).toContain('data-log-manage-query-body-input="shared-log-body-input"');
+    expect(html).toContain('data-log-manage-query-body-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-width="log-query-body"');
+    expect(html).toContain('placeholder="service.name = &quot;checkout&quot;"');
     expect(html).toContain('data-log-manage-chart-band="cold-chart-band"');
+    expect(html).toContain('data-log-manage-chart-padding-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="chart"');
     expect(html).toContain('data-log-manage-log-list="cold-dense-log-list"');
+    expect(html).toContain('data-log-manage-table-chrome-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).toContain('data-log-manage-table-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-ui="panel-header"');
+    expect(html).toContain('data-log-manage-table-count-badge-owner="hertzbeat-ui-status-badge"');
     expect(html).toContain('data-log-manage-detail-panel="cold-detail-panel"');
+    expect(html).toContain('data-log-manage-detail-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-panel-header-eyebrow="true"');
     expect(html).toContain('data-log-manage-row-detail-action="true"');
+    expect(html).toContain('data-log-manage-severity-tone="danger"');
     expect(html).toContain('查看日志');
     expect(html).toContain('data-log-manage-selected-evidence="selected-log-evidence"');
+    expect(html).toContain('data-log-manage-selected-evidence-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-log-manage-detail-facts-owner="hertzbeat-ui-detail-rows"');
     expect(html).toContain('日志证据');
     expect(html).toContain('日志时间');
     expect(html).toContain('日志级别');
@@ -607,6 +855,9 @@ describe('log manage page', () => {
     expect(html).toContain('最近上报');
     expect(html).not.toContain('data-log-manage-hertzbeat-loop="collector-template-alert-loop"');
     expect(html).toContain('data-log-manage-entity-context="hertzbeat-signal-entity-context"');
+    expect(html).toContain('data-log-manage-entity-context-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-log-manage-selected-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(html).toContain('data-hz-ui="attribute-diagnostics"');
     expect(html).not.toContain('data-log-manage-intake-quality="logs-collector-quality"');
     expect(html).not.toContain('data-log-manage-intake-quality-row=');
     expect(html).toContain('data-log-manage-row-trace-detail-action="true"');
@@ -672,8 +923,7 @@ describe('log manage page', () => {
     ];
 
     try {
-      const { default: LogManagePage } = await import('./page');
-      const html = renderToStaticMarkup(<LogManagePage />);
+      const html = renderLogManagePage();
 
       expect(html).toContain('data-log-manage-results-open-trace-action="true"');
       expect(html).toContain('data-log-manage-results-open-trace-action-disabled="missing-trace-id"');
@@ -686,14 +936,12 @@ describe('log manage page', () => {
 
   it('opens the first trace-matched log in the side detail when a trace route lands on history', async () => {
     mockState.searchParams = new URLSearchParams('view=list&traceId=trace-123&spanId=span-456');
-
-    const { default: LogManagePage } = await import('./page');
     interactionContainer = document.createElement('div');
     document.body.appendChild(interactionContainer);
     interactionRoot = createRoot(interactionContainer);
 
     await act(async () => {
-      interactionRoot?.render(<LogManagePage />);
+      renderInteractiveLogManagePage();
       await Promise.resolve();
       await Promise.resolve();
     });
@@ -711,14 +959,12 @@ describe('log manage page', () => {
 
   it('opens the first trace-matched seeded stream row in the side detail when a trace route lands on stream', async () => {
     mockState.searchParams = new URLSearchParams('view=stream&traceId=trace-123&spanId=span-456');
-
-    const { default: LogManagePage } = await import('./page');
     interactionContainer = document.createElement('div');
     document.body.appendChild(interactionContainer);
     interactionRoot = createRoot(interactionContainer);
 
     await act(async () => {
-      interactionRoot?.render(<LogManagePage />);
+      renderInteractiveLogManagePage();
       await Promise.resolve();
       await Promise.resolve();
     });
@@ -753,13 +999,12 @@ describe('log manage page', () => {
     ];
 
     try {
-      const { default: LogManagePage } = await import('./page');
       interactionContainer = document.createElement('div');
       document.body.appendChild(interactionContainer);
       interactionRoot = createRoot(interactionContainer);
 
       await act(async () => {
-        interactionRoot?.render(<LogManagePage />);
+        renderInteractiveLogManagePage();
         await Promise.resolve();
       });
 
@@ -806,8 +1051,7 @@ describe('log manage page', () => {
     ];
 
     try {
-      const { default: LogManagePage } = await import('./page');
-      const html = renderToStaticMarkup(<LogManagePage />);
+      const html = renderLogManagePage();
 
       expect(html).toContain('data-log-manage-selected-attribution-diagnostics="hertzbeat-attribute-diagnostics"');
       expect(html).toContain('归因诊断');
@@ -886,14 +1130,12 @@ describe('log manage page', () => {
       }
       throw new Error(`Unexpected trace preview URL: ${url}`);
     });
-
-    const { default: LogManagePage } = await import('./page');
     interactionContainer = document.createElement('div');
     document.body.appendChild(interactionContainer);
     interactionRoot = createRoot(interactionContainer);
 
     await act(async () => {
-      interactionRoot?.render(<LogManagePage />);
+      renderInteractiveLogManagePage();
       await Promise.resolve();
     });
 
@@ -960,14 +1202,12 @@ describe('log manage page', () => {
 
   it('keeps side-panel trace actions on the log detail drawer before loading trace preview', async () => {
     mockState.searchParams = new URLSearchParams('view=list');
-
-    const { default: LogManagePage } = await import('./page');
     interactionContainer = document.createElement('div');
     document.body.appendChild(interactionContainer);
     interactionRoot = createRoot(interactionContainer);
 
     await act(async () => {
-      interactionRoot?.render(<LogManagePage />);
+      renderInteractiveLogManagePage();
       await Promise.resolve();
     });
 
@@ -996,17 +1236,38 @@ describe('log manage page', () => {
       .mockResolvedValueOnce(mockState.renderData.list)
       .mockResolvedValueOnce(mockState.renderData.trend)
       .mockResolvedValueOnce(mockState.renderData.coverage);
-
-    const { default: LogManagePage } = await import('./page');
-    renderToStaticMarkup(<LogManagePage />);
+    const html = renderLogManagePage();
     await mockState.lastLoad?.();
 
-    expect(apiMessageGet.mock.calls).toEqual([
-      ['/logs/stats/overview?search=timeout&traceId=trace-123&severityText=ERROR&severityNumber=17'],
-      ['/logs/list?pageIndex=0&pageSize=8&search=timeout&traceId=trace-123&severityText=ERROR&severityNumber=17'],
-      ['/logs/stats/trend?search=timeout&traceId=trace-123&severityText=ERROR&severityNumber=17'],
-      ['/logs/stats/trace-coverage?search=timeout&traceId=trace-123&severityText=ERROR&severityNumber=17']
+    expect(html).toContain('data-cache-key="log-manage|list|');
+    expect(html).toContain('/logs/stats/overview?search=timeout&amp;traceId=trace-123&amp;severityText=ERROR&amp;severityNumber=17');
+    expect(html).toContain('/logs/list?pageIndex=0&amp;pageSize=8&amp;search=timeout&amp;traceId=trace-123&amp;severityText=ERROR&amp;severityNumber=17');
+    expect(html).toContain('/logs/stats/trend?search=timeout&amp;traceId=trace-123&amp;severityText=ERROR&amp;severityNumber=17');
+    expect(html).toContain('/logs/stats/trace-coverage?search=timeout&amp;traceId=trace-123&amp;severityText=ERROR&amp;severityNumber=17');
+    expect(html).toContain('data-cache-settled-ttl="10000"');
+    expect(apiMessageGet.mock.calls.map(([url]) => url)).toEqual([
+      '/logs/stats/overview?search=timeout&traceId=trace-123&severityText=ERROR&severityNumber=17',
+      '/logs/list?pageIndex=0&pageSize=8&search=timeout&traceId=trace-123&severityText=ERROR&severityNumber=17',
+      '/logs/stats/trend?search=timeout&traceId=trace-123&severityText=ERROR&severityNumber=17',
+      '/logs/stats/trace-coverage?search=timeout&traceId=trace-123&severityText=ERROR&severityNumber=17'
     ]);
+    expect(apiMessageGet.mock.calls.every(([, init]) => init?.signal instanceof AbortSignal)).toBe(true);
+  });
+
+  it('returns an explicit degraded empty state when the log API blocks the initial route load', async () => {
+    mockState.searchParams = new URLSearchParams('view=list');
+    apiMessageGet.mockRejectedValueOnce(new Error('Log API request timed out after 3500ms'));
+    renderLogManagePage();
+    const result = await mockState.lastLoad?.() as any;
+
+    expect(result.loadStatus).toEqual({
+      state: 'degraded',
+      message: 'Log API request timed out after 3500ms'
+    });
+    expect(result.list.content).toEqual([]);
+    expect(result.overview.totalLogs).toBe(0);
+    expect(result.trend.hourlyStats).toEqual({});
+    expect(result.coverage.traceCoverage.withTrace).toBe(0);
   });
 
   it('normalizes live log overview counters before rendering status cards', async () => {
@@ -1022,9 +1283,7 @@ describe('log manage page', () => {
       .mockResolvedValueOnce(mockState.renderData.list)
       .mockResolvedValueOnce(mockState.renderData.trend)
       .mockResolvedValueOnce(mockState.renderData.coverage);
-
-    const { default: LogManagePage } = await import('./page');
-    renderToStaticMarkup(<LogManagePage />);
+    renderLogManagePage();
     const result = await mockState.lastLoad?.() as any;
 
     expect(result.overview.totalLogs).toBe(42);
diff --git a/web-next/app/log/manage/page.tsx b/web-next/app/log/manage/page.tsx
index a2665958bd..d745431f3a 100644
--- a/web-next/app/log/manage/page.tsx
+++ b/web-next/app/log/manage/page.tsx
@@ -1,8 +1,13 @@
-'use client';
-
 import React from 'react';
 import LogManagePage from './log-manage-page';
+import { readLogManageRouteState, type LogManageSearchParams } from '@/lib/log-manage/query-state';
 
-export default function LogManageRoutePage() {
-  return <LogManagePage />;
+export default async function LogManageRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<LogManageSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readLogManageRouteState(resolvedSearchParams);
+  return <LogManagePage initialRouteState={routeState} />;
 }
diff --git a/web-next/app/log/manage/route-state.test.ts b/web-next/app/log/manage/route-state.test.ts
index 3d91630f57..86155bd746 100644
--- a/web-next/app/log/manage/route-state.test.ts
+++ b/web-next/app/log/manage/route-state.test.ts
@@ -1,6 +1,7 @@
 import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { describe, expect, it } from 'vitest';
+import { readSignalRouteContext } from '../../../lib/signal-route-context';
 import { buildLogManageRoute, buildResetLogManageRoute } from './route-state';
 
 function createSearchParams(input: string) {
@@ -10,9 +11,9 @@ function createSearchParams(input: string) {
 describe('log manage route state', () => {
   it('preserves workbench context when applying a query', () => {
     const route = buildLogManageRoute(
-      createSearchParams(
+      readSignalRouteContext(createSearchParams(
         'timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=Asia%2FShanghai&entityId=42&entityName=checkout&returnTo=%2Foverview&returnLabel=Overview&serviceName=checkout&serviceNamespace=payments&environment=prod&codeRepo=https%3A%2F%2Fexample.test%2Frepo&codeProvider=github&codePath=src%2Fapp.ts&codeSearch=CheckoutService&codeLabel=source'
-      ),
+      )),
       {
         search: 'timeout',
         logContent: '',
@@ -32,7 +33,7 @@ describe('log manage route state', () => {
 
   it('keeps route context when resetting back to an empty stream query', () => {
     const route = buildResetLogManageRoute(
-      createSearchParams('timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=UTC&returnTo=%2Fentities&returnLabel=Entities&serviceName=checkout'),
+      readSignalRouteContext(createSearchParams('timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=UTC&returnTo=%2Fentities&returnLabel=Entities&serviceName=checkout')),
       'stream'
     );
 
@@ -44,9 +45,9 @@ describe('log manage route state', () => {
 
   it('overrides only the shared time context when applying the visible time control', () => {
     const route = buildLogManageRoute(
-      createSearchParams(
+      readSignalRouteContext(createSearchParams(
         'timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=UTC&entityId=42&returnTo=%2Foverview&serviceName=checkout'
-      ),
+      )),
       {
         search: 'timeout',
         logContent: '',
diff --git a/web-next/app/log/manage/route-state.ts b/web-next/app/log/manage/route-state.ts
index 654f97da72..f63932a5aa 100644
--- a/web-next/app/log/manage/route-state.ts
+++ b/web-next/app/log/manage/route-state.ts
@@ -1,29 +1,28 @@
 import {
   buildLogRouteUrl,
-  copyLogRouteContextParams,
   type LogQueryState,
-  type LogWorkbenchView,
-  type SearchParamReader
+  type LogWorkbenchView
 } from '../../../lib/log-manage/query-state';
+import { appendSignalRouteContext, type SignalRouteContext } from '../../../lib/signal-route-context';
 import { appendTimeContextParams, type TimeContext } from '../../../lib/time-context';
 
 export function buildLogManageRoute(
-  searchParams: SearchParamReader,
+  routeContext: SignalRouteContext,
   nextQuery: LogQueryState,
   view: LogWorkbenchView,
   timeContext?: TimeContext
 ) {
   const next = new URLSearchParams(buildLogRouteUrl(nextQuery, { view }).split('?')[1] || '');
-  copyLogRouteContextParams(searchParams, next);
+  appendSignalRouteContext(next, routeContext);
   if (timeContext) {
     appendTimeContextParams(next, timeContext);
   }
   return next.toString() ? `/log/manage?${next.toString()}` : '/log/manage';
 }
 
-export function buildResetLogManageRoute(searchParams: SearchParamReader, view: LogWorkbenchView) {
+export function buildResetLogManageRoute(routeContext: SignalRouteContext, view: LogWorkbenchView) {
   return buildLogManageRoute(
-    searchParams,
+    routeContext,
     { search: '', logContent: '', traceId: '', spanId: '', severityNumber: '', severityText: '' },
     view
   );
diff --git a/web-next/app/log/stream/page.tsx b/web-next/app/log/stream/page.tsx
index c3249d8a5f..64a4e7133a 100644
--- a/web-next/app/log/stream/page.tsx
+++ b/web-next/app/log/stream/page.tsx
@@ -1,5 +1,3 @@
-'use client';
-
 import React from 'react';
 import LogManagePage from '../manage/log-manage-page';
 
diff --git a/web-next/app/trace/manage/page.route-context.test.ts b/web-next/app/trace/manage/page.route-context.test.ts
index c2d8dd9fa2..7ecc226df9 100644
--- a/web-next/app/trace/manage/page.route-context.test.ts
+++ b/web-next/app/trace/manage/page.route-context.test.ts
@@ -4,9 +4,10 @@ import { describe, expect, it } from 'vitest';
 
 describe('trace manage page route-context wiring', () => {
   it('threads signal-route context into trace API url construction for seeded parity routes', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
 
-    expect(source).toContain('const routeContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);');
+    expect(source).toContain('const traceManageRouteState = initialRouteState ?? EMPTY_TRACE_MANAGE_ROUTE_STATE');
+    expect(source).toContain('const routeContext = traceManageRouteState.routeContext;');
     expect(source).toContain('const traceUrls = useMemo(() => buildTraceUrls(query, routeContext), [query, routeContext]);');
   });
 });
diff --git a/web-next/app/trace/manage/page.test.tsx b/web-next/app/trace/manage/page.test.tsx
index 027ede79f9..fa3f0a98ee 100644
--- a/web-next/app/trace/manage/page.test.tsx
+++ b/web-next/app/trace/manage/page.test.tsx
@@ -4,8 +4,10 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import TraceManagePage from './trace-manage-page';
+import type { TraceManageRouteState, TraceQueryState } from '@/lib/trace-manage/query-state';
 
-const apiMessageGet = vi.fn();
+const apiMessageGet = vi.hoisted(() => vi.fn());
 
 const mockState = vi.hoisted(() => ({
   searchParams: new URLSearchParams(),
@@ -39,10 +41,22 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 }));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
-  ClientWorkbench: ({ children, load }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown> }) => {
+  ClientWorkbench: ({
+    children,
+    load,
+    loadingCopy,
+    cacheKey,
+    cacheSettledTtlMs
+  }: {
+    children: (data: any) => React.ReactNode;
+    load: () => Promise<unknown>;
+    loadingCopy?: string;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
+  }) => {
     mockState.lastLoad = load;
     return (
-      <div data-client-workbench="true">
+      <div data-client-workbench="true" data-loading-copy={loadingCopy} data-cache-key={cacheKey} data-cache-settled-ttl={cacheSettledTtlMs}>
         {children({
           overview: {
             totalTraceCount: 8,
@@ -98,6 +112,21 @@ vi.mock('@/lib/signal-route-context', () => ({
       if (value) target.set(key, value);
     });
   },
+  appendSignalRouteContext: (target: URLSearchParams, routeContext: Record<string, string | undefined>) => {
+    ['entityId', 'entityName', 'returnTo', 'serviceName', 'serviceNamespace', 'environment', 'start', 'end', 'timeRange', 'source'].forEach(key => {
+      const value = routeContext[key];
+      if (!value) return;
+      if (key === 'returnTo') {
+        const [path, query = ''] = value.split('?');
+        const params = new URLSearchParams(query);
+        params.delete('returnLabel');
+        const queryString = params.toString();
+        target.set(key, queryString ? `${path}?${queryString}` : path);
+        return;
+      }
+      target.set(key, value);
+    });
+  },
   stripReturnLabelFromHref: (href?: string | null) => {
     if (!href) return undefined;
     const [path, query = ''] = href.split('?');
@@ -212,6 +241,7 @@ vi.mock('@/lib/trace-manage/view-model', () => ({
       namespace: item.serviceNamespace || 'default',
       duration: formatDurationNanos(item.durationNanos),
       status: item.status || 'UNSET',
+      statusTone: item.status === 'ERROR' ? 'danger' : item.status === 'OK' ? 'success' : undefined,
       startTime: formatTime(item.startTime)
     })),
   buildTraceWaterfallRows: () => [],
@@ -241,6 +271,58 @@ vi.mock('@/lib/trace-manage/view-model', () => ({
   }
 }));
 
+function stripReturnLabelFromRoute(href?: string | null) {
+  if (!href) return undefined;
+  const [path, query = ''] = href.split('?');
+  const params = new URLSearchParams(query);
+  params.delete('returnLabel');
+  const queryString = params.toString();
+  return queryString ? `${path}?${queryString}` : path;
+}
+
+function buildTraceManageRouteState(): TraceManageRouteState {
+  const initialQuery: TraceQueryState = {
+    traceId: mockState.searchParams.get('traceId') || '',
+    spanId: mockState.searchParams.get('spanId') || '',
+    serviceName: mockState.searchParams.get('serviceName') || '',
+    errorOnly: mockState.searchParams.get('errorOnly') === 'true'
+  };
+  const start = mockState.searchParams.get('start');
+  const end = mockState.searchParams.get('end');
+  const hasDirtyTimeBound = (value: string | null) => Boolean(value?.trim() && !/^\d+$/.test(value.trim()));
+
+  return {
+    initialQuery,
+    routeContext: {
+      entityId: mockState.searchParams.get('entityId') || undefined,
+      entityName: mockState.searchParams.get('entityName') || undefined,
+      returnTo: stripReturnLabelFromRoute(mockState.searchParams.get('returnTo')),
+      serviceName: mockState.searchParams.get('serviceName') || undefined,
+      serviceNamespace: mockState.searchParams.get('serviceNamespace') || undefined,
+      environment: mockState.searchParams.get('environment') || undefined,
+      start: start && !hasDirtyTimeBound(start) ? start : undefined,
+      end: end && !hasDirtyTimeBound(end) ? end : undefined,
+      timeRange: mockState.searchParams.get('timeRange') || undefined,
+      refresh: mockState.searchParams.get('refresh') || undefined,
+      live: mockState.searchParams.get('live') || undefined,
+      tz: mockState.searchParams.get('tz') || undefined,
+      source: mockState.searchParams.get('source') || undefined,
+      traceId: mockState.searchParams.get('traceId') || undefined,
+      spanId: mockState.searchParams.get('spanId') || undefined
+    },
+    shouldCleanUrl: Boolean(
+      mockState.searchParams.get('returnLabel') ||
+        mockState.searchParams.get('returnTo')?.includes('returnLabel=') ||
+        hasDirtyTimeBound(start) ||
+        hasDirtyTimeBound(end)
+    )
+  };
+}
+
+function renderTraceManagePage(initialRouteState = buildTraceManageRouteState()) {
+  return renderToStaticMarkup(<TraceManagePage initialRouteState={initialRouteState} />);
+}
+
 beforeEach(() => {
   mockState.searchParams = new URLSearchParams();
   mockState.replace.mockReset();
@@ -257,25 +339,119 @@ describe('trace manage page', () => {
       .mockResolvedValueOnce({ totalTraceCount: 8, errorTraceCount: 2, latestObservedAt: 1713200000000, hasActiveTrace: true })
       .mockResolvedValueOnce({ content: [] });
 
-    const { default: TraceManagePage } = await import('./page');
-    const html = renderToStaticMarkup(<TraceManagePage />);
+    const html = renderTraceManagePage();
     await mockState.lastLoad?.();
 
     expect(html).toContain('data-trace-manage-route="otlp-cold-trace-workbench"');
+    expect(html).toContain('data-loading-copy="Loading trace workbench"');
+    expect(html).toContain(
+      'data-cache-key="trace-manage:/traces/list?pageIndex=0&amp;pageSize=8&amp;traceId=trace-123&amp;serviceName=checkout&amp;errorOnly=true|/traces/stats/overview?traceId=trace-123&amp;serviceName=checkout&amp;errorOnly=true|last-30m'
+    );
+    expect(html).toContain('data-cache-settled-ttl="10000"');
     expect(html).toContain('data-trace-manage-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-trace-manage-panel-surface="header"');
+    expect(html).toContain('data-trace-manage-header-padding-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="header"');
+    expect(html).toContain('data-trace-manage-header-layout="shared-header-actions"');
+    expect(html).toContain('data-trace-manage-header-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="header-actions"');
+    expect(html).toContain('data-trace-manage-action-row-layout-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-action-group-layout="full-end"');
+    expect(html).toContain('data-trace-manage-header-action-icon="intake"');
+    expect(html).toContain('data-trace-manage-header-action-icon="collector"');
+    expect(html).toContain('data-trace-manage-header-action-icon="entity"');
+    expect(html).toContain('data-trace-manage-header-action-icon="alerts"');
+    expect(html).toContain('data-trace-manage-header-action-icon="templates"');
+    expect(html).toContain('data-trace-manage-header-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-ui="disabled-action-shell"');
+    expect(html).toContain('data-hz-disabled-action-shell-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(html).toContain('data-trace-manage-disabled-action-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(html).toContain('data-trace-manage-disabled-action-scope="header"');
     expect(html).toContain('data-trace-manage-query-bar="cold-query-row"');
+    expect(html).toContain('data-trace-manage-panel-surface="query"');
+    expect(html).toContain('data-trace-manage-panel-surface-padding-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="query"');
+    expect(html).toContain('data-trace-manage-query-search-frame="shared-search-field-frame"');
+    expect(html).toContain('data-trace-manage-query-search-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-ui="search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-icon="true"');
+    expect(html).toContain('data-trace-manage-query-search-icon="service"');
+    expect(html).toContain('data-trace-manage-query-search-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-hz-ui="search-field-icon"');
+    expect(html).toContain('data-hz-search-field-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-hz-search-field-icon-size="md"');
+    expect(html).toContain('data-trace-manage-query-search-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-inset="search-icon"');
+    expect(html).toContain('data-trace-manage-query-token-field="trace-id"');
+    expect(html).toContain('data-trace-manage-query-token-field="span-id"');
+    expect(html).toContain('data-trace-manage-query-token-field-owner="hertzbeat-ui-query-token-field"');
+    expect(html).toContain('data-hz-ui="query-token-field"');
+    expect(html).toContain('data-hz-query-token-field-owner="hertzbeat-ui-query-token-field"');
+    expect(html).toContain('data-hz-query-token-field-width="trace-id"');
+    expect(html).toContain('data-hz-query-token-field-width="span-id"');
+    expect(html).toContain('data-hz-query-token-input-owner="hertzbeat-ui-query-token-field"');
+    expect(html).toContain('data-trace-manage-query-status-select="shared-query-status-select"');
+    expect(html).toContain('data-trace-manage-query-status-select-owner="hertzbeat-ui-query-status-select"');
+    expect(html).toContain('data-hz-query-status-select-owner="hertzbeat-ui-query-status-select"');
+    expect(html).toContain('data-hz-query-status-select-width="status"');
+    expect(html).toContain('data-trace-manage-query-action-group="shared-query-action-group"');
+    expect(html).toContain('data-trace-manage-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-query-action-group-kind="run-reset"');
+    expect(html).toContain('data-trace-manage-query-action-icon="run"');
+    expect(html).toContain('data-trace-manage-query-action-icon="reset"');
+    expect(html).toContain('data-trace-manage-query-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-button-icon-owner="hertzbeat-ui-button-icon"');
     expect(html).toContain('data-trace-manage-chart-band="cold-chart-band"');
+    expect(html).toContain('data-trace-manage-panel-surface="chart"');
+    expect(html).toContain('data-trace-manage-chart-padding-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="chart"');
+    expect(html).toContain('data-trace-manage-chart-layout="shared-summary-trend"');
+    expect(html).toContain('data-trace-manage-chart-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="summary-trend"');
+    expect(html).toContain('data-trace-manage-table-detail-layout="shared-table-detail"');
+    expect(html).toContain('data-trace-manage-table-detail-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui="workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="table-detail"');
     expect(html).toContain('data-trace-manage-trace-table="cold-dense-trace-list"');
+    expect(html).toContain('data-trace-manage-panel-surface-clip-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-clip="true"');
+    expect(html).toContain('data-trace-manage-table-chrome-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).toContain('data-trace-manage-start-cell-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-trace-manage-service-cell-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-trace-manage-duration-cell-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-trace-manage-trace-id-cell-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-trace-manage-row-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-table-row-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-table-row-action-width="root-span"');
+    expect(html).toContain('data-hz-ui="data-cell-text"');
+    expect(html).toContain('data-hz-data-cell-variant="timestamp"');
+    expect(html).toContain('data-hz-data-cell-variant="identifier"');
+    expect(html).toContain('data-hz-data-cell-width="trace-id"');
+    expect(html).toContain('data-trace-manage-table-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-ui="panel-header"');
+    expect(html).toContain('data-trace-manage-table-count-badge-owner="hertzbeat-ui-status-badge"');
     expect(html).toContain('data-trace-manage-detail-panel="cold-detail-panel"');
+    expect(html).toContain('data-trace-manage-panel-surface-clip-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-trace-manage-detail-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-trace-manage-detail-body-layout="shared-detail-stack"');
+    expect(html).toContain('data-trace-manage-detail-body-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="detail-stack"');
+    expect(html).toContain('data-hz-panel-header-eyebrow="true"');
     expect(html).toContain('data-trace-manage-entity-context="hertzbeat-signal-entity-context"');
+    expect(html).toContain('data-trace-manage-entity-context-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-trace-manage-selected-evidence-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-trace-manage-detail-facts-owner="hertzbeat-ui-detail-rows"');
     expect(html).not.toContain('data-trace-manage-hertzbeat-loop="collector-template-alert-loop"');
-    expect(html).toContain('链路工作台');
-    expect(html).toContain('服务名称');
-    expect(html).toContain('错误链路');
-    expect(html).toContain('趋势带');
-    expect(html).toContain('最近链路');
-    expect(html).toContain('运行查询');
-    expect(html).toContain('按服务、链路 ID、跨度 ID 和错误状态筛选链路,直接查看趋势、列表和详情。');
+    expect(html).toContain('Traces Workbench');
+    expect(html).toContain('Service name');
+    expect(html).toContain('Error traces');
+    expect(html).toContain('Trend band');
+    expect(html).toContain('Recent traces');
+    expect(html).toContain('Run query');
+    expect(html).toContain('Filter traces by service, trace ID, span ID, and error state, then inspect trend, list, and detail evidence.');
     expect(html).not.toContain('高密度工作面');
     expect(html).not.toContain('HertzBeat 采集闭环');
     expect(html).not.toContain('告警闭环');
@@ -285,22 +461,26 @@ describe('trace manage page', () => {
     expect(html).toContain('POST /checkout');
     expect(html).toContain('checkout');
     expect(html).toContain('ERROR');
-    expect(html).toContain('实体上下文');
+    expect(html).toContain('data-trace-manage-status-tone="danger"');
+    expect(html).toContain('Entity context');
     expect(html).toContain('data-trace-manage-attribution-diagnostics="hertzbeat-attribute-diagnostics"');
-    expect(html).toContain('归因诊断');
+    expect(html).toContain('data-trace-manage-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(html).toContain('data-hz-ui="attribute-diagnostics"');
+    expect(html).toContain('Attribution diagnostics');
     expect(html).toContain('hertzbeat.entity_id');
     expect(html).toContain('缺少实体 ID,实体详情会保持禁用');
     expect(html).toContain('当前实体');
     expect(html).toContain('采集来源');
-    expect(html).toContain('实体详情');
-    expect(html).toContain('告警处理');
+    expect(html).toContain('Entity detail');
+    expect(html).toContain('Alert handling');
     expect(html).toContain('data-trace-manage-signal-handoff-hint="trace-log-metric-context"');
-    expect(html).toContain('当前链路的 traceId、spanId 和服务上下文会带入日志与指标工作台');
+    expect(html).toContain('The current traceId, spanId, and service context carry into logs and metrics workbenches.');
     expect(html).toContain('data-trace-manage-selected-evidence="selected-trace-evidence"');
-    expect(html).toContain('链路证据');
-    expect(html).toContain('链路状态');
-    expect(html).toContain('开始时间');
-    expect(html).toContain('最近上报');
+    expect(html).toContain('data-trace-manage-selected-evidence-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('Trace evidence');
+    expect(html).toContain('Trace status');
+    expect(html).toContain('Start time');
+    expect(html).toContain('Latest observed');
     expect(html).not.toContain('告警规则');
     expect(html).not.toContain('保存视图');
     expect(html).not.toContain('创建告警');
@@ -319,13 +499,12 @@ describe('trace manage page', () => {
       ['/traces/stats/overview?traceId=trace-123&serviceName=checkout&errorOnly=true'],
       ['/traces/list?pageIndex=0&pageSize=8&traceId=trace-123&serviceName=checkout&errorOnly=true']
     ]);
-  }, 15000);
+  }, 60000);
 
   it('renders stable trace query selectors for browser retirement smoke', async () => {
     mockState.searchParams = new URLSearchParams('traceId=trace-123&serviceName=checkout&errorOnly=true');
 
-    const { default: TraceManagePage } = await import('./page');
-    const html = renderToStaticMarkup(<TraceManagePage />);
+    const html = renderTraceManagePage();
 
     expect(html).toContain('data-trace-manage-trace-id-input="true"');
     expect(html).toContain('data-trace-manage-service-input="true"');
@@ -334,23 +513,48 @@ describe('trace manage page', () => {
     expect(html).toContain('data-trace-manage-search-action="true"');
     expect(html).toContain('data-trace-manage-open-logs-action="true"');
     expect(html).toContain('data-trace-manage-query-bar="cold-query-row"');
+    expect(html).toContain('data-trace-manage-query-control-stack="shared-inline-controls"');
+    expect(html).toContain('data-trace-manage-query-control-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-control-stack-layout="inline-wrap"');
+    expect(html).toContain('data-trace-manage-query-search-frame="shared-search-field-frame"');
+    expect(html).toContain('data-trace-manage-query-search-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-trace-manage-query-search-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-inset="search-icon"');
+    expect(html).toContain('data-trace-manage-query-token-field-owner="hertzbeat-ui-query-token-field"');
+    expect(html).toContain('data-hz-query-token-field-owner="hertzbeat-ui-query-token-field"');
+    expect(html).toContain('data-trace-manage-query-status-select-owner="hertzbeat-ui-query-status-select"');
+    expect(html).toContain('data-hz-query-status-select-owner="hertzbeat-ui-query-status-select"');
+    expect(html).toContain('data-trace-manage-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-query-action-group-owner="hertzbeat-ui-query-action-group"');
     expect(html).toContain('data-trace-manage-chart-band="cold-chart-band"');
+    expect(html).toContain('data-trace-manage-table-detail-layout="shared-table-detail"');
+    expect(html).toContain('data-trace-manage-table-detail-layout-owner="hertzbeat-ui-workbench-layout"');
     expect(html).toContain('data-trace-manage-trace-table="cold-dense-trace-list"');
+    expect(html).toContain('data-trace-manage-table-chrome-owner="hertzbeat-ui-data-table"');
     expect(html).toContain('data-trace-manage-detail-panel="cold-detail-panel"');
-  });
+    expect(html).toContain('data-trace-manage-detail-footer-layout="shared-detail-footer"');
+    expect(html).toContain('data-trace-manage-detail-footer-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="detail-footer"');
+    expect(html).toContain('data-trace-manage-detail-footer-action-icon="entity"');
+    expect(html).toContain('data-trace-manage-detail-footer-action-icon="alerts"');
+    expect(html).toContain('data-trace-manage-detail-footer-action-icon="logs"');
+    expect(html).toContain('data-trace-manage-detail-footer-action-icon="metrics"');
+    expect(html).toContain('data-trace-manage-detail-footer-action-icon="entities"');
+    expect(html).toContain('data-trace-manage-detail-footer-action-icon-owner="hertzbeat-ui-button-icon"');
+  }, 15000);
 
   it('uses the shared narrow time rail in the top-right trace header with inherited route state visible', async () => {
     mockState.searchParams = new URLSearchParams(
       'traceId=trace-123&serviceName=checkout&timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=Asia%2FShanghai'
     );
-    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
 
-    const { default: TraceManagePage } = await import('./page');
-    const html = renderToStaticMarkup(<TraceManagePage />);
+    const html = renderTraceManagePage();
 
     expect(source).toContain("import { buildTimeRangeControlLabels, TimeRangeControl } from '@/components/observability/time-range-control';");
     expect(source).toContain('labels={buildTimeRangeControlLabels(t)}');
-    expect(source).toContain('buildTraceManageRoute(searchParams, query, appliedContext)');
+    expect(source).toContain('buildTraceManageRoute(routeContext, query, appliedContext)');
     expect(source).toContain('const traceTimeRefreshKey = useMemo(');
     expect(source).toContain('traceTimeContext.timeRange');
     expect(source).toContain('traceTimeContext.refresh');
@@ -358,25 +562,39 @@ describe('trace manage page', () => {
     expect(source).toContain('traceTimeContext.tz');
     expect(source).toContain('`trace-manage:${traceUrls.listUrl}|${traceUrls.overviewUrl}|${traceTimeRefreshKey}`');
     expect(source).toContain('data-trace-manage-time-toolbar="top-right-corner"');
+    expect(source).toContain('data-trace-manage-time-toolbar-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="time-toolbar"');
     expect(source).toContain('data-trace-manage-time-control="shared-time-context-control"');
+    expect(source).toContain('data-trace-manage-time-control-owner="hertzbeat-ui-control-stack"');
+    expect(source).toContain('layout="end-inline"');
     expect(source).toContain('data-trace-manage-time-control-placement="top-right"');
     expect(source).toContain('data-trace-manage-time-control-visual="narrow-top-right-rail"');
     expect(source).toContain('data-trace-manage-time-control-fit="no-clipping"');
     expect(source).toContain('variant="narrow-rail"');
+    expect(source).toContain('data-trace-manage-time-range-control-owner="hertzbeat-shared-time-range-control"');
     expect(source).toContain('showAbsoluteFields');
     expect(source).not.toContain('data-trace-manage-time-control="shared-time-context-control" className="min-w-[520px] flex-1"');
+    expect(source).not.toContain('className="flex max-w-full justify-end"');
+    expect(source).not.toContain('className="justify-end"');
+    expect(source).not.toContain('className="ml-auto grid w-full min-w-0 max-w-[1120px] gap-2 xl:w-auto"');
     expect(source.indexOf('data-trace-manage-time-control="shared-time-context-control"')).toBeLessThan(
       source.indexOf('data-trace-manage-query-bar="cold-query-row"')
     );
     expect(html).toContain('data-trace-manage-time-toolbar="top-right-corner"');
+    expect(html).toContain('data-trace-manage-time-toolbar-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="time-toolbar"');
     expect(html).toContain('data-trace-manage-time-control="shared-time-context-control"');
+    expect(html).toContain('data-trace-manage-time-control-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-control-stack-layout="end-inline"');
     expect(html).toContain('data-trace-manage-time-control-placement="top-right"');
     expect(html).toContain('data-trace-manage-time-control-visual="narrow-top-right-rail"');
     expect(html).toContain('data-trace-manage-time-control-fit="no-clipping"');
     expect(html).toContain('data-time-range-control="hertzbeat-shared"');
     expect(html).toContain('data-time-range-control-visual="grafana-like-narrow-rail"');
     expect(html).toContain('data-time-range-control-layout="nowrap-top-right-rail"');
+    expect(html).toContain('data-time-range-control-align="end"');
     expect(html).toContain('data-time-range-control-overflow="fit-without-scroll"');
+    expect(html).toContain('data-trace-manage-time-range-control-owner="hertzbeat-shared-time-range-control"');
     expect(html).toContain('data-time-range-live-toggle="paused"');
     expect(html).toContain('data-trace-manage-time-refresh-action="true"');
     expect(html).toContain('value="last-1h"');
@@ -387,11 +605,10 @@ describe('trace manage page', () => {
       'entityId=7&entityName=checkout&returnTo=%2Foverview&returnLabel=Overview&traceId=trace-123&spanId=span-456&serviceName=checkout&serviceNamespace=payments&environment=prod&start=1713200000000&end=1713203600000'
     );
 
-    const { default: TraceManagePage } = await import('./page');
-    const html = renderToStaticMarkup(<TraceManagePage />);
+    const html = renderTraceManagePage();
 
     expect(html).toContain('href="/overview"');
-    expect(html).toContain('返回来源');
+    expect(html).toContain('Return to source');
     expect(html).not.toContain('Overview');
     expect(html).toContain('data-trace-manage-return-action="true"');
     expect(html).toContain('/log/manage?traceId=trace-123');
@@ -405,12 +622,13 @@ describe('trace manage page', () => {
   }, 15000);
 
   it('does not keep the old Angular or external-product workbench owners inside the cold trace workbench', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
 
     expect(source).not.toContain('ThreeSignalDeskShell');
     expect(source).not.toContain('FactsStrip');
     expect(source).not.toContain('StageSection');
-    expect(source).not.toContain('DataTable');
+    expect(source).not.toContain("components/observability/data-table");
+    expect(source).not.toContain('<DataTable');
     expect(source).not.toContain('DrawerSection');
     expect(source).not.toContain('signoz-');
     expect(source).not.toContain('data-trace-manage-floating-actions');
@@ -418,50 +636,187 @@ describe('trace manage page', () => {
     expect(source).not.toContain('创建告警');
     expect(source).not.toContain('加入仪表盘');
     expect(source).toContain('data-trace-manage-entity-context="hertzbeat-signal-entity-context"');
+    expect(source).toContain('data-trace-manage-entity-context-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('data-trace-manage-selected-evidence-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('data-trace-manage-detail-facts-owner="hertzbeat-ui-detail-rows"');
     expect(source).toContain('buildTraceAttributionDiagnostics(null, null, routeContext, t)');
     expect(source).toContain('buildTraceAttributionDiagnostics(detail, selectedSpan, handoffRouteContext, t)');
     expect(source).toContain('data-trace-manage-attribution-diagnostics="hertzbeat-attribute-diagnostics"');
-    expect(source).toContain('data-trace-manage-attribution-diagnostic-state={row.state}');
-    expect(source).toContain('归因诊断');
+    expect(source).toContain('HzAttributeDiagnostics');
+    expect(source).toContain('data-trace-manage-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(source).toContain("'data-trace-manage-attribution-diagnostic-state': row.state");
+    expect(source).toContain("t('trace.manage.attribution-diagnostics.title')");
+    expect(source).toContain("t('trace.manage.attribution-diagnostics.present')");
+    expect(source).toContain("t('trace.manage.attribution-diagnostics.missing')");
     expect(source).toContain('hertzbeat.entity_id');
-    expect(source).toContain('实体上下文');
-    expect(source).toContain('aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"');
+    expect(source).toContain("t('trace.manage.route.entity-context.title')");
+    expect(source).toContain("aria-label={t('trace.manage.route.entity-context.aria')}");
     expect(source).not.toContain('data-trace-manage-hertzbeat-loop="collector-template-alert-loop"');
     expect(source).not.toContain('HertzBeat 采集闭环');
     expect(source).not.toContain('接入质量');
     expect(source).not.toContain('链路查询继续保留接入质量、采集节点、模板归属和告警闭环上下文。');
-    expect(source).toContain('采集集群');
-    expect(source).toContain('监控模板');
-    expect(source).toContain('采集来源');
-    expect(source).toContain('实体详情');
-    expect(source).toContain('告警处理');
+    expect(source).toContain("t('trace.manage.route.action.collectors')");
+    expect(source).toContain("t('trace.manage.route.action.templates')");
+    expect(source).toContain("t('trace.manage.route.action.entity')");
+    expect(source).toContain("t('trace.manage.route.action.alerts')");
+    expect(source).toContain('HzActionGroup');
+    expect(source).toContain('data-trace-manage-action-row="cold-workbench-actions"');
+    expect(source).toContain('data-trace-manage-action-row-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-trace-manage-action-row-layout-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('layout="full-end"');
+    expect(source).toContain('data-trace-manage-header-layout="shared-header-actions"');
+    expect(source).toContain('data-trace-manage-header-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="header-actions"');
+    expect(source).toContain('HzWorkbenchHeaderCopy');
+    expect(source).toContain('data-trace-manage-header-copy="shared-workbench-header-copy"');
+    expect(source).toContain('data-trace-manage-header-copy-owner="hertzbeat-ui-workbench-header-copy"');
+    expect(source).toContain("eyebrow={t('trace.manage.route.kicker')}");
+    expect(source).toContain("title={t('trace.manage.route.title')}");
+    expect(source).toContain("copy={t('trace.manage.route.subtitle')}");
+    expect(source).toContain('data-trace-manage-time-toolbar-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="time-toolbar"');
+    expect(source).toContain('data-trace-manage-time-control-owner="hertzbeat-ui-control-stack"');
+    expect(source).toContain('layout="end-inline"');
+    expect(source).toContain('data-trace-manage-time-range-control-owner="hertzbeat-shared-time-range-control"');
+    expect(source).not.toContain('className="justify-end"');
+    expect(source).toContain('data-trace-manage-header-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(source).toContain('data-trace-manage-header-action-icon="collector"');
+    expect(source).toContain('data-trace-manage-header-action-icon="templates"');
+    expect(source).not.toContain('<Server className="h-4 w-4" aria-hidden="true" />');
+    expect(source).not.toContain('<ListChecks className="h-4 w-4" aria-hidden="true" />');
+    expect(source).toContain('data-trace-manage-query-search-input-owner="hertzbeat-ui-input"');
+    expect(source).toContain('data-trace-manage-query-search-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(source).toContain('inset="search-icon"');
+    expect(source).toContain('data-trace-manage-query-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(source).not.toContain('<Play className="h-4 w-4" aria-hidden="true" />');
+    expect(source).not.toContain('<RotateCcw className="h-4 w-4" aria-hidden="true" />');
+    expect(source).not.toContain('className="w-full pl-9"');
+    expect(source).not.toContain('icon={<Search className="h-4 w-4" aria-hidden="true" />}');
+    expect(source).not.toContain('className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-start"');
+    expect(source).not.toContain('<div className="min-w-0">');
+    expect(source).not.toContain('<p className="mb-2 text-[12px] font-semibold tracking-[0.08em] text-[#8792a5]">');
+    expect(source).not.toContain('<h1 className="text-[30px] font-semibold tracking-normal text-[#f4f7fb]">');
+    expect(source).not.toContain('<p className="mt-3 max-w-[820px] text-[13px] leading-6 text-[#9ca7ba]">');
+    expect(source).not.toContain('className="ml-auto grid w-full min-w-0 max-w-[1120px] gap-2 xl:w-auto"');
+    expect(source).not.toContain('className="flex max-w-full justify-end"');
     expect(source).toContain('href={handoffLinks.alertHandlingHref}');
     expect(source).toContain('data-trace-manage-alert-context-hint="entity-trace-alert-handoff"');
-    expect(source).toContain('按当前实体、服务和已带入的链路上下文查看相关告警');
+    expect(source).toContain("t('trace.manage.route.handoff.alert-hint')");
     expect(source).toContain('data-trace-manage-empty-guidance="operator-no-data-guidance"');
-    expect(source).toContain('确认时间范围、实体归因、采集器和监控模板后再查看链路。');
+    expect(source).toContain('data-trace-manage-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(source).toContain('layout="table-panel"');
+    expect(source).not.toContain('className="mx-auto h-[260px] max-w-[420px] border-y-0 text-left"');
+    expect(source).toContain('data-trace-manage-table-chrome-owner="hertzbeat-ui-data-table"');
+    expect(source).toContain('HzDataTable');
+    expect(source).toContain('HzWorkbenchLayout');
+    expect(source).toContain('data-trace-manage-chart-layout="shared-summary-trend"');
+    expect(source).toContain('data-trace-manage-chart-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="summary-trend"');
+    expect(source).toContain('data-trace-manage-detail-body-layout="shared-detail-stack"');
+    expect(source).toContain('data-trace-manage-detail-body-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="detail-stack"');
+    expect(source).toContain('data-trace-manage-detail-footer-layout="shared-detail-footer"');
+    expect(source).toContain('data-trace-manage-detail-footer-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('variant="detail-footer"');
+    expect(source).toContain('data-trace-manage-detail-footer-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(source).toContain('data-trace-manage-detail-footer-action-icon="metrics"');
+    expect(source).toContain('data-trace-manage-detail-footer-action-icon="entities"');
+    expect(source).toContain('variant="table-detail"');
+    expect(source).toContain('data-trace-manage-table-detail-layout="shared-table-detail"');
+    expect(source).toContain('data-trace-manage-table-detail-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(source).toContain('HzPanelHeader');
+    expect(source).toContain('data-trace-manage-table-header-owner="hertzbeat-ui-panel-header"');
+    expect(source).toContain('data-trace-manage-table-count-badge-owner="hertzbeat-ui-status-badge"');
+    expect(source).toContain('data-trace-manage-detail-header-owner="hertzbeat-ui-panel-header"');
+    expect(source).toContain("eyebrow={t('trace.manage.route.detail.title')}");
+    expect(source).toContain('HzEmptyState');
+    expect(source).toContain("t('trace.manage.route.empty.copy')");
     expect(source).not.toContain('运行查询后会在这里展示最近链路。');
     expect(source).not.toContain('告警规则');
+    expect(source).not.toContain('className="flex flex-wrap items-center justify-end gap-2"');
+    expect(source).not.toContain('className="grid gap-4 2xl:grid-cols-[minmax(0,1fr)_330px]"');
+    expect(source).not.toContain('className="grid gap-3 lg:grid-cols-[repeat(4,minmax(0,160px))_minmax(0,1fr)]"');
+    expect(source).not.toContain('className="space-y-3 px-4 py-4"');
+    expect(source).not.toContain('className="grid gap-2 border-t border-[#252b35] px-4 py-4"');
+    expect(source).toContain('HzTableRowActionButton');
+    expect(source).toContain('data-trace-manage-row-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('width="root-span"');
+    expect(source).not.toContain('className="max-w-[240px] justify-start truncate font-semibold"');
     expect(source).toContain('data-trace-manage-route="otlp-cold-trace-workbench"');
+    expect(source).toContain('HzSignalWorkbenchShell');
+    expect(source).toContain('data-trace-manage-shell-owner="hertzbeat-ui-signal-workbench-shell"');
+    expect(source).not.toContain('className="min-h-[calc(100vh-56px)] bg-[#07090b] text-[#e8edf5]"');
+    expect(source).not.toContain('<div className="mx-auto flex w-full max-w-[1600px] flex-col gap-4 px-6 py-6">');
   });
 
   it('opens trace details in a side waterfall modal instead of the inline route-level demo span band', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
 
     expect(source).toContain('TraceWaterfallDrawer');
     expect(source).toContain('loadTraceDetailBundle');
     expect(source).toContain('openTraceDetailDrawer');
+    expect(source).toContain('function buildSelectedTraceReturnHref(');
+    expect(source).toContain(
+      'const selectedTraceReturnHref = buildSelectedTraceReturnHref(currentTraceReturnHref, detail, selectedSpan, handoffRouteContext);'
+    );
+    expect(source).toContain('logsReturnTo: selectedTraceReturnHref');
+    expect(source).toContain('metricsReturnTo: selectedTraceReturnHref');
     expect(source).toContain('data-trace-manage-open-detail-action="side-waterfall-modal"');
     expect(source).toContain('data-trace-manage-detail-drawer="waterfall-side-modal"');
+    expect(source).toContain('data-trace-manage-detail-drawer-owner="hertzbeat-ui-dialog-body-layout"');
+    expect(source).toContain('data-trace-manage-drawer-body-layout="shared-waterfall-detail"');
+    expect(source).toContain('data-trace-manage-drawer-body-layout-owner="hertzbeat-ui-dialog-body-layout"');
+    expect(source).toContain('data-trace-manage-drawer-side-stack="selected-facts"');
+    expect(source).toContain('data-trace-manage-drawer-side-stack-owner="hertzbeat-ui-dialog-body-layout"');
+    expect(source).toContain('HzDialogBodyLayout');
+    expect(source).toContain('variant="waterfall-detail"');
+    expect(source).toContain('variant="side-stack"');
+    expect(source).toContain('data-trace-manage-drawer-meta="shared-chip-group"');
+    expect(source).toContain('data-trace-manage-drawer-meta-owner="hertzbeat-ui-toolbar-chips"');
+    expect(source).toContain('data-trace-manage-drawer-meta-item="trace-id"');
+    expect(source).toContain('data-trace-manage-drawer-meta-item="span-count"');
+    expect(source).toContain('data-trace-manage-drawer-meta-item="duration"');
+    expect(source).toContain('data-trace-manage-drawer-meta-item-owner="hertzbeat-ui-dialog-meta-item"');
+    expect(source).toContain('HzChipGroup');
+    expect(source).toContain('data-trace-manage-drawer-action-group-layout-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('layout="full-end"');
+    expect(source).toContain('HzDialogMetaItem');
+    expect(source).not.toContain('className="h-6 max-w-[320px] font-mono text-[11px]"');
+    expect(source).not.toContain('className="h-6 max-w-[180px] text-[11px]"');
+    expect(source).not.toContain('className="h-6 max-w-[160px] text-[11px]"');
+    expect(source).toContain('data-trace-manage-drawer-action-group="handoff-actions"');
+    expect(source).toContain('data-trace-manage-drawer-action-group-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-trace-manage-drawer-stage-stats="shared-stat-strip"');
+    expect(source).toContain('data-trace-manage-drawer-stage-stats-owner="hertzbeat-ui-stat-strip"');
+    expect(source).toContain('HzStatStrip');
+    expect(source).toContain('data-trace-manage-drawer-stage-stat-owner="hertzbeat-ui-stat-cell"');
+    expect(source).toContain('data-trace-manage-drawer-stage-stat={item.id}');
+    expect(source).toContain('HzStatCell');
+    expect(source).toContain('density="compact"');
+    expect(source).not.toContain('className="min-h-[64px]"');
+    expect(source).toContain('data-trace-manage-drawer-selected-facts-owner="hertzbeat-ui-detail-rows"');
     expect(source).toContain('ObservabilityWaterfall');
     expect(source).toContain('autoOpenedTraceDetailKeyRef');
     expect(source).toContain('requestedTraceId');
     expect(source).toContain('const traceEventCount = waterfallRows.reduce');
-    expect(source).toContain("['事件', String(traceEventCount)]");
-    expect(source).toContain('title: `关联 · ${row.title}`');
+    expect(source).toContain("t('trace.manage.drawer.stat.events')");
+    expect(source).toContain("t('trace.manage.drawer.link-prefix')");
+    expect(source).toContain("t('trace.manage.drawer.event-prefix')");
     expect(source).toContain('data-trace-manage-event-detail-copy="span-event-not-span"');
-    expect(source).toContain('不是新的跨度,是当前跨度上的时间点');
-    expect(source).toContain('个跨度');
+    expect(source).toContain('data-trace-manage-event-detail-owner="hertzbeat-ui-dialog-event-notice"');
+    expect(source).toContain('HzDialogEventNotice');
+    expect(source).toContain('HzDialogEventText');
+    expect(source).toContain('data-trace-manage-event-detail-text-owner="hertzbeat-ui-dialog-event-text"');
+    expect(source).not.toContain('className="border-x-0 border-b border-t-0 bg-transparent px-0 pb-2 pt-0"');
+    expect(source).not.toContain('<span data-trace-manage-event-detail-copy="span-event-not-span">');
+    expect(source).not.toContain('<span data-trace-manage-event-detail-meta="span-event-label">');
+    expect(source).toContain('data-trace-manage-event-detail-meta="span-event-label"');
+    expect(source).toContain('variant="hint"');
+    expect(source).toContain("t('trace.manage.drawer.event-detail.copy')");
+    expect(source).toContain("t('trace.manage.drawer.span-count'");
+    expect(source).toContain("t('trace.manage.drawer.waterfall.span')");
+    expect(source).toContain("t('trace.manage.drawer.waterfall.duration')");
+    expect(source).toContain("t('trace.manage.drawer.waterfall.timeline')");
     expect(source).not.toContain('个 Span');
     expect(source).not.toContain("['事件', String((selectedSpan?.events || []).length)]");
     expect(source).not.toContain('Link ·');
@@ -469,10 +824,19 @@ describe('trace manage page', () => {
     expect(source).not.toContain('data-trace-manage-span-band="cold-span-band"');
     expect(source).not.toContain("tone: 'bg-[#4566e8]'");
     expect(source).not.toContain("tone: 'bg-[#6b85d7]'");
+    expect(source).not.toContain('className="grid gap-4 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,.75fr)]"');
+    expect(source).not.toContain('className="grid gap-3"');
+    expect(source).not.toContain('className="flex flex-wrap items-center gap-2 text-[11px] text-[var(--ops-text-secondary)]"');
+    expect(source).not.toContain('className="flex flex-wrap justify-end gap-2"');
+    expect(source).not.toContain('className="w-full justify-end"');
+    expect(source).not.toContain('className="overflow-hidden"');
+    expect(source).not.toContain('data-trace-manage-header="cold-compact-header" data-trace-manage-panel-surface="header" className="px-5 py-4"');
+    expect(source).not.toContain('data-trace-manage-query-bar="cold-query-row" data-trace-manage-panel-surface="query" className="px-4 py-3"');
+    expect(source).not.toContain('data-trace-manage-chart-band="cold-chart-band" data-trace-manage-panel-surface="chart" className="px-4 py-4"');
   });
 
   it('keeps missing-id trace handoffs visible but disabled with i18n hover guidance', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
     const messages = readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8');
 
     expect(source).toContain("const missingTraceHandoffTitle = t('trace.manage.handoff.logs-disabled')");
@@ -480,16 +844,26 @@ describe('trace manage page', () => {
     expect(source).toContain('data-trace-manage-open-logs-action-disabled="missing-trace-id"');
     expect(source).toContain('data-trace-manage-entity-action-disabled="missing-entity-id"');
     expect(source).toContain("const canOpenEntity = handoffLinks.entityHref.startsWith('/entities/');");
+    expect(source).toContain('HzDisabledActionShell');
+    expect(source).toContain('data-trace-manage-disabled-action-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(source).toContain('data-trace-manage-disabled-action-scope="header"');
+    expect(source).toContain('data-trace-manage-disabled-action-scope="drawer-footer"');
+    expect(source).toContain('data-trace-manage-disabled-action-scope="detail-footer"');
     expect(source).toContain('title={missingTraceHandoffTitle}');
     expect(source).toContain('title={missingEntityHandoffTitle}');
+    expect(source).not.toContain('<span className="inline-flex" title={missingTraceHandoffTitle}>');
+    expect(source).not.toContain('<span className="inline-flex" title={missingEntityHandoffTitle}>');
     expect(messages).toContain("'trace.manage.handoff.logs-disabled'");
     expect(messages).toContain("'trace.manage.handoff.entity-disabled'");
   });
 
   it('keeps route-level actions on the cold-matte palette instead of bright blue demo buttons', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
 
     expect(source).toContain('data-trace-manage-search-action="true"');
+    expect(source).toContain('HzControlStack');
+    expect(source).toContain('data-trace-manage-query-control-stack="shared-inline-controls"');
+    expect(source).toContain('data-trace-manage-query-control-stack-owner="hertzbeat-ui-control-stack"');
     expect(source).not.toContain('border-[#4f6df0]');
     expect(source).not.toContain('bg-[#4566e8]');
     expect(source).not.toContain('hover:bg-[#5574f4]');
diff --git a/web-next/app/trace/manage/page.tsx b/web-next/app/trace/manage/page.tsx
index c60e328f17..afacfd6649 100644
--- a/web-next/app/trace/manage/page.tsx
+++ b/web-next/app/trace/manage/page.tsx
@@ -1,990 +1,13 @@
-'use client';
+import React from 'react';
+import TraceManagePage from './trace-manage-page';
+import { readTraceManageRouteState, type TraceManageSearchParams } from '@/lib/trace-manage/query-state';
 
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import Link from 'next/link';
-import { BarChart3, BellRing, GitBranch, ListChecks, Play, RotateCcw, Search, Server, Timer, Workflow } from 'lucide-react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { buildTimeRangeControlLabels, TimeRangeControl } from '@/components/observability/time-range-control';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { Input } from '@/components/ui/input';
-import { Select } from '@/components/ui/select';
-import { apiMessageGet } from '@/lib/api-client';
-import { formatDurationNanos, formatTime } from '@/lib/format';
-import { buildTraceUrls, queryStateFromParams, type TraceQueryState } from '@/lib/trace-manage/query-state';
-import {
-  buildSelectedSpanEventRows,
-  buildSelectedSpanFacts,
-  buildSelectedSpanLinkRows,
-  buildTraceAttributionDiagnostics,
-  buildTraceExplorerRows,
-  buildTraceHandoffLinks,
-  buildTraceWaterfallRows
-} from '@/lib/trace-manage/view-model';
-import { buildSignalEntityContextRows, readEpochMillisRouteParam, readSignalRouteContext, type SignalRouteContext } from '@/lib/signal-route-context';
-import { resolveAppliedTimeContext, sanitizeTimeContext, type TimeContext } from '@/lib/time-context';
-import type { PageResult, TraceDetail, TraceListItem, TraceOverview } from '@/lib/types';
-import { ObservabilityStatusState, ObservabilityWaterfall, type ObservabilityWaterfallTick } from '../../../components/observability';
-import { OverlayDialog } from '../../../components/workbench/overlay-dialog';
-import { RowList } from '../../../components/workbench/workbench-page';
-import { loadTraceDetailBundle } from '../../../lib/trace-manage/controller';
-import { buildResetTraceManageRoute, buildTraceManageRoute } from './route-state';
-
-type TraceManageData = {
-  overview: TraceOverview;
-  list: PageResult<TraceListItem>;
-};
-
-const emptyTraceQuery: TraceQueryState = {
-  traceId: '',
-  spanId: '',
-  serviceName: '',
-  errorOnly: false
-};
-
-type TraceDetailDrawerState = {
-  open: boolean;
-  loading: boolean;
-  error?: string | null;
-  detail: TraceDetail | null;
-  selectedSpanId?: string | null;
-  selectedEventKey?: string | null;
-};
-
-const actionClass =
-  'inline-flex h-8 items-center justify-center gap-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe3ee] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] transition-colors hover:border-[#3b4454] hover:bg-[#151821]';
-const primaryActionClass =
-  'inline-flex h-8 items-center justify-center gap-2 rounded-[3px] border border-[#3b4454] bg-[#18202c] px-4 text-[12px] font-semibold text-[#f2f6fb] shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] transition-colors hover:bg-[#202938]';
-const disabledActionClass =
-  'inline-flex h-8 cursor-not-allowed items-center justify-center gap-2 rounded-[3px] border border-[#242a34] bg-[#0b0e13] px-3 text-[12px] font-semibold text-[#687386] opacity-80 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]';
-const panelClass = 'rounded-[4px] border border-[#252b35] bg-[#0d1015] shadow-[0_18px_60px_rgba(0,0,0,0.28)]';
-const inputClass =
-  'h-8 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#e6edf7] outline-none transition-colors placeholder:text-[#697386] focus:border-[#4e74f8] focus:ring-2 focus:ring-[rgba(78,116,248,0.12)]';
-
-function hasNavigationId(value?: string | null) {
-  const trimmed = value?.trim();
-  return Boolean(trimmed && trimmed !== '-');
-}
-
-function firstNavigationId(...values: Array<string | null | undefined>) {
-  return values.find(hasNavigationId);
-}
-
-function readTraceAttribute(source: Record<string, string> | undefined, key: string) {
-  return firstNavigationId(source?.[key]);
-}
-
-function updateDraftField(field: keyof TraceQueryState, value: string | boolean) {
-  return (previous: TraceQueryState): TraceQueryState => ({
-    ...previous,
-    [field]: value
-  });
-}
-
-function buildTraceReturnHref(searchParams: URLSearchParams, query: TraceQueryState) {
-  return buildTraceManageRoute(searchParams, query);
-}
-
-function mergeTraceDetailSpans(detail: TraceDetail, spans: TraceDetail['spans']): TraceDetail {
-  return {
-    ...detail,
-    spans: spans.length > 0 ? spans : detail.spans || []
-  };
-}
-
-function buildTraceTimelineTicks(durationNanos?: number | null): ObservabilityWaterfallTick[] {
-  const totalNanos = Math.max(Number(durationNanos || 0), 1_000_000);
-  return [0, 25, 50, 75, 100].map(percent => ({
-    percent,
-    label: percent === 0 ? '0 ms' : formatDurationNanos((totalNanos * percent) / 100)
-  }));
-}
-
-function hasDisplayReturnLabel(searchParams: { get(name: string): string | null }) {
-  const returnTo = searchParams.get('returnTo') || '';
-  return Boolean(searchParams.get('returnLabel') || returnTo.includes('returnLabel='));
-}
-
-function hasUnsanitizedTraceTimeBounds(searchParams: { get(name: string): string | null }) {
-  const start = searchParams.get('start');
-  const end = searchParams.get('end');
-  return Boolean(
-    (start && readEpochMillisRouteParam(start) !== start.trim()) ||
-      (end && readEpochMillisRouteParam(end) !== end.trim())
-  );
-}
-
-function statusTone(status: string) {
-  const normalized = status.toUpperCase();
-  if (normalized.includes('ERROR')) return 'border-[#70424a] bg-[#231116] text-[#f39aa8]';
-  if (normalized.includes('OK')) return 'border-[#315b49] bg-[#0f211b] text-[#8bd8ad]';
-  return 'border-[#303642] bg-[#151821] text-[#d7dce6]';
-}
-
-type TraceAttributionDiagnosticRow = ReturnType<typeof buildTraceAttributionDiagnostics>[number];
-
-function TraceAttributionDiagnostics({ rows }: { rows: TraceAttributionDiagnosticRow[] }) {
-  if (rows.length === 0) return null;
-
-  return (
-    <div
-      data-trace-manage-attribution-diagnostics="hertzbeat-attribute-diagnostics"
-      aria-label="归因诊断 hertzbeat.entity_id hertzbeat.entity_name hertzbeat.workspace_id hertzbeat.collector hertzbeat.template"
-      className="rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3"
-    >
-      <div className="mb-2 flex items-center justify-between gap-2">
-        <p className="text-[11px] font-semibold text-[#8792a5]">归因诊断</p>
-        <span className="text-[11px] font-semibold text-[#6d7788]">hertzbeat.*</span>
-      </div>
-      <div className="space-y-2">
-        {rows.map(row => (
-          <div
-            key={row.key}
-            data-trace-manage-attribution-diagnostic-state={row.state}
-            className="grid grid-cols-[minmax(0,1fr)_52px] gap-2 text-[11px]"
-          >
-            <span className="min-w-0">
-              <span className="block truncate font-mono font-semibold text-[#e6edf7]">{row.label}</span>
-              <span className="block truncate text-[#6d7788]">
-                {row.value} · {row.meta}
-              </span>
-            </span>
-            <span
-              className={`inline-flex h-5 items-center justify-center rounded-[3px] border px-1.5 font-semibold ${
-                row.state === 'present' ? 'border-[#24543b] bg-[#0d241a] text-[#76d69b]' : 'border-[#4a3140] bg-[#24111a] text-[#d68aa6]'
-              }`}
-            >
-              {row.state === 'present' ? '已提供' : '缺失'}
-            </span>
-          </div>
-        ))}
-      </div>
-    </div>
-  );
-}
-
-type TraceWaterfallRow = ReturnType<typeof buildTraceWaterfallRows>[number];
-type TraceWaterfallEventSelection = {
-  row: TraceWaterfallRow;
-  event: NonNullable<TraceWaterfallRow['events']>[number];
-};
-
-function findTraceWaterfallEvent(rows: TraceWaterfallRow[], eventKey?: string | null): TraceWaterfallEventSelection | null {
-  if (!eventKey) return null;
-  for (const row of rows) {
-    const event = row.events?.find(item => item.key === eventKey);
-    if (event) return { row, event };
-  }
-  return null;
-}
-
-function buildTraceWaterfallEventFacts(selection: TraceWaterfallEventSelection) {
-  const { row, event } = selection;
-  return [
-    {
-      title: '跨度事件',
-      copy: event.label || '未命名事件',
-      meta: '不是新的跨度,是当前跨度上的时间点'
-    },
-    {
-      title: '所属跨度',
-      copy: row.title || row.key,
-      meta: row.detailLabel || row.copy
-    },
-    {
-      title: '事件位置',
-      copy: event.offsetLabel || `${Math.round(event.leftPct)}%`,
-      meta: '相对链路时间轴'
-    },
-    {
-      title: '事件属性',
-      copy: event.attributesLabel || '无属性',
-      meta: 'attributes'
-    }
-  ];
-}
-
-function chartHeight(index: number, total: number, isError: boolean) {
-  const base = 26 + ((index * 17 + total * 9) % 48);
-  return `${Math.min(86, base + (isError ? 14 : 0))}%`;
-}
-
-function TraceWaterfallDrawer({
-  state,
-  routeContext,
-  currentTraceReturnHref,
-  onClose,
-  onSelectSpan,
-  onSelectEvent
+export default async function TraceManageRoutePage({
+  searchParams
 }: {
-  state: TraceDetailDrawerState;
-  routeContext: SignalRouteContext;
-  currentTraceReturnHref: string;
-  onClose: () => void;
-  onSelectSpan: (spanId: string) => void;
-  onSelectEvent: (eventKey: string | null, spanId?: string) => void;
-}) {
-  const { t } = useI18n();
-  const detail = state.detail;
-  const selectedSpan = detail?.spans.find(span => span.spanId === state.selectedSpanId) || detail?.spans[0] || null;
-  const detailEntityId = firstNavigationId(
-    routeContext.entityId,
-    readTraceAttribute(detail?.resourceAttributes, 'hertzbeat.entity_id'),
-    readTraceAttribute(selectedSpan?.resourceAttributes, 'hertzbeat.entity_id')
-  );
-  const handoffRouteContext = detailEntityId && !routeContext.entityId ? { ...routeContext, entityId: detailEntityId } : routeContext;
-  const attributionDiagnostics = buildTraceAttributionDiagnostics(detail, selectedSpan, handoffRouteContext, t);
-  const waterfallRows = buildTraceWaterfallRows(detail, selectedSpan?.spanId || state.selectedSpanId, formatDurationNanos);
-  const selectedSpanFacts = buildSelectedSpanFacts(selectedSpan, detail, t, formatDurationNanos);
-  const selectedSpanEvents = buildSelectedSpanEventRows(selectedSpan, formatTime);
-  const selectedSpanLinks = buildSelectedSpanLinkRows(selectedSpan);
-  const selectedTraceEvent = findTraceWaterfallEvent(waterfallRows, state.selectedEventKey);
-  const handoffLinks = buildTraceHandoffLinks(detail, selectedSpan, handoffRouteContext, {
-    intakeReturnTo: currentTraceReturnHref,
-    logsReturnTo: currentTraceReturnHref
-  });
-  const traceEventCount = waterfallRows.reduce((count, row) => count + (row.events?.length || 0), 0);
-  const missingTraceHandoffTitle = t('trace.manage.handoff.logs-disabled');
-  const missingEntityHandoffTitle = t('trace.manage.handoff.entity-disabled');
-  const canOpenLogs = hasNavigationId(detail?.traceId);
-  const canOpenEntity = handoffLinks.entityHref.startsWith('/entities/');
-  const selectedFacts = selectedTraceEvent ? buildTraceWaterfallEventFacts(selectedTraceEvent) : [
-    ...selectedSpanFacts,
-    ...selectedSpanEvents.map(row => ({ ...row, title: `事件 · ${row.title}` })),
-    ...selectedSpanLinks.map(row => ({ ...row, title: `关联 · ${row.title}` }))
-  ];
-  const showSelectedSpan = () => onSelectEvent(null, selectedTraceEvent?.row.key);
-
-  return (
-    <OverlayDialog
-      open={state.open}
-      onClose={onClose}
-      placement="right"
-      maxWidthClassName="max-w-[1120px]"
-      kicker="链路详情"
-      title={selectedSpan?.spanName || detail?.rootSpanName || detail?.traceId || '链路瀑布'}
-      footer={
-        <div className="flex flex-wrap justify-end gap-2">
-          {canOpenLogs ? (
-            <Link data-trace-manage-open-logs-action="true" href={handoffLinks.logsHref} className={actionClass}>
-              查看日志
-            </Link>
-          ) : (
-            <span className="inline-flex" title={missingTraceHandoffTitle}>
-              <button
-                data-trace-manage-open-logs-action="true"
-                data-trace-manage-open-logs-action-disabled="missing-trace-id"
-                type="button"
-                className={disabledActionClass}
-                disabled
-                title={missingTraceHandoffTitle}
-                aria-label={missingTraceHandoffTitle}
-              >
-                查看日志
-              </button>
-            </span>
-          )}
-          <Link href={handoffLinks.metricsHref} className={actionClass}>
-            查看指标
-          </Link>
-          {canOpenEntity ? (
-            <Link href={handoffLinks.entityHref} className={actionClass}>
-              实体详情
-            </Link>
-          ) : (
-            <span className="inline-flex" title={missingEntityHandoffTitle}>
-              <button
-                data-trace-manage-entity-action-disabled="missing-entity-id"
-                type="button"
-                className={disabledActionClass}
-                disabled
-                title={missingEntityHandoffTitle}
-                aria-label={missingEntityHandoffTitle}
-              >
-                实体详情
-              </button>
-            </span>
-          )}
-        </div>
-      }
-    >
-      <div data-trace-manage-detail-drawer="waterfall-side-modal" className="grid gap-4">
-        {state.loading ? (
-          <ObservabilityStatusState title="正在加载链路详情" copy="正在加载该链路的 Span、事件、关联关系和关联日志。" />
-        ) : state.error ? (
-          <ObservabilityStatusState title="加载链路详情失败" copy={state.error} tone="danger" />
-        ) : detail ? (
-          <>
-            <div className="flex flex-wrap items-center gap-2 text-[11px] text-[var(--ops-text-secondary)]">
-              <span className="font-mono">{detail.traceId}</span>
-              <span>{waterfallRows.length} 个跨度</span>
-              <span>{formatDurationNanos(detail.durationNanos)}</span>
-            </div>
-            <div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
-              {[
-                 ['当前跨度', selectedSpan?.spanName || selectedSpan?.spanId || detail.rootSpanName || '-'],
-                 ['错误跨度', String(detail.errorSpanCount || waterfallRows.filter(row => row.tone === 'danger').length)],
-                 ['事件', String(traceEventCount)],
-                 ['关联', String((selectedSpan?.links || []).length)]
-               ].map(([label, value]) => (
-                <article key={label} className="rounded-[4px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-3 py-2.5">
-                  <p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--ops-text-tertiary)]">{label}</p>
-                  <p className="mt-1 truncate text-[13px] font-semibold text-[var(--ops-text-primary)]">{value}</p>
-                </article>
-              ))}
-            </div>
-            <div className="grid gap-4 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,.75fr)]">
-              <ObservabilityWaterfall
-                rows={waterfallRows}
-                spanLabel="跨度"
-                durationLabel="耗时"
-                timelineLabel="时间轴"
-                timelineTicks={buildTraceTimelineTicks(detail.durationNanos)}
-                selectedEventKey={state.selectedEventKey}
-                onSelect={onSelectSpan}
-                onSelectEvent={onSelectEvent}
-              />
-              <div className="grid gap-3">
-                <TraceAttributionDiagnostics rows={attributionDiagnostics} />
-                {selectedTraceEvent ? (
-                  <div
-                    data-trace-manage-event-detail="span-event-detail"
-                    className="flex items-center justify-between gap-3 border-b border-[#252b35] pb-2"
-                  >
-                    <div className="min-w-0">
-                      <p className="text-[11px] font-semibold text-[#8792a5]">事件详情</p>
-                      <p className="mt-1 truncate text-[13px] font-semibold text-[#e6edf7]">{selectedTraceEvent.event.label}</p>
-                      <p data-trace-manage-event-detail-copy="span-event-not-span" className="mt-0.5 text-[11px] text-[#8792a5]">
-                        不是新的跨度,是当前跨度上的时间点
-                      </p>
-                    </div>
-                    <button type="button" className="text-[11px] font-semibold text-[#8f9bad] hover:text-[#f2f6fb]" onClick={showSelectedSpan}>
-                      查看跨度
-                    </button>
-                  </div>
-                ) : null}
-                <RowList rows={selectedFacts} />
-              </div>
-            </div>
-          </>
-        ) : (
-          <ObservabilityStatusState title="选择一条链路" copy="从链路列表打开详情后,会在这里展示 waterfall 和跨度证据。" />
-        )}
-      </div>
-    </OverlayDialog>
-  );
-}
-
-function TraceExplorer({
-  data,
-  draft,
-  setDraft,
-  applyQuery,
-  resetQuery,
-  routeContext,
-  timeContext,
-  applyTimeContext,
-  currentTraceReturnHref
-}: {
-  data: TraceManageData;
-  draft: TraceQueryState;
-  setDraft: React.Dispatch<React.SetStateAction<TraceQueryState>>;
-  applyQuery: (nextQuery?: TraceQueryState) => void;
-  resetQuery: () => void;
-  routeContext: SignalRouteContext;
-  timeContext: TimeContext;
-  applyTimeContext: (timeContext: TimeContext) => void;
-  currentTraceReturnHref: string;
-}) {
-  const { t } = useI18n();
-  const rows = useMemo(() => buildTraceExplorerRows(data.list.content || [], formatDurationNanos, formatTime), [data.list.content]);
-  const selectedTrace = useMemo(
-    () =>
-      rows[0] ?? {
-        key: draft.traceId || 'trace-empty',
-        traceId: draft.traceId || '-',
-        rootSpanId: draft.spanId || '-',
-        name: draft.traceId ? '过滤链路' : '等待查询',
-        service: draft.serviceName || routeContext.serviceName || '-',
-        namespace: routeContext.serviceNamespace || '-',
-        duration: '-',
-        status: draft.errorOnly ? 'ERROR' : 'UNSET',
-        startTime: '-'
-      },
-    [
-      draft.errorOnly,
-      draft.serviceName,
-      draft.spanId,
-      draft.traceId,
-      routeContext.serviceName,
-      routeContext.serviceNamespace,
-      rows
-    ]
-  );
-  const handoffLinks = buildTraceHandoffLinks(null, null, routeContext, {
-    traceId: selectedTrace.traceId !== '-' ? selectedTrace.traceId : draft.traceId || undefined,
-    spanId: selectedTrace.rootSpanId !== '-' ? selectedTrace.rootSpanId : draft.spanId || undefined,
-    serviceName: selectedTrace.service !== '-' ? selectedTrace.service : draft.serviceName || routeContext.serviceName || undefined,
-    serviceNamespace: selectedTrace.namespace !== '-' ? selectedTrace.namespace : routeContext.serviceNamespace || undefined,
-    environment: routeContext.environment,
-    intakeReturnTo: currentTraceReturnHref,
-    logsReturnTo: currentTraceReturnHref
-  });
-  const missingTraceHandoffTitle = t('trace.manage.handoff.logs-disabled');
-  const missingEntityHandoffTitle = t('trace.manage.handoff.entity-disabled');
-  const canOpenLogs = hasNavigationId(selectedTrace.traceId) || hasNavigationId(draft.traceId) || hasNavigationId(routeContext.traceId);
-  const canOpenEntity = hasNavigationId(routeContext.entityId);
-  const entityContextRows = buildSignalEntityContextRows(routeContext, {
-    serviceName: selectedTrace.service !== '-' ? selectedTrace.service : draft.serviceName,
-    serviceNamespace: selectedTrace.namespace !== '-' ? selectedTrace.namespace : routeContext.serviceNamespace,
-    environment: routeContext.environment,
-    traceId: selectedTrace.traceId !== '-' ? selectedTrace.traceId : draft.traceId,
-    spanId: selectedTrace.rootSpanId !== '-' ? selectedTrace.rootSpanId : draft.spanId,
-    source: routeContext.source || 'OTLP'
-  });
-  const routeAttributionDiagnostics = buildTraceAttributionDiagnostics(null, null, routeContext, t);
-  const latestObservedAt = data.overview.latestObservedAt ? formatTime(data.overview.latestObservedAt) : '-';
-  const chartRows = rows.length ? rows.slice(0, 10) : [selectedTrace];
-  const activeTraceId = selectedTrace.traceId !== '-' ? selectedTrace.traceId : '未选择';
-  const selectedTraceEvidenceRows = [
-    ['链路 ID', activeTraceId, 'traceId'],
-    ['链路状态', selectedTrace.status, draft.errorOnly ? '仅看错误链路' : '当前筛选'],
-    ['开始时间', selectedTrace.startTime, '根跨度开始'],
-    ['最近上报', latestObservedAt, '工作台最新链路']
-  ];
-  const [traceDetailDrawer, setTraceDetailDrawer] = useState<TraceDetailDrawerState>({
-    open: false,
-    loading: false,
-    error: null,
-    detail: null,
-    selectedSpanId: null,
-    selectedEventKey: null
-  });
-  const autoOpenedTraceDetailKeyRef = useRef<string | null>(null);
-  const requestedTraceId = firstNavigationId(draft.traceId, routeContext.traceId);
-  const requestedSpanId = firstNavigationId(draft.spanId, routeContext.spanId);
-
-  const closeTraceDetailDrawer = () => {
-    setTraceDetailDrawer({
-      open: false,
-      loading: false,
-      error: null,
-      detail: null,
-      selectedSpanId: null,
-      selectedEventKey: null
-    });
-  };
-
-  const openTraceDetailDrawer = useCallback(async (row: typeof rows[number], preferredSpanId?: string | null) => {
-    const traceId = row.traceId?.trim();
-    if (!traceId || traceId === '-') return;
-    const initialSelectedSpanId = firstNavigationId(preferredSpanId, row.rootSpanId) || null;
-    setTraceDetailDrawer({
-      open: true,
-      loading: true,
-      error: null,
-      detail: null,
-      selectedSpanId: initialSelectedSpanId,
-      selectedEventKey: null
-    });
-    try {
-      const bundle = await loadTraceDetailBundle(apiMessageGet, traceId);
-      const detail = mergeTraceDetailSpans(bundle.detail, bundle.spans);
-      const selectedSpanId = firstNavigationId(
-        detail.spans.find(span => span.spanId === preferredSpanId)?.spanId,
-        detail.spans.find(span => span.spanId === row.rootSpanId)?.spanId,
-        detail.spans[0]?.spanId,
-        row.rootSpanId
-      ) || null;
-      setTraceDetailDrawer({
-        open: true,
-        loading: false,
-        error: null,
-        detail,
-        selectedSpanId,
-        selectedEventKey: null
-      });
-    } catch {
-      setTraceDetailDrawer({
-        open: true,
-        loading: false,
-        error: '链路详情暂时不可用,请重试或调整查询条件。',
-        detail: null,
-        selectedSpanId: initialSelectedSpanId,
-        selectedEventKey: null
-      });
-    }
-  }, []);
-
-  useEffect(() => {
-    if (!requestedTraceId || traceDetailDrawer.open) return;
-    const traceRow = rows.find(row => row.traceId === requestedTraceId) || (selectedTrace.traceId === requestedTraceId ? selectedTrace : null);
-    if (!traceRow || !hasNavigationId(traceRow.traceId)) return;
-
-    const autoOpenKey = `${traceRow.traceId}:${requestedSpanId || traceRow.rootSpanId || ''}`;
-    if (autoOpenedTraceDetailKeyRef.current === autoOpenKey) return;
-
-    autoOpenedTraceDetailKeyRef.current = autoOpenKey;
-    void openTraceDetailDrawer(traceRow, requestedSpanId);
-  }, [openTraceDetailDrawer, requestedSpanId, requestedTraceId, rows, selectedTrace, traceDetailDrawer.open]);
-
-  return (
-    <main
-      data-trace-manage-route="otlp-cold-trace-workbench"
-      data-trace-manage-style-baseline="hertzbeat-cold-matte"
-      className="min-h-[calc(100vh-56px)] bg-[#07090b] text-[#e8edf5]"
-    >
-      <div className="mx-auto flex w-full max-w-[1600px] flex-col gap-4 px-6 py-6">
-        <section data-trace-manage-header="cold-compact-header" className={`${panelClass} px-5 py-4`}>
-          <div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-start">
-            <div className="min-w-0">
-              <p className="mb-2 text-[12px] font-semibold tracking-[0.08em] text-[#8792a5]">可观测</p>
-	              <h1 className="text-[30px] font-semibold tracking-normal text-[#f4f7fb]">链路工作台</h1>
-	              <p className="mt-3 max-w-[820px] text-[13px] leading-6 text-[#9ca7ba]">
-	                按服务、链路 ID、跨度 ID 和错误状态筛选链路,直接查看趋势、列表和详情。
-	              </p>
-            </div>
-            <div
-              data-trace-manage-time-toolbar="top-right-corner"
-              className="ml-auto grid w-full min-w-0 max-w-[1120px] gap-2 xl:w-auto"
-            >
-              <div
-                data-trace-manage-time-control="shared-time-context-control"
-                data-trace-manage-time-control-placement="top-right"
-                data-trace-manage-time-control-visual="narrow-top-right-rail"
-                data-trace-manage-time-control-fit="no-clipping"
-                className="flex max-w-full justify-end"
-              >
-                <TimeRangeControl
-                  value={timeContext}
-                  labels={buildTimeRangeControlLabels(t)}
-                  onApply={applyTimeContext}
-                  onRefresh={() => applyTimeContext(timeContext)}
-                  onReset={() => applyTimeContext({ timeRange: 'last-30m', refresh: timeContext.refresh, live: 'false', tz: timeContext.tz })}
-                  showAbsoluteFields
-                  variant="narrow-rail"
-                  className="justify-end"
-                  presetSelectProps={{ 'data-trace-manage-time-range-select': 'true' }}
-                  presetOptionDataAttribute="data-trace-manage-time-range-preset"
-                  refreshActionProps={{ 'data-trace-manage-time-refresh-action': 'true' }}
-                />
-              </div>
-              <div data-trace-manage-action-row="cold-workbench-actions" className="flex flex-wrap items-center justify-end gap-2">
-                {routeContext.returnTo ? (
-                  <Link data-trace-manage-return-action="true" href={routeContext.returnTo} className={actionClass}>
-                    <Workflow className="h-4 w-4" aria-hidden="true" />
-                    返回来源
-                  </Link>
-                ) : null}
-                <Link href={handoffLinks.intakeHref} className={actionClass}>
-                  <Workflow className="h-4 w-4" aria-hidden="true" />
-                  返回 OTLP 接入
-                </Link>
-                <Link href="/setting/collector" className={actionClass}>
-                  <Server className="h-4 w-4" aria-hidden="true" />
-                  采集集群
-                </Link>
-                {canOpenEntity ? (
-                  <Link href={handoffLinks.entityHref} className={actionClass}>
-                    <Workflow className="h-4 w-4" aria-hidden="true" />
-                    实体详情
-                  </Link>
-                ) : (
-                  <span className="inline-flex" title={missingEntityHandoffTitle}>
-                    <button
-                      data-trace-manage-entity-action-disabled="missing-entity-id"
-                      type="button"
-                      className={disabledActionClass}
-                      disabled
-                      title={missingEntityHandoffTitle}
-                      aria-label={missingEntityHandoffTitle}
-                    >
-                      <Workflow className="h-4 w-4" aria-hidden="true" />
-                      实体详情
-                    </button>
-                  </span>
-                )}
-                <Link href={handoffLinks.alertHandlingHref} className={actionClass}>
-                  <BellRing className="h-4 w-4" aria-hidden="true" />
-                  告警处理
-                </Link>
-                <Link href="/setting/define" className={actionClass}>
-                  <ListChecks className="h-4 w-4" aria-hidden="true" />
-                  监控模板
-                </Link>
-              </div>
-            </div>
-          </div>
-        </section>
-
-        <section data-trace-manage-query-bar="cold-query-row" className={`${panelClass} px-4 py-3`}>
-          <div className="flex flex-wrap items-center gap-2">
-            <div className="relative min-w-[280px] max-w-[420px] flex-1">
-              <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#7d8798]" aria-hidden="true" />
-              <Input
-                aria-label="服务名称"
-                placeholder="服务名称"
-                value={draft.serviceName}
-                onChange={event => setDraft(updateDraftField('serviceName', event.target.value))}
-                data-trace-manage-service-input="true"
-                className={`${inputClass} w-full pl-9`}
-              />
-            </div>
-            <Input
-              aria-label="链路 ID"
-              value={draft.traceId}
-              onChange={event => setDraft(updateDraftField('traceId', event.target.value))}
-              placeholder="链路 ID"
-              data-trace-manage-trace-id-input="true"
-              className={`${inputClass} w-[240px] font-mono`}
-            />
-            <Input
-              aria-label="跨度 ID"
-              value={draft.spanId}
-              onChange={event => setDraft(updateDraftField('spanId', event.target.value))}
-              placeholder="跨度 ID"
-              className={`${inputClass} w-[220px] font-mono`}
-            />
-            <Select
-              aria-label="链路状态"
-              value={draft.errorOnly ? 'error' : 'all'}
-              onChange={event => setDraft(updateDraftField('errorOnly', event.target.value === 'error'))}
-              data-trace-manage-status-filter="true"
-              containerClassName="w-[120px]"
-              className="h-8 min-w-0 text-[#d5dce8]"
-            >
-              <option value="all">全部链路</option>
-              <option value="error">错误链路</option>
-            </Select>
-            <button data-trace-manage-search-action="true" type="button" className={primaryActionClass} onClick={() => applyQuery()}>
-              <Play className="h-4 w-4" aria-hidden="true" />
-              运行查询
-            </button>
-            <button data-trace-manage-reset-action="true" type="button" className={actionClass} onClick={resetQuery}>
-              <RotateCcw className="h-4 w-4" aria-hidden="true" />
-              重置
-            </button>
-          </div>
-        </section>
-
-        <section data-trace-manage-chart-band="cold-chart-band" className={`${panelClass} px-4 py-4`}>
-          <div className="grid gap-3 lg:grid-cols-[repeat(4,minmax(0,160px))_minmax(0,1fr)]">
-            {[
-              ['链路总数', data.overview.totalTraceCount ?? 0],
-              ['错误链路', data.overview.errorTraceCount ?? 0],
-              ['列表条数', rows.length],
-              ['最近时间', latestObservedAt]
-            ].map(([label, value]) => (
-              <div key={String(label)} className="rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3">
-                <p className="text-[12px] font-semibold text-[#8792a5]">{label}</p>
-                <p className="mt-2 truncate text-[18px] font-semibold text-[#f2f6fb]">{value}</p>
-              </div>
-            ))}
-            <div className="min-w-0 rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3">
-              <div className="mb-3 flex items-center justify-between text-[12px] text-[#8792a5]">
-                <span className="font-semibold text-[#c6cfdd]">趋势带</span>
-                <span>近 10 条链路</span>
-              </div>
-              <div className="flex h-16 items-end gap-1.5">
-                {chartRows.map((row, index) => {
-                  const isError = row.status.toUpperCase().includes('ERROR');
-                  return (
-                    <span
-                      key={`${row.key}-${index}`}
-                      className={`min-w-0 flex-1 rounded-t-[3px] border ${isError ? 'border-[#70424a] bg-[#35131d]' : 'border-[#2b4462] bg-[#132235]'}`}
-                      style={{ height: chartHeight(index, chartRows.length, isError) }}
-                      title={`${row.service} ${row.duration}`}
-                    />
-                  );
-                })}
-              </div>
-            </div>
-          </div>
-        </section>
-
-        <section className="grid gap-4 2xl:grid-cols-[minmax(0,1fr)_330px]">
-          <div data-trace-manage-trace-table="cold-dense-trace-list" className={`${panelClass} min-w-0 overflow-hidden`}>
-            <div className="flex h-11 items-center justify-between border-b border-[#252b35] px-4">
-              <div>
-                <p className="text-[12px] font-semibold text-[#8792a5]">最近链路</p>
-                <p className="mt-0.5 text-[11px] text-[#5f6979]">按当前查询条件展示</p>
-              </div>
-              <span className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#9ca7ba]">
-                {rows.length} 条
-              </span>
-            </div>
-            <table className="w-full border-collapse text-left text-[12px]">
-              <thead className="border-b border-[#252b35] bg-[#10141b] text-[#8792a5]">
-                <tr>
-                  <th className="px-4 py-3 font-semibold">开始时间</th>
-                  <th className="px-4 py-3 font-semibold">服务</th>
-                  <th className="px-4 py-3 font-semibold">根跨度</th>
-                  <th className="px-4 py-3 font-semibold">耗时</th>
-                  <th className="px-4 py-3 font-semibold">状态</th>
-                  <th className="px-4 py-3 font-semibold">链路 ID</th>
-                </tr>
-              </thead>
-              <tbody>
-                {rows.length ? (
-                  rows.map(row => (
-                    <tr key={row.key} className="border-b border-[#1f2530] last:border-b-0 hover:bg-[#111721]">
-                      <td className="px-4 py-3 font-mono text-[11px] text-[#c8d2df]">{row.startTime}</td>
-                      <td className="px-4 py-3 font-semibold text-[#edf3fb]">{row.service}</td>
-                      <td className="px-4 py-3">
-                        <button
-                          type="button"
-                          data-trace-manage-open-detail-action="side-waterfall-modal"
-                          onClick={() => openTraceDetailDrawer(row)}
-                          className="text-left font-semibold text-[#edf3fb] hover:text-[#9fb6ff]"
-                        >
-                          {row.name}
-                        </button>
-                      </td>
-                      <td className="px-4 py-3 text-[#c8d2df]">{row.duration}</td>
-                      <td className="px-4 py-3">
-                        <span className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold ${statusTone(row.status)}`}>
-                          {row.status}
-                        </span>
-                      </td>
-                      <td className="max-w-[220px] truncate px-4 py-3 font-mono text-[11px] text-[#8792a5]">{row.traceId}</td>
-                    </tr>
-                  ))
-                ) : (
-                  <tr>
-                    <td colSpan={6} className="h-[260px] text-center">
-                      <div className="mx-auto flex max-w-[280px] flex-col items-center">
-                        <GitBranch className="h-8 w-8 text-[#7d8798]" aria-hidden="true" />
-                        <p className="mt-3 text-[14px] font-semibold text-[#edf3fb]">暂无链路</p>
-                        <p data-trace-manage-empty-guidance="operator-no-data-guidance" className="mt-2 text-[12px] leading-5 text-[#7f8a9d]">
-                          确认时间范围、实体归因、采集器和监控模板后再查看链路。
-                        </p>
-                      </div>
-                    </td>
-                  </tr>
-                )}
-              </tbody>
-            </table>
-          </div>
-
-          <aside data-trace-manage-detail-panel="cold-detail-panel" className={`${panelClass} min-w-0 overflow-hidden`}>
-            <div className="border-b border-[#252b35] px-4 py-3">
-              <p className="text-[12px] font-semibold text-[#8792a5]">详情面板</p>
-              <h2 className="mt-2 truncate text-[18px] font-semibold text-[#f2f6fb]">{selectedTrace.name}</h2>
-              <p className="mt-1 truncate font-mono text-[11px] text-[#6d7788]">{activeTraceId}</p>
-            </div>
-            <div className="space-y-3 px-4 py-4">
-              {[
-                ['服务', selectedTrace.service],
-                ['命名空间', selectedTrace.namespace],
-                ['根跨度', selectedTrace.rootSpanId],
-                ['耗时', selectedTrace.duration]
-              ].map(([label, value]) => (
-                <div key={label} className="rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-2">
-                  <p className="text-[11px] font-semibold text-[#8792a5]">{label}</p>
-                  <p className="mt-1 truncate text-[13px] font-semibold text-[#e6edf7]">{value}</p>
-                </div>
-              ))}
-              <div
-                data-trace-manage-selected-evidence="selected-trace-evidence"
-                aria-label="链路证据 链路 ID 链路状态 开始时间 最近上报"
-                className="rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3"
-              >
-                <div className="mb-2 flex items-center justify-between gap-2">
-                  <p className="text-[11px] font-semibold text-[#8792a5]">链路证据</p>
-                  <span className="text-[11px] font-semibold text-[#6d7788]">当前选中</span>
-                </div>
-                <div className="space-y-2">
-                  {selectedTraceEvidenceRows.map(([label, value, meta]) => (
-                    <div key={label} className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]">
-                      <span className="text-[#7f8a9d]">{label}</span>
-                      <span className="min-w-0">
-                        <span className="block truncate font-semibold text-[#e6edf7]">{value}</span>
-                        <span className="block truncate text-[#6d7788]">{meta}</span>
-                      </span>
-                    </div>
-                  ))}
-                </div>
-              </div>
-              <div
-                data-trace-manage-entity-context="hertzbeat-signal-entity-context"
-                aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"
-                className="rounded-[4px] border border-[#252b35] bg-[#10141b] px-3 py-3"
-	              >
-	                <div className="mb-2 flex items-center justify-between gap-2">
-	                  <p className="text-[11px] font-semibold text-[#8792a5]">实体上下文</p>
-	                </div>
-                <div className="space-y-2">
-                  {entityContextRows.map(row => (
-                    <div key={row.label} className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]">
-                      <span className="text-[#7f8a9d]">{row.label}</span>
-                      <span className="min-w-0">
-                        <span className="block truncate font-semibold text-[#e6edf7]">{row.value}</span>
-                        <span className="block truncate text-[#6d7788]">{row.meta}</span>
-                      </span>
-                    </div>
-                  ))}
-                </div>
-              </div>
-              <TraceAttributionDiagnostics rows={routeAttributionDiagnostics} />
-            </div>
-            <div className="grid gap-2 border-t border-[#252b35] px-4 py-4">
-              <p
-                data-trace-manage-alert-context-hint="entity-trace-alert-handoff"
-                className="rounded-[3px] border border-[#252b35] bg-[#10141b] px-2.5 py-2 text-[11px] leading-4 text-[#8792a5]"
-              >
-                按当前实体、服务和已带入的链路上下文查看相关告警
-              </p>
-              <p
-                data-trace-manage-signal-handoff-hint="trace-log-metric-context"
-                className="rounded-[3px] border border-[#252b35] bg-[#10141b] px-2.5 py-2 text-[11px] leading-4 text-[#8792a5]"
-              >
-                当前链路的 traceId、spanId 和服务上下文会带入日志与指标工作台
-              </p>
-              {canOpenEntity ? (
-                <Link href={handoffLinks.entityHref} className={actionClass}>
-                  <Workflow className="h-4 w-4" aria-hidden="true" />
-                  实体详情
-                </Link>
-              ) : (
-                <span className="inline-flex" title={missingEntityHandoffTitle}>
-                  <button
-                    data-trace-manage-entity-action-disabled="missing-entity-id"
-                    type="button"
-                    className={disabledActionClass}
-                    disabled
-                    title={missingEntityHandoffTitle}
-                    aria-label={missingEntityHandoffTitle}
-                  >
-                    <Workflow className="h-4 w-4" aria-hidden="true" />
-                    实体详情
-                  </button>
-                </span>
-              )}
-              <Link href={handoffLinks.alertHandlingHref} className={actionClass}>
-                <BellRing className="h-4 w-4" aria-hidden="true" />
-                告警处理
-              </Link>
-              {canOpenLogs ? (
-                <Link data-trace-manage-open-logs-action="true" href={handoffLinks.logsHref} className={actionClass}>
-                  <Timer className="h-4 w-4" aria-hidden="true" />
-                  查看日志
-                </Link>
-              ) : (
-                <span className="inline-flex" title={missingTraceHandoffTitle}>
-                  <button
-                    data-trace-manage-open-logs-action="true"
-                    data-trace-manage-open-logs-action-disabled="missing-trace-id"
-                    type="button"
-                    className={disabledActionClass}
-                    disabled
-                    title={missingTraceHandoffTitle}
-                    aria-label={missingTraceHandoffTitle}
-                  >
-                    <Timer className="h-4 w-4" aria-hidden="true" />
-                    查看日志
-                  </button>
-                </span>
-              )}
-              <Link href={handoffLinks.metricsHref} className={actionClass}>
-                <BarChart3 className="h-4 w-4" aria-hidden="true" />
-                查看指标
-              </Link>
-              <Link href={handoffLinks.entitiesHref} className={actionClass}>
-                <Workflow className="h-4 w-4" aria-hidden="true" />
-                对象目录
-              </Link>
-            </div>
-          </aside>
-        </section>
-        <TraceWaterfallDrawer
-          state={traceDetailDrawer}
-          routeContext={routeContext}
-          currentTraceReturnHref={currentTraceReturnHref}
-          onClose={closeTraceDetailDrawer}
-          onSelectSpan={spanId => setTraceDetailDrawer(previous => ({ ...previous, selectedSpanId: spanId, selectedEventKey: null }))}
-          onSelectEvent={(eventKey, spanId) => setTraceDetailDrawer(previous => ({ ...previous, selectedSpanId: spanId || previous.selectedSpanId, selectedEventKey: eventKey }))}
-        />
-      </div>
-    </main>
-  );
-}
-
-export default function TraceManagePage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const router = useRouter();
-  const initialQuery = useMemo(() => queryStateFromParams(searchParams), [searchParams]);
-  const routeContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
-  const [draft, setDraft] = useState<TraceQueryState>(initialQuery);
-  const [query, setQuery] = useState<TraceQueryState>(initialQuery);
-  const traceTimeContext = useMemo(() => sanitizeTimeContext({
-    timeRange: routeContext.timeRange || 'last-30m',
-    start: routeContext.start,
-    end: routeContext.end,
-    refresh: routeContext.refresh,
-    live: routeContext.live || 'false',
-    tz: routeContext.tz
-  }), [routeContext]);
-  const traceUrls = useMemo(() => buildTraceUrls(query, routeContext), [query, routeContext]);
-  const traceTimeRefreshKey = useMemo(
-    () =>
-      [
-        traceTimeContext.timeRange || '',
-        traceTimeContext.start || '',
-        traceTimeContext.end || '',
-        traceTimeContext.refresh || '',
-        traceTimeContext.live || '',
-        traceTimeContext.tz || ''
-      ].join('|'),
-    [traceTimeContext.end, traceTimeContext.live, traceTimeContext.refresh, traceTimeContext.start, traceTimeContext.timeRange, traceTimeContext.tz]
-  );
-  const loadCacheKey = useMemo(() => `trace-manage:${traceUrls.listUrl}|${traceUrls.overviewUrl}|${traceTimeRefreshKey}`, [traceTimeRefreshKey, traceUrls]);
-
-  const load = useCallback(async (): Promise<TraceManageData> => {
-    const [overview, list] = await Promise.all([
-      apiMessageGet<TraceOverview>(traceUrls.overviewUrl),
-      apiMessageGet<PageResult<TraceListItem>>(traceUrls.listUrl)
-    ]);
-    return { overview, list };
-  }, [traceUrls]);
-
-  const applyQuery = useCallback((nextQuery: TraceQueryState = draft) => {
-    setQuery(nextQuery);
-    router.replace(buildTraceManageRoute(searchParams, nextQuery));
-  }, [draft, router, searchParams]);
-
-  const applyTraceTimeContext = useCallback((timeContext: TimeContext) => {
-    const appliedContext = resolveAppliedTimeContext(timeContext, traceTimeContext);
-    router.replace(buildTraceManageRoute(searchParams, query, appliedContext));
-  }, [query, router, searchParams, traceTimeContext]);
-
-  const resetQuery = useCallback(() => {
-    setDraft(emptyTraceQuery);
-    setQuery(emptyTraceQuery);
-    router.replace(buildResetTraceManageRoute(searchParams));
-  }, [router, searchParams]);
-
-  useEffect(() => {
-    if (hasDisplayReturnLabel(searchParams) || hasUnsanitizedTraceTimeBounds(searchParams)) {
-      router.replace(buildTraceManageRoute(searchParams, query));
-    }
-  }, [query, router, searchParams]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('trace.manage.loading')} cacheKey={loadCacheKey}>
-      {data => (
-        <TraceExplorer
-          data={data}
-          draft={draft}
-          setDraft={setDraft}
-          applyQuery={applyQuery}
-          resetQuery={resetQuery}
-          routeContext={routeContext}
-          timeContext={traceTimeContext}
-          applyTimeContext={applyTraceTimeContext}
-          currentTraceReturnHref={buildTraceReturnHref(searchParams, query)}
-        />
-      )}
-    </ClientWorkbench>
-  );
+  searchParams?: Promise<TraceManageSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readTraceManageRouteState(resolvedSearchParams);
+  return <TraceManagePage initialRouteState={routeState} />;
 }
diff --git a/web-next/app/trace/manage/route-state.test.ts b/web-next/app/trace/manage/route-state.test.ts
index 2194ef0476..38c0865fda 100644
--- a/web-next/app/trace/manage/route-state.test.ts
+++ b/web-next/app/trace/manage/route-state.test.ts
@@ -1,6 +1,7 @@
 import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { describe, expect, it } from 'vitest';
+import { readSignalRouteContext } from '../../../lib/signal-route-context';
 import { buildResetTraceManageRoute, buildTraceManageRoute } from './route-state';
 
 function createSearchParams(input: string) {
@@ -10,9 +11,9 @@ function createSearchParams(input: string) {
 describe('trace manage route state', () => {
   it('preserves context links when applying a trace query', () => {
     const route = buildTraceManageRoute(
-      createSearchParams(
+      readSignalRouteContext(createSearchParams(
         'timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=Asia%2FShanghai&entityId=42&entityName=checkout&returnTo=%2Foverview&returnLabel=Overview&serviceNamespace=payments&environment=prod&codeRepo=https%3A%2F%2Fexample.test%2Frepo&codeProvider=github&codePath=src%2Ftrace.ts&codeSearch=TraceService&codeLabel=source'
-      ),
+      )),
       {
         traceId: 'trace-123',
         spanId: 'span-456',
@@ -29,7 +30,7 @@ describe('trace manage route state', () => {
 
   it('keeps navigation context when clearing trace filters', () => {
     const route = buildResetTraceManageRoute(
-      createSearchParams('timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=UTC&returnTo=%2Fentities&returnLabel=Entities&serviceNamespace=payments&codeProvider=github')
+      readSignalRouteContext(createSearchParams('timeRange=last-1h&start=1713200000000&end=1713203600000&refresh=30&live=false&tz=UTC&returnTo=%2Fentities&returnLabel=Entities&serviceNamespace=payments&codeProvider=github'))
     );
 
... 6989 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 43/50: fix(web-next): surface topology windowed metric table

Posted by zh...@apache.org.
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 f47ac7311fc233ca5bce740c94cbff2e8ebfeb80
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 09:50:20 2026 +0800

    fix(web-next): surface topology windowed metric table
---
 web-next/app/topology/page.test.tsx     | 82 +++++++++++++++++++++++++++++++++
 web-next/app/topology/topology-page.tsx | 75 +++++++++++++++++++++---------
 2 files changed, 134 insertions(+), 23 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index f7db52bbe8..5b2c37689a 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -181,6 +181,54 @@ describe('topology page', () => {
     };
   }
 
+  function buildLargeApiTopologyFixture() {
+    const nodes = Array.from({ length: 230 }, (_, index) => {
+      const padded = String(index).padStart(3, '0');
+      return {
+        id: `scale-svc-${padded}`,
+        entityId: `service:scale/${padded}`,
+        entityName: `Scale API ${padded}`,
+        entityType: index % 20 === 0 && index > 0 ? 'database' : 'service',
+        namespace: 'scale',
+        environment: 'prod',
+        health: index % 17 === 0 ? 'critical' : index % 7 === 0 ? 'warning' : 'healthy',
+        evidenceBadges: ['entity-relation', 'otlp-trace-call'],
+        redMetrics: {
+          requestRatePerSecond: 4 + (index % 23),
+          errorRate: Number((((index % 11) + 1) / 1000).toFixed(3)),
+          latencyP95Ms: 40 + (index % 13) * 8
+        }
+      };
+    });
+    const edges = Array.from({ length: 229 }, (_, index) => {
+      const from = String(index).padStart(3, '0');
+      const to = String(index + 1).padStart(3, '0');
+      return {
+        sourceNodeId: `scale-svc-${from}`,
+        targetNodeId: `scale-svc-${to}`,
+        relationType: 'HTTP call',
+        relationSource: 'otlp-trace-call',
+        status: index % 17 === 0 ? 'critical' : index % 7 === 0 ? 'warning' : 'active',
+        score: 80 - (index % 12),
+        evidenceBadges: ['entity-relation', 'otlp-trace-call'],
+        redMetrics: {
+          requestRatePerSecond: 5 + (index % 29),
+          errorRate: Number((((index % 13) + 1) / 1000).toFixed(3)),
+          latencyP95Ms: 45 + (index % 19) * 7
+        }
+      };
+    });
+
+    return {
+      apiBacked: true,
+      focusEntityId: 'service:scale/000',
+      depth: 2,
+      sourceKinds: ['entity-relation', 'otlp-trace-call'],
+      nodes,
+      edges
+    };
+  }
+
   it('keeps topology on the HertzBeat entity relationship surface instead of copied service-map chrome', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
 
@@ -1551,6 +1599,40 @@ describe('topology page', () => {
     expect(html).toContain('边指标排行');
   }, 60000);
 
+  it('surfaces the render-window RED table below the graph when a real API topology is windowed', async () => {
+    const routeContext = {
+      environment: 'prod',
+      timeRange: 'last-1h',
+      sourceKind: 'otlp-trace-call',
+      viewMode: 'service-call',
+      groupBy: 'source-kind',
+      depth: '2'
+    };
+    const { default: TopologyPage } = await import('./topology-page');
+    const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildLargeApiTopologyFixture()} />);
+
+    expect(source).toContain('const topologyShouldShowRenderWindowMetricTable =');
+    expect(source).toContain("topologyRenderWindowCompanion.mode === 'windowed'");
+    expect(source).toContain("topologyRenderWindowCompanion.tableCompanion === 'required'");
+    expect(html).toContain('data-hz-topology-g6-render-window-mode="windowed"');
+    expect(html).toContain('data-hz-topology-g6-render-window-total-node-count="230"');
+    expect(html).toContain('data-hz-topology-g6-render-window-hidden-node-count="30"');
+    expect(html).toContain('data-topology-metric-table-placement="graph-bottom"');
+    expect(html).toContain('data-topology-metric-table-visibility="render-window-companion"');
+    expect(html).toContain('data-topology-metric-table-scope="edge-red-render-window"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="230"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-hidden-node-count="30"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-table-companion="required"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="required"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
+    expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="hidden"');
+    expect(html.indexOf('data-topology-g6-canvas-owner="hertzbeat-ui-g6-canvas"')).toBeLessThan(
+      html.indexOf('data-topology-metric-table-placement="graph-bottom"')
+    );
+  }, 60000);
+
   it('renders API-backed impact timeline evidence when topology returns change events', async () => {
     i18nState.locale = 'en-US';
     const { default: TopologyPage } = await import('./topology-page');
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index 76feb0035c..20ec9f0449 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -1017,6 +1017,31 @@ export default function TopologyPage({
     const strategy = buildHzTopologyG6LargeGraphStrategy(topologyG6Graph);
     return buildHzTopologyG6RenderWindow(topologyG6Graph, strategy, { priorityNodeIds: topologyRenderWindowPriorityNodeIds });
   }, [topologyG6Graph, topologyRenderWindowPriorityNodeIds]);
+  const topologyMetricRenderWindowCompanion = React.useMemo(() => ({
+    mode: topologyRenderWindowCompanion.mode,
+    totalNodeCount: topologyRenderWindowCompanion.totalNodeCount,
+    renderedNodeCount: topologyRenderWindowCompanion.renderedNodeCount,
+    hiddenNodeCount: topologyRenderWindowCompanion.hiddenNodeCount,
+    visibleNodeBudget: topologyRenderWindowCompanion.visibleNodeBudget,
+    tableCompanion: topologyRenderWindowCompanion.tableCompanion,
+    priorityNodeIds: topologyRenderWindowCompanion.priorityNodeIds,
+    renderedNodeIds: topologyRenderWindowCompanion.graph.nodes.map(node => node.id)
+  }), [topologyRenderWindowCompanion]);
+  const topologyShouldShowRenderWindowMetricTable =
+    topologyRenderWindowCompanion.mode === 'windowed' || topologyRenderWindowCompanion.tableCompanion === 'required';
+  const topologyMetricTableLabels = React.useMemo(() => ({
+    edgeCount: t('topology.metric-table.edge-count', { count: topologyMetricRows.length }),
+    requestRate: t('topology.metric-table.request-rate-unit'),
+    errorRate: t('topology.metric-table.error-rate-unit'),
+    latencyP95: t('topology.metric-table.latency-p95-unit'),
+    rowAction: t('topology.metric-table.row-action'),
+    renderWindowFilterAll: t('topology.metric-table.filter.all'),
+    renderWindowFilterVisible: t('topology.metric-table.filter.visible'),
+    renderWindowFilterPartial: t('topology.metric-table.filter.partial'),
+    renderWindowFilterHidden: t('topology.metric-table.filter.hidden'),
+    renderWindowFilterUnknown: t('topology.metric-table.filter.unknown'),
+    rowAriaLabel: (row: HzTopologyMetricRow) => t('topology.metric-table.open-edge-aria', { edge: String(row.id) })
+  }), [t, topologyMetricRows.length]);
   const topologyHoveredDetailEdge = topologyG6HoveredEdgeId ? findEdge(map.edges, topologyG6HoveredEdgeId) : undefined;
   const topologyLiveHoverEdge = topologyHoveredDetailEdge;
   const topologyInvestigationEdge = topologyHoveredDetailEdge ?? topologyDetailEdge;
@@ -1738,30 +1763,9 @@ export default function TopologyPage({
               selectedRowId={topologyMetricSelectedRowId}
               renderWindowFilter={topologyMetricWindowFilter}
               onRenderWindowFilterChange={setTopologyMetricWindowFilter}
-              renderWindowCompanion={{
-                mode: topologyRenderWindowCompanion.mode,
-                totalNodeCount: topologyRenderWindowCompanion.totalNodeCount,
-                renderedNodeCount: topologyRenderWindowCompanion.renderedNodeCount,
-                hiddenNodeCount: topologyRenderWindowCompanion.hiddenNodeCount,
-                visibleNodeBudget: topologyRenderWindowCompanion.visibleNodeBudget,
-                tableCompanion: topologyRenderWindowCompanion.tableCompanion,
-                priorityNodeIds: topologyRenderWindowCompanion.priorityNodeIds,
-                renderedNodeIds: topologyRenderWindowCompanion.graph.nodes.map(node => node.id)
-              }}
+              renderWindowCompanion={topologyMetricRenderWindowCompanion}
               emptyLabel={t('topology.metric-table.empty')}
-              labels={{
-                edgeCount: t('topology.metric-table.edge-count', { count: topologyMetricRows.length }),
-                requestRate: t('topology.metric-table.request-rate-unit'),
-                errorRate: t('topology.metric-table.error-rate-unit'),
-                latencyP95: t('topology.metric-table.latency-p95-unit'),
-                rowAction: t('topology.metric-table.row-action'),
-                renderWindowFilterAll: t('topology.metric-table.filter.all'),
-                renderWindowFilterVisible: t('topology.metric-table.filter.visible'),
-                renderWindowFilterPartial: t('topology.metric-table.filter.partial'),
-                renderWindowFilterHidden: t('topology.metric-table.filter.hidden'),
-                renderWindowFilterUnknown: t('topology.metric-table.filter.unknown'),
-                rowAriaLabel: row => t('topology.metric-table.open-edge-aria', { edge: String(row.id) })
-              }}
+              labels={topologyMetricTableLabels}
               onRowSelect={handleTopologyMetricRowSelect}
               boundary="framed"
             />
@@ -1901,6 +1905,31 @@ export default function TopologyPage({
         </HzTopologyWorkbenchSlot>
       </HzTopologyWorkbenchGrid>
 
+      {topologyShouldShowRenderWindowMetricTable && topologyMetricRows.length > 0 ? (
+        <HzTopologyMetricTable
+          id="topology-metric-table"
+          data-topology-metric-table-owner="hertzbeat-ui"
+          data-topology-metric-table-placement="graph-bottom"
+          data-topology-metric-table-visibility="render-window-companion"
+          data-topology-metric-table-scope="edge-red-render-window"
+          data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
+          data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
+          data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
+          data-topology-metric-table-filter-behavior="in-page-no-route-change"
+          title={t('topology.metric-table.title')}
+          density="graph-first"
+          rows={topologyMetricRows}
+          selectedRowId={topologyMetricSelectedRowId}
+          renderWindowFilter={topologyMetricWindowFilter}
+          onRenderWindowFilterChange={setTopologyMetricWindowFilter}
+          renderWindowCompanion={topologyMetricRenderWindowCompanion}
+          emptyLabel={t('topology.metric-table.empty')}
+          labels={topologyMetricTableLabels}
+          onRowSelect={handleTopologyMetricRowSelect}
+          boundary="framed"
+        />
+      ) : null}
+
       {map.faultContextRows.length > 0 ? (
         <HzTopologyEvidenceList
           data-topology-fault-context="incoming-evidence"


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 50/50: feat(web-next): finalize alpha localization cleanup

Posted by zh...@apache.org.
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 cd0b0a4c37e7c6d26c10e2dd14ba11a9f964b06e
Author: Logic <zq...@dromara.org>
AuthorDate: Wed Jun 3 11:42:10 2026 +0800

    feat(web-next): finalize alpha localization cleanup
---
 web-next/app/actions/page.test.tsx                 |  64 +-
 web-next/app/alert/inhibit/page.test.tsx           |   2 +-
 .../app/alert/integration/[source]/page.test.tsx   |  42 +-
 web-next/app/alert/integration/[source]/page.tsx   |   2 +-
 web-next/app/alert/notice/page.test.tsx            |  78 +-
 web-next/app/alert/page.test.tsx                   |   4 +-
 web-next/app/alert/silence/page.test.tsx           |   2 +-
 .../entities/[entityId]/definition/page.test.tsx   |   4 +-
 web-next/app/entities/[entityId]/page.test.tsx     |  41 +-
 web-next/app/entities/discovery/page.test.tsx      |   4 +-
 web-next/app/entities/import/page.test.tsx         |   3 +-
 web-next/app/entities/page.test.tsx                | 127 ++--
 web-next/app/explorer/page.test.tsx                |  23 +-
 web-next/app/hb-i18n/[lang]/route.test.ts          |  50 +-
 web-next/app/hb-i18n/[lang]/route.ts               |  31 +-
 web-next/app/incidents/page.test.tsx               |  50 +-
 .../ingestion/otlp/metrics/otlp-metrics-page.tsx   |  74 +-
 web-next/app/ingestion/otlp/metrics/page.test.tsx  | 258 +++----
 .../app/ingestion/otlp/metrics/route-state.test.ts |   7 +-
 web-next/app/ingestion/otlp/page.test.tsx          | 200 +++---
 web-next/app/log/manage/page.test.tsx              | 210 +++---
 web-next/app/log/stream/page.test.tsx              |   6 +-
 .../app/monitors/[monitorId]/edit/page.test.tsx    |   4 +-
 web-next/app/monitors/new/page.test.tsx            |   6 +-
 web-next/app/monitors/page.test.tsx                |   7 +-
 web-next/app/passport/lock/page.test.tsx           |   5 +-
 web-next/app/setting/collector/page.test.tsx       |  30 +-
 web-next/app/setting/define/page.test.tsx          |  16 +-
 web-next/app/setting/labels/page.test.tsx          |  12 +-
 web-next/app/setting/plugins/page.test.tsx         |  10 +-
 web-next/app/setting/settings/config/page.test.tsx |  23 +-
 .../setting/settings/object-store/page.test.tsx    |  27 +-
 web-next/app/setting/settings/server/page.test.tsx |  45 +-
 .../settings/settings-layout-client.test.tsx       |  18 +-
 web-next/app/setting/settings/token/page.test.tsx  |  66 +-
 web-next/app/setting/status/page.test.tsx          |   6 +-
 web-next/app/topology/page.test.tsx                | 188 +++--
 web-next/app/trace/manage/page.test.tsx            |  42 +-
 web-next/app/trace/manage/trace-manage-page.tsx    |   1 +
 web-next/app/ui-lab/page.test.tsx                  |  10 +-
 web-next/app/ui-lab/page.tsx                       |  96 +--
 .../log-manage/log-dialog.chrome.test.ts           |  26 +-
 .../log-manage/log-related-trace-dialog.test.tsx   |  11 +-
 .../log-manage/log-stream-detail-dialog.test.tsx   |  10 +-
 .../monitor-detail/monitor-detail-console.test.tsx |   8 +-
 .../monitor-detail-sections.test.tsx               |   4 +-
 .../monitor-history-chart-grid.test.tsx            |   6 +-
 .../monitor-detail/monitor-summary-card.test.tsx   |   2 +-
 .../components/observability/page-header.test.tsx  |  26 +-
 .../observability/time-range-control.test.tsx      |  20 +-
 .../components/observability/waterfall.test.tsx    |  22 +-
 .../pages/alert-authoring-primitives.test.tsx      |   6 +-
 .../components/pages/alert-center-surface.test.tsx | 184 ++---
 .../components/pages/alert-group-surface.test.tsx  |  59 +-
 .../pages/alert-inhibit-surface.test.tsx           |  74 +-
 .../pages/alert-integration-markdown.test.tsx      |  30 +-
 .../pages/alert-notice-console-shell.test.tsx      |   6 +-
 .../pages/alert-notice-rule-fields.test.tsx        | 157 +++--
 .../pages/alert-notice-template-fields.test.tsx    |  18 +-
 .../pages/alert-setting-create-dialog.test.tsx     |  20 +-
 .../pages/alert-setting-surface.test.tsx           |  60 +-
 .../components/pages/alert-setting-surface.tsx     |   2 +-
 .../pages/alert-silence-authoring-fields.test.tsx  |   8 +-
 .../pages/alert-silence-surface.test.tsx           |  74 +-
 .../pages/alert-surface-primitives.test.tsx        |   8 +-
 .../pages/bulletin-center-surface.test.tsx         |  12 +-
 .../pages/bulletin-manage-dialog.test.tsx          |  18 +-
 .../pages/bulletin-metrics-table.test.tsx          |  10 +-
 .../pages/collector-manage-surface.test.tsx        | 103 +--
 .../entity-definition-workspace-surface.test.tsx   | 215 +++---
 .../pages/entity-detail-surface.test.tsx           |  18 +-
 .../pages/entity-discovery-surface.test.tsx        | 174 ++---
 .../pages/entity-editor-surface.test.tsx           | 320 +++------
 .../components/pages/entity-editor-surface.tsx     |  21 +-
 .../components/pages/entity-list-surface.test.tsx  |  44 +-
 .../pages/exception-center-surface.test.tsx        |  32 +-
 .../components/pages/label-manage-surface.test.tsx | 211 ++----
 .../pages/log-integration-redirect-shell.test.tsx  |   7 +-
 web-next/components/pages/login-form.test.tsx      |  28 +-
 .../pages/monitor-editor-surface.test.tsx          |  18 +-
 web-next/components/pages/passport-shell.test.tsx  |  13 +-
 .../pages/plugin-manage-surface.test.tsx           | 100 ++-
 .../pages/setting-define-surface.test.tsx          | 133 ++--
 .../pages/settings-surface-page.test.tsx           |  33 +-
 .../pages/status-setting-surface.test.tsx          |  66 +-
 .../components/pages/status-setting-surface.tsx    |  86 +--
 .../components/providers/i18n-provider.test.ts     | 128 ++--
 .../settings/settings-console-shell.test.tsx       |  32 +-
 .../settings/settings-dialog-form.test.tsx         |  14 +-
 .../components/settings/settings-form.test.tsx     |   8 +-
 .../settings/settings-summary-list.test.tsx        |  26 +-
 .../components/shell/app-frame.chrome.test.tsx     |  46 +-
 web-next/components/shell/app-sidebar.test.tsx     |  20 +-
 .../components/shell/locale-option-list.test.tsx   |   4 +-
 .../shell/platform-copyright-footer.test.tsx       |   2 +-
 web-next/components/ui/checkbox.test.tsx           |   2 +-
 .../components/ui/cold-confirm-dialog.test.tsx     |  24 +-
 web-next/components/ui/date-time-range.test.tsx    |  12 +-
 web-next/components/ui/search-row.test.tsx         |  14 +-
 web-next/components/ui/select.test.tsx             |   4 +-
 web-next/components/ui/weekday-picker.test.tsx     |   6 +-
 .../components/workbench/client-workbench.test.tsx |  16 +-
 .../components/workbench/overlay-dialog.test.tsx   |  16 +-
 .../components/workbench/workbench-page.test.tsx   |  16 +-
 web-next/lib/actions-surface/model.test.ts         | 132 ++--
 web-next/lib/actions-surface/query-state.test.ts   |   5 +-
 web-next/lib/actions-surface/view-model.test.ts    | 137 ++--
 web-next/lib/alert-group/view-model.test.ts        |  80 ++-
 web-next/lib/alert-inhibit/controller.test.ts      |   5 +-
 web-next/lib/alert-inhibit/view-model.test.ts      |  48 +-
 web-next/lib/alert-integration/controller.test.ts  |   5 +-
 web-next/lib/alert-integration/view-model.test.ts  |   6 +-
 web-next/lib/alert-integration/view-model.ts       |   2 +-
 web-next/lib/alert-manage/display-mapping.test.ts  |   8 +-
 web-next/lib/alert-manage/query-state.test.ts      |   3 +-
 web-next/lib/alert-manage/view-model.test.ts       | 245 ++++---
 web-next/lib/alert-notice/view-model.test.ts       |  79 +--
 web-next/lib/alert-setting/controller.test.ts      |   9 +-
 web-next/lib/alert-setting/query-state.test.ts     |  17 +-
 web-next/lib/alert-setting/view-model.test.ts      |  22 +-
 web-next/lib/alert-silence/controller.test.ts      |   5 +-
 web-next/lib/alert-silence/view-model.test.ts      |  50 +-
 web-next/lib/api-facade.test.ts                    |   8 +-
 web-next/lib/bulletin-center/view-model.test.ts    |  72 +-
 web-next/lib/collector-manage/view-model.test.ts   |  36 +-
 web-next/lib/ecosystem-alignment.test.ts           |   6 +-
 web-next/lib/ecosystem-alignment.ts                |   4 +-
 web-next/lib/entity-definition/view-model.test.ts  |  16 +-
 web-next/lib/entity-detail/controller.test.ts      |  10 +-
 web-next/lib/entity-discovery/view-model.test.ts   | 159 +++--
 web-next/lib/entity-import/view-model.test.ts      |  48 +-
 web-next/lib/entity-manage/display-mapping.test.ts |  16 +-
 web-next/lib/entity-manage/view-model.test.ts      |  69 +-
 web-next/lib/exception-center/view-model.test.ts   |  38 +-
 web-next/lib/explorer-surface/controller.test.ts   |  10 +-
 web-next/lib/explorer-surface/view-model.test.ts   |  48 +-
 web-next/lib/hertzbeat-product-language.test.ts    |  58 +-
 web-next/lib/hertzbeat-product-language.ts         |  31 +-
 web-next/lib/i18n-runtime-messages.ts              | 634 ++++++++++++++++-
 web-next/lib/i18n.test.ts                          |  16 +-
 web-next/lib/incidents-surface/model.test.ts       |  44 +-
 web-next/lib/incidents-surface/view-model.test.ts  |  68 +-
 web-next/lib/label-manage/view-model.test.ts       |  12 +-
 web-next/lib/log-manage/view-model.test.ts         |  48 +-
 web-next/lib/monitor-editor/controller.test.ts     |   6 +-
 web-next/lib/monitor-editor/localized-text.test.ts |   6 +-
 .../lib/monitor-manage/display-mapping.test.ts     |   6 +-
 web-next/lib/monitor-manage/view-model.test.ts     |  26 +-
 web-next/lib/nav.test.ts                           |  37 +-
 web-next/lib/object-store/view-model.test.ts       |  17 +-
 web-next/lib/ops-surface/view-model.test.ts        |  16 +-
 web-next/lib/otlp-center/view-model.test.ts        |  16 +-
 web-next/lib/otlp-metrics/view-model.test.ts       |  82 +--
 web-next/lib/overview/view-model.test.ts           | 174 ++++-
 web-next/lib/parity/route-manifest.json            | 784 ++++++++++-----------
 web-next/lib/parity/route-manifest.test.ts         | 402 +++++------
 web-next/lib/passport-lock/view-model.test.ts      |   8 +-
 web-next/lib/passport-login/view-model.test.ts     |  10 +-
 web-next/lib/plugin-manage/controller.test.ts      |   5 +-
 web-next/lib/plugin-manage/view-model.test.ts      |  24 +-
 web-next/lib/setting-define/controller.test.ts     |  21 +-
 web-next/lib/setting-define/controller.ts          |  23 +-
 web-next/lib/setting-define/view-model.test.ts     |  48 +-
 web-next/lib/setting-server/view-model.test.ts     |  52 +-
 .../lib/setting-settings-layout/view-model.test.ts |  14 +-
 web-next/lib/setting-status/view-model.test.ts     | 165 ++---
 web-next/lib/setting-token/view-model.test.ts      |  14 +-
 web-next/lib/settings-surface/view-model.test.ts   |  34 +-
 web-next/lib/shell/sidebar.test.ts                 |  10 +-
 web-next/lib/signal-route-context.test.ts          |   6 +-
 web-next/lib/status-center/view-model.test.ts      | 148 ++--
 web-next/lib/topology-surface/query-state.test.ts  |   3 +-
 web-next/lib/topology-surface/view-model.test.ts   |   2 +-
 web-next/lib/trace-manage/view-model.test.ts       |  40 +-
 web-next/lib/workspace-navigation.test.ts          |  24 +-
 web-next/packages/hertzbeat-ui/src/index.test.tsx  |  96 ++-
 .../packages/hertzbeat-ui/src/topology-g6.test.tsx |  11 +-
 .../scripts/entity-editor-browser-smoke.spec.ts    |  29 +-
 .../scripts/frontend-convergence-contract.test.ts  |  16 +-
 web-next/scripts/i18n-report.test.ts               |  14 +-
 .../scripts/monitor-route-browser-smoke.spec.ts    |  23 +-
 .../scripts/parity/angular-reference-server.mjs    |  43 +-
 web-next/scripts/parity/harness-lib.test.ts        |   4 +-
 .../three-signal-workbench-browser-smoke.spec.ts   |   2 +-
 .../three-signal-workbench-browser-smoke.test.ts   |   3 +-
 web-next/test/i18n-test-helper.ts                  | 750 +++++++++++++++++++-
 186 files changed, 5804 insertions(+), 4506 deletions(-)

diff --git a/web-next/app/actions/page.test.tsx b/web-next/app/actions/page.test.tsx
index ebbcd15558..be895c3e89 100644
--- a/web-next/app/actions/page.test.tsx
+++ b/web-next/app/actions/page.test.tsx
@@ -63,26 +63,24 @@ describe('actions page', () => {
     expect(html).toContain('data-actions-approval-decision-execution-mode="manual-approval-draft-only"');
     expect(html).toContain('data-actions-approval-decision-execution-allowed="false"');
     expect(html).toContain('data-actions-empty-state="cold-ops-domain-adapter"');
-    expect(html).toContain('自动化处置');
-    expect(html).toContain('按 OTLP 工作台的冷色基线统一入口、上下文和审批语义。');
-    expect(html).toContain('冷色入口已接入');
-    expect(html).toContain('执行边界');
-    expect(html).toContain('roadmap 示例快照');
-    expect(html).toContain('不代表实时运行状态');
-    expect(html).toContain('告警上下文建议是当前证据生成的人工交接');
-    expect(html).toContain('工作流自动化');
-    expect(html).toContain('处置手册编排');
+    expect(html).toContain(t('actions.entry.title'));
+    expect(html).toContain(t('actions.entry.subtitle'));
+    expect(html).toContain(t('actions.entry.shell.eyebrow'));
+    expect(html).toContain(t('actions.adapter-boundary.label'));
+    expect(html).toContain(t('actions.adapter-boundary.copy'));
+    expect(html).toContain(t('actions.adapter-boundary.roadmap.workflow-automation'));
+    expect(html).toContain(t('actions.adapter-boundary.roadmap.runbook-orchestration'));
     expect(html).not.toContain('workflow-automation');
     expect(html).not.toContain('runbook-orchestration');
-    expect(html).toContain('等待接入执行适配器');
-    expect(html).toContain('打开概览');
-    expect(html).toContain('查看对象');
-    expect(html).toContain('自动化目录');
-    expect(html).toContain('风险动作');
-    expect(html).toContain('审批流');
-    expect(html).toContain('统一入口上下文');
-    expect(html).toContain('接入执行适配器');
-    expect(html).toContain('保留证据跳转');
+    expect(html).toContain(t('actions.entry.empty.title'));
+    expect(html).toContain(t('actions.entry.action.overview'));
+    expect(html).toContain(t('actions.entry.action.entities'));
+    expect(html).toContain(t('actions.entry.chip.catalog'));
+    expect(html).toContain(t('actions.entry.chip.risk'));
+    expect(html).toContain(t('actions.entry.chip.approval'));
+    expect(html).toContain(t('actions.entry.checklist.context.title'));
+    expect(html).toContain(t('actions.entry.checklist.adapter.title'));
+    expect(html).toContain(t('actions.entry.checklist.evidence.title'));
     expect(html).not.toContain('angular-dark-ops-placeholder');
     expect(html).not.toContain('DARK OPS');
     expect(html).not.toContain('V1 SHELL IS LIVE');
@@ -127,7 +125,7 @@ describe('actions page', () => {
           spanId: 'span-456',
           collector: 'edge-collector-a',
           template: 'java-service',
-          returnTo: '/alert?status=firing&returnLabel=告警'
+          returnTo: `/alert?status=firing&returnLabel=${encodeURIComponent(t('alert.center.default-title'))}`
         }}
       />
     );
@@ -135,25 +133,25 @@ describe('actions page', () => {
     expect(html).toContain('data-actions-suggested-remediation="alert-context-human-confirmation"');
     expect(html).toContain('data-hz-action-workbench-owner="hertzbeat-ui-action-workbench"');
     expect(html).toContain('data-actions-adapter-boundary="adapter-pending"');
-    expect(html).toContain('建议动作');
-    expect(html).toContain('只生成建议,不自动执行。');
+    expect(html).toContain(t('actions.entry.suggested.title'));
+    expect(html).toContain(t('actions.entry.suggested.copy'));
     expect(html).toContain('data-actions-suggested-action="suggest-restart-checkout"');
-    expect(html).toContain('建议重启 checkout-api');
-    expect(html).toContain('高风险 · 重启 checkout 部署');
-    expect(html).toContain('中风险 · 静默边缘饱和告警');
+    expect(html).toContain(t('actions.suggestion.restart.title', { target: 'checkout-api' }));
+    expect(html).toContain(`${t('actions.risk.high')} · ${t('actions.catalog.restart.name')}`);
+    expect(html).toContain(`${t('actions.risk.medium')} · ${t('actions.catalog.mute.name')}`);
     expect(html).not.toContain('high risk · restart-checkout');
     expect(html).not.toContain('medium risk · mute-edge-alerts');
-    expect(html).toContain('来源 告警中心');
-    expect(html).toContain('信号 链路');
-    expect(html).not.toContain('来源 alert');
-    expect(html).not.toContain('信号 traces');
+    expect(html).toContain(t('actions.suggestion.evidence.source', { value: t('actions.suggestion.source.alert') }));
+    expect(html).toContain(t('actions.suggestion.evidence.signal', { value: t('actions.suggestion.signal.traces') }));
+    expect(html).not.toContain(t('actions.suggestion.evidence.source', { value: 'alert' }));
+    expect(html).not.toContain(t('actions.suggestion.evidence.signal', { value: 'traces' }));
     expect(html).toContain('data-actions-suggested-action-confirm="manual-required"');
     expect(html).toContain('data-actions-approval-draft-state="ready"');
     expect(html).toContain('data-actions-approval-draft-status="ready"');
     expect(html).toContain('data-actions-approval-draft-request="preview"');
     expect(html).toContain('&quot;actionId&quot;:&quot;suggest-restart-checkout&quot;');
     expect(html).toContain('&quot;executionAllowed&quot;:false');
-    expect(html).toContain('人工确认后执行');
+    expect(html).toContain(t('actions.entry.suggested.confirm'));
     expect(html).toContain('data-actions-suggested-action-evidence="suggest-restart-checkout"');
     expect(html).toContain('/alert?status=firing');
     expect(html).toContain('traceId=trace-123');
@@ -168,13 +166,15 @@ describe('actions page', () => {
         suggestionContext={{
           entityId: 'service:commerce/checkout',
           source: 'entity',
-          returnTo: '/entities/service%3Acommerce%2Fcheckout?returnLabel=实体'
+          returnTo: `/entities/service%3Acommerce%2Fcheckout?returnLabel=${encodeURIComponent(t('actions.suggestion.source.entity'))}`
         }}
       />
     );
 
-    expect(html).toContain('建议重启 实体 service:commerce/checkout');
-    expect(html).not.toContain('建议重启 service:commerce/checkout');
+    expect(html).toContain(t('actions.suggestion.restart.title', {
+      target: t('actions.suggestion.target.entity-id', { entityId: 'service:commerce/checkout' })
+    }));
+    expect(html).not.toContain(t('actions.suggestion.restart.title', { target: 'service:commerce/checkout' }));
     expect(html).toContain('entityId=service%3Acommerce%2Fcheckout');
     expect(html).not.toContain('returnLabel=');
   });
diff --git a/web-next/app/alert/inhibit/page.test.tsx b/web-next/app/alert/inhibit/page.test.tsx
index 7865540998..7c50eabd2a 100644
--- a/web-next/app/alert/inhibit/page.test.tsx
+++ b/web-next/app/alert/inhibit/page.test.tsx
@@ -206,7 +206,7 @@ describe('alert inhibit page', () => {
     expect(html).toContain('&quot;sourceKind&quot;:&quot;database-middleware-connection&quot;');
     expect(html).toContain('&quot;returnTo&quot;:&quot;/topology?viewMode=resource-dependency&amp;sourceKind=database-middleware-connection&amp;edgeId=svc-checkout--res-orders-db');
     expect(html).not.toContain('returnLabel=');
-    expect(html).not.toContain('HertzBeat 企业运维拓扑');
+    expect(html).not.toContain('HertzBeat operations topology');
   });
 
   it('preserves three-signal evidence context into new inhibit authoring', async () => {
diff --git a/web-next/app/alert/integration/[source]/page.test.tsx b/web-next/app/alert/integration/[source]/page.test.tsx
index 76391d1c9b..b7e4f3a241 100644
--- a/web-next/app/alert/integration/[source]/page.test.tsx
+++ b/web-next/app/alert/integration/[source]/page.test.tsx
@@ -19,10 +19,10 @@ Use this provider.
 
 \`\`\`mermaid
 graph LR
-  A[外部系统告警] --> B[Webhook]
+  A[External alert] --> B[Webhook]
 \`\`\`
 
-1. 检查配置
+1. Check configuration
     \`\`\`bash
     curl http://localhost:9090/api/v1/rules
     \`\`\`
@@ -87,45 +87,45 @@ vi.mock('@/lib/utils', () => ({
 
 vi.mock('@/lib/alert-integration/controller', () => ({
   fallbackDocCopy: 'No integration guide is available for this provider yet.',
-  getAlertIntegrationFallbackDocCopy: () => '当前告警源暂未提供集成指南。',
+  getAlertIntegrationFallbackDocCopy: () => 'No integration guide is available for this alert source yet.',
   loadIntegrationDoc: mockLoadIntegrationDoc
 }));
 
 vi.mock('@/lib/alert-integration/view-model', () => ({
   DATA_SOURCES: [
-    { id: 'webhook', name: '默认Webhook', icon: '/assets/logo.svg' },
+    { id: 'webhook', name: 'Default Webhook', icon: '/assets/logo.svg' },
     { id: 'prometheus', name: 'Prometheus', icon: '/assets/img/integration/prometheus.svg' }
   ],
   buildAlertIntegrationSourceHref: (source: any) => `/alert/integration/${source.id}`,
   getIntegrationSource: (source: string) =>
     [
-      { id: 'webhook', name: '默认Webhook', icon: '/assets/logo.svg' },
+      { id: 'webhook', name: 'Default Webhook', icon: '/assets/logo.svg' },
       { id: 'prometheus', name: 'Prometheus', icon: '/assets/img/integration/prometheus.svg' }
-    ].find(item => item.id === source) ?? { id: 'webhook', name: '默认Webhook', icon: '/assets/logo.svg' },
+    ].find(item => item.id === source) ?? { id: 'webhook', name: 'Default Webhook', icon: '/assets/logo.svg' },
   createAlertIntegrationTranslator: () => (key: string) =>
     ({
-      'alert.integration.kicker': '集成接入',
-      'alert.integration.sources': '集成告警源',
-      'alert.integration.token.manage': '管理令牌'
+      'alert.integration.kicker': 'Alert integration',
+      'alert.integration.sources': 'Integration alert sources',
+      'alert.integration.token.manage': 'Manage tokens'
     })[key] ?? key,
   getIntegrationSourceName: (item: any) => item.name ?? item.id,
   translateAlertIntegration: (key: string) =>
     ({
-      'alert.integration.kicker': '集成接入',
-      'alert.integration.sources': '集成告警源',
-      'alert.integration.token.manage': '管理令牌'
+      'alert.integration.kicker': 'Alert integration',
+      'alert.integration.sources': 'Integration alert sources',
+      'alert.integration.token.manage': 'Manage tokens'
     })[key] ?? key,
   buildIntegrationFacts: (source: string, hasDoc: boolean) => [
-    { label: '集成接入', value: `alert/integration/${source}` },
-    { label: '集成告警源', value: source },
-    { label: '文档状态', value: hasDoc ? '已加载' : '回退文案' }
+    { label: 'Alert integration', value: `alert/integration/${source}` },
+    { label: 'Integration alert sources', value: source },
+    { label: 'Document status', value: hasDoc ? 'Loaded' : 'Fallback copy' }
   ],
   buildIntegrationSourceRows: (source: string) => [
-    { title: '默认Webhook', copy: 'webhook', meta: source === 'webhook' ? 'selected' : '/alert/integration/webhook' },
+    { title: 'Default Webhook', copy: 'webhook', meta: source === 'webhook' ? 'selected' : '/alert/integration/webhook' },
     { title: 'Prometheus', copy: 'prometheus', meta: source === 'prometheus' ? 'selected' : '/alert/integration/prometheus' }
   ],
   buildIntegrationPostureRows: (source: string, hasDoc: boolean) => [
-    { title: 'doc source', copy: `web-app/src/assets/doc/alert-integration/${source}.*.md`, meta: 'existing asset' },
+    { title: 'doc source', copy: `web-next/public/assets/doc/alert-integration/${source}.*.md`, meta: 'existing asset' },
     { title: 'fallback behavior', copy: hasDoc ? 'provider doc loaded' : 'show fallback copy when no provider doc exists', meta: 'behavior preserved' },
     { title: 'token management', copy: 'Continue to use the current token management entry point.', meta: '/setting/settings/token' }
   ]
@@ -157,10 +157,10 @@ describe('alert integration page', () => {
     expect(html).toContain('data-alert-integration-code-block="json"');
     expect(html).toContain('data-alert-integration-code-block="bash"');
     expect(html).toContain('data-alert-integration-mermaid="pending"');
-    expect(html).toContain('集成接入');
-    expect(html).toContain('集成告警源');
-    expect(html).toContain('默认Webhook');
-    expect(html).toContain('管理令牌');
+    expect(html).toContain('Alert integration');
+    expect(html).toContain('Integration alert sources');
+    expect(html).toContain('Default Webhook');
+    expect(html).toContain('Manage tokens');
     expect(html).toContain('Webhook guide');
     expect(html).toContain('curl http://localhost:9090/api/v1/rules');
     expect(mockLoadIntegrationDoc).toHaveBeenLastCalledWith(expect.stringContaining('alert-integration'), 'webhook', 'zh-CN');
diff --git a/web-next/app/alert/integration/[source]/page.tsx b/web-next/app/alert/integration/[source]/page.tsx
index 0e8924c4e7..1e49f154de 100644
--- a/web-next/app/alert/integration/[source]/page.tsx
+++ b/web-next/app/alert/integration/[source]/page.tsx
@@ -30,7 +30,7 @@ export default async function AlertIntegrationPage({ params }: { params: Promise
   const t = createAlertIntegrationTranslator(locale);
   const selectedSourceName = getIntegrationSourceName(selectedSource, t);
   const sourceRailLabel = t('alert.integration.sources');
-  const baseDir = path.join(process.cwd(), '..', 'web-app', 'src', 'assets', 'doc', 'alert-integration');
+  const baseDir = path.join(process.cwd(), 'public', 'assets', 'doc', 'alert-integration');
   const doc = await loadIntegrationDoc(baseDir, selectedSource.id, locale);
   const fallbackDocCopy = getAlertIntegrationFallbackDocCopy(locale);
 
diff --git a/web-next/app/alert/notice/page.test.tsx b/web-next/app/alert/notice/page.test.tsx
index 51dba30aaa..8dd99b70cf 100644
--- a/web-next/app/alert/notice/page.test.tsx
+++ b/web-next/app/alert/notice/page.test.tsx
@@ -121,16 +121,7 @@ vi.mock('next/link', () => ({
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'alert.notice.receivers.title': '接收人',
-        'alert.notice.rules.title': '规则',
-        'alert.notice.templates.title': '模板',
-        'alert.notice.receiver.type': '通知方式',
-        'alert.notice.lanes.receivers.title': '接收人',
-        'alert.notice.lanes.rules.title': '规则',
-        'alert.notice.lanes.templates.title': '模板'
-      }
+      locale: 'zh-CN'
     }),
     locale: 'zh-CN'
   })
@@ -262,7 +253,7 @@ vi.mock('../../../lib/alert-notice/controller', () => ({
     periodEnd: '18:00'
   }),
   buildNoticeRuleDisplayNames: vi.fn(() => ({
-    receiverName: ['Receiver page 0-邮箱'],
+    receiverName: ['Receiver page 0-Email'],
     templateName: 'WebhookTemplate'
   })),
   createNoticeReceiver: vi.fn(),
@@ -359,16 +350,13 @@ describe('alert notice page', () => {
   });
 
   it('renders the OTLP cold notice tab shell with the receiver console selected by default', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
     const html = await renderAlertNoticePage();
 
-    expect(html).toContain('消息通知');
-    expect(html).not.toContain('工作区:alert/notice');
-    expect(html).not.toContain('接收人:17');
-    expect(html).not.toContain('规则:17');
-    expect(html).not.toContain('模板:4');
+    expect(html).toContain(t('alert.notice.title'));
     expect(html).toContain('data-alert-notice-console="true"');
-    expect(html).toContain('data-loading-copy="正在加载通知中心"');
+    expect(html).toContain(`data-loading-copy="${t('alert.notice.loading')}"`);
     expect(html).toContain('data-selected-tab="receiver"');
     expect(html).toContain('data-tab="receiver"');
     expect(html).toContain('data-tab="rule"');
@@ -403,28 +391,24 @@ describe('alert notice page', () => {
     expect(html).toContain('data-hz-pagination-page-jump="number-input"');
     expect(html).toContain('data-alert-notice-pagination-page-size-owner="hertzbeat-ui-select"');
     expect(html).toContain('data-alert-notice-pagination-page-jump-owner="hertzbeat-ui-input"');
-    expect(html).toContain('第 1 / 3 页 · 1-1 / 17');
-    expect(html).toContain('每页条数');
-    expect(html).toContain('页码');
+    expect(html).toContain(t('alert.notice.pagination.summary', { page: 1, totalPages: 3, from: 1, to: 1, total: 17 }));
+    expect(html).toContain(t('alert.notice.pagination.page-size'));
+    expect(html).toContain(t('alert.notice.pagination.page'));
     expect(html).toContain('data-alert-notice-receiver-row="7"');
-    expect(html).toContain('新增接收对象');
-    expect(html).toContain('接收对象名称');
-    expect(html).toContain('接收对象');
-    expect(html).toContain('通知方式');
-    expect(html).toContain('配置');
-    expect(html).toContain('更新时间');
-    expect(html).toContain('操作');
+    expect(html).toContain(t('alert.notice.receiver.new'));
+    expect(html).toContain(t('alert.notice.receiver.people.name'));
+    expect(html).toContain(t('alert.notice.receiver.people'));
+    expect(html).toContain(t('alert.notice.receiver.type'));
+    expect(html).toContain(t('alert.notice.receiver.setting'));
+    expect(html).toContain(t('common.edit-time'));
+    expect(html).toContain(t('common.edit'));
     expect(html).toContain('Receiver page 0');
-    expect(html).toContain('邮箱');
+    expect(html).toContain(t('alert.notice.type.email'));
     expect(html).toContain('ops@example.com');
     expect(html).toContain('2026-04-10 18:00:00');
-    expect(html).not.toContain('data-stage-section="接收人"');
+    expect(html).not.toContain(`data-stage-section="${t('alert.notice.receivers.title')}"`);
     expect(html).not.toContain('data-evidence-list="true"');
-    expect(html).not.toContain('Receiver page 0||ops@example.com||邮箱 · 更新时间 2026-04-10 18:00:00');
-    expect(html).not.toContain('Rule page 0||已启用 · 发送到 Receiver page||WebhookTemplate · 更新时间 2026-04-10 18:00:00');
     expect(html).not.toContain('WebhookTemplate||');
-    expect(html).not.toContain('当前接收对象:1');
-    expect(html).not.toContain('管理告警发往哪些接收对象。');
     expect(html).not.toContain('data-workbench-page="true"');
     expect(html).not.toContain('data-tone="operator"');
     expect(html).not.toContain('notice/receivers');
@@ -522,9 +506,10 @@ describe('alert notice page', () => {
 
     try {
       const html = await renderAlertNoticePage();
+      const t = createTranslatorMock({ locale: 'zh-CN' });
 
       expect(html).toContain('Receiver missing setting');
-      expect(html).toContain('title="无"');
+      expect(html).toContain(`title="${t('common.none')}"`);
       expect(html).not.toContain('title="-"');
     } finally {
       mockState.renderData = originalRenderData;
@@ -566,10 +551,11 @@ describe('alert notice page', () => {
 
     try {
       const html = await renderAlertNoticePage();
+      const t = createTranslatorMock({ locale: 'zh-CN' });
       const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
 
       expect(html).toContain('Receiver missing type');
-      expect(html).toContain('>无</span>');
+      expect(html).toContain(`>${t('common.none')}</span>`);
       expect(html).not.toContain('>-</span>');
       expect(source).toContain('getNoticeTemplateTypeLabel(template.type, t, emptyValue)');
     } finally {
@@ -724,6 +710,7 @@ describe('alert notice page', () => {
     };
 
     try {
+      const t = createTranslatorMock({ locale: 'zh-CN' });
       const html = await renderAlertNoticePage({
         signal: 'metrics',
         signalContext: {}
@@ -731,8 +718,8 @@ describe('alert notice page', () => {
 
       expect(html).toContain('data-selected-tab="rule"');
       expect(html).toContain('Rule without receivers');
-      expect(html).toContain('未绑定接收对象');
-      expect(html).toContain('系统内置模版');
+      expect(html).toContain(t('alert.notice.row.no-receiver'));
+      expect(html).toContain(t('alert.notice.template.preset.true'));
       expect(html).not.toContain('<td class="px-3 py-3 text-center">-</td>');
     } finally {
       mockState.renderData = originalRenderData;
@@ -762,6 +749,7 @@ describe('alert notice page', () => {
     };
 
     try {
+      const t = createTranslatorMock({ locale: 'zh-CN' });
       const html = await renderAlertNoticePage({
         signal: 'metrics',
         signalContext: {}
@@ -769,7 +757,7 @@ describe('alert notice page', () => {
 
       expect(html).toContain('data-alert-notice-rule-template-display="angular-template-id-fallback"');
       expect(html).toContain('Preset template rule');
-      expect(html).toContain('系统内置模版');
+      expect(html).toContain(t('alert.notice.template.preset.true'));
       expect(html).not.toContain('Stale template');
     } finally {
       mockState.renderData = originalRenderData;
@@ -1181,8 +1169,12 @@ describe('alert notice page', () => {
     expect(html).toContain('hertzbeat.entity.id:7');
     expect(html).toContain('service.name:checkout');
     expect(html).toContain('trace_id:trace-123');
-    expect(html).toContain('来自日志的通知策略上下文');
-    expect(html).toContain('新建通知策略时会按当前实体、服务、环境和链路标签做匹配');
+    expect(html).toContain(
+      createTranslatorMock({ locale: 'zh-CN' })('alert.rule.evidence.notice.title', {
+        signal: createTranslatorMock({ locale: 'zh-CN' })('alert.rule.signal.logs')
+      })
+    );
+    expect(html).toContain(createTranslatorMock({ locale: 'zh-CN' })('alert.rule.evidence.notice.copy'));
     expect(html).toContain('data-alert-notice-evidence-return="true"');
     expect(html).toContain('href="/log/manage?traceId=trace-123"');
     expect(source).not.toContain('useSearchParams');
@@ -1206,7 +1198,7 @@ describe('alert notice page', () => {
     expect(html).toContain('data-alert-notice-evidence-context="signal-route"');
     expect(html).toContain('data-alert-notice-prefill-labels=""');
     expect(html).toContain('data-alert-notice-evidence-labels="generated-labels"');
-    expect(html).toContain('>无</div>');
+    expect(html).toContain(`>${createTranslatorMock({ locale: 'zh-CN' })('common.none')}</div>`);
     expect(html).not.toContain('data-alert-notice-prefill-labels="-"');
     expect(html).not.toContain('>-</div>');
   }, 30_000);
@@ -1225,11 +1217,11 @@ describe('alert notice page', () => {
 
     try {
       const html = await renderAlertNoticePage();
+      const t = createTranslatorMock({ locale: 'zh-CN' });
 
       expect(html).toContain('data-alert-notice-receiver-empty-state="cold-empty-state"');
       expect(html).toContain('data-alert-notice-receiver-empty-icon="cold-empty-icon"');
-      expect(html).toContain('暂无数据');
-      expect(html).not.toContain('总量 0');
+      expect(html).toContain(t('common.no-data'));
     } finally {
       mockState.renderData = previousData;
     }
diff --git a/web-next/app/alert/page.test.tsx b/web-next/app/alert/page.test.tsx
index c82bc703eb..7eab01b9d4 100644
--- a/web-next/app/alert/page.test.tsx
+++ b/web-next/app/alert/page.test.tsx
@@ -218,8 +218,8 @@ describe('alert center page', () => {
         serviceNamespace: 'commerce',
         environment: 'prod',
         timeRange: 'last-1h',
-        returnTo: `${returnTo}&returnLabel=HertzBeat%20%E4%BC%81%E4%B8%9A%E8%BF%90%E7%BB%B4%E6%8B%93%E6%89%91`,
-        returnLabel: 'HertzBeat 企业运维拓扑'
+        returnTo: `${returnTo}&returnLabel=HertzBeat%20operations%20topology`,
+        returnLabel: 'HertzBeat operations topology'
       })
     );
 
diff --git a/web-next/app/alert/silence/page.test.tsx b/web-next/app/alert/silence/page.test.tsx
index 739d1ea4fd..973b466ffd 100644
--- a/web-next/app/alert/silence/page.test.tsx
+++ b/web-next/app/alert/silence/page.test.tsx
@@ -206,7 +206,7 @@ describe('alert silence page', () => {
     expect(html).toContain('&quot;sourceKind&quot;:&quot;database-middleware-connection&quot;');
     expect(html).toContain('&quot;returnTo&quot;:&quot;/topology?viewMode=resource-dependency&amp;sourceKind=database-middleware-connection&amp;edgeId=svc-checkout--res-orders-db');
     expect(html).not.toContain('returnLabel=');
-    expect(html).not.toContain('HertzBeat 企业运维拓扑');
+    expect(html).not.toContain('HertzBeat operations topology');
   }, 30_000);
 
   it('preserves three-signal evidence context into new silence authoring', async () => {
diff --git a/web-next/app/entities/[entityId]/definition/page.test.tsx b/web-next/app/entities/[entityId]/definition/page.test.tsx
index 58872c688f..24753b77dd 100644
--- a/web-next/app/entities/[entityId]/definition/page.test.tsx
+++ b/web-next/app/entities/[entityId]/definition/page.test.tsx
@@ -27,6 +27,8 @@ vi.mock('@/components/providers/i18n-provider', () => ({
   })
 }));
 
+const expectedT = createTranslatorMock({ locale: 'zh-CN' });
+
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
@@ -94,7 +96,7 @@ describe('EntityDefinitionPage', () => {
     const html = renderToStaticMarkup(<EntityDefinitionPage entityId="42" />);
 
     expect(html).toContain('data-entity-definition-workspace="definition"');
-    expect(html).toContain('data-loading-copy="编辑实体定义"');
+    expect(html).toContain(`data-loading-copy="${expectedT('entities.definition.loading')}"`);
     expect(html).toContain(
       'data-cache-key="entity-definition:/entities/42/definition?format=yaml:/entities/definition-activities?entityId=42&amp;limit=8:/entities/definition/templates?limit=8"'
     );
diff --git a/web-next/app/entities/[entityId]/page.test.tsx b/web-next/app/entities/[entityId]/page.test.tsx
index 28869f52a3..8a6faf86c9 100644
--- a/web-next/app/entities/[entityId]/page.test.tsx
+++ b/web-next/app/entities/[entityId]/page.test.tsx
@@ -86,6 +86,7 @@ vi.mock('@/components/workbench/client-workbench', () => ({
 
 vi.mock('@/components/pages/entity-detail-surface', () => ({
   EntityDetailSurface: ({ detail, routeContext }: any) => {
+    const surfaceT = createTranslatorMock({ locale: 'zh-CN' });
     const entity = detail.entity?.entity || {};
     return (
       <main
@@ -96,22 +97,22 @@ vi.mock('@/components/pages/entity-detail-surface', () => ({
         data-entity-detail-route-time-range={routeContext?.timeRange || ''}
       >
         <header data-entity-detail-header="cold-compact-header">
-          实体详情
-          对象优先调查
+          {surfaceT('entities.detail.header.badge')}
+          {surfaceT('entities.detail.header.kicker')}
           <div data-entity-detail-command-row="standard-equal-buttons">
-            <span data-href="/entities">全部实体</span>
-            <button>刷新</button>
-            <span data-href={`/entities/${entity.id}/definition`}>编辑定义</span>
-            <button>删除</button>
-            <span data-href={`/entities/${entity.id}/edit`}>编辑</span>
+            <span data-href="/entities">{surfaceT('entities.detail.action.all-entities')}</span>
+            <button>{surfaceT('common.refresh')}</button>
+            <span data-href={`/entities/${entity.id}/definition`}>{surfaceT('entities.detail.action.edit-definition')}</span>
+            <button>{surfaceT('entities.detail.action.delete')}</button>
+            <span data-href={`/entities/${entity.id}/edit`}>{surfaceT('entities.detail.action.edit')}</span>
           </div>
         </header>
         <div data-entity-detail-count-strip="cold-inline-counts" />
         <div data-entity-detail-signal-grid="cold-detail-grid">
-          <section data-entity-detail-overview-panel="cold-overview-panel">上下文</section>
-          <section data-entity-detail-related-panel="cold-related-panel">相关信号</section>
-          <section data-entity-detail-next-panel="cold-next-panel">下一步</section>
-          <section data-entity-detail-drilldown-panel="cold-drilldown-panel">高级入口</section>
+          <section data-entity-detail-overview-panel="cold-overview-panel">{surfaceT('entities.detail.panel.overview.title')}</section>
+          <section data-entity-detail-related-panel="cold-related-panel">{surfaceT('entities.detail.panel.related.title')}</section>
+          <section data-entity-detail-next-panel="cold-next-panel">{surfaceT('entities.detail.panel.next.title')}</section>
+          <section data-entity-detail-drilldown-panel="cold-drilldown-panel">{surfaceT('entities.detail.panel.drilldown.title')}</section>
         </div>
       </main>
     );
@@ -152,6 +153,7 @@ describe('EntityDetailPage', () => {
   });
 
   it('renders the cold full-width entity detail workbench without old Workbench side panels', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     const source = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/entity-detail-page.tsx'), 'utf8');
     const html = renderToStaticMarkup(<EntityDetailPage entityId="42" />);
 
@@ -166,12 +168,17 @@ describe('EntityDetailPage', () => {
     expect(html).toContain('data-entity-detail-related-panel="cold-related-panel"');
     expect(html).toContain('data-entity-detail-next-panel="cold-next-panel"');
     expect(html).toContain('data-entity-detail-drilldown-panel="cold-drilldown-panel"');
-    expect(html).toContain('实体详情');
-    expect(html).toContain('对象优先调查');
-    expect(html).toContain('刷新');
-    expect(html).toContain('编辑定义');
-    expect(html).toContain('删除');
-    expect(html).toContain('编辑');
+    expect(html).toContain(expectedT('entities.detail.header.badge'));
+    expect(html).toContain(expectedT('entities.detail.header.kicker'));
+    expect(html).toContain(expectedT('entities.detail.action.all-entities'));
+    expect(html).toContain(expectedT('common.refresh'));
+    expect(html).toContain(expectedT('entities.detail.action.edit-definition'));
+    expect(html).toContain(expectedT('entities.detail.action.delete'));
+    expect(html).toContain(expectedT('entities.detail.action.edit'));
+    expect(html).toContain(expectedT('entities.detail.panel.overview.title'));
+    expect(html).toContain(expectedT('entities.detail.panel.related.title'));
+    expect(html).toContain(expectedT('entities.detail.panel.next.title'));
+    expect(html).toContain(expectedT('entities.detail.panel.drilldown.title'));
     expect(html).toContain('/entities/42/definition');
     expect(html).not.toContain('Refresh');
     expect(html).not.toContain('Edit definition');
diff --git a/web-next/app/entities/discovery/page.test.tsx b/web-next/app/entities/discovery/page.test.tsx
index 89650b5d7c..a34dad70ab 100644
--- a/web-next/app/entities/discovery/page.test.tsx
+++ b/web-next/app/entities/discovery/page.test.tsx
@@ -26,6 +26,8 @@ vi.mock('@/components/providers/i18n-provider', () => ({
   })
 }));
 
+const expectedT = createTranslatorMock({ locale: 'zh-CN' });
+
 vi.mock('next/navigation', () => ({
   useSearchParams: () =>
     new URLSearchParams(
@@ -108,7 +110,7 @@ describe('EntityDiscoveryPage', () => {
     const html = renderToStaticMarkup(<EntityDiscoveryPage />);
 
     expect(html).toContain('data-entity-discovery-surface="otlp-cold-discovery-console"');
-    expect(html).toContain('data-loading-copy="遥测发现"');
+    expect(html).toContain(`data-loading-copy="${expectedT('entities.discovery.loading')}"`);
     expect(html).toContain(
       'data-cache-key="entity-discovery:/entities/discovery/governance-presets?limit=8:/entities/discovery/governance-activities?limit=8:/entities/catalog-suggestions?limit=120"'
     );
diff --git a/web-next/app/entities/import/page.test.tsx b/web-next/app/entities/import/page.test.tsx
index ac3164fee7..6caf95860d 100644
--- a/web-next/app/entities/import/page.test.tsx
+++ b/web-next/app/entities/import/page.test.tsx
@@ -84,10 +84,11 @@ describe('EntityImportPage', () => {
   });
 
   it('loads shared import data and renders the shared import surface', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     const html = renderToStaticMarkup(<EntityImportPage />);
 
     expect(html).toContain('data-entity-import-surface="true"');
-    expect(html).toContain('data-loading-copy="导入实体定义"');
+    expect(html).toContain(`data-loading-copy="${expectedT('entities.import.loading')}"`);
     expect(html).toContain(
       'data-cache-key="entity-import:/entities/definition/templates?limit=8:/entities/definition-activities?limit=8"'
     );
diff --git a/web-next/app/entities/page.test.tsx b/web-next/app/entities/page.test.tsx
index 89c9edd3ba..4df21322f9 100644
--- a/web-next/app/entities/page.test.tsx
+++ b/web-next/app/entities/page.test.tsx
@@ -28,52 +28,7 @@ vi.mock('next/link', () => ({
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'entities.list.title': '对象目录',
-        'entities.list.kicker': '对象优先调查',
-        'entities.list.subtitle': '围绕服务、资源与实体定位问题并进入调查,先按对象筛出风险,再决定进入日志、链路、指标还是告警工作台。',
-        'entities.list.environment.select': '选择环境',
-        'entities.list.environment.all': '全部环境',
-        'entities.list.resource-search.placeholder': '搜索并过滤资源属性',
-        'entities.list.action.new': '新建对象',
-        'entities.list.action.create': '创建实体',
-        'entities.list.action.discovery': '从遥测发现',
-        'entities.list.action.import': '导入定义',
-        'entities.list.metric.total': '实体总数',
-        'entities.list.metric.abnormal': '活跃异常对象',
-        'entities.list.metric.alerting': '高风险对象',
-        'entities.list.metric.linked': '有关联对象',
-        'entities.list.search.placeholder': '搜索实体名称、命名空间、负责人',
-        'entities.list.column.object': '对象',
-        'entities.list.column.owner': '负责人',
-        'entities.list.column.progress': '进展',
-        'entities.list.column.evidence': '证据关联',
-        'entities.list.column.type': '类型',
-        'entities.list.column.status': '状态',
-        'entities.list.column.next-action': '下一步动作',
-        'entities.list.column.alerts': '告警',
-        'entities.list.column.monitors': '监控',
-        'entities.list.column.relations': '关系',
-        'entities.list.column.updated': '最近证据',
-        'entities.list.table.total': '{{total}} 个实体',
-        'entities.list.table.range': '显示 {{from}}-{{to}} / {{total}}',
-        'entities.list.pagination.items': '{{from}}-{{to}} / {{total}} 项',
-        'entities.list.row.owner.unset': '未设置',
-        'entities.list.row.progress.incomplete': '{{percent}} · 待完善',
-        'entities.list.row.progress.missing': '当前缺少负责人、处置手册',
-        'entities.list.row.evidence.monitors': '{{count}} 监控',
-        'entities.list.row.evidence.alerts': '{{count}} 告警',
-        'entities.list.row.evidence.identities': '{{count}} 身份标识',
-        'entities.list.row.evidence.updated': '最近更新 {{time}}',
-        'entities.list.row.action.owner': '设置负责人',
-        'entities.list.row.action.logs': '日志线索',
-        'entities.list.row.action.traces': '链路证据',
-        'common.clear': '清除',
-        'common.apply-filters': '应用筛选'
-      }
-    })
+    t: createTranslatorMock({ locale: 'zh-CN' })
   })
 }));
 
@@ -184,20 +139,23 @@ vi.mock('@/lib/entity-manage/query-state', () => ({
 vi.mock('@/lib/entity-manage/view-model', () => ({
   isEntityHealthyStatus: (status: string | null | undefined) =>
     ['healthy', 'up', 'normal'].includes(String(status || '').toLowerCase().replace(/[\s-]+/g, '_')),
-  buildEntityTableRows: vi.fn(() => [
-    {
-      key: '1',
-      name: 'checkout-service',
-      type: '服务',
-      environment: '本地',
-      status: 'healthy',
-      monitorCount: '1',
-      activeAlertCount: '0',
-      relationCount: '2',
-      updatedAt: 'now',
-      href: '/entities/1'
-    }
-  ])
+  buildEntityTableRows: vi.fn(() => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+    return [
+      {
+        key: '1',
+        name: 'checkout-service',
+        type: t('entities.list.type.service'),
+        environment: t('entities.list.environment.local'),
+        status: 'healthy',
+        monitorCount: '1',
+        activeAlertCount: '0',
+        relationCount: '2',
+        updatedAt: 'now',
+        href: '/entities/1'
+      }
+    ];
+  })
 }));
 
 vi.mock('@/lib/format', () => ({
@@ -221,11 +179,12 @@ describe('EntityListPage', () => {
   });
 
   it('loads the entity list through the shared controller contract', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     const source = readFileSync(resolve(process.cwd(), 'app/entities/entity-list-page.tsx'), 'utf8');
     const initialQuery = { search: 'checkout', type: 'service', status: 'healthy' };
     const html = renderToStaticMarkup(<EntityListPage initialQuery={initialQuery} />);
 
-    expect(html).toContain('data-loading-copy="正在加载对象目录"');
+    expect(html).toContain(`data-loading-copy="${expectedT('entities.list.loading')}"`);
     expect(html).toContain('data-entity-list-surface="otlp-cold-entity-console"');
     expect(html).toContain('data-entity-list-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-entity-list-header="cold-compact-header"');
@@ -243,31 +202,31 @@ describe('EntityListPage', () => {
     expect(html).toContain('data-entity-list-table-shell="cold-dense-table"');
     expect(html).toContain('data-entity-list-table="cold-entity-table"');
     expect(html).toContain('data-entity-list-row-actions="cold-inline-actions"');
-    expect(html).toContain('对象优先调查');
-    expect(html).toContain('对象目录');
-    expect(html).toContain('围绕服务、资源与实体定位问题并进入调查');
-    expect(html).toContain('实体总数');
-    expect(html).toContain('活跃异常对象');
-    expect(html).toContain('搜索实体名称、命名空间、负责人');
-    expect(html).toContain('搜索');
-    expect(html).toContain('刷新');
-    expect(html).toContain('创建实体');
-    expect(html).toContain('从遥测发现');
-    expect(html).toContain('导入定义');
-    expect(html).toContain('对象');
-    expect(html).toContain('负责人');
-    expect(html).toContain('进展');
-    expect(html).toContain('证据关联');
-    expect(html).toContain('下一步动作');
-    expect(html).toContain('状态');
-    expect(html).toContain('告警');
-    expect(html).toContain('监控');
-    expect(html).toContain('设置负责人');
-    expect(html).toContain('日志线索');
+    expect(html).toContain(expectedT('entities.list.kicker'));
+    expect(html).toContain(expectedT('entities.list.title'));
+    expect(html).toContain(expectedT('entities.list.subtitle'));
+    expect(html).toContain(expectedT('entities.list.metric.total'));
+    expect(html).toContain(expectedT('entities.list.metric.abnormal'));
+    expect(html).toContain(expectedT('entities.list.search.placeholder'));
+    expect(html).toContain(expectedT('common.search'));
+    expect(html).toContain(expectedT('common.refresh'));
+    expect(html).toContain(expectedT('entities.list.action.create'));
+    expect(html).toContain(expectedT('entities.list.action.discovery'));
+    expect(html).toContain(expectedT('entities.list.action.import'));
+    expect(html).toContain(expectedT('entities.list.column.object'));
+    expect(html).toContain(expectedT('entities.list.column.owner'));
+    expect(html).toContain(expectedT('entities.list.column.progress'));
+    expect(html).toContain(expectedT('entities.list.column.evidence'));
+    expect(html).toContain(expectedT('entities.list.column.next-action'));
+    expect(html).toContain(expectedT('entities.list.column.status'));
+    expect(html).toContain(expectedT('entities.list.row.evidence.alerts', { count: 0 }));
+    expect(html).toContain(expectedT('entities.list.row.evidence.monitors', { count: 1 }));
+    expect(html).toContain(expectedT('entities.list.row.action.owner'));
+    expect(html).toContain(expectedT('entities.list.row.action.logs'));
     expect(html).toContain('checkout-service');
     expect(html).toContain('href="/entities/1"');
-    expect(html).not.toContain('补负责人');
-    expect(html).not.toContain('选择环境 · 全部环境');
+    expect(html).not.toContain(expectedT('entities.editor.attribution.owner.missing-meta'));
+    expect(html).not.toContain(`${expectedT('entities.list.environment.select')} · ${expectedT('entities.list.environment.all')}`);
     expect(html).not.toContain('data-cold-search-input-shell');
     expect(html).not.toContain('data-entity-list-rail=');
     expect(html).not.toContain('data-entity-list-action-panel=');
diff --git a/web-next/app/explorer/page.test.tsx b/web-next/app/explorer/page.test.tsx
index e60233d84c..8c6d1165b8 100644
--- a/web-next/app/explorer/page.test.tsx
+++ b/web-next/app/explorer/page.test.tsx
@@ -37,10 +37,10 @@ vi.mock('../../components/workbench/client-workbench', () => ({
           signalKey: 'trace',
           signalTone: 'trace',
           href: '/trace/manage?traceId=trace-1',
-          signal: '链路',
+          signal: 'Trace',
           service: 'checkout',
           operation: 'POST /checkout',
-          status: '错误',
+          status: 'Error',
           duration: '1.25s',
           timestamp: '2026/03/30 11:50:57'
         },
@@ -49,7 +49,7 @@ vi.mock('../../components/workbench/client-workbench', () => ({
           signalKey: 'log',
           signalTone: 'log',
           href: '/log/manage?traceId=trace-1',
-          signal: '日志',
+          signal: 'Logs',
           service: 'payment',
           operation: 'payment failed',
           status: 'ERROR',
@@ -76,6 +76,7 @@ vi.mock('../../components/workbench/client-workbench', () => ({
 
 describe('explorer page', () => {
   it('renders the OTLP cold Workbench explorer baseline in Chinese', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const { default: ExplorerPage } = await import('./page');
     const html = renderToStaticMarkup(<ExplorerPage />);
 
@@ -100,15 +101,15 @@ describe('explorer page', () => {
     expect(html).toContain('data-explorer-signal-tone="log"');
     expect(html).toContain('href="/trace/manage?traceId=trace-1"');
     expect(html).toContain('href="/log/manage?traceId=trace-1"');
-    expect(html).toContain('查询工作台');
-    expect(html).toContain('运行查询');
-    expect(html).toContain('信号类型');
-    expect(html).toContain('服务');
-    expect(html).toContain('操作');
+    expect(html).toContain(t('explorer.title'));
+    expect(html).toContain(t('explorer.query.run'));
+    expect(html).toContain(t('explorer.signal.aria'));
+    expect(html).toContain(t('explorer.table.service'));
+    expect(html).toContain(t('explorer.table.operation'));
     expect(html).toContain('checkout');
-    expect(html).toContain('保存视图');
-    expect(html).toContain('创建告警');
-    expect(html).toContain('加入仪表盘');
+    expect(html).toContain(t('explorer.actions.save-view'));
+    expect(html).toContain(t('explorer.actions.create-alert'));
+    expect(html).toContain(t('explorer.actions.add-dashboard'));
     expect(html).toContain('data-explorer-query-input="url-owned"');
     expect(html).toContain('data-explorer-signal-select="url-owned"');
     expect(html).toContain('data-explorer-query-url="/explorer?q=checkout&amp;signal=trace"');
diff --git a/web-next/app/hb-i18n/[lang]/route.test.ts b/web-next/app/hb-i18n/[lang]/route.test.ts
index 875fa00f2c..dcac0a4f40 100644
--- a/web-next/app/hb-i18n/[lang]/route.test.ts
+++ b/web-next/app/hb-i18n/[lang]/route.test.ts
@@ -1,54 +1,48 @@
 import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { SUPPLEMENTAL_MESSAGES } from '../../../lib/i18n-runtime-messages';
 
-const readFile = vi.fn();
-
-vi.mock('node:fs', () => ({
-  promises: {
-    readFile
-  }
-}));
+const zhMessages = SUPPLEMENTAL_MESSAGES['zh-CN'] ?? {};
+const enMessages = SUPPLEMENTAL_MESSAGES['en-US'] ?? {};
 
 describe('hb-i18n route', () => {
   beforeEach(() => {
     vi.resetModules();
-    readFile.mockReset();
   });
 
-  it('serves the normalized local locale bundle', async () => {
-    readFile.mockResolvedValue('{"menu.dashboard":"Dashboard"}');
-
+  it('serves the normalized Next runtime locale bundle', async () => {
     const { GET } = await import('./route');
     const response = await GET(new Request('http://localhost/hb-i18n/zh_CN'), {
       params: Promise.resolve({ lang: 'zh_CN' })
     });
 
     await expect(response.json()).resolves.toMatchObject({
-      'menu.dashboard': 'Dashboard',
-      'common.save': '保存',
-      'alert.notice.title': '通知中心'
+      'common.save': zhMessages['common.save'],
+      'layout.setup.progress.headline': zhMessages['layout.setup.progress.headline'],
+      'alert.notice.title': zhMessages['alert.notice.title']
     });
-    expect(readFile).toHaveBeenCalledWith(
-      expect.stringContaining('/web-app/src/assets/i18n/zh-CN.json'),
-      'utf-8'
-    );
   });
 
-  it('lets web-next supplemental messages override legacy web-app locale keys', async () => {
-    readFile.mockResolvedValue(
-      JSON.stringify({
-        'alert.workbench.empty.copy': '当前时间范围和筛选条件下还没有可处理的告警,点击刷新后重新查询最新结果。',
-        'alert.workbench.empty.copy.filtered': '当前筛选条件下没有匹配结果,可以调整筛选条件或重新查询最新结果。'
-      })
-    );
-
+  it('serves web-next-owned alert workbench copy without a legacy locale bundle', async () => {
     const { GET } = await import('./route');
     const response = await GET(new Request('http://localhost/hb-i18n/zh-CN'), {
       params: Promise.resolve({ lang: 'zh-CN' })
     });
 
     await expect(response.json()).resolves.toMatchObject({
-      'alert.workbench.empty.copy': '当前时间范围和筛选条件下还没有可处理的告警。',
-      'alert.workbench.empty.copy.filtered': '当前筛选条件下没有匹配结果,可以调整筛选条件后重新查询。'
+      'alert.workbench.empty.copy': zhMessages['alert.workbench.empty.copy'],
+      'alert.workbench.empty.copy.filtered': zhMessages['alert.workbench.empty.copy.filtered']
+    });
+  });
+
+  it('falls back to English runtime copy when a normalized locale has no local catalog yet', async () => {
+    const { GET } = await import('./route');
+    const response = await GET(new Request('http://localhost/hb-i18n/ja-JP'), {
+      params: Promise.resolve({ lang: 'ja-JP' })
+    });
+
+    await expect(response.json()).resolves.toMatchObject({
+      'common.save': enMessages['common.save'],
+      'layout.setup.progress.headline': enMessages['layout.setup.progress.headline']
     });
   });
 });
diff --git a/web-next/app/hb-i18n/[lang]/route.ts b/web-next/app/hb-i18n/[lang]/route.ts
index f9b0b27e3a..b21ecf29fd 100644
--- a/web-next/app/hb-i18n/[lang]/route.ts
+++ b/web-next/app/hb-i18n/[lang]/route.ts
@@ -1,35 +1,12 @@
-import { promises as fs } from 'node:fs';
-import path from 'node:path';
 import { NextResponse } from 'next/server';
-import { normalizeLocale, type LocaleCode } from '../../../lib/i18n';
+import { normalizeLocale } from '../../../lib/i18n';
 import { SUPPLEMENTAL_MESSAGES } from '../../../lib/i18n-runtime-messages';
 
-const FILES: Record<LocaleCode, string> = {
-  'en-US': 'en-US.json',
-  'zh-CN': 'zh-CN.json',
-  'zh-TW': 'zh-TW.json',
-  'ja-JP': 'ja-JP.json',
-  'pt-BR': 'pt-BR.json'
-};
-
-const WEB_NEXT_OWNED_OVERRIDE_KEYS = new Set([
-  'alert.workbench.empty.copy',
-  'alert.workbench.empty.copy.filtered'
-]);
-
 export async function GET(_: Request, context: { params: Promise<{ lang: string }> }) {
   const { lang } = await context.params;
   const locale = normalizeLocale(lang);
-  const filePath = path.join(process.cwd(), '..', 'web-app', 'src', 'assets', 'i18n', FILES[locale]);
-  const raw = await fs.readFile(filePath, 'utf-8');
-  const supplementalMessages = SUPPLEMENTAL_MESSAGES[locale] || {};
-  const webNextOwnedOverrides = Object.fromEntries(
-    Object.entries(supplementalMessages).filter(([key]) => WEB_NEXT_OWNED_OVERRIDE_KEYS.has(key))
-  );
+  const fallbackMessages = locale.startsWith('zh') ? SUPPLEMENTAL_MESSAGES['zh-CN'] : SUPPLEMENTAL_MESSAGES['en-US'];
+  const supplementalMessages = SUPPLEMENTAL_MESSAGES[locale] || fallbackMessages || {};
 
-  return NextResponse.json({
-    ...supplementalMessages,
-    ...(JSON.parse(raw) as Record<string, string>),
-    ...webNextOwnedOverrides
-  });
+  return NextResponse.json(supplementalMessages);
 }
diff --git a/web-next/app/incidents/page.test.tsx b/web-next/app/incidents/page.test.tsx
index 6eae1a998f..efae690b0a 100644
--- a/web-next/app/incidents/page.test.tsx
+++ b/web-next/app/incidents/page.test.tsx
@@ -33,33 +33,33 @@ vi.mock('../../components/workbench/client-workbench', () => ({
           detailSource: 'status-page-incident-detail',
           detailId: '42',
           queryLabel: '/status/page/incident?pageIndex=0&pageSize=8',
-          title: '故障事件',
-          subtitle: '按 OTLP 工作台的冷色基线统一响应时间线、责任人和证据入口。',
-          kicker: '事件响应台',
+          title: 'Incidents',
+          subtitle: 'Align response timeline, owners, and evidence entry points with the OTLP cold baseline.',
+          kicker: 'Incident response desk',
           metrics: [
-            { label: '打开事件', value: '1', tone: 'warning' },
-            { label: '严重事件', value: '1', tone: 'critical' },
-            { label: '缓解中', value: '1', tone: 'info' },
-            { label: '责任队列', value: '1', tone: 'info' }
+            { label: 'Open incidents', value: '1', tone: 'warning' },
+            { label: 'Critical incidents', value: '1', tone: 'critical' },
+            { label: 'Mitigating', value: '1', tone: 'info' },
+            { label: 'Ownership queues', value: '1', tone: 'info' }
           ],
           incidents: [
             {
               id: '42',
-              title: 'API 延迟事件',
+              title: 'API latency incident',
               severity: 'critical',
-              stage: '调查中',
+              stage: 'Investigating',
               service: 'api-gateway',
               owner: 'platform-oncall',
               openedAt: '2026-04-10 18:00:00',
-              blastRadius: '1 组件'
+              blastRadius: '1 component'
             }
           ],
           timelineRows: [
             {
               id: 'incident-timeline-42-8',
-              title: '2026-04-10 18:05:00 · 已识别',
-              copy: '已开始回滚',
-              meta: 'API 延迟事件',
+              title: '2026-04-10 18:05:00 · Identified',
+              copy: 'Rollback started',
+              meta: 'API latency incident',
               tone: 'warning'
             }
           ],
@@ -67,20 +67,20 @@ vi.mock('../../components/workbench/client-workbench', () => ({
             {
               id: 'incident-owner-42',
               owner: 'platform-oncall',
-              queue: '调查中',
-              copy: 'API 延迟事件',
-              meta: '1 组件 · 2026-04-10 18:00:00',
+              queue: 'Investigating',
+              copy: 'API latency incident',
+              meta: '1 component · 2026-04-10 18:00:00',
               tone: 'critical'
             }
           ],
           nextHops: [
-            { label: '打开概览', href: '/overview', variant: 'subtle' },
-            { label: '查看日志', href: '/log/manage', variant: 'default' }
+            { label: 'Open overview', href: '/overview', variant: 'subtle' },
+            { label: 'View logs', href: '/log/manage', variant: 'default' }
           ],
           selectedIncidentId: '42',
           selectedIncident: {
             id: 42,
-            name: 'API 延迟事件',
+            name: 'API latency incident',
             state: 1,
             contents: []
           },
@@ -107,7 +107,7 @@ describe('incidents page', () => {
         content: [
           {
             id: 42,
-            title: 'API 延迟事件',
+            title: 'API latency incident',
             state: 0,
             startTime: 1712730000000,
             creator: 'platform-oncall',
@@ -121,12 +121,12 @@ describe('incidents page', () => {
       })
       .mockResolvedValueOnce({
         id: 42,
-        name: 'API 延迟事件详情',
+        name: 'API latency incident details',
         state: 1,
         startTime: 1712730000000,
         modifier: 'platform-oncall',
         components: [{ id: 7, name: 'api-gateway' }],
-        contents: [{ id: 8, state: 1, message: '详情已加载', timestamp: 1712730300000 }]
+        contents: [{ id: 8, state: 1, message: 'Detail loaded', timestamp: 1712730300000 }]
       });
     const routeSource = readFileSync(resolve(process.cwd(), 'app/incidents/page.tsx'), 'utf8');
     const source = readFileSync(resolve(process.cwd(), 'app/incidents/incidents-page.tsx'), 'utf8');
@@ -140,7 +140,7 @@ describe('incidents page', () => {
     expect(routeSource).toContain('const resolvedSearchParams = await searchParams');
     expect(routeSource).toContain('return <IncidentsPage initialQuery={initialQuery} />');
     expect(html).toContain('data-client-workbench="true"');
-    expect(html).toContain('data-loading-copy="正在准备页面数据,请稍等。"');
+    expect(html).toContain(`data-loading-copy="${t('common.workbench.loading.copy')}"`);
     expect(html).toContain('data-cache-key="incident-workbench::0:8:0"');
     expect(html).toContain('data-cache-ttl="10000"');
     expect(html).toContain('data-incidents-route="incident-workbench-api-ui-lab-shared"');
@@ -176,8 +176,8 @@ describe('incidents page', () => {
     expect(html).toContain('data-hz-ui="data-table"');
     expect(html).toContain('data-hz-incident-timeline-item="incident-timeline-42-8"');
     expect(html).toContain('data-hz-incident-owner-item="incident-owner-42"');
-    expect(html).toContain('故障事件');
-    expect(html).toContain('API 延迟事件');
+    expect(html).toContain('Incidents');
+    expect(html).toContain('API latency incident');
     expect(html).toContain('data-hz-row-selected="true"');
     expect(apiMessageGet).toHaveBeenNthCalledWith(1, '/status/page/incident?pageIndex=0&pageSize=8');
     expect(apiMessageGet).toHaveBeenNthCalledWith(2, '/status/page/incident/42');
diff --git a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
index b60b7a5b2e..f65f6c00de 100644
--- a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
+++ b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
@@ -293,7 +293,7 @@ export default function OtlpMetricsPage() {
           value: row.value,
           meta: row.meta,
           state: row.state,
-          stateLabel: row.state === 'present' ? '已提供' : '缺失',
+          stateLabel: row.state === 'present' ? t('otlp.metrics.attribution.state.present') : t('otlp.metrics.attribution.state.missing'),
           tone: row.state === 'present' ? 'success' as const : 'critical' as const,
           rowProps: {
             'data-otlp-metrics-attribution-diagnostic-state': row.state
@@ -682,7 +682,7 @@ export default function OtlpMetricsPage() {
                         data-otlp-metrics-chart-title-label="shared-panel-title-label"
                         data-otlp-metrics-chart-title-label-owner="hertzbeat-ui-panel-title-label"
                       >
-                        趋势带
+                        {t('otlp.metrics.trend.title')}
                       </HzPanelTitleLabel>
                       <HzActionGroup
                         layout="end-wrap"
@@ -806,8 +806,8 @@ export default function OtlpMetricsPage() {
                             tone="info"
                             variant="hint"
                             frame="trend-empty"
-                            title="暂无指标趋势"
-                            description="运行查询后展示真实采样点。"
+                            title={t('otlp.metrics.trend.empty.title')}
+                            description={t('otlp.metrics.trend.empty.copy')}
                           />
                         )}
                       </HzTrendFrame>
@@ -820,7 +820,7 @@ export default function OtlpMetricsPage() {
                         casing="plain"
                         spacing="trend-helper"
                       >
-                        {trendBars.length ? `${trendBars.length} 个采样点` : '-'}
+                        {trendBars.length ? t('otlp.metrics.trend.sample-count', { count: trendBars.length }) : '-'}
                       </HzDataMetaText>
                     )}
                   </HzPanelSurface>
@@ -844,7 +844,7 @@ export default function OtlpMetricsPage() {
                     data-otlp-metrics-series-table-header="shared-panel-header"
                     data-otlp-metrics-series-table-header-owner="hertzbeat-ui-panel-header"
                     chrome="transparent-topless"
-                    title="序列集合"
+                    title={t('otlp.metrics.series.collection.title')}
                     meta={
                       <HzStatusBadge
                         data-otlp-metrics-series-table-summary="result-count"
@@ -852,7 +852,7 @@ export default function OtlpMetricsPage() {
                         tone="neutral"
                         size="xs"
                       >
-                        {seriesRows.length} 条
+                        {t('otlp.metrics.scope.series-count', { count: seriesRows.length })}
                       </HzStatusBadge>
                     }
                   />
@@ -910,17 +910,17 @@ export default function OtlpMetricsPage() {
                     columns={[
                       {
                         key: 'name',
-                        header: '指标名称',
+                        header: t('otlp.metrics.series.context.metric-name'),
                         render: row => <HzDataCellText variant="title" display="block">{row.title}</HzDataCellText>
                       },
                       {
                         key: 'service',
-                        header: '服务',
+                        header: t('otlp.metrics.field.service'),
                         render: row => <HzDataCellText variant="value">{row.copy}</HzDataCellText>
                       },
                       {
                         key: 'entity',
-                        header: '实体',
+                        header: t('otlp.metrics.series.context.entity'),
                         render: row => (
                           <HzDataCellStack
                             display="block"
@@ -953,17 +953,17 @@ export default function OtlpMetricsPage() {
                       },
                       {
                         key: 'latest',
-                        header: '最新值',
+                        header: t('otlp.metrics.evidence.latest-value'),
                         render: row => <HzDataCellText variant="value" font="mono" tone="bright" data-otlp-metrics-series-latest-owner="hertzbeat-ui-data-cell-text">{row.meta}</HzDataCellText>
                       },
                       {
                         key: 'points',
-                        header: '采样点',
+                        header: t('otlp.metrics.evidence.samples'),
                         render: row => <HzDataCellText variant="value">{row.pointCount}</HzDataCellText>
                       },
                       {
                         key: 'time',
-                        header: '最近时间',
+                        header: t('otlp.metrics.table.recent-time'),
                         render: () => <HzDataCellText variant="timestamp">{formatTime(latestObservedAt)}</HzDataCellText>
                       }
                     ]}
@@ -983,7 +983,7 @@ export default function OtlpMetricsPage() {
                     data-otlp-metrics-detail-panel-header="shared-panel-header"
                     data-otlp-metrics-detail-panel-header-owner="hertzbeat-ui-panel-header"
                     chrome="transparent"
-                    eyebrow="详情面板"
+                    eyebrow={t('otlp.metrics.detail.eyebrow')}
                     title={firstSeries.title}
                     subtitle={workbenchState.chartLabel}
                   />
@@ -1015,16 +1015,16 @@ export default function OtlpMetricsPage() {
                       data-otlp-metrics-detail-context-rows="shared-detail-rows"
                       data-otlp-metrics-detail-context-rows-owner="hertzbeat-ui-detail-rows"
                       offset="top"
-                      heading="查询上下文"
+                      heading={t('otlp.metrics.detail.query-context')}
                       rows={metricsDetailContextRows}
                     />
                     {selectedSeriesContextRows.length > 0 ? (
                       <HzDetailRows
                         data-otlp-metrics-selected-series-context="selected-series-attribution"
                         data-otlp-metrics-selected-series-context-owner="hertzbeat-ui-detail-rows"
-                        aria-label="当前选中序列 关联实体 当前服务 采集模板 当前环境"
+                        aria-label={t('otlp.metrics.detail.selected-series.aria')}
                         boundary="top"
-                        heading="当前选中序列"
+                        heading={t('otlp.metrics.series.context.selected-series')}
                         rows={selectedSeriesContextDetailRows}
                       />
                     ) : null}
@@ -1032,9 +1032,9 @@ export default function OtlpMetricsPage() {
                       <HzDetailRows
                         data-otlp-metrics-selected-series-evidence="real-sample-evidence"
                         data-otlp-metrics-selected-series-evidence-owner="hertzbeat-ui-detail-rows"
-                        aria-label="指标证据 采样点 最新值 值域 采样窗口 关联链路"
+                        aria-label={t('otlp.metrics.detail.evidence.aria')}
                         boundary="top"
-                        heading="指标证据"
+                        heading={t('otlp.metrics.detail.evidence.heading')}
                         rows={selectedSeriesEvidenceDetailRows}
                       />
                     ) : null}
@@ -1051,7 +1051,7 @@ export default function OtlpMetricsPage() {
                     >
                       {canOpenEntity ? (
                         <HzButtonLink component={Link} href={handoffLinks.entityHref} size="md" layout="full" data-otlp-metrics-entity-action="true">
-                          实体详情
+                          {t('topology.context-link.entity')}
                         </HzButtonLink>
                       ) : (
                         <HzDisabledActionShell
@@ -1067,21 +1067,21 @@ export default function OtlpMetricsPage() {
                             title={missingEntityHandoffTitle}
                             aria-label={missingEntityHandoffTitle}
                           >
-                            实体详情
+                            {t('topology.context-link.entity')}
                           </HzButton>
                         </HzDisabledActionShell>
                       )}
                       <HzButtonLink component={Link} href={handoffLinks.alertHandlingHref} size="md" layout="full" data-otlp-metrics-alert-handling-action="true">
-                        告警处理
+                        {t('otlp.metrics.handoff.alerts')}
                       </HzButtonLink>
                       <HzButtonLink component={Link} href={handoffLinks.logsHref} size="md" layout="full" data-otlp-metrics-logs-action="true">
-                        查看日志
+                        {t('otlp.metrics.handoff.logs.action')}
                       </HzButtonLink>
                       <HzButtonLink component={Link} href={handoffLinks.tracesHref} size="md" layout="full" data-otlp-metrics-traces-action="true">
-                        查看链路
+                        {t('otlp.metrics.handoff.traces.action')}
                       </HzButtonLink>
                       <HzButtonLink component={Link} href={handoffLinks.entitiesHref} size="md" layout="full" data-otlp-metrics-entities-action="true">
-                        对象目录
+                        {t('overview.lane.entities.title')}
                       </HzButtonLink>
                     </HzActionGroup>
                   </HzPanelSection>
@@ -1096,9 +1096,9 @@ export default function OtlpMetricsPage() {
                         data-otlp-metrics-linked-record-summary="log-trace-alert-links"
                         data-otlp-metrics-linked-record-summary-panel="collapsible"
                         data-otlp-metrics-linked-record-summary-owner="hertzbeat-ui-collapsible-section"
-                        aria-label="关联记录 历史日志 链路瀑布图 告警处理"
-                        title="关联记录"
-                        meta="日志 / 链路 / 告警"
+                        aria-label={t('otlp.metrics.linked-records.aria')}
+                        title={t('otlp.metrics.linked-records.title')}
+                        meta={t('otlp.metrics.linked-records.meta')}
                         surface="inset"
                       >
                         <div
@@ -1106,8 +1106,8 @@ export default function OtlpMetricsPage() {
                           data-otlp-metrics-linked-record-handoff-owner="hertzbeat-ui-context-handoff"
                         >
                           <HzContextHandoff
-                            title="证据跳转"
-                            context="日志 / 链路 / 告警"
+                            title={t('otlp.metrics.linked-records.handoff.title')}
+                            context={t('otlp.metrics.linked-records.meta')}
                             targets={linkedRecordHandoffTargets}
                             frame="flush"
                           />
@@ -1118,15 +1118,15 @@ export default function OtlpMetricsPage() {
                       <HzCollapsibleSection
                         data-otlp-metrics-attribution-diagnostics-panel="collapsible"
                         data-otlp-metrics-attribution-diagnostics-panel-owner="hertzbeat-ui-collapsible-section"
-                        aria-label="归因诊断 hertzbeat.entity_id hertzbeat.entity_name hertzbeat.workspace_id hertzbeat.collector hertzbeat.template"
-                        title="归因诊断"
+                        aria-label={t('otlp.metrics.attribution.diagnostics.aria')}
+                        title={t('otlp.metrics.attribution.diagnostics.title')}
                         meta="hertzbeat.*"
                         surface="inset"
                       >
                         <HzAttributeDiagnostics
                           data-otlp-metrics-attribution-diagnostics="hertzbeat-attribute-diagnostics"
                           data-otlp-metrics-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"
-                          title="归因诊断"
+                          title={t('otlp.metrics.attribution.diagnostics.title')}
                           namespaceLabel="hertzbeat.*"
                           rows={attributionDiagnosticRows}
                           frame="embedded"
@@ -1137,14 +1137,14 @@ export default function OtlpMetricsPage() {
                       data-otlp-metrics-entity-context="hertzbeat-signal-entity-context"
                       data-otlp-metrics-entity-context-panel="collapsible"
                       data-otlp-metrics-entity-context-panel-owner="hertzbeat-ui-collapsible-section"
-                      aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"
-                      title="实体上下文"
-                      meta="当前窗口"
+                      aria-label={t('otlp.metrics.entity-context.aria')}
+                      title={t('otlp.metrics.entity-context.title')}
+                      meta={t('otlp.metrics.entity-context.current-window')}
                       surface="inset"
                     >
                       <HzDetailRows
                         data-otlp-metrics-entity-context-owner="hertzbeat-ui-detail-rows"
-                        heading="实体上下文"
+                        heading={t('otlp.metrics.entity-context.title')}
                         padding="compact-y"
                         rows={entityContextDetailRows}
                       />
diff --git a/web-next/app/ingestion/otlp/metrics/page.test.tsx b/web-next/app/ingestion/otlp/metrics/page.test.tsx
index 781de4a004..9ad0ee446b 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -7,7 +7,7 @@ import { createRoot, type Root } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../../test/i18n-test-helper';
-import { SUPPLEMENTAL_MESSAGES } from '../../../../lib/i18n-runtime-messages';
+import type { TranslationParams } from '../../../../lib/i18n';
 
 (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
 
@@ -30,6 +30,11 @@ const mockState = vi.hoisted(() => ({
 
 const apiMessageGet = vi.fn();
 const loadOtlpMetricsConsole = vi.fn();
+const zhT = createTranslatorMock({ locale: 'zh-CN' });
+
+function tZh(key: string, params?: TranslationParams) {
+  return zhT(key, params);
+}
 
 vi.mock('next/link', () => ({
   default: ({ href, children, ...props }: any) => <a href={href} {...props}>{children}</a>
@@ -47,7 +52,7 @@ vi.mock('next/navigation', () => ({
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock({ overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] })
+    t: zhT
   })
 }));
 
@@ -133,11 +138,11 @@ vi.mock('@/lib/otlp-metrics/controller', () => ({
 
 vi.mock('@/lib/signal-route-context', () => ({
   buildSignalEntityContextRows: () => [
-    { label: '当前实体', value: 'checkout', meta: 'entityId 7' },
-    { label: '当前服务', value: 'checkout', meta: 'payments' },
-    { label: '当前环境', value: 'prod', meta: '环境' },
-    { label: '时间范围', value: 'last-1h', meta: '查询窗口' },
-    { label: '采集来源', value: 'OTLP', meta: 'HertzBeat OTLP 接入' }
+    { label: tZh('signal.context.entity.label'), value: 'checkout', meta: 'entityId 7' },
+    { label: tZh('signal.context.service.label'), value: 'checkout', meta: 'payments' },
+    { label: tZh('signal.context.environment.label'), value: 'prod', meta: tZh('signal.context.environment.meta') },
+    { label: tZh('signal.context.time.label'), value: 'last-1h', meta: tZh('signal.context.time.meta.default') },
+    { label: tZh('signal.context.source.label'), value: 'OTLP', meta: tZh('signal.context.source.otlp.meta') }
   ],
   readEpochMillisRouteParam: (value: string | null | undefined) => {
     const trimmed = value?.trim();
@@ -168,67 +173,67 @@ vi.mock('@/lib/signal-route-context', () => ({
 
 vi.mock('@/lib/otlp-metrics/view-model', () => ({
   buildMetricsExplorerState: () => ({
-    chartLabel: `${mockState.metricSeries.length} 条有数据序列`,
+    chartLabel: tZh('otlp.metrics.explorer.chart-label', { count: mockState.metricSeries.length }),
     hasSeries: mockState.metricSeries.length > 0,
-    emptyTitle: '暂无指标序列',
-    noMetricsTitle: '确认时间范围、实体归因、采集器和监控模板后再查看指标。',
-    sendMetricsLabel: '等待 OTLP 指标写入',
-    seriesCountLabel: `${mockState.metricSeries.length} 条序列`
+    emptyTitle: tZh('otlp.metrics.explorer.empty-title'),
+    noMetricsTitle: tZh('otlp.metrics.explorer.no-metrics-title'),
+    sendMetricsLabel: tZh('otlp.metrics.explorer.waiting-ingest'),
+    seriesCountLabel: tZh('otlp.metrics.explorer.series-count', { count: mockState.metricSeries.length })
   }),
   buildConsoleFacts: () => [
-    { label: '指标序列', value: '0' },
-    { label: '有数据序列', value: '0' },
-    { label: '存储来源', value: 'prometheus' },
-    { label: '最近上报', value: '-' }
+    { label: tZh('otlp.metrics.stats.total-series'), value: '0' },
+    { label: tZh('otlp.metrics.stats.non-empty-series'), value: '0' },
+    { label: tZh('otlp.metrics.stats.datasource'), value: 'prometheus' },
+    { label: tZh('otlp.metrics.stats.latest-observed'), value: '-' }
   ],
   buildConsoleMetrics: () => [
-    { label: '有数据序列', value: '0' },
-    { label: '序列总数', value: '0' },
-    { label: '接入状态', value: '空' }
+    { label: tZh('otlp.metrics.stats.non-empty-series'), value: '0' },
+    { label: tZh('otlp.metrics.stats.series-total'), value: '0' },
+    { label: tZh('otlp.metrics.stats.intake-state'), value: tZh('common.empty') }
   ],
   buildContextRows: () => [
-    { title: '当前服务', copy: 'checkout', meta: 'payments' },
-    { title: '时间范围', copy: '2026-04-12 20:00:00 → 2026-04-12 20:00:00', meta: 'ok' }
+    { title: tZh('otlp.metrics.context.current-service'), copy: 'checkout', meta: 'payments' },
+    { title: tZh('otlp.metrics.context.time-range'), copy: '2026-04-12 20:00:00 → 2026-04-12 20:00:00', meta: 'ok' }
   ],
   buildMetricSeriesContextRows: (series: any) => series
     ? [
-        { label: '指标名称', value: series.name, meta: '当前选中序列' },
-        { label: '关联实体', value: series.labels['hertzbeat.entity_name'] || '-', meta: `entityId ${series.labels['hertzbeat.entity_id'] || '-'}` },
-        { label: '当前服务', value: series.labels['service.name'] || series.labels.service_name || '-', meta: series.labels['service.namespace'] || series.labels.service_namespace || '-' },
-        { label: '采集模板', value: series.labels['hertzbeat.template'] || '-', meta: `采集器 ${series.labels['hertzbeat.collector'] || '-'}` },
-        { label: '当前环境', value: series.labels['deployment.environment.name'] || series.labels.deployment_environment_name || '-', meta: '部署环境' }
+        { label: tZh('otlp.metrics.series.context.metric-name'), value: series.name, meta: tZh('otlp.metrics.series.context.selected-series') },
+        { label: tZh('otlp.metrics.series.context.entity'), value: series.labels['hertzbeat.entity_name'] || '-', meta: tZh('otlp.metrics.series.entity-id', { entityId: series.labels['hertzbeat.entity_id'] || '-' }) },
+        { label: tZh('otlp.metrics.series.context.service'), value: series.labels['service.name'] || series.labels.service_name || '-', meta: series.labels['service.namespace'] || series.labels.service_namespace || '-' },
+        { label: tZh('otlp.metrics.series.context.template'), value: series.labels['hertzbeat.template'] || '-', meta: tZh('otlp.metrics.series.context.collector', { collector: series.labels['hertzbeat.collector'] || '-' }) },
+        { label: tZh('otlp.metrics.series.context.environment'), value: series.labels['deployment.environment.name'] || series.labels.deployment_environment_name || '-', meta: tZh('otlp.metrics.series.context.deployment-environment') }
       ]
     : [],
   buildMetricSeriesEvidenceRows: (series: any) => series
     ? [
-        { label: '采样点', value: String((series.points || []).length), meta: '真实采样点' },
-        { label: '最新值', value: String(series.latestValue ?? '-'), meta: '最近采样' },
-        { label: '值域', value: '12 - 20', meta: '平均 16' },
-        { label: '采样窗口', value: 'T1000 → T2000', meta: '真实采样时间' },
-        { label: '关联链路', value: series.labels.trace_id || '-', meta: series.labels.span_id || '-' }
+        { label: tZh('otlp.metrics.evidence.samples'), value: String((series.points || []).length), meta: tZh('otlp.metrics.evidence.real-samples') },
+        { label: tZh('otlp.metrics.evidence.latest-value'), value: String(series.latestValue ?? '-'), meta: 'Recent sample' },
+        { label: tZh('otlp.metrics.evidence.value-range'), value: '12 - 20', meta: tZh('otlp.metrics.evidence.average', { average: 16 }) },
+        { label: tZh('otlp.metrics.evidence.sample-window'), value: 'T1000 → T2000', meta: tZh('otlp.metrics.evidence.real-sample-time') },
+        { label: tZh('otlp.metrics.evidence.linked-trace'), value: series.labels.trace_id || '-', meta: series.labels.span_id || '-' }
       ]
     : [],
   buildMetricSeriesLinkedRecordRows: (series: any, handoffLinks: any) => series
     ? [
         {
           key: 'logs',
-          label: '历史日志',
-          value: series.labels.trace_id ? '按链路查看' : '按服务查看',
-          meta: series.labels.span_id ? '历史日志会定位到当前 span' : '缺少链路 ID 时按服务筛选',
+          label: tZh('otlp.metrics.handoff.logs'),
+          value: series.labels.trace_id ? tZh('otlp.metrics.handoff.logs-by-trace') : tZh('otlp.metrics.handoff.logs-by-service'),
+          meta: series.labels.span_id ? tZh('otlp.metrics.handoff.logs-current-span') : tZh('otlp.metrics.handoff.logs-service-filter'),
           href: handoffLinks.logsHref
         },
         {
           key: 'traces',
-          label: '链路瀑布图',
-          value: series.labels.trace_id ? '可打开' : '等待链路 ID',
-          meta: series.labels.span_id ? '打开完整链路并保留当前 span' : '指标没有链路 ID,暂不能定位链路',
+          label: tZh('otlp.metrics.handoff.traces'),
+          value: series.labels.trace_id ? tZh('otlp.metrics.handoff.trace-open') : tZh('otlp.metrics.handoff.trace-waiting-id'),
+          meta: series.labels.span_id ? tZh('otlp.metrics.handoff.trace-full-current-span') : tZh('otlp.metrics.handoff.trace-missing-id'),
           href: handoffLinks.tracesHref
         },
         {
           key: 'alerts',
-          label: '告警处理',
-          value: series.labels['hertzbeat.entity_id'] ? '带实体处理' : '按服务处理',
-          meta: series.labels['hertzbeat.entity_id'] ? '按实体、服务和指标进入告警' : '缺少实体 ID 时按服务进入告警',
+          label: tZh('otlp.metrics.handoff.alerts'),
+          value: series.labels['hertzbeat.entity_id'] ? tZh('otlp.metrics.handoff.alerts-by-entity') : tZh('otlp.metrics.handoff.alerts-by-service'),
+          meta: series.labels['hertzbeat.entity_id'] ? tZh('otlp.metrics.handoff.alerts-by-entity-meta') : tZh('otlp.metrics.handoff.alerts-by-service-meta'),
           href: handoffLinks.alertHandlingHref
         }
       ]
@@ -243,11 +248,11 @@ vi.mock('@/lib/otlp-metrics/view-model', () => ({
       meta
     });
     return [
-      row('hertzbeat.entity_id', series.labels['hertzbeat.entity_id'], series.labels['hertzbeat.entity_id'] ? '可打开实体详情' : '缺少实体 ID,实体详情会保持禁用'),
-      row('hertzbeat.entity_name', series.labels['hertzbeat.entity_name'], '用于展示实体名称'),
-      row('hertzbeat.workspace_id', series.labels['hertzbeat.workspace_id'], '缺少工作区字段时使用当前部署上下文'),
-      row('hertzbeat.collector', series.labels['hertzbeat.collector'], '采集器来源'),
-      row('hertzbeat.template', series.labels['hertzbeat.template'], '监控模板归属')
+      row('hertzbeat.entity_id', series.labels['hertzbeat.entity_id'], series.labels['hertzbeat.entity_id'] ? tZh('otlp.metrics.attribution.entity-id.present') : tZh('otlp.metrics.attribution.entity-id.missing')),
+      row('hertzbeat.entity_name', series.labels['hertzbeat.entity_name'], tZh('otlp.metrics.attribution.entity-name')),
+      row('hertzbeat.workspace_id', series.labels['hertzbeat.workspace_id'], tZh('otlp.metrics.attribution.workspace-id')),
+      row('hertzbeat.collector', series.labels['hertzbeat.collector'], tZh('otlp.metrics.attribution.collector')),
+      row('hertzbeat.template', series.labels['hertzbeat.template'], tZh('otlp.metrics.attribution.template'))
     ];
   },
   buildMetricSeriesViews: () => mockState.metricSeries,
@@ -268,7 +273,9 @@ vi.mock('@/lib/otlp-metrics/view-model', () => ({
     copy: series.labels['service.name'] || series.labels.service_name || '-',
     meta: series.latestValue == null ? '-' : String(series.latestValue),
     entityLabel: series.labels['hertzbeat.entity_name'] || series.labels.hertzbeat_entity_name || '-',
-    entityMeta: series.labels['hertzbeat.entity_id'] || series.labels.hertzbeat_entity_id ? `entityId ${series.labels['hertzbeat.entity_id'] || series.labels.hertzbeat_entity_id}` : '等待实体归因',
+    entityMeta: series.labels['hertzbeat.entity_id'] || series.labels.hertzbeat_entity_id
+      ? tZh('otlp.metrics.series.entity-id', { entityId: series.labels['hertzbeat.entity_id'] || series.labels.hertzbeat_entity_id })
+      : tZh('otlp.metrics.series.entity-missing'),
     entityState: series.labels['hertzbeat.entity_id'] || series.labels.hertzbeat_entity_id ? 'present' : 'missing'
   })),
   buildMetricTrendBars: () => [],
@@ -480,10 +487,7 @@ describe('otlp metrics page', () => {
     expect(source).not.toContain('<span className="shrink-0 font-semibold text-[#7f8a9d]">{pill.label}</span>');
     expect(source).not.toContain('<span className="truncate font-semibold text-[#dbe5f3]">{pill.value}</span>');
     expect(source).not.toContain('data-otlp-metrics-header-copy="operator-query-copy"');
-    expect(source).not.toContain('查询指标序列,按服务、命名空间、环境或标签分组。');
-    expect(source).not.toContain('按当前查询条件展示');
     expect(source).not.toContain('mx-auto flex w-full max-w-[1600px] flex-col gap-4 px-6 py-6');
-    expect(source).not.toContain('按 OTLP 上下文、时间范围和指标表达式查看序列,并把采集集群、监控模板和阈值规则接到同一条排障链路里。');
     expect(source).not.toContain('xl:grid-cols-[minmax(0,1fr)_auto]');
     expect(source).toContain('data-otlp-metrics-time-toolbar="top-right-corner"');
     expect(source).toContain('data-otlp-metrics-time-toolbar-owner="hertzbeat-ui-workbench-layout"');
@@ -516,7 +520,6 @@ describe('otlp metrics page', () => {
     expect(source).not.toContain('className={`${panelClass} px-4 py-3`}');
     expect(source).not.toContain("'border-[var(--ops-primary)] px-3'");
     expect(source).not.toContain('data-otlp-metrics-action-row="aligned-context-actions"');
-    expect(source).not.toContain('返回 OTLP 接入');
     expect(source).not.toContain('href="/setting/collector"');
     expect(source).not.toContain('href="/setting/define"');
     expect(source).toContain('variant="narrow-rail"');
@@ -658,7 +661,7 @@ describe('otlp metrics page', () => {
     expect(source).toContain('data-otlp-metrics-series-table-header-owner="hertzbeat-ui-panel-header"');
     expect(source).toContain('chrome="transparent-topless"');
     expect(source).not.toContain('className="border-x-0 border-t-0 bg-transparent"');
-    expect(source).toContain('title="序列集合"');
+    expect(source).toContain("title={t('otlp.metrics.series.collection.title')}");
     expect(source).toContain('data-otlp-metrics-series-table-summary-owner="hertzbeat-ui-status-badge"');
     expect(source).toContain('data-otlp-metrics-series-table-empty-state="shared-empty-state"');
     expect(source).toContain('data-otlp-metrics-series-table-empty-state-owner="hertzbeat-ui-empty-state"');
@@ -752,21 +755,19 @@ describe('otlp metrics page', () => {
     expect(source).not.toContain('className="rounded-none border-0 bg-transparent"');
     expect(source).toContain('attributionDiagnosticRows');
     expect(source).toContain("'data-otlp-metrics-attribution-diagnostic-state': row.state");
-    expect(source).toContain('当前选中序列');
-    expect(source).toContain('指标证据');
-    expect(source).toContain('关联记录');
-    expect(source).toContain('历史日志');
-    expect(source).toContain('链路瀑布图');
-    expect(source).toContain('采样窗口');
-    expect(source).toContain('归因诊断');
-    expect(source).toContain('hertzbeat.entity_id');
-    expect(source).toContain('采集模板');
-    expect(source).not.toContain('<p className="text-[11px] font-semibold text-[#8792a5]">当前选中序列</p>');
-    expect(source).not.toContain('<p className="text-[11px] font-semibold text-[#8792a5]">指标证据</p>');
+    expect(source).toContain("t('otlp.metrics.series.context.selected-series')");
+    expect(source).toContain("t('otlp.metrics.detail.evidence.heading')");
+    expect(source).toContain("t('otlp.metrics.linked-records.title')");
+    expect(source).toContain("t('otlp.metrics.handoff.logs.action')");
+    expect(source).toContain("t('otlp.metrics.handoff.traces.action')");
+    expect(source).toContain("t('otlp.metrics.attribution.diagnostics.title')");
+    expect(source).toContain('namespaceLabel="hertzbeat.*"');
+    expect(source).toContain("t('otlp.metrics.detail.selected-series.aria')");
+    expect(source).not.toContain('<p className="text-[11px] font-semibold text-[#8792a5]">');
     expect(source).not.toContain('grid grid-cols-2 gap-px overflow-hidden rounded-[3px] border border-[#252b35] bg-[#252b35]');
     expect(source).not.toContain('className="col-span-2 min-w-0 bg-[#10141b] px-3 py-2"');
     expect(source).not.toContain('className="grid grid-cols-[minmax(0,1fr)_52px] gap-2 text-[11px]"');
-    expect(source).not.toContain("row.state === 'present' ? '已提供' : '缺失'}</span>");
+    expect(source).toContain("stateLabel: row.state === 'present' ? t('otlp.metrics.attribution.state.present')");
     expect(source).not.toContain('className="grid grid-cols-[72px_minmax(0,1fr)] gap-2 text-[11px]"');
     expect(source).not.toContain('hover:border-[#344052] hover:bg-[#121823]');
     expect(source).not.toContain('<table className="w-full border-collapse text-left text-[12px]">');
@@ -777,10 +778,10 @@ describe('otlp metrics page', () => {
     expect(source).not.toContain('<Database className="h-8 w-8 text-[#7d8798]" aria-hidden="true" />');
     expect(source).not.toContain('<summary className="flex cursor-pointer list-none items-center justify-between gap-2 px-2.5 py-2 text-[11px] font-semibold text-[#8792a5]">');
     expect(source).not.toContain('className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#9ca7ba]"');
-    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#8792a5]">详情面板</p>');
+    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#8792a5]">');
     expect(source).not.toContain('<h2 className="mt-2 truncate text-[18px] font-semibold text-[#f2f6fb]">{firstSeries.title}</h2>');
     expect(source).not.toContain('className="flex h-full min-w-0 flex-1 items-center justify-center rounded-[3px] border border-dashed border-[#2a303a] bg-[#0c1016] px-3 text-center"');
-    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#a7b0bf]">暂无指标趋势</p>');
+    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#a7b0bf]">');
     expect(source).not.toContain('<span>{workbenchState.seriesCountLabel}</span>');
     expect(source).toContain('data-otlp-metrics-workbench-grid="series-detail-split"');
     expect(source).toContain('data-otlp-metrics-workbench-grid-owner="hertzbeat-ui-workbench-layout"');
@@ -797,10 +798,9 @@ describe('otlp metrics page', () => {
     expect(source).toContain('data-otlp-metrics-series-set-summary-owner="hertzbeat-ui-stat-strip"');
     expect(source).toContain('data-otlp-metrics-series-set-summary="service-entity-scope"');
     expect(source).toContain('seriesSetScopeRows.map');
-    expect(source).toContain('序列集合');
-    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#8792a5]">序列集合</p>');
+    expect(source).toContain("t('otlp.metrics.series.collection.title')");
+    expect(source).not.toContain('<p className="text-[12px] font-semibold text-[#8792a5]">');
     expect(source).not.toContain('className="flex min-w-0 items-center justify-between gap-3"');
-    expect(source).not.toContain('最近序列');
     expect(source).toContain('data-otlp-metrics-detail-panel="cold-detail-panel"');
     expect(source).toContain('data-otlp-metrics-detail-panel-owner="hertzbeat-ui-panel-surface"');
     expect(source).toContain('data-otlp-metrics-detail-panel-priority="secondary-inspector"');
@@ -842,27 +842,19 @@ describe('otlp metrics page', () => {
     expect(source).not.toContain('data-otlp-metrics-entities-action="true" className="w-full px-2"');
     expect(source).not.toContain('className="grid grid-cols-2 gap-2 border-t border-[#252b35] px-4 py-3"');
     expect(source).toContain('data-otlp-metrics-entity-context="hertzbeat-signal-entity-context"');
-    expect(source).toContain('aria-label="实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源"');
+    expect(source).toContain("aria-label={t('otlp.metrics.entity-context.aria')}");
     expect(source).not.toContain('data-otlp-metrics-hertzbeat-loop="collector-template-alert-loop"');
     expect(source).toContain("t('otlp.metrics.title')");
     expect(source).toContain("t('otlp.metrics.query.run')");
     expect(source).not.toContain('data-otlp-metrics-action-row="aligned-context-actions"');
-    expect(source).not.toContain('返回 OTLP 接入');
     expect(source).not.toContain('href="/setting/collector"');
     expect(source).not.toContain('href="/setting/define"');
-    expect(source).not.toContain('接入质量');
-    expect(source).not.toContain('HertzBeat 采集闭环');
-    expect(source).not.toContain('>采集闭环<');
-    expect(source).not.toContain('从接入健康、采集节点、模板归属到告警处理,保留 HertzBeat 自己的运维闭环。');
-    expect(source).toContain('实体上下文');
-    expect(source).toContain('采集来源');
-    expect(source).toContain('实体详情');
-    expect(source).toContain('告警处理');
+    expect(source).toContain("t('otlp.metrics.entity-context.title')");
+    expect(source).toContain("t('topology.context-link.entity')");
+    expect(source).toContain("t('otlp.metrics.handoff.alerts')");
     expect(source).toContain('href={handoffLinks.alertHandlingHref}');
     expect(source).not.toContain('data-otlp-metrics-alert-context-hint="entity-trace-alert-handoff"');
-    expect(source).not.toContain('按当前实体、服务和已带入的链路上下文查看相关告警');
     expect(source).not.toContain('data-otlp-metrics-signal-handoff-hint="metric-log-trace-context"');
-    expect(source).not.toContain('当前指标序列的实体、服务和链路标签会带入日志与链路工作台');
     expect(source).toContain('data-otlp-metrics-entity-action="true"');
     expect(source).toContain('data-otlp-metrics-alert-handling-action="true"');
     expect(source).toContain('data-otlp-metrics-logs-action="true"');
@@ -883,7 +875,6 @@ describe('otlp metrics page', () => {
     expect(source).not.toContain('data-otlp-metrics-entity-action-disabled="missing-entity-id"\n                            size="md"\n                            className="w-full px-2"');
     expect(source).toContain('title={missingEntityHandoffTitle}');
     expect(source).toContain('buildOtlpMetricsRoute');
-    expect(source).not.toContain('告警规则');
     expect(source).not.toContain('returnLabel=');
     expect(source).not.toContain('signoz-metrics-explorer');
     expect(source).not.toContain('signoz-query-builder');
@@ -1082,12 +1073,12 @@ describe('otlp metrics page', () => {
     expect(html).toContain('data-hz-state-variant="hint"');
     expect(html).toContain('data-hz-state-frame="trend-empty"');
     expect(html).toContain('data-hz-state-hint-owner="hertzbeat-ui-state-notice"');
-    expect(html).toContain('暂无指标趋势');
-    expect(html).toContain('运行查询后展示真实采样点。');
+    expect(html).toContain(tZh('otlp.metrics.trend.empty.title'));
+    expect(html).toContain(tZh('otlp.metrics.trend.empty.copy'));
     expect(html).toContain('data-otlp-metrics-empty-guidance="operator-no-data-guidance"');
     expect(html).toContain('data-otlp-metrics-empty-state="honest-no-series"');
     expect(html).toContain('data-otlp-metrics-empty-state-context="applied-query-visible"');
-    expect(html).toContain('确认时间范围、实体归因、采集器和监控模板后再查看指标。');
+    expect(html).toContain(tZh('otlp.metrics.explorer.no-metrics-title'));
     expect(html).not.toContain('data-otlp-metrics-trend-bar="empty-series-placeholder"');
     expect(html).toContain('data-otlp-metrics-workbench-grid="series-detail-split"');
     expect(html).toContain('data-otlp-metrics-workbench-grid-owner="hertzbeat-ui-workbench-layout"');
@@ -1140,42 +1131,23 @@ describe('otlp metrics page', () => {
     expect(html).not.toContain('data-otlp-metrics-handoff-actions="compact-context-actions"');
     expect(html).not.toContain('data-otlp-metrics-entity-context="hertzbeat-signal-entity-context"');
     expect(html).not.toContain('data-otlp-metrics-hertzbeat-loop="collector-template-alert-loop"');
-    expect(html).toContain('指标工作台');
-    expect(html).not.toContain('查询指标序列,按服务、命名空间、环境或标签分组。');
-    expect(html).not.toContain('按当前查询条件展示');
-    expect(html).toContain('服务');
+    expect(html).toContain(tZh('otlp.metrics.title'));
+    expect(html).toContain(tZh('otlp.metrics.field.service'));
     expect(html).toContain('checkout');
-    expect(html).toContain('命名空间');
+    expect(html).toContain(tZh('otlp.metrics.field.namespace'));
     expect(html).toContain('payments');
-    expect(html).not.toContain('按 OTLP 上下文、时间范围和指标表达式查看序列,并把采集集群、监控模板和阈值规则接到同一条排障链路里。');
-    expect(html).toContain('指标查询');
-    expect(html).toContain('分组');
-    expect(html).toContain('聚合方式');
-    expect(html).toContain('趋势带');
-    expect(html).toContain('序列集合');
-    expect(html).toContain('服务范围');
-    expect(html).toContain('命名空间');
-    expect(html).toContain('序列');
-    expect(html).not.toContain('最近序列');
-    expect(html).not.toContain('详情面板');
-    expect(html).toContain('时间范围');
-    expect(html).toContain('运行查询');
+    expect(html).toContain(tZh('otlp.metrics.query.aria'));
+    expect(html).toContain(tZh('otlp.metrics.field.group-by'));
+    expect(html).toContain(tZh('otlp.metrics.aggregation.aria'));
+    expect(html).toContain(tZh('otlp.metrics.trend.title'));
+    expect(html).toContain(tZh('otlp.metrics.series.collection.title'));
+    expect(html).toContain(tZh('otlp.metrics.scope.service'));
+    expect(html).toContain(tZh('otlp.metrics.scope.series'));
+    expect(html).toContain(tZh('otlp.metrics.context.time-range'));
+    expect(html).toContain(tZh('otlp.metrics.query.run'));
     expect(html).toContain('data-otlp-metrics-header-actions="removed"');
     expect(html).not.toContain('data-otlp-metrics-action-row="aligned-context-actions"');
-    expect(html).not.toContain('返回 OTLP 接入');
-    expect(html).not.toContain('采集集群');
-    expect(html).not.toContain('阈值规则');
-    expect(html).not.toContain('HertzBeat 采集闭环');
-    expect(html).not.toContain('采集闭环');
-    expect(html).not.toContain('接入质量');
-    expect(html).not.toContain('从接入健康、采集节点、模板归属到告警处理,保留 HertzBeat 自己的运维闭环。');
-    expect(html).not.toContain('实体上下文');
-    expect(html).not.toContain('当前实体');
-    expect(html).not.toContain('采集来源');
-    expect(html).not.toContain('实体详情');
-    expect(html).not.toContain('告警处理');
     expect(html).not.toContain('status=firing');
-    expect(html).not.toContain('告警规则');
     expect(html).not.toContain('Summary');
     expect(html).not.toContain('Explorer');
     expect(html).not.toContain('Views');
@@ -1184,13 +1156,7 @@ describe('otlp metrics page', () => {
     expect(html).not.toContain('Create an Alert');
     expect(html).not.toContain('Add to Dashboard');
     expect(html).not.toContain('Sending metrics to SigNoZ');
-    expect(html).not.toContain('保存视图');
-    expect(html).not.toContain('>数据源<');
-    expect(html).not.toContain('>查询模式<');
-    expect(html).not.toContain('>查询状态<');
     expect(html).not.toContain('>service.name<');
-    expect(html).not.toContain('>时间窗口<');
-    expect(html).not.toContain('1 个查询');
   });
 
   it('renders a shared return action when metrics inherit a trace return path', async () => {
@@ -1208,7 +1174,7 @@ describe('otlp metrics page', () => {
     expect(html).toContain('data-otlp-metrics-header-action-icon="return-source"');
     expect(html).toContain('data-otlp-metrics-header-action-icon-owner="hertzbeat-ui-button-icon"');
     expect(html).toContain('href="/trace/manage?traceId=trace-123&amp;spanId=span-456&amp;serviceName=checkout"');
-    expect(html).toContain('返回来源');
+    expect(html).toContain(tZh('otlp.metrics.route.action.return-source'));
   });
 
   it('does not show fake zero metric cards or a selected-series inspector when no real series exists', async () => {
@@ -1230,9 +1196,7 @@ describe('otlp metrics page', () => {
     expect(html).not.toContain('data-otlp-metrics-detail-panel-body="compact-evidence-stack"');
     expect(html).not.toContain('data-otlp-metrics-handoff-actions="compact-context-actions"');
     expect(html).not.toContain('data-otlp-metrics-entity-action-disabled="missing-entity-id"');
-    expect(html).not.toContain('接入状态</p><p');
-    expect(html).not.toContain('就绪');
-    expect(html).not.toContain('空</p>');
+    expect(html).not.toContain('data-otlp-metrics-facts-grid="inline-summary-strip"');
   });
 
   it('renders OTLP metrics chart zoom as local observation with an explicit disabled apply action until zoomed', async () => {
@@ -1282,7 +1246,7 @@ describe('otlp metrics page', () => {
     expect(html).toContain('data-hz-status-badge-part="value"');
     expect(html).not.toContain('data-otlp-metrics-facts-grid="inline-summary-strip"');
     expect(html).toContain('disabled=""');
-    expect(html).toContain('应用为查询时间');
+    expect(html).toContain(tZh('time.context.zoom.apply'));
   });
 
   it('applies OTLP metric chart zoom to the route only after the explicit apply action', async () => {
@@ -1720,15 +1684,15 @@ describe('otlp metrics page', () => {
     expect(evidencePanel?.getAttribute('data-otlp-metrics-selected-series-evidence-owner')).toBe('hertzbeat-ui-detail-rows');
     expect(evidencePanel?.getAttribute('data-hz-detail-rows-owner')).toBe('hertzbeat-ui-detail-rows');
     expect(evidencePanel?.getAttribute('data-hz-detail-rows-boundary')).toBe('top');
-    expect(evidencePanel?.textContent).toContain('指标证据');
-    expect(evidencePanel?.textContent).toContain('采样窗口');
+    expect(evidencePanel?.textContent).toContain(tZh('otlp.metrics.detail.evidence.heading'));
+    expect(evidencePanel?.textContent).toContain(tZh('otlp.metrics.evidence.sample-window'));
     const detailBody = interactionContainer.querySelector('[data-otlp-metrics-detail-panel-body="compact-evidence-stack"]') as HTMLElement | null;
     expect(detailBody).not.toBeNull();
     expect(detailBody?.getAttribute('data-otlp-metrics-detail-panel-body-owner')).toBe('hertzbeat-ui-panel-section');
     expect(detailBody?.getAttribute('data-hz-panel-section-owner')).toBe('hertzbeat-ui-panel-section');
     expect(detailBody?.getAttribute('data-hz-panel-section-divider')).toBe('none');
-    expect(evidencePanel?.textContent).toContain('真实采样时间');
-    expect(evidencePanel?.textContent).toContain('关联链路');
+    expect(evidencePanel?.textContent).toContain(tZh('otlp.metrics.evidence.real-sample-time'));
+    expect(evidencePanel?.textContent).toContain(tZh('otlp.metrics.evidence.linked-trace'));
     expect(evidencePanel?.textContent).toContain('trace-inventory');
     const sampleHelper = interactionContainer.querySelector('[data-otlp-metrics-trend-sample-helper="real-sample-count"]') as HTMLElement | null;
     expect(sampleHelper).not.toBeNull();
@@ -1740,7 +1704,7 @@ describe('otlp metrics page', () => {
     expect(selectedContextPanel?.getAttribute('data-otlp-metrics-selected-series-context-owner')).toBe('hertzbeat-ui-detail-rows');
     expect(selectedContextPanel?.getAttribute('data-hz-detail-rows-owner')).toBe('hertzbeat-ui-detail-rows');
     expect(selectedContextPanel?.getAttribute('data-hz-detail-rows-boundary')).toBe('top');
-    expect(selectedContextPanel?.textContent).toContain('当前选中序列');
+    expect(selectedContextPanel?.textContent).toContain(tZh('otlp.metrics.series.context.selected-series'));
     expect(selectedContextPanel?.textContent).toContain('fastapi');
     const detailPanelHeader = interactionContainer.querySelector('[data-otlp-metrics-detail-panel-header="shared-panel-header"]') as HTMLElement | null;
     const detailPanel = interactionContainer.querySelector('[data-otlp-metrics-detail-panel="cold-detail-panel"]') as HTMLElement | null;
@@ -1757,7 +1721,7 @@ describe('otlp metrics page', () => {
     expect(detailPanelHeader?.getAttribute('data-hz-ui')).toBe('panel-header');
     expect(detailPanelHeader?.getAttribute('data-hz-panel-header-owner')).toBe('hertzbeat-ui-panel-header');
     expect(detailPanelHeader?.getAttribute('data-hz-panel-header-chrome')).toBe('transparent');
-    expect(detailPanelHeader?.textContent).toContain('详情面板');
+    expect(detailPanelHeader?.textContent).toContain(tZh('otlp.metrics.detail.eyebrow'));
     expect(detailPanelHeader?.textContent).toContain('db.client.duration');
     const detailSummaryStats = interactionContainer.querySelector('[data-otlp-metrics-detail-summary-stats="shared-stat-strip"]') as HTMLElement | null;
     expect(detailSummaryStats?.getAttribute('data-otlp-metrics-detail-summary-stats-owner')).toBe('hertzbeat-ui-stat-strip');
@@ -1769,11 +1733,11 @@ describe('otlp metrics page', () => {
     expect(detailContextRows?.getAttribute('data-otlp-metrics-detail-context-rows-owner')).toBe('hertzbeat-ui-detail-rows');
     expect(detailContextRows?.getAttribute('data-hz-detail-rows-owner')).toBe('hertzbeat-ui-detail-rows');
     expect(detailContextRows?.getAttribute('data-hz-detail-rows-offset')).toBe('top');
-    expect(detailContextRows?.textContent).toContain('查询上下文');
+    expect(detailContextRows?.textContent).toContain(tZh('otlp.metrics.detail.query-context'));
     const entityContextRows = interactionContainer.querySelector('[data-otlp-metrics-entity-context="hertzbeat-signal-entity-context"] [data-otlp-metrics-entity-context-owner="hertzbeat-ui-detail-rows"]') as HTMLElement | null;
     expect(entityContextRows?.getAttribute('data-hz-detail-rows-owner')).toBe('hertzbeat-ui-detail-rows');
     expect(entityContextRows?.getAttribute('data-hz-detail-rows-padding')).toBe('compact-y');
-    expect(entityContextRows?.textContent).toContain('实体上下文');
+    expect(entityContextRows?.textContent).toContain(tZh('otlp.metrics.entity-context.title'));
 
     const logHref = (interactionContainer.querySelector('[data-otlp-metrics-logs-action="true"]') as HTMLAnchorElement | null)?.href;
     const traceHref = (interactionContainer.querySelector('[data-otlp-metrics-traces-action="true"]') as HTMLAnchorElement | null)?.href;
@@ -1913,15 +1877,15 @@ describe('otlp metrics page', () => {
     expect(html).toContain('data-hz-ui="context-handoff"');
     expect(html).toContain('data-hz-context-handoff-owner="hertzbeat-ui-context-handoff"');
     expect(html).toContain('data-hz-context-handoff-frame="flush"');
-    expect(html).toContain('关联记录');
-    expect(html).toContain('证据跳转');
-    expect(html).toContain('历史日志');
-    expect(html).toContain('按链路查看');
-    expect(html).toContain('历史日志会定位到当前 span');
-    expect(html).toContain('链路瀑布图');
-    expect(html).toContain('打开完整链路并保留当前 span');
-    expect(html).toContain('告警处理');
-    expect(html).toContain('按实体、服务和指标进入告警');
+    expect(html).toContain(tZh('otlp.metrics.linked-records.title'));
+    expect(html).toContain(tZh('otlp.metrics.linked-records.handoff.title'));
+    expect(html).toContain(tZh('otlp.metrics.handoff.logs'));
+    expect(html).toContain(tZh('otlp.metrics.handoff.logs-by-trace'));
+    expect(html).toContain(tZh('otlp.metrics.handoff.logs-current-span'));
+    expect(html).toContain(tZh('otlp.metrics.handoff.traces'));
+    expect(html).toContain(tZh('otlp.metrics.handoff.trace-full-current-span'));
+    expect(html).toContain(tZh('otlp.metrics.handoff.alerts'));
+    expect(html).toContain(tZh('otlp.metrics.handoff.alerts-by-entity-meta'));
     expect(html).toContain('data-otlp-metrics-linked-record-action="logs"');
     expect(html).toContain('data-otlp-metrics-linked-record-action="traces"');
     expect(html).toContain('data-otlp-metrics-linked-record-action="alerts"');
@@ -1965,15 +1929,15 @@ describe('otlp metrics page', () => {
     expect(html).toContain('data-hz-attribute-diagnostics-frame="embedded"');
     expect(html).toContain('data-hz-attribute-diagnostic-row="true"');
     expect(html).toContain('data-hz-attribute-diagnostic-badge="true"');
-    expect(html).toContain('归因诊断');
+    expect(html).toContain(tZh('otlp.metrics.attribution.diagnostics.title'));
     expect(html).toContain('hertzbeat.entity_id');
-    expect(html).toContain('缺少实体 ID,实体详情会保持禁用');
+    expect(html).toContain(tZh('otlp.metrics.attribution.entity-id.missing'));
     expect(html).toContain('hertzbeat.collector');
     expect(html).toContain('collector-local');
     expect(html).toContain('hertzbeat.template');
     expect(html).toContain('jvm');
-    expect(html).toContain('title="缺少实体 ID,暂时不能打开实体详情。"');
-    expect(html).toContain('aria-label="缺少实体 ID,暂时不能打开实体详情。"');
+    expect(html).toContain(`title="${tZh('otlp.metrics.handoff.entity-disabled')}"`);
+    expect(html).toContain(`aria-label="${tZh('otlp.metrics.handoff.entity-disabled')}"`);
     expect(html).not.toContain('data-otlp-metrics-entity-action="true" href="/entities/');
     expect(html).toContain('/entities?search=self-monitor');
   });
diff --git a/web-next/app/ingestion/otlp/metrics/route-state.test.ts b/web-next/app/ingestion/otlp/metrics/route-state.test.ts
index a9044ac7f5..f1c4d8146a 100644
--- a/web-next/app/ingestion/otlp/metrics/route-state.test.ts
+++ b/web-next/app/ingestion/otlp/metrics/route-state.test.ts
@@ -17,13 +17,16 @@
 
 import { describe, expect, it } from 'vitest';
 import { buildOtlpMetricsRoute, hasMetricsDisplayReturnLabel } from './route-state';
+import { createTranslatorMock } from '../../../../test/i18n-test-helper';
 
 describe('otlp metrics route state', () => {
   it('drops localized display labels from metrics workbench URLs', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const localizedReturnLabel = t('log.manage.route.title');
     const route = buildOtlpMetricsRoute({
       entityId: '7',
       entityName: 'checkout',
-      returnTo: '/log/manage?returnLabel=日志工作台',
+      returnTo: `/log/manage?returnLabel=${localizedReturnLabel}`,
       traceId: 'trace-123',
       spanId: 'span-456',
       query: 'http_server_duration_milliseconds_count',
@@ -43,7 +46,7 @@ describe('otlp metrics route state', () => {
       '/ingestion/otlp/metrics?entityId=7&entityName=checkout&returnTo=%2Flog%2Fmanage&traceId=trace-123&spanId=span-456&query=http_server_duration_milliseconds_count&aggregation=sum&groupBy=service_name&timeRange=last-1h&serviceName=checkout&serviceNamespace=payments&environment=prod&collector=collector-a&template=spring-boot&start=1712730000000&end=1712733600000'
     );
     expect(route).not.toContain('returnLabel=');
-    expect(route).not.toContain('%E6%97%A5%E5%BF%97%E5%B7%A5%E4%BD%9C%E5%8F%B0');
+    expect(route).not.toContain(encodeURIComponent(localizedReturnLabel));
   });
 
   it('detects legacy display labels in direct and nested return URLs', () => {
diff --git a/web-next/app/ingestion/otlp/page.test.tsx b/web-next/app/ingestion/otlp/page.test.tsx
index 7b8aa82c62..2d3a0fe5bc 100644
--- a/web-next/app/ingestion/otlp/page.test.tsx
+++ b/web-next/app/ingestion/otlp/page.test.tsx
@@ -2,7 +2,13 @@ import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
-import { SUPPLEMENTAL_MESSAGES } from '../../../lib/i18n-runtime-messages';
+import type { TranslationParams } from '../../../lib/i18n';
+
+const zhT = createTranslatorMock({ locale: 'zh-CN' });
+
+function tZh(key: string, params?: TranslationParams) {
+  return zhT(key, params);
+}
 
 vi.mock('next/link', () => ({
   default: ({ href, children, ...props }: any) => <a href={href} {...props}>{children}</a>
@@ -18,7 +24,7 @@ vi.mock('next/navigation', () => ({
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
     locale: 'zh-CN',
-    t: createTranslatorMock({ overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] })
+    t: zhT
   })
 }));
 
@@ -67,19 +73,19 @@ vi.mock('@/lib/otlp-center/controller', () => ({
 vi.mock('@/lib/otlp-center/view-model', () => ({
   buildBindingRows: (items: any[]) => items.map(item => ({ title: item.name || item.displayName, copy: item.primaryIdentityValue || '-', meta: 'binds 1' })),
   buildCollectionLoopLinks: () => [
-    { key: 'otlp-intake', title: 'OTLP 三信号接入', copy: '接入 OpenTelemetry 指标、日志和链路,再进入对应工作台排查。', href: '/ingestion/otlp', meta: '三信号' },
-    { key: 'traditional-monitoring', title: '传统监控资源', copy: '继续保留主机、数据库、中间件、网络设备等模板化监控。', href: '/monitors', meta: '已有资源' },
-    { key: 'collector-cluster', title: '采集器集群', copy: '管理私有化部署中的采集节点、任务分发和接入状态。', href: '/setting/collector', meta: 'Collector' },
-    { key: 'monitoring-template', title: '监控模板', copy: '维护多协议采集模板,让传统监控和 OTLP 实体归到同一对象。', href: '/setting/define', meta: '模板' },
-    { key: 'service-discovery', title: '服务发现', copy: '把新发现的服务、资源和遥测身份确认到 HertzBeat 实体。', href: '/entities/discovery', meta: '发现' },
-    { key: 'object-directory', title: '对象目录', copy: '围绕实体查看资源、三信号、拓扑和告警处理上下文。', href: '/entities', meta: '实体' }
+    { key: 'otlp-intake', title: tZh('otlp.collection-loop.otlp-intake.title'), copy: tZh('otlp.collection-loop.otlp-intake.copy'), href: '/ingestion/otlp', meta: tZh('otlp.collection-loop.otlp-intake.meta') },
+    { key: 'traditional-monitoring', title: tZh('otlp.collection-loop.traditional-monitoring.title'), copy: tZh('otlp.collection-loop.traditional-monitoring.copy'), href: '/monitors', meta: tZh('otlp.collection-loop.traditional-monitoring.meta') },
+    { key: 'collector-cluster', title: tZh('otlp.collection-loop.collector-cluster.title'), copy: tZh('otlp.collection-loop.collector-cluster.copy'), href: '/setting/collector', meta: tZh('otlp.collection-loop.collector-cluster.meta') },
+    { key: 'monitoring-template', title: tZh('otlp.collection-loop.monitoring-template.title'), copy: tZh('otlp.collection-loop.monitoring-template.copy'), href: '/setting/define', meta: tZh('otlp.collection-loop.monitoring-template.meta') },
+    { key: 'service-discovery', title: tZh('otlp.collection-loop.service-discovery.title'), copy: tZh('otlp.collection-loop.service-discovery.copy'), href: '/entities/discovery', meta: tZh('otlp.collection-loop.service-discovery.meta') },
+    { key: 'object-directory', title: tZh('otlp.collection-loop.object-directory.title'), copy: tZh('otlp.collection-loop.object-directory.copy'), href: '/entities', meta: tZh('otlp.collection-loop.object-directory.meta') }
   ],
   buildGuideAuthRows: () => [],
   buildReadinessRows: () => [
-    { key: 'signals', title: '三信号接入', copy: '3 / 3 活跃', meta: 'Metrics 12 · Logs 9 · Traces 4', tone: 'success' },
-    { key: 'latest-report', title: '最近上报', copy: '2026-04-12 20:00:00', meta: '已收到遥测', tone: 'neutral' },
-    { key: 'entity-binding', title: '实体归因', copy: '1 个实体', meta: '对象目录', tone: 'success' },
-    { key: 'service-discovery', title: '服务发现', copy: '2 个服务', meta: '最近 24 小时', tone: 'success' }
+    { key: 'signals', title: tZh('otlp.readiness.signals.title'), copy: tZh('otlp.readiness.signals.copy', { active: 3, total: 3 }), meta: 'Metrics 12 · Logs 9 · Traces 4', tone: 'success' },
+    { key: 'latest-report', title: tZh('otlp.readiness.latest.title'), copy: '2026-04-12 20:00:00', meta: tZh('otlp.readiness.latest.received'), tone: 'neutral' },
+    { key: 'entity-binding', title: tZh('otlp.readiness.entity.title'), copy: tZh('otlp.readiness.entity.copy', { count: 1 }), meta: tZh('otlp.readiness.entity.meta'), tone: 'success' },
+    { key: 'service-discovery', title: tZh('otlp.readiness.discovery.title'), copy: tZh('otlp.readiness.discovery.copy', { count: 2 }), meta: tZh('otlp.readiness.discovery.meta'), tone: 'success' }
   ],
   buildSelfCheckRows: (checks: any[]) => checks.map(check => ({
     key: check.key,
@@ -121,10 +127,10 @@ vi.mock('@/components/workbench/client-workbench', () => ({
         traces: { signal: 'traces', totalCount: 4, summary: 'traces ready', active: true },
         recentEvents: [{ signal: 'metrics', title: 'metrics event', detail: 'metric batch', observedAt: 1712730000000 }],
         readinessChecks: [
-          { key: 'collector', title: 'Collector 集群', status: 'warning', summary: '2 / 3 在线', detail: '1 个采集节点离线' },
-          { key: 'storage', title: '历史存储', status: 'success', summary: '1 / 1 可用', detail: 'HistoryDataReader 可用' },
-          { key: 'query', title: '查询服务', status: 'success', summary: '指标、日志和链路查询可用', detail: 'PromQL 与历史查询可用' },
-          { key: 'greptime', title: 'GreptimeDB', status: 'success', summary: 'SQL 自检通过', detail: 'SELECT 1 成功' }
+          { key: 'collector', title: 'Collector cluster', status: 'warning', summary: '2 / 3 online', detail: '1 collector node offline' },
+          { key: 'storage', title: 'History storage', status: 'success', summary: '1 / 1 available', detail: 'HistoryDataReader available' },
+          { key: 'query', title: 'Query service', status: 'success', summary: 'Metrics, logs, and traces query available', detail: 'PromQL and history query available' },
+          { key: 'greptime', title: 'GreptimeDB', status: 'success', summary: 'SQL self-check passed', detail: 'SELECT 1 passed' }
         ]
       },
       guide: {
@@ -176,12 +182,12 @@ describe('otlp page', () => {
     const sections = [
       {
         key: 'migrate',
-        label: '现有接入',
+        label: 'Existing intake',
         items: [
           {
             key: 'open-telemetry',
-            label: '已有 OpenTelemetry',
-            description: '复用现有 OpenTelemetry Collector 或 SDK 管道。',
+            label: 'Existing OpenTelemetry',
+            description: 'Reuse an existing OpenTelemetry Collector or SDK pipeline.',
             href: '/entities/discovery',
             icon
           }
@@ -189,12 +195,12 @@ describe('otlp page', () => {
       },
       {
         key: 'apm-traces',
-        label: 'APM / 链路',
+        label: 'APM / traces',
         items: [
           {
             key: 'java',
             label: 'Java',
-            description: 'Java OpenTelemetry SDK 配置。',
+            description: 'Java OpenTelemetry SDK configuration.',
             href: '/trace/manage',
             icon
           }
@@ -215,13 +221,13 @@ describe('otlp page', () => {
     const sections = [
       {
         key: 'apm-traces',
-        label: 'APM / 链路',
+        label: 'APM / traces',
         items: [
-          { key: 'java', label: 'Java', description: 'Java OpenTelemetry SDK 配置。', href: '/trace/manage', icon },
-          { key: 'python', label: 'Python', description: 'Python OpenTelemetry SDK 配置。', href: '/trace/manage', icon },
-          { key: 'nodejs', label: 'JavaScript', description: 'Node.js 与浏览器链路配置。', href: '/trace/manage', icon },
-          { key: 'golang', label: 'Golang', description: 'Go 服务链路配置。', href: '/trace/manage', icon },
-          { key: 'php', label: 'PHP', description: 'PHP 应用链路配置。', href: '/trace/manage', icon }
+          { key: 'java', label: 'Java', description: 'Java OpenTelemetry SDK configuration.', href: '/trace/manage', icon },
+          { key: 'python', label: 'Python', description: 'Python OpenTelemetry SDK configuration.', href: '/trace/manage', icon },
+          { key: 'nodejs', label: 'JavaScript', description: 'Node.js and browser trace configuration.', href: '/trace/manage', icon },
+          { key: 'golang', label: 'Golang', description: 'Go service trace configuration.', href: '/trace/manage', icon },
+          { key: 'php', label: 'PHP', description: 'PHP application trace configuration.', href: '/trace/manage', icon }
         ]
       }
     ];
@@ -263,15 +269,14 @@ describe('otlp page', () => {
     expect(html).toContain('data-otlp-readiness-row="latest-report"');
     expect(html).toContain('data-otlp-readiness-row="entity-binding"');
     expect(html).toContain('data-otlp-readiness-row="service-discovery"');
-    expect(html).toContain('接入自检');
+    expect(html).toContain(tZh('otlp.section.readiness.kicker'));
     expect(html).not.toContain('Collector Quality');
-    expect(html).toContain('真实状态');
-    expect(html).toContain('展示已确认的三信号、实体归因和最近上报;未接入的健康项不显示为空指标。');
-    expect(html).not.toContain('再决定进入指标、日志、链路还是告警规则。');
-    expect(html).toContain('三信号接入');
-    expect(html).toContain('最近上报');
-    expect(html).toContain('实体归因');
-    expect(html).toContain('服务发现');
+    expect(html).toContain(tZh('otlp.section.readiness.title'));
+    expect(html).toContain(tZh('otlp.section.readiness.copy'));
+    expect(html).toContain(tZh('otlp.readiness.signals.title'));
+    expect(html).toContain(tZh('otlp.readiness.latest.title'));
+    expect(html).toContain(tZh('otlp.readiness.entity.title'));
+    expect(html).toContain(tZh('otlp.readiness.discovery.title'));
     expect(html).toContain('data-otlp-self-check-status="backend-backed"');
     expect(html).toContain('data-otlp-self-check-row="collector"');
     expect(html).toContain('data-otlp-self-check-row="storage"');
@@ -282,27 +287,18 @@ describe('otlp page', () => {
     expect(html).toContain('data-otlp-entity-candidate-row="service.name:billing:commerce:prod"');
     expect(html).toContain('data-otlp-entity-candidate-signals="logs,metrics"');
     expect(html).toContain('data-otlp-entity-candidate-identities="service.name=billing;service.namespace=commerce;deployment.environment.name=prod"');
-    expect(html).toContain('候选实体');
+    expect(html).toContain(tZh('otlp.section.candidates.title'));
     expect(html).toContain('billing');
     expect(html).toContain('service.name = billing');
     expect(html).toContain('commerce · prod · logs, metrics');
     expect(html).toContain('href="/entities/discovery?identityKey=service.name&amp;identityValue=billing&amp;serviceName=billing&amp;serviceNamespace=commerce&amp;environment=prod"');
-    expect(html).toContain('运行自检');
-    expect(html).toContain('Collector 集群');
-    expect(html).toContain('历史存储');
-    expect(html).toContain('查询服务');
+    expect(html).toContain(tZh('otlp.section.self-check.kicker'));
+    expect(html).toContain('Collector cluster');
+    expect(html).toContain('History storage');
+    expect(html).toContain('Query service');
     expect(html).toContain('GreptimeDB');
-    expect(html).toContain('SQL 自检通过');
-    expect(html).toContain('检查采集节点、历史存储、查询服务和 GreptimeDB 连接状态。');
-    expect(html).not.toContain('来自当前后端');
-    expect(html).not.toContain('只读检查');
-    expect(html).not.toContain('查询执行器');
-    expect(html).not.toContain('接收量');
-    expect(html).not.toContain('丢弃量');
-    expect(html).not.toContain('解析失败');
-    expect(html).not.toContain('实体归并失败');
-    expect(html).not.toContain('模板绑定');
-    expect(html).not.toContain('Collector 节点');
+    expect(html).toContain('SQL self-check passed');
+    expect(html).toContain(tZh('otlp.section.self-check.copy'));
     expect(html).toContain('data-otlp-center-stepper="hertzbeat-intake-steps"');
     expect(html).toContain('data-otlp-center-stepper-phase="source-selection"');
     expect(html).toContain('data-otlp-center-stepper-align="centered-860-balanced"');
@@ -390,60 +386,41 @@ describe('otlp page', () => {
     expect(html).toContain('/ingestion/otlp/metrics?');
     expect(html).toContain('traceId=trace-123');
     expect(html).toContain('spanId=span-456');
-    expect(html).toContain('OTLP 协议接入');
-    expect(html).toContain('选择 OTLP 数据源');
-    expect(html).toContain('配置协议与令牌');
-    expect(html).toContain('进入信号工作台');
-    expect(html).toContain('已有 OpenTelemetry');
-    expect(html).toContain('已有可观测平台');
-    expect(html).toContain('接入服务、日志和指标后,在对象目录和排障工作台继续处理。');
-    expect(html).not.toContain('商业可观测迁移');
-    expect(html).not.toContain('把已有服务、日志和指标流接入 HertzBeat 采集闭环。');
+    expect(html).toContain(tZh('otlp.hero.title'));
+    expect(html).toContain(tZh('otlp.stepper.source'));
+    expect(html).toContain(tZh('otlp.stepper.protocol'));
+    expect(html).toContain(tZh('otlp.stepper.workbench'));
+    expect(html).toContain(tZh('otlp.source.item.open-telemetry.label'));
+    expect(html).toContain(tZh('otlp.source.item.commercial-observability.label'));
+    expect(html).toContain(tZh('otlp.source.item.commercial-observability.description'));
     expect(html).toContain('Honeycomb');
-    expect(html).toContain('把链路数据接入链路工作台,并关联服务与日志。');
-    expect(html).toContain('自托管 OTLP 管道');
-    expect(html).toContain('复用内网或离线环境中的 OTLP Collector 管道。');
-    expect(html).not.toContain('自托管管道迁移');
-    expect(html).not.toContain('把链路优先的可观测流程迁移到统一查询台。');
-    expect(html).not.toContain('把自托管 OTLP 管道迁移到本页的采集接入流程。');
-    expect(html).toContain('APM / 链路');
+    expect(html).toContain(tZh('otlp.source.item.honeycomb.description'));
+    expect(html).toContain(tZh('otlp.source.item.self-hosted-observability.label'));
+    expect(html).toContain(tZh('otlp.source.item.self-hosted-observability.description'));
+    expect(html).toContain(tZh('otlp.source.section.apm-traces'));
     expect(html).toContain('.NET');
     expect(html).toContain('Deno');
-    expect(html).toContain('Kubernetes Pod 日志');
-    expect(html).toContain('Cloudflare 日志');
-    expect(html).toContain('筛选');
-    expect(html).toContain('全部');
+    expect(html).toContain(tZh('otlp.source.item.kubernetes-pod-logs.label'));
+    expect(html).toContain(tZh('otlp.source.item.cloudflare-logs.label'));
+    expect(html).toContain(tZh('otlp.source.search.label'));
+    expect(html).toContain(tZh('otlp.source.filter.all'));
     expect(html).toContain('37');
-    expect(html).toContain('快速开始 (1)');
-    expect(html).toContain('现有接入 (7)');
-    expect(html).not.toContain('迁移接入 (7)');
-    expect(html).toContain('APM / 链路 (13)');
-    expect(html).toContain('日志 (11)');
-    expect(html).toContain('指标 (5)');
-    expect(html).toContain('活跃信号');
-    expect(html).toContain('指标通道');
-    expect(html).toContain('日志通道');
-    expect(html).toContain('链路通道');
-    expect(html).toContain('搜索数据源、云服务、SDK 或运行时');
-    expect(html).toContain('进入遥测发现');
-    expect(html).not.toContain('APM / 链路 (18)');
-    expect(html).not.toContain('日志 (25)');
-    expect(html).not.toContain('指标 (19)');
-    expect(html).not.toContain('LLM 监控');
+    expect(html).toContain(`${tZh('otlp.source.section.quickstart')} (1)`);
+    expect(html).toContain(`${tZh('otlp.source.section.migrate')} (7)`);
+    expect(html).toContain(`${tZh('otlp.source.section.apm-traces')} (13)`);
+    expect(html).toContain(`${tZh('otlp.source.section.logs')} (11)`);
+    expect(html).toContain(`${tZh('otlp.source.section.metrics')} (5)`);
+    expect(html).toContain(tZh('otlp.ribbon.intake.detail'));
+    expect(html).toContain(tZh('otlp.ribbon.metrics.detail'));
+    expect(html).toContain(tZh('otlp.ribbon.logs.detail'));
+    expect(html).toContain(tZh('otlp.ribbon.traces.detail'));
+    expect(html).toContain(tZh('otlp.source.search.placeholder'));
+    expect(html).toContain(tZh('otlp.open-discovery'));
     expect(html).not.toContain('AWS');
     expect(html).not.toContain('AZURE');
     expect(html).not.toContain('GCP');
-    expect(html).not.toContain('接入中枢');
-    expect(html).not.toContain('高密度控制台');
-    expect(html).not.toContain('工作区已就绪');
-    expect(html).not.toContain('接入首条遥测链路');
-    expect(html).not.toContain('配置信号工作台');
-    expect(html).not.toContain('当前接入');
-    expect(html).not.toContain('从目录进入指标');
     expect(html).not.toContain('165');
     expect(html).not.toContain('HertzBeat Intake Cortex');
-    expect(html).not.toContain('HertzBeat OTLP 接入');
-    expect(html).not.toContain('HERTZBEAT OTLP 接入');
     expect(html).not.toContain('text-[20px]');
     expect(html).not.toContain('Intake Cortex');
     expect(html).not.toContain('Select a source');
@@ -490,22 +467,22 @@ describe('otlp page', () => {
       [
         {
           key: 'quickstart',
-          label: '快速开始',
-          items: [{ key: 'demo-data', label: '5 分钟内接入演示数据', description: '写入演示数据', href: '/overview', icon: vi.fn() as any }]
+          label: 'Quick start',
+          items: [{ key: 'demo-data', label: 'Ingest demo data in 5 minutes', description: 'Write demo data', href: '/overview', icon: vi.fn() as any }]
         },
         {
           key: 'migrate',
-          label: '现有接入',
+          label: 'Existing intake',
           items: [
-            { key: 'open-telemetry', label: '已有 OpenTelemetry', description: '复用 OpenTelemetry Collector', href: '/entities/discovery', icon: vi.fn() as any },
-            { key: 'grafana', label: 'Grafana', description: '接入 Grafana 信号', href: '/ingestion/otlp/metrics', icon: vi.fn() as any }
+            { key: 'open-telemetry', label: 'Existing OpenTelemetry', description: 'Reuse OpenTelemetry Collector', href: '/entities/discovery', icon: vi.fn() as any },
+            { key: 'grafana', label: 'Grafana', description: 'Ingest Grafana signals', href: '/ingestion/otlp/metrics', icon: vi.fn() as any }
           ]
         },
         {
           key: 'metrics',
-          label: '指标',
+          label: 'Metrics',
           items: [
-            { key: 'otel-metrics', label: 'OpenTelemetry 指标', description: '检查 OTLP 指标数据流', href: '/ingestion/otlp/metrics', icon: vi.fn() as any }
+            { key: 'otel-metrics', label: 'OpenTelemetry metrics', description: 'Inspect OTLP metric streams', href: '/ingestion/otlp/metrics', icon: vi.fn() as any }
           ]
         }
       ],
@@ -535,12 +512,12 @@ describe('otlp page', () => {
     expect(html).toContain('href="/setting/define"');
     expect(html).toContain('href="/entities/discovery"');
     expect(html).toContain('href="/entities"');
-    expect(html).toContain('OTLP 三信号接入');
-    expect(html).toContain('传统监控资源');
-    expect(html).toContain('采集器集群');
-    expect(html).toContain('监控模板');
-    expect(html).toContain('服务发现');
-    expect(html).toContain('对象目录');
+    expect(html).toContain(tZh('otlp.collection-loop.otlp-intake.title'));
+    expect(html).toContain(tZh('otlp.collection-loop.traditional-monitoring.title'));
+    expect(html).toContain(tZh('otlp.collection-loop.collector-cluster.title'));
+    expect(html).toContain(tZh('otlp.collection-loop.monitoring-template.title'));
+    expect(html).toContain(tZh('otlp.collection-loop.service-discovery.title'));
+    expect(html).toContain(tZh('otlp.collection-loop.object-directory.title'));
     expect(html).not.toContain('href="/observability-pipelines"');
     expect(html).not.toContain('href="/pipelines"');
     expect(html).not.toContain('href="/fleet"');
@@ -558,11 +535,6 @@ describe('otlp page', () => {
     expect(source).not.toContain('?? 19');
     expect(source).not.toContain('?? 18');
     expect(source).not.toContain('data-otlp-intake-quality');
-    expect(source).not.toContain('采集质量');
-    expect(source).not.toContain('丢弃量');
-    expect(source).not.toContain('解析失败');
-    expect(source).not.toContain('实体归并失败');
-    expect(source).not.toContain('Collector 节点');
     expect(source).toContain('data-otlp-self-check-status="backend-backed"');
   });
 });
diff --git a/web-next/app/log/manage/page.test.tsx b/web-next/app/log/manage/page.test.tsx
index 4436d13bb6..7fc33324d4 100644
--- a/web-next/app/log/manage/page.test.tsx
+++ b/web-next/app/log/manage/page.test.tsx
@@ -7,6 +7,7 @@ import { createRoot, type Root } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import type { TranslationParams } from '@/lib/i18n';
 import LogManagePage from './log-manage-page';
 import type { LogManageRouteState, LogQueryState, LogWorkbenchView } from '@/lib/log-manage/query-state';
 
@@ -206,13 +207,17 @@ vi.mock('@/lib/log-manage/query-state', () => ({
 }));
 
 vi.mock('@/lib/signal-route-context', () => ({
-  buildSignalEntityContextRows: () => [
-    { label: '当前实体', value: 'checkout', meta: 'entityId 7' },
-    { label: '当前服务', value: 'checkout', meta: 'payments' },
-    { label: '当前环境', value: 'prod', meta: '环境' },
-    { label: '时间范围', value: 'last-1h', meta: '查询窗口' },
-    { label: '采集来源', value: 'OTLP', meta: 'HertzBeat OTLP 接入' }
-  ],
+  buildSignalEntityContextRows: () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    return [
+      { label: t('signal.context.entity.label'), value: 'checkout', meta: 'entityId 7' },
+      { label: t('signal.context.service.label'), value: 'checkout', meta: 'payments' },
+      { label: t('signal.context.environment.label'), value: 'prod', meta: t('signal.context.environment.meta') },
+      { label: t('signal.context.time.label'), value: 'last-1h', meta: t('signal.context.time.meta.default') },
+      { label: t('signal.context.source.label'), value: 'OTLP', meta: t('signal.context.source.otlp.meta') }
+    ];
+  },
   readSignalRouteContext: () => ({
     entityId: mockState.searchParams.get('entityId') || undefined,
     entityName: mockState.searchParams.get('entityName') || undefined,
@@ -233,46 +238,57 @@ vi.mock('@/lib/signal-route-context', () => ({
 }));
 
 vi.mock('@/lib/log-manage/view-model', () => ({
-  buildSelectedLogFacts: (entry: any) =>
-    entry
+  buildSelectedLogFacts: (entry: any) => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    return entry
       ? [
-          { label: '严重级别', value: entry.severityText || 'LOG' },
-          { label: '链路 ID', value: entry.traceId || '-', monospace: true }
+          { label: t('log.manage.detail.severity'), value: entry.severityText || 'LOG' },
+          { label: t('log.manage.detail.trace-id'), value: entry.traceId || '-', monospace: true }
         ]
-      : [],
-  buildLogAttributionDiagnostics: (entry: any) =>
-    entry
+      : [];
+  },
+  buildLogAttributionDiagnostics: (entry: any) => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    return entry
       ? [
           {
             key: 'hertzbeat.entity_id',
             label: 'hertzbeat.entity_id',
             value: entry.resource?.['hertzbeat.entity_id'] || '-',
             state: entry.resource?.['hertzbeat.entity_id'] ? 'present' : 'missing',
-            meta: entry.resource?.['hertzbeat.entity_id'] ? '可打开实体详情' : '缺少实体 ID,实体详情会保持禁用'
+            meta: entry.resource?.['hertzbeat.entity_id']
+              ? t('log.manage.attribution.entity-id.present')
+              : t('log.manage.attribution.entity-id.missing')
           },
           {
             key: 'hertzbeat.collector',
             label: 'hertzbeat.collector',
             value: entry.resource?.['hertzbeat.collector'] || '-',
             state: entry.resource?.['hertzbeat.collector'] ? 'present' : 'missing',
-            meta: '采集器来源'
+            meta: t('log.manage.attribution.collector')
           },
           {
             key: 'hertzbeat.template',
             label: 'hertzbeat.template',
             value: entry.resource?.['hertzbeat.template'] || '-',
             state: entry.resource?.['hertzbeat.template'] ? 'present' : 'missing',
-            meta: '监控模板归属'
+            meta: t('log.manage.attribution.template')
           }
         ]
-      : [],
-  buildSelectedLogRows: (entry: any) =>
-    entry
+      : [];
+  },
+  buildSelectedLogRows: (entry: any) => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    return entry
       ? [
           { title: 'checkout', copy: String(entry.body), meta: entry.traceId },
-          { title: '资源', copy: '1', meta: 'attributes 1' }
+          { title: t('log.manage.selected.resource-keys'), copy: '1', meta: 'attributes 1' }
         ]
-      : [],
+      : [];
+  },
   buildLogCodeNavigationUrl: () => undefined,
   buildLogExplorerRows: (entries: any[]) =>
     entries.map(entry => ({
@@ -321,6 +337,12 @@ vi.mock('./route-state', () => ({
   buildResetLogManageRoute: () => '/log/manage'
 }));
 
+const zhT = createTranslatorMock({ locale: 'zh-CN' });
+
+function tZh(key: string, params?: TranslationParams) {
+  return zhT(key, params);
+}
+
 function buildLogManageRouteState(): LogManageRouteState {
   const query: LogQueryState = {
     search: mockState.searchParams.get('search') || mockState.searchParams.get('content') || '',
@@ -476,9 +498,9 @@ describe('log manage page', () => {
     expect(source).toContain('HzAttributeDiagnostics');
     expect(source).toContain('data-log-manage-selected-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
     expect(source).toContain("t('log.manage.context.entity.title')");
-    expect(source).not.toContain('HertzBeat 采集闭环');
-    expect(source).not.toContain('告警闭环');
-    expect(source).not.toContain('日志排查可返回接入总览、采集集群、监控模板和告警处理上下文。');
+    expect(source).not.toContain('HertzBeat collection loop');
+    expect(source).not.toContain('Alert loop');
+    expect(source).not.toContain('Log troubleshooting can return to intake overview, collector clusters, monitor templates, and alert handling context.');
     [
       'log.manage.route.action.collector',
       'log.manage.route.action.templates',
@@ -503,7 +525,7 @@ describe('log manage page', () => {
     expect(source).not.toContain('data-log-manage-header-action="alerts">\n                  <BellRing className="h-4 w-4"');
     expect(source).toContain('data-log-manage-alert-context-hint="entity-trace-alert-handoff"');
     expect(source).toContain("t('log.manage.handoff.alert-hint')");
-    expect(source).not.toContain('告警规则');
+    expect(source).not.toContain('Alert rules');
     expect(source).not.toContain('signoz-');
     expect(source).not.toContain('data-log-manage-floating-actions');
     expect(source).not.toContain('Run Query');
@@ -511,11 +533,11 @@ describe('log manage page', () => {
     expect(source).not.toContain('Create an Alert');
     expect(source).not.toContain('Add to Dashboard');
     expect(source).not.toContain('Logs Workbench');
-    expect(source).not.toContain("returnLabel: '日志工作台'");
-    expect(source).not.toContain("returnLabel: '链路工作台'");
-    expect(source).not.toContain('保存视图');
-    expect(source).not.toContain('创建告警');
-    expect(source).not.toContain('加入仪表盘');
+    expect(source).not.toContain("returnLabel: 'Logs Workbench'");
+    expect(source).not.toContain("returnLabel: 'Trace Workbench'");
+    expect(source).not.toContain('Save view');
+    expect(source).not.toContain('Create alert');
+    expect(source).not.toContain('Add to dashboard');
     expect(source).not.toContain('ThreeSignalDeskShell');
     expect(source).not.toContain('FactsStrip');
     expect(source).not.toContain('StageSection');
@@ -549,7 +571,7 @@ describe('log manage page', () => {
     expect(html).toContain('data-log-manage-header-action-icon="return-source"');
     expect(html).toContain('data-log-manage-header-action-icon-owner="hertzbeat-ui-button-icon"');
     expect(html).toContain('href="/trace/manage?traceId=trace-123&amp;spanId=span-456&amp;serviceName=checkout"');
-    expect(html).toContain('返回来源');
+    expect(html).toContain(tZh('log.manage.route.action.return-source'));
   });
 
   it('keeps the Angular log-to-trace drilldown contract as drawer preview before route navigation', () => {
@@ -659,13 +681,13 @@ describe('log manage page', () => {
     expect(html).toContain('data-log-manage-stream-severity-tone="danger"');
     expect(html).toContain('checkout timeout');
     expect(html).not.toContain('data-log-manage-stream-empty-state="true"');
-    expect(html).toContain('日志流');
-    expect(html).toContain('历史检索');
-    expect(html).toContain('实时日志流');
-    expect(html).toContain('应用到日志流');
-    expect(html).toContain('选中日志');
-    expect(html).not.toContain('完整 JSON');
-    expect(html).not.toContain('运行查询');
+    expect(html).toContain(tZh('log.manage.stream.view.stream'));
+    expect(html).toContain(tZh('log.manage.stream.view.history'));
+    expect(html).toContain(tZh('log.manage.stream.stage.title'));
+    expect(html).toContain(tZh('log.manage.query.run.stream'));
+    expect(html).toContain(tZh('log.manage.stream.selected.title'));
+    expect(html).not.toContain('Full JSON');
+    expect(html).not.toContain(tZh('log.manage.query.run.history'));
     expect(html).not.toContain('/api/logs/sse/subscribe');
     expect(html).not.toContain('data-log-manage-log-list="cold-dense-log-list"');
   }, 30000);
@@ -676,8 +698,8 @@ describe('log manage page', () => {
     expect(source).toContain('streamSequenceRef');
     expect(source).toContain('buildStreamItemKey(entry, sequence)');
     expect(source).not.toContain('buildLogEntryKey(entry, nextIndex)');
-    expect(source).not.toContain('无法进入前端缓冲区');
-    expect(source).not.toContain('日志流过快');
+    expect(source).not.toContain('cannot enter frontend buffer');
+    expect(source).not.toContain('log stream too fast');
   });
 
   it('keeps no-log empty guidance operator-facing instead of generic storage copy', () => {
@@ -689,8 +711,8 @@ describe('log manage page', () => {
     expect(source).toContain('data-log-manage-stream-empty-owner="hertzbeat-ui-empty-state"');
     expect(source).toContain('HzEmptyState');
     expect(source).toContain("t('log.manage.empty.copy')");
-    expect(messagesSource).toContain("'log.manage.empty.copy': '确认时间范围、实体归因、采集器和监控模板后再查看日志。'");
-    expect(source).not.toContain('日志写入后会在这里按时间倒序展示。');
+    expect(messagesSource).toContain("'log.manage.empty.copy':");
+    expect(source).not.toContain('Logs appear here in reverse chronological order after writes.');
   });
 
   it('renders live log stream rows through a virtualized viewport backed by EventSource', () => {
@@ -728,8 +750,8 @@ describe('log manage page', () => {
     expect(source).not.toContain('className={`grid w-full grid-cols-[58px_minmax(0,112px)_minmax(0,1fr)]');
     expect(source).not.toContain('<aside className="border-l border-[#252b35] bg-[#0b0e13] px-4 py-4">');
     expect(source).not.toContain('<div className="mt-4 space-y-2">');
-    expect(source).not.toContain('日志流过快');
-    expect(source).not.toContain('前端已隐藏');
+    expect(source).not.toContain('log stream too fast');
+    expect(source).not.toContain('frontend has hidden');
     expect(source).not.toContain('shouldShowStreamBackpressureNotice');
     expect(source).not.toContain('streamItems.map(item =>');
   });
@@ -756,8 +778,8 @@ describe('log manage page', () => {
     expect(html).toContain('data-log-manage-stream-pause-notice-owner="hertzbeat-ui-state-notice"');
     expect(html).toContain('data-log-manage-stream-pause-notice="paused-buffer-visible"');
     expect(html).toContain('data-hz-ui="state-notice"');
-    expect(html).toContain('已暂停');
-    expect(html).toContain('继续');
+    expect(html).toContain(tZh('log.manage.stream.state.paused'));
+    expect(html).toContain(tZh('log.manage.stream.action.resume'));
   }, 15000);
 
   it('uses the shared narrow time rail in the top-right page header instead of the log query row', async () => {
@@ -806,7 +828,7 @@ describe('log manage page', () => {
     const html = renderLogManagePage();
 
     expect(html).toContain('data-log-manage-route="otlp-cold-log-workbench"');
-    expect(html).toContain('data-loading-copy="正在加载日志工作台"');
+    expect(html).toContain(`data-loading-copy="${tZh('log.manage.loading')}"`);
     expect(html).toContain('data-log-manage-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-log-manage-query-bar="cold-query-row"');
     expect(html).toContain('data-log-manage-panel-surface-padding-owner="hertzbeat-ui-panel-surface"');
@@ -844,15 +866,15 @@ describe('log manage page', () => {
     expect(html).toContain('data-hz-panel-header-eyebrow="true"');
     expect(html).toContain('data-log-manage-row-detail-action="true"');
     expect(html).toContain('data-log-manage-severity-tone="danger"');
-    expect(html).toContain('查看日志');
+    expect(html).toContain(tZh('log.manage.stream.action.view-log'));
     expect(html).toContain('data-log-manage-selected-evidence="selected-log-evidence"');
     expect(html).toContain('data-log-manage-selected-evidence-owner="hertzbeat-ui-detail-rows"');
     expect(html).toContain('data-log-manage-detail-facts-owner="hertzbeat-ui-detail-rows"');
-    expect(html).toContain('日志证据');
-    expect(html).toContain('日志时间');
-    expect(html).toContain('日志级别');
-    expect(html).toContain('正文摘要');
-    expect(html).toContain('最近上报');
+    expect(html).toContain(tZh('log.manage.evidence.title'));
+    expect(html).toContain(tZh('log.manage.evidence.time.title'));
+    expect(html).toContain(tZh('log.manage.evidence.severity.title'));
+    expect(html).toContain(tZh('log.manage.evidence.body.title'));
+    expect(html).toContain(tZh('log.manage.evidence.latest.title'));
     expect(html).not.toContain('data-log-manage-hertzbeat-loop="collector-template-alert-loop"');
     expect(html).toContain('data-log-manage-entity-context="hertzbeat-signal-entity-context"');
     expect(html).toContain('data-log-manage-entity-context-owner="hertzbeat-ui-detail-rows"');
@@ -861,39 +883,39 @@ describe('log manage page', () => {
     expect(html).not.toContain('data-log-manage-intake-quality="logs-collector-quality"');
     expect(html).not.toContain('data-log-manage-intake-quality-row=');
     expect(html).toContain('data-log-manage-row-trace-detail-action="true"');
-    expect(html).toContain('日志工作台');
-    expect(html).toContain('围绕采集来源、实体、链路和告警处理筛选日志');
-    expect(html).not.toContain('日志排查可返回接入总览、采集集群、监控模板和告警处理上下文。');
-    expect(html).not.toContain('保留查询、趋势、列表和详情在一个高密度工作面中');
-    expect(html).not.toContain('日志查询继续保留');
-    expect(html).toContain('运行查询');
-    expect(html).toContain('严重级别');
-    expect(html).toContain('趋势带');
-    expect(html).toContain('最近日志');
-    expect(html).not.toContain('HertzBeat 采集闭环');
-    expect(html).not.toContain('告警闭环');
-    expect(html).not.toContain('接入质量');
-    expect(html).not.toContain('日志采集质量');
-    expect(html).not.toContain('接收量');
-    expect(html).not.toContain('解析失败');
-    expect(html).not.toContain('实体归并失败');
-    expect(html).not.toContain('Collector 节点');
+    expect(html).toContain(tZh('log.manage.route.title'));
+    expect(html).toContain(tZh('log.manage.route.subtitle'));
+    expect(html).not.toContain('Log troubleshooting can return to intake overview, collector clusters, monitor templates, and alert handling context.');
+    expect(html).not.toContain('Keep query, trend, list, and detail in one dense workbench');
+    expect(html).not.toContain('Log query continues to retain');
+    expect(html).toContain(tZh('log.manage.query.run.history'));
+    expect(html).toContain(tZh('log.manage.query.severity.aria'));
+    expect(html).toContain(tZh('log.manage.trend.title'));
+    expect(html).toContain(tZh('log.manage.list.title'));
+    expect(html).not.toContain('HertzBeat collection loop');
+    expect(html).not.toContain('Alert loop');
+    expect(html).not.toContain('Intake quality');
+    expect(html).not.toContain('Log collection quality');
+    expect(html).not.toContain('Received volume');
+    expect(html).not.toContain('Parse failures');
+    expect(html).not.toContain('Entity merge failures');
+    expect(html).not.toContain('Collector nodes');
     expect(html).toContain('/ingestion/otlp?signal=logs');
-    expect(html).toContain('实体上下文');
-    expect(html).toContain('当前实体');
-    expect(html).toContain('采集来源');
-    expect(html).toContain('实体详情');
-    expect(html).toContain('告警处理');
+    expect(html).toContain(tZh('log.manage.context.entity.title'));
+    expect(html).toContain(tZh('signal.context.entity.label'));
+    expect(html).toContain(tZh('signal.context.source.label'));
+    expect(html).toContain(tZh('log.manage.route.action.entity'));
+    expect(html).toContain(tZh('log.manage.route.action.alerts'));
     expect(html).toContain('data-log-manage-signal-handoff-hint="log-trace-metric-context"');
-    expect(html).toContain('当前日志的链路、跨度和服务上下文会带入链路与指标工作台');
-    expect(html).not.toContain('告警规则');
-    expect(html).not.toContain('保存视图');
-    expect(html).not.toContain('创建告警');
-    expect(html).not.toContain('加入仪表盘');
+    expect(html).toContain(tZh('log.manage.handoff.signal-hint'));
+    expect(html).not.toContain('Alert rules');
+    expect(html).not.toContain('Save view');
+    expect(html).not.toContain('Create alert');
+    expect(html).not.toContain('Add to dashboard');
     expect(html).toContain('checkout timeout');
     expect(html).toContain('checkout');
-    expect(html).toContain('查看链路');
-    expect(html).toContain('查看指标');
+    expect(html).toContain(tZh('log.manage.handoff.traces'));
+    expect(html).toContain(tZh('log.manage.handoff.metrics'));
     expect(html).toContain('/entities/7?entityId=7');
     expect(html).toContain('/alert?status=firing&amp;signal=logs');
     expect(html).not.toContain('signoz-');
@@ -1018,9 +1040,9 @@ describe('log manage page', () => {
 
       const detailDialog = interactionContainer.querySelector('[data-log-stream-detail-dialog="true"]');
       expect(detailDialog).not.toBeNull();
-      expect(detailDialog?.textContent).toContain('归因诊断');
+      expect(detailDialog?.textContent).toContain(tZh('log.manage.selected.attribution.title'));
       expect(detailDialog?.textContent).toContain('hertzbeat.entity_id');
-      expect(detailDialog?.textContent).toContain('缺少实体 ID,实体详情会保持禁用');
+      expect(detailDialog?.textContent).toContain(tZh('log.manage.attribution.entity-id.missing'));
       expect(detailDialog?.textContent).toContain('hertzbeat.collector');
       expect(detailDialog?.textContent).toContain('collector-local');
       expect(detailDialog?.textContent).toContain('hertzbeat.template');
@@ -1054,9 +1076,9 @@ describe('log manage page', () => {
       const html = renderLogManagePage();
 
       expect(html).toContain('data-log-manage-selected-attribution-diagnostics="hertzbeat-attribute-diagnostics"');
-      expect(html).toContain('归因诊断');
+      expect(html).toContain(tZh('log.manage.selected.attribution.title'));
       expect(html).toContain('hertzbeat.entity_id');
-      expect(html).toContain('缺少实体 ID,实体详情会保持禁用');
+      expect(html).toContain(tZh('log.manage.attribution.entity-id.missing'));
       expect(html).toContain('hertzbeat.collector');
       expect(html).toContain('collector-local');
       expect(html).toContain('hertzbeat.template');
@@ -1167,14 +1189,14 @@ describe('log manage page', () => {
     expect(apiMessageGet).toHaveBeenCalledWith('/traces/trace-123/spans');
     expect(relatedTraceDialog).not.toBeNull();
     expect(relatedTraceDialog?.textContent).toContain('POST /checkout');
-    expect(relatedTraceDialog?.textContent).toContain('链路预览');
-    expect(relatedTraceDialog?.textContent).toContain('2 个跨度');
-    expect(relatedTraceDialog?.textContent).toContain('2 个事件');
-    expect(relatedTraceDialog?.textContent).toContain('服务与命名空间');
-    expect(relatedTraceDialog?.textContent).toContain('暂无链路状态');
-    expect(relatedTraceDialog?.textContent).toContain('关联');
+    expect(relatedTraceDialog?.textContent).toContain(tZh('log.manage.related-trace.badge'));
+    expect(relatedTraceDialog?.textContent).toContain(tZh('log.manage.related-trace.spans-count', { count: 2 }));
+    expect(relatedTraceDialog?.textContent).toContain(tZh('log.manage.related-trace.fact.events'));
+    expect(relatedTraceDialog?.textContent).toContain(tZh('trace.manage.selected-span.service-namespace'));
+    expect(relatedTraceDialog?.textContent).toContain(tZh('trace.manage.trace-state-empty'));
+    expect(relatedTraceDialog?.textContent).toContain(tZh('log.manage.related-trace.fact.links'));
     const stageFacts = relatedTraceDialog?.querySelector('[data-log-related-trace-stage-facts="true"]');
-    expect(stageFacts?.textContent).toContain('事件');
+    expect(stageFacts?.textContent).toContain(tZh('log.manage.related-trace.fact.events'));
     expect(stageFacts?.textContent).toContain('2');
     expect(relatedTraceDialog?.querySelectorAll('[data-waterfall-event-marker="true"]')).toHaveLength(2);
     expect(relatedTraceDialog?.querySelectorAll('[data-waterfall-minimap-event-marker="true"]')).toHaveLength(2);
diff --git a/web-next/app/log/stream/page.test.tsx b/web-next/app/log/stream/page.test.tsx
index b9411bb209..d2cb147b1f 100644
--- a/web-next/app/log/stream/page.test.tsx
+++ b/web-next/app/log/stream/page.test.tsx
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 vi.mock('../manage/log-manage-page', () => ({
   default: ({ forcedView, showViewToggle }: { forcedView: string; showViewToggle: boolean }) => (
@@ -12,6 +13,7 @@ vi.mock('../manage/log-manage-page', () => ({
 
 describe('log stream route', () => {
   it('uses the canonical EventSource-backed virtualized stream instead of a static compatibility shell', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     const { default: LogStreamPage } = await import('./page');
     const html = renderToStaticMarkup(<LogStreamPage />);
     const source = readFileSync(resolve(process.cwd(), 'app/log/stream/page.tsx'), 'utf8');
@@ -24,8 +26,8 @@ describe('log stream route', () => {
     expect(source).toContain('forcedView="stream"');
     expect(source).toContain('showViewToggle={false}');
     expect(source).not.toContain('data-log-stream-surface="angular-log-stream"');
-    expect(source).not.toContain('连接中...');
-    expect(source).not.toContain('0 条日志');
+    expect(source).not.toContain(`${expectedT('log.manage.stream.status.connecting')}...`);
+    expect(source).not.toContain(expectedT('log.manage.stream.count', { count: 0 }));
     expect(source).not.toContain('filterFields.map');
   });
 });
diff --git a/web-next/app/monitors/[monitorId]/edit/page.test.tsx b/web-next/app/monitors/[monitorId]/edit/page.test.tsx
index 66af81e4b5..527b3412ac 100644
--- a/web-next/app/monitors/[monitorId]/edit/page.test.tsx
+++ b/web-next/app/monitors/[monitorId]/edit/page.test.tsx
@@ -50,6 +50,8 @@ vi.mock('@/components/providers/i18n-provider', () => ({
   })
 }));
 
+const expectedT = createTranslatorMock({ locale: 'zh-CN' });
+
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
@@ -223,7 +225,7 @@ describe('MonitorEditPage', () => {
     expect(html).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
     expect(html).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
     expect(html).toContain('checkout');
-    expect(html).toContain('确定');
+    expect(html).toContain(expectedT('common.button.ok'));
   });
 
   it('loads the edit-monitor draft with the current monitor id', async () => {
diff --git a/web-next/app/monitors/new/page.test.tsx b/web-next/app/monitors/new/page.test.tsx
index f90bf7fe33..b9c1a0f57c 100644
--- a/web-next/app/monitors/new/page.test.tsx
+++ b/web-next/app/monitors/new/page.test.tsx
@@ -54,6 +54,8 @@ vi.mock('@/components/providers/i18n-provider', () => ({
   })
 }));
 
+const expectedT = createTranslatorMock({ locale: 'zh-CN' });
+
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
@@ -226,8 +228,8 @@ describe('MonitorNewPage', () => {
     expect(html).toContain('data-monitor-editor-ssl-port-notice="angular-info-notification"');
     expect(html).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
     expect(html).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
-    expect(html).toContain('新建监控');
-    expect(html).toContain('确定');
+    expect(html).toContain(expectedT('monitor.new-monitor'));
+    expect(html).toContain(expectedT('common.button.ok'));
   });
 
   it('loads the new-monitor draft with the current app context', async () => {
diff --git a/web-next/app/monitors/page.test.tsx b/web-next/app/monitors/page.test.tsx
index 4432712d50..2136b80153 100644
--- a/web-next/app/monitors/page.test.tsx
+++ b/web-next/app/monitors/page.test.tsx
@@ -2607,6 +2607,7 @@ describe('monitors page', () => {
   }, 15000);
 
   it('keeps monitor row updated cells as timestamp-only table values', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
 
     expect(source).toContain('data-monitor-row-meta={updatedTime}');
@@ -2616,7 +2617,7 @@ describe('monitors page', () => {
 
     expect(html).toContain('data-monitor-row-meta="2026-04-19 09:00:00"');
     expect(html).not.toContain('Updated 2026-04-19 09:00:00');
-    expect(html).not.toContain('更新时间 2026-04-19 09:00:00');
+    expect(html).not.toContain(`${expectedT('common.edit-time')} 2026-04-19 09:00:00`);
   });
 
   it('keeps monitor row status badges on the shared badge density without page-local sizing overrides', async () => {
@@ -2808,6 +2809,7 @@ describe('monitors page', () => {
   });
 
   it('routes the monitor workbench through the shared HertzBeat UI shell owners', () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
 
     expect(source).toContain('HzExplorerFrame');
@@ -2928,7 +2930,7 @@ describe('monitors page', () => {
     expect(source).not.toContain('font-mono text-[11px] uppercase tracking-[0.14em] text-[#8f99ab]');
     expect(source).not.toContain('text-[11px] text-[#8f99ab]');
     expect(source).not.toContain('scrapeLabel');
-    expect(source).not.toContain('采集方式');
+    expect(source).not.toContain(expectedT('monitor.scrape'));
     expect(source).toContain("id: 'import'");
     expect(source).toContain('onSelect: handleImportClick');
     expect(source).toContain('importMonitorsFromFacade(api.monitors.import, file)');
@@ -3021,7 +3023,6 @@ describe('monitors page', () => {
     expect(source).toContain("title={batchResponseConfirm?.action === 'enable' ? t('common.confirm.enable-batch') : t('common.confirm.cancel-batch')}");
     expect(source).not.toContain('router.push(buildMonitorEditHref');
     expect(source).not.toContain("eyebrow={t('monitors.kicker')}");
-    expect(source).not.toContain("查看传统采集资源");
     expect(source).toContain('data-monitors-open-detail-action="true"');
     expect(source).not.toContain('HzFilterWorkbench');
     expect(source).not.toContain('HzQueryBar');
diff --git a/web-next/app/passport/lock/page.test.tsx b/web-next/app/passport/lock/page.test.tsx
index d5fe6d9548..e8de102236 100644
--- a/web-next/app/passport/lock/page.test.tsx
+++ b/web-next/app/passport/lock/page.test.tsx
@@ -63,6 +63,7 @@ vi.mock('@/components/observability', () => ({
 
 describe('passport lock page', () => {
   it('renders inside the shared passport shell with the angular-style unlock panel', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     const { default: PassportLockPage } = await import('./page');
     const html = renderToStaticMarkup(<PassportLockPage />);
 
@@ -95,8 +96,8 @@ describe('passport lock page', () => {
     expect(html).toContain('data-hz-passport-lock-submit-state="disabled"');
     expect(html).toContain('max-w-[712px]');
     expect(html).toContain('rounded-none');
-    expect(html).toContain('解除锁定');
-    expect(html).toContain('输入任意解锁');
+    expect(html).toContain(expectedT('app.lock'));
+    expect(html).toContain(expectedT('app.lock.placeholder'));
     expect(html).toContain('/assets/bg.png');
     expect(html).toContain('Apache HertzBeat™');
   }, 30000);
diff --git a/web-next/app/setting/collector/page.test.tsx b/web-next/app/setting/collector/page.test.tsx
index 9f2406a33a..2e6952b616 100644
--- a/web-next/app/setting/collector/page.test.tsx
+++ b/web-next/app/setting/collector/page.test.tsx
@@ -157,13 +157,13 @@ vi.mock('@/components/pages/collector-manage-surface', () => ({
         data-collector-pagination-owner="hertzbeat-ui-pagination-bar"
       >
         <section data-collector-admin-layout="full-width-admin-list">
-          <span>刷新</span>
-          <span>部署采集器</span>
-          <span>上线采集器</span>
-          <span>下线采集器</span>
-          <span>删除采集器</span>
-          <span>搜索</span>
-          <span>采集集群</span>
+          <span>Refresh</span>
+          <span>Deploy collector</span>
+          <span>Online collector</span>
+          <span>Offline collector</span>
+          <span>Delete collector</span>
+          <span>Search</span>
+          <span>Collector cluster</span>
           <span>{search}</span>
           <span>{selectedCollectors?.length ?? 0}</span>
           <span>{actionMessage ?? 'no-action-message'}</span>
@@ -303,15 +303,13 @@ describe('setting collector page', () => {
     expect(html).toContain('data-collector-pagination-owner="hertzbeat-ui-pagination-bar"');
     expect(html).toContain('data-collector-admin-layout="full-width-admin-list"');
     expect(html).not.toContain('data-collector-summary-rail=');
-    expect(html).not.toContain('当前采集集群');
-    expect(html).not.toContain('选中节点');
-    expect(html).toContain('刷新');
-    expect(html).toContain('部署采集器');
-    expect(html).toContain('上线采集器');
-    expect(html).toContain('下线采集器');
-    expect(html).toContain('删除采集器');
-    expect(html).toContain('搜索');
-    expect(html).toContain('采集集群');
+    expect(html).toContain('Refresh');
+    expect(html).toContain('Deploy collector');
+    expect(html).toContain('Online collector');
+    expect(html).toContain('Offline collector');
+    expect(html).toContain('Delete collector');
+    expect(html).toContain('Search');
+    expect(html).toContain('Collector cluster');
     expect(html).toContain('no-action-message');
     expect(html).toContain('no-action-error');
     expect(html).toContain('no-action-meta');
diff --git a/web-next/app/setting/define/page.test.tsx b/web-next/app/setting/define/page.test.tsx
index 357cf19468..b5b4a0cedb 100644
--- a/web-next/app/setting/define/page.test.tsx
+++ b/web-next/app/setting/define/page.test.tsx
@@ -139,7 +139,7 @@ vi.mock('@/components/pages/setting-define-surface', () => ({
         data-setting-define-menu-select-query-contract="angular-replace-with-app-only"
         data-setting-define-menu-select-query-owner="setting-define-page-router"
       >
-        <span>定义</span>
+        <span>Define</span>
         <span>{selectedApp}</span>
         <span>{yamlLabel}</span>
         <span>{editorValue}</span>
@@ -463,6 +463,7 @@ describe('setting define page', () => {
   });
 
   it('keeps mutation success when the Angular startup refresh side effect fails', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     window.history.pushState({}, '', '/setting/define?app=mysql');
     mockState.renderData = {
       menuGroups: [
@@ -502,7 +503,7 @@ describe('setting define page', () => {
       false
     );
     expect(reloadTemplateDefinitionStartupContext).toHaveBeenCalledWith(apiMessageGet, 'zh-CN');
-    expect(mockState.lastSurfaceProps?.message).toContain('应用成功');
+    expect(mockState.lastSurfaceProps?.message).toContain(expectedT('common.notify.apply-success'));
     expect(mockState.lastSurfaceProps?.message).not.toContain('startup refresh down');
 
     await act(async () => {
@@ -698,7 +699,7 @@ describe('setting define page', () => {
 
   it('preserves the old Angular edit flag when saving a direct new-template YML draft', async () => {
     const draftYaml = 'app: custom\ncategory: custom\n# still drafting';
-    const draftOriginalYaml = '# 请在此通过编写YML内容来定义新的监控类型';
+    const draftOriginalYaml = '# Define a new monitoring type by editing YML content here';
     mockState.renderData = {
       menuGroups: [
         {
@@ -747,6 +748,7 @@ describe('setting define page', () => {
   });
 
   it('keeps old Angular apply-fail title with backend detail when YML save fails', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     window.history.pushState({}, '', '/setting/define?app=mysql');
     mockState.renderData = {
       menuGroups: [
@@ -785,7 +787,7 @@ describe('setting define page', () => {
       'app: mysql\ncategory: database\n# invalid',
       false
     );
-    expect(mockState.lastSurfaceProps?.message).toContain('应用失败');
+    expect(mockState.lastSurfaceProps?.message).toContain(expectedT('common.notify.apply-fail'));
     expect(mockState.lastSurfaceProps?.message).not.toContain('schema invalid');
     expect(mockState.lastSurfaceProps?.messageMeta).toBe('schema invalid');
     expect(mockState.lastSurfaceProps?.messageContract).toBe('angular-apply-fail-notification');
@@ -799,6 +801,7 @@ describe('setting define page', () => {
   });
 
   it('keeps old Angular delete-fail title with backend detail when YML delete fails', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     window.history.pushState({}, '', '/setting/define?app=mysql');
     mockState.renderData = {
       menuGroups: [
@@ -832,7 +835,7 @@ describe('setting define page', () => {
     });
 
     expect(deleteTemplateDefine).toHaveBeenCalledWith(apiMessageDelete, 'mysql');
-    expect(mockState.lastSurfaceProps?.message).toContain('删除失败');
+    expect(mockState.lastSurfaceProps?.message).toContain(expectedT('common.notify.delete-fail'));
     expect(mockState.lastSurfaceProps?.message).not.toContain('template is still in use');
     expect(mockState.lastSurfaceProps?.messageMeta).toBe('template is still in use');
     expect(mockState.lastSurfaceProps?.messageContract).toBe('angular-delete-fail-notification');
@@ -849,6 +852,7 @@ describe('setting define page', () => {
   });
 
   it('keeps old Angular apply-fail title with backend detail when template visibility update fails', async () => {
+    const expectedT = createTranslatorMock({ locale: 'zh-CN' });
     window.history.pushState({}, '', '/setting/define?app=mysql');
     mockState.renderData = {
       menuGroups: [
@@ -882,7 +886,7 @@ describe('setting define page', () => {
     });
 
     expect(updateTemplateVisibility).toHaveBeenCalledWith(apiMessagePut, 'mysql', true);
-    expect(mockState.lastSurfaceProps?.message).toContain('应用失败');
+    expect(mockState.lastSurfaceProps?.message).toContain(expectedT('common.notify.apply-fail'));
     expect(mockState.lastSurfaceProps?.message).not.toContain('visibility locked by policy');
     expect(mockState.lastSurfaceProps?.messageMeta).toBe('visibility locked by policy');
     expect(mockState.lastSurfaceProps?.messageContract).toBe('angular-apply-fail-notification');
diff --git a/web-next/app/setting/labels/page.test.tsx b/web-next/app/setting/labels/page.test.tsx
index e90db4cb8e..de73cd47ab 100644
--- a/web-next/app/setting/labels/page.test.tsx
+++ b/web-next/app/setting/labels/page.test.tsx
@@ -169,7 +169,7 @@ vi.mock('@/components/pages/label-manage-surface', () => ({
       data-label-monitor-handoff-owner="next-monitor-query-link"
     >
       <section data-label-admin-layout="full-width-admin-list">
-        <span>标签管理</span>
+        <span>Label management</span>
         <span>{search}</span>
         <span data-label-test-first-name="true">{data.list.content[0]?.name || 'no-row'}</span>
         <span>{draftLabel ? draftLabel.name : 'no-draft'}</span>
@@ -253,9 +253,7 @@ describe('setting labels page', () => {
     expect(html).toContain('data-label-manage-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-label-admin-layout="full-width-admin-list"');
     expect(html).not.toContain('data-label-summary-rail=');
-    expect(html).not.toContain('标签摘要');
-    expect(html).not.toContain('当前标签集');
-    expect(html).toContain('标签管理');
+    expect(html).toContain('Label management');
     expect(html).toContain('no-draft');
     expect(html).toContain('false');
     expect(html).toContain('data-label-save-feedback="none"');
@@ -541,7 +539,7 @@ describe('setting labels page', () => {
       expect.objectContaining({ name: 'team', tagValue: 'ops' }),
       true
     );
-    expect(mockState.lastSurfaceProps?.actionError).toBe('新增失败!');
+    expect(mockState.lastSurfaceProps?.actionError).toBe(createTranslatorMock({ locale: 'zh-CN' })('common.notify.new-fail'));
     expect(mockState.lastSurfaceProps?.actionMeta).toBe('backend refused label create');
 
     await act(async () => {
@@ -579,7 +577,7 @@ describe('setting labels page', () => {
       mockState.renderData.list.content[0],
       false
     );
-    expect(mockState.lastSurfaceProps?.actionError).toBe('修改失败!');
+    expect(mockState.lastSurfaceProps?.actionError).toBe(createTranslatorMock({ locale: 'zh-CN' })('common.notify.edit-fail'));
     expect(mockState.lastSurfaceProps?.actionMeta).toBe('backend refused label edit');
 
     await act(async () => {
@@ -658,7 +656,7 @@ describe('setting labels page', () => {
 
     expect(deleteLabel).toHaveBeenCalledWith(apiMessageDelete, 1);
     expect(mockState.lastSurfaceProps?.deleteTarget).toBeNull();
-    expect(mockState.lastSurfaceProps?.actionError).toBe('删除失败!');
+    expect(mockState.lastSurfaceProps?.actionError).toBe(createTranslatorMock({ locale: 'zh-CN' })('common.notify.delete-fail'));
     expect(mockState.lastSurfaceProps?.actionMeta).toBe('backend refused label delete');
 
     await act(async () => {
diff --git a/web-next/app/setting/plugins/page.test.tsx b/web-next/app/setting/plugins/page.test.tsx
index 50cedeec34..e3f8f90850 100644
--- a/web-next/app/setting/plugins/page.test.tsx
+++ b/web-next/app/setting/plugins/page.test.tsx
@@ -180,10 +180,10 @@ vi.mock('@/components/pages/plugin-manage-surface', () => ({
       data-plugin-pagination-contract="angular-search-pagination"
     >
       <section data-plugin-admin-layout="full-width-admin-list">
-        <span>插件表格</span>
+        <span>Plugin table</span>
       </section>
       <span data-plugin-test-row-count="true">{data.list.content.length}</span>
-      <span>插件管理</span>
+      <span>Plugin management</span>
       <span>{search}</span>
       <span>{draftPlugin ? draftPlugin.name : 'no-draft'}</span>
       <span>{isUploadDialogOpen ? 'upload-dialog-open' : 'upload-dialog-closed'}</span>
@@ -369,9 +369,7 @@ describe('setting plugins page', () => {
     expect(html).toContain('data-plugin-pagination-contract="angular-search-pagination"');
     expect(html).toContain('data-plugin-admin-layout="full-width-admin-list"');
     expect(html).not.toContain('data-plugin-summary-rail=');
-    expect(html).not.toContain('插件摘要');
-    expect(html).not.toContain('当前插件');
-    expect(html).toContain('插件管理');
+    expect(html).toContain('Plugin management');
     expect(html).toContain('no-draft');
     expect(html).toContain('upload-dialog-closed');
     expect(html).toContain('no-param-draft');
@@ -404,7 +402,7 @@ describe('setting plugins page', () => {
     expect(html).toContain('data-plugin-load-failure-owner="plugin-route-controller"');
     expect(html).toContain('data-plugin-load-failure="angular-console-only-shell"');
     expect(html).toContain('data-plugin-test-row-count="true">0');
-    expect(html).toContain('插件表格');
+    expect(html).toContain('Plugin table');
     expect(html).not.toContain('backend refused plugin load');
     expect(html).not.toContain('common.load-failed');
     expect(html).not.toContain('data-observability-status');
diff --git a/web-next/app/setting/settings/config/page.test.tsx b/web-next/app/setting/settings/config/page.test.tsx
index 46c5fc6c1c..0a76a7c598 100644
--- a/web-next/app/setting/settings/config/page.test.tsx
+++ b/web-next/app/setting/settings/config/page.test.tsx
@@ -103,13 +103,14 @@ describe('setting config page', () => {
   });
 
   it('renders the constrained locale, timezone, and theme selectors on the shared settings console contract', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const { default: SettingConfigPage } = await import('./page');
     const html = renderToStaticMarkup(<SettingConfigPage />);
 
     expect(html).toContain('data-client-workbench="true"');
-    expect(html).toContain('data-loading-copy="正在加载系统配置。"');
+    expect(html).toContain(`data-loading-copy="${t('setting.settings.config.loading')}"`);
     expect(html).toContain('data-settings-console-title="true"');
-    expect(html).toContain('系统配置');
+    expect(html).toContain(t('settings.system-config'));
     expect(html).toContain('data-setting-config-surface="otlp-cold-system-config"');
     expect(html).toContain('data-setting-config-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-setting-config-layout="full-width-settings-form"');
@@ -128,20 +129,18 @@ describe('setting config page', () => {
     expect(html).toContain('data-setting-config-select-kind="theme"');
     expect(html).toContain('data-setting-config-actions="standard-equal-buttons"');
     expect(html).toContain('data-settings-form-owner="cold-settings-form-owner"');
-    expect(html).toContain('系统语言');
-    expect(html).toContain('系统时区');
-    expect(html).toContain('系统主题');
-    expect(html).toContain('英语(en_US)');
-    expect(html).toContain('简体中文(zh_CN)');
-    expect(html).toContain('深色主题');
-    expect(html).toContain('紧凑主题');
+    expect(html).toContain(t('settings.system-config.locale'));
+    expect(html).toContain(t('settings.system-config.timezone'));
+    expect(html).toContain(t('settings.system-config.theme'));
+    expect(html).toContain(t('settings.system-config.locale.en_US'));
+    expect(html).toContain(t('settings.system-config.locale.zh_CN'));
+    expect(html).toContain(t('settings.system-config.theme.dark'));
+    expect(html).toContain(t('settings.system-config.theme.compact'));
     expect(html).toContain('Asia/Shanghai (+08:00) Shanghai');
-    expect(html).toContain('确认更新');
+    expect(html).toContain(t('settings.system-config.ok'));
     expect(html).not.toContain('data-setting-config-surface="angular-system-config"');
     expect(html).not.toContain('data-setting-config-form="angular-vertical-form"');
     expect(html).not.toContain('data-setting-config-summary-rail=');
-    expect(html).not.toContain('系统配置摘要');
-    expect(html).not.toContain('当前系统配置');
     expect(html).not.toContain('System Configuration');
     expect(html).not.toContain('System Language');
     expect(html).not.toContain('Save');
diff --git a/web-next/app/setting/settings/object-store/page.test.tsx b/web-next/app/setting/settings/object-store/page.test.tsx
index 793f18217c..703692fadc 100644
--- a/web-next/app/setting/settings/object-store/page.test.tsx
+++ b/web-next/app/setting/settings/object-store/page.test.tsx
@@ -89,6 +89,7 @@ describe('setting object store page', () => {
   });
 
   it('renders the cold object store form on the shared settings console contract', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const { default: SettingObjectStorePage } = await import('./page');
     const html = renderToStaticMarkup(<SettingObjectStorePage />);
 
@@ -106,32 +107,30 @@ describe('setting object store page', () => {
     expect(html).toContain('data-setting-object-store-type-change="angular-reset-config-on-type-change"');
     expect(html).toContain('data-setting-object-store-obs-fields="cold-obs-fields"');
     expect(html).toContain('data-setting-object-store-actions="standard-equal-buttons"');
-    expect(html).toContain('文件服务配置');
+    expect(html).toContain(t('settings.object-store'));
     expect(html).toContain('data-settings-form-owner="cold-settings-form-owner"');
     expect(html).toContain('data-settings-form-control="cold-select-control"');
     expect(html).toContain('data-settings-form-control="cold-input-control"');
-    expect(html).toContain('文件服务提供商');
+    expect(html).toContain(t('settings.object-store.type'));
     expect(html).toContain('AccessKey');
     expect(html).toContain('SecretKey');
     expect(html).toContain('Bucket');
     expect(html).toContain('Endpoint');
-    expect(html).toContain('保存路径');
-    expect(html).toContain('placeholder="华为云 AccessKeyId"');
-    expect(html).toContain('placeholder="华为云 AccessKeySecret"');
-    expect(html).toContain('placeholder="华为云 OBS 中创建的 Bucket 名称"');
-    expect(html).toContain('placeholder="华为云 OBS 地域域名,不包括 Bucket 名"');
-    expect(html).toContain('placeholder="备份文件保存路径,默认是 hertzbeat"');
-    expect(html).toContain('本地数据库(默认)');
-    expect(html).toContain('本地文件');
-    expect(html).toContain('华为云 OBS');
-    expect(html).toContain('确认更新');
+    expect(html).toContain(t('settings.object-store.obs.savePath'));
+    expect(html).toContain(`placeholder="${t('settings.object-store.obs.accessKey.placeholder')}"`);
+    expect(html).toContain(`placeholder="${t('settings.object-store.obs.secretKey.placeholder')}"`);
+    expect(html).toContain(`placeholder="${t('settings.object-store.obs.bucketName.placeholder')}"`);
+    expect(html).toContain(`placeholder="${t('settings.object-store.obs.endpoint.placeholder')}"`);
+    expect(html).toContain(`placeholder="${t('settings.object-store.obs.savePath.placeholder')}"`);
+    expect(html).toContain(t('settings.object-store.type.database'));
+    expect(html).toContain(t('settings.object-store.type.file'));
+    expect(html).toContain(t('settings.object-store.type.obs'));
+    expect(html).toContain(t('settings.system-config.ok'));
     expect(html).not.toContain('data-setting-object-store-page="angular-object-store"');
     expect(html).not.toContain('data-setting-object-store-form="angular-vertical-form"');
     expect(html).not.toContain('data-setting-object-store-provider="angular-provider-select"');
     expect(html).not.toContain('data-setting-object-store-obs-fields="angular-obs-fields"');
     expect(html).not.toContain('data-setting-object-store-summary-rail=');
-    expect(html).not.toContain('文件服务摘要');
-    expect(html).not.toContain('当前文件服务');
     expect(html).not.toContain('File Server Configuration');
     expect(html).not.toContain('Store Type');
     expect(html).not.toContain('Save</button>');
diff --git a/web-next/app/setting/settings/server/page.test.tsx b/web-next/app/setting/settings/server/page.test.tsx
index 49c1e2a5fc..f9b5cc2969 100644
--- a/web-next/app/setting/settings/server/page.test.tsx
+++ b/web-next/app/setting/settings/server/page.test.tsx
@@ -170,12 +170,13 @@ describe('setting server page', () => {
     apiMessagePost.mockReset();
   });
 
-  it('renders the cold Chinese summary list and message-server dialog wiring on the shared settings console contract', async () => {
+  it('renders the localized summary list and message-server dialog wiring on the shared settings console contract', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const { default: SettingServerPage } = await import('./page');
     const html = renderToStaticMarkup(<SettingServerPage />);
 
     expect(html).toContain('data-client-workbench="true"');
-    expect(html).toContain('data-loading-copy="正在加载消息服务配置。"');
+    expect(html).toContain(`data-loading-copy="${t('setting.settings.server.loading')}"`);
     expect(html).toContain('data-settings-console-title="true"');
     expect(html).toContain('data-settings-server-page="otlp-cold-message-server"');
     expect(html).toContain('data-settings-server-style-baseline="hertzbeat-cold-matte"');
@@ -192,19 +193,19 @@ describe('setting server page', () => {
     expect(html).toContain('data-settings-summary-action="email"');
     expect(html).toContain('data-settings-summary-action="sms"');
     expect(html).toContain('data-settings-summary-action-style="cold-compact-action"');
-    expect(html).toContain('aria-label="配置邮件服务器"');
-    expect(html).toContain('aria-label="配置短信配置"');
-    expect(html).toContain('消息服务配置');
-    expect(html).toContain('邮件服务器');
-    expect(html).toContain('短信配置');
-    expect(html).toContain('邮箱服务器地址: smtp.example.com');
-    expect(html).toContain('邮箱账号: ops');
-    expect(html).toContain('是否启用邮箱配置: 是');
-    expect(html).toContain('腾讯短信AppId: 10001');
-    expect(html).toContain('腾讯短信SignName: old-sign');
-    expect(html).toContain('启用状态: 否');
-    expect(html).toContain('data-overlay-dialog-title="配置邮件服务器"');
-    expect(html).toContain('data-overlay-dialog-title="配置短信参数"');
+    expect(html).toContain(`aria-label="${t('settings.server.summary.configure-action', { title: t('settings.server.email') })}"`);
+    expect(html).toContain(`aria-label="${t('settings.server.summary.configure-action', { title: t('settings.server.sms') })}"`);
+    expect(html).toContain(t('settings.server'));
+    expect(html).toContain(t('settings.server.email'));
+    expect(html).toContain(t('settings.server.sms'));
+    expect(html).toContain(`${t('alert.notice.sender.mail.host')}: smtp.example.com`);
+    expect(html).toContain(`${t('alert.notice.sender.mail.username')}: ops`);
+    expect(html).toContain(`${t('alert.notice.sender.mail.enable')}: ${t('common.yes')}`);
+    expect(html).toContain(`${t('alert.notice.sender.sms.tencent.appId')}: 10001`);
+    expect(html).toContain(`${t('alert.notice.sender.sms.tencent.signName')}: old-sign`);
+    expect(html).toContain(`${t('common.enable')}: ${t('common.no')}`);
+    expect(html).toContain(`data-overlay-dialog-title="${t('settings.server.email.setting')}"`);
+    expect(html).toContain(`data-overlay-dialog-title="${t('settings.server.sms.setting')}"`);
     expect(html).toContain('data-overlay-dialog-max-width="w-[min(92vw,520px)] md:w-[40vw] md:max-w-[40vw]"');
     expect(html).toContain('data-settings-server-email-dialog-width="angular-width-40-percent"');
     expect(html).toContain('data-settings-server-email-dialog-mask="angular-mask-closable-false"');
@@ -215,18 +216,16 @@ describe('setting server page', () => {
     expect(html).toContain('data-settings-dialog-control-span="12"');
     expect(html).toContain('data-settings-server-email-apply-contract="angular-apply-notify"');
     expect(html).toContain('data-settings-server-sms-apply-contract="angular-apply-notify"');
-    expect(html).toContain('邮箱密码');
-    expect(html).toContain('短信类型');
-    expect(html).toContain('腾讯短信');
+    expect(html).toContain(t('alert.notice.sender.mail.password'));
+    expect(html).toContain(t('alert.notice.sender.sms.type'));
+    expect(html).toContain(t('alert.notice.sender.sms.type.tencent'));
     expect(html).toContain('Twilio Sms');
-    expect(html).toContain('取消');
-    expect(html).toContain('保存');
-    expect(html).toContain('配置');
+    expect(html).toContain(t('common.button.cancel'));
+    expect(html).toContain(t('common.button.save'));
+    expect(html).toContain(t('common.button.setting'));
     expect(html).not.toContain('data-settings-server-page="angular-message-server"');
     expect(html).not.toContain('data-settings-server-summary-rail=');
     expect(html).not.toContain('data-settings-summary-list-style="angular-nz-list"');
-    expect(html).not.toContain('消息服务摘要');
-    expect(html).not.toContain('当前消息服务');
     expect(html).not.toContain('Message Server Setting');
     expect(html).not.toContain('Email Server Address');
     expect(html).not.toContain('Setting</button>');
diff --git a/web-next/app/setting/settings/settings-layout-client.test.tsx b/web-next/app/setting/settings/settings-layout-client.test.tsx
index 1b69691c02..4c6657c81c 100644
--- a/web-next/app/setting/settings/settings-layout-client.test.tsx
+++ b/web-next/app/setting/settings/settings-layout-client.test.tsx
@@ -58,15 +58,15 @@ describe('settings settings layout client', () => {
     );
 
     expect(html).toContain('data-active-href="/setting/settings/config"');
-    expect(html).toContain('设置');
-    expect(html).toContain('系统设置');
-    expect(html).toContain('在同一控制台配置系统、消息服务、对象存储和令牌。');
-    expect(html).toContain('aria-label="设置分区"');
-    expect(html).toContain('aria-label="设置内容"');
-    expect(html).toContain('系统配置');
-    expect(html).toContain('消息服务配置');
-    expect(html).toContain('文件服务配置');
-    expect(html).toContain('令牌管理');
+    expect(html).toContain(t('settings.console.kicker'));
+    expect(html).toContain(t('settings.console.title'));
+    expect(html).toContain(t('settings.console.copy'));
+    expect(html).toContain(`aria-label="${t('settings.console.navigation')}"`);
+    expect(html).toContain(`aria-label="${t('settings.console.content')}"`);
+    expect(html).toContain(t('settings.system-config'));
+    expect(html).toContain(t('settings.server'));
+    expect(html).toContain(t('settings.object-store'));
+    expect(html).toContain(t('settings.token'));
     expect(html).toContain('content');
     expect(html).not.toContain('settings.console.');
   });
diff --git a/web-next/app/setting/settings/token/page.test.tsx b/web-next/app/setting/settings/token/page.test.tsx
index f05321148d..c50c237f89 100644
--- a/web-next/app/setting/settings/token/page.test.tsx
+++ b/web-next/app/setting/settings/token/page.test.tsx
@@ -91,14 +91,18 @@ vi.mock('../../../../lib/setting-token/controller', () => ({
 }));
 
 vi.mock('../../../../lib/setting-token/view-model', () => ({
-  buildTokenExpirationOptions: () => [
-    { value: '-1', label: '永不过期' },
-    { value: '604800', label: '7 天' },
-    { value: '2592000', label: '30 天' },
-    { value: '7776000', label: '90 天' },
-    { value: '15552000', label: '180 天' },
-    { value: '31536000', label: '365 天' }
-  ],
+  buildTokenExpirationOptions: () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    return [
+      { value: '-1', label: t('setting.token.expiration.never') },
+      { value: '604800', label: t('setting.token.expiration.7d') },
+      { value: '2592000', label: t('setting.token.expiration.30d') },
+      { value: '7776000', label: t('setting.token.expiration.90d') },
+      { value: '15552000', label: t('setting.token.expiration.180d') },
+      { value: '31536000', label: t('setting.token.expiration.365d') }
+    ];
+  },
   isExpired: (token: { expireTime?: string | null }) => token.expireTime === '2026-04-02T08:00:00Z'
 }));
 
@@ -115,6 +119,7 @@ describe('setting token page', () => {
   });
 
   it('renders the cold settings token console with counts and the token table', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const { default: SettingTokenPage } = await import('./page');
     const html = renderToStaticMarkup(<SettingTokenPage />);
 
@@ -141,25 +146,25 @@ describe('setting token page', () => {
     expect(html).toContain('data-setting-token-delete-confirm-trigger="angular-modal-confirm"');
     expect(html).toContain('data-setting-token-load-failure-contract="angular-load-failed-retry"');
     expect(html).toContain('data-setting-token-load-failure-contract-owner="hertzbeat-ui-inline-feedback"');
-    expect(html).toContain('令牌管理');
-    expect(html).toContain('API 密钥');
-    expect(html).toContain('统一管理遥测和自动化令牌');
-    expect(html).toContain('在同一页查看令牌状态、最近使用情况和撤销入口。');
-    expect(html).toContain('生成令牌');
-    expect(html).toContain('令牌总数');
-    expect(html).toContain('可用令牌');
-    expect(html).toContain('过期令牌');
-    expect(html).toContain('令牌名称');
-    expect(html).toContain('令牌值');
-    expect(html).toContain('创建者');
-    expect(html).toContain('创建时间');
-    expect(html).toContain('过期时间');
-    expect(html).toContain('最近使用时间');
-    expect(html).toContain('操作');
-    expect(html).toContain('删除选中项');
-    expect(html).toContain('aria-label="删除OTLP ingestion"');
-    expect(html).toContain('aria-label="删除Expired token"');
-    expect(html).toContain('永不过期');
+    expect(html).toContain(t('settings.token'));
+    expect(html).toContain(t('settings.token.console.kicker'));
+    expect(html).toContain(t('settings.token.console.title'));
+    expect(html).toContain(t('settings.token.console.copy'));
+    expect(html).toContain(t('settings.token.generate'));
+    expect(html).toContain(t('settings.token.console.result.total'));
+    expect(html).toContain(t('settings.token.console.result.active'));
+    expect(html).toContain(t('settings.token.console.result.expired'));
+    expect(html).toContain(t('settings.token.name'));
+    expect(html).toContain(t('settings.token.value'));
+    expect(html).toContain(t('settings.token.creator'));
+    expect(html).toContain(t('settings.token.create-time'));
+    expect(html).toContain(t('settings.token.expire-time'));
+    expect(html).toContain(t('settings.token.last-used'));
+    expect(html).toContain(t('common.edit'));
+    expect(html).toContain(t('common.button.delete'));
+    expect(html).toContain(`aria-label="${t('settings.token.delete-action', { name: 'OTLP ingestion' })}"`);
+    expect(html).toContain(`aria-label="${t('settings.token.delete-action', { name: 'Expired token' })}"`);
+    expect(html).toContain(t('settings.token.expire.never'));
     expect(html).toContain('OTLP ingestion');
     expect(html).toContain('Expired token');
     expect(html).toContain('hb_xxx');
@@ -173,8 +178,6 @@ describe('setting token page', () => {
     expect(html).not.toContain('data-workbench-page="true"');
     expect(html).not.toContain('data-stage-section=');
     expect(html).not.toContain('API Keys');
-    expect(html).not.toContain('令牌摘要');
-    expect(html).not.toContain('当前令牌');
     expect(html).not.toContain('Generate token');
     expect(html).not.toContain('Token management');
 
@@ -260,10 +263,11 @@ describe('setting token page', () => {
     try {
       const { default: SettingTokenPage } = await import('./page');
       const html = renderToStaticMarkup(<SettingTokenPage />);
+      const t = createTranslatorMock({ locale: 'zh-CN' });
 
       expect(html).toContain('data-setting-token-table="cold-token-table"');
-      expect(html).toContain('无');
-      expect(html).toContain('永不过期');
+      expect(html).toContain(t('common.none'));
+      expect(html).toContain(t('settings.token.expire.never'));
       expect(html).not.toContain('<td class="border-b border-r border-[#2b3039] bg-[#0b0c0e] px-3 py-3 text-[#d0d5dd] last:border-r-0">-</td>');
       expect(html).not.toContain('<code class="rounded-[3px] border border-[#2b3039] bg-[#101217] px-1.5 py-0.5 text-[12px] text-[#c8d2df]">-</code>');
     } finally {
diff --git a/web-next/app/setting/status/page.test.tsx b/web-next/app/setting/status/page.test.tsx
index 97eb011536..cdb12d775d 100644
--- a/web-next/app/setting/status/page.test.tsx
+++ b/web-next/app/setting/status/page.test.tsx
@@ -47,7 +47,7 @@ vi.mock('@/components/pages/status-setting-surface', () => ({
   StatusSettingSurface: ({ publicStatusHref, mode }: any) => (
     <div data-status-setting-surface="otlp-cold-status-console" data-status-setting-style-baseline="hertzbeat-cold-matte">
       <section data-status-admin-layout="full-width-admin-list">
-        <span>状态页表格</span>
+        <span>Status page table</span>
       </section>
       <span>{publicStatusHref}</span>
       <span>{mode}</span>
@@ -179,8 +179,8 @@ describe('setting status page', () => {
     expect(html).toContain('data-status-setting-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-status-admin-layout="full-width-admin-list"');
     expect(html).not.toContain('data-status-summary-rail=');
-    expect(html).not.toContain('状态页摘要');
-    expect(html).not.toContain('当前状态页');
+    expect(html).not.toContain('data-status-setting-route="angular-status-page"');
+    expect(html).not.toContain('data-status-header-public-link');
     expect(html).toContain('/status');
     expect(html).toContain('HB Status');
     expect(loadStatusManagementData).toHaveBeenCalledWith(apiMessageGet, {
diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 99e0ce9978..38056bc2ea 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
+import type { TranslationParams } from '../../lib/i18n';
 
 const i18nState = vi.hoisted(() => ({
   locale: 'zh-CN' as 'zh-CN' | 'en-US'
@@ -25,6 +26,16 @@ vi.mock('next/navigation', () => ({
   })
 }));
 
+const zhT = createTranslatorMock({ locale: 'zh-CN' });
+
+function tZh(key: string, params?: TranslationParams) {
+  return zhT(key, params);
+}
+
+function escapeRegExp(value: string) {
+  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
 describe('topology page', () => {
   beforeEach(() => {
     i18nState.locale = 'zh-CN';
@@ -425,7 +436,7 @@ describe('topology page', () => {
     expect(source).toContain('HzTopologySectionLabel');
     expect(source).toContain('useI18n');
     expect(source).toContain("t('topology.search.placeholder')");
-    expect(source).not.toContain('搜索实体、服务、资源或标签');
+    expect(source).not.toContain(tZh('topology.search.placeholder'));
     expect(source).not.toContain('OpsSurfacePage');
     expect(source).not.toContain('buildTopologySurfaceConfig');
     expect(source).not.toContain('Monitor center');
@@ -622,8 +633,14 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-path-summary-hovered-edge-id="none"');
     expect(html).toContain('data-hz-topology-path-selected-edge="none"');
     expect(html).toContain('data-hz-topology-path-hovered-edge="none"');
-    expect(html).toContain('路径预览');
-    expect(html).not.toMatch(/data-hz-topology-path-summary-title-owner="hertzbeat-ui-path-summary-title"[^>]*>选中路径</);
+    expect(html).toContain(tZh('topology.path-summary.title.preview'));
+    expect(html).not.toMatch(
+      new RegExp(
+        `data-hz-topology-path-summary-title-owner="hertzbeat-ui-path-summary-title"[^>]*>${escapeRegExp(
+          tZh('topology.path-summary.title.selected')
+        )}</`
+      )
+    );
     expect(html).toContain('data-topology-g6-mount-lifecycle-browser-regression="selection-preserve-viewport"');
     expect(html).toContain('data-topology-g6-mount-lifecycle-browser-regression-owner="hertzbeat-ui-g6-mount-lifecycle"');
     expect(html).toContain('data-topology-g6-mount-lifecycle-selection-policy="node-edge-table-drawer-only"');
@@ -742,7 +759,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-focus-filter="view"');
     expect(html).not.toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
     expect(html).not.toContain('data-topology-focus-trail-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
-    expect(html).not.toContain('当前范围隐藏 0 个');
+    expect(html).not.toContain(tZh('topology.focus-trail.hidden-count', { count: 0 }));
     expect(html).toContain('data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit"');
     expect(html).toContain('data-topology-focus-trail-exit-owner="hertzbeat-ui-focus-trail-exit"');
     expect(html).toMatch(
@@ -774,8 +791,14 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-path-summary-hovered-edge-id="none"');
     expect(html).toContain('data-hz-topology-path-selected-edge="none"');
     expect(html).toContain('data-hz-topology-path-hovered-edge="none"');
-    expect(html).toContain('路径预览');
-    expect(html).not.toMatch(/data-hz-topology-path-summary-title-owner="hertzbeat-ui-path-summary-title"[^>]*>选中路径</);
+    expect(html).toContain(tZh('topology.path-summary.title.preview'));
+    expect(html).not.toMatch(
+      new RegExp(
+        `data-hz-topology-path-summary-title-owner="hertzbeat-ui-path-summary-title"[^>]*>${escapeRegExp(
+          tZh('topology.path-summary.title.selected')
+        )}</`
+      )
+    );
     expect(html).toContain('data-hz-topology-path-source-id="svc-frontend"');
     expect(html).toContain('data-hz-topology-path-target-id="svc-checkout"');
     expect(html).toContain('data-hz-topology-path-relation-type="trace-call"');
@@ -1242,22 +1265,22 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-detail-signal-action-link-owner="hertzbeat-ui-detail-signal-action-link"');
     expect(html).toContain('data-hz-topology-detail-signal-action-label-owner="hertzbeat-ui-detail-signal-action-label"');
     expect(html).toContain('data-hz-topology-detail-signal-action="metrics"');
-    expect(html).toContain('运维拓扑');
-    expect(html).not.toContain('HertzBeat 企业运维拓扑');
-    expect(html).toContain('应用拓扑');
-    expect(html).toContain('服务调用');
-    expect(html).toContain('资源依赖');
-    expect(html).toContain('告警影响面');
-    expect(html).toContain('OTLP 调用关系');
-    expect(html).toContain('监控对象归属');
-    expect(html).toContain('模板依赖');
-    expect(html).toContain('K8s 工作负载');
-    expect(html).toContain('数据库 / 中间件连接');
-    expect(html).toContain('CMDB / 手工标签');
+    expect(html).toContain(tZh('topology.identity'));
+    expect(html).not.toContain('HertzBeat enterprise operations topology');
+    expect(html).toContain(tZh('topology.view.application.label'));
+    expect(html).toContain(tZh('topology.view.service-call.label'));
+    expect(html).toContain(tZh('topology.view.resource-dependency.label'));
+    expect(html).toContain(tZh('topology.view.alert-impact.label'));
+    expect(html).toContain(tZh('topology.source.otlp-trace-call.label'));
+    expect(html).toContain(tZh('topology.source.monitor-ownership.label'));
+    expect(html).toContain(tZh('topology.source.template-dependency.label'));
+    expect(html).toContain(tZh('topology.source.k8s-workload.label'));
+    expect(html).toContain(tZh('topology.source.database-middleware-connection.label'));
+    expect(html).toContain(tZh('topology.source.cmdb-manual-label.label'));
     expect(html).toContain('checkout-api');
     expect(html).toContain('redis');
-    expect(html).toContain('刷新拓扑');
-    expect(html).toContain('适配视图');
+    expect(html).toContain(tZh('topology.refresh'));
+    expect(html).toContain(tZh('topology.view.fit'));
     expect(html).toContain('data-topology-alert-impact-link="alert-center"');
     expect(html).toContain('data-topology-alert-impact-link-owner="hertzbeat-ui-action-link"');
     expect(html).toContain('data-topology-alert-impact-link-spacing-owner="hertzbeat-ui-action-link-spacing"');
@@ -1269,8 +1292,8 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-action-link-spacing-owner="hertzbeat-ui-action-link-spacing"');
     expect(html).toContain('data-hz-topology-action-link-label-owner="hertzbeat-ui-action-link-label"');
     expect(html).toContain('data-hz-topology-action-link-copy-owner="hertzbeat-ui-action-link-copy"');
-    expect(html).toContain('查看实体详情');
-    expect(html).toContain('打开三信号');
+    expect(html).toContain(tZh('topology.context-link.entity'));
+    expect(html).toContain(tZh('topology.context-link.signals'));
     expect(html).toContain('data-topology-node-select-mode="in-page-drawer"');
     expect(html).toContain('data-topology-node-select-url-policy="preserve-current-url"');
     expect(html).not.toContain('data-topology-node-select-href=');
@@ -1281,7 +1304,7 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-context-link="traces"');
     expect(html).toContain('data-topology-node-id="svc-checkout"');
     expect(html).toContain('checkout-api');
-    expect(html).toContain('健康评分 82');
+    expect(html).toContain(tZh('entity.health.label', { score: 82 }));
     expect(html).toContain('/ingestion/otlp/metrics?');
     expect(html).toContain('/log/manage?');
     expect(html).toContain('/trace/manage?');
@@ -1309,14 +1332,14 @@ describe('topology page', () => {
     expect(headerHtml).not.toContain('data-topology-source-strip-density="label-only"');
     expect(headerHtml).not.toContain('data-topology-source-strip-compactness="single-line-wrap"');
     expect(headerHtml).not.toContain('data-hz-topology-filter-strip-variant="source-rail"');
-    expect(headerHtml).toContain('运维拓扑');
-    expect(headerHtml).not.toContain('OTLP 调用关系');
-    expect(headerHtml).not.toContain('CMDB / 手工标签');
+    expect(headerHtml).toContain(tZh('topology.identity'));
+    expect(headerHtml).not.toContain(tZh('topology.source.otlp-trace-call.label'));
+    expect(headerHtml).not.toContain(tZh('topology.source.cmdb-manual-label.label'));
     expect(headerHtml).not.toContain('data-hz-topology-workbench-copy-owner="hertzbeat-ui-workbench-copy"');
-    expect(headerHtml).not.toContain('以实体中心为锚点');
-    expect(headerHtml).not.toContain('从 trace span 归并服务调用边');
-    expect(headerHtml).not.toContain('从 HertzBeat 监控对象绑定实体');
-    expect(headerHtml).not.toContain('从 CMDB 导入和手工标签补齐归属');
+    expect(headerHtml).not.toContain(tZh('topology.copy'));
+    expect(headerHtml).not.toContain(tZh('topology.source.otlp-trace-call.copy'));
+    expect(headerHtml).not.toContain(tZh('topology.source.monitor-ownership.copy'));
+    expect(headerHtml).not.toContain(tZh('topology.source.cmdb-manual-label.copy'));
   });
 
   it('renders API-backed topology graph data when the topology read API returns nodes and edges', async () => {
@@ -1411,10 +1434,18 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-focus-crumb-active="true"');
     expect(html).toContain('data-topology-depth-state="1"');
     expect(html).toMatch(
-      /<a(?=[^>]*data-hz-topology-focus-crumb="active-entity")[\s\S]*?<span[^>]*data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value">1 跳<\/span>/
+      new RegExp(
+        `<a(?=[^>]*data-hz-topology-focus-crumb="active-entity")[\\s\\S]*?<span[^>]*data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value">${escapeRegExp(
+          tZh('topology.state.depth.one-hop')
+        )}<\\/span>`
+      )
     );
     expect(html).not.toMatch(
-      /<a(?=[^>]*data-hz-topology-focus-crumb="active-entity")[\s\S]*?<span[^>]*data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value">2 跳<\/span>/
+      new RegExp(
+        `<a(?=[^>]*data-hz-topology-focus-crumb="active-entity")[\\s\\S]*?<span[^>]*data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value">${escapeRegExp(
+          tZh('topology.state.depth.two-hop')
+        )}<\\/span>`
+      )
     );
     expect(html).toContain('data-hz-topology-focus-filter="source"');
     expect(html).toContain('data-topology-focus-filter-source="cmdb-manual-label"');
@@ -1452,8 +1483,12 @@ describe('topology page', () => {
     expect(html).not.toContain('absolute left-3 top-12 flex');
     expect(html).toContain('data-hz-topology-g6-filter-visible-node-count="2"');
     expect(html).toContain('data-hz-topology-g6-filter-visible-edge-count="1"');
-    expect(html).toContain('2 个可见节点 · 1 条可见边');
-    expect(html).not.toContain('7 个可见节点 · 7 条可见边');
+    expect(html).toContain(
+      `${tZh('topology.group-panel.node-count', { count: 2 })} · ${tZh('topology.group-panel.edge-count', { count: 1 })}`
+    );
+    expect(html).not.toContain(
+      `${tZh('topology.group-panel.node-count', { count: 7 })} · ${tZh('topology.group-panel.edge-count', { count: 7 })}`
+    );
     expect(html).toContain('data-hz-topology-g6-group-item="otlp-trace-call"');
     expect(html).toContain('data-hz-topology-g6-group-item-action-owner="hertzbeat-ui-g6-group-item-action"');
     expect(html).toMatch(
@@ -1757,7 +1792,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-request-rate="7.25"');
     expect(html).toContain('data-hz-topology-error-rate="0.021"');
     expect(html).toContain('data-hz-topology-latency-p95-ms="123"');
-    expect(html).toContain('边指标排行');
+    expect(html).toContain(tZh('topology.metric-table.title'));
   }, 60000);
 
   it('surfaces the render-window RED table below the graph when a real API topology is windowed', async () => {
@@ -1818,8 +1853,17 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-summary-total-node-count="230"');
     expect(html).toContain('data-hz-topology-g6-summary-rendered-edge-count="180"');
     expect(html).toContain('data-hz-topology-g6-summary-total-edge-count="229"');
-    expect(html).toContain('200/230 个节点已渲染 · 180/229 条边已渲染');
-    expect(html).not.toContain('230 个可见节点 · 229 条可见边');
+    expect(html).toContain(
+      `${tZh('topology.g6.summary.rendered-node-count', { rendered: 200, total: 230 })} · ${tZh(
+        'topology.g6.summary.rendered-edge-count',
+        { rendered: 180, total: 229 }
+      )}`
+    );
+    expect(html).not.toContain(
+      `${tZh('topology.group-panel.node-count', { count: 230 })} · ${tZh('topology.group-panel.edge-count', {
+        count: 229
+      })}`
+    );
     expect(html).toContain('data-topology-metric-table-placement="graph-bottom"');
     expect(html).toContain('data-topology-metric-table-visibility="render-window-companion"');
     expect(html).toContain('data-topology-metric-table-scope="edge-red-render-window"');
@@ -1846,7 +1890,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-metric-table-canvas-rendered-edge-count="180"');
     expect(html).toContain('data-hz-topology-metric-table-table-edge-count="229"');
     expect(html).toContain('data-hz-topology-metric-table-edge-summary-owner="hertzbeat-ui-metric-table-edge-summary"');
-    expect(html).toContain('画布 180 / 表格 229');
+    expect(html).toContain(tZh('topology.metric-table.edge-summary', { rendered: 180, total: 229 }));
     expect(readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8')).toContain("'topology.metric-table.edge-summary'");
     expect(html).toContain('data-hz-topology-metric-table-render-window-table-companion="required"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="required"');
@@ -1855,15 +1899,15 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-metric-table-rendered-row-count="120"');
     expect(html).toContain('data-hz-topology-metric-table-rendered-hidden-row-count="109"');
     expect(html).toContain('data-hz-topology-metric-table-row-render-summary-owner="hertzbeat-ui-metric-table-row-render-summary"');
-    expect(html).toContain('显示 120 / 229 条关系');
+    expect(html).toContain(tZh('topology.metric-table.row-render-summary', { rendered: 120, total: 229 }));
     expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
-    expect(html).toContain('两端在画布');
-    expect(html).toContain('一端在画布');
-    expect(html).toContain('画布外');
-    expect(html).toContain('端点未知');
-    expect(html).not.toContain('半可见');
+    expect(html).toContain(tZh('topology.metric-table.filter.visible'));
+    expect(html).toContain(tZh('topology.metric-table.filter.partial'));
+    expect(html).toContain(tZh('topology.metric-table.filter.hidden'));
+    expect(html).toContain(tZh('topology.metric-table.filter.unknown'));
+    expect(html).not.toContain('semi-visible');
     const renderedMetricRows = html.match(/data-hz-topology-edge-row="/g) ?? [];
     const metricRowCountMatch = html.match(/data-hz-topology-metric-rows="(\d+)"/);
     const metricRenderedRowCountMatch = html.match(/data-hz-topology-metric-table-rendered-row-count="(\d+)"/);
@@ -1894,8 +1938,8 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-edge-render-window-target-visible="false"');
     expect(html).toContain('data-topology-edge-render-window-fact="partial"');
     expect(html).toContain('data-topology-edge-render-window-fact-owner="hertzbeat-ui-detail-render-window-context"');
-    expect(html).toContain('画布窗口');
-    expect(html).toContain('一端在画布');
+    expect(html).toContain(tZh('topology.edge.render-window.label'));
+    expect(html).toContain(tZh('topology.metric-table.filter.partial'));
   }, 60000);
 
   it('does not turn an overview route into a focused graph when the API returns a default focus entity', async () => {
@@ -2026,8 +2070,8 @@ describe('topology page', () => {
     expect(html).toContain('viewMode=service-call');
     expect(html).toContain('relationType=trace-call');
     expect(html).toContain('data-topology-empty-state="none"');
-    expect(html).toContain('未返回关系边');
-    expect(html).toContain('查看调用关系');
+    expect(html).toContain(tZh('topology.relation-gap.title'));
+    expect(html).toContain(tZh('topology.relation-gap.action.trace-call'));
   }, 60000);
 
   it('renders API-backed impact timeline evidence when topology returns change events', async () => {
@@ -2197,9 +2241,9 @@ describe('topology page', () => {
     expect(html).not.toContain('data-hz-ui="topology-metric-table"');
     expect(html).not.toContain('data-hz-ui="topology-detail-drawer"');
     expect(html).not.toContain('data-topology-current-entity-panel=');
-    expect(html).toContain('暂无 trace 调用拓扑证据');
-    expect(html).toContain('当前窗口无 trace 调用边。');
-    expect(html).not.toContain('Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。');
+    expect(html).toContain(tZh('topology.empty.trace-call.title'));
+    expect(html).toContain(tZh('topology.empty.trace-call.copy'));
+    expect(html).not.toContain(tZh('topology.degraded.trace-call.copy'));
     expect(html).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
     expect(html).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
     expect(html).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
@@ -2297,9 +2341,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-empty-relation-type="trace-call"');
     expect(html).toContain('data-hz-topology-empty-result-count="0"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace"');
-    expect(html).toContain('暂无 trace 调用拓扑证据');
-    expect(html).toContain('当前窗口无 trace 调用边。');
-    expect(html).not.toContain('Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。');
+    expect(html).toContain(tZh('topology.empty.trace-call.title'));
+    expect(html).toContain(tZh('topology.empty.trace-call.copy'));
+    expect(html).not.toContain(tZh('topology.degraded.trace-call.copy'));
     expect(html).toContain('data-hz-topology-g6-node-count="0"');
     expect(html).toContain('data-hz-topology-g6-edge-count="0"');
     expect(html).not.toContain('data-hz-ui="topology-companion-rail"');
@@ -2532,7 +2576,7 @@ describe('topology page', () => {
     expect(pendingHtml).toContain('data-hz-topology-loading-source="API"');
     expect(pendingHtml).toContain('data-hz-topology-loading-time-scope="last-1h"');
     expect(pendingHtml).toContain('data-hz-topology-loading-evidence-sources="api greptime trace relation"');
-    expect(pendingHtml).toContain('正在加载拓扑证据');
+    expect(pendingHtml).toContain(tZh('topology.loading.api.title'));
     expect(pendingHtml).not.toContain('data-topology-empty-state="api-empty"');
     expect(pendingTraceCallHtml).toContain('data-topology-trace-call-state="pending"');
     expect(pendingTraceCallHtml).toContain('data-topology-trace-call-red-state="pending"');
@@ -2558,9 +2602,9 @@ describe('topology page', () => {
     expect(unavailableHtml).toContain('data-hz-topology-empty-copy-visibility="assistive"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-kind="degraded"');
-    expect(unavailableHtml).toContain('data-hz-topology-empty-source="拓扑 API 不可用"');
+    expect(unavailableHtml).toContain(`data-hz-topology-empty-source="${tZh('topology.degraded.api.source')}"`);
     expect(unavailableHtml).toContain('data-hz-topology-empty-evidence-sources="api unavailable"');
-    expect(unavailableHtml).toContain('拓扑 API 不可用');
+    expect(unavailableHtml).toContain(tZh('topology.degraded.api.title'));
     expect(traceCallUnavailableHtml).toContain('data-topology-trace-call-state="degraded"');
     expect(traceCallUnavailableHtml).toContain('data-topology-empty-state="trace-call-degraded"');
     expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-source="Greptime trace graph"');
@@ -2597,13 +2641,13 @@ describe('topology page', () => {
     expect(html).not.toContain('value="checkout-api"');
     expect(html).toContain('last-30m');
     expect(html).toContain('timeRange=last-30m');
-    expect(html).toContain('当前筛选');
+    expect(html).toContain(tZh('topology.current-filter'));
     expect(html).toContain('data-topology-view-scope="focused-adjacency"');
     expect(html).toContain('data-topology-view-scope-owner="hertzbeat-ui-focus-scope-guide"');
     expect(html).toContain('data-topology-view-scope-global-action="open-global-topology"');
     expect(html).toContain('data-topology-view-scope-global-href="/topology?');
-    expect(html).toContain('当前为局部聚焦视角');
-    expect(html).toContain('查看全局拓扑');
+    expect(html).toContain(tZh('topology.view-scope.focused-adjacency'));
+    expect(html).toContain(tZh('topology.view-scope.focused-adjacency.copy'));
     expect(html).not.toMatch(/data-topology-view-scope-global-href="[^"]*entityId=/);
     expect(html).not.toMatch(/data-topology-view-scope-global-href="[^"]*serviceName=/);
   }, 15000);
@@ -2747,20 +2791,20 @@ describe('topology page', () => {
     expect(html.indexOf('data-topology-edge-evidence-panel="svc-checkout--res-orders-db"')).toBeLessThan(
       html.indexOf('data-topology-companion-section="view-mode"')
     );
-    expect(html).toContain('关系证据');
+    expect(html).toContain(tZh('topology.edge.evidence.title'));
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-detail-fact-density="compressed"');
     expect(html).toContain('database connection');
-    expect(html).toContain('数据库 / 中间件连接');
-    expect(html).toContain('采集证据');
+    expect(html).toContain(tZh('topology.source.database-middleware-connection.label'));
+    expect(html).toContain(tZh('topology.evidence.row.source'));
     expect(html).toContain('checkout-api');
     expect(html).toContain('orders-db');
     expect(html).toContain('data-topology-edge-evidence-boundary="roadmap-boundary"');
-    expect(html).toContain('当前边仅基于已采集的关系证据;依赖自动发现和根因分析仍是 roadmap 能力。');
+    expect(html).toContain(tZh('topology.evidence.boundary'));
     expect(html).toContain('data-topology-edge-link-copy="alert-impact"');
-    expect(html).toContain('带当前边、实体和三信号上下文进入告警影响面。');
+    expect(html).toContain(tZh('topology.evidence.alert-impact.copy'));
     expect(html).toMatch(/data-topology-edge-link="metrics"[^>]+href="\/ingestion\/otlp\/metrics\?[^"]*viewMode=resource-dependency[^"]*sourceKind=database-middleware-connection[^"]*edgeId=svc-checkout--res-orders-db/);
     expect(html).toMatch(/data-topology-edge-link="logs"[^>]+href="\/log\/manage\?[^"]*viewMode=resource-dependency[^"]*sourceKind=database-middleware-connection[^"]*edgeId=svc-checkout--res-orders-db/);
     expect(html).toMatch(/data-topology-edge-link="traces"[^>]+href="\/trace\/manage\?[^"]*viewMode=resource-dependency[^"]*sourceKind=database-middleware-connection[^"]*edgeId=svc-checkout--res-orders-db/);
@@ -2975,19 +3019,21 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-evidence-title-owner="hertzbeat-ui-evidence-list-title"');
     expect(html).toContain('data-hz-topology-evidence-copy-owner="hertzbeat-ui-evidence-list-copy"');
     expect(html).toContain('data-hz-topology-evidence-count-owner="hertzbeat-ui-evidence-list-count"');
-    expect(html).toContain('data-topology-fault-context-row="当前实体"');
+    expect(html).toContain(`data-topology-fault-context-row="${tZh('signal.context.entity.label')}"`);
     expect(html).toContain('data-hz-topology-evidence-item-owner="hertzbeat-ui-evidence-list-item"');
     expect(html).toContain('data-hz-topology-evidence-item-label-owner="hertzbeat-ui-evidence-list-item-label"');
     expect(html).toContain('data-hz-topology-evidence-item-value-owner="hertzbeat-ui-evidence-list-item-value"');
     expect(html).toContain('data-hz-topology-evidence-item-meta-owner="hertzbeat-ui-evidence-list-item-meta"');
     expect(html).toContain('entityId service:commerce/checkout');
-    expect(html).toContain('data-topology-fault-context-row="当前服务"');
+    expect(html).toContain(`data-topology-fault-context-row="${tZh('signal.context.service.label')}"`);
     expect(html).toContain('checkout-api');
-    expect(html).toContain('data-topology-fault-context-row="链路上下文"');
+    expect(html).toContain(`data-topology-fault-context-row="${tZh('signal.context.trace.label')}"`);
     expect(html).toContain('trace-123');
     expect(html).toContain('spanId span-456');
-    expect(html).toContain('data-topology-fault-context-row="采集来源"');
-    expect(html).toContain('采集器 edge-collector-a · 模板 java-service');
+    expect(html).toContain(`data-topology-fault-context-row="${tZh('signal.context.source.label')}"`);
+    expect(html).toContain(
+      `${tZh('signal.context.collector.prefix')} edge-collector-a · ${tZh('signal.context.template.prefix')} java-service`
+    );
     expect(html).toContain('data-topology-active-node-id="svc-checkout"');
     expect(html).toContain('data-topology-active-edge-id="svc-checkout--res-orders-db"');
     expect(html).toContain('trace-123');
@@ -3032,7 +3078,7 @@ describe('topology page', () => {
     expect(html).toContain('Current service');
     expect(html).toContain('Trace context');
     expect(html).toContain('Source');
-    expect(html).not.toMatch(/[一-龥]/);
+    expect(html).not.toMatch(/[\u4e00-\u9fa5]/);
     expect(html).not.toContain('topology.');
     expect(html).not.toContain('signal.context.');
   });
diff --git a/web-next/app/trace/manage/page.test.tsx b/web-next/app/trace/manage/page.test.tsx
index fa3f0a98ee..c74f3469aa 100644
--- a/web-next/app/trace/manage/page.test.tsx
+++ b/web-next/app/trace/manage/page.test.tsx
@@ -136,11 +136,11 @@ vi.mock('@/lib/signal-route-context', () => ({
     return queryString ? `${path}?${queryString}` : path;
   },
   buildSignalEntityContextRows: () => [
-    { label: '当前实体', value: 'checkout', meta: 'entityId 7' },
-    { label: '当前服务', value: 'checkout', meta: 'payments' },
-    { label: '当前环境', value: 'prod', meta: '环境' },
-    { label: '时间范围', value: 'last-1h', meta: '查询窗口' },
-    { label: '采集来源', value: 'OTLP', meta: 'HertzBeat OTLP 接入' }
+    { label: 'Current entity', value: 'checkout', meta: 'entityId 7' },
+    { label: 'Current service', value: 'checkout', meta: 'payments' },
+    { label: 'Current environment', value: 'prod', meta: 'environment' },
+    { label: 'Time range', value: 'last-1h', meta: 'query window' },
+    { label: 'Collection source', value: 'OTLP', meta: 'HertzBeat OTLP intake' }
   ],
   readSignalRouteContext: () => ({
     entityId: mockState.searchParams.get('entityId') || undefined,
@@ -214,21 +214,21 @@ vi.mock('@/lib/trace-manage/view-model', () => ({
       label: 'hertzbeat.entity_id',
       value: routeContext?.entityId || '-',
       state: routeContext?.entityId ? 'present' : 'missing',
-      meta: routeContext?.entityId ? '可打开实体详情' : '缺少实体 ID,实体详情会保持禁用'
+      meta: routeContext?.entityId ? 'Entity detail available' : 'Missing entity ID keeps entity detail disabled'
     },
     {
       key: 'hertzbeat.collector',
       label: 'hertzbeat.collector',
       value: routeContext?.collector || 'collector-local',
       state: 'present',
-      meta: '采集器来源'
+      meta: 'Collector source'
     },
     {
       key: 'hertzbeat.template',
       label: 'hertzbeat.template',
       value: routeContext?.template || 'hertzbeat-self',
       state: 'present',
-      meta: '监控模板归属'
+      meta: 'Monitor template owner'
     }
   ],
   buildTraceExplorerRows: (items: any[], formatDurationNanos: any, formatTime: any) =>
@@ -452,11 +452,6 @@ describe('trace manage page', () => {
     expect(html).toContain('Recent traces');
     expect(html).toContain('Run query');
     expect(html).toContain('Filter traces by service, trace ID, span ID, and error state, then inspect trend, list, and detail evidence.');
-    expect(html).not.toContain('高密度工作面');
-    expect(html).not.toContain('HertzBeat 采集闭环');
-    expect(html).not.toContain('告警闭环');
-    expect(html).not.toContain('接入质量');
-    expect(html).not.toContain('链路查询继续保留接入质量、采集节点、模板归属和告警闭环上下文。');
     expect(html).toContain('/ingestion/otlp?signal=traces');
     expect(html).toContain('POST /checkout');
     expect(html).toContain('checkout');
@@ -468,9 +463,9 @@ describe('trace manage page', () => {
     expect(html).toContain('data-hz-ui="attribute-diagnostics"');
     expect(html).toContain('Attribution diagnostics');
     expect(html).toContain('hertzbeat.entity_id');
-    expect(html).toContain('缺少实体 ID,实体详情会保持禁用');
-    expect(html).toContain('当前实体');
-    expect(html).toContain('采集来源');
+    expect(html).toContain('Missing entity ID keeps entity detail disabled');
+    expect(html).toContain('Current entity');
+    expect(html).toContain('Collection source');
     expect(html).toContain('Entity detail');
     expect(html).toContain('Alert handling');
     expect(html).toContain('data-trace-manage-signal-handoff-hint="trace-log-metric-context"');
@@ -481,10 +476,6 @@ describe('trace manage page', () => {
     expect(html).toContain('Trace status');
     expect(html).toContain('Start time');
     expect(html).toContain('Latest observed');
-    expect(html).not.toContain('告警规则');
-    expect(html).not.toContain('保存视图');
-    expect(html).not.toContain('创建告警');
-    expect(html).not.toContain('加入仪表盘');
     expect(html).not.toContain('signoz-');
     expect(html).not.toContain('data-trace-manage-floating-actions');
     expect(html).not.toContain('Explorer');
@@ -632,9 +623,6 @@ describe('trace manage page', () => {
     expect(source).not.toContain('DrawerSection');
     expect(source).not.toContain('signoz-');
     expect(source).not.toContain('data-trace-manage-floating-actions');
-    expect(source).not.toContain('保存视图');
-    expect(source).not.toContain('创建告警');
-    expect(source).not.toContain('加入仪表盘');
     expect(source).toContain('data-trace-manage-entity-context="hertzbeat-signal-entity-context"');
     expect(source).toContain('data-trace-manage-entity-context-owner="hertzbeat-ui-detail-rows"');
     expect(source).toContain('data-trace-manage-selected-evidence-owner="hertzbeat-ui-detail-rows"');
@@ -652,9 +640,6 @@ describe('trace manage page', () => {
     expect(source).toContain("t('trace.manage.route.entity-context.title')");
     expect(source).toContain("aria-label={t('trace.manage.route.entity-context.aria')}");
     expect(source).not.toContain('data-trace-manage-hertzbeat-loop="collector-template-alert-loop"');
-    expect(source).not.toContain('HertzBeat 采集闭环');
-    expect(source).not.toContain('接入质量');
-    expect(source).not.toContain('链路查询继续保留接入质量、采集节点、模板归属和告警闭环上下文。');
     expect(source).toContain("t('trace.manage.route.action.collectors')");
     expect(source).toContain("t('trace.manage.route.action.templates')");
     expect(source).toContain("t('trace.manage.route.action.entity')");
@@ -731,8 +716,6 @@ describe('trace manage page', () => {
     expect(source).toContain("eyebrow={t('trace.manage.route.detail.title')}");
     expect(source).toContain('HzEmptyState');
     expect(source).toContain("t('trace.manage.route.empty.copy')");
-    expect(source).not.toContain('运行查询后会在这里展示最近链路。');
-    expect(source).not.toContain('告警规则');
     expect(source).not.toContain('className="flex flex-wrap items-center justify-end gap-2"');
     expect(source).not.toContain('className="grid gap-4 2xl:grid-cols-[minmax(0,1fr)_330px]"');
     expect(source).not.toContain('className="grid gap-3 lg:grid-cols-[repeat(4,minmax(0,160px))_minmax(0,1fr)]"');
@@ -803,6 +786,7 @@ describe('trace manage page', () => {
     expect(source).toContain("t('trace.manage.drawer.link-prefix')");
     expect(source).toContain("t('trace.manage.drawer.event-prefix')");
     expect(source).toContain('data-trace-manage-event-detail-copy="span-event-not-span"');
+    expect(source).toContain('data-trace-manage-event-detail-type="span-event"');
     expect(source).toContain('data-trace-manage-event-detail-owner="hertzbeat-ui-dialog-event-notice"');
     expect(source).toContain('HzDialogEventNotice');
     expect(source).toContain('HzDialogEventText');
@@ -817,8 +801,6 @@ describe('trace manage page', () => {
     expect(source).toContain("t('trace.manage.drawer.waterfall.span')");
     expect(source).toContain("t('trace.manage.drawer.waterfall.duration')");
     expect(source).toContain("t('trace.manage.drawer.waterfall.timeline')");
-    expect(source).not.toContain('个 Span');
-    expect(source).not.toContain("['事件', String((selectedSpan?.events || []).length)]");
     expect(source).not.toContain('Link ·');
     expect(source).not.toContain('<Button type="button" variant="subtle" onClick={onClose}>');
     expect(source).not.toContain('data-trace-manage-span-band="cold-span-band"');
diff --git a/web-next/app/trace/manage/trace-manage-page.tsx b/web-next/app/trace/manage/trace-manage-page.tsx
index 3eed44dcc7..3c52fb5f29 100644
--- a/web-next/app/trace/manage/trace-manage-page.tsx
+++ b/web-next/app/trace/manage/trace-manage-page.tsx
@@ -466,6 +466,7 @@ function TraceWaterfallDrawer({
                 {selectedTraceEvent ? (
                   <HzDialogEventNotice
                     data-trace-manage-event-detail="span-event-detail"
+                    data-trace-manage-event-detail-type="span-event"
                     data-trace-manage-event-detail-owner="hertzbeat-ui-dialog-event-notice"
                     title={selectedTraceEvent.event.label}
                     description={(
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index 876a2a4d2d..eb72b7ddb7 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -1553,7 +1553,7 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-alert-notice-rule-template-active-type-owner="route-form-contract"');
     expect(html).toContain('data-hz-ui-lab-alert-notice-rule-single-switch-visual="unframed-switch"');
     expect(html).toContain('data-alert-notice-rule-switch="filter-all"');
-    expect(html).toContain('转发所有');
+    expect(html).toContain('Forward all');
     expect(html).toContain('data-hz-ui-lab-alert-notice-rule-table-switch-update="angular-edit-notify"');
     expect(html).toContain('data-alert-notice-rule-table-switch-update-owner="route-action-feedback-contract"');
     expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-test-validation="angular-backend-owned"');
@@ -4089,12 +4089,12 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-setting-define-save-pending="true"');
     expect(html).toContain('data-cold-code-editor-license="codemirror-mit"');
     expect(html).toContain('data-mocked-codemirror="true"');
-    expect(html).not.toContain('连接、慢查询、可用性和复制状态');
-    expect(html).not.toContain('连接池、事务、锁等待和复制延迟');
-    expect(html).not.toContain('CPU、内存、磁盘、进程和端口');
+    expect(html).toContain('Review monitor resources, template catalog, collection state, and YAML definitions in one place.');
+    expect(html).toContain('Resource type plus non-down baseline');
+    expect(html).toContain('Show only alerting and collecting resources');
     expect(html).toContain('var(--hz-ui-line-soft)');
     expect(html).toContain('var(--hz-ui-line-faint)');
-    expect(html).toContain('新增监控');
+    expect(html).toContain('New monitor');
     expect(html).toContain('Monitor results');
     expect(html).toContain('2xl:grid-cols-[minmax(0,1fr)_520px]');
     expect((html.match(/data-hz-ui="status-badge"/g) || []).length).toBeGreaterThanOrEqual(6);
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
index fdea1c2085..51fc6db695 100644
--- a/web-next/app/ui-lab/page.tsx
+++ b/web-next/app/ui-lab/page.tsx
@@ -226,19 +226,19 @@ import { TimeRangeControl } from '@/components/observability/time-range-control'
 const monitorTypeCategories: HzTemplateCategory[] = [
   {
     id: 'db',
-    label: '数据库监控',
+    label: 'Database monitoring',
     items: [
       {
         id: 'mysql',
         label: 'MySQL',
-        description: '连接、慢查询、可用性和复制状态',
+        description: 'Connections, slow queries, availability, and replication state',
         meta: 'JDBC',
         icon: <Database size={13} />
       },
       {
         id: 'postgresql',
         label: 'PostgreSQL',
-        description: '连接池、事务、锁等待和复制延迟',
+        description: 'Connection pools, transactions, lock waits, and replication lag',
         meta: 'JDBC',
         icon: <Database size={13} />
       }
@@ -246,19 +246,19 @@ const monitorTypeCategories: HzTemplateCategory[] = [
   },
   {
     id: 'os',
-    label: '操作系统监控',
+    label: 'Operating system monitoring',
     items: [
       {
         id: 'linux',
         label: 'Linux',
-        description: 'CPU、内存、磁盘、进程和端口',
+        description: 'CPU, memory, disk, processes, and ports',
         meta: 'SSH',
         icon: <Server size={13} />
       },
       {
         id: 'windows',
         label: 'Windows',
-        description: 'WMI、服务状态和性能计数器',
+        description: 'WMI, service state, and performance counters',
         meta: 'WMI',
         icon: <HardDrive size={13} />
       }
@@ -266,12 +266,12 @@ const monitorTypeCategories: HzTemplateCategory[] = [
   },
   {
     id: 'bigdata',
-    label: '大数据监控',
+    label: 'Big data monitoring',
     items: [
       {
         id: 'flink_on_yarn',
         label: 'Flink on Yarn',
-        description: 'Job、TaskManager、Checkpoint 和队列资源',
+        description: 'Jobs, TaskManagers, checkpoints, and queue resources',
         meta: 'REST',
         icon: <Activity size={13} />
       }
@@ -279,12 +279,12 @@ const monitorTypeCategories: HzTemplateCategory[] = [
   },
   {
     id: 'network',
-    label: '网络监控',
+    label: 'Network monitoring',
     items: [
       {
         id: 'snmp',
         label: 'SNMP Device',
-        description: '接口、流量、错误包和设备状态',
+        description: 'Interfaces, traffic, error packets, and device state',
         meta: 'SNMP',
         icon: <Network size={13} />
       }
@@ -587,10 +587,10 @@ const topologyG6NodeTypeLegendItems = HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(ic
 }));
 
 const monitorRows = [
-  { name: 'mysql-prod-01', app: 'MySQL', collector: 'collector-a', signal: 'metrics', status: '可用', latency: '38 ms', tone: 'success' as const },
-  { name: 'linux-edge-03', app: 'Linux', collector: 'collector-b', signal: 'metrics/logs', status: '采集中', latency: '72 ms', tone: 'info' as const },
-  { name: 'flink-yarn-main', app: 'Flink on Yarn', collector: 'collector-a', signal: 'metrics', status: '告警中', latency: '118 ms', tone: 'warning' as const },
-  { name: 'snmp-core-sw-02', app: 'SNMP Device', collector: 'collector-c', signal: 'metrics', status: '可用', latency: '45 ms', tone: 'success' as const }
+  { name: 'mysql-prod-01', app: 'MySQL', collector: 'collector-a', signal: 'metrics', status: 'Available', latency: '38 ms', tone: 'success' as const },
+  { name: 'linux-edge-03', app: 'Linux', collector: 'collector-b', signal: 'metrics/logs', status: 'Collecting', latency: '72 ms', tone: 'info' as const },
+  { name: 'flink-yarn-main', app: 'Flink on Yarn', collector: 'collector-a', signal: 'metrics', status: 'Alerting', latency: '118 ms', tone: 'warning' as const },
+  { name: 'snmp-core-sw-02', app: 'SNMP Device', collector: 'collector-c', signal: 'metrics', status: 'Available', latency: '45 ms', tone: 'success' as const }
 ];
 
 const filterGroups: HzFilterGroup[] = [
@@ -695,13 +695,13 @@ const savedFilterViewPresets: Array<{
   {
     id: 'baseline',
     label: 'Baseline',
-    description: '资源类型 + 非宕机基线',
+    description: 'Resource type plus non-down baseline',
     clauses: initialFilterClauses
   },
   {
     id: 'collector-latency',
     label: 'Collector latency',
-    description: '聚焦 collector-a 延迟',
+    description: 'Focus collector-a latency',
     clauses: [
       { id: 'collector-IN-collector-a', field: 'collector', operator: 'IN', value: 'collector-a' },
       { id: 'latency-gt-80ms', field: 'latency', operator: '>', value: '80ms' }
@@ -710,7 +710,7 @@ const savedFilterViewPresets: Array<{
   {
     id: 'open-alerts',
     label: 'Open alerts',
-    description: '只看告警与采集中资源',
+    description: 'Show only alerting and collecting resources',
     clauses: [{ id: 'status-IN-warning-collecting', field: 'status', operator: 'IN', value: 'warning, collecting' }]
   }
 ];
@@ -883,9 +883,9 @@ const monitorFilterTypeOptions = [
 
 const monitorFilterStatusOptions = [
   { value: '', label: 'All status' },
-  { value: '1', label: '正常' },
-  { value: '2', label: '宕机' },
-  { value: '0', label: '暂停' }
+  { value: '1', label: 'Normal' },
+  { value: '2', label: 'Down' },
+  { value: '0', label: 'Paused' }
 ];
 
 const initialResultColumns = [
@@ -1574,7 +1574,7 @@ function createFieldClause(field: string, value: string, operator: 'IN' | 'NOT_I
 
 const defaultYaml = `app: mysql
 name:
-  zh-CN: MySQL数据库
+  zh-CN: MySQL Database
   en-US: MySQL Database
 params:
   - field: host
@@ -1638,7 +1638,7 @@ export default function HertzBeatUiLabPage() {
   const [hiddenSignalSegmentIds, setHiddenSignalSegmentIds] = React.useState<string[]>([]);
   const [hiddenLatencySeriesIds, setHiddenLatencySeriesIds] = React.useState<string[]>([]);
   const [chartWindowSource, setChartWindowSource] = React.useState('signal-volume');
-  const [contextMessage, setContextMessage] = React.useState('上下文窗口: collector-a · 最近 15 分钟 · metrics/logs');
+  const [contextMessage, setContextMessage] = React.useState('Context window: collector-a · last 15 minutes · metrics/logs');
   const [topologyCompanionCollapsedSections, setTopologyCompanionCollapsedSections] = React.useState<Record<string, boolean>>({
     timeline: true
   });
@@ -1925,7 +1925,7 @@ export default function HertzBeatUiLabPage() {
     <HzExplorerFrame
       eyebrow="@hertzbeat/ui explorer"
       title="HertzBeat UI Lab"
-      description="统一查看监控资源、模板目录、采集状态与 YAML 定义。"
+      description="Review monitor resources, template catalog, collection state, and YAML definitions in one place."
       mainId="hz-ui-lab-main"
       mainLabel="HertzBeat UI Lab workbench"
       filterRailLabel="UI lab filters"
@@ -1948,11 +1948,11 @@ export default function HertzBeatUiLabPage() {
             data-monitor-app-picker-trigger-owner="hertzbeat-ui-button"
           >
             <Plus size={14} />
-            新增监控
+            New monitor
           </HzButton>
           <HzButton intent="primary">
             <CheckCircle2 size={14} />
-            UI 基线
+            UI baseline
           </HzButton>
         </>
       }
@@ -4965,12 +4965,12 @@ export default function HertzBeatUiLabPage() {
             onBucketSelect={bucket => {
               setSelectedSignalBucketId(bucket.id);
               setChartWindowSource('signal-volume');
-              setContextMessage(`图表窗口: ${bucket.id} · signal volume`);
+              setContextMessage(`Chart window: ${bucket.id} · signal volume`);
             }}
             onLegendToggle={segment => {
               const hidden = hiddenSignalSegmentIds.includes(segment.id);
               setHiddenSignalSegmentIds(previous => (hidden ? previous.filter(id => id !== segment.id) : [...previous, segment.id]));
-              setContextMessage(`图例: ${segment.id} ${hidden ? 'shown' : 'hidden'} · signal volume`);
+              setContextMessage(`Legend: ${segment.id} ${hidden ? 'shown' : 'hidden'} · signal volume`);
             }}
             className="border-x-0 border-t-0"
           />
@@ -4997,15 +4997,15 @@ export default function HertzBeatUiLabPage() {
               selectedPointId={selectedLatencyPointId}
               hiddenSeriesIds={hiddenLatencySeriesIds}
               onPointSelect={(point, series) => {
-                setSelectedLatencyPointId(`${series.id}:${point.label}`);
-                setSelectedSignalBucketId(point.label);
-                setChartWindowSource('collector-latency');
-                setContextMessage(`图表窗口: ${series.id} ${point.label} · ${point.value}ms`);
+              setSelectedLatencyPointId(`${series.id}:${point.label}`);
+              setSelectedSignalBucketId(point.label);
+              setChartWindowSource('collector-latency');
+                setContextMessage(`Chart window: ${series.id} ${point.label} · ${point.value}ms`);
               }}
               onLegendToggle={series => {
                 const hidden = hiddenLatencySeriesIds.includes(series.id);
                 setHiddenLatencySeriesIds(previous => (hidden ? previous.filter(id => id !== series.id) : [...previous, series.id]));
-                setContextMessage(`图例: ${series.id} ${hidden ? 'shown' : 'hidden'} · collector latency`);
+                setContextMessage(`Legend: ${series.id} ${hidden ? 'shown' : 'hidden'} · collector latency`);
               }}
               height={128}
               className="border-x-0 border-y-0 2xl:border-r 2xl:border-[var(--hz-ui-line-soft)]"
@@ -6439,7 +6439,7 @@ export default function HertzBeatUiLabPage() {
                 onTraceSelect={trace => {
                   setSelectedTraceId(trace.id);
                   setTraceDetailOpen(true);
-                  setContextMessage(`上下文窗口: ${trace.id} · ${trace.durationMs}ms · trace detail`);
+                  setContextMessage(`Context window: ${trace.id} · ${trace.durationMs}ms · trace detail`);
                 }}
                 className="border-x-0 border-t border-[var(--hz-ui-line-soft)]"
               />
@@ -7628,7 +7628,7 @@ export default function HertzBeatUiLabPage() {
                 criticalPathSpanIds={['api', 'collector']}
                 onSpanSelect={span => {
                   setSelectedTraceSpanId(span.id);
-                  setContextMessage(`上下文窗口: trace span ${span.id} · ${span.durationMs}ms · logs/traces`);
+                  setContextMessage(`Context window: trace span ${span.id} · ${span.durationMs}ms · logs/traces`);
                 }}
                 className="border-x-0 border-b-0 border-t border-[var(--hz-ui-line-soft)]"
               />
@@ -7755,7 +7755,7 @@ export default function HertzBeatUiLabPage() {
           <div className="flex min-h-10 items-center justify-between gap-2 border-b border-[var(--hz-ui-line-soft)] px-3 py-2">
             <div className="min-w-0">
               <div className="text-[13px] font-semibold text-[#f3f6fb]">Monitor results</div>
-              <div className="text-[11px] text-[#727b8c]">最近 15 分钟 · 按采集延迟降序</div>
+              <div className="text-[11px] text-[#727b8c]">Last 15 minutes · sorted by collection latency</div>
             </div>
             <div className="flex shrink-0 items-center gap-2">
               <span className="font-mono text-[11px] text-[#8f99ab]">list view</span>
@@ -7777,7 +7777,7 @@ export default function HertzBeatUiLabPage() {
             selectedRowKey={selectedMonitor.name}
             onRowClick={row => {
               setSelectedMonitorName(row.name);
-              setContextMessage(`上下文窗口: ${row.name} · ${row.collector} · 最近 15 分钟`);
+              setContextMessage(`Context window: ${row.name} · ${row.collector} · last 15 minutes`);
               setInspectorOpen(true);
             }}
           />
@@ -7803,7 +7803,7 @@ export default function HertzBeatUiLabPage() {
                 id: 'enable',
                 label: 'Enable',
                 presentation: 'menu',
-                onSelect: () => setContextMessage('批量操作: enable only paused selected monitors'),
+                onSelect: () => setContextMessage('Batch action: enable only paused selected monitors'),
                 buttonProps: {
                   'data-monitor-batch-response-filter': 'angular-status-filtered-selection',
                   'data-monitor-batch-response-eligible-status': 'paused',
@@ -7815,7 +7815,7 @@ export default function HertzBeatUiLabPage() {
                 id: 'pause',
                 label: 'Pause',
                 presentation: 'menu',
-                onSelect: () => setContextMessage('批量操作: pause only active selected monitors'),
+                onSelect: () => setContextMessage('Batch action: pause only active selected monitors'),
                 buttonProps: {
                   'data-monitor-batch-response-filter': 'angular-status-filtered-selection',
                   'data-monitor-batch-response-eligible-status': 'active',
@@ -7851,7 +7851,7 @@ export default function HertzBeatUiLabPage() {
                 id: 'import',
                 label: 'Import',
                 presentation: 'menu',
-                onSelect: () => setContextMessage('批量操作: import monitors'),
+                onSelect: () => setContextMessage('Batch action: import monitors'),
                 buttonProps: {
                   'data-monitor-batch-more-menu-action': 'import'
                 }
@@ -7861,7 +7861,7 @@ export default function HertzBeatUiLabPage() {
                 label: 'Delete',
                 tone: 'critical',
                 presentation: 'menu',
-                onSelect: () => setContextMessage('批量操作: delete requires confirmation'),
+                onSelect: () => setContextMessage('Batch action: delete requires confirmation'),
                 buttonProps: {
                   'data-monitor-batch-more-menu-action': 'delete'
                 }
@@ -7894,7 +7894,7 @@ export default function HertzBeatUiLabPage() {
             data-monitor-unavailable-refresh-feedback-owner="hertzbeat-ui-inline-feedback"
           />
           <SearchRow
-            value="数据库"
+            value="Database"
             placeholder="Search threshold rules"
             searchLabel="Search"
             clearLabel="Clear"
@@ -10566,7 +10566,7 @@ export default function HertzBeatUiLabPage() {
             <AlertNoticeRuleSwitch
               row="filter-all"
               checked={noticeRuleFilterAllDemo}
-              label="转发所有"
+              label="Forward all"
               testId="ui-lab-alert-notice-rule-filter-all"
               onCheckedChange={setNoticeRuleFilterAllDemo}
             />
@@ -10798,7 +10798,7 @@ export default function HertzBeatUiLabPage() {
             field="collector"
             selectedValue={contextMessage}
             values={collectorFieldStats}
-            onShowContext={() => setContextMessage('上下文窗口: collector-a · logs/traces · 15m before/after')}
+            onShowContext={() => setContextMessage('Context window: collector-a · logs/traces · 15m before/after')}
             onDrilldown={() => addFilterClause(createFieldClause('collector', 'collector-a', 'IN'))}
           />
           <div className="grid min-w-0 border-t border-[var(--hz-ui-line-faint)] 2xl:grid-cols-[minmax(0,1fr)_340px]">
@@ -10806,7 +10806,7 @@ export default function HertzBeatUiLabPage() {
               title="Query history"
               items={queryHistoryItems}
               onRestore={item => setQuery(stringifyFilterValue(item.query))}
-              onCompare={item => setContextMessage(`对比窗口: ${item.id} · ${stringifyFilterValue(item.query)}`)}
+              onCompare={item => setContextMessage(`Compare window: ${item.id} · ${stringifyFilterValue(item.query)}`)}
               actions={<HzButton size="sm" intent="ghost">Pin run</HzButton>}
               className="border-x-0 border-y-0 2xl:border-r 2xl:border-[var(--hz-ui-line-soft)]"
             />
@@ -10841,7 +10841,7 @@ export default function HertzBeatUiLabPage() {
               query="collector-a"
               placeholder="Search commands, pages, entities"
               items={commandPaletteItems}
-              onSelect={item => setContextMessage(`命令准备: ${item.id}`)}
+              onSelect={item => setContextMessage(`Command prepared: ${item.id}`)}
               actions={<HzButton size="sm" intent="ghost">Cmd K</HzButton>}
               className="border-x-0 border-y-0 2xl:border-r 2xl:border-[var(--hz-ui-line-soft)]"
             />
@@ -10849,7 +10849,7 @@ export default function HertzBeatUiLabPage() {
               title="Context handoff"
               context="mysql-prod-01 · collector-a · last 15m"
               targets={contextHandoffTargets}
-              onOpen={target => setContextMessage(`上下文跳转: ${target.id}`)}
+              onOpen={target => setContextMessage(`Context handoff: ${target.id}`)}
               actions={<HzButton size="sm" intent="ghost">Copy context</HzButton>}
               className="border-x-0 border-b-0 border-t border-[var(--hz-ui-line-soft)] 2xl:border-t-0"
             />
@@ -11247,8 +11247,8 @@ export default function HertzBeatUiLabPage() {
 
       <HzTypePickerDialog
         open={typePickerOpen}
-        title="新增监控"
-        description="选择资源类型后再进入对应表单,不让默认 app=mysql 偷偷决定用户流程。"
+        title="New monitor"
+        description="Choose the resource type before opening the matching form, so the default app=mysql never decides the flow for the operator."
         categories={monitorTypeCategories}
         selectedId={selectedType}
         search={typeSearch}
diff --git a/web-next/components/log-manage/log-dialog.chrome.test.ts b/web-next/components/log-manage/log-dialog.chrome.test.ts
index 5d333e2046..bf2cca4da6 100644
--- a/web-next/components/log-manage/log-dialog.chrome.test.ts
+++ b/web-next/components/log-manage/log-dialog.chrome.test.ts
@@ -28,27 +28,27 @@ describe('log dialog shared chrome', () => {
     expect(workbenchPageSource).toContain('divide-[var(--ops-border-color)]');
     expect(workbenchPageSource).toContain('text-[var(--ops-text-primary)]');
     expect(workbenchPageSource).toContain('text-[var(--ops-text-secondary)]');
-    expect(streamDialogSource).toContain('border-[var(--ops-border-color)]');
-    expect(streamDialogSource).toContain('text-[var(--ops-text-primary)]');
-    expect(streamDialogSource).toContain('text-[var(--ops-text-secondary)]');
+    expect(streamDialogSource).toContain('HzDialogBodyLayout');
+    expect(streamDialogSource).toContain('HzDetailRows');
+    expect(streamDialogSource).toContain('HzAttributeDiagnostics');
     expect(streamDialogSource).toContain("t('log.manage.stream.detail.attribution-title')");
     expect(streamDialogSource).toContain("t('log.manage.stream.detail.attribution-present')");
     expect(streamDialogSource).toContain("t('log.manage.stream.detail.attribution-missing')");
     expect(streamDialogSource).toContain('data-log-stream-detail-trace-id');
     expect(streamDialogSource).toContain('data-log-stream-detail-selection');
-    expect(streamDialogSource).not.toContain('归因诊断');
-    expect(streamDialogSource).not.toContain('已提供');
-    expect(streamDialogSource).not.toContain('缺失');
-    expect(relatedTraceDialogSource).toContain('border-[var(--ops-border-color)]');
-    expect(relatedTraceDialogSource).toContain('text-[var(--ops-text-primary)]');
-    expect(relatedTraceDialogSource).toContain('text-[var(--ops-text-tertiary)]');
+    expect(streamDialogSource).not.toMatch(/\u5f52\u56e0\u8bca\u65ad/);
+    expect(streamDialogSource).not.toMatch(/\u5df2\u63d0\u4f9b/);
+    expect(streamDialogSource).not.toMatch(/\u7f3a\u5931/);
+    expect(relatedTraceDialogSource).toContain('HzDialogBodyLayout');
+    expect(relatedTraceDialogSource).toContain('HzStateNotice');
+    expect(relatedTraceDialogSource).toContain('HzStatStrip');
     expect(relatedTraceDialogSource).toContain("t('log.manage.related-trace.kicker')");
     expect(relatedTraceDialogSource).toContain("t('log.manage.related-trace.event-detail.title')");
     expect(relatedTraceDialogSource).toContain("t('log.manage.related-trace.event-detail.action.span')");
-    expect(relatedTraceDialogSource).not.toContain('相关链路');
-    expect(relatedTraceDialogSource).not.toContain('跨度事件');
-    expect(relatedTraceDialogSource).not.toContain('事件详情');
-    expect(relatedTraceDialogSource).not.toContain('查看跨度');
+    expect(relatedTraceDialogSource).not.toMatch(/\u76f8\u5173\u94fe\u8def/);
+    expect(relatedTraceDialogSource).not.toMatch(/\u8de8\u5ea6\u4e8b\u4ef6/);
+    expect(relatedTraceDialogSource).not.toMatch(/\u4e8b\u4ef6\u8be6\u60c5/);
+    expect(relatedTraceDialogSource).not.toMatch(/\u67e5\u770b\u8de8\u5ea6/);
     expect(logManagePageSource).toContain('data-log-manage-stream-trace-id');
     expect(logManagePageSource).toContain('data-log-manage-stream-selected');
   });
diff --git a/web-next/components/log-manage/log-related-trace-dialog.test.tsx b/web-next/components/log-manage/log-related-trace-dialog.test.tsx
index eaa25f1143..a720a73ff9 100644
--- a/web-next/components/log-manage/log-related-trace-dialog.test.tsx
+++ b/web-next/components/log-manage/log-related-trace-dialog.test.tsx
@@ -12,6 +12,7 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 
 describe('log related trace dialog', () => {
   it('renders the preview toolbar, stage facts, and full-trace follow-up action', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const html = renderToStaticMarkup(
       <LogRelatedTraceDialog
         open={true}
@@ -106,13 +107,13 @@ describe('log related trace dialog', () => {
     expect(html).toContain('data-log-related-trace-event-detail-meta="span-event-label"');
     expect(html).toContain('data-hz-ui="state-notice"');
     expect(html).toContain('data-hz-state-variant="hint"');
-    expect(html).toContain('跨度事件');
-    expect(html).toContain('不是新的跨度,是当前跨度上的时间点');
-    expect(html).toContain('所属跨度');
-    expect(html).toContain('事件位置');
+    expect(html).toContain(t('log.manage.related-trace.event-fact.title'));
+    expect(html).toContain(t('log.manage.related-trace.event-detail.copy'));
+    expect(html).toContain(t('log.manage.related-trace.event-fact.span'));
+    expect(html).toContain(t('log.manage.related-trace.event-fact.position'));
     expect(html).toContain('+72 ms');
     expect(html).toContain('exception.type=TimeoutError');
-    expect(html).toContain('查看跨度');
+    expect(html).toContain(t('log.manage.related-trace.event-detail.action.span'));
     expect(html).toContain('data-waterfall-minimap-event-marker="true"');
     expect(html).toContain('aria-label="exception"');
     expect(html).toContain('data-log-related-trace-stage-meta="true"');
diff --git a/web-next/components/log-manage/log-stream-detail-dialog.test.tsx b/web-next/components/log-manage/log-stream-detail-dialog.test.tsx
index 7aad2d0807..0a43f52821 100644
--- a/web-next/components/log-manage/log-stream-detail-dialog.test.tsx
+++ b/web-next/components/log-manage/log-stream-detail-dialog.test.tsx
@@ -12,6 +12,8 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 
 describe('log stream detail dialog', () => {
   it('renders facts and toolbar actions for the selected stream log', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const missingEntityCopy = 'Missing entity ID; entity detail remains disabled';
     const html = renderToStaticMarkup(
       <LogStreamDetailDialog
         open={true}
@@ -34,14 +36,14 @@ describe('log stream detail dialog', () => {
             label: 'hertzbeat.entity_id',
             value: '-',
             state: 'missing',
-            meta: '缺少实体 ID,实体详情会保持禁用'
+            meta: missingEntityCopy
           },
           {
             key: 'hertzbeat.collector',
             label: 'hertzbeat.collector',
             value: 'collector-local',
             state: 'present',
-            meta: '采集器来源'
+            meta: 'Collector source'
           }
         ]}
         actions={<button type="button">Open Related Trace</button>}
@@ -80,9 +82,9 @@ describe('log stream detail dialog', () => {
     expect(html).toContain('data-log-stream-detail-actions-owner="hertzbeat-ui-action-group"');
     expect(html).toContain('data-log-stream-detail-actions="dialog-actions"');
     expect(html).toContain('data-hz-ui="action-group"');
-    expect(html).toContain('归因诊断');
+    expect(html).toContain(t('log.manage.stream.detail.attribution-title'));
     expect(html).toContain('hertzbeat.entity_id');
-    expect(html).toContain('缺少实体 ID,实体详情会保持禁用');
+    expect(html).toContain(missingEntityCopy);
     expect(html).toContain('hertzbeat.collector');
     expect(html).toContain('collector-local');
     expect(html).toContain('Severity');
diff --git a/web-next/components/monitor-detail/monitor-detail-console.test.tsx b/web-next/components/monitor-detail/monitor-detail-console.test.tsx
index 2c8fd32a12..c08b770285 100644
--- a/web-next/components/monitor-detail/monitor-detail-console.test.tsx
+++ b/web-next/components/monitor-detail/monitor-detail-console.test.tsx
@@ -47,15 +47,15 @@ describe('monitor detail console', () => {
       />
     );
 
-    expect(html).toContain('监控列表');
-    expect(html).toContain('监控详情');
-    expect(html).toContain('监控实时数据详情');
+    expect(html).toContain(zh('monitor.list'));
+    expect(html).toContain(zh('monitor.detail'));
+    expect(html).toContain(zh('monitor.detail.realtime'));
     expect(html).toContain('data-monitor-detail-header-mode="breadcrumb-only"');
     expect(html).toContain('data-monitor-detail-reference-source="apache-hertzbeat-master-monitor-detail"');
     expect(html).toContain('data-monitor-detail-context-mark="breadcrumb"');
     expect(html).toContain('data-monitor-detail-context-mark-owner="hertzbeat-ui-inline-context-mark"');
     expect(html).not.toContain('data-monitor-detail-app-chip="breadcrumb"');
-    expect(html).not.toContain('监控工作台');
+    expect(html).not.toMatch(/\u76d1\u63a7\u5de5\u4f5c\u53f0/);
     expect(html).not.toContain('Monitors');
     expect(html).not.toContain('Monitor detail');
   });
diff --git a/web-next/components/monitor-detail/monitor-detail-sections.test.tsx b/web-next/components/monitor-detail/monitor-detail-sections.test.tsx
index 031713847c..60288c5b36 100644
--- a/web-next/components/monitor-detail/monitor-detail-sections.test.tsx
+++ b/web-next/components/monitor-detail/monitor-detail-sections.test.tsx
@@ -250,7 +250,7 @@ describe('monitor detail sections', () => {
     expect(contextHtml).toBe('');
     expect(contextHtml).not.toContain('Pinned evidence');
     expect(realtimeHtml).not.toContain('Monitor Observation');
-    expect(realtimeHtml).toContain('Monitoring Basic');
+    expect(realtimeHtml).toContain(t('monitor.detail.basic'));
     expect(realtimeHtml).not.toContain('This card shows the base observation details and latest status for monitor instance 10.0.0.1.');
     expect(realtimeHtml).not.toContain('HertzBeat Collection');
     expect(realtimeHtml).toContain('data-hz-ui="monitor-basic-card"');
@@ -384,7 +384,7 @@ describe('monitor detail sections', () => {
     expect(realtimeHtml).not.toContain('data-monitor-detail-signal-selected-style="angular-neutral"');
     expect(realtimeHtml).toContain('data-hz-monitor-metric-table-row="metric-fields"');
     expect(realtimeHtml).toContain('data-monitor-detail-signal-row-title="true"');
-    expect(realtimeHtml).not.toMatch(/>\s*\d+\s+(?:Fields|字段)\s*</);
+    expect(realtimeHtml).not.toMatch(/>\s*\d+\s+(?:Fields|\u5b57\u6bb5)\s*</u);
     expect(realtimeHtml).not.toContain('data-monitor-detail-signal-card-table-row="source"');
     expect(realtimeHtml).not.toContain('Real-time Metric');
     expect(realtimeHtml).not.toContain('This card shows the latest sampled result for summary while keeping the native HertzBeat collection semantics.');
diff --git a/web-next/components/monitor-detail/monitor-history-chart-grid.test.tsx b/web-next/components/monitor-detail/monitor-history-chart-grid.test.tsx
index b550b3a72a..2e32314fca 100644
--- a/web-next/components/monitor-detail/monitor-history-chart-grid.test.tsx
+++ b/web-next/components/monitor-detail/monitor-history-chart-grid.test.tsx
@@ -558,11 +558,11 @@ describe('monitor history chart grid', () => {
       />
     );
 
-    expect(chartHtml).toContain('61 个采样点');
+    expect(chartHtml).toContain(`61 ${zhT('monitor.detail.history-series.search.count')}`);
     expect(chartHtml).not.toContain('61 samples');
     expect(chartHtml).not.toContain('monitor.detail.history-series.search.count');
-    expect(emptyHtml).toContain('历史未就绪');
-    expect(emptyHtml).toContain('历史查询还没有返回可用序列。');
+    expect(emptyHtml).toContain(zhT('monitor.detail.history.blocker.title'));
+    expect(emptyHtml).toContain(zhT('monitor.detail.history.blocker.copy'));
     expect(emptyHtml).not.toContain('History store has not returned series yet.');
   });
 });
diff --git a/web-next/components/monitor-detail/monitor-summary-card.test.tsx b/web-next/components/monitor-detail/monitor-summary-card.test.tsx
index 939aba9614..7ed4a94dbf 100644
--- a/web-next/components/monitor-detail/monitor-summary-card.test.tsx
+++ b/web-next/components/monitor-detail/monitor-summary-card.test.tsx
@@ -33,7 +33,7 @@ describe('monitor summary card', () => {
 
     expect(html).toContain('ID');
     expect(html).toContain('checkout-core');
-    expect(html).toContain('正常');
+    expect(html).toContain(t('monitor.status.up'));
     expect(html).not.toContain('>UP<');
     expect(html).toContain('60s');
     expect(html).toContain('Core checkout monitor');
diff --git a/web-next/components/observability/page-header.test.tsx b/web-next/components/observability/page-header.test.tsx
index 0cffe27deb..ae3901de59 100644
--- a/web-next/components/observability/page-header.test.tsx
+++ b/web-next/components/observability/page-header.test.tsx
@@ -29,14 +29,14 @@ describe('observability page header', () => {
     const html = renderToStaticMarkup(
       <ObservabilityPageHeader
         tone="operator"
-        kicker="告警中心"
-        title="集中查看并处理当前告警"
-        subtitle="按时间、状态和对象筛选告警。"
+        kicker="Alert center"
+        title="Review and handle active alerts"
+        subtitle="Filter alerts by time, status, and entity."
         facts={[
-          { label: '全部告警组', value: '0' },
-          { label: '告警中', value: '0' },
-          { label: '已确认', value: '0' },
-          { label: '已恢复', value: '0' }
+          { label: 'All alert groups', value: '0' },
+          { label: 'Firing', value: '0' },
+          { label: 'Acknowledged', value: '0' },
+          { label: 'Recovered', value: '0' }
         ]}
         factsVariant="pills"
       />
@@ -44,8 +44,8 @@ describe('observability page header', () => {
 
     expect(html).toContain('data-observability-stat-grid="pills"');
     expect(html).toContain('rounded-[8px]');
-    expect(html).toContain('全部告警组');
-    expect(html).toContain('告警中');
+    expect(html).toContain('All alert groups');
+    expect(html).toContain('Firing');
     expect(html).not.toContain('divide-x');
     expect(html).not.toContain('border-y');
   });
@@ -55,14 +55,14 @@ describe('observability page header', () => {
       <ObservabilityPageHeader
         tone="operator"
         kickerVariant="plain"
-        kicker="对象优先调查"
-        title="导入实体定义"
-        subtitle="支持一次导入多个定义。"
+        kicker="Entity-first investigation"
+        title="Import entity definitions"
+        subtitle="Import multiple definitions at once."
       />
     );
 
     expect(html).toContain('data-observability-kicker="plain"');
-    expect(html).toContain('对象优先调查');
+    expect(html).toContain('Entity-first investigation');
     expect(html).toContain('border-b border-[var(--ops-border-color)]');
     expect(html).not.toContain('data-badge');
   });
diff --git a/web-next/components/observability/time-range-control.test.tsx b/web-next/components/observability/time-range-control.test.tsx
index 488e9124aa..cfb98b0879 100644
--- a/web-next/components/observability/time-range-control.test.tsx
+++ b/web-next/components/observability/time-range-control.test.tsx
@@ -91,9 +91,9 @@ describe('TimeRangeControl', () => {
     expect(html).toContain('Last 30 minutes');
     expect(html).toContain('Last 1 hour');
     expect(html).toContain('Manual');
-    expect(html).not.toContain('近 30 分钟');
-    expect(html).not.toContain('相对');
-    expect(html).not.toContain('刷新');
+    expect(html).not.toMatch(/\u8fd1 30 \u5206\u949f/);
+    expect(html).not.toMatch(/\u76f8\u5bf9/);
+    expect(html).not.toMatch(/\u5237\u65b0/);
   });
 
   it('localizes provided preset values when a route passes custom preset options', () => {
@@ -125,8 +125,8 @@ describe('TimeRangeControl', () => {
         value={{ timeRange: 'last-1h', refresh: '', live: 'false', tz: '' }}
         labels={buildTimeRangeControlLabels(t)}
         presets={[
-          { value: 'last-30m', label: '近 30 分钟' },
-          { value: 'last-1h', label: '近 1 小时' }
+          { value: 'last-30m', label: '\u8fd1 30 \u5206\u949f' },
+          { value: 'last-1h', label: '\u8fd1 1 \u5c0f\u65f6' }
         ]}
         onApply={() => undefined}
       />
@@ -134,8 +134,8 @@ describe('TimeRangeControl', () => {
 
     expect(html).toContain('Last 30 minutes');
     expect(html).toContain('Last 1 hour');
-    expect(html).not.toContain('近 30 分钟');
-    expect(html).not.toContain('近 1 小时');
+    expect(html).not.toMatch(/\u8fd1 30 \u5206\u949f/);
+    expect(html).not.toMatch(/\u8fd1 1 \u5c0f\u65f6/);
   });
 
   it('renders the shared cold operator toolbar without becoming another card layer', () => {
@@ -319,7 +319,7 @@ describe('TimeRangeControl', () => {
     const html = renderToStaticMarkup(
       <TimeRangeControl
         value={{ timeRange: 'last-30m', refresh: '', live: 'false', tz: 'Asia/Shanghai' }}
-        labels={{ manualRefresh: '手动' }}
+        labels={{ manualRefresh: '\u624b\u52a8' }}
         showAbsoluteFields
         variant="narrow-rail"
         onApply={() => undefined}
@@ -331,7 +331,7 @@ describe('TimeRangeControl', () => {
     expect(source).toContain("const refreshSelectWidth = isNarrowRail ? 'w-[64px]' : 'w-[64px]'");
     expect(html).toContain('data-time-range-refresh-field-fit="full-manual-label"');
     expect(html).toContain('data-time-range-refresh-select="true"');
-    expect(html).toContain('手动');
+    expect(html).toContain('\u624b\u52a8');
     expect(html).toContain('w-[96px]');
     expect(html).toContain('w-[64px]');
     expect(html).not.toContain("const refreshFrameClass = isNarrowRail ? 'w-[82px]'");
@@ -343,7 +343,7 @@ describe('TimeRangeControl', () => {
     const html = renderToStaticMarkup(
       <TimeRangeControl
         value={{ timeRange: 'last-30m', refresh: '', live: 'false', tz: 'Asia/Shanghai' }}
-        labels={{ apply: '应用', applyAria: '应用时间范围' }}
+        labels={{ apply: '\u5e94\u7528', applyAria: '\u5e94\u7528\u65f6\u95f4\u8303\u56f4' }}
         showAbsoluteFields
         variant="narrow-rail"
         onApply={() => undefined}
diff --git a/web-next/components/observability/waterfall.test.tsx b/web-next/components/observability/waterfall.test.tsx
index b1220f999b..e77713bc92 100644
--- a/web-next/components/observability/waterfall.test.tsx
+++ b/web-next/components/observability/waterfall.test.tsx
@@ -197,17 +197,17 @@ describe('observability waterfall', () => {
     expect(html).toContain('width:32px');
   });
 
-  it('uses Chinese count labels for the trace workbench waterfall timeline and row events', () => {
+  it('uses localized count labels for the trace workbench waterfall timeline and row events', () => {
     const html = renderToStaticMarkup(
       <ObservabilityWaterfall
-        spanLabel="跨度"
-        durationLabel="耗时"
-        timelineLabel="时间轴"
+        spanLabel="\u8de8\u5ea6"
+        durationLabel="\u8017\u65f6"
+        timelineLabel="\u65f6\u95f4\u8f74"
         waterfallCopy={{
-          overviewTitle: '时间轴总览',
-          spanCount: count => `${count} 个跨度`,
-          eventCount: count => `${count} 个事件`,
-          rowEventCount: count => `${count} 个事件`
+          overviewTitle: '\u65f6\u95f4\u8f74\u603b\u89c8',
+          spanCount: count => `${count} \u4e2a\u8de8\u5ea6`,
+          eventCount: count => `${count} \u4e2a\u4e8b\u4ef6`,
+          rowEventCount: count => `${count} \u4e2a\u4e8b\u4ef6`
         }}
         rows={[
           {
@@ -228,9 +228,9 @@ describe('observability waterfall', () => {
       />
     );
 
-    expect(html).toContain('时间轴总览');
-    expect(html).toContain('1 个跨度 · 2 个事件');
-    expect(html).toContain('2 个事件');
+    expect(html).toContain('\u65f6\u95f4\u8f74\u603b\u89c8');
+    expect(html).toContain('1 \u4e2a\u8de8\u5ea6 · 2 \u4e2a\u4e8b\u4ef6');
+    expect(html).toContain('2 \u4e2a\u4e8b\u4ef6');
     expect(html).not.toContain('1 spans');
     expect(html).not.toContain('2 events');
   });
diff --git a/web-next/components/pages/alert-authoring-primitives.test.tsx b/web-next/components/pages/alert-authoring-primitives.test.tsx
index a592d8c08d..782657bde5 100644
--- a/web-next/components/pages/alert-authoring-primitives.test.tsx
+++ b/web-next/components/pages/alert-authoring-primitives.test.tsx
@@ -16,10 +16,10 @@ describe('alert authoring primitives', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-authoring-primitives.tsx'), 'utf8');
     const html = renderToStaticMarkup(
       <div>
-        <AlertAuthoringPanel heading="分组标签">panel-body</AlertAuthoringPanel>
-        <AlertAuthoringCallout title="提示" copy="短说明" warning="检查配置" />
+        <AlertAuthoringPanel heading="Group labels">panel-body</AlertAuthoringPanel>
+        <AlertAuthoringCallout title="Hint" copy="Short note" warning="Review config" />
         <AlertAuthoringToggleRow>inline-toggle</AlertAuthoringToggleRow>
-        <AlertAuthoringActionPill>操作</AlertAuthoringActionPill>
+        <AlertAuthoringActionPill>Action</AlertAuthoringActionPill>
         <AlertAuthoringValuePill>service:checkout</AlertAuthoringValuePill>
       </div>
     );
diff --git a/web-next/components/pages/alert-center-surface.test.tsx b/web-next/components/pages/alert-center-surface.test.tsx
index dddde328bb..9e68209b70 100644
--- a/web-next/components/pages/alert-center-surface.test.tsx
+++ b/web-next/components/pages/alert-center-surface.test.tsx
@@ -165,16 +165,16 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-alert-center-query-toolbar="single-query-form"');
     expect(html).toContain('data-alert-center-list-shell="cold-alert-list"');
     expect(html).not.toContain('data-alert-center-summary-rail=');
-    expect(html).toContain('告警中心');
-    expect(html).toContain('集中查看并处理当前告警');
-    expect(html).toContain('按时间、状态和对象筛选告警,并在这里完成确认、恢复或继续查看详情。');
-    expect(html).toContain('告警中');
-    expect(html).toContain('已确认');
-    expect(html).toContain('已恢复');
-    expect(html).toContain('搜索告警');
-    expect(html).toContain('告警状态');
-    expect(html).toContain('严重级别');
-    expect((html.match(/>刷新</g) ?? []).length).toBe(1);
+    expect(html).toContain(zh('alert.workbench.kicker'));
+    expect(html).toContain(zh('alert.workbench.title'));
+    expect(html).toContain(zh('alert.workbench.copy'));
+    expect(html).toContain(zh('alert.workbench.firing'));
+    expect(html).toContain(zh('alert.workbench.acknowledged'));
+    expect(html).toContain(zh('alert.workbench.resolved'));
+    expect(html).toContain(zh('alert.center.search'));
+    expect(html).toContain(zh('alert.center.filter-status'));
+    expect(html).toContain(zh('entity.response.context.severity'));
+    expect((html.match(new RegExp(`>${zh('alert.workbench.action.refresh')}<`, 'g')) ?? []).length).toBe(1);
     expect(html).not.toContain('alert.center.kicker');
     expect(html).not.toContain('alert.center.title');
     expect(html).not.toContain('Alert Center');
@@ -198,10 +198,10 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-alert-center-list-shell="cold-alert-list"');
     expect(html).toContain('data-alert-center-empty-state="cold-table-empty"');
     expect(html).toContain('data-alert-center-empty-icon="cold-empty-box"');
-    expect(html).toContain('当前范围内没有告警');
-    expect(html).toContain('当前时间范围和筛选条件下还没有可处理的告警。');
+    expect(html).toContain(zh('alert.workbench.empty.title'));
+    expect(html).toContain(zh('alert.workbench.empty.copy'));
     expect(html).not.toContain('data-alert-center-empty-action="refresh"');
-    expect(html).not.toContain('点击刷新');
+    expect(html).not.toContain('alert.center.empty.action');
     expect(html).not.toContain('data-alert-center-empty-state="angular-inline"');
     expect(html).not.toContain('alert.center.empty.title');
     expect(html).not.toContain('alert.center.empty.copy');
@@ -269,9 +269,9 @@ describe('AlertCenterSurface', () => {
       html.indexOf('data-cold-search-action="submit"')
     );
     expect(html).not.toContain('data-alert-center-refresh-slot="right"');
-    expect(html).not.toContain('<span>搜索告警</span>');
-    expect(html).not.toContain('<span>告警状态</span>');
-    expect(html).not.toContain('<span>严重级别</span>');
+    expect(html).not.toContain(`<span>${zh('alert.center.search')}</span>`);
+    expect(html).not.toContain(`<span>${zh('alert.center.filter-status')}</span>`);
+    expect(html).not.toContain(`<span>${zh('entity.response.context.severity')}</span>`);
     expect(source).toContain('rounded-[4px]');
     expect(source).toContain('rounded-[3px]');
     expect(source).toContain("from '../ui/search-row'");
@@ -303,9 +303,9 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-hz-pagination-page-jump="number-input"');
     expect(html).toContain('data-alert-center-pagination-page-size-owner="hertzbeat-ui-select"');
     expect(html).toContain('data-alert-center-pagination-page-jump-owner="hertzbeat-ui-input"');
-    expect(html).toContain('第 2 / 4 页 · 9-9 / 31');
-    expect(html).toContain('每页条数');
-    expect(html).toContain('页码');
+    expect(html).toContain(zh('alert.center.pagination.summary', { page: 2, totalPages: 4, from: 9, to: 9, total: 31 }));
+    expect(html).toContain(zh('alert.center.pagination.page-size'));
+    expect(html).toContain(zh('alert.center.pagination.page'));
     expect(source).toContain('HzPaginationBar');
     expect(source).toContain('data-alert-center-pagination-owner="hertzbeat-ui-pagination-bar"');
     expect(source).toContain('onPageIndexChange?.(Math.max(currentPageIndex - 1, 0))');
@@ -454,12 +454,12 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-alert-center-batch-action="inhibit-selected"');
     expect(html).toContain('data-alert-center-batch-dialog-source="selected-groups"');
     expect(html).toContain('data-hz-batch-selection-count="2"');
-    expect(html).toContain('条告警组已选择');
-    expect(html).toContain('确认 1 条');
-    expect(html).toContain('取消确认 1 条');
-    expect(html).toContain('恢复 1 条');
-    expect(html).toContain('创建静默 2 条');
-    expect(html).toContain('创建抑制 2 条');
+    expect(html).toContain(zh('alert.center.batch.selection-label'));
+    expect(html).toContain(zh('alert.center.batch.acknowledge-selected', { count: 1 }));
+    expect(html).toContain(zh('alert.center.batch.unacknowledge-selected', { count: 1 }));
+    expect(html).toContain(zh('alert.center.batch.resolve-selected', { count: 1 }));
+    expect(html).toContain(zh('alert.center.batch.silence-selected', { count: 2 }));
+    expect(html).toContain(zh('alert.center.batch.inhibit-selected', { count: 2 }));
     expect(source).toContain('HzBatchToolbar');
     expect(source).toContain('HzCheckbox');
     expect(source).toContain('SELECTED_BATCH_DIALOG_GROUP_KEY');
@@ -512,13 +512,13 @@ describe('AlertCenterSurface', () => {
     expect(silenceHtml).toContain('data-alert-rule-dialog="silence"');
     expect(silenceHtml).toContain('data-alert-rule-dialog-submit="silence"');
     expect(silenceHtml).toContain('data-alert-rule-dialog-submit-owner="alert-center-quick-dialog"');
-    expect(silenceHtml).toContain('将基于 2 条告警的共用标签预填静默规则');
+    expect(silenceHtml).toContain(zh('entity.alert.workbench.silence.selection', { count: 2 }));
     expect(silenceHtml).toContain('service:checkout');
     expect(silenceHtml).toContain('name="silence_labels" value="service:checkout"');
     expect(inhibitHtml).toContain('data-alert-rule-dialog="inhibit"');
     expect(inhibitHtml).toContain('data-alert-rule-dialog-submit="inhibit"');
     expect(inhibitHtml).toContain('data-alert-rule-dialog-submit-owner="alert-center-quick-dialog"');
-    expect(inhibitHtml).toContain('将基于 2 条告警的共用标签预填抑制规则');
+    expect(inhibitHtml).toContain(zh('entity.alert.workbench.inhibit.selection', { count: 2 }));
     expect(inhibitHtml).toContain('service:checkout');
     expect(inhibitHtml).toContain('name="inhibit_source_labels" value="service:checkout"');
     expect(inhibitHtml).toContain('name="inhibit_target_labels" value="service:checkout"');
@@ -608,7 +608,7 @@ describe('AlertCenterSurface', () => {
     expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm="angular-status-confirm"]')).not.toBeNull();
     expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-action="acknowledge"]')).not.toBeNull();
     expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-count="1"]')).not.toBeNull();
-    expect(interactionContainer.textContent).toContain('确认选中的告警?');
+    expect(interactionContainer.textContent).toContain(zh('entity.alert.workbench.confirm.acknowledge-selected'));
 
     await act(async () => {
       (interactionContainer.querySelector(
@@ -631,7 +631,7 @@ describe('AlertCenterSurface', () => {
       resolve?.click();
     });
     expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-action="resolve"]')).not.toBeNull();
-    expect(interactionContainer.textContent).toContain('请确认是否批量标记已处理!');
+    expect(interactionContainer.textContent).toContain(zh('alert.center.confirm.mark-done-batch'));
 
     await act(async () => {
       (interactionContainer.querySelector(
@@ -643,7 +643,7 @@ describe('AlertCenterSurface', () => {
       reopen?.click();
     });
     expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-action="reopen"]')).not.toBeNull();
-    expect(interactionContainer.textContent).toContain('请确认是否批量标记未处理!');
+    expect(interactionContainer.textContent).toContain(zh('alert.center.confirm.mark-no-batch'));
 
     await act(async () => {
       (interactionContainer.querySelector(
@@ -816,7 +816,9 @@ describe('AlertCenterSurface', () => {
 
     expect(onRuleQuickCreate).not.toHaveBeenCalled();
     expect(interactionContainer.querySelector('[data-alert-rule-dialog="silence"]')).not.toBeNull();
-    expect(interactionContainer.querySelector('[data-alert-rule-submit-error="true"]')?.textContent).toContain('规则名称为必填项');
+    expect(interactionContainer.querySelector('[data-alert-rule-submit-error="true"]')?.textContent).toContain(
+      zh('alert.silence.validation.name')
+    );
   });
 
   it('renders cold entity context and noise-control summary shells', () => {
@@ -926,22 +928,22 @@ describe('AlertCenterSurface', () => {
         viewMode: 'resource-dependency',
         sourceKind: 'database-middleware-connection',
         edgeId: 'svc-checkout--res-orders-db',
-        returnTo: `${returnTo}&returnLabel=HertzBeat%20%E4%BC%81%E4%B8%9A%E8%BF%90%E7%BB%B4%E6%8B%93%E6%89%91`,
-        returnLabel: 'HertzBeat 企业运维拓扑'
+        returnTo: `${returnTo}&returnLabel=${encodeURIComponent('HertzBeat enterprise topology')}`,
+        returnLabel: 'HertzBeat enterprise topology'
       }
     }, zh);
 
     expect(html).toContain('data-alert-topology-context="impact-filter-panel"');
-    expect(html).toContain('拓扑影响面');
+    expect(html).toContain(zh('alert.center.topology.context.kicker'));
     expect(html).toContain('checkout-api');
-    expect(html).toContain('资源依赖');
+    expect(html).toContain(zh('topology.view.resource-dependency.label'));
     expect(html).toContain('resource-dependency');
-    expect(html).toContain('数据库 / 中间件连接');
+    expect(html).toContain(zh('alert.center.topology.source.database-middleware-connection'));
     expect(html).toContain('svc-checkout--res-orders-db');
     expect(html).toContain('data-alert-topology-return="true"');
     expect(html).toContain('href="/topology?viewMode=resource-dependency&amp;sourceKind=database-middleware-connection&amp;edgeId=svc-checkout--res-orders-db');
     expect(html).not.toContain('returnLabel');
-    expect(html).not.toContain('HertzBeat 企业运维拓扑');
+    expect(html).not.toContain('HertzBeat enterprise topology');
   });
 
   it('renders unknown topology source kind as a localized fallback while preserving the route slug', () => {
@@ -963,7 +965,7 @@ describe('AlertCenterSurface', () => {
       }
     }, zh);
 
-    expect(html).toContain('未知关系来源 custom-edge-source');
+    expect(html).toContain(zh('alert.center.topology.source.unknown', { sourceKind: 'custom-edge-source' }));
     expect(html).toContain('data-alert-topology-source-kind="custom-edge-source"');
     expect(html).toContain('href="/topology?sourceKind=custom-edge-source"');
     expect(html).not.toContain('>custom-edge-source</span>');
@@ -988,7 +990,7 @@ describe('AlertCenterSurface', () => {
       }
     }, zh);
 
-    expect(html).toContain('未知拓扑视图 custom-impact-view');
+    expect(html).toContain(zh('alert.center.topology.view-mode.unknown', { viewMode: 'custom-impact-view' }));
     expect(html).toContain('data-alert-topology-view-mode="custom-impact-view"');
     expect(html).toContain('href="/topology?viewMode=custom-impact-view"');
     expect(html).not.toContain('>custom-impact-view</span>');
@@ -1017,15 +1019,31 @@ describe('AlertCenterSurface', () => {
     }, zh);
 
     expect(html).toContain('data-alert-evidence-closure="otlp-alert-evidence-workbench"');
-    expect(html).toContain('告警证据闭环');
-    expect(html).toContain('把实体、指标、日志、链路和拓扑证据放在同一个处理入口。');
+    expect(html).toContain(zh('alert.center.evidence.closure.kicker'));
+    expect(html).toContain(zh('alert.center.evidence.closure.copy'));
     expect(html).toContain('data-alert-closure-summary="evidence-and-actions"');
     expect(html).toContain('data-alert-evidence-summary="entity,metrics,logs,traces,topology"');
     expect(html).toContain('data-alert-operation-summary="acknowledge,recover,threshold,notice,group,silence,inhibit,automation,close"');
-    expect(html).toContain('证据范围');
-    expect(html).toContain('实体详情 / 指标证据 / 日志证据 / 链路证据 / 拓扑影响面');
-    expect(html).toContain('处理动作');
-    expect(html).toContain('确认告警 / 标记已恢复 / 创建阈值规则 / 配置通知策略 / 配置分组收敛 / 创建静默 / 创建抑制 / 建议自动化动作 / 关闭告警');
+    expect(html).toContain(zh('alert.center.evidence.closure.summary.evidence'));
+    expect(html).toContain([
+      zh('alert.center.evidence.entity.title'),
+      zh('alert.center.evidence.metrics.title'),
+      zh('alert.center.evidence.logs.title'),
+      zh('alert.center.evidence.traces.title'),
+      zh('alert.center.evidence.topology.title')
+    ].join(' / '));
+    expect(html).toContain(zh('alert.center.evidence.closure.summary.operations'));
+    expect(html).toContain([
+      zh('alert.center.operation.acknowledge.label'),
+      zh('alert.center.operation.recover.label'),
+      zh('alert.center.operation.threshold.label'),
+      zh('alert.center.operation.notice.label'),
+      zh('alert.center.operation.group.label'),
+      zh('alert.center.operation.silence.label'),
+      zh('alert.center.operation.inhibit.label'),
+      zh('alert.center.operation.automation.label'),
+      zh('alert.center.operation.close.label')
+    ].join(' / '));
     expect(html).toContain('data-alert-evidence-link="entity"');
     expect(html).toContain('data-alert-evidence-link="metrics"');
     expect(html).toContain('data-alert-evidence-link="logs"');
@@ -1045,17 +1063,17 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-alert-closure-action="inhibit"');
     expect(html).toContain('data-alert-closure-action="automation"');
     expect(html).toContain('data-alert-closure-action="close"');
-    expect(html).toContain('确认告警');
-    expect(html).toContain('标记已恢复');
-    expect(html).toContain('创建阈值规则');
-    expect(html).toContain('配置通知策略');
-    expect(html).toContain('配置分组收敛');
-    expect(html).toContain('创建静默');
-    expect(html).toContain('创建抑制');
-    expect(html).toContain('建议自动化动作');
+    expect(html).toContain(zh('alert.center.operation.acknowledge.label'));
+    expect(html).toContain(zh('alert.center.operation.recover.label'));
+    expect(html).toContain(zh('alert.center.operation.threshold.label'));
+    expect(html).toContain(zh('alert.center.operation.notice.label'));
+    expect(html).toContain(zh('alert.center.operation.group.label'));
+    expect(html).toContain(zh('alert.center.operation.silence.label'));
+    expect(html).toContain(zh('alert.center.operation.inhibit.label'));
+    expect(html).toContain(zh('alert.center.operation.automation.label'));
     expect(html).toContain('/actions?');
     expect(html).toContain('alertGroupId=7');
-    expect(html).toContain('关闭告警');
+    expect(html).toContain(zh('alert.center.operation.close.label'));
   });
 
   it('renders inherited time and monitor context inside the existing alert evidence panel', () => {
@@ -1087,19 +1105,19 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-alert-evidence-closure="otlp-alert-evidence-workbench"');
     expect(html).toContain('data-alert-evidence-context="inherited-time-context"');
     expect(html).toContain('data-alert-evidence-context-row="time"');
-    expect(html).toContain('时间范围');
+    expect(html).toContain(zh('alert.center.context.time.title'));
     expect(html).toContain('last-45m');
-    expect(html).toContain('2024/04/16 00:53:20 → 2024/04/16 01:38:20 · 已暂停 · Asia/Shanghai');
+    expect(html).toContain(`2024/04/16 00:53:20 → 2024/04/16 01:38:20 · ${zh('alert.center.context.time.paused')} · Asia/Shanghai`);
     expect(html).toContain('data-alert-evidence-context-row="monitor"');
-    expect(html).toContain('监控实例');
+    expect(html).toContain(zh('alert.center.context.monitor.title'));
     expect(html).toContain('checkout-http');
-    expect(html).toContain('website · example.com:443 · 监控 ID 632051474676992');
+    expect(html).toContain(`website · example.com:443 · ${zh('alert.center.context.monitor.id-meta', { monitorId: '632051474676992' })}`);
     expect(html).toContain('data-alert-evidence-context-row="source"');
-    expect(html).toContain('采集来源');
-    expect(html).toContain('传统监控');
-    expect(html).toContain('监控中心上下文');
-    expect(html).not.toContain('健康');
-    expect(html).not.toContain('0 个证据');
+    expect(html).toContain(zh('alert.center.context.source.title'));
+    expect(html).toContain(zh('alert.center.context.source.monitor.copy'));
+    expect(html).toContain(zh('alert.center.context.source.monitor.meta'));
+    expect(html).not.toContain(zh('topology.legend.health.healthy'));
+    expect(html).not.toContain('0 evidence');
     expect(html).not.toContain('fake');
   });
 
@@ -1107,21 +1125,21 @@ describe('AlertCenterSurface', () => {
     const successHtml = renderSurface({
       operationFeedback: {
         tone: 'success',
-        copy: '告警状态已更新,列表正在刷新。'
+        copy: zh('alert.center.operation.success')
       }
     }, zh);
     const failedHtml = renderSurface({
       operationFeedback: {
         tone: 'danger',
-        copy: '操作未完成,请稍后重试。'
+        copy: zh('alert.center.operation.failed', { reason: 'retry later' })
       }
     }, zh);
 
     expect(successHtml).toContain('data-alert-operation-feedback="success"');
     expect(successHtml).toContain('aria-live="polite"');
-    expect(successHtml).toContain('告警状态已更新,列表正在刷新。');
+    expect(successHtml).toContain(zh('alert.center.operation.success'));
     expect(failedHtml).toContain('data-alert-operation-feedback="danger"');
-    expect(failedHtml).toContain('操作未完成,请稍后重试。');
+    expect(failedHtml).toContain(zh('alert.center.operation.failed', { reason: 'retry later' }));
   });
 
   it('explains disabled direct closure actions with localized title and aria labels', () => {
@@ -1145,11 +1163,11 @@ describe('AlertCenterSurface', () => {
     }, zh);
 
     expect(html).toContain('data-alert-closure-action-disabled="missing-alert-group-id"');
-    expect(html).toContain('title="需要选择告警组后才能执行此操作"');
-    expect(html).toContain('aria-label="确认告警:需要选择告警组后才能执行此操作"');
-    expect(html).toContain('aria-label="标记已恢复:需要选择告警组后才能执行此操作"');
-    expect(html).toContain('aria-label="关闭告警:需要选择告警组后才能执行此操作"');
-    expect(html).not.toContain('missing-alert-group-id 需要');
+    expect(html).toContain(`title="${zh('alert.center.closure-action.disabled.no-group')}"`);
+    expect(html).toContain(`aria-label="${zh('alert.center.operation.acknowledge.label')}:${zh('alert.center.closure-action.disabled.no-group')}"`);
+    expect(html).toContain(`aria-label="${zh('alert.center.operation.recover.label')}:${zh('alert.center.closure-action.disabled.no-group')}"`);
+    expect(html).toContain(`aria-label="${zh('alert.center.operation.close.label')}:${zh('alert.center.closure-action.disabled.no-group')}"`);
+    expect(html).not.toContain('missing-alert-group-id ');
   });
 
   it('fires direct closure operation buttons with the primary group id while keeping silence and inhibit as rule links', async () => {
@@ -1360,7 +1378,7 @@ describe('AlertCenterSurface', () => {
     expect(onClosureAction).not.toHaveBeenCalled();
     expect(interactionContainer.querySelector('[data-alert-center-row-delete-confirm-dialog="angular-single-delete-confirm"]')).not.toBeNull();
     expect(interactionContainer.querySelector('[data-alert-center-row-delete-confirm-group-id="7"]')).not.toBeNull();
-    expect(interactionContainer.textContent).toContain('请确认是否删除!');
+    expect(interactionContainer.textContent).toContain(zh('common.confirm.delete'));
 
     await act(async () => {
       (interactionContainer.querySelector(
@@ -1447,9 +1465,9 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-overlay-dialog="true"');
     expect(html).toContain('data-alert-rule-dialog="silence"');
     expect(html).toContain('data-alert-rule-dialog-submit="silence"');
-    expect(html).toContain('Create alert silence');
+    expect(html).toContain(t('entity.alert.workbench.silence.title'));
     expect(html).toContain('Checkout API');
-    expect(html).toContain('Silence Strategy Name');
+    expect(html).toContain(t('alert.silence.name'));
     expect(html).toContain('service:checkout');
     expect(html).toContain('severity:critical');
   });
@@ -1471,14 +1489,14 @@ describe('AlertCenterSurface', () => {
 
     expect(html).toContain('data-alert-rule-dialog="inhibit"');
     expect(html).toContain('data-alert-rule-dialog-submit="inhibit"');
-    expect(html).toContain('Create alert inhibit');
-    expect(html).toContain('Copy source to target');
-    expect(html).toContain('Target without severity');
-    expect(html).toContain('Clear target');
-    expect(html).toContain('Clear equal labels');
-    expect(html).toContain('Source Labels');
-    expect(html).toContain('Target Labels');
-    expect(html).toContain('Equal Labels');
+    expect(html).toContain(t('entity.alert.workbench.inhibit.title'));
+    expect(html).toContain(t('entity.alert.workbench.inhibit.shortcut.copy-source'));
+    expect(html).toContain(t('entity.alert.workbench.inhibit.shortcut.drop-severity'));
+    expect(html).toContain(t('entity.alert.workbench.inhibit.shortcut.clear-target'));
+    expect(html).toContain(t('entity.alert.workbench.inhibit.shortcut.clear-equal'));
+    expect(html).toContain(t('alert.inhibit.source_labels'));
+    expect(html).toContain(t('alert.inhibit.target_labels'));
+    expect(html).toContain(t('alert.inhibit.equal_labels'));
   });
 
   it('pins the alert center shell to the shared cold visual owner instead of page-local primitives', () => {
diff --git a/web-next/components/pages/alert-group-surface.test.tsx b/web-next/components/pages/alert-group-surface.test.tsx
index 7a07026fc3..c02220a500 100644
--- a/web-next/components/pages/alert-group-surface.test.tsx
+++ b/web-next/components/pages/alert-group-surface.test.tsx
@@ -78,6 +78,13 @@ vi.mock('./alert-group-authoring-fields', () => ({
 
 describe('AlertGroupSurface', () => {
   const t = createTranslatorMock({ locale: 'zh-CN' });
+  function groupEvidenceTitle(signal: 'logs' | 'traces' | 'metrics') {
+    return t('alert.rule.evidence.group.title', { signal: t(`alert.rule.signal.${signal}`) });
+  }
+  function groupEvidenceDraftName(signal: 'logs' | 'traces' | 'metrics', target: string) {
+    return t('alert.rule.evidence.group.draft-name', { signal: t(`alert.rule.signal.${signal}`), target });
+  }
+
   const data = {
     list: {
       content: [
@@ -114,8 +121,8 @@ describe('AlertGroupSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'traces',
-          title: '来自链路的分组上下文',
-          copy: '保存或取消后仍可回到原排障上下文。',
+          title: groupEvidenceTitle('traces'),
+          copy: t('alert.rule.evidence.group.copy'),
           groupLabelsText: 'hertzbeat.entity.id, service.name',
           returnHref: '/trace/manage?traceId=trace-123',
           rows: [],
@@ -183,15 +190,15 @@ describe('AlertGroupSurface', () => {
     expect(html).toContain('data-alert-group-row-checkbox="cold-checkbox"');
     expect(html).toContain('data-alert-group-enable-checkbox="cold-checkbox"');
     expect(html.match(/data-cold-checkbox-owner="cold-checkbox"/g)?.length).toBeGreaterThanOrEqual(2);
-    expect(html).toContain('刷新');
-    expect(html).toContain('新增分组');
-    expect(html).toContain('搜索');
+    expect(html).toContain(t('common.refresh'));
+    expect(html).toContain(t('alert.group.action.new'));
+    expect(html).toContain(t('common.search'));
     expect(html).toContain('data-alert-group-delete-selected="toolbar"');
     expect(html).toContain('data-alert-group-delete-selected-owner="route-no-select-warning"');
-    expect(html).toContain('分组收敛');
-    expect(html).toContain('管理 Alertmanager 分组收敛规则');
-    expect(html).toContain('策略名称');
-    expect(html).toContain('分组标签');
+    expect(html).toContain(t('alert.group.title'));
+    expect(html).toContain(t('alert.group.copy'));
+    expect(html).toContain(t('alert.group-converge.name'));
+    expect(html).toContain(t('alert.group-converge.group-labels'));
     expect(html).toContain('ops-group');
     expect(html).toContain('alertname');
     expect(html).toContain('data-overlay-dialog="true"');
@@ -291,7 +298,7 @@ describe('AlertGroupSurface', () => {
     expect(html).toContain('data-alert-group-empty-copy="true"');
     expect(html).not.toContain('align-top');
     expect(html).not.toContain('pt-[54px]');
-    expect(html).toContain('还没有分组规则');
+    expect(html).toContain(t('alert.group.empty.title'));
   });
 
   it('renders three-signal evidence context before group authoring', () => {
@@ -309,16 +316,16 @@ describe('AlertGroupSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'metrics',
-          title: '来自指标的分组上下文',
-          copy: '新建分组时会按当前实体、服务、命名空间和环境做收敛。',
+          title: groupEvidenceTitle('metrics'),
+          copy: t('alert.rule.evidence.group.copy'),
           groupLabelsText: 'hertzbeat.entity.id, service.name, service.namespace, deployment.environment',
           returnHref: '/metrics/manage?entityId=service%3Acommerce%2Fcheckout',
           rows: [
-            { label: '当前实体', value: 'checkout', meta: 'entityId service:commerce/checkout' },
-            { label: '链路上下文', value: 'trace-123', meta: 'spanId span-456' }
+            { label: t('signal.context.entity.label'), value: 'checkout', meta: 'entityId service:commerce/checkout' },
+            { label: t('signal.context.trace.label'), value: 'trace-123', meta: 'spanId span-456' }
           ],
           draftPatch: {
-            name: '指标 checkout 分组',
+            name: groupEvidenceDraftName('metrics', 'checkout'),
             groupLabelsText: 'hertzbeat.entity.id, service.name, service.namespace, deployment.environment'
           }
         }}
@@ -351,11 +358,11 @@ describe('AlertGroupSurface', () => {
     expect(html).toContain('data-alert-group-evidence-context="signal-route"');
     expect(html).toContain('data-alert-group-evidence-signal="metrics"');
     expect(html).toContain('data-alert-group-prefill-labels="hertzbeat.entity.id, service.name, service.namespace, deployment.environment"');
-    expect(html).toContain('来自指标的分组上下文');
-    expect(html).toContain('返回排障上下文');
+    expect(html).toContain(groupEvidenceTitle('metrics'));
+    expect(html).toContain(t('alert.rule.evidence.return'));
     expect(html).toContain('href="/metrics/manage?entityId=service%3Acommerce%2Fcheckout"');
-    expect(html).toContain('当前实体');
-    expect(html).toContain('链路上下文');
+    expect(html).toContain(t('signal.context.entity.label'));
+    expect(html).toContain(t('signal.context.trace.label'));
   });
 
   it('renders missing group evidence labels with the localized empty fallback', () => {
@@ -373,8 +380,8 @@ describe('AlertGroupSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'logs',
-          title: '来自日志的分组上下文',
-          copy: '按当前实体创建分组规则。',
+          title: groupEvidenceTitle('logs'),
+          copy: t('alert.rule.evidence.group.copy'),
           groupLabelsText: '',
           returnHref: '/log/manage?entityId=service-1',
           rows: [],
@@ -410,7 +417,7 @@ describe('AlertGroupSurface', () => {
 
     expect(html).toContain('data-alert-group-evidence-context="signal-route"');
     expect(html).toContain('data-alert-group-evidence-labels="localized-fallback"');
-    expect(html).toContain('无');
+    expect(html).toContain(t('common.none'));
   });
 
   it('renders validation errors visibly inside the group editor dialog', () => {
@@ -425,7 +432,7 @@ describe('AlertGroupSurface', () => {
         editorLoading={false}
         editorSaving={false}
         editorMessage={null}
-        editorError="请填写策略名称"
+        editorError={t('alert.group.validation.name')}
         draft={{
           name: '',
           enable: true,
@@ -454,7 +461,7 @@ describe('AlertGroupSurface', () => {
 
     expect(html).toContain('data-alert-group-editor-error-inline="cold-validation"');
     expect(html).toContain('role="alert"');
-    expect(html).toContain('请填写策略名称');
+    expect(html).toContain(t('alert.group.validation.name'));
   });
 
   it('renders Angular alert group save failure title/detail through shared feedback', () => {
@@ -519,7 +526,7 @@ describe('AlertGroupSurface', () => {
         editorLoading={false}
         editorSaving={false}
         editorMessage={null}
-        editorError="请先选择要删除的监控"
+        editorError={t('common.notify.no-select-delete')}
         draft={{
           name: '',
           enable: true,
@@ -550,7 +557,7 @@ describe('AlertGroupSurface', () => {
     expect(html).toContain('data-alert-group-action-feedback="warning"');
     expect(html).toContain('data-hz-feedback-tone="warning"');
     expect(html).toContain('role="alert"');
-    expect(html).toContain('请先选择要删除的监控');
+    expect(html).toContain(t('common.notify.no-select-delete'));
   });
 
   it('renders Angular alert group enable failure title/detail outside the editor', () => {
diff --git a/web-next/components/pages/alert-inhibit-surface.test.tsx b/web-next/components/pages/alert-inhibit-surface.test.tsx
index 43c8558c6f..6b183f6039 100644
--- a/web-next/components/pages/alert-inhibit-surface.test.tsx
+++ b/web-next/components/pages/alert-inhibit-surface.test.tsx
@@ -105,6 +105,11 @@ vi.mock('./alert-inhibit-authoring-fields', () => ({
 
 describe('AlertInhibitSurface', () => {
   const t = createTranslatorMock({ locale: 'zh-CN' });
+  const inhibitEvidenceTitle = (signal: 'logs' | 'traces' | 'metrics') =>
+    t('alert.rule.evidence.inhibit.title', { signal: t(`alert.rule.signal.${signal}`) });
+  const inhibitDraftName = (signal: 'logs' | 'traces' | 'metrics', target: string) =>
+    t('alert.rule.evidence.inhibit.draft-name', { signal: t(`alert.rule.signal.${signal}`), target });
+
   const data = {
     list: {
       content: [
@@ -140,8 +145,8 @@ describe('AlertInhibitSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'traces',
-          title: '来自链路的抑制上下文',
-          copy: '保存或取消后仍可回到原排障上下文。',
+          title: inhibitEvidenceTitle('traces'),
+          copy: t('alert.rule.evidence.inhibit.copy'),
           sourceLabelsText: 'hertzbeat.signal:traces, service.name:checkout',
           targetLabelsText: 'hertzbeat.signal:traces, service.name:checkout',
           equalLabelsText: 'service.name',
@@ -210,15 +215,14 @@ describe('AlertInhibitSurface', () => {
     expect(html).toContain('data-alert-inhibit-row-checkbox="cold-checkbox"');
     expect(html).toContain('data-alert-inhibit-enable-checkbox="cold-checkbox"');
     expect(html.match(/data-cold-checkbox-owner="cold-checkbox"/g)?.length).toBeGreaterThanOrEqual(3);
-    expect(html).toContain('刷新');
-    expect(html).toContain('新增抑制');
-    expect(html).toContain('批量删除');
-    expect(html).toContain('搜索');
-    expect(html).not.toContain('当前抑制');
-    expect(html).toContain('抑制规则名称');
-    expect(html).toContain('源标签');
-    expect(html).toContain('目标标签');
-    expect(html).toContain('相等标签');
+    expect(html).toContain(t('common.refresh'));
+    expect(html).toContain(t('alert.inhibit.action.new'));
+    expect(html).toContain(t('common.button.delete-batch'));
+    expect(html).toContain(t('common.search'));
+    expect(html).toContain(t('alert.inhibit.name'));
+    expect(html).toContain(t('alert.inhibit.source_labels'));
+    expect(html).toContain(t('alert.inhibit.target_labels'));
+    expect(html).toContain(t('alert.inhibit.equal_labels'));
     expect(html).toContain('db-inhibit');
     expect(html).toContain('service:checkout');
     expect(html).toContain('data-overlay-dialog="true"');
@@ -312,7 +316,7 @@ describe('AlertInhibitSurface', () => {
     expect(html).toContain('data-alert-inhibit-pagination="cold-dense-pagination"');
     expect(html).toContain('data-alert-inhibit-empty-state="cold-table-empty"');
     expect(html).toContain('data-alert-inhibit-empty-icon="cold-empty-box"');
-    expect(html).toContain('还没有抑制规则');
+    expect(html).toContain(t('alert.inhibit.empty.title'));
   });
 
   it('renders the topology return context when opened from alert impact inhibit closure', () => {
@@ -345,7 +349,7 @@ describe('AlertInhibitSurface', () => {
           sourceKind: 'database-middleware-connection',
           edgeId: 'svc-checkout--res-orders-db',
           returnTo,
-          returnLabel: 'HertzBeat 企业运维拓扑'
+          returnLabel: t('topology.identity')
         }}
         draft={{
           name: '',
@@ -379,7 +383,7 @@ describe('AlertInhibitSurface', () => {
     expect(html).toContain('data-alert-inhibit-return-context="topology-edge"');
     expect(html).toContain('data-alert-inhibit-return-edge-id="svc-checkout--res-orders-db"');
     expect(html).toContain('href="/topology?viewMode=resource-dependency&amp;sourceKind=database-middleware-connection&amp;edgeId=svc-checkout--res-orders-db');
-    expect(html).not.toContain('HertzBeat 企业运维拓扑');
+    expect(html).not.toContain(t('topology.identity'));
     expect(html).not.toContain('returnLabel=');
     expect(html).toContain('checkout-api');
     expect(html).toContain('resource-dependency');
@@ -451,7 +455,7 @@ describe('AlertInhibitSurface', () => {
     expect(html).toContain('data-alert-inhibit-match-action="view-all"');
     expect(html).toContain('data-alert-inhibit-entity-return="true"');
     expect(html).toContain('checkout-api');
-    expect(html).toContain('已有 1 条命中规则不可用或已不存在');
+    expect(html).toContain(t('entity.noise-controls.management.missing', { count: 1 }));
   });
 
   it('renders Angular created-outside-matched authoring notice', () => {
@@ -513,8 +517,8 @@ describe('AlertInhibitSurface', () => {
     expect(html).toContain('data-alert-inhibit-created-outside-matched="angular-authoring-notice"');
     expect(html).toContain('data-alert-inhibit-created-outside-matched-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('data-alert-inhibit-created-outside-matched-action="view-all"');
-    expect(html).toContain('新规则已创建');
-    expect(html).toContain('当前视图不会自动显示这条新规则');
+    expect(html).toContain(t('entity.noise-controls.authoring.created-outside-matched.title'));
+    expect(html).toContain(t('entity.noise-controls.authoring.created-outside-matched.copy'));
   });
 
   it('renders three-signal evidence context before inhibit authoring', () => {
@@ -532,18 +536,18 @@ describe('AlertInhibitSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'traces',
-          title: '来自链路的抑制上下文',
-          copy: '新建抑制时会按当前实体、服务、环境和链路标签做匹配。',
+          title: inhibitEvidenceTitle('traces'),
+          copy: t('alert.rule.evidence.inhibit.copy'),
           sourceLabelsText: 'hertzbeat.signal:traces, service.name:checkout, trace_id:trace-123',
           targetLabelsText: 'hertzbeat.signal:traces, service.name:checkout, trace_id:trace-123',
           equalLabelsText: 'service.name, deployment.environment',
           returnHref: '/trace/manage?traceId=trace-123',
           rows: [
-            { label: '当前实体', value: 'checkout', meta: 'entityId service:commerce/checkout' },
-            { label: '链路上下文', value: 'trace-123', meta: 'spanId span-456' }
+            { label: t('signal.context.entity.label'), value: 'checkout', meta: 'entityId service:commerce/checkout' },
+            { label: t('signal.context.trace.label'), value: 'trace-123', meta: 'spanId span-456' }
           ],
           draftPatch: {
-            name: '链路 checkout 抑制',
+            name: inhibitDraftName('traces', 'checkout'),
             sourceLabelsText: 'hertzbeat.signal:traces, service.name:checkout, trace_id:trace-123',
             targetLabelsText: 'hertzbeat.signal:traces, service.name:checkout, trace_id:trace-123',
             equalLabelsText: 'service.name, deployment.environment'
@@ -583,11 +587,11 @@ describe('AlertInhibitSurface', () => {
     expect(html).toContain('data-alert-inhibit-prefill-source-labels="hertzbeat.signal:traces, service.name:checkout, trace_id:trace-123"');
     expect(html).toContain('data-alert-inhibit-prefill-target-labels="hertzbeat.signal:traces, service.name:checkout, trace_id:trace-123"');
     expect(html).toContain('data-alert-inhibit-prefill-equal-labels="service.name, deployment.environment"');
-    expect(html).toContain('来自链路的抑制上下文');
-    expect(html).toContain('返回排障上下文');
+    expect(html).toContain(inhibitEvidenceTitle('traces'));
+    expect(html).toContain(t('alert.rule.evidence.return'));
     expect(html).toContain('href="/trace/manage?traceId=trace-123"');
-    expect(html).toContain('当前实体');
-    expect(html).toContain('链路上下文');
+    expect(html).toContain(t('signal.context.entity.label'));
+    expect(html).toContain(t('signal.context.trace.label'));
   });
 
   it('renders missing inhibit evidence labels with the localized empty fallback', () => {
@@ -605,8 +609,8 @@ describe('AlertInhibitSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'metrics',
-          title: '来自指标的抑制上下文',
-          copy: '按当前实体创建抑制规则。',
+          title: inhibitEvidenceTitle('metrics'),
+          copy: t('alert.rule.evidence.inhibit.copy'),
           sourceLabelsText: '',
           targetLabelsText: '',
           equalLabelsText: '',
@@ -651,7 +655,7 @@ describe('AlertInhibitSurface', () => {
     expect(html).toContain('data-alert-inhibit-source-labels="localized-fallback"');
     expect(html).toContain('data-alert-inhibit-target-labels="localized-fallback"');
     expect(html).toContain('data-alert-inhibit-equal-labels="localized-fallback"');
-    expect(html.match(/无/g)?.length).toBeGreaterThanOrEqual(3);
+    expect(html.split(t('common.none')).length - 1).toBeGreaterThanOrEqual(3);
   });
 
   it('renders validation errors visibly inside the inhibit editor dialog', () => {
@@ -666,7 +670,7 @@ describe('AlertInhibitSurface', () => {
         editorLoading={false}
         editorSaving={false}
         editorMessage={null}
-        editorError="请填写抑制规则名称"
+        editorError={t('alert.inhibit.validation.name')}
         draft={{
           name: '',
           enable: true,
@@ -698,7 +702,7 @@ describe('AlertInhibitSurface', () => {
 
     expect(html).toContain('data-alert-inhibit-editor-error-inline="cold-validation"');
     expect(html).toContain('role="alert"');
-    expect(html).toContain('请填写抑制规则名称');
+    expect(html).toContain(t('alert.inhibit.validation.name'));
   });
 
   it('renders Angular inhibit save failure title/detail through shared feedback', () => {
@@ -911,8 +915,10 @@ describe('AlertInhibitSurface', () => {
     );
 
     expect(html).toContain('data-alert-inhibit-entity-prefill="angular-alert-common-labels"');
-    expect(html).toContain('data-alert-inhibit-authoring-prefill-title="为当前实体创建抑制规则"');
-    expect(html).toContain('data-alert-inhibit-authoring-prefill-copy="已提取当前实体可见告警的共享标签,并填入源条件、目标条件和相等标签。"');
-    expect(html).not.toContain('data-alert-inhibit-authoring-prefill-warning="当前没有稳定的共享告警标签,请手动填写抑制条件。"');
+    expect(html).toContain(`data-alert-inhibit-authoring-prefill-title="${t('entity.noise-controls.authoring.inhibit.title')}"`);
+    expect(html).toContain(`data-alert-inhibit-authoring-prefill-copy="${t('entity.noise-controls.authoring.inhibit.prefill-success')}"`);
+    expect(html).not.toContain(
+      `data-alert-inhibit-authoring-prefill-warning="${t('entity.noise-controls.authoring.inhibit.prefill-warning')}"`
+    );
   });
 });
diff --git a/web-next/components/pages/alert-integration-markdown.test.tsx b/web-next/components/pages/alert-integration-markdown.test.tsx
index 1c53f55570..601a0f89ab 100644
--- a/web-next/components/pages/alert-integration-markdown.test.tsx
+++ b/web-next/components/pages/alert-integration-markdown.test.tsx
@@ -9,9 +9,9 @@ describe('AlertIntegrationMarkdown', () => {
   it('renders fenced code, indented fenced code, links, and mermaid diagrams without exposing raw fences', () => {
     const html = renderToStaticMarkup(
       <AlertIntegrationMarkdown
-        content={`# 指南
+        content={`# Guide
 
-请查看 [Prometheus 文档](https://prometheus.io/docs/alerting/latest/configuration/)。
+Read the [Prometheus docs](https://prometheus.io/docs/alerting/latest/configuration/).
 
 \`\`\`json
 {"status":"ok"}
@@ -19,10 +19,10 @@ describe('AlertIntegrationMarkdown', () => {
 
 \`\`\`mermaid
 graph LR
-  A[外部系统告警] --> B[Webhook]
+  A[External alert] --> B[Webhook]
 \`\`\`
 
-1. 检查配置
+1. Check configuration
     \`\`\`bash
     curl http://localhost:9090/api/v1/rules
     \`\`\`
@@ -47,12 +47,12 @@ graph LR
   it('renders provider markdown headings, emphasis, blockquotes, and pipe tables instead of leaking raw syntax', () => {
     const html = renderToStaticMarkup(
       <AlertIntegrationMarkdown
-        content={`>将 Zabbix 的告警通过 Webhook 方式发送到 HertzBeat 告警平台。
+        content={`>Send Zabbix alerts to the HertzBeat alert platform through Webhook.
 
-#### 进入通知模板配置
-2. 进入 **告警管理** > **告警配置** > **通知模板**
+#### Open notification template settings
+2. Open **Alert management** > **Alert settings** > **Notification templates**
 
-| 名称 | 值 |
+| Name | Value |
 |-----|-----|
 | URL | http://your-hertzbeat-server:1157/api/alerts/report/zabbix |
 | TriggerSeverity | {TRIGGER.SEVERITY} |
@@ -64,22 +64,22 @@ graph LR
     expect(html).toContain('data-alert-integration-markdown-quote="true"');
     expect(html).toContain('data-alert-integration-markdown-table="true"');
     expect(html).toContain('<strong');
-    expect(html).toContain('告警管理');
+    expect(html).toContain('Alert management');
     expect(html).toContain('<table');
     expect(html).toContain('<th');
     expect(html).toContain('<td');
     expect(html).toContain('TriggerSeverity');
     expect(html).not.toContain('####');
-    expect(html).not.toContain('**告警管理**');
-    expect(html).not.toContain('| 名称 | 值 |');
+    expect(html).not.toContain('**Alert management**');
+    expect(html).not.toContain('| Name | Value |');
     expect(html).not.toContain('|-----|-----|');
-    expect(html).not.toContain('&gt;将 Zabbix');
+    expect(html).not.toContain('&gt;Send Zabbix');
   });
 
   it('renders the real alert integration provider docs without leaking heading, emphasis, table, or fence syntax', () => {
     const docs = ['tencent', 'zabbix', 'prometheus'].map(source => ({
       source,
-      content: readFileSync(resolve(process.cwd(), '..', 'web-app', 'src', 'assets', 'doc', 'alert-integration', `${source}.zh-CN.md`), 'utf8')
+      content: readFileSync(resolve(process.cwd(), 'public', 'assets', 'doc', 'alert-integration', `${source}.zh-CN.md`), 'utf8')
     }));
 
     for (const { source, content } of docs) {
@@ -91,13 +91,13 @@ graph LR
         expect(html, source).toContain('data-alert-integration-markdown-strong="true"');
       }
       expect(html, source).not.toContain('####');
-      expect(html, source).not.toContain('**告警');
+      expect(html, source).not.toMatch(/\*\*[\u4e00-\u9fff]/);
       expect(html, source).not.toContain('```');
     }
 
     const zabbixHtml = renderToStaticMarkup(<AlertIntegrationMarkdown content={docs.find(doc => doc.source === 'zabbix')!.content} />);
     expect(zabbixHtml).toContain('data-alert-integration-markdown-table="true"');
-    expect(zabbixHtml).not.toContain('| 名称 | 值 |');
+    expect(zabbixHtml).not.toMatch(/\| \u540d\u79f0 \| \u503c \|/);
 
     const prometheusHtml = renderToStaticMarkup(<AlertIntegrationMarkdown content={docs.find(doc => doc.source === 'prometheus')!.content} />);
     expect(prometheusHtml).toContain('data-alert-integration-code-block="bash"');
diff --git a/web-next/components/pages/alert-notice-console-shell.test.tsx b/web-next/components/pages/alert-notice-console-shell.test.tsx
index b83292bab1..960d57a80e 100644
--- a/web-next/components/pages/alert-notice-console-shell.test.tsx
+++ b/web-next/components/pages/alert-notice-console-shell.test.tsx
@@ -30,9 +30,9 @@ describe('AlertNoticeConsoleShell', () => {
     expect(html).toContain('data-tab="rule"');
     expect(html).toContain('data-tab="template"');
     expect(html).toContain('data-selected-tab="rule"');
-    expect(html).toContain('通知媒介');
-    expect(html).toContain('通知策略');
-    expect(html).toContain('通知模板');
+    expect(html).toContain(t('alert.notice.receiver'));
+    expect(html).toContain(t('alert.notice.rule'));
+    expect(html).toContain(t('alert.notice.template'));
     expect(html).toContain('data-alert-notice-console-panel="true"');
     expect(html).toContain('data-panel-tab="rule"');
     expect(html).toContain('role="tabpanel"');
diff --git a/web-next/components/pages/alert-notice-rule-fields.test.tsx b/web-next/components/pages/alert-notice-rule-fields.test.tsx
index c79fb67863..f45fc56c46 100644
--- a/web-next/components/pages/alert-notice-rule-fields.test.tsx
+++ b/web-next/components/pages/alert-notice-rule-fields.test.tsx
@@ -16,6 +16,18 @@ vi.mock('../ui/select', () => ({
 
 describe('AlertNoticeRuleFields', () => {
   const t = createTranslatorMock({ locale: 'zh-CN' });
+  const receiverIdsPlaceholder = 'Receiver IDs, for example 1, 2';
+  const templateIdPlaceholder = 'Template ID, -1 means default template';
+  const labelsPlaceholder = 'Label match, for example severity:critical';
+  const daysPlaceholder = 'Date range, for example 1,2,3,4,5';
+  const emailReceiverLabel = 'Platform on-call email';
+  const paymentWebhookReceiverLabel = 'Payment on-call webhook';
+  const webhookReceiverLabel = 'Webhook on-call';
+  const phoneReceiverLabel = 'Platform on-call phone';
+  const emailTemplateLabel = 'Email alert template';
+  const webhookTemplateLabel = 'Webhook alert template';
+  const phoneTemplateLabel = 'Phone alert template';
+  const missingTypeTemplateLabel = 'Template without type';
 
   it('renders the current notice rule field order and scheduling posture', () => {
     const html = renderToStaticMarkup(
@@ -33,21 +45,21 @@ describe('AlertNoticeRuleFields', () => {
           periodEnd: '18:00'
         }}
         receiverOptions={[
-          { value: '1', label: '平台值班-邮箱' },
-          { value: '2', label: '支付值班-Webhook' }
+          { value: '1', label: emailReceiverLabel },
+          { value: '2', label: paymentWebhookReceiverLabel }
         ]}
         templateOptions={[
-          { value: '-1', label: '系统内置模版' },
-          { value: '9', label: '邮件告警模板' }
+          { value: '-1', label: t('alert.notice.template.preset.true') },
+          { value: '9', label: emailTemplateLabel }
         ]}
         labelOptions={{
           keys: ['severity', 'service'],
           valuesByKey: { severity: ['critical', 'warning'], service: ['checkout'] }
         }}
-        receiverIdsPlaceholder="接收对象 ID,例如 1, 2"
-        templateIdPlaceholder="模板 ID,-1 表示默认模板"
-        labelsPlaceholder="标签匹配,例如 severity:critical"
-        daysPlaceholder="日期范围,例如 1,2,3,4,5"
+        receiverIdsPlaceholder={receiverIdsPlaceholder}
+        templateIdPlaceholder={templateIdPlaceholder}
+        labelsPlaceholder={labelsPlaceholder}
+        daysPlaceholder={daysPlaceholder}
         onDraftChange={vi.fn()}
       />
     );
@@ -64,7 +76,7 @@ describe('AlertNoticeRuleFields', () => {
     expect(html).toContain('data-alert-notice-rule-receiver-selector="cold-multi-select"');
     expect(html).toContain('data-alert-notice-rule-template-selector="cold-select"');
     expect(html).toContain('data-alert-notice-rule-days-selector="cold-weekday-checkboxes"');
-    expect(html).toContain('策略名称');
+    expect(html).toContain(t('alert.notice.rule.name'));
     expect(html).not.toContain('alert.notice.rule.name');
     expect(html).toContain('data-testid="notice-rule-field-name"');
     expect(html).toContain('data-testid="notice-rule-field-receiverIdsText"');
@@ -79,13 +91,13 @@ describe('AlertNoticeRuleFields', () => {
     expect(html).toContain('role="switch"');
     expect(html).toContain('aria-checked="false"');
     expect(html).toContain('aria-checked="true"');
-    expect(html).toContain('平台值班-邮箱');
-    expect(html).toContain('支付值班-Webhook');
-    expect(html).toContain('邮件告警模板');
-    expect(html).toContain('接收对象');
-    expect(html).toContain('通知模板');
-    expect(html).toContain('标签匹配');
-    expect(html).toContain('启用状态');
+    expect(html).toContain(emailReceiverLabel);
+    expect(html).toContain(paymentWebhookReceiverLabel);
+    expect(html).toContain(emailTemplateLabel);
+    expect(html).toContain(t('alert.notice.receiver.people'));
+    expect(html).toContain(t('alert.notice.template'));
+    expect(html).toContain(t('alert.notice.rule.tag'));
+    expect(html).toContain(t('common.enable'));
     expect(html).not.toContain('alert.notice.receiver.people');
     expect(html).not.toContain('common.enable');
     expect(html).not.toContain('alert.notice.rule.receiver');
@@ -137,10 +149,10 @@ describe('AlertNoticeRuleFields', () => {
           periodStart: '',
           periodEnd: ''
         }}
-        receiverIdsPlaceholder="接收对象 ID,例如 1, 2"
-        templateIdPlaceholder="模板 ID,-1 表示默认模板"
-        labelsPlaceholder="标签匹配,例如 severity:critical"
-        daysPlaceholder="日期范围,例如 1,2,3,4,5"
+        receiverIdsPlaceholder={receiverIdsPlaceholder}
+        templateIdPlaceholder={templateIdPlaceholder}
+        labelsPlaceholder={labelsPlaceholder}
+        daysPlaceholder={daysPlaceholder}
         onDraftChange={vi.fn()}
       />
     );
@@ -168,11 +180,11 @@ describe('AlertNoticeRuleFields', () => {
           periodEnd: ''
         }}
         receiverOptions={[]}
-        templateOptions={[{ value: '-1', label: '系统内置模版' }]}
-        receiverIdsPlaceholder="接收对象 ID,例如 1, 2"
-        templateIdPlaceholder="模板 ID,-1 表示默认模板"
-        labelsPlaceholder="标签匹配,例如 severity:critical"
-        daysPlaceholder="日期范围,例如 1,2,3,4,5"
+        templateOptions={[{ value: '-1', label: t('alert.notice.template.preset.true') }]}
+        receiverIdsPlaceholder={receiverIdsPlaceholder}
+        templateIdPlaceholder={templateIdPlaceholder}
+        labelsPlaceholder={labelsPlaceholder}
+        daysPlaceholder={daysPlaceholder}
         onDraftChange={vi.fn()}
       />
     );
@@ -181,7 +193,7 @@ describe('AlertNoticeRuleFields', () => {
     expect(html).toContain('data-alert-notice-rule-edit-option-seeding-owner="route-form-contract"');
     expect(html).toContain('Detail receiver');
     expect(html).toContain('Detail template');
-    expect(html).not.toContain('暂无接收对象');
+    expect(html).not.toContain(t('alert.notice.rule.receivers.empty'));
 
     const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-rule-fields.tsx'), 'utf8');
     expect(source).toContain('function mergeDetailReceiverOptions');
@@ -214,25 +226,25 @@ describe('AlertNoticeRuleFields', () => {
           periodEnd: '18:00'
         }}
         receiverOptions={[
-          { value: '1', label: '平台值班-邮箱', type: '1' },
-          { value: '2', label: 'Webhook 值班-WebHook', type: '2' }
+          { value: '1', label: emailReceiverLabel, type: '1' },
+          { value: '2', label: webhookReceiverLabel, type: '2' }
         ]}
         templateOptions={[
-          { value: '-1', label: '系统内置模版' },
-          { value: '9', label: '邮件告警模板', type: '1' },
-          { value: '10', label: 'Webhook 告警模板', type: '2' }
+          { value: '-1', label: t('alert.notice.template.preset.true') },
+          { value: '9', label: emailTemplateLabel, type: '1' },
+          { value: '10', label: webhookTemplateLabel, type: '2' }
         ]}
-        receiverIdsPlaceholder="接收对象 ID,例如 1, 2"
-        templateIdPlaceholder="模板 ID,-1 表示默认模板"
-        labelsPlaceholder="标签匹配,例如 severity:critical"
-        daysPlaceholder="日期范围,例如 1,2,3,4,5"
+        receiverIdsPlaceholder={receiverIdsPlaceholder}
+        templateIdPlaceholder={templateIdPlaceholder}
+        labelsPlaceholder={labelsPlaceholder}
+        daysPlaceholder={daysPlaceholder}
         onDraftChange={vi.fn()}
       />
     );
 
-    expect(html).toContain('系统内置模版');
-    expect(html).toContain('邮件告警模板');
-    expect(html).not.toContain('Webhook 告警模板');
+    expect(html).toContain(t('alert.notice.template.preset.true'));
+    expect(html).toContain(emailTemplateLabel);
+    expect(html).not.toContain(webhookTemplateLabel);
 
     const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-rule-fields.tsx'), 'utf8');
     expect(source).toContain('filterTemplateOptionsForReceivers');
@@ -255,27 +267,27 @@ describe('AlertNoticeRuleFields', () => {
           periodEnd: ''
         }}
         receiverOptions={[
-          { value: '1', label: '平台值班-Phone', type: 0 }
+          { value: '1', label: phoneReceiverLabel, type: 0 }
         ]}
         templateOptions={[
-          { value: '-1', label: '系统内置模版' },
-          { value: '8', label: '电话告警模板', type: 0 },
-          { value: '9', label: '邮件告警模板', type: 1 },
-          { value: '10', label: '缺少类型模板' }
+          { value: '-1', label: t('alert.notice.template.preset.true') },
+          { value: '8', label: phoneTemplateLabel, type: 0 },
+          { value: '9', label: emailTemplateLabel, type: 1 },
+          { value: '10', label: missingTypeTemplateLabel }
         ]}
-        receiverIdsPlaceholder="接收对象 ID,例如 1, 2"
-        templateIdPlaceholder="模板 ID,-1 表示默认模板"
-        labelsPlaceholder="标签匹配,例如 severity:critical"
-        daysPlaceholder="日期范围,例如 1,2,3,4,5"
+        receiverIdsPlaceholder={receiverIdsPlaceholder}
+        templateIdPlaceholder={templateIdPlaceholder}
+        labelsPlaceholder={labelsPlaceholder}
+        daysPlaceholder={daysPlaceholder}
         onDraftChange={vi.fn()}
       />
     );
 
     expect(html).toContain('data-alert-notice-rule-template-type-filter="angular-selected-receiver-type"');
     expect(html).toContain('data-alert-notice-rule-template-type-filter-owner="route-form-contract"');
-    expect(html).toContain('电话告警模板');
-    expect(html).not.toContain('邮件告警模板');
-    expect(html).not.toContain('缺少类型模板');
+    expect(html).toContain(phoneTemplateLabel);
+    expect(html).not.toContain(emailTemplateLabel);
+    expect(html).not.toContain(missingTypeTemplateLabel);
 
     const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-rule-fields.tsx'), 'utf8');
     expect(source).toContain('if (value == null)');
@@ -298,26 +310,26 @@ describe('AlertNoticeRuleFields', () => {
           periodEnd: ''
         }}
         receiverOptions={[
-          { value: '1', label: '平台值班-Email', type: 1 },
-          { value: '2', label: 'Webhook 值班-WebHook', type: 2 }
+          { value: '1', label: emailReceiverLabel, type: 1 },
+          { value: '2', label: webhookReceiverLabel, type: 2 }
         ]}
         templateOptions={[
-          { value: '-1', label: '系统内置模版' },
-          { value: '9', label: '邮件告警模板', type: 1 },
-          { value: '10', label: 'Webhook 告警模板', type: 2 }
+          { value: '-1', label: t('alert.notice.template.preset.true') },
+          { value: '9', label: emailTemplateLabel, type: 1 },
+          { value: '10', label: webhookTemplateLabel, type: 2 }
         ]}
-        receiverIdsPlaceholder="接收对象 ID,例如 1, 2"
-        templateIdPlaceholder="模板 ID,-1 表示默认模板"
-        labelsPlaceholder="标签匹配,例如 severity:critical"
-        daysPlaceholder="日期范围,例如 1,2,3,4,5"
+        receiverIdsPlaceholder={receiverIdsPlaceholder}
+        templateIdPlaceholder={templateIdPlaceholder}
+        labelsPlaceholder={labelsPlaceholder}
+        daysPlaceholder={daysPlaceholder}
         onDraftChange={vi.fn()}
       />
     );
 
     expect(html).toContain('data-alert-notice-rule-template-active-type="angular-switch-receiver"');
     expect(html).toContain('data-alert-notice-rule-template-active-type-owner="route-form-contract"');
-    expect(html).toContain('Webhook 告警模板');
-    expect(html).not.toContain('邮件告警模板');
+    expect(html).toContain(webhookTemplateLabel);
+    expect(html).not.toContain(emailTemplateLabel);
 
     const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-rule-fields.tsx'), 'utf8');
     expect(source).toContain('let activeReceiverType: string | null = null;');
@@ -329,12 +341,7 @@ describe('AlertNoticeRuleFields', () => {
   it('keeps the empty receiver selector prompt localized without changing the cold selector shell', () => {
     const html = renderToStaticMarkup(
       <AlertNoticeRuleFields
-        t={createTranslatorMock({
-          locale: 'zh-CN',
-          overrides: {
-            'alert.notice.rule.receivers.empty': '暂无接收对象,请先新增接收对象'
-          }
-        })}
+        t={t}
         draft={{
           name: 'Email route',
           receiverIdsText: '',
@@ -348,21 +355,21 @@ describe('AlertNoticeRuleFields', () => {
         }}
         receiverOptions={[]}
         templateOptions={[]}
-        receiverIdsPlaceholder="接收对象 ID,例如 1, 2"
-        templateIdPlaceholder="模板 ID,-1 表示默认模板"
-        labelsPlaceholder="标签匹配,例如 severity:critical"
-        daysPlaceholder="日期范围,例如 1,2,3,4,5"
+        receiverIdsPlaceholder={receiverIdsPlaceholder}
+        templateIdPlaceholder={templateIdPlaceholder}
+        labelsPlaceholder={labelsPlaceholder}
+        daysPlaceholder={daysPlaceholder}
         onDraftChange={vi.fn()}
       />
     );
 
     expect(html).toContain('data-alert-notice-rule-receiver-selector="cold-multi-select"');
     expect(html).toContain('data-testid="notice-rule-field-receiverIdsText"');
-    expect(html).toContain('暂无接收对象,请先新增接收对象');
+    expect(html).toContain(t('alert.notice.rule.receivers.empty'));
 
     const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-rule-fields.tsx'), 'utf8');
     expect(source).toContain("t('alert.notice.rule.receivers.empty')");
-    expect(source).not.toContain('暂无接收对象,请先新增接收对象');
+    expect(source).not.toContain(t('alert.notice.rule.receivers.empty'));
   });
 
   it('does not keep page-local native checkbox, raw id inputs, or time chrome in the notice rule authoring fields', () => {
@@ -370,8 +377,8 @@ describe('AlertNoticeRuleFields', () => {
 
     expect(source).toContain("t('alert.notice.receiver.people')");
     expect(source).toContain("t('common.enable')");
-    expect(source).not.toContain('接收对象');
-    expect(source).not.toContain('启用状态');
+    expect(source).not.toContain(t('alert.notice.receiver.people'));
+    expect(source).not.toContain(t('common.enable'));
     expect(source).toContain("from '../ui/checkbox'");
     expect(source).toContain("from '../ui/date-time-range'");
     expect(source).toContain("from '../ui/label-record-input'");
diff --git a/web-next/components/pages/alert-notice-template-fields.test.tsx b/web-next/components/pages/alert-notice-template-fields.test.tsx
index 64edd8d787..a85f504abd 100644
--- a/web-next/components/pages/alert-notice-template-fields.test.tsx
+++ b/web-next/components/pages/alert-notice-template-fields.test.tsx
@@ -75,12 +75,12 @@ describe('AlertNoticeTemplateFields', () => {
     expect(html).toContain('data-testid="notice-template-field-type"');
     expect(html).toContain('data-testid="notice-template-field-preset"');
     expect(html).toContain('data-testid="notice-template-field-content"');
-    expect(html).toContain('模版类型');
-    expect(html).toContain('用户自定义模版');
-    expect(html).toContain('邮箱');
+    expect(html).toContain(t('alert.notice.template.preset'));
+    expect(html).toContain(t('alert.notice.template.preset.false'));
+    expect(html).toContain(t('alert.notice.type.email'));
     expect(html).toContain('Webhook');
     expect(html).toContain('Telegram');
-    expect(html).not.toContain('Telegram 机器人');
+    expect(html).not.toContain(t('alert.notice.type.telegram-bot'));
     expect(html).toContain('value="9"');
     expect(html.indexOf('value="9"')).toBeLessThan(html.indexOf('value="8"'));
     expect(html.indexOf('value="8"')).toBeLessThan(html.indexOf('value="4"'));
@@ -101,7 +101,7 @@ describe('AlertNoticeTemplateFields', () => {
     expect(source).toContain('data-alert-notice-template-type-required="angular-required-select"');
     expect(source).toContain('data-alert-notice-template-type-required-owner="route-validation-contract"');
     expect(source).not.toContain("['3', 'alert.notice.type.wechat'");
-    expect(source).not.toContain("['0', 'alert.notice.type.sms', '短信']");
+    expect(source).not.toMatch(/\['0', 'alert\.notice\.type\.sms', '\u77ed\u4fe1'\]/);
     expect(source).toContain('data-alert-notice-template-type-selector="cold-select"');
     expect(source).toContain('data-alert-notice-template-preset-view="readonly-type-pill"');
     expect(source).toContain("draft.preset ? t('alert.notice.template.preset.true') : t('alert.notice.template.preset.false')");
@@ -132,7 +132,7 @@ describe('AlertNoticeTemplateFields', () => {
     expect(html).toContain('data-alert-notice-template-type-required-owner="route-validation-contract"');
     expect(html).toContain('<select');
     expect(html).toContain('value="" disabled=""');
-    expect(html).toContain('选择通知方式');
+    expect(html).toContain(t('alert.notice.receiver.type.placeholder'));
     expect(html).toContain('value="1"');
   });
 
@@ -145,7 +145,7 @@ describe('AlertNoticeTemplateFields', () => {
           name: 'EmailTemplate',
           type: '1',
           preset: true,
-          content: '系统内置模板正文'
+          content: 'System preset template body'
         }}
         onDraftChange={vi.fn()}
       />
@@ -156,7 +156,7 @@ describe('AlertNoticeTemplateFields', () => {
     expect(html).toContain('data-hz-code-editor-readonly="true"');
     expect(html).toContain('disabled=""');
     expect(html).toContain('data-alert-notice-template-form-row="preset"');
-    expect(html).toContain('系统内置模版');
-    expect(html).toContain('系统内置模板正文');
+    expect(html).toContain(t('alert.notice.template.preset.true'));
+    expect(html).toContain('System preset template body');
   });
 });
diff --git a/web-next/components/pages/alert-setting-create-dialog.test.tsx b/web-next/components/pages/alert-setting-create-dialog.test.tsx
index b27e3d316b..d4475f8396 100644
--- a/web-next/components/pages/alert-setting-create-dialog.test.tsx
+++ b/web-next/components/pages/alert-setting-create-dialog.test.tsx
@@ -45,7 +45,7 @@ describe('AlertSettingCreateDialog', () => {
   const t = createTranslatorMock({ locale: 'zh-CN' });
   const enT = createTranslatorMock({ locale: 'en-US' });
   const draft: AlertSettingCreateDraft = {
-    name: 'CPU 高负载',
+    name: 'CPU high load',
     kind: 'periodic',
     dataType: 'metric',
     datasource: 'promql',
@@ -79,9 +79,9 @@ describe('AlertSettingCreateDialog', () => {
     expect(html).toContain('data-alert-setting-create-option="realtime"');
     expect(html).toContain('data-alert-setting-create-option="periodic"');
     expect(html).toContain('data-alert-setting-create-periodic-disabled="false"');
-    expect(html).toContain('新增阈值');
-    expect(html).toContain('实时阈值');
-    expect(html).toContain('周期阈值');
+    expect(html).toContain(t('alert.setting.create.title.type'));
+    expect(html).toContain(t('alert.setting.create.realtime.title'));
+    expect(html).toContain(t('alert.setting.create.periodic.title'));
   });
 
   it('renders a cold single-column threshold authoring form with shared controls', () => {
@@ -126,9 +126,9 @@ describe('AlertSettingCreateDialog', () => {
     expect(source).toContain("t('alert.setting.validation.name')");
     expect(source).toContain("t('alert.setting.validation.expr')");
     expect(source).toContain("t('alert.setting.validation.template')");
-    expect(source).not.toContain("setValidationMessage('规则名称为必填项')");
-    expect(source).not.toContain("setValidationMessage('阈值表达式为必填项')");
-    expect(source).not.toContain("setValidationMessage('告警内容为必填项')");
+    expect(source).not.toMatch(/setValidationMessage\('\u89c4\u5219\u540d\u79f0\u4e3a\u5fc5\u586b\u9879'\)/);
+    expect(source).not.toMatch(/setValidationMessage\('\u9608\u503c\u8868\u8fbe\u5f0f\u4e3a\u5fc5\u586b\u9879'\)/);
+    expect(source).not.toMatch(/setValidationMessage\('\u544a\u8b66\u5185\u5bb9\u4e3a\u5fc5\u586b\u9879'\)/);
     expect(source).not.toContain('<textarea');
     expect(source).not.toContain('router.push');
   });
@@ -184,7 +184,7 @@ describe('AlertSettingCreateDialog', () => {
 
     expect(html).toContain('data-alert-setting-editor-return="evidence-context"');
     expect(html).toContain('href="/trace/manage?traceId=trace-123"');
-    expect(html).toContain('返回排障上下文');
+    expect(html).toContain(t('alert.rule.evidence.return'));
   });
 
   it('localizes visible threshold authoring copy outside zh-CN', () => {
@@ -232,7 +232,7 @@ describe('AlertSettingCreateDialog', () => {
     expect(authoringHtml).toContain('Threshold expression');
     expect(authoringHtml).toContain('Alert content');
     expect(authoringHtml).toContain('Return to troubleshooting context');
-    expect(`${typeHtml} ${authoringHtml}`).not.toMatch(/[一-龥]/);
+    expect(`${typeHtml} ${authoringHtml}`).not.toMatch(/[\u4e00-\u9fff]/);
     expect(`${typeHtml} ${authoringHtml}`).not.toContain('alert.setting.create');
   });
 
@@ -242,7 +242,7 @@ describe('AlertSettingCreateDialog', () => {
       kind: 'periodic',
       dataType: 'trace'
     })).toEqual({
-      name: 'CPU 高负载',
+      name: 'CPU high load',
       type: 'periodic_trace',
       datasource: 'promql',
       expr: 'cpu_usage > 80',
diff --git a/web-next/components/pages/alert-setting-surface.test.tsx b/web-next/components/pages/alert-setting-surface.test.tsx
index 718a4608ff..df48d1bbac 100644
--- a/web-next/components/pages/alert-setting-surface.test.tsx
+++ b/web-next/components/pages/alert-setting-surface.test.tsx
@@ -53,6 +53,9 @@ vi.mock('../ui/button', () => ({
 
 describe('AlertSettingSurface', () => {
   const t = createTranslatorMock({ locale: 'zh-CN' });
+  const alertSettingEvidenceTitle = (signal: 'logs' | 'traces' | 'metrics') =>
+    t('alert.rule.evidence.setting.title', { signal: t(`alert.rule.signal.${signal}`) });
+
   const data = {
     list: {
       content: [
@@ -136,25 +139,24 @@ describe('AlertSettingSurface', () => {
     expect(html).toContain('data-alert-setting-pagination-action="previous"');
     expect(html).toContain('data-alert-setting-pagination-action="next"');
     expect(html).toContain('data-hz-ui="pagination-bar"');
-    expect(html).toContain('导出');
-    expect(html).toContain('导入');
+    expect(html).toContain(t('common.button.export'));
+    expect(html).toContain(t('common.button.import'));
     expect(html).toContain('data-alert-setting-select-all="cold-checkbox"');
     expect(html).toContain('data-alert-setting-row-checkbox="cold-checkbox"');
     expect(html).toContain('data-alert-setting-enable-checkbox="cold-checkbox"');
     expect(html.match(/data-cold-checkbox-owner="cold-checkbox"/g)?.length).toBeGreaterThanOrEqual(3);
-    expect(html).toContain('阈值规则');
-    expect(html).toContain('刷新');
-    expect(html).toContain('新增阈值');
-    expect(html).toContain('批量删除');
-    expect(html).toContain('搜索');
-    expect(html).not.toContain('当前阈值');
-    expect(html).toContain('阈值名称');
-    expect(html).toContain('阈值类型');
-    expect(html).toContain('阈值表达式');
-    expect(html).toContain('告警内容');
-    expect(html).toContain('绑定标签');
+    expect(html).toContain(t('menu.alert.setting'));
+    expect(html).toContain(t('common.refresh'));
+    expect(html).toContain(t('alert.setting.action.new'));
+    expect(html).toContain(t('common.button.delete-batch'));
+    expect(html).toContain(t('common.search'));
+    expect(html).toContain(t('alert.setting.name'));
+    expect(html).toContain(t('alert.setting.type'));
+    expect(html).toContain(t('alert.setting.expr'));
+    expect(html).toContain(t('alert.setting.content'));
+    expect(html).toContain(t('alert.setting.bind-labels'));
     expect(html).toContain('cpu threshold');
-    expect(html).toContain('指标实时');
+    expect(html).toContain(t('alert.setting.type.realtime.metric'));
     expect(html).toContain('severity:warning');
     expect(html).toContain('team:core');
     expect(html).not.toContain('data-workbench-page="true"');
@@ -210,7 +212,7 @@ describe('AlertSettingSurface', () => {
         onDelete={vi.fn()}
         onCheckedIdsChange={vi.fn()}
         pendingActionId="import"
-        actionFeedback={{ tone: 'info', title: '已提交导入 [rules.json]' }}
+        actionFeedback={{ tone: 'info', title: t('common.notify.import-submitted', { taskName: 'rules.json' }) }}
       />
     );
 
@@ -218,7 +220,7 @@ describe('AlertSettingSurface', () => {
     expect(html).toContain('data-alert-setting-action-feedback-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('data-hz-ui="inline-feedback"');
     expect(html).toContain('data-hz-feedback-tone="info"');
-    expect(html).toContain('已提交导入 [rules.json]');
+    expect(html).toContain(t('common.notify.import-submitted', { taskName: 'rules.json' }));
   });
 
   it('renders Angular import success and failure notification markers', () => {
@@ -490,7 +492,7 @@ describe('AlertSettingSurface', () => {
     expect(html).toContain('data-alert-setting-table-shell="cold-dense-table"');
     expect(html).toContain('data-alert-setting-empty-state="cold-table-empty"');
     expect(html).toContain('data-alert-setting-empty-icon="cold-empty-box"');
-    expect(html).toContain('还没有阈值规则');
+    expect(html).toContain(t('alert.setting.empty.title'));
   });
 
   it('shows three-signal evidence context before alert-rule authoring', () => {
@@ -503,13 +505,13 @@ describe('AlertSettingSurface', () => {
         formatTime={() => '2026-04-20 00:20:00'}
         evidenceContext={{
           signal: 'traces',
-          title: '来自链路的阈值上下文',
-          copy: '新建阈值时会预填当前实体、服务、环境和链路标签,返回链接仍指向原排障上下文。',
+          title: alertSettingEvidenceTitle('traces'),
+          copy: t('alert.rule.evidence.setting.copy'),
           labelsText: 'hertzbeat.signal:traces, service.name:checkout',
           returnHref: '/trace/manage?traceId=trace-123',
           rows: [
-            { label: '当前实体', value: 'Checkout API', meta: 'entityId 7' },
-            { label: '链路上下文', value: 'trace-123', meta: 'spanId span-456' }
+            { label: t('signal.context.entity.label'), value: 'Checkout API', meta: 'entityId 7' },
+            { label: t('signal.context.trace.label'), value: 'trace-123', meta: 'spanId span-456' }
           ]
         }}
         onSearchChange={vi.fn()}
@@ -530,12 +532,12 @@ describe('AlertSettingSurface', () => {
     expect(html).toContain('data-alert-setting-evidence-context="signal-route"');
     expect(html).toContain('data-alert-setting-evidence-signal="traces"');
     expect(html).toContain('data-alert-setting-prefill-labels="hertzbeat.signal:traces, service.name:checkout"');
-    expect(html).toContain('来自链路的阈值上下文');
-    expect(html).toContain('新建阈值时会预填当前实体、服务、环境和链路标签,返回链接仍指向原排障上下文。');
+    expect(html).toContain(alertSettingEvidenceTitle('traces'));
+    expect(html).toContain(t('alert.rule.evidence.setting.copy'));
     expect(html).toContain('data-alert-setting-evidence-return="true"');
     expect(html).toContain('href="/trace/manage?traceId=trace-123"');
-    expect(html).toContain('当前实体');
-    expect(html).toContain('链路上下文');
+    expect(html).toContain(t('signal.context.entity.label'));
+    expect(html).toContain(t('signal.context.trace.label'));
   });
 
   it('renders alert setting evidence and row label gaps with the localized empty fallback', () => {
@@ -570,8 +572,8 @@ describe('AlertSettingSurface', () => {
         formatTime={() => '2026-04-20 00:20:00'}
         evidenceContext={{
           signal: 'metrics',
-          title: '来自指标的阈值上下文',
-          copy: '新建阈值时会预填当前实体、服务、环境和指标标签。',
+          title: alertSettingEvidenceTitle('metrics'),
+          copy: t('alert.rule.evidence.setting.copy'),
           labelsText: '',
           rows: []
         }}
@@ -592,8 +594,8 @@ describe('AlertSettingSurface', () => {
 
     expect(html).toContain('data-alert-setting-evidence-labels="localized-fallback"');
     expect(html).toContain('data-alert-setting-empty-labels="localized-fallback"');
-    expect(html).toContain('>无</div>');
-    expect(html).toContain('>无</span>');
+    expect(html).toContain(`>${t('common.none')}</div>`);
+    expect(html).toContain(`>${t('common.none')}</span>`);
     expect(html).not.toContain('>-</div>');
     expect(html).not.toContain('>-</span>');
   });
diff --git a/web-next/components/pages/alert-setting-surface.tsx b/web-next/components/pages/alert-setting-surface.tsx
index ce2292bb2c..9fdaa26c41 100644
--- a/web-next/components/pages/alert-setting-surface.tsx
+++ b/web-next/components/pages/alert-setting-surface.tsx
@@ -327,7 +327,7 @@ export function AlertSettingSurface({
                         <th className="w-[17%] px-3 py-2.5">{t('alert.setting.name')}</th>
                         <th className="w-[12%] px-3 py-2.5">{t('alert.setting.type')}</th>
                         <th className="w-[19%] px-3 py-2.5">{t('alert.setting.expr')}</th>
-                        <th className="w-[16%] px-3 py-2.5">{t('alert.setting.template')}</th>
+                        <th className="w-[16%] px-3 py-2.5">{t('alert.setting.content')}</th>
                         <th className="w-[15%] px-3 py-2.5">{t('alert.setting.bind-labels')}</th>
                         <th className="w-[8%] px-3 py-2.5">{t('common.enable')}</th>
                         <th className="w-[13%] px-3 py-2.5">{t('common.edit-time')}</th>
diff --git a/web-next/components/pages/alert-silence-authoring-fields.test.tsx b/web-next/components/pages/alert-silence-authoring-fields.test.tsx
index 0835d9eab1..1ee21ffd5d 100644
--- a/web-next/components/pages/alert-silence-authoring-fields.test.tsx
+++ b/web-next/components/pages/alert-silence-authoring-fields.test.tsx
@@ -51,10 +51,10 @@ describe('AlertSilenceAuthoringFields', () => {
     expect(html).toContain('data-alert-silence-authoring-layout="single-column-angular-function"');
     expect(html).toContain('data-alert-silence-validation-contract="angular-name-labels-only"');
     expect(html).toContain('Create a silence rule for this entity');
-    expect(html).toContain('策略名称');
-    expect(html).toContain('匹配全部');
-    expect(html).toContain('静默类型');
-    expect(html).toContain('静默时段');
+    expect(html).toContain(t('alert.silence.name'));
+    expect(html).toContain(t('alert.silence.match-all'));
+    expect(html).toContain(t('alert.silence.type'));
+    expect(html).toContain(t('alert.silence.time'));
     expect(html).toContain('data-cold-checkbox-owner="cold-checkbox"');
     expect(html).toContain('data-cold-checkbox-control="native-hidden"');
     expect(html).toContain('data-cold-checkbox-box="indicator"');
diff --git a/web-next/components/pages/alert-silence-surface.test.tsx b/web-next/components/pages/alert-silence-surface.test.tsx
index 7fe7537589..49bd7e755d 100644
--- a/web-next/components/pages/alert-silence-surface.test.tsx
+++ b/web-next/components/pages/alert-silence-surface.test.tsx
@@ -90,6 +90,10 @@ vi.mock('./alert-silence-authoring-fields', () => ({
 
 describe('AlertSilenceSurface', () => {
   const t = createTranslatorMock({ locale: 'zh-CN' });
+  const silenceEvidenceTitle = (signal: 'logs' | 'traces' | 'metrics') =>
+    t('alert.rule.evidence.silence.title', { signal: t(`alert.rule.signal.${signal}`) });
+  const silenceDraftName = (signal: 'logs' | 'traces' | 'metrics', target: string) =>
+    t('alert.rule.evidence.silence.draft-name', { signal: t(`alert.rule.signal.${signal}`), target });
   const data = {
     list: {
       content: [
@@ -127,8 +131,8 @@ describe('AlertSilenceSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'logs',
-          title: '来自日志的静默上下文',
-          copy: '保存或取消后仍可回到原排障上下文。',
+          title: silenceEvidenceTitle('logs'),
+          copy: t('alert.rule.evidence.silence.copy'),
           labelsText: 'hertzbeat.signal:logs, service.name:checkout',
           returnHref: '/log/manage?view=list&traceId=trace-123',
           rows: [],
@@ -190,16 +194,16 @@ describe('AlertSilenceSurface', () => {
     expect(html).toContain('data-alert-silence-row-checkbox="cold-checkbox"');
     expect(html).toContain('data-alert-silence-enable-checkbox="cold-checkbox"');
     expect(html.match(/data-cold-checkbox-owner="cold-checkbox"/g)?.length).toBeGreaterThanOrEqual(3);
-    expect(html).toContain('刷新');
-    expect(html).toContain('新增静默');
-    expect(html).toContain('批量删除');
-    expect(html).toContain('搜索');
-    expect(html).not.toContain('当前静默');
-    expect(html).toContain('策略名称');
-    expect(html).toContain('静默类型');
-    expect(html).toContain('已静默告警数');
+    expect(html).toContain(t('common.refresh'));
+    expect(html).toContain(t('alert.silence.action.new'));
+    expect(html).toContain(t('common.button.delete-batch'));
+    expect(html).toContain(t('common.search'));
+    expect(html).not.toContain(t('alert.silence.selected.empty.title'));
+    expect(html).toContain(t('alert.silence.name'));
+    expect(html).toContain(t('alert.silence.type'));
+    expect(html).toContain(t('alert.silence.times'));
     expect(html).toContain('weekday');
-    expect(html).toContain('周期性静默');
+    expect(html).toContain(t('alert.silence.type.cyc'));
     expect(html).toContain('data-overlay-dialog="true"');
     expect(html).toContain('data-alert-silence-authoring-fields="workspace"');
     expect(html).toContain('data-alert-silence-editor-return="evidence-context"');
@@ -252,7 +256,7 @@ describe('AlertSilenceSurface', () => {
         editorLoading={false}
         editorSaving={false}
         editorMessage={null}
-        editorError="请先选择要删除的监控"
+        editorError={t('common.notify.no-select-delete')}
         draft={{
           name: '',
           enable: true,
@@ -285,7 +289,7 @@ describe('AlertSilenceSurface', () => {
     expect(html).toContain('data-alert-silence-action-feedback="warning"');
     expect(html).toContain('data-alert-silence-action-feedback-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('role="alert"');
-    expect(html).toContain('请先选择要删除的监控');
+    expect(html).toContain(t('common.notify.no-select-delete'));
   });
 
   it('renders Angular silence save failure title/detail through shared feedback', () => {
@@ -487,7 +491,7 @@ describe('AlertSilenceSurface', () => {
     expect(html).toContain('data-alert-silence-compact-canvas="content-height"');
     expect(html).toContain('min-height:auto');
     expect(html).toContain('data-alert-silence-empty-icon="cold-empty-box"');
-    expect(html).toContain('还没有静默规则');
+    expect(html).toContain(t('alert.silence.empty.title'));
   });
 
   it('renders the topology return context when opened from alert impact silence closure', () => {
@@ -520,7 +524,7 @@ describe('AlertSilenceSurface', () => {
           sourceKind: 'database-middleware-connection',
           edgeId: 'svc-checkout--res-orders-db',
           returnTo,
-          returnLabel: 'HertzBeat 企业运维拓扑'
+          returnLabel: t('topology.identity')
         }}
         draft={{
           name: '',
@@ -553,7 +557,7 @@ describe('AlertSilenceSurface', () => {
     expect(html).toContain('data-alert-silence-return-context="topology-edge"');
     expect(html).toContain('data-alert-silence-return-edge-id="svc-checkout--res-orders-db"');
     expect(html).toContain('href="/topology?viewMode=resource-dependency&amp;sourceKind=database-middleware-connection&amp;edgeId=svc-checkout--res-orders-db');
-    expect(html).not.toContain('HertzBeat 企业运维拓扑');
+    expect(html).not.toContain(t('topology.identity'));
     expect(html).not.toContain('returnLabel=');
     expect(html).toContain('checkout-api');
     expect(html).toContain('resource-dependency');
@@ -686,8 +690,8 @@ describe('AlertSilenceSurface', () => {
     expect(html).toContain('data-alert-silence-created-outside-matched="angular-authoring-notice"');
     expect(html).toContain('data-alert-silence-created-outside-matched-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('data-alert-silence-created-outside-matched-action="view-all"');
-    expect(html).toContain('新规则已创建');
-    expect(html).toContain('当前视图不会自动显示这条新规则');
+    expect(html).toContain(t('entity.noise-controls.authoring.created-outside-matched.title'));
+    expect(html).toContain(t('entity.noise-controls.authoring.created-outside-matched.copy'));
   });
 
   it('renders three-signal evidence context before silence authoring', () => {
@@ -705,16 +709,16 @@ describe('AlertSilenceSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'logs',
-          title: '来自日志的静默上下文',
-          copy: '新建静默时会按当前实体、服务、环境和链路标签做匹配。',
+          title: silenceEvidenceTitle('logs'),
+          copy: t('alert.rule.evidence.silence.copy'),
           labelsText: 'hertzbeat.signal:logs, service.name:checkout, trace_id:trace-123',
           returnHref: '/log/manage?view=list&traceId=trace-123',
           rows: [
-            { label: '当前实体', value: 'checkout', meta: 'entityId service:commerce/checkout' },
-            { label: '链路上下文', value: 'trace-123', meta: 'spanId span-456' }
+            { label: t('signal.context.entity.label'), value: 'checkout', meta: 'entityId service:commerce/checkout' },
+            { label: t('signal.context.trace.label'), value: 'trace-123', meta: 'spanId span-456' }
           ],
           draftPatch: {
-            name: '日志 checkout 静默',
+            name: silenceDraftName('logs', 'checkout'),
             matchAll: false,
             labelsText: 'hertzbeat.signal:logs, service.name:checkout, trace_id:trace-123'
           }
@@ -750,11 +754,11 @@ describe('AlertSilenceSurface', () => {
     expect(html).toContain('data-alert-silence-evidence-context="signal-route"');
     expect(html).toContain('data-alert-silence-evidence-signal="logs"');
     expect(html).toContain('data-alert-silence-prefill-labels="hertzbeat.signal:logs, service.name:checkout, trace_id:trace-123"');
-    expect(html).toContain('来自日志的静默上下文');
-    expect(html).toContain('返回排障上下文');
+    expect(html).toContain(silenceEvidenceTitle('logs'));
+    expect(html).toContain(t('alert.rule.evidence.return'));
     expect(html).toContain('href="/log/manage?view=list&amp;traceId=trace-123"');
-    expect(html).toContain('当前实体');
-    expect(html).toContain('链路上下文');
+    expect(html).toContain(t('signal.context.entity.label'));
+    expect(html).toContain(t('signal.context.trace.label'));
   });
 
   it('renders Angular entity alert prefill copy inside the silence editor', () => {
@@ -814,9 +818,9 @@ describe('AlertSilenceSurface', () => {
     );
 
     expect(html).toContain('data-alert-silence-entity-prefill="angular-alert-common-labels"');
-    expect(html).toContain('data-alert-silence-authoring-prefill-title="为当前实体创建静默规则"');
-    expect(html).toContain('data-alert-silence-authoring-prefill-copy="已提取当前实体可见告警的共享标签,作为静默条件默认值。"');
-    expect(html).not.toContain('data-alert-silence-authoring-prefill-warning="当前没有稳定的共享告警标签,请手动填写静默条件。"');
+    expect(html).toContain(`data-alert-silence-authoring-prefill-title="${t('entity.noise-controls.authoring.silence.title')}"`);
+    expect(html).toContain(`data-alert-silence-authoring-prefill-copy="${t('entity.noise-controls.authoring.silence.prefill-success')}"`);
+    expect(html).not.toContain(`data-alert-silence-authoring-prefill-warning="${t('entity.noise-controls.authoring.silence.prefill-warning')}"`);
   });
 
   it('renders missing silence evidence labels with the localized empty fallback', () => {
@@ -834,8 +838,8 @@ describe('AlertSilenceSurface', () => {
         editorError={null}
         evidenceContext={{
           signal: 'metrics',
-          title: '来自指标的静默上下文',
-          copy: '按当前实体创建静默规则。',
+          title: silenceEvidenceTitle('metrics'),
+          copy: t('alert.rule.evidence.silence.copy'),
           labelsText: '',
           returnHref: '/ingestion/otlp/metrics?entityId=service-1',
           rows: [],
@@ -873,7 +877,7 @@ describe('AlertSilenceSurface', () => {
 
     expect(html).toContain('data-alert-silence-evidence-context="signal-route"');
     expect(html).toContain('data-alert-silence-evidence-labels="localized-fallback"');
-    expect(html).toContain('无');
+    expect(html).toContain(t('common.none'));
   });
 
   it('renders validation errors visibly inside the silence editor dialog', () => {
@@ -888,7 +892,7 @@ describe('AlertSilenceSurface', () => {
         editorLoading={false}
         editorSaving={false}
         editorMessage={null}
-        editorError="请填写策略名称"
+        editorError={t('alert.silence.validation.name')}
         draft={{
           name: '',
           enable: true,
@@ -919,6 +923,6 @@ describe('AlertSilenceSurface', () => {
 
     expect(html).toContain('data-alert-silence-editor-error-inline="cold-validation"');
     expect(html).toContain('role="alert"');
-    expect(html).toContain('请填写策略名称');
+    expect(html).toContain(t('alert.silence.validation.name'));
   });
 });
diff --git a/web-next/components/pages/alert-surface-primitives.test.tsx b/web-next/components/pages/alert-surface-primitives.test.tsx
index 34f26e92aa..1f64e255bf 100644
--- a/web-next/components/pages/alert-surface-primitives.test.tsx
+++ b/web-next/components/pages/alert-surface-primitives.test.tsx
@@ -23,7 +23,7 @@ describe('alert surface primitives', () => {
           <AlertSurfaceTable>
             <AlertSurfaceTableHead>
               <tr>
-                <th>策略名称</th>
+                <th>Rule name</th>
               </tr>
             </AlertSurfaceTableHead>
             <tbody>
@@ -33,9 +33,9 @@ describe('alert surface primitives', () => {
             </tbody>
           </AlertSurfaceTable>
         </AlertSurfaceTableShell>
-        <AlertSurfaceActionButton>搜索</AlertSurfaceActionButton>
-        <AlertSurfaceActionLink href="/alert/silence">查看</AlertSurfaceActionLink>
-        <AlertSurfaceValuePill>一次性静默</AlertSurfaceValuePill>
+        <AlertSurfaceActionButton>Search</AlertSurfaceActionButton>
+        <AlertSurfaceActionLink href="/alert/silence">View</AlertSurfaceActionLink>
+        <AlertSurfaceValuePill>One-time silence</AlertSurfaceValuePill>
       </div>
     );
 
diff --git a/web-next/components/pages/bulletin-center-surface.test.tsx b/web-next/components/pages/bulletin-center-surface.test.tsx
index faf20bc389..56ebcf001d 100644
--- a/web-next/components/pages/bulletin-center-surface.test.tsx
+++ b/web-next/components/pages/bulletin-center-surface.test.tsx
@@ -61,6 +61,7 @@ vi.mock('../ui/checkbox', () => ({
 
 describe('bulletin center surface', () => {
   it('renders the shared HertzBeat toolbar, tabs, and metrics desk shell', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const html = renderToStaticMarkup(
       <BulletinCenterSurface
         data={{
@@ -80,15 +81,16 @@ describe('bulletin center surface', () => {
     expect(html).toContain('data-bulletin-center-surface="true"');
     expect(html).toContain('data-action-toolbar="true"');
     expect(html).toContain('data-bulletin-tabs="true"');
-    expect(html).toContain('aria-label="公告导航"');
+    expect(html).toContain(`aria-label="${t('bulletin.navigation.aria')}"`);
     expect(html).toContain('data-bulletin-metrics-table="true"');
     expect(html).toContain('Ops board');
     expect(html).toContain('DB board');
-    expect(html).toContain('刷新');
-    expect(html).toContain('新增');
+    expect(html).toContain(t('common.refresh'));
+    expect(html).toContain(t('common.button.new'));
   });
 
   it('routes current bulletin deletion through a cold modal instead of native confirm', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const source = readFileSync(resolve(process.cwd(), 'components/pages/bulletin-center-surface.tsx'), 'utf8');
 
     expect(source).not.toContain('window.confirm');
@@ -101,8 +103,8 @@ describe('bulletin center surface', () => {
     expect(source).toContain("t('bulletin.delete.copy')");
     expect(source).toContain("t('bulletin.delete.confirm')");
     expect(source).toContain("t('common.cancel')");
-    expect(source).not.toContain('确认删除公告');
-    expect(source).not.toContain('删除后该公告看板会从当前工作台移除');
+    expect(source).not.toContain(t('bulletin.delete.title'));
+    expect(source).not.toContain(t('bulletin.delete.copy'));
   });
 
   it('trims blank current bulletin names to the localized empty fallback', () => {
diff --git a/web-next/components/pages/bulletin-manage-dialog.test.tsx b/web-next/components/pages/bulletin-manage-dialog.test.tsx
index 57f54ec524..f9526f28c6 100644
--- a/web-next/components/pages/bulletin-manage-dialog.test.tsx
+++ b/web-next/components/pages/bulletin-manage-dialog.test.tsx
@@ -45,15 +45,15 @@ describe('BulletinManageDialog', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/bulletin-manage-dialog.tsx'), 'utf8');
     const t = createTranslatorMock({
       overrides: {
-        'menu.monitor.bulletin': '公告',
-        'bulletin.new': '新增公告',
-        'bulletin.name': '公告名称',
-        'bulletin.monitor.type': '监控类型',
-        'bulletin.monitor.name': '监控对象',
-        'bulletin.monitor.metrics': '监控指标',
-        'common.cancel': '取消',
-        'common.save': '保存',
-        'common.saving': '保存中'
+        'menu.monitor.bulletin': 'Bulletin',
+        'bulletin.new': 'New bulletin',
+        'bulletin.name': 'Bulletin name',
+        'bulletin.monitor.type': 'Monitor type',
+        'bulletin.monitor.name': 'Monitor object',
+        'bulletin.monitor.metrics': 'Monitor metrics',
+        'common.cancel': 'Cancel',
+        'common.save': 'Save',
+        'common.saving': 'Saving'
       }
     });
 
diff --git a/web-next/components/pages/bulletin-metrics-table.test.tsx b/web-next/components/pages/bulletin-metrics-table.test.tsx
index 0d5cf4d107..7401ec29ac 100644
--- a/web-next/components/pages/bulletin-metrics-table.test.tsx
+++ b/web-next/components/pages/bulletin-metrics-table.test.tsx
@@ -41,9 +41,10 @@ describe('BulletinMetricsTable', () => {
   });
 
   it('renders missing metric values with the localized empty fallback', () => {
+    const emptyMetricValue = 'No metric value';
     const t = createTranslatorMock({
       overrides: {
-        'common.none': '无指标值'
+        'common.none': emptyMetricValue
       }
     });
     const html = renderToStaticMarkup(
@@ -77,14 +78,15 @@ describe('BulletinMetricsTable', () => {
     );
 
     expect(html).toContain('Cart probe');
-    expect(html).toContain('无指标值');
+    expect(html).toContain(emptyMetricValue);
     expect(t).toHaveBeenCalledWith('common.none');
   });
 
   it('renders missing monitor identity with the localized empty fallback', () => {
+    const missingMonitorIdentity = 'No monitor identity';
     const t = createTranslatorMock({
       overrides: {
-        'common.none': '无监控身份'
+        'common.none': missingMonitorIdentity
       }
     });
     const html = renderToStaticMarkup(
@@ -118,7 +120,7 @@ describe('BulletinMetricsTable', () => {
     );
 
     expect(html).toContain('Checkout probe');
-    expect((html.match(/无监控身份/g) ?? []).length).toBeGreaterThanOrEqual(2);
+    expect((html.match(new RegExp(missingMonitorIdentity, 'g')) ?? []).length).toBeGreaterThanOrEqual(2);
     expect(t).toHaveBeenCalledWith('common.none');
   });
 });
diff --git a/web-next/components/pages/collector-manage-surface.test.tsx b/web-next/components/pages/collector-manage-surface.test.tsx
index 1fea926bc8..513a392480 100644
--- a/web-next/components/pages/collector-manage-surface.test.tsx
+++ b/web-next/components/pages/collector-manage-surface.test.tsx
@@ -8,34 +8,7 @@ import { CollectorManageSurface } from './collector-manage-surface';
 
 describe('collector manage surface', () => {
   it('renders the cold-matte collector toolbar and dense table shell', () => {
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.collector': '采集集群',
-        'collector.deploy': '部署采集器',
-        'collector.online': '上线采集器',
-        'collector.offline': '下线采集器',
-        'collector.delete': '删除采集器',
-        'collector.name': '采集器名称',
-        'collector.status': '运行状态',
-        'collector.mode': '运行模式',
-        'collector.task': '总任务数量',
-        'collector.pinned': '固定任务',
-        'collector.dispatched': '调度任务',
-        'collector.ip': 'IP地址',
-        'collector.version': '版本',
-        'collector.start-time': '启动时间',
-        'collector.mode.public': '公共集群模式',
-        'collector.mode.private': '私有云边模式',
-        'monitor.collector.status.online': '在线',
-        'monitor.collector.status.offline': '离线',
-        'common.refresh': '刷新',
-        'common.search': '搜索',
-        'common.edit': '操作',
-        'common.total': 'Total',
-        'common.current-page-count': 'Current page'
-      }
-    });
+    const t = createTranslatorMock({ locale: 'zh-CN' });
 
     const html = renderToStaticMarkup(
       <CollectorManageSurface
@@ -192,28 +165,28 @@ describe('collector manage surface', () => {
     expect(html).toContain('data-collector-status-tone="danger"');
     expect(html).not.toContain('data-cold-search-input-shell');
     expect(html).not.toContain('data-collector-summary-rail=');
-    expect(html).toContain('刷新');
-    expect(html).toContain('部署采集器');
-    expect(html).toContain('上线采集器');
-    expect(html).toContain('下线采集器');
-    expect(html).toContain('删除采集器');
-    expect(html).toContain('采集器 1 / 2 在线');
-    expect(html).toContain('任务 11 · 离线 1');
-    expect(html).toContain('最近上报 2026-04-10 18:00:00');
-    expect(html).toContain('第 2 / 3 页 · 9-10 / 18');
-    expect(html).toContain('每页条数');
-    expect(html).toContain('页码');
-    expect(html).toContain('搜索');
-    expect(html).toContain('采集集群');
-    expect(html).toContain('采集器名称');
-    expect(html).toContain('运行状态');
-    expect(html).toContain('运行模式');
-    expect(html).toContain('总任务数量');
-    expect(html).toContain('固定任务');
-    expect(html).toContain('调度任务');
-    expect(html).toContain('IP地址');
-    expect(html).toContain('版本');
-    expect(html).toContain('启动时间');
+    expect(html).toContain(t('common.refresh'));
+    expect(html).toContain(t('collector.deploy'));
+    expect(html).toContain(t('collector.online'));
+    expect(html).toContain(t('collector.offline'));
+    expect(html).toContain(t('collector.delete'));
+    expect(html).toContain(t('collector.health.cluster.copy', { online: 1, total: 2 }));
+    expect(html).toContain(t('collector.health.cluster.meta', { tasks: 11, offline: 1 }));
+    expect(html).toContain(t('collector.health.cluster.freshness', { time: '2026-04-10 18:00:00' }));
+    expect(html).toContain(t('collector.pagination.summary', { page: 2, totalPages: 3, from: 9, to: 10, total: 18 }));
+    expect(html).toContain(t('collector.pagination.page-size'));
+    expect(html).toContain(t('collector.pagination.page'));
+    expect(html).toContain(t('common.search'));
+    expect(html).toContain(t('menu.advanced.collector'));
+    expect(html).toContain(t('collector.name'));
+    expect(html).toContain(t('collector.status'));
+    expect(html).toContain(t('collector.mode'));
+    expect(html).toContain(t('collector.task'));
+    expect(html).toContain(t('collector.pinned'));
+    expect(html).toContain(t('collector.dispatched'));
+    expect(html).toContain(t('collector.ip'));
+    expect(html).toContain(t('collector.version'));
+    expect(html).toContain(t('collector.start-time'));
     expect(html).toContain('edge-a');
     expect(html).toContain('10.0.0.1');
     expect(html).toContain('1.0.0');
@@ -512,7 +485,7 @@ describe('collector manage surface', () => {
         }}
         search=""
         selectedCollectors={['edge-a']}
-        actionError="请先选择要删除的监控"
+        actionError={t('common.notify.no-select-delete')}
         actionTone="warning"
         deleteTarget={{
           collectors: ['edge-a'],
@@ -562,8 +535,8 @@ describe('collector manage surface', () => {
     expect(html).toContain('data-collector-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
     expect(html).toContain('data-collector-row-delete-menu-panel-open="false"');
     expect(html).toContain('data-collector-delete-one="edge-a"');
-    expect(html).toContain('请先选择要删除的监控');
-    expect(html).toContain('请确认是否删除!');
+    expect(html).toContain(t('common.notify.no-select-delete'));
+    expect(html).toContain(t('common.confirm.delete'));
   });
 
   it('renders Angular collector delete and operate failure titles with backend detail metadata', () => {
@@ -581,7 +554,7 @@ describe('collector manage surface', () => {
           }
         }}
         search=""
-        actionError="删除失败!"
+        actionError={t('common.notify.delete-fail')}
         actionMeta="backend delete refused"
         actionTone="critical"
         actionFeedbackContract="angular-delete-fail"
@@ -603,7 +576,7 @@ describe('collector manage surface', () => {
     expect(deleteFailureHtml).toContain('data-collector-delete-feedback="angular-delete-fail"');
     expect(deleteFailureHtml).toContain('data-collector-delete-feedback-title="common.notify.delete-fail"');
     expect(deleteFailureHtml).toContain('data-collector-delete-feedback-detail="backend-message"');
-    expect(deleteFailureHtml).toContain('删除失败!');
+    expect(deleteFailureHtml).toContain(t('common.notify.delete-fail'));
     expect(deleteFailureHtml).toContain('backend delete refused');
 
     const operateFailureHtml = renderToStaticMarkup(
@@ -618,7 +591,7 @@ describe('collector manage surface', () => {
           }
         }}
         search=""
-        actionError="操作失败!"
+        actionError={t('common.notify.operate-fail')}
         actionMeta="backend operate refused"
         actionTone="critical"
         actionFeedbackContract="angular-operate-fail"
@@ -640,7 +613,7 @@ describe('collector manage surface', () => {
     expect(operateFailureHtml).toContain('data-collector-operate-feedback="angular-operate-fail"');
     expect(operateFailureHtml).toContain('data-collector-operate-feedback-title="common.notify.operate-fail"');
     expect(operateFailureHtml).toContain('data-collector-operate-feedback-detail="backend-message"');
-    expect(operateFailureHtml).toContain('操作失败!');
+    expect(operateFailureHtml).toContain(t('common.notify.operate-fail'));
     expect(operateFailureHtml).toContain('backend operate refused');
   });
 
@@ -673,7 +646,7 @@ describe('collector manage surface', () => {
         }}
         search=""
         selectedCollectors={['edge-a']}
-        actionError="未选中任何待下线项!"
+        actionError={t('collector.notify.no-select-offline')}
         actionTone="warning"
         actionFeedbackContract="angular-no-select-offline"
         deleteTarget={{
@@ -717,8 +690,8 @@ describe('collector manage surface', () => {
     expect(html).toContain('aria-busy="true"');
     expect(html).toContain('data-collector-delete-confirm-target="edge-a"');
     expect(html).toContain('data-collector-offline-one="edge-a"');
-    expect(html).toContain('未选中任何待下线项!');
-    expect(html).toContain('请确认是否下线此采集器!');
+    expect(html).toContain(t('collector.notify.no-select-offline'));
+    expect(html).toContain(t('collector.confirm.offline'));
   });
 
   it('keeps the Angular header select-all batch payload while the default row stays visually disabled', () => {
@@ -801,7 +774,7 @@ describe('collector manage surface', () => {
         deployHost="10.0.0.10"
         deployDockerShell="$ docker run -d -e IDENTITY=identity-123"
         deployPackageShell="identity: identity-123\nmanager-host: 10.0.0.10"
-        deployCopyMessage="复制成功!"
+        deployCopyMessage={t('common.notify.copy-success')}
         formatTime={() => '2026-04-10 18:00:00'}
         onSearchChange={() => {}}
         onSearch={() => {}}
@@ -857,8 +830,8 @@ describe('collector manage surface', () => {
     expect(html).toContain('target="_blank"');
     expect(html).toContain('identity-123');
     expect(html).toContain('10.0.0.10');
-    expect(html).toContain('通过 Docker 方式部署');
-    expect(html).toContain('通过安装包部署');
+    expect(html).toContain(t('collector.deploy.docker'));
+    expect(html).toContain(t('collector.deploy.package'));
   });
 
   it('keeps collector deploy required-name validation submit-triggered like Angular', () => {
@@ -926,7 +899,7 @@ describe('collector manage surface', () => {
     expect(dirtyHtml).toContain('data-collector-deploy-name-validation-state="dirty-invalid"');
     expect(dirtyHtml).toContain('data-collector-deploy-name-validation="validation.required"');
     expect(dirtyHtml).toContain('data-collector-deploy-name-validation-contract="angular-submit-marks-required"');
-    expect(dirtyHtml).toContain('请填充必填项!');
+    expect(dirtyHtml).toContain(t('validation.required').trim());
   });
 
   it('keeps collector deploy OK loading visible while identity generation is pending', () => {
@@ -963,7 +936,7 @@ describe('collector manage surface', () => {
     expect(html).toContain('data-collector-deploy-ok-loading-owner="angular-nz-ok-loading"');
     expect(html).toContain('aria-busy="true"');
     expect(html).toContain('disabled=""');
-    expect(html).toContain('加载中');
+    expect(html).toContain(t('common.loading'));
   });
 
   it('keeps collector deploy generate failures as Angular apply-fail notifications', () => {
diff --git a/web-next/components/pages/entity-definition-workspace-surface.test.tsx b/web-next/components/pages/entity-definition-workspace-surface.test.tsx
index 0665846e38..29cced637d 100644
--- a/web-next/components/pages/entity-definition-workspace-surface.test.tsx
+++ b/web-next/components/pages/entity-definition-workspace-surface.test.tsx
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 import { getImportStarterDraft } from './entity-definition-workspace-surface';
 
@@ -10,65 +11,7 @@ vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
       locale: 'zh-CN',
-      overrides: {
-        'entities.definition.workspace.kicker': '实体定义',
-        'entities.definition.workspace.title': '编辑实体定义',
-        'entities.definition.workspace.subtitle': '直接编辑当前定义。保存后会同步更新基础信息、遥测绑定和依赖。',
-        'entities.definition.workspace.format-help': '支持 YAML、JSON 和 cURL 请求内容,保存前可先预览解析结果。',
-        'entities.definition.workspace.attribution-check': '归因检查',
-        'entities.definition.workspace.attribution-state.ready': '已就绪',
-        'entities.definition.workspace.attribution-state.missing': '需补齐',
-        'entities.definition.workspace.attribution-state.unknown': '待确认',
-        'entities.definition.workspace.metric.ready': '可直接导入',
-        'entities.definition.workspace.metric.attention': '待完善',
-        'entities.definition.workspace.metric.telemetry-gaps': '缺少遥测绑定',
-        'entities.definition.workspace.current-definition': '当前定义',
-        'entities.definition.workspace.template-title': '自定义模板',
-        'entities.definition.workspace.template-error-help': '定义加载失败后可先确认实体是否存在,再继续编辑。',
-        'entities.definition.workspace.template-empty-title': '暂无模板',
-        'entities.definition.workspace.template-empty-copy': '可复用模板会显示在这里。',
-        'entities.definition.workspace.batch-title': '模板与批量编辑',
-        'entities.definition.workspace.batch-copy': '查看模板、草稿来源和可选操作。',
-        'entities.definition.workspace.batch-expand': '展开',
-        'entities.definition.workspace.activity-title': '最近活动',
-        'entities.definition.workspace.activity-empty-title': '暂无最近活动',
-        'entities.definition.workspace.activity-empty-copy': '预览和导入活动会显示在这里。',
-        'entities.definition.workspace.entry-title': '入口',
-        'entities.definition.workspace.entry-view-entity': '查看实体',
-        'entities.definition.workspace.entry-back-to-form': '回到表单',
-        'entities.definition.message.load-failed': '加载实体定义失败。',
-        'entities.definition.message.entity-not-exist': '实体不存在。',
-        'entities.definition.message.process-failed': '实体定义处理失败。',
-        'entities.definition.message.update-missing-entity': '无法确认要更新的实体定义。',
-        'entities.definition.message.update-failed': '实体定义保存失败。',
-        'entities.definition.message.updated': '实体定义已更新。',
-        'entities.definition.message.backend-fallback': '后端返回:{{message}}',
-        'entity.definition.import.empty-preview': '请先粘贴定义,再预览。',
-        'entity.definition.import.empty-save': '请先粘贴或加载定义,再保存。',
-        'entity.definition.import.empty-import': '请先粘贴定义,再导入。',
-        'entity.definition.import.activity.preview-one': '已预览 1 个定义。',
-        'entity.definition.import.activity.preview-many': '已预览 {{count}} 个定义。',
-        'entity.definition.import.parse-failed': '定义解析失败。',
-        'entity.definition.workspace.single-required': '编辑已有实体时一次只能保存一个定义文档。',
-        'entity.definition.import.blocked': '部分定义仍需处理,请检查后再导入。',
-        'entity.definition.import.success-bundle': '已导入 {{count}} 个实体。',
-        'entity.definition.import.success-single': '实体定义已导入。',
-        'entity.definition.import.create-failed': '实体导入失败。',
-        'entity.definition.import.placeholder.yaml': '粘贴 YAML 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。',
-        'entity.definition.import.placeholder.json': '粘贴 JSON 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。',
-        'entity.definition.import.placeholder.curl': '粘贴从预览或自动化流程生成的 cURL 请求。',
-        'entity.definition.format.label': '导入格式',
-        'entity.definition.format.option.yaml': 'YAML 格式',
-        'entity.definition.format.option.json': 'JSON 格式',
-        'entity.definition.format.option.curl': 'cURL 格式',
-        'entity.definition.action.clear-draft': '清空草稿',
-        'entity.definition.action.preview': '预览定义',
-        'entity.definition.action.save': '保存定义',
-        'entity.definition.action.import': '导入实体',
-        'entities.import.workspace.title': '导入实体定义',
-        'entities.import.workspace.subtitle': '从 YAML、JSON 或 cURL 请求内容导入一个或多个实体定义,先预览再写入目录。',
-        'entities.import.workspace.format-help': '支持 YAML、JSON 和从预览或自动化流程生成的 cURL。导入前会先解析实体基础信息、遥测绑定和依赖关系。'
-      }
+      overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] ?? {}
     })
   })
 }));
@@ -182,6 +125,12 @@ vi.mock('@/lib/utils', () => ({
 }));
 
 describe('EntityDefinitionWorkspaceSurface', () => {
+  const t = createTranslatorMock({
+    locale: 'zh-CN',
+    overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] ?? {}
+  });
+  const han = (...codes: number[]) => String.fromCodePoint(...codes);
+
   it('moves definition queue selectors and preview cards onto shared inset workbench panels', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/entity-definition-workspace-surface.tsx'), 'utf8');
 
@@ -193,10 +142,10 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(source).toContain("t('entities.definition.workspace.attribution-state.ready')");
     expect(source).toContain("t('entities.definition.workspace.attribution-state.missing')");
     expect(source).toContain("t('entities.definition.workspace.attribution-state.unknown')");
-    expect(source).not.toContain('>归因检查<');
-    expect(source).not.toContain("return '已就绪'");
-    expect(source).not.toContain("return '需补齐'");
-    expect(source).not.toContain("return '待确认'");
+    expect(source).not.toContain(`>${t('entities.definition.workspace.attribution-check')}<`);
+    expect(source).not.toContain(`return '${t('entities.definition.workspace.attribution-state.ready')}'`);
+    expect(source).not.toContain(`return '${t('entities.definition.workspace.attribution-state.missing')}'`);
+    expect(source).not.toContain(`return '${t('entities.definition.workspace.attribution-state.unknown')}'`);
     expect(source).toContain('row.attributionLabel');
     expect(source).toContain('row.attributionRows.map');
     expect(source).toContain("t('entities.definition.workspace.metric.ready')");
@@ -237,41 +186,41 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(source).toContain("t('entity.definition.import.placeholder.json')");
     expect(source).toContain("t('entity.definition.import.placeholder.curl')");
     expect(source).toContain('localizeEntityDefinitionMessage(error.message, t)');
-    expect(source).not.toContain("label: '可直接导入'");
-    expect(source).not.toContain("label: '待完善'");
-    expect(source).not.toContain("label: '缺少遥测绑定'");
-    expect(source).not.toContain('>当前定义<');
-    expect(source).not.toContain('>自定义模板<');
-    expect(source).not.toContain('定义加载失败后可先确认实体是否存在,再继续编辑。');
-    expect(source).not.toContain("title: '暂无模板'");
-    expect(source).not.toContain("copy: '可复用模板会显示在这里。'");
-    expect(source).not.toContain('>模板与批量编辑<');
-    expect(source).not.toContain('查看模板、草稿来源和可选操作。');
-    expect(source).not.toContain('>展开<');
-    expect(source).not.toContain('>最近活动<');
-    expect(source).not.toContain("title: '暂无最近活动'");
-    expect(source).not.toContain("copy: '预览和导入活动会显示在这里。'");
-    expect(source).not.toContain('>入口<');
-    expect(source).not.toContain('>查看实体<');
-    expect(source).not.toContain('>回到表单<');
-    expect(source).not.toContain('加载实体定义失败。');
-    expect(source).not.toContain('请先粘贴定义,再预览。');
-    expect(source).not.toContain('已预览 1 个定义。');
-    expect(source).not.toContain('定义解析失败。');
-    expect(source).not.toContain('请先粘贴或加载定义,再保存。');
-    expect(source).not.toContain('请先粘贴定义,再导入。');
-    expect(source).not.toContain('编辑已有实体时一次只能保存一个定义文档。');
-    expect(source).not.toContain('无法确认要更新的实体定义。');
-    expect(source).not.toContain('实体定义已更新。');
-    expect(source).not.toContain('实体定义保存失败。');
-    expect(source).not.toContain('部分定义仍需处理,请检查后再导入。');
-    expect(source).not.toContain('实体定义已导入。');
-    expect(source).not.toContain('实体导入失败。');
-    expect(source).not.toContain('实体不存在。');
-    expect(source).not.toContain('实体定义处理失败。');
-    expect(source).not.toContain('粘贴 YAML 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。');
-    expect(source).not.toContain('粘贴 JSON 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。');
-    expect(source).not.toContain('粘贴从预览或自动化流程生成的 cURL 请求。');
+    expect(source).not.toContain(`label: '${t('entities.definition.workspace.metric.ready')}'`);
+    expect(source).not.toContain(`label: '${t('entities.definition.workspace.metric.attention')}'`);
+    expect(source).not.toContain(`label: '${t('entities.definition.workspace.metric.telemetry-gaps')}'`);
+    expect(source).not.toContain(`>${t('entities.definition.workspace.current-definition')}<`);
+    expect(source).not.toContain(`>${t('entities.definition.workspace.template-title')}<`);
+    expect(source).not.toContain(t('entities.definition.workspace.template-error-help'));
+    expect(source).not.toContain(`title: '${t('entities.definition.workspace.template-empty-title')}'`);
+    expect(source).not.toContain(`copy: '${t('entities.definition.workspace.template-empty-copy')}'`);
+    expect(source).not.toContain(`>${t('entities.definition.workspace.batch-title')}<`);
+    expect(source).not.toContain(t('entities.definition.workspace.batch-copy'));
+    expect(source).not.toContain(`>${t('entities.definition.workspace.batch-expand')}<`);
+    expect(source).not.toContain(`>${t('entities.definition.workspace.activity-title')}<`);
+    expect(source).not.toContain(`title: '${t('entities.definition.workspace.activity-empty-title')}'`);
+    expect(source).not.toContain(`copy: '${t('entities.definition.workspace.activity-empty-copy')}'`);
+    expect(source).not.toContain(`>${t('entities.definition.workspace.entry-title')}<`);
+    expect(source).not.toContain(`>${t('entities.definition.workspace.entry-view-entity')}<`);
+    expect(source).not.toContain(`>${t('entities.definition.workspace.entry-back-to-form')}<`);
+    expect(source).not.toContain(t('entities.definition.message.load-failed'));
+    expect(source).not.toContain(t('entity.definition.import.empty-preview'));
+    expect(source).not.toContain(t('entity.definition.import.activity.preview-one'));
+    expect(source).not.toContain(t('entity.definition.import.parse-failed'));
+    expect(source).not.toContain(t('entity.definition.import.empty-save'));
+    expect(source).not.toContain(t('entity.definition.import.empty-import'));
+    expect(source).not.toContain(t('entity.definition.workspace.single-required'));
+    expect(source).not.toContain(t('entities.definition.message.update-missing-entity'));
+    expect(source).not.toContain(t('entities.definition.message.updated'));
+    expect(source).not.toContain(t('entities.definition.message.update-failed'));
+    expect(source).not.toContain(t('entity.definition.import.blocked'));
+    expect(source).not.toContain(t('entity.definition.import.success-single'));
+    expect(source).not.toContain(t('entity.definition.import.create-failed'));
+    expect(source).not.toContain(t('entities.definition.message.entity-not-exist'));
+    expect(source).not.toContain(t('entities.definition.message.process-failed'));
+    expect(source).not.toContain(t('entity.definition.import.placeholder.yaml'));
+    expect(source).not.toContain(t('entity.definition.import.placeholder.json'));
+    expect(source).not.toContain(t('entity.definition.import.placeholder.curl'));
     expect(source).not.toContain('rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] p-3.5 text-left text-[var(--ops-text-secondary)] transition-colors hover:border-[var(--ops-primary)] hover:bg-[var(--ops-surface-hover)] hover:text-[var(--ops-text-primary)]');
     expect(source).not.toContain('rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] p-3.5');
   });
@@ -306,9 +255,9 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(source).not.toContain('<option value="yaml">YAML</option>');
     expect(source).not.toContain('<option value="json">JSON</option>');
     expect(source).not.toContain('<option value="curl">cURL</option>');
-    expect(html).toContain('YAML 格式');
-    expect(html).toContain('JSON 格式');
-    expect(html).toContain('cURL 格式');
+    expect(html).toContain(t('entity.definition.format.option.yaml'));
+    expect(html).toContain(t('entity.definition.format.option.json'));
+    expect(html).toContain(t('entity.definition.format.option.curl'));
   });
 
   it('localizes unknown backend feedback while preserving the raw server message', async () => {
@@ -329,8 +278,10 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(source).toContain("t('entities.definition.message.backend-fallback', { message: normalized })");
     expect(source).not.toContain('default:\\n      return message;');
     expect(html).toContain('data-entity-definition-load-error="cold-inline"');
-    expect(html).toContain('后端返回:Backend parser saw custom-field drift.');
-    expect(html).toContain('定义加载失败后可先确认实体是否存在,再继续编辑。');
+    expect(html).toContain(
+      t('entities.definition.message.backend-fallback', { message: 'Backend parser saw custom-field drift.' })
+    );
+    expect(html).toContain(t('entities.definition.workspace.template-error-help'));
   });
 
   it('renders the shared edit-definition workspace with the cold full-width workbench contract', async () => {
@@ -363,23 +314,23 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(html).toContain('data-entity-definition-metric-strip="cold-inline-counts"');
     expect(html).toContain('auto-rows-min');
     expect(html).toContain('rounded-[4px]');
-    expect(html).toContain('实体定义');
-    expect(html).toContain('编辑实体定义');
-    expect(html).toContain('直接编辑当前定义。保存后会同步更新基础信息、遥测绑定和依赖。');
-    expect(html).toContain('清空草稿');
-    expect(html).toContain('预览定义');
-    expect(html).toContain('保存定义');
+    expect(html).toContain(t('entities.definition.workspace.kicker'));
+    expect(html).toContain(t('entities.definition.workspace.title'));
+    expect(html).toContain(t('entities.definition.workspace.subtitle'));
+    expect(html).toContain(t('entity.definition.action.clear-draft'));
+    expect(html).toContain(t('entity.definition.action.preview'));
+    expect(html).toContain(t('entity.definition.action.save'));
     expect(html).toContain('YAML');
     expect(html).toContain('JSON');
     expect(html).toContain('cURL');
-    expect(html).toContain('模板与批量编辑');
-    expect(html).toContain('自定义模板');
-    expect(html).toContain('最近活动');
-    expect(html).toContain('可直接导入');
-    expect(html).toContain('待完善');
-    expect(html).toContain('缺少遥测绑定');
-    expect(html).toContain('查看实体');
-    expect(html).toContain('回到表单');
+    expect(html).toContain(t('entities.definition.workspace.batch-title'));
+    expect(html).toContain(t('entities.definition.workspace.template-title'));
+    expect(html).toContain(t('entities.definition.workspace.activity-title'));
+    expect(html).toContain(t('entities.definition.workspace.metric.ready'));
+    expect(html).toContain(t('entities.definition.workspace.metric.attention'));
+    expect(html).toContain(t('entities.definition.workspace.metric.telemetry-gaps'));
+    expect(html).toContain(t('entities.definition.workspace.entry-view-entity'));
+    expect(html).toContain(t('entities.definition.workspace.entry-back-to-form'));
     expect(html).toContain('kind: service');
     expect(html).not.toContain('data-entity-definition-workspace-offset="angular-sidebar-flush"');
     expect(html).not.toContain('data-entity-definition-workspace-stretch="angular-right-edge"');
@@ -416,10 +367,10 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(html).toContain('auto-rows-min');
     expect(html).not.toContain('lg:grid-cols-[190px_minmax(0,1fr)_460px]');
     expect(html).not.toContain('data-entity-definition-left-rail-density="angular-compact-icons"');
-    expect(html).toContain('导入格式');
-    expect(html).toContain('清空草稿');
-    expect(html).toContain('预览定义');
-    expect(html).toContain('保存定义');
+    expect(html).toContain(t('entity.definition.format.label'));
+    expect(html).toContain(t('entity.definition.action.clear-draft'));
+    expect(html).toContain(t('entity.definition.action.preview'));
+    expect(html).toContain(t('entity.definition.action.save'));
     expect(html).toContain('kind: service');
   });
 
@@ -441,11 +392,11 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(html).toContain('data-entity-definition-error-state="cold-reference"');
     expect(html).toContain('data-entity-definition-error-placement="cold-context-panel"');
     expect(html).not.toContain('lg:pt-[91px]');
-    expect(html).toContain('实体不存在。');
-    expect(html).toContain('粘贴 YAML 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。');
+    expect(html).toContain(t('entities.definition.message.entity-not-exist'));
+    expect(html).toContain(t('entity.definition.import.placeholder.yaml'));
     expect(html).not.toContain('apiVersion: hertzbeat.apache.org/v1');
-    expect(html).not.toContain('1 个模板可用');
-    expect(html).not.toContain('最近活动');
+    expect(html).not.toContain(`1 ${han(0x4e2a, 0x6a21, 0x677f, 0x53ef, 0x7528)}`);
+    expect(html).not.toContain(t('entities.definition.workspace.activity-title'));
     expect(html).not.toContain('Entity not exist.');
   });
 
@@ -481,17 +432,17 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(html).toContain('data-entity-definition-metric-strip="cold-inline-counts"');
     expect(html).toContain('auto-rows-min');
     expect(html).toContain('rounded-[4px]');
-    expect(html).toContain('导入格式');
-    expect(html).toContain('清空草稿');
-    expect(html).toContain('预览定义');
-    expect(html).toContain('导入实体');
+    expect(html).toContain(t('entity.definition.format.label'));
+    expect(html).toContain(t('entity.definition.action.clear-draft'));
+    expect(html).toContain(t('entity.definition.action.preview'));
+    expect(html).toContain(t('entity.definition.action.import'));
     expect(html).toContain('apiVersion: hertzbeat/v1');
     expect(html).toContain('kind: system');
     expect(html).not.toContain('base-template');
     expect(html).not.toContain('bundle previewed');
-    expect(html).not.toContain('模板与批量编辑');
-    expect(html).not.toContain('最近活动');
-    expect(html).not.toContain('待补充');
+    expect(html).not.toContain(t('entities.definition.workspace.batch-title'));
+    expect(html).not.toContain(t('entities.definition.workspace.activity-title'));
+    expect(html).not.toContain(t('entity.definition.import.summary.attention'));
     expect(html).not.toContain('data-entity-definition-workspace-offset="angular-sidebar-flush"');
     expect(html).not.toContain('data-entity-definition-workspace-stretch="angular-right-edge"');
     expect(html).not.toContain('class="-mx-4 -mb-3 -mt-4');
diff --git a/web-next/components/pages/entity-detail-surface.test.tsx b/web-next/components/pages/entity-detail-surface.test.tsx
index df16bf361a..9195657c2f 100644
--- a/web-next/components/pages/entity-detail-surface.test.tsx
+++ b/web-next/components/pages/entity-detail-surface.test.tsx
@@ -129,12 +129,12 @@ describe('EntityDetailSurface', () => {
     expect(source).toContain("translateEntityDetail('entities.detail.attribution.ready')");
     expect(source).toContain("translateEntityDetail('entities.detail.attribution.review')");
     expect(source).toContain("translateEntityDetail('entities.detail.attribution.missing')");
-    expect(source).not.toContain('title="确认删除实体"');
-    expect(source).not.toContain('kicker="对象目录"');
-    expect(source).not.toContain('删除后实体会从对象目录移除');
-    expect(source).not.toContain('已归因');
-    expect(source).not.toContain('待确认');
-    expect(source).not.toContain('缺失');
+    expect(source).not.toMatch(/title="\u786e\u8ba4\u5220\u9664\u5b9e\u4f53"/);
+    expect(source).not.toMatch(/kicker="\u5bf9\u8c61\u76ee\u5f55"/);
+    expect(source).not.toMatch(/\u5220\u9664\u540e\u5b9e\u4f53\u4f1a\u4ece\u5bf9\u8c61\u76ee\u5f55\u79fb\u9664/);
+    expect(source).not.toMatch(/\u5df2\u5f52\u56e0/);
+    expect(source).not.toMatch(/\u5f85\u786e\u8ba4/);
+    expect(source).not.toMatch(/\u7f3a\u5931/);
     expect(source).not.toContain('window.confirm');
     expect(source).not.toContain('confirm(');
     expect(source).toContain('buildEntityContextHandoffLinks');
@@ -250,8 +250,8 @@ describe('EntityDetailSurface', () => {
     expect(html).toContain('Inspect abnormal monitors first.');
     expect(html).not.toContain('Next steps');
     expect(html).not.toContain('Review the summary first');
-    expect(html).not.toContain('对象优先调查');
-    expect(html).not.toContain('关联指标');
+    expect(html).not.toMatch(/\u5bf9\u8c61\u4f18\u5148\u8c03\u67e5/);
+    expect(html).not.toMatch(/\u5173\u8054\u6307\u6807/);
     expect(html).not.toContain('definition workspace');
     expect(html).not.toContain('data-stage-section');
     expect(html).not.toContain('data-drawer-section');
@@ -331,7 +331,7 @@ describe('EntityDetailSurface', () => {
     expect(html).toContain('website · example.com:443 · monitorId 632051474676992');
     expect(html).toContain('data-entity-detail-inherited-context-row="Source"');
     expect(html).toContain('Traditional monitoring');
-    expect(html).not.toContain('0 个证据');
+    expect(html).not.toMatch(/0 \u4e2a\u8bc1\u636e/);
     expect(html).not.toContain('0 evidence');
   }, 30000);
 });
diff --git a/web-next/components/pages/entity-discovery-surface.test.tsx b/web-next/components/pages/entity-discovery-surface.test.tsx
index 5d42bfd33c..e291634152 100644
--- a/web-next/components/pages/entity-discovery-surface.test.tsx
+++ b/web-next/components/pages/entity-discovery-surface.test.tsx
@@ -7,55 +7,7 @@ import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'entities.discovery.workspace.kicker': '对象优先调查',
-        'entities.discovery.workspace.title': '遥测发现',
-        'entities.discovery.workspace.subtitle': '先搜索一组需要治理的监控线索,再决定归并、完善归属,还是送入定义工作台继续收口。',
-        'entities.discovery.action.import': '从定义创建',
-        'entities.discovery.action.create': '创建实体',
-        'entities.discovery.action.catalog': '对象目录',
-        'entities.discovery.action.search': '搜索',
-        'entities.discovery.action.clear': '清空',
-        'entities.discovery.search.placeholder': '搜索监控名称或实例',
-        'entities.discovery.search.error': '遥测发现查询失败',
-        'entities.discovery.candidate.title': 'OTLP 候选实体',
-        'entities.discovery.candidate.copy': '保留 OTLP 资源身份,确认后再写入对象目录。',
-        'entities.discovery.candidate.action.create': '创建实体草稿',
-        'entities.discovery.candidate.identity': '资源身份',
-        'entities.discovery.candidate.namespace': '命名空间',
-        'entities.discovery.candidate.environment': '环境',
-        'entities.discovery.policy.title': '治理筛选与共享策略',
-        'entities.discovery.policy.copy': '按已有负责人、系统和环境预设给线索完善上下文。',
-        'entities.discovery.policy.latest-activity': '最近活动 {{activity}}',
-        'entities.discovery.policy.empty': '暂无共享策略,先搜索一条监控线索。',
-        'entities.discovery.catalog.owner': '负责人',
-        'entities.discovery.catalog.system': '系统',
-        'entities.discovery.catalog.environment': '环境',
-        'entities.discovery.metric.clues': '线索',
-        'entities.discovery.metric.matched': '已匹配',
-        'entities.discovery.metric.create-suggested': '建议新建',
-        'entities.discovery.metric.catalog-sources': '目录来源',
-        'entities.discovery.table.title': '发现线索',
-        'entities.discovery.table.result-count': '{{count}} 条结果',
-        'entities.discovery.table.waiting': '等待搜索',
-        'entities.discovery.table.column.clue': '线索',
-        'entities.discovery.table.column.instance': '实例',
-        'entities.discovery.table.column.status': '状态',
-        'entities.discovery.table.column.owner': '归属',
-        'entities.discovery.table.column.system': '系统',
-        'entities.discovery.table.column.environment': '环境',
-        'entities.discovery.table.column.attribution': '归因',
-        'entities.discovery.table.column.action': '操作',
-        'entities.discovery.empty.title': '先搜索一组需要治理的监控线索',
-        'entities.discovery.empty.search.copy': '没有找到匹配的监控线索。',
-        'entities.discovery.empty.idle.copy': '先搜索一条监控,再把它转成实体草稿。',
-        'entities.discovery.activity.preset-synced': '预设已同步',
-        'entities.discovery.activity.shared-governance-updated': '共享治理已更新',
-        'entities.discovery.activity.empty': '暂无活动'
-      }
-    })
+    t: createTranslatorMock({ locale: 'zh-CN' })
   })
 }));
 
@@ -111,37 +63,45 @@ vi.mock('@/lib/entity-discovery/controller', () => ({
   searchDiscoveryMonitors: vi.fn()
 }));
 
-vi.mock('@/lib/entity-discovery/view-model', () => ({
-  buildCatalogRows: () => [],
-  buildDiscoveryBulkOverrideTags: () => [],
-  buildDiscoveryBulkSuggestionChips: () => ({ ownerChips: [], systemChips: [], presetActions: [] }),
-  buildDiscoveryBulkSummary: () => ({ totalCount: 0, selectedCount: 0, mergeReadyCount: 0, createReadyCount: 0, reviewCount: 0 }),
-  buildDiscoveryFacts: () => [],
-  buildDiscoveryGovernanceCards: () => [],
-  buildDiscoveryIntakeQueueGroups: () => [],
-  buildDiscoveryMetrics: () => [],
-  buildDiscoveryMonitorRows: () => [],
-  buildDiscoveryScopeOptions: () => [],
-  filterDiscoveryCardsByScope: (cards: any[]) => cards,
-  buildDiscoveryTableRows: (_monitors: any[], presets: any[]) =>
-    presets.map(preset => ({
-      key: `preset-${preset.id}`,
-      name: preset.name,
-      instance: preset.system,
-      status: preset.status,
-      owner: preset.owner,
-      system: preset.system,
-      environment: preset.environment,
-      activity: 'catalog preset',
-      href: `/entities/discovery?preset=${preset.id}`,
-      attributionState: 'preset',
-      attributionLabel: '目录预设',
-      attributionCopy: '可作为候选确认基线',
-      primaryActionLabel: '查看预设'
-    }))
-}));
+vi.mock('@/lib/entity-discovery/view-model', () => {
+  const t = createTranslatorMock({ locale: 'zh-CN' });
+
+  return {
+    buildCatalogRows: () => [],
+    buildDiscoveryBulkOverrideTags: () => [],
+    buildDiscoveryBulkSuggestionChips: () => ({ ownerChips: [], systemChips: [], presetActions: [] }),
+    buildDiscoveryBulkSummary: () => ({ totalCount: 0, selectedCount: 0, mergeReadyCount: 0, createReadyCount: 0, reviewCount: 0 }),
+    buildDiscoveryFacts: () => [],
+    buildDiscoveryGovernanceCards: () => [],
+    buildDiscoveryIntakeQueueGroups: () => [],
+    buildDiscoveryMetrics: () => [],
+    buildDiscoveryMonitorRows: () => [],
+    buildDiscoveryScopeOptions: () => [],
+    filterDiscoveryCardsByScope: (cards: any[]) => cards,
+    buildDiscoveryTableRows: (_monitors: any[], presets: any[]) =>
+      presets.map(preset => ({
+        key: `preset-${preset.id}`,
+        name: preset.name,
+        instance: preset.system,
+        status: preset.status,
+        owner: preset.owner,
+        system: preset.system,
+        environment: preset.environment,
+        activity: 'catalog preset',
+        href: `/entities/discovery?preset=${preset.id}`,
+        attributionState: 'preset',
+        attributionLabel: t('entities.discovery.row.attribution.preset.label'),
+        attributionCopy: t('entities.discovery.row.attribution.preset.copy'),
+        primaryActionLabel: t('entities.discovery.row.attribution.preset.action')
+      }))
+  };
+});
+
+const han = (...codes: number[]) => String.fromCodePoint(...codes);
 
 describe('EntityDiscoverySurface', () => {
+  const zh = createTranslatorMock({ locale: 'zh-CN' });
+
   it('keeps discovery on the cold full-width HertzBeat owner instead of the old rail contract', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/entity-discovery-surface.tsx'), 'utf8');
 
@@ -170,7 +130,7 @@ describe('EntityDiscoverySurface', () => {
     expect(source).not.toContain('data-entity-discovery-search-input-shell');
     expect(source).not.toContain('coldEntityDiscoveryVisual.search.input');
     expect(source).not.toContain("from '@/components/ui/input'");
-    expect(source).not.toContain('补齐');
+    expect(source).not.toContain(han(0x8865, 0x9f50));
     expect(source).not.toContain('data-entity-discovery-route="signoz-discovery-table"');
     expect(source).not.toContain('data-entity-discovery-rail="signoz-status-rail"');
     expect(source).not.toContain('data-entity-discovery-controls="signoz-filter-bar"');
@@ -184,8 +144,8 @@ describe('EntityDiscoverySurface', () => {
     expect(source).not.toContain('data-entity-discovery-primary-action-row="angular-inline"');
     expect(source).not.toContain('data-entity-discovery-table-shell="signoz-table-shell"');
     expect(source).not.toContain('data-entity-discovery-table="signoz-discovery-table"');
-    expect(source).not.toContain('原因');
-    expect(source).not.toContain('接着可做');
+    expect(source).not.toContain(han(0x539f, 0x56e0));
+    expect(source).not.toContain(han(0x63a5, 0x7740, 0x53ef, 0x505a));
     expect(source).not.toContain('WorkbenchPage');
     expect(source).not.toContain('SurfaceSection');
     expect(source).not.toContain('RailSection');
@@ -195,8 +155,8 @@ describe('EntityDiscoverySurface', () => {
     expect(source).not.toContain('RowList');
     expect(source).not.toContain('EntityDiscoveryGovernanceList');
     expect(source).not.toContain('EntityDiscoveryIntakeConsole');
-    expect(source).not.toContain('aria-label="范围"');
-    expect(source).not.toContain('全部来源');
+    expect(source).not.toContain(`aria-label="${han(0x8303, 0x56f4)}"`);
+    expect(source).not.toContain(han(0x5168, 0x90e8, 0x6765, 0x6e90));
   });
 
   it('renders a compact cold discovery console with Chinese object-directory copy and no right rail', async () => {
@@ -229,30 +189,30 @@ describe('EntityDiscoverySurface', () => {
     expect(html).toContain('data-entity-discovery-empty-state="cold-inline-empty"');
     expect(html).toContain('data-entity-discovery-table-shell="cold-dense-table"');
     expect(html).toContain('data-entity-discovery-table="cold-discovery-table"');
-    expect(html).toContain('归因');
-    expect(html).toContain('遥测发现');
-    expect(html).toContain('先搜索一组需要治理的监控线索');
-    expect(html).toContain('搜索监控名称或实例');
-    expect(html).toContain('先搜索一条监控,再把它转成实体草稿。');
-    expect(html).toContain('治理筛选与共享策略');
-    expect(html).toContain('完善上下文');
-    expect(html).toContain('从定义创建');
-    expect(html).toContain('创建实体');
-    expect(html).toContain('实体');
-    expect(html).toContain('负责人 · platform');
-    expect(html).toContain('系统 · checkout');
-    expect(html).toContain('环境 · prod');
+    expect(html).toContain(zh('entities.discovery.table.column.attribution'));
+    expect(html).toContain(zh('entities.discovery.workspace.title'));
+    expect(html).toContain(zh('entities.discovery.empty.title'));
+    expect(html).toContain(zh('entities.discovery.search.placeholder'));
+    expect(html).toContain(zh('entities.discovery.empty.idle.copy'));
+    expect(html).toContain(zh('entities.discovery.policy.title'));
+    expect(html).toContain(zh('entities.discovery.policy.copy'));
+    expect(html).toContain(zh('entities.discovery.action.import'));
+    expect(html).toContain(zh('entities.discovery.action.create'));
+    expect(html).toContain(zh('dashboard.home.status.entities'));
+    expect(html).toContain(`${zh('entities.discovery.catalog.owner')} · platform`);
+    expect(html).toContain(`${zh('entities.discovery.catalog.system')} · checkout`);
+    expect(html).toContain(`${zh('entities.discovery.catalog.environment')} · prod`);
     expect(html).not.toContain('checkout baseline');
     expect(html).not.toContain('data-entity-discovery-route="signoz-discovery-table"');
     expect(html).not.toContain('data-entity-discovery-rail="signoz-status-rail"');
     expect(html).not.toContain('data-entity-discovery-right-panel="angular-density"');
     expect(html).not.toContain('data-entity-discovery-table="signoz-discovery-table"');
     expect(html).not.toContain('data-entity-discovery-empty-canvas="angular-blank-canvas"');
-    expect(html).not.toContain('常用入口');
-    expect(html).not.toContain('原因');
-    expect(html).not.toContain('接着可做');
-    expect(html).not.toContain('全部来源');
-    expect(html).not.toContain('aria-label="范围"');
+    expect(html).not.toContain(han(0x5e38, 0x7528, 0x5165, 0x53e3));
+    expect(html).not.toContain(han(0x539f, 0x56e0));
+    expect(html).not.toContain(han(0x63a5, 0x7740, 0x53ef, 0x505a));
+    expect(html).not.toContain(han(0x5168, 0x90e8, 0x6765, 0x6e90));
+    expect(html).not.toContain(`aria-label="${han(0x8303, 0x56f4)}"`);
     expect(html).not.toContain('data-workbench-page');
     expect(html).not.toContain('Governance presets');
     expect(html).not.toContain('Catalog suggestions');
@@ -284,17 +244,17 @@ describe('EntityDiscoverySurface', () => {
     expect(html).toContain('data-entity-discovery-candidate-service="billing-api"');
     expect(html).toContain('data-entity-discovery-candidate-namespace="commerce"');
     expect(html).toContain('data-entity-discovery-candidate-environment="prod"');
-    expect(html).toContain('OTLP 候选实体');
+    expect(html).toContain(zh('entities.discovery.candidate.title'));
     expect(html).toContain('service.name = billing');
-    expect(html).toContain('命名空间 · commerce');
-    expect(html).toContain('环境 · prod');
+    expect(html).toContain(`${zh('entities.discovery.candidate.namespace')} · commerce`);
+    expect(html).toContain(`${zh('entities.discovery.candidate.environment')} · prod`);
     expect(html).toContain('value="billing-api"');
     expect(html).toContain('data-entity-discovery-candidate-action="create-draft"');
     expect(html).toContain(
       'href="/entities/new?source=otlp-candidate&amp;identityKey=service.name&amp;identityValue=billing&amp;serviceName=billing-api&amp;serviceNamespace=commerce&amp;environment=prod"'
     );
-    expect(html).not.toContain('已归属');
-    expect(html).not.toContain('健康正常');
-    expect(html).not.toContain('拓扑已确认');
+    expect(html).not.toContain(han(0x5df2, 0x5f52, 0x5c5e));
+    expect(html).not.toContain(han(0x5065, 0x5eb7, 0x6b63, 0x5e38));
+    expect(html).not.toContain(han(0x62d3, 0x6251, 0x5df2, 0x786e, 0x8ba4));
   }, 30000);
 });
diff --git a/web-next/components/pages/entity-editor-surface.test.tsx b/web-next/components/pages/entity-editor-surface.test.tsx
index 6c7ac9a843..fed65dc2d6 100644
--- a/web-next/components/pages/entity-editor-surface.test.tsx
+++ b/web-next/components/pages/entity-editor-surface.test.tsx
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 
 vi.mock('next/link', () => ({
   default: ({ href, children, ...props }: any) => (
@@ -17,131 +18,7 @@ vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
       locale: 'zh-CN',
-      overrides: {
-        'common.button.cancel': '取消',
-        'common.cancel': '取消',
-        'common.save': '保存',
-        'common.saving': '保存中',
-        'common.remove': '移除',
-        'common.item': '项目',
-        'common.save-success': '保存成功',
-        'common.save-failed': '保存失败',
-        'entities.editor.json.invalid': '{{label}} 第 {{index}} 项不是合法 JSON',
-        'entities.editor.message.create-success': '已创建实体 #{{id}}',
-        'entities.editor.submit.create': '创建实体',
-        'entities.editor.stage.basic.label': '基本信息',
-        'entities.editor.stage.basic.description': '先确认类型、名称和核心元数据。',
-        'entities.editor.stage.ownership.label': '归属与处置',
-        'entities.editor.stage.ownership.description': '补齐负责人、联系人和处置入口。',
-        'entities.editor.stage.signals.label': '证据关联',
-        'entities.editor.stage.signals.description': '绑定监控、身份标识和遥测证据。',
-        'entities.editor.stage.relations.label': '关系与扩展',
-        'entities.editor.stage.relations.description': '记录上下游关系、标签和扩展字段。',
-        'entities.editor.route-tab.detail': '实体详情',
-        'entities.editor.route-tab.monitor': '监控中心',
-        'entities.editor.route-tab.logs': '日志管理与统计',
-        'entities.editor.route-tab.traces': '链路',
-        'entities.editor.shell.kicker': '实体',
-        'entities.editor.shell.title.new': '新建实体',
-        'entities.editor.shell.title.edit': '编辑实体',
-        'entities.editor.shell.copy.new': '先在页面中建立实体记录,再逐步补齐证据、负责人和依赖关系。',
-        'entities.editor.shell.copy.edit': '先在页面中建立实体记录,再逐步补充证据、负责人和依赖关系。',
-        'entities.editor.shell.freshness.new': '0% 最近更新',
-        'entities.editor.shell.freshness.edit': '75% 最近更新',
-        'entities.editor.shell.all-entities': '全部实体',
-        'entities.editor.shell.definition-workspace': '定义工作区',
-        'entities.editor.shell.entity-label': '实体',
-        'entities.editor.shell.preview.show': '显示快照',
-        'entities.editor.shell.preview.hide': '隐藏快照',
-        'entities.editor.shell.definition-version': '定义版本 · HertzBeat v1 / OTel 资源规范',
-        'entities.editor.shell.name-label': '名称',
-        'entities.editor.shell.name-placeholder': '实体唯一名称',
-        'entities.editor.shell.type-label': '类型',
-        'entities.editor.shell.entry-source-label': '录入来源',
-        'entities.editor.shell.stage-label': '编辑阶段',
-        'entities.editor.shell.preview-rail.title': '实体元数据',
-        'entities.editor.shell.preview-rail.copy': '查看或粘贴定义,应用到表单。',
-        'entities.editor.shell.definition-content.title': '定义内容',
-        'entities.editor.shell.definition-content.copy': '查看或粘贴定义,应用到表单。',
-        'entities.editor.mode.editor': '表单',
-        'entities.editor.type.system.label': '系统',
-        'entities.editor.type.system.description': '业务域或产品线',
-        'entities.editor.type.service.label': '服务',
-        'entities.editor.type.service.description': '对外提供业务能力的应用或接口',
-        'entities.editor.type.host.label': '主机',
-        'entities.editor.type.host.description': '承载服务的运行节点',
-        'entities.editor.type.database.label': '数据库',
-        'entities.editor.type.database.description': '数据持久化实例',
-        'entities.editor.type.queue.label': '队列',
-        'entities.editor.type.queue.description': '异步消息通道',
-        'entities.editor.type.middleware.label': '中间件',
-        'entities.editor.type.middleware.description': '共享基础组件',
-        'entities.editor.type.api.label': 'API',
-        'entities.editor.type.api.description': '接口或网关资源',
-        'entities.editor.type.endpoint.label': '端点',
-        'entities.editor.type.endpoint.description': '可观测访问入口',
-        'entities.editor.type.device.label': '设备',
-        'entities.editor.type.device.description': '网络或边缘设备',
-        'entities.editor.type.k8s-workload.label': 'K8s 工作负载',
-        'entities.editor.type.k8s-workload.description': 'K8s 原生工作负载',
-        'entities.editor.entry-source.manual': '页面录入',
-        'entities.editor.entry-source.telemetry': '遥测发现',
-        'entities.editor.entry-source.definition': '定义优先',
-        'entities.editor.telemetry-handoff.title': '遥测发现',
-        'entities.editor.telemetry-handoff.copy': '先通过监控和 OTel 资源字段识别实体,再补充基础信息。',
-        'entities.editor.telemetry-handoff.monitor-count': '{{count}} 个监控绑定',
-        'entities.editor.telemetry-handoff.identity-count': '{{count}} 个身份标识',
-        'entities.editor.telemetry-handoff.attribution-check': '归因检查',
-        'entities.editor.field.display-name': '显示名称',
-        'entities.editor.field.namespace': '命名空间',
-        'entities.editor.field.environment': '环境',
-        'entities.editor.field.subtype': '子类型',
-        'entities.editor.field.owner': '负责人',
-        'entities.editor.field.system': '所属系统',
-        'entities.editor.field.source': '来源',
-        'entities.editor.field.description': '描述',
-        'entities.editor.field.description-placeholder': '例如负责交易结算和支付编排。',
-        'entities.editor.field.runbook': '运行手册',
-        'entities.editor.placeholder.display-name': '例如 Checkout API',
-        'entities.editor.placeholder.namespace': '例如 commerce',
-        'entities.editor.placeholder.environment': '例如 prod',
-        'entities.editor.placeholder.subtype': '例如 http',
-        'entities.editor.placeholder.owner': '例如 payments-team',
-        'entities.editor.placeholder.system': '例如 website',
-        'entities.editor.placeholder.source': 'manual',
-        'entities.editor.contact.title': '联系人',
-        'entities.editor.contact.add': '新增联系人',
-        'entities.editor.contact.name-placeholder': '联系人',
-        'entities.editor.contact.type-placeholder': '类型',
-        'entities.editor.contact.value-placeholder': '联系方式',
-        'entities.editor.contact.contact-placeholder': '联系字段',
-        'entities.editor.link.title': '链接',
-        'entities.editor.link.add': '新增链接',
-        'entities.editor.link.name-placeholder': '链接名称',
-        'entities.editor.link.type-placeholder': '类型',
-        'entities.editor.link.provider-placeholder': '提供方',
-        'entities.editor.link.url-placeholder': '链接地址',
-        'entities.editor.collection.identities': '身份标识',
-        'entities.editor.collection.identities.add': '新增身份标识',
-        'entities.editor.collection.monitor-binds': '监控绑定',
-        'entities.editor.collection.monitor-binds.add': '新增监控绑定',
-        'entities.editor.collection.relations': '关系',
-        'entities.editor.collection.relations.add': '新增关系',
-        'entities.editor.relation.component-of': '上级组件',
-        'entities.editor.relation.components': '下级组件',
-        'entities.editor.relation.implemented-by': '实现服务',
-        'entities.editor.relation.languages': '开发语言',
-        'entities.editor.relation.component-of-placeholder': 'payment-platform, commerce',
-        'entities.editor.relation.components-placeholder': 'checkout-ui, order-router',
-        'entities.editor.relation.implemented-by-placeholder': 'checkout-api, order-worker',
-        'entities.editor.relation.languages-placeholder': 'java, typescript',
-        'entities.editor.relation.labels': '标签',
-        'entities.editor.relation.label-key': '标签键',
-        'entities.editor.relation.label-value': '标签值',
-        'entities.editor.relation.label-add': '新增标签',
-        'entities.editor.definition.aria.yaml': 'YAML 定义快照',
-        'entities.editor.definition.aria.json': 'JSON 定义快照'
-      }
+      overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] ?? {}
     })
   })
 }));
@@ -268,63 +145,67 @@ vi.mock('@/lib/entity-editor/initial-state', () => ({
   })
 }));
 
-vi.mock('@/lib/entity-editor/view-model', () => ({
-  buildEntityEditorAttributionRows: () => [
-    {
-      key: 'identity',
-      title: '身份标识',
-      copy: '1 个身份标识',
-      meta: 'service.name=checkout',
-      state: 'ready'
-    },
-    {
-      key: 'monitor-binding',
-      title: '监控绑定',
-      copy: '1 个监控绑定',
-      meta: 'monitorId 42',
-      state: 'ready'
-    },
-    {
-      key: 'ownership',
-      title: '负责人',
-      copy: '缺少负责人',
-      meta: '先补负责人或值班组',
-      state: 'missing'
-    },
-    {
-      key: 'system-environment',
-      title: '系统与环境',
-      copy: 'website · 缺少环境',
-      meta: '用于告警收敛和拓扑',
-      state: 'review'
-    },
-    {
-      key: 'discovery-return',
-      title: '发现回路',
-      copy: '可回到遥测发现',
-      meta: '/entities/discovery',
-      state: 'review',
-      href: '/entities/discovery'
-    }
-  ],
-  buildEntityEditorCatalogRows: () => [
-    { title: 'catalog', copy: 'platform', meta: 'count 1' }
-  ],
-  buildEntityEditorFacts: () => [],
-  buildEntityEditorNextStepRows: () => [
-    { title: 'definition', copy: 'Open definition', meta: '/entities/42/definition' }
-  ],
-  buildEntityEditorSuggestions: () => ({
-    ownerSuggestions: ['platform'],
-    systemSuggestions: ['commerce'],
-    environmentSuggestions: ['prod'],
-    lifecycleSuggestions: ['production'],
-    tierSuggestions: ['tier-1'],
-    languageSuggestions: ['java'],
-    providerSuggestions: ['grafana']
-  }),
-  buildEntityEditorTitle: (mode: 'new' | 'edit', entityId?: string) => (mode === 'new' ? 'Create entity' : `Edit entity · ${entityId}`)
-}));
+vi.mock('@/lib/entity-editor/view-model', () => {
+  const t = createTranslatorMock({ locale: 'zh-CN', overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] ?? {} });
+
+  return {
+    buildEntityEditorAttributionRows: () => [
+      {
+        key: 'identity',
+        title: t('entities.editor.attribution.identity.title'),
+        copy: t('entities.editor.attribution.identity.count', { count: 1 }),
+        meta: 'service.name=checkout',
+        state: 'ready'
+      },
+      {
+        key: 'monitor-binding',
+        title: t('entities.editor.attribution.monitor.title'),
+        copy: t('entities.editor.attribution.monitor.count', { count: 1 }),
+        meta: 'monitorId 42',
+        state: 'ready'
+      },
+      {
+        key: 'ownership',
+        title: t('entities.editor.attribution.owner.title'),
+        copy: t('entities.editor.attribution.owner.missing'),
+        meta: t('entities.editor.attribution.owner.missing-meta'),
+        state: 'missing'
+      },
+      {
+        key: 'system-environment',
+        title: t('entities.editor.attribution.system-environment.title'),
+        copy: t('entities.editor.attribution.system-environment.missing-environment', { system: 'website' }),
+        meta: t('entities.editor.attribution.system-environment.meta'),
+        state: 'review'
+      },
+      {
+        key: 'discovery-return',
+        title: t('entities.editor.attribution.discovery.title'),
+        copy: t('entities.editor.attribution.discovery.copy'),
+        meta: '/entities/discovery',
+        state: 'review',
+        href: '/entities/discovery'
+      }
+    ],
+    buildEntityEditorCatalogRows: () => [
+      { title: 'catalog', copy: 'platform', meta: 'count 1' }
+    ],
+    buildEntityEditorFacts: () => [],
+    buildEntityEditorNextStepRows: () => [
+      { title: 'definition', copy: 'Open definition', meta: '/entities/42/definition' }
+    ],
+    buildEntityEditorSuggestions: () => ({
+      ownerSuggestions: ['platform'],
+      systemSuggestions: ['commerce'],
+      environmentSuggestions: ['prod'],
+      lifecycleSuggestions: ['production'],
+      tierSuggestions: ['tier-1'],
+      languageSuggestions: ['java'],
+      providerSuggestions: ['grafana']
+    }),
+    buildEntityEditorTitle: (mode: 'new' | 'edit', entityId?: string) => (mode === 'new' ? 'Create entity' : `Edit entity · ${entityId}`)
+  };
+});
 
 vi.mock('@/lib/workspace-navigation', () => ({
   buildEntityEditorWorkspaceTabs: () => []
@@ -335,6 +216,9 @@ vi.mock('@/lib/utils', () => ({
 }));
 
 describe('EntityEditorSurface', () => {
+  const t = createTranslatorMock({ locale: 'zh-CN', overrides: SUPPLEMENTAL_MESSAGES['zh-CN'] ?? {} });
+  const han = (...codes: number[]) => String.fromCodePoint(...codes);
+
   it('keeps only nested row editors on shared inset panels while the page shell stays cold-owned', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/entity-editor-surface.tsx'), 'utf8');
 
@@ -424,38 +308,38 @@ describe('EntityEditorSurface', () => {
     expect(html).not.toContain('data-entity-editor-frame="angular-flush"');
     expect(html).not.toContain('data-entity-editor-definition-footer-raise="angular-flush-tabs"');
     expect(html).not.toContain('data-workbench-side="true"');
-    expect(html).toContain('表单');
+    expect(html).toContain(t('entities.editor.mode.editor'));
     expect(html).toContain('YAML');
     expect(html).toContain('JSON');
-    expect(html).toContain('隐藏快照');
+    expect(html).toContain(t('entities.editor.shell.preview.hide'));
     expect(html).toContain('data-entity-editor-preview-toggle-icon="cold"');
-    expect(html).toContain('取消');
-    expect(html).toContain('显示名称');
-    expect(html).toContain('命名空间');
-    expect(html).toContain('环境');
-    expect(html).toContain('aria-label="显示名称"');
-    expect(html).toContain('aria-label="名称"');
-    expect(html).toContain('aria-label="来源"');
+    expect(html).toContain(t('common.button.cancel'));
+    expect(html).toContain(t('entities.editor.field.display-name'));
+    expect(html).toContain(t('entities.editor.field.namespace'));
+    expect(html).toContain(t('entities.editor.field.environment'));
+    expect(html).toContain(`aria-label="${t('entities.editor.field.display-name')}"`);
+    expect(html).toContain(`aria-label="${t('entities.editor.shell.name-label')}"`);
+    expect(html).toContain(`aria-label="${t('entities.editor.field.source')}"`);
     expect(html).not.toContain('Cancel');
     expect(html).not.toContain('Namespace');
     expect(html).not.toContain('Environment');
     expect(html).not.toContain('Display Name');
     expect(html).not.toContain('aria-label="Name"');
     expect(html).not.toContain('aria-label="Source"');
-    expect(html).toContain('创建实体');
-    expect(html).toContain('系统');
-    expect(html).toContain('服务');
+    expect(html).toContain(t('entities.editor.submit.create'));
+    expect(html).toContain(t('entities.editor.type.system.label'));
+    expect(html).toContain(t('entities.editor.type.service.label'));
     expect(html).toContain('data-entity-type-icon="service"');
     expect(html).toContain('data-entity-type-icon="database"');
     expect(html).toContain('<svg');
-    expect(html).toContain('页面录入');
+    expect(html).toContain(t('entities.editor.entry-source.manual'));
     expect(html).toContain('data-entity-entry-source-icon="manual"');
     expect(html).toContain('data-entity-entry-source-icon="telemetry"');
     expect(html).toContain('data-entity-entry-source-icon="definition"');
-    expect(html).toContain('基本信息');
-    expect(html).toContain('归属与处置');
-    expect(html).toContain('证据关联');
-    expect(html).toContain('关系与扩展');
+    expect(html).toContain(t('entities.editor.stage.basic.label'));
+    expect(html).toContain(t('entities.editor.stage.ownership.label'));
+    expect(html).toContain(t('entities.editor.stage.signals.label'));
+    expect(html).toContain(t('entities.editor.stage.relations.label'));
     expect(html).toContain('data-entity-editor-footer="true"');
 
     const source = readFileSync(resolve(process.cwd(), 'components/pages/entity-editor-surface.tsx'), 'utf8');
@@ -549,8 +433,8 @@ describe('EntityEditorSurface', () => {
     expect(html).toContain('data-entity-editor-definition-handoff="cold-hidden"');
     expect(html).toContain('data-entity-type-icon="service"');
     expect(html).toContain('data-entity-type-icon="database"');
-    expect(html).toContain('编辑实体');
-    expect(html).toContain('保存');
+    expect(html).toContain(t('entities.editor.shell.title.edit'));
+    expect(html).toContain(t('common.save'));
     expect(html).toContain('/entities/42/definition');
     expect(html).toContain('data-entity-editor-mode="editor"');
     expect(html).not.toContain('data-workbench-page="true"');
@@ -601,18 +485,18 @@ describe('EntityEditorSurface', () => {
     expect(html).toContain('data-entity-editor-attribution-row="system-environment"');
     expect(html).toContain('data-entity-editor-attribution-state="review"');
     expect(html).toContain('data-entity-editor-attribution-row="discovery-return"');
-    expect(html).toContain('遥测发现');
-    expect(html).toContain('归因检查');
+    expect(html).toContain(t('entities.editor.telemetry-handoff.title'));
+    expect(html).toContain(t('entities.editor.telemetry-handoff.attribution-check'));
     expect(html).toContain('service.name=checkout');
     expect(html).toContain('monitorId 42');
-    expect(html).toContain('先补负责人或值班组');
-    expect(html).toContain('website · 缺少环境');
-    expect(html).toContain('1 个监控绑定');
-    expect(html).toContain('1 个身份标识');
+    expect(html).toContain(t('entities.editor.attribution.owner.missing-meta'));
+    expect(html).toContain(t('entities.editor.attribution.system-environment.missing-environment', { system: 'website' }));
+    expect(html).toContain(t('entities.editor.attribution.monitor.count', { count: 1 }));
+    expect(html).toContain(t('entities.editor.attribution.identity.count', { count: 1 }));
     expect(html).toContain('/entities/discovery');
-    expect(html).toContain('aria-label="名称"');
-    expect(html).toContain('aria-label="显示名称"');
-    expect(html).toContain('aria-label="来源"');
+    expect(html).toContain(`aria-label="${t('entities.editor.shell.name-label')}"`);
+    expect(html).toContain(`aria-label="${t('entities.editor.field.display-name')}"`);
+    expect(html).toContain(`aria-label="${t('entities.editor.field.source')}"`);
     expect(html).not.toContain('Telemetry Discovery');
     expect(html).not.toContain('bound monitor');
   });
@@ -650,19 +534,19 @@ describe('EntityEditorSurface', () => {
 
     expect(html).toContain('data-entity-editor-telemetry-handoff="true"');
     expect(html).toContain('data-entity-editor-telemetry-handoff-source="otlp-candidate"');
-    expect(html).toContain('0 个监控绑定');
-    expect(html).toContain('3 个身份标识');
+    expect(html).toContain(t('entities.editor.attribution.monitor.count', { count: 0 }));
+    expect(html).toContain(t('entities.editor.attribution.identity.count', { count: 3 }));
     expect(html).toContain(
       'href="/entities/discovery?identityKey=service.name&amp;identityValue=billing&amp;serviceName=billing-api&amp;serviceNamespace=commerce&amp;environment=prod"'
     );
-    expect(html).toContain('aria-label="名称"');
+    expect(html).toContain(`aria-label="${t('entities.editor.shell.name-label')}"`);
     expect(html).toContain('value="billing-api"');
-    expect(html).toContain('aria-label="命名空间"');
+    expect(html).toContain(`aria-label="${t('entities.editor.field.namespace')}"`);
     expect(html).toContain('value="commerce"');
-    expect(html).toContain('aria-label="环境"');
+    expect(html).toContain(`aria-label="${t('entities.editor.field.environment')}"`);
     expect(html).toContain('value="prod"');
-    expect(html).not.toContain('已归属');
-    expect(html).not.toContain('健康正常');
-    expect(html).not.toContain('拓扑已确认');
+    expect(html).not.toContain(han(0x5df2, 0x5f52, 0x5c5e));
+    expect(html).not.toContain(han(0x5065, 0x5eb7, 0x6b63, 0x5e38));
+    expect(html).not.toContain(han(0x62d3, 0x6251, 0x5df2, 0x786e, 0x8ba4));
   }, 30000);
 });
diff --git a/web-next/components/pages/entity-editor-surface.tsx b/web-next/components/pages/entity-editor-surface.tsx
index 8f39ff6f69..96b1c6860f 100644
--- a/web-next/components/pages/entity-editor-surface.tsx
+++ b/web-next/components/pages/entity-editor-surface.tsx
@@ -778,6 +778,7 @@ export function EntityEditorSurface({
       value: string,
       placeholder: string,
       onChange: (value: string) => void,
+      fieldKey: string,
       extraClassName = '',
       ariaLabel = label
     ) => (
@@ -785,6 +786,7 @@ export function EntityEditorSurface({
         <span className={coldFieldLabelTextClassName}>{label}</span>
         <Input
           aria-label={ariaLabel}
+          data-entity-editor-input={fieldKey}
           className={coldInputClassName}
           value={value}
           placeholder={placeholder}
@@ -885,6 +887,7 @@ export function EntityEditorSurface({
                   <span className={coldFieldLabelTextClassName}>{entityNameLabel}</span>
                   <Input
                     aria-label={entityNameLabel}
+                    data-entity-editor-input="name"
                     className={coldNameInputClassName}
                     required
                     maxLength={128}
@@ -1125,13 +1128,13 @@ export function EntityEditorSurface({
                 {activeStage === 'basic' ? (
                   <>
                     <div className="grid gap-3 md:grid-cols-3">
-                      {renderField(t('entities.editor.field.display-name'), draft.entity.displayName || '', t('entities.editor.placeholder.display-name'), value => updateEntityField('displayName', value))}
-                      {renderField(t('entities.editor.field.namespace'), draft.entity.namespace || '', t('entities.editor.placeholder.namespace'), value => updateEntityField('namespace', value))}
-                      {renderField(t('entities.editor.field.environment'), draft.entity.environment || '', t('entities.editor.placeholder.environment'), value => updateEntityField('environment', value))}
-                      {renderField(t('entities.editor.field.subtype'), draft.entity.subtype || '', t('entities.editor.placeholder.subtype'), value => updateEntityField('subtype', value))}
-                      {renderField(t('entities.editor.field.owner'), draft.entity.owner || '', t('entities.editor.placeholder.owner'), value => updateEntityField('owner', value))}
-                      {renderField(t('entities.editor.field.system'), draft.entity.system || '', t('entities.editor.placeholder.system'), value => updateEntityField('system', value))}
-                      {renderField(t('entities.editor.field.source'), draft.entity.source || '', t('entities.editor.placeholder.source'), value => updateEntityField('source', value))}
+                      {renderField(t('entities.editor.field.display-name'), draft.entity.displayName || '', t('entities.editor.placeholder.display-name'), value => updateEntityField('displayName', value), 'display-name')}
+                      {renderField(t('entities.editor.field.namespace'), draft.entity.namespace || '', t('entities.editor.placeholder.namespace'), value => updateEntityField('namespace', value), 'namespace')}
+                      {renderField(t('entities.editor.field.environment'), draft.entity.environment || '', t('entities.editor.placeholder.environment'), value => updateEntityField('environment', value), 'environment')}
+                      {renderField(t('entities.editor.field.subtype'), draft.entity.subtype || '', t('entities.editor.placeholder.subtype'), value => updateEntityField('subtype', value), 'subtype')}
+                      {renderField(t('entities.editor.field.owner'), draft.entity.owner || '', t('entities.editor.placeholder.owner'), value => updateEntityField('owner', value), 'owner')}
+                      {renderField(t('entities.editor.field.system'), draft.entity.system || '', t('entities.editor.placeholder.system'), value => updateEntityField('system', value), 'system')}
+                      {renderField(t('entities.editor.field.source'), draft.entity.source || '', t('entities.editor.placeholder.source'), value => updateEntityField('source', value), 'source')}
                     </div>
                     <label className="grid gap-2">
                       <span className={coldFieldLabelTextClassName}>{t('entities.editor.field.description')}</span>
@@ -1148,8 +1151,8 @@ export function EntityEditorSurface({
 
                 {activeStage === 'ownership' ? (
                   <div className="grid gap-3 md:grid-cols-2">
-                    {renderField(t('entities.editor.field.owner'), draft.entity.owner || '', 'payments-team', value => updateEntityField('owner', value))}
-                    {renderField(t('entities.editor.field.runbook'), draft.entity.runbook || '', 'https://runbook.internal/...', value => updateEntityField('runbook', value))}
+                    {renderField(t('entities.editor.field.owner'), draft.entity.owner || '', 'payments-team', value => updateEntityField('owner', value), 'owner')}
+                    {renderField(t('entities.editor.field.runbook'), draft.entity.runbook || '', 'https://runbook.internal/...', value => updateEntityField('runbook', value), 'runbook')}
                     <ContactEditor
                       value={contacts}
                       onChange={setContacts}
diff --git a/web-next/components/pages/entity-list-surface.test.tsx b/web-next/components/pages/entity-list-surface.test.tsx
index 29f5fc3eec..2726b080bc 100644
--- a/web-next/components/pages/entity-list-surface.test.tsx
+++ b/web-next/components/pages/entity-list-surface.test.tsx
@@ -35,16 +35,16 @@ describe('EntityListSurface', () => {
           {
             key: '1',
             name: 'checkout-service',
-            type: '服务',
-            environment: '本地',
-            status: '健康',
+            type: 'Service',
+            environment: 'Local',
+            status: 'Healthy',
             statusTone: 'success',
             health: {
               score: 84,
               scoreText: '84 / 100',
-              label: '健康评分 84',
-              copy: '采集 4 / 4 健康',
-              meta: '告警 2 · 异常 0',
+              label: 'Health score 84',
+              copy: 'Collected 4 / 4 healthy',
+              meta: 'Alerts 2 · exceptions 0',
               tone: 'warning'
             },
             monitorCount: '1',
@@ -87,21 +87,21 @@ describe('EntityListSurface', () => {
     expect(html).toContain('data-entity-list-row-actions="cold-inline-actions"');
     expect(html).toContain('data-entity-list-health-affordance="lightweight-service-health"');
     expect(html).toContain('data-entity-health-score="84"');
-    expect(html).toContain('对象目录');
-    expect(html).toContain('对象优先调查');
-    expect(html).toContain('实体总数');
-    expect(html).toContain('活跃异常对象');
-    expect(html).toContain('搜索实体名称、命名空间、负责人');
-    expect(html).toContain('设置负责人');
-    expect(html).toContain('创建实体');
-    expect(html).toContain('从遥测发现');
-    expect(html).toContain('导入定义');
-    expect(html).toContain('显示 1-1 / 1');
+    expect(html).toContain(t('entities.list.title'));
+    expect(html).toContain(t('entities.list.kicker'));
+    expect(html).toContain(t('entities.list.metric.total'));
+    expect(html).toContain(t('entities.list.metric.abnormal'));
+    expect(html).toContain(t('entities.list.search.placeholder'));
+    expect(html).toContain(t('entities.list.row.action.owner'));
+    expect(html).toContain(t('entities.list.action.create'));
+    expect(html).toContain(t('entities.list.action.discovery'));
+    expect(html).toContain(t('entities.list.action.import'));
+    expect(html).toContain(t('entities.list.table.range', { from: 1, to: 1, total: 1 }));
     expect(html).toContain('checkout-service');
-    expect(html).toContain('健康评分 84');
-    expect(html).toContain('采集 4 / 4 健康');
-    expect(html).not.toContain('补负责人');
-    expect(html).not.toContain('选择环境 · 全部环境');
+    expect(html).toContain('Health score 84');
+    expect(html).toContain('Collected 4 / 4 healthy');
+    expect(html).not.toContain('\u8865\u8d1f\u8d23\u4eba');
+    expect(html).not.toContain('\u9009\u62e9\u73af\u5883 \u00b7 \u5168\u90e8\u73af\u5883');
     expect(html).not.toContain('data-cold-search-input-shell');
     expect(html).not.toContain('data-entity-list-rail=');
     expect(html).not.toContain('data-entity-list-action-panel=');
@@ -119,8 +119,8 @@ describe('EntityListSurface', () => {
     expect(source).not.toContain('data-cold-search-input-shell');
     expect(source).not.toContain('coldEntityVisual.search.row');
     expect(source).not.toContain('coldEntityVisual.search.input');
-    expect(source).not.toContain('补负责人');
-    expect(source).not.toContain('选择环境 ·');
+    expect(source).not.toContain('\u8865\u8d1f\u8d23\u4eba');
+    expect(source).not.toContain('\u9009\u62e9\u73af\u5883 \u00b7');
     expect(source).not.toContain('UserPlus');
     expect(source).not.toContain('WorkbenchPage');
     expect(source).not.toContain('from \'../workbench/primitives\'');
diff --git a/web-next/components/pages/exception-center-surface.test.tsx b/web-next/components/pages/exception-center-surface.test.tsx
index 2c9b7a5f2b..a4871e8aac 100644
--- a/web-next/components/pages/exception-center-surface.test.tsx
+++ b/web-next/components/pages/exception-center-surface.test.tsx
@@ -23,7 +23,7 @@ vi.mock('../providers/i18n-provider', () => ({
 }));
 
 describe('ExceptionCenterSurface', () => {
-  it('renders a HertzBeat-native Chinese exception explorer without English shell copy', () => {
+  it('renders a HertzBeat-native localized exception explorer without English shell copy', () => {
     const html = renderToStaticMarkup(<ExceptionCenterSurface type="500" />);
 
     expect(html).toContain('data-exception-center-surface="hertzbeat-exceptions"');
@@ -31,18 +31,18 @@ describe('ExceptionCenterSurface', () => {
     expect(html).toContain('data-exception-filter-sidebar="hertzbeat-exception-filters"');
     expect(html).toContain('data-exception-query-bar="hertzbeat-error-query"');
     expect(html).toContain('data-exception-table="hertzbeat-exception-list"');
-    expect(html).toContain('异常中心');
-    expect(html).toContain('筛选');
-    expect(html).toContain('aria-label="刷新筛选"');
-    expect(html).toContain('部署环境');
-    expect(html).toContain('服务');
-    expect(html).toContain('最近 7 天');
-    expect(html).toContain('运行查询');
-    expect(html).toContain('异常类型');
-    expect(html).toContain('错误信息');
-    expect(html).toContain('次数');
-    expect(html).toContain('最后出现');
-    expect(html).toContain('应用');
+    expect(html).toContain(t('exception.chrome.title'));
+    expect(html).toContain(t('exception.chrome.filter-title'));
+    expect(html).toContain(`aria-label="${t('exception.chrome.refresh-filters')}"`);
+    expect(html).toContain(t('exception.filters.deployment-environment'));
+    expect(html).toContain(t('exception.filters.service'));
+    expect(html).toContain(t('exception.time.last-7d'));
+    expect(html).toContain(t('exception.action.run-query'));
+    expect(html).toContain(t('exception.table.type'));
+    expect(html).toContain(t('exception.table.message'));
+    expect(html).toContain(t('exception.table.count'));
+    expect(html).toContain(t('exception.table.last-seen'));
+    expect(html).toContain(t('exception.table.application'));
     expect(html).toContain('ECONNRESET');
     expect(html).toContain('href="/exception/500?error=econnreset-browser-frontend"');
     expect(html).toContain('href="/exception/500?error=payment-402-checkout"');
@@ -67,9 +67,9 @@ describe('ExceptionCenterSurface', () => {
     expect(html).toContain('href="/overview"');
     expect(html).toContain('href="/log/manage"');
     expect(html).toContain('href="/trace/manage"');
-    expect(html).toContain('打开总览');
-    expect(html).toContain('日志工作台');
-    expect(html).toContain('链路工作台');
+    expect(html).toContain(t('menu.dashboard.back'));
+    expect(html).toContain(t('menu.log.manage'));
+    expect(html).toContain(t('menu.trace.manage'));
   });
 
   it('does not keep old workbench/card owners inside the HertzBeat exception explorer', () => {
diff --git a/web-next/components/pages/label-manage-surface.test.tsx b/web-next/components/pages/label-manage-surface.test.tsx
index 8665493263..2b1a81b493 100644
--- a/web-next/components/pages/label-manage-surface.test.tsx
+++ b/web-next/components/pages/label-manage-surface.test.tsx
@@ -7,32 +7,11 @@ import { createTranslatorMock } from '../../test/i18n-test-helper';
 import { LabelManageSurface } from './label-manage-surface';
 
 describe('label manage surface', () => {
+  const zh = createTranslatorMock({ locale: 'zh-CN' });
+  const han = (...codes: number[]) => String.fromCodePoint(...codes);
+
   it('renders the OTLP cold-matte label console, Angular card grid, and authoring dialog shell', () => {
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.labels': '标签管理',
-        'common.refresh': '刷新',
-        'common.button.new': '新增',
-        'common.search': '搜索',
-        'common.clear': '清除',
-        'common.copy.button': '复制',
-        'common.button.edit': '编辑',
-        'common.button.delete': '删除选中项',
-        'common.button.cancel': '取消',
-        'common.button.save': '保存',
-        'common.none': '无标签预览',
-        'label.search': '搜索标签',
-        'label.new': '新增标签',
-        'label.edit': '编辑标签',
-        'label.name': '标签名',
-        'label.value': '标签值',
-        'label.description': '标签描述',
-        'label.display': '效果',
-        'setting.labels.empty.title': '暂无标签',
-        'setting.labels.empty.copy': '请先新增标签。'
-      }
-    });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -82,7 +61,7 @@ describe('label manage surface', () => {
         deleteTarget={null}
         isDeletePending={false}
         isSavePending
-        actionMessage="新增成功"
+        actionMessage={t('common.notify.new-success')}
         formatTime={() => '2026-04-10 18:00:00'}
         onSearchChange={() => {}}
         onSearch={() => {}}
@@ -201,7 +180,7 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-description-display-owner="label-manage-surface"');
     expect(html).toContain('data-hz-ui="inline-feedback"');
     expect(html).toContain('data-hz-feedback-tone="success"');
-    expect(html).toContain('新增成功');
+    expect(html).toContain(t('common.notify.new-success'));
     expect(html).toContain('data-cold-search-row-owner="cold-search-row"');
     expect(html).toContain('data-cold-search-input="fixed-width-direct"');
     expect(html).toContain('data-cold-search-control="direct-input"');
@@ -231,42 +210,32 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-row-action-label="team:ops"');
     expect(html).not.toContain('data-label-summary-rail=');
     expect(html).not.toContain('data-cold-search-input-shell');
-    expect(html).toContain('标签管理');
-    expect(html).toContain('新增');
-    expect(html).toContain('搜索标签');
-    expect(html).toContain('搜索');
-    expect(html).toContain('aria-label="复制标签 team:ops"');
-    expect(html).toContain('aria-label="编辑标签 team:ops"');
-    expect(html).toContain('aria-label="删除标签 team:ops"');
+    expect(html).toContain(t('menu.advanced.labels'));
+    expect(html).toContain(t('common.button.new'));
+    expect(html).toContain(t('label.search'));
+    expect(html).toContain(t('common.search'));
+    expect(html).toContain(`aria-label="${t('setting.labels.action.copy-aria', { name: 'team:ops' })}"`);
+    expect(html).toContain(`aria-label="${t('setting.labels.action.edit-aria', { name: 'team:ops' })}"`);
+    expect(html).toContain(`aria-label="${t('setting.labels.action.delete-aria', { name: 'team:ops' })}"`);
     expect(html).toContain('href="/monitors?labels=team%3Aops"');
     expect(html).toContain('team:ops');
     expect(html).toContain('source:system');
     expect(html).toContain('ops team');
-    expect(html).not.toContain('用户标签');
+    expect(html).not.toContain(t('setting.labels.type.user'));
     expect(html).toContain('data-overlay-dialog="true"');
-    expect(html).toContain('新增标签');
-    expect(html).toContain('标签名');
-    expect(html).toContain('标签值');
-    expect(html).toContain('标签描述');
-    expect(html).toContain('效果');
-    expect(html).toContain('取消');
-    expect(html).toContain('保存');
+    expect(html).toContain(t('label.new'));
+    expect(html).toContain(t('label.name'));
+    expect(html).toContain(t('label.value'));
+    expect(html).toContain(t('label.description'));
+    expect(html).toContain(t('label.display'));
+    expect(html).toContain(t('common.button.cancel'));
+    expect(html).toContain(t('common.button.save'));
     expect(html).not.toContain('data-label-table="cold-label-table"');
     expect(html).not.toContain('setting.labels.subtitle');
   }, 30000);
 
   it('renders a cold card empty state with Chinese fallback copy when no labels exist', () => {
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.labels': '标签管理',
-        'common.refresh': '刷新',
-        'common.button.new': '新增',
-        'common.search': '搜索',
-        'common.clear': '清除',
-        'label.search': '搜索标签'
-      }
-    });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -303,14 +272,14 @@ describe('label manage surface', () => {
 
     expect(html).toContain('data-label-empty-state="cold-card-empty"');
     expect(html).toContain('data-label-empty-icon="cold-empty-box"');
-    expect(html).toContain('暂无标签');
-    expect(html).toContain('请先新增标签。');
+    expect(html).toContain(t('setting.labels.empty.title'));
+    expect(html).toContain(t('setting.labels.empty.copy'));
     expect(html).not.toContain('setting.labels.empty.title');
     expect(html).not.toContain('setting.labels.empty.copy');
   });
 
   it('keeps Angular formatLabelName raw tag value spacing for display, copy, and monitor handoff', () => {
-    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -362,7 +331,7 @@ describe('label manage surface', () => {
   });
 
   it('renders whitespace-only descriptions because Angular only checks description truthiness', () => {
-    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -414,27 +383,7 @@ describe('label manage surface', () => {
   });
 
   it('keeps the new-label dialog preview hidden until the Angular name-defined path runs', () => {
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.labels': '标签管理',
-        'common.refresh': '刷新',
-        'common.button.new': '新增',
-        'common.search': '搜索',
-        'common.button.cancel': '取消',
-        'common.button.save': '保存',
-        'common.none': '无标签预览',
-        'label.search': '搜索标签',
-        'label.new': '新增标签',
-        'label.edit': '编辑标签',
-        'label.name': '标签名',
-        'label.value': '标签值',
-        'label.description': '标签描述',
-        'label.display': '效果',
-        'setting.labels.empty.title': '暂无标签',
-        'setting.labels.empty.copy': '请先新增标签。'
-      }
-    });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -492,31 +441,11 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-optional-save-payload-owner="label-manage-controller"');
     expect(html).toContain('data-label-type-save-payload-contract="angular-preserve-implicit-type"');
     expect(html).toContain('data-label-type-save-payload-owner="label-manage-controller"');
-    expect(html).not.toContain('无标签预览');
+    expect(html).not.toContain(han(0x65e0, 0x6807, 0x7b7e, 0x9884, 0x89c8));
   });
 
   it('shows the Angular display preview row once the label name is defined', () => {
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.labels': '标签管理',
-        'common.refresh': '刷新',
-        'common.button.new': '新增',
-        'common.search': '搜索',
-        'common.button.cancel': '取消',
-        'common.button.save': '保存',
-        'common.none': '无标签预览',
-        'label.search': '搜索标签',
-        'label.new': '新增标签',
-        'label.edit': '编辑标签',
-        'label.name': '标签名',
-        'label.value': '标签值',
-        'label.description': '标签描述',
-        'label.display': '效果',
-        'setting.labels.empty.title': '暂无标签',
-        'setting.labels.empty.copy': '请先新增标签。'
-      }
-    });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -559,27 +488,7 @@ describe('label manage surface', () => {
   });
 
   it('marks the missing label name dirty only after the Angular OK-submit validation path runs', () => {
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.labels': '标签管理',
-        'common.refresh': '刷新',
-        'common.button.new': '新增',
-        'common.search': '搜索',
-        'common.button.cancel': '取消',
-        'common.button.save': '保存',
-        'common.none': '无标签预览',
-        'validation.required': '必填项',
-        'label.search': '搜索标签',
-        'label.new': '新增标签',
-        'label.name': '标签名',
-        'label.value': '标签值',
-        'label.description': '标签描述',
-        'label.display': '效果',
-        'setting.labels.empty.title': '暂无标签',
-        'setting.labels.empty.copy': '请先新增标签。'
-      }
-    });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -611,32 +520,12 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-dialog-name-state="dirty-invalid"');
     expect(html).toContain('data-label-save-submit-state="dirty-invalid"');
     expect(html).toContain('data-label-dialog-name-validation="validation.required"');
-    expect(html).toContain('必填项');
+    expect(html).toContain(t('validation.required'));
     expect(html).not.toContain('disabled=""');
   });
 
   it('does not mark a whitespace-only label name invalid before Angular trim-before-save runs', () => {
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.labels': '标签管理',
-        'common.refresh': '刷新',
-        'common.button.new': '新增',
-        'common.search': '搜索',
-        'common.button.cancel': '取消',
-        'common.button.save': '保存',
-        'common.none': '无标签预览',
-        'validation.required': '必填项',
-        'label.search': '搜索标签',
-        'label.new': '新增标签',
-        'label.name': '标签名',
-        'label.value': '标签值',
-        'label.description': '标签描述',
-        'label.display': '效果',
-        'setting.labels.empty.title': '暂无标签',
-        'setting.labels.empty.copy': '请先新增标签。'
-      }
-    });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -670,25 +559,11 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-dialog-name-state="pristine"');
     expect(html).toContain('data-label-save-submit-state="pristine"');
     expect(html).not.toContain('data-label-dialog-name-validation="validation.required"');
-    expect(html).not.toContain('必填项');
+    expect(html).not.toContain(t('validation.required'));
   });
 
   it('renders the shared Angular-style delete confirmation dialog when a label is targeted', () => {
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.labels': '标签管理',
-        'common.confirm.delete': '请确认是否删除!',
-        'common.button.cancel': '取消',
-        'common.button.ok': '确定',
-        'common.refresh': '刷新',
-        'common.button.new': '新增',
-        'common.search': '搜索',
-        'label.search': '搜索标签',
-        'setting.labels.empty.title': '暂无标签',
-        'setting.labels.empty.copy': '请先新增标签。'
-      }
-    });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -740,12 +615,12 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-delete-confirm-close="angular-close-before-delete-result"');
     expect(html).toContain('data-label-delete-confirm-close-owner="route-state-contract"');
     expect(html).toContain('data-label-delete-confirm-target="team:ops"');
-    expect(html).toContain('请确认是否删除!');
-    expect(html).toContain('确定');
+    expect(html).toContain(t('common.confirm.delete'));
+    expect(html).toContain(t('common.button.ok'));
   });
 
   it('renders Angular label delete failure title with backend detail', () => {
-    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -762,7 +637,7 @@ describe('label manage surface', () => {
         draftLabel={null}
         isManageModalAdd={false}
         deleteTarget={null}
-        actionError="删除失败!"
+        actionError={t('common.notify.delete-fail')}
         actionMeta="backend refused label delete"
         formatTime={() => '-'}
         onSearchChange={() => {}}
@@ -784,12 +659,12 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-delete-feedback="angular-delete-fail"');
     expect(html).toContain('data-label-delete-feedback-title="common.notify.delete-fail"');
     expect(html).toContain('data-label-delete-feedback-detail="backend-message"');
-    expect(html).toContain('删除失败!');
+    expect(html).toContain(t('common.notify.delete-fail'));
     expect(html).toContain('backend refused label delete');
   });
 
   it('renders Angular label save failure title with backend detail', () => {
-    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
@@ -806,7 +681,7 @@ describe('label manage surface', () => {
         draftLabel={null}
         isManageModalAdd={false}
         deleteTarget={null}
-        actionError="修改失败!"
+        actionError={t('common.notify.edit-fail')}
         actionMeta="backend refused label edit"
         formatTime={() => '-'}
         onSearchChange={() => {}}
@@ -829,12 +704,12 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-save-failure-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('data-label-save-feedback-title="common.notify.edit-fail"');
     expect(html).toContain('data-label-save-feedback-detail="backend-message"');
-    expect(html).toContain('修改失败!');
+    expect(html).toContain(t('common.notify.edit-fail'));
     expect(html).toContain('backend refused label edit');
   });
 
   it('keeps the Angular label shell visible without action feedback on load failure', () => {
-    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const t = zh;
 
     const html = renderToStaticMarkup(
       <LabelManageSurface
diff --git a/web-next/components/pages/log-integration-redirect-shell.test.tsx b/web-next/components/pages/log-integration-redirect-shell.test.tsx
index fc606ecb1b..63461f0ec4 100644
--- a/web-next/components/pages/log-integration-redirect-shell.test.tsx
+++ b/web-next/components/pages/log-integration-redirect-shell.test.tsx
@@ -27,6 +27,7 @@ vi.mock('../ui/button', () => ({
 
 describe('log integration redirect shell', () => {
   it('renders the shared placeholder shell with HertzBeat OTLP intake and manage CTAs', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const { LogIntegrationRedirectShell } = await import('./log-integration-redirect-shell');
     const html = renderToStaticMarkup(
       <LogIntegrationRedirectShell
@@ -38,9 +39,9 @@ describe('log integration redirect shell', () => {
     expect(html).toContain('data-parity-app-shell="true"');
     expect(html).toContain('data-parity-app-header="true"');
     expect(html).toContain('data-parity-placeholder-shell="true"');
-    expect(html).toContain('日志接入已合并到可观测接入');
-    expect(html).toContain('前往可观测接入');
-    expect(html).toContain('前往日志工作台');
+    expect(html).toContain(t('log.integration.redirect.title'));
+    expect(html).toContain(t('log.integration.redirect.cta.ingestion'));
+    expect(html).toContain(t('log.integration.redirect.cta.manage'));
     expect(html).not.toContain('Observability Intake');
   });
 
diff --git a/web-next/components/pages/login-form.test.tsx b/web-next/components/pages/login-form.test.tsx
index fb4100d697..f80913690f 100644
--- a/web-next/components/pages/login-form.test.tsx
+++ b/web-next/components/pages/login-form.test.tsx
@@ -6,21 +6,21 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 
-const ZH_CN_LABEL = '简体中文(zh_CN)';
+const zhT = createTranslatorMock({ locale: 'zh-CN' });
+const ZH_CN_LABEL = zhT('settings.system-config.locale.zh_CN');
 const JA_JP_LABEL = 'Japanese(ja_JP)';
 const LOGIN_NOTICE_COPY =
   '\u767b\u5f55\u6210\u529f\u540e\u4f1a\u81ea\u52a8\u6062\u590d\u5f53\u524d\u5de5\u4f5c\u53f0\u4f1a\u8bdd\uff0c\u5e76\u5728\u9700\u8981\u65f6\u5c1d\u8bd5\u5237\u65b0\u4ee4\u724c\u3002';
-const HERO_TITLE = '开源私有化企业运维可观测平台';
-const HERO_LEAD = '采集器、监控模板、实体、指标、日志和链路';
-const HERO_FOCUS = '处理告警并关闭问题';
-const HERO_BODY =
-  '通过采集器接入应用、数据库、操作系统、中间件和网络设备指标,数据留在私有化部署内。';
-const LOGIN_HEADING = '登入 HertzBeat';
-const USERNAME_PROMPT = '请输入用户名';
-const PASSWORD_PROMPT = '请输入密码';
-const PASSWORD_TOGGLE_LABEL = '显示密码';
-const REMEMBER_ME_LABEL = '记住我';
-const LOGIN_BUTTON = '登录';
+const HERO_TITLE = zhT('app.passport.desc');
+const HERO_LEAD = zhT('app.passport.intro-1');
+const HERO_FOCUS = zhT('app.passport.intro-2');
+const HERO_BODY = zhT('about.point.1');
+const LOGIN_HEADING = zhT('app.login.tab-login-credentials');
+const USERNAME_PROMPT = zhT('app.login.message-need-identifier');
+const PASSWORD_PROMPT = zhT('app.login.message-need-credential');
+const PASSWORD_TOGGLE_LABEL = zhT('app.login.password-show');
+const REMEMBER_ME_LABEL = zhT('app.login.remember-me');
+const LOGIN_BUTTON = zhT('app.login.login');
 
 vi.mock('next/navigation', () => ({
   useRouter: () => ({
@@ -34,7 +34,7 @@ vi.mock('next/image', () => ({
 
 vi.mock('../providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock({ locale: 'zh-CN' }),
+    t: zhT,
     locale: 'zh-CN',
     locales: [
       { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: '🇬🇧' },
@@ -143,7 +143,7 @@ describe('LoginForm', () => {
     expect(html).toContain('Apache HertzBeat™');
     expect(html).toContain('Apache HertzBeat™ v1.8.0');
     expect(html).toContain('Copyright ©');
-    expect(html).not.toContain('遵循 Apache License, Version 2.0 授权');
+    expect(html).not.toContain(zhT('platform.footer.license'));
     expect(html).not.toContain(LOGIN_NOTICE_COPY);
     expect(html).not.toContain('value="admin"');
     expect(html).not.toContain('value="hertzbeat"');
diff --git a/web-next/components/pages/monitor-editor-surface.test.tsx b/web-next/components/pages/monitor-editor-surface.test.tsx
index 9537e8f561..fb8b09758b 100644
--- a/web-next/components/pages/monitor-editor-surface.test.tsx
+++ b/web-next/components/pages/monitor-editor-surface.test.tsx
@@ -77,8 +77,8 @@ vi.mock('../ui/number-stepper', () => ({
   NumberStepper: ({ value, onValueChange: _onValueChange, ...props }: any) => (
     <span data-cold-number-stepper-owner="cold-number-stepper">
       <input type="text" data-cold-number-stepper-input="true" value={value} readOnly {...props} />
-      <button type="button" data-cold-number-stepper-action="decrement">减少</button>
-      <button type="button" data-cold-number-stepper-action="increment">增加</button>
+      <button type="button" data-cold-number-stepper-action="decrement">Decrease</button>
+      <button type="button" data-cold-number-stepper-action="increment">Increase</button>
     </span>
   )
 }));
@@ -217,6 +217,8 @@ vi.mock('@/lib/utils', () => ({
 }));
 
 describe('MonitorEditorSurface', () => {
+  const zhT = createTranslatorMock({ locale: 'zh-CN' });
+
   it('renders structured label and annotation rows instead of raw JSON textareas', () => {
     const html = renderToStaticMarkup(
       <MonitorEditorSurface
@@ -303,8 +305,8 @@ describe('MonitorEditorSurface', () => {
         />
       );
 
-      expect(html).toContain('添加标签');
-      expect(html).toContain('添加注解');
+      expect(html).toContain(zhT('monitor.editor.labels.add'));
+      expect(html).toContain(zhT('monitor.editor.annotations.add'));
       expect(html).not.toContain('Add label');
       expect(html).not.toContain('Add annotation');
     } finally {
@@ -333,7 +335,7 @@ describe('MonitorEditorSurface', () => {
             collector: '',
             grafanaDashboard: { enabled: false },
             params: [{ field: 'host', paramValue: '127.0.0.1' }],
-            paramDefines: [{ field: 'host', type: 'text', name: '目标Host', required: true }],
+            paramDefines: [{ field: 'host', type: 'text', name: 'Target Host', required: true }],
             advancedParams: [],
             advancedParamDefines: [],
             scrapeParams: [],
@@ -345,11 +347,11 @@ describe('MonitorEditorSurface', () => {
         />
       );
 
-      expect(html).toContain('默认系统调度');
+      expect(html).toContain(zhT('monitor.collector.system.default'));
       expect(html).toContain('data-monitor-editor-collector-selection="angular-collectors-selection-tags"');
       expect(html).toContain('data-monitor-editor-collector-tags="angular-status-ip-mode-tags"');
       expect(html).toContain('data-monitor-editor-collector-tag="system-default"');
-      expect(html).not.toContain('<option value="" selected="">无</option>');
+      expect(html).not.toMatch(/<option value="" selected="">\u65e0<\/option>/);
     } finally {
       i18nMockState.locale = 'en-US';
     }
@@ -376,7 +378,7 @@ describe('MonitorEditorSurface', () => {
             collector: 'edge-a',
             grafanaDashboard: { enabled: false },
             params: [{ field: 'host', paramValue: '127.0.0.1' }],
-            paramDefines: [{ field: 'host', type: 'text', name: '目标Host', required: true }],
+            paramDefines: [{ field: 'host', type: 'text', name: 'Target Host', required: true }],
             advancedParams: [],
             advancedParamDefines: [],
             scrapeParams: [],
diff --git a/web-next/components/pages/passport-shell.test.tsx b/web-next/components/pages/passport-shell.test.tsx
index 0ef944d5f0..9eb9c7a954 100644
--- a/web-next/components/pages/passport-shell.test.tsx
+++ b/web-next/components/pages/passport-shell.test.tsx
@@ -46,6 +46,7 @@ vi.mock('../shell/platform-copyright-footer', () => ({
 
 describe('PassportShell', () => {
   it('renders the shared auth shell and panel structure on the cold-workbench baseline', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
     const html = renderToStaticMarkup(
       <PassportShell panelClassName="max-w-[368px]">
         <PassportPanel title="Sign In HertzBeat">
@@ -83,12 +84,12 @@ describe('PassportShell', () => {
     expect(html).toContain('data-passport-footer-band="angular-raised"');
     expect(html).toContain('/assets/bg.png');
     expect(html).toContain('Apache HertzBeat™ v1.8.0');
-    expect(html).toContain('开源私有化企业运维可观测平台');
-    expect(html).toContain('采集器、监控模板、实体、指标、日志和链路');
-    expect(html).toContain('处理告警并关闭问题');
-    expect(html).toContain('通过采集器接入应用、数据库、操作系统、中间件和网络设备指标,数据留在私有化部署内。');
-    expect(html).toContain('按网络区域扩展 Collector 集群,支撑私有化、隔离采集和状态页。');
-    expect(html).not.toContain('遵循 Apache License, Version 2.0 授权');
+    expect(html).toContain(t('app.passport.desc'));
+    expect(html).toContain(t('app.passport.intro-1'));
+    expect(html).toContain(t('app.passport.intro-2'));
+    expect(html).toContain(t('about.point.1'));
+    expect(html).toContain(t('about.point.6'));
+    expect(html).not.toContain(t('platform.footer.license'));
     expect(html).not.toContain('Apache License, Version 2.0');
   });
 
diff --git a/web-next/components/pages/plugin-manage-surface.test.tsx b/web-next/components/pages/plugin-manage-surface.test.tsx
index 59f980c6b8..5389b44bd0 100644
--- a/web-next/components/pages/plugin-manage-surface.test.tsx
+++ b/web-next/components/pages/plugin-manage-surface.test.tsx
@@ -8,13 +8,7 @@ import { createTranslatorMock } from '../../test/i18n-test-helper';
 describe('plugin manage surface', () => {
   it('renders the OTLP cold-matte plugin console, dense table, translated types, and upload dialog shell', async () => {
     const { PluginManageSurface } = await import('./plugin-manage-surface');
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'common.none': '无类型',
-        'common.file.select': '选择文件'
-      }
-    });
+    const t = createTranslatorMock({ locale: 'zh-CN' });
 
     const html = renderToStaticMarkup(
       <PluginManageSurface
@@ -115,7 +109,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-table-fill-stage="viewport-fill-on-empty-or-short"');
     expect(html).toContain('data-plugin-manage-table="cold-plugin-table"');
     expect(html).toContain('data-plugin-table-columns="angular-five-column-edit-actions"');
-    expect(html).not.toContain('>参数</th>');
+    expect(html).not.toContain(`>${t('setting.plugins.table.params')}</th>`);
     expect(html).toContain('data-plugin-pagination="cold-dense-pagination"');
     expect(html).toContain('data-plugin-pagination-owner="hertzbeat-ui-pagination-bar"');
     expect(html).toContain('data-plugin-pagination-contract="angular-search-pagination"');
@@ -165,26 +159,26 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-row-action-label="smtp"');
     expect(html).not.toContain('data-plugin-summary-rail=');
     expect(html).not.toContain('data-cold-search-input-shell');
-    expect(html).toContain('插件管理');
-    expect(html).toContain('上传插件');
-    expect(html).toContain('搜索插件');
-    expect(html).toContain('插件名称');
-    expect(html).toContain('插件类型');
-    expect(html).toContain('启用状态');
-    expect(html).toContain('操作');
+    expect(html).toContain(t('menu.advanced.plugins'));
+    expect(html).toContain(t('plugin.upload'));
+    expect(html).toContain(t('plugin.search'));
+    expect(html).toContain(t('plugin.name'));
+    expect(html).toContain(t('plugin.type'));
+    expect(html).toContain(t('plugin.status'));
+    expect(html).toContain(t('common.edit'));
     expect(html).toContain('smtp');
     expect(html).toContain('slack');
-    expect(html).toContain('告警后');
-    expect(html).toContain('采集后');
-    expect(html).toContain('无类型');
-    expect(html).toContain('已启用');
-    expect(html).toContain('已停用');
-    expect(html).toContain('已选择');
-    expect(html).toContain('第 1 / 1 页 · 1-2 / 2');
+    expect(html).toContain(t('plugin.type.POST_ALERT'));
+    expect(html).toContain(t('plugin.type.POST_COLLECT'));
+    expect(html).toContain(t('common.none'));
+    expect(html).toContain(t('common.enabled'));
+    expect(html).toContain(t('common.disabled'));
+    expect(html).toContain(t('setting.plugins.selected-count', { count: 1 }));
+    expect(html).toContain(t('setting.plugins.pagination.summary', { page: 1, totalPages: 1, from: 1, to: 2, total: 2 }));
     expect(html).not.toContain('POST_ALERT');
     expect(html).not.toContain('POST_COLLECT');
-    expect(html).toContain('编辑插件参数 smtp');
-    expect(html).toContain('删除插件 smtp');
+    expect(html).toContain(t('setting.plugins.action.params-aria', { name: 'smtp' }));
+    expect(html).toContain(t('setting.plugins.action.delete-aria', { name: 'smtp' }));
     expect(html).toContain('data-overlay-dialog="true"');
     expect(html).toContain('data-overlay-dialog-mask-closable="false"');
     expect(html).toContain('data-plugin-upload-mask-closable="false"');
@@ -199,10 +193,10 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-upload-status-control-owner="hertzbeat-ui-switch"');
     expect(html).toContain('data-hz-ui="switch"');
     expect(html).toContain('role="switch"');
-    expect(html).toContain('Jar包');
-    expect(html).toContain('选择文件');
-    expect(html).toContain('取消');
-    expect(html).toContain('保存');
+    expect(html).toContain(t('plugin.jar.file'));
+    expect(html).toContain(t('common.file.select'));
+    expect(html).toContain(t('common.button.cancel'));
+    expect(html).toContain(t('common.button.save'));
     expect(html).not.toContain('data-plugin-manage-route="angular-plugin-table"');
     expect(html).not.toContain('data-plugin-manage-table="angular-nz-table"');
   });
@@ -248,7 +242,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('colSpan="5"');
     expect(html).toContain('data-plugin-manage-empty-state="cold-table-empty"');
     expect(html).toContain('data-plugin-manage-empty-icon="cold-empty-box"');
-    expect(html).toContain('暂无数据');
+    expect(html).toContain(t('common.no-data'));
   });
 
   it('keeps the Angular plugin management shell mounted without showing backend load errors', async () => {
@@ -292,7 +286,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-manage-toolbar="cold-table-toolbar"');
     expect(html).toContain('data-plugin-manage-table-shell="cold-dense-table"');
     expect(html).toContain('data-plugin-manage-empty-state="cold-table-empty"');
-    expect(html).toContain('插件管理');
+    expect(html).toContain(t('menu.advanced.plugins'));
     expect(html).not.toContain('backend refused plugin load');
     expect(html).not.toContain('data-observability-status');
   });
@@ -683,7 +677,7 @@ describe('plugin manage surface', () => {
         search=""
         selectedIds={[]}
         draftPlugin={null}
-        actionError="请先选择要删除的监控"
+        actionError={t('common.notify.no-select-delete')}
         actionTone="warning"
         actionKind="delete"
         deleteTarget={{
@@ -727,8 +721,8 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
     expect(html).toContain('data-plugin-delete-confirm-submit="angular-modal-confirm"');
     expect(html).toContain('data-plugin-delete-confirm-target="webhook"');
-    expect(html).toContain('请先选择要删除的监控');
-    expect(html).toContain('请确认是否删除!');
+    expect(html).toContain(t('common.notify.no-select-delete'));
+    expect(html).toContain(t('common.confirm.delete'));
   });
 
   it('renders Angular plugin upload required validation, jar file input, and save feedback contracts', async () => {
@@ -833,7 +827,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-upload-validation="name-required"');
     expect(html).toContain('data-plugin-upload-validation="jar-required"');
     expect(html).toContain('Add Success!');
-    expect(html).toContain('请填充必填项!');
+    expect(html).toContain(t('validation.required'));
   });
 
   it('renders Angular plugin upload failure title with backend detail', async () => {
@@ -877,7 +871,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-upload-feedback="angular-new-fail"');
     expect(html).toContain('data-plugin-upload-feedback-title="common.notify.new-fail"');
     expect(html).toContain('data-plugin-upload-feedback-detail="backend-message"');
-    expect(html).toContain('新增失败!');
+    expect(html).toContain(t('common.notify.new-fail'));
     expect(html).toContain('backend rejected duplicate plugin');
   });
 
@@ -899,7 +893,7 @@ describe('plugin manage surface', () => {
         search=""
         selectedIds={[]}
         draftPlugin={null}
-        actionError="删除失败!"
+        actionError={t('common.notify.delete-fail')}
         actionMeta="backend refused plugin delete"
         actionKind="delete"
         deleteTarget={null}
@@ -925,7 +919,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-delete-failure-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('data-plugin-delete-feedback-title="common.notify.delete-fail"');
     expect(html).toContain('data-plugin-delete-feedback-detail="backend-message"');
-    expect(html).toContain('删除失败!');
+    expect(html).toContain(t('common.notify.delete-fail'));
     expect(html).toContain('backend refused plugin delete');
   });
 
@@ -947,7 +941,7 @@ describe('plugin manage surface', () => {
         search=""
         selectedIds={[]}
         draftPlugin={null}
-        actionError="修改失败!"
+        actionError={t('common.notify.edit-fail')}
         actionMeta="backend refused plugin toggle"
         actionKind="enable"
         deleteTarget={null}
@@ -973,7 +967,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-enable-failure-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('data-plugin-enable-feedback-title="common.notify.edit-fail"');
     expect(html).toContain('data-plugin-enable-feedback-detail="backend-message"');
-    expect(html).toContain('修改失败!');
+    expect(html).toContain(t('common.notify.edit-fail'));
     expect(html).toContain('backend refused plugin toggle');
   });
 
@@ -1020,7 +1014,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-param-failure-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('data-plugin-param-feedback-title="common.notify.edit-fail"');
     expect(html).toContain('data-plugin-param-feedback-detail="backend-message"');
-    expect(html).toContain('修改失败!');
+    expect(html).toContain(t('common.notify.edit-fail'));
     expect(html).toContain('backend rejected plugin params');
   });
 
@@ -1053,15 +1047,15 @@ describe('plugin manage surface', () => {
         paramDraft={{
           plugin: { id: 31, name: 'webhook', enableStatus: true } as any,
           paramDefines: [
-            { field: 'endpoint', type: 'text', name: '端点', required: true },
-            { field: 'authType', type: 'radio', name: '认证类型', options: [{ label: 'Basic', value: 'basic' }, { label: 'Bearer', value: 'bearer' }] },
-            { field: 'script', type: 'textarea', name: '脚本' },
+            { field: 'endpoint', type: 'text', name: 'Endpoint', required: true },
+            { field: 'authType', type: 'radio', name: 'Authentication type', options: [{ label: 'Basic', value: 'basic' }, { label: 'Bearer', value: 'bearer' }] },
+            { field: 'script', type: 'textarea', name: 'Script' },
             { field: 'payload', type: 'json', name: 'JSON' },
-            { field: 'headers', type: 'key-value', name: '请求头' },
-            { field: 'labels', type: 'labels', name: '标签' },
-            { field: 'selector', type: 'label-selector', name: '标签选择器' },
-            { field: 'metrics', type: 'metrics-field', name: '指标字段' },
-            { field: 'recipients', type: 'array', name: '收件人' }
+            { field: 'headers', type: 'key-value', name: 'Request headers' },
+            { field: 'labels', type: 'labels', name: 'Labels' },
+            { field: 'selector', type: 'label-selector', name: 'Label selector' },
+            { field: 'metrics', type: 'metrics-field', name: 'Metric fields' },
+            { field: 'recipients', type: 'array', name: 'Recipients' }
           ],
           params: {
             endpoint: { pluginMetadataId: 31, type: 1, field: 'endpoint', paramValue: 'https://hooks.example' },
@@ -1241,13 +1235,7 @@ describe('plugin manage surface', () => {
 
   it('keeps the Angular plugin params modal open with an empty field set', async () => {
     const { PluginManageSurface } = await import('./plugin-manage-surface');
-    const t = createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'common.no-data': '暂无数据',
-        'plugin.edit': '编辑插件'
-      }
-    });
+    const t = createTranslatorMock({ locale: 'zh-CN' });
 
     const html = renderToStaticMarkup(
       <PluginManageSurface
@@ -1289,7 +1277,7 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-param-form="angular-dynamic-form-field"');
     expect(html).toContain('data-plugin-param-empty="angular-empty-params-modal"');
     expect(html).toContain('data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"');
-    expect(html).toContain('暂无数据');
+    expect(html).toContain(t('common.no-data'));
     expect(html).not.toContain('data-plugin-param-field=');
   });
 });
diff --git a/web-next/components/pages/setting-define-surface.test.tsx b/web-next/components/pages/setting-define-surface.test.tsx
index de10d09d0c..d66057abcd 100644
--- a/web-next/components/pages/setting-define-surface.test.tsx
+++ b/web-next/components/pages/setting-define-surface.test.tsx
@@ -59,43 +59,8 @@ describe('setting define monitor-template surface', () => {
     interactionContainer = null;
   });
 
-  const t = createTranslatorMock({
-    locale: 'zh-CN',
-    overrides: {
-      'setting.define.title': '定义',
-      'setting.define.subtitle': '管理监控模板 YAML,直接维护采集协议、参数和指标字段。',
-      'setting.define.action.new': '新增类型',
-      'common.button.edit': '编辑',
-      'common.button.cancel': '取消',
-      'setting.define.action.save-apply': '保存并应用',
-      'setting.define.action.delete': '删除 {{app}}',
-      'define.save-apply.confirm': '请确认是否保存修改并应用此监控类型定义? 这会影响到您的监控内容。',
-      'define.save-apply.no-code': '监控类型定义内容不能为空。',
-      'define.delete.confirm': '请确认是否删除 {{app}} 监控类型? 删除后无法添加此类监控。',
-      'define.hide-true.confirm': '确认是否显示此菜单?',
-      'define.hide-false.confirm': '确认是否隐藏此菜单?',
-      'setting.define.action.hide': '隐藏',
-      'setting.define.action.hide-aria': '隐藏模板 {{app}}',
-      'setting.define.action.show': '显示',
-      'setting.define.action.show-aria': '显示模板 {{app}}',
-      'setting.define.save.pending': '正在应用定义...',
-      'common.button.ok': '确定',
-      'common.search': '搜索',
-      'common.dark-mode': '深色模式',
-      'setting.define.empty.title': '暂无模板',
-      'setting.define.empty.copy': '请先新增监控模板 YAML。',
-      'setting.define.empty.search-title': '没有匹配的模板',
-      'setting.define.empty.search-copy': '调整搜索关键字后再试。',
-      'setting.define.template.visible': '可见',
-      'setting.define.template.hidden': '隐藏',
-      'setting.define.new-template': '新增模板草稿',
-      'setting.define.summary.yaml-lines': '{{count}} 行 YAML',
-      'setting.define.summary.mode.edit': '编辑',
-      'setting.define.summary.mode.new': '新增',
-      'setting.define.diff.original': '原始 YAML',
-      'setting.define.diff.current': '当前编辑 YAML'
-    }
-  });
+  const t = createTranslatorMock({ locale: 'zh-CN' });
+  const han = (...codes: number[]) => String.fromCodePoint(...codes);
 
   const data = {
     menuGroups: [
@@ -126,7 +91,7 @@ describe('setting define monitor-template surface', () => {
         yamlLabel="app-mysql.yml"
         darkMode
         isEditing
-        message="保存成功"
+        message={t('common.notify.apply-success')}
         onSearchChange={() => {}}
         onSearch={() => {}}
         onSelectApp={() => {}}
@@ -233,8 +198,8 @@ describe('setting define monitor-template surface', () => {
     expect(html).toContain('data-setting-define-template-visibility-label="MySQL"');
     expect(html).toContain('data-setting-define-template-visibility-action="hide"');
     expect(html).toContain('data-setting-define-template-visibility-next-hide="true"');
-    expect(html).toContain('aria-label="隐藏模板 MySQL"');
-    expect(html).toContain('title="隐藏模板 MySQL"');
+    expect(html).toContain(`aria-label="${t('setting.define.action.hide-aria', { app: 'MySQL' })}"`);
+    expect(html).toContain(`title="${t('setting.define.action.hide-aria', { app: 'MySQL' })}"`);
     expect(html).toContain('data-setting-define-new-action="angular-current-app-reset"');
     expect(html).toContain('data-setting-define-new-action-contract="angular-current-app-reset-url-retained"');
     expect(html).toContain('data-setting-define-new-action-owner="setting-define-controller"');
@@ -248,8 +213,8 @@ describe('setting define monitor-template surface', () => {
     expect(html).toContain('data-setting-define-delete-action="angular-current-app-id"');
     expect(html).toContain('data-setting-define-delete-action-owner="hertzbeat-ui-button"');
     expect(html).toContain('data-setting-define-delete-action-label="mysql"');
-    expect(html).toContain('aria-label="删除 mysql"');
-    expect(html).toContain('title="删除 mysql"');
+    expect(html).toContain(`aria-label="${t('setting.define.action.delete', { app: 'mysql' })}"`);
+    expect(html).toContain(`title="${t('setting.define.action.delete', { app: 'mysql' })}"`);
     expect(html).toContain('data-setting-define-save-confirm="closed"');
     expect(html).toContain('data-setting-define-save-confirm-owner="hertzbeat-ui-confirm-dialog"');
     expect(html).toContain('data-setting-define-save-confirm-closable="false"');
@@ -262,21 +227,21 @@ describe('setting define monitor-template surface', () => {
     expect(html).toContain('data-setting-define-delete-confirm-ok-type="primary"');
     expect(html).toContain('data-setting-define-template-visibility-confirm="closed"');
     expect(html).toContain('data-setting-define-template-visibility-confirm-owner="hertzbeat-ui-confirm-dialog"');
-    expect(html).toContain('定义');
-    expect(html).toContain('管理监控模板 YAML');
-    expect(html).toContain('新增类型');
+    expect(html).toContain(t('setting.define.title'));
+    expect(html).toContain(t('setting.define.subtitle'));
+    expect(html).toContain(t('setting.define.action.new'));
     expect(html).toContain('app-mysql.yml');
     expect(html).toContain('MySQL');
     expect(html).toContain('DATABASE');
-    expect(html).toContain('保存并应用');
-    expect(html).toContain('删除 mysql');
-    expect(html).toContain('深色模式');
+    expect(html).toContain(t('setting.define.action.save-apply'));
+    expect(html).toContain(t('setting.define.action.delete', { app: 'mysql' }));
+    expect(html).toContain(t('common.dark-mode'));
     expect(html).not.toContain('data-cold-checkbox-owner="cold-checkbox"');
-    expect(html).toContain('保存成功');
+    expect(html).toContain(t('common.notify.apply-success'));
     expect(html).not.toContain('data-setting-define-preview-panel');
     expect(html).not.toContain('data-setting-define-datasource-panel');
-    expect(html).not.toContain('预览查询');
-    expect(html).not.toContain('数据源状态');
+    expect(html).not.toContain(han(0x9884, 0x89c8, 0x67e5, 0x8be2));
+    expect(html).not.toContain(han(0x6570, 0x636e, 0x6e90, 0x72b6, 0x6001));
   });
 
   it('hides edit and save-apply while editing an existing template until YAML differs from the original', async () => {
@@ -307,10 +272,10 @@ describe('setting define monitor-template surface', () => {
     );
 
     expect(html).toContain('data-setting-define-diff-shell="monitor-template-diff"');
-    expect(html).toContain('取消');
-    expect(html).toContain('删除 mysql');
+    expect(html).toContain(t('common.button.cancel'));
+    expect(html).toContain(t('setting.define.action.delete', { app: 'mysql' }));
     expect(html).not.toContain('lucide-pencil');
-    expect(html).not.toContain('保存并应用');
+    expect(html).not.toContain(t('setting.define.action.save-apply'));
   });
 
   it('keeps new-template drafts out of existing-template rollback actions', async () => {
@@ -340,16 +305,16 @@ describe('setting define monitor-template surface', () => {
       />
     );
 
-    expect(html).toContain('新增模板草稿');
+    expect(html).toContain(t('setting.define.new-template'));
     expect(html).toContain('data-setting-define-code-editor="monitor-template-yaml"');
-    expect(html).toContain('保存并应用');
+    expect(html).toContain(t('setting.define.action.save-apply'));
     expect(html).toContain('data-setting-define-new-action-contract="angular-current-app-reset-url-retained"');
     expect(html).toContain('data-setting-define-new-action-state="hidden"');
     expect(html).not.toContain('data-setting-define-diff-shell="monitor-template-diff"');
     expect(html).not.toContain('data-setting-define-monitor-link=');
-    expect(html).not.toContain('新增类型');
-    expect(html).not.toContain('取消');
-    expect(html).not.toContain('删除');
+    expect(html).not.toContain(t('setting.define.action.new'));
+    expect(html).not.toContain(t('common.button.cancel'));
+    expect(html).not.toContain(t('setting.define.action.delete', { app: '' }).trim());
   });
 
   it('keeps the Angular save action visible for dirty code even when the editor is read-only', async () => {
@@ -382,7 +347,7 @@ describe('setting define monitor-template surface', () => {
     expect(html).toContain('data-setting-define-save-visibility-contract="angular-code-diff-independent-of-editing"');
     expect(html).toContain('data-cold-code-editor-readonly="true"');
     expect(html).toContain('data-setting-define-save-action="request"');
-    expect(html).toContain('保存并应用');
+    expect(html).toContain(t('setting.define.action.save-apply'));
     expect(html).not.toContain('data-setting-define-diff-shell="monitor-template-diff"');
   });
 
@@ -422,9 +387,9 @@ describe('setting define monitor-template surface', () => {
     expect(html).toContain('data-cold-code-editor-loading="true"');
     expect(html).toContain('data-cold-code-editor-loading-state="angular-nz-code-editor-loading"');
     expect(html).toContain('data-cold-code-editor-loading-state-owner="cold-code-editor"');
-    expect(html).toContain('加载定义中心');
+    expect(html).toContain(t('setting.define.loading'));
     expect(html).not.toContain('data-setting-define-save-action="request"');
-    expect(html).not.toContain('保存并应用');
+    expect(html).not.toContain(t('setting.define.action.save-apply'));
   });
 
   it('renders a cold empty state in the template list', async () => {
@@ -457,8 +422,8 @@ describe('setting define monitor-template surface', () => {
     expect(html).toContain('data-setting-define-empty-state="cold-list-empty"');
     expect(html).toContain('data-hz-ui="yaml-workspace"');
     expect(html).toContain('data-hz-ui="template-picker"');
-    expect(html).toContain('暂无模板');
-    expect(html).not.toContain('请先新增监控模板 YAML。');
+    expect(html).toContain(t('setting.define.empty.title'));
+    expect(html).not.toContain(t('setting.define.empty.copy'));
   });
 
   it('keeps the Angular define shell mounted without showing backend load errors', async () => {
@@ -531,10 +496,10 @@ describe('setting define monitor-template surface', () => {
 
     expect(html).toContain('data-setting-define-empty-state="cold-list-empty"');
     expect(html).toContain('data-hz-template-empty-state="angular-no-matched-children"');
-    expect(html).toContain('没有匹配的模板');
+    expect(html).toContain(t('setting.define.empty.search-title'));
     expect(html).toContain('data-hz-ui="template-picker"');
-    expect(html).not.toContain('暂无模板');
-    expect(html).not.toContain('请先新增监控模板 YAML。');
+    expect(html).not.toContain(t('setting.define.empty.title'));
+    expect(html).not.toContain(t('setting.define.empty.copy'));
     expect(html).toContain('app-mysql.yml');
   });
 
@@ -577,8 +542,8 @@ describe('setting define monitor-template surface', () => {
       'button[data-setting-define-template-visibility="postgresql"]'
     ) as HTMLButtonElement | null;
     expect(showPostgresql).not.toBeNull();
-    expect(showPostgresql?.getAttribute('aria-label')).toBe('显示模板 PostgreSQL');
-    expect(showPostgresql?.getAttribute('title')).toBe('显示模板 PostgreSQL');
+    expect(showPostgresql?.getAttribute('aria-label')).toBe(t('setting.define.action.show-aria', { app: 'PostgreSQL' }));
+    expect(showPostgresql?.getAttribute('title')).toBe(t('setting.define.action.show-aria', { app: 'PostgreSQL' }));
     expect(showPostgresql?.getAttribute('data-setting-define-template-visibility-owner')).toBe('hertzbeat-ui-button');
     expect(showPostgresql?.getAttribute('data-setting-define-template-visibility-label')).toBe('PostgreSQL');
     expect(showPostgresql?.getAttribute('data-setting-define-template-visibility-action')).toBe('show');
@@ -604,7 +569,7 @@ describe('setting define monitor-template surface', () => {
     expect(
       interactionContainer.querySelector('[data-setting-define-template-visibility-confirm-dialog="angular-popconfirm"][data-setting-define-confirm-owner="hertzbeat-ui-confirm-dialog"]')
     ).not.toBeNull();
-    expect(interactionContainer.textContent).toContain('确认是否显示此菜单?');
+    expect(interactionContainer.textContent).toContain(t('define.hide-true.confirm'));
 
     const confirmButtons = interactionContainer
       .querySelector('[data-setting-define-template-visibility-confirm="open"]')
@@ -666,7 +631,7 @@ describe('setting define monitor-template surface', () => {
     expect(onSave).not.toHaveBeenCalled();
     expect(interactionContainer.querySelector('[data-setting-define-save-confirm="open"]')).toBeNull();
     expect(interactionContainer.querySelector('[data-setting-define-save-confirm="closed"]')).not.toBeNull();
-    expect(interactionContainer.textContent).toContain('监控类型定义内容不能为空。');
+    expect(interactionContainer.textContent).toContain(t('define.save-apply.no-code'));
   });
 
   it('uses the Angular confirmation copy as save and delete modal titles', async () => {
@@ -714,7 +679,7 @@ describe('setting define monitor-template surface', () => {
     const saveDialog = interactionContainer.querySelector(
       '[data-setting-define-save-confirm-dialog="angular-modal-confirm"][data-setting-define-confirm-title="angular-title-copy"]'
     );
-    expect(saveDialog?.getAttribute('aria-label')).toBe('请确认是否保存修改并应用此监控类型定义? 这会影响到您的监控内容。');
+    expect(saveDialog?.getAttribute('aria-label')).toBe(t('define.save-apply.confirm'));
     expect(saveDialog?.getAttribute('data-hz-confirm-closable')).toBe('false');
     expect(saveDialog?.getAttribute('data-hz-confirm-ok-danger')).toBe('true');
     expect(saveDialog?.getAttribute('data-hz-confirm-ok-type')).toBe('primary');
@@ -723,7 +688,7 @@ describe('setting define monitor-template surface', () => {
     expect(saveDialog?.getAttribute('data-setting-define-confirm-ok-type')).toBe('primary');
     expect(saveDialog?.querySelector('[data-setting-define-save-confirm-submit="angular-modal-confirm"]')?.getAttribute('data-setting-define-save-confirm-submit-danger')).toBe('true');
     expect(saveDialog?.querySelector('[data-setting-define-save-confirm-submit="angular-modal-confirm"]')?.getAttribute('data-setting-define-save-confirm-submit-type')).toBe('primary');
-    expect(saveDialog?.textContent).toContain('请确认是否保存修改并应用此监控类型定义? 这会影响到您的监控内容。');
+    expect(saveDialog?.textContent).toContain(t('define.save-apply.confirm'));
 
     await act(async () => {
       saveDialog?.querySelector('[data-hz-confirm-action="cancel"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
@@ -731,7 +696,7 @@ describe('setting define monitor-template surface', () => {
     });
 
     const deleteButton = Array.from(interactionContainer.querySelectorAll('button')).find(button =>
-      button.textContent?.includes('删除 mysql')
+      button.textContent?.includes(t('setting.define.action.delete', { app: 'mysql' }))
     ) as HTMLButtonElement | undefined;
     expect(deleteButton).toBeDefined();
 
@@ -743,7 +708,7 @@ describe('setting define monitor-template surface', () => {
     const deleteDialog = interactionContainer.querySelector(
       '[data-setting-define-delete-confirm-dialog="angular-modal-confirm"][data-setting-define-confirm-title="angular-title-copy"]'
     );
-    expect(deleteDialog?.getAttribute('aria-label')).toBe('请确认是否删除 MySQL 监控类型? 删除后无法添加此类监控。');
+    expect(deleteDialog?.getAttribute('aria-label')).toBe(t('define.delete.confirm', { app: 'MySQL' }));
     expect(deleteDialog?.getAttribute('data-hz-confirm-closable')).toBe('false');
     expect(deleteDialog?.getAttribute('data-hz-confirm-ok-danger')).toBe('true');
     expect(deleteDialog?.getAttribute('data-hz-confirm-ok-type')).toBe('primary');
@@ -752,7 +717,7 @@ describe('setting define monitor-template surface', () => {
     expect(deleteDialog?.getAttribute('data-setting-define-confirm-ok-type')).toBe('primary');
     expect(deleteDialog?.querySelector('[data-setting-define-delete-confirm-submit="angular-modal-confirm"]')?.getAttribute('data-setting-define-delete-confirm-submit-danger')).toBe('true');
     expect(deleteDialog?.querySelector('[data-setting-define-delete-confirm-submit="angular-modal-confirm"]')?.getAttribute('data-setting-define-delete-confirm-submit-type')).toBe('primary');
-    expect(deleteDialog?.textContent).toContain('请确认是否删除 MySQL 监控类型? 删除后无法添加此类监控。');
+    expect(deleteDialog?.textContent).toContain(t('define.delete.confirm', { app: 'MySQL' }));
   });
 
   it('keeps the save action in the old Angular loading state while applying YML', () => {
@@ -789,7 +754,7 @@ describe('setting define monitor-template surface', () => {
     expect(html).toContain('disabled=""');
     expect(html).toContain('data-setting-define-save-pending-feedback="angular-save-loading"');
     expect(html).toContain('data-setting-define-save-pending-feedback-owner="hertzbeat-ui-workspace-feedback"');
-    expect(html).toContain('正在应用定义...');
+    expect(html).toContain(t('setting.define.save.pending'));
   });
 
   it('keeps mutation failure feedback as Angular fixed title plus backend detail metadata', () => {
@@ -804,7 +769,7 @@ describe('setting define monitor-template surface', () => {
         yamlLabel="app-mysql.yml"
         darkMode={false}
         isEditing
-        message="应用失败"
+        message={t('common.notify.apply-fail')}
         messageMeta="schema invalid"
         messageContract="angular-apply-fail-notification"
         onSearchChange={() => {}}
@@ -831,7 +796,7 @@ describe('setting define monitor-template surface', () => {
     expect(saveHtml).toContain('data-setting-define-visibility-feedback="angular-apply-fail-notification"');
     expect(saveHtml).toContain('data-setting-define-visibility-feedback-title="common.notify.apply-fail"');
     expect(saveHtml).toContain('data-setting-define-visibility-feedback-detail="backend-message"');
-    expect(saveHtml).toContain('应用失败');
+    expect(saveHtml).toContain(t('common.notify.apply-fail'));
     expect(saveHtml).toContain('schema invalid');
 
     const deleteHtml = renderToStaticMarkup(
@@ -845,7 +810,7 @@ describe('setting define monitor-template surface', () => {
         yamlLabel="app-mysql.yml"
         darkMode={false}
         isEditing={false}
-        message="删除失败"
+        message={t('common.notify.delete-fail')}
         messageMeta="template is still in use"
         messageContract="angular-delete-fail-notification"
         onSearchChange={() => {}}
@@ -868,7 +833,7 @@ describe('setting define monitor-template surface', () => {
     expect(deleteHtml).toContain('data-setting-define-delete-feedback="angular-delete-fail-notification"');
     expect(deleteHtml).toContain('data-setting-define-delete-feedback-title="common.notify.delete-fail"');
     expect(deleteHtml).toContain('data-setting-define-delete-feedback-detail="backend-message"');
-    expect(deleteHtml).toContain('删除失败');
+    expect(deleteHtml).toContain(t('common.notify.delete-fail'));
     expect(deleteHtml).toContain('template is still in use');
   });
 
@@ -940,9 +905,9 @@ describe('setting define monitor-template surface', () => {
     expect(html).toContain('data-setting-define-diff-current="true"');
     expect(html).toContain('data-setting-define-code-editor="monitor-template-yaml-original"');
     expect(html).toContain('data-setting-define-code-editor="monitor-template-yaml"');
-    expect(html).toContain('原始 YAML');
-    expect(html).toContain('当前编辑 YAML');
-    expect(html).toContain('保存并应用');
+    expect(html).toContain(t('setting.define.diff.original'));
+    expect(html).toContain(t('setting.define.diff.current'));
+    expect(html).toContain(t('setting.define.action.save-apply'));
     expect(html).toContain('app: mysql');
     expect(html).toContain('category: database');
     expect(html).toContain('metrics:');
diff --git a/web-next/components/pages/settings-surface-page.test.tsx b/web-next/components/pages/settings-surface-page.test.tsx
index fa612a9c91..bd5df809f8 100644
--- a/web-next/components/pages/settings-surface-page.test.tsx
+++ b/web-next/components/pages/settings-surface-page.test.tsx
@@ -45,28 +45,31 @@ vi.mock('@/components/workbench/primitives', () => ({
 describe('SettingsSurfacePage', () => {
   it('renders platform governance groups without fake future navigation entries', async () => {
     const { SettingsSurfacePage } = await import('./settings-surface-page');
+    const pageTitle = 'System settings';
+    const pageSubtitle = 'Configure platform governance entry points.';
+    const nextStep = 'Organize platform governance';
 
     const html = renderToStaticMarkup(
-      <SettingsSurfacePage title="系统设置" subtitle="配置平台治理入口。" nextStep="整理平台治理" />
+      <SettingsSurfacePage title={pageTitle} subtitle={pageSubtitle} nextStep={nextStep} />
     );
 
-    expect(html).toContain('平台治理');
-    expect(html).toContain('设置总览');
-    expect(html).toContain('把平台配置入口收拢到一个工作台。');
-    expect(html).toContain('路由契约');
-    expect(html).toContain('API 契约');
-    expect(html).toContain('只展示当前已有入口');
-    expect(html).toContain('未来大域保留在路线图文档');
-    expect(html).toContain('API 访问');
+    expect(html).toContain(pageTitle);
+    expect(html).toContain(pageSubtitle);
+    expect(html).toContain(t('settings.surface.governance.title'));
+    expect(html).toContain(t('settings.surface.title'));
+    expect(html).toContain(t('settings.surface.copy'));
+    expect(html).toContain(t('settings.surface.route-contract.meta'));
+    expect(html).toContain(t('settings.surface.api-contract.meta'));
+    expect(html).toContain(t('settings.surface.governance.copy'));
+    expect(html).toContain(t('settings.surface.governance.group.api-access'));
     expect(html).toContain('/setting/settings/token');
-    expect(html).toContain('通知通道');
+    expect(html).toContain(t('settings.surface.governance.group.notifications'));
     expect(html).toContain('/alert/notice');
-    expect(html).toContain('模板与插件');
+    expect(html).toContain(t('settings.surface.governance.group.template-marketplace'));
     expect(html).toContain('/setting/define · /setting/plugins');
-    expect(html).toContain('未来大域边界');
-    expect(html).toContain('仅规划');
-    expect(html).toContain('安全治理');
-    expect(html).toContain('路线图能力规划');
+    expect(html).toContain(t('settings.surface.governance.future.title'));
+    expect(html).toContain(t('settings.surface.governance.future.meta'));
+    expect(html).toContain(t('settings.surface.governance.future.security'));
     expect(html).toContain('/docs/roadmap/future-security');
     expect(html).not.toContain('href="/security"');
   });
diff --git a/web-next/components/pages/status-setting-surface.test.tsx b/web-next/components/pages/status-setting-surface.test.tsx
index 0715b79456..cd107a3ffb 100644
--- a/web-next/components/pages/status-setting-surface.test.tsx
+++ b/web-next/components/pages/status-setting-surface.test.tsx
@@ -118,29 +118,29 @@ describe('status setting surface', () => {
     expect(html).toContain('data-status-incident-delete-menu-contract="angular-ellipsis-dropdown-delete"');
     expect(html).toContain('data-status-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
     expect(html).not.toContain('data-status-summary-rail=');
-    expect(html).toContain('状态页面');
-    expect(html).toContain('组织名称');
-    expect(html).toContain('组织介绍');
-    expect(html).toContain('网站链接');
-    expect(html).toContain('标志图片');
-    expect(html).toContain('反馈地址');
-    expect(html).toContain('主题颜色');
-    expect(html).toContain('服务组件');
-    expect(html).toContain('维护事件');
+    expect(html).toContain(t('setting.status.title'));
+    expect(html).toContain(t('setting.status.org.name'));
+    expect(html).toContain(t('setting.status.org.description'));
+    expect(html).toContain(t('setting.status.org.home'));
+    expect(html).toContain(t('setting.status.org.logo'));
+    expect(html).toContain(t('setting.status.org.feedback'));
+    expect(html).toContain(t('setting.status.org.color'));
+    expect(html).toContain(t('setting.status.components.tab'));
+    expect(html).toContain(t('setting.status.incidents.tab'));
     expect(html).toContain('HB Status');
     expect(html).toContain('/status');
-    expect(html).toContain('公开状态页的组件说明');
-    expect(html).toContain('新增组件');
-    expect(html).toContain('组件状态');
-    expect(html).toContain('状态统计方式');
-    expect(html).toContain('匹配标签');
-    expect(html).toContain('更新时间');
-    expect(html).toContain('操作');
+    expect(html).toContain(t('setting.status.component.description.placeholder'));
+    expect(html).toContain(t('setting.status.components.new'));
+    expect(html).toContain(t('setting.status.components.state'));
+    expect(html).toContain(t('setting.status.components.method'));
+    expect(html).toContain(t('setting.status.components.labels'));
+    expect(html).toContain(t('common.edit-time'));
+    expect(html).toContain(t('common.edit'));
     expect(html).toContain('API');
-    expect(html).toContain('正常');
-    expect(html).toContain('手动设置');
+    expect(html).toContain(t('status.component.state.0'));
+    expect(html).toContain(t('setting.status.components.method.1'));
     expect(html).toContain('source:codex-smoke');
-    expect(html).toContain('确定');
+    expect(html).toContain(t('common.button.ok'));
   }, 15000);
 
   it('hides the public status link until Angular statusOrg id and name are both available', () => {
@@ -219,7 +219,7 @@ describe('status setting surface', () => {
     const t = createTranslatorMock({
       locale: 'zh-CN',
       overrides: {
-        'common.none': '无状态值'
+        'common.none': 'empty-status-value'
       }
     });
 
@@ -288,7 +288,7 @@ describe('status setting surface', () => {
 
     expect(html).toContain('data-status-component-row="1"');
     expect(html).toContain('data-status-incident-row="2"');
-    expect((html.match(/无状态值/g) ?? []).length).toBeGreaterThanOrEqual(5);
+    expect((html.match(/empty-status-value/g) ?? []).length).toBeGreaterThanOrEqual(5);
   }, 15000);
 
   it('renders Angular-style incident edit history in descending order with shared ownership', () => {
@@ -382,7 +382,7 @@ describe('status setting surface', () => {
     expect(html).toContain('data-status-incident-message-placeholder-contract="angular-state-placeholder"');
     expect(html).toContain('data-status-incident-message-placeholder="status.incident.message.tip.2"');
     expect(html).toContain('data-hz-ui="textarea"');
-    expect(html).toContain('我们正在监控该事件,观察处理效果');
+    expect(html).toContain(t('status.incident.message.tip.2'));
     expect(html).toContain('data-status-incident-component-picker="angular-checkbox-group"');
     expect(html).toContain('data-status-incident-component-picker-owner="hertzbeat-ui-checkbox"');
     expect(html).toContain('data-status-incident-component-option="1"');
@@ -392,9 +392,9 @@ describe('status setting surface', () => {
     expect(html).toContain('data-hz-ui="radio-button-group"');
     expect(html).toContain('data-hz-radio-button-option="2"');
     expect(html).toContain('data-hz-radio-button-checked="true"');
-    expect(html).toContain('事件历史');
-    expect(html).toContain('观察中');
-    expect(html).toContain('已确认');
+    expect(html).toContain(t('status.incident.history'));
+    expect(html).toContain(t('status.incident.state.2'));
+    expect(html).toContain(t('status.incident.state.1'));
   }, 15000);
 
   it('uses the shared cold visual owner instead of Workbench or alert primitives', () => {
@@ -472,14 +472,14 @@ describe('status setting surface', () => {
     expect(source).toContain("t('setting.status.delete.confirm')");
     expect(source).toContain("t('common.no-data')");
     expect(source).toContain("t('common.button.cancel')");
-    expect(source).not.toContain('确认删除组件');
-    expect(source).not.toContain('确认删除维护事件');
-    expect(source).not.toContain('确认删除');
-    expect(source).not.toContain('取消');
-    expect(source).not.toContain('暂无数据');
-    expect(source).not.toContain('管理公开状态页的组织档案');
-    expect(source).not.toContain('组织档案');
-    expect(source).not.toContain('公开状态页展示的组织信息和反馈入口。');
+    expect(source).not.toContain('\u786e\u8ba4\u5220\u9664\u7ec4\u4ef6');
+    expect(source).not.toContain('\u786e\u8ba4\u5220\u9664\u7ef4\u62a4\u4e8b\u4ef6');
+    expect(source).not.toContain('\u786e\u8ba4\u5220\u9664');
+    expect(source).not.toContain('\u53d6\u6d88');
+    expect(source).not.toContain('\u6682\u65e0\u6570\u636e');
+    expect(source).not.toContain('\u7ba1\u7406\u516c\u5f00\u72b6\u6001\u9875\u7684\u7ec4\u7ec7\u6863\u6848');
+    expect(source).not.toContain('\u7ec4\u7ec7\u6863\u6848');
+    expect(source).not.toContain('\u516c\u5f00\u72b6\u6001\u9875\u5c55\u793a\u7684\u7ec4\u7ec7\u4fe1\u606f\u548c\u53cd\u9988\u5165\u53e3\u3002');
     expect(source).not.toContain('data-status-summary-rail');
     expect(source).not.toContain('coldStatusVisual.layout.heroGrid');
     expect(source).not.toContain('coldStatusVisual.layout.railGrid');
diff --git a/web-next/components/pages/status-setting-surface.tsx b/web-next/components/pages/status-setting-surface.tsx
index a8f9759e08..e04d537a52 100644
--- a/web-next/components/pages/status-setting-surface.tsx
+++ b/web-next/components/pages/status-setting-surface.tsx
@@ -327,8 +327,8 @@ function StatusTabs({
       className="mb-4 inline-flex rounded-[3px] border border-[#2b3039] bg-[#101217] p-1"
     >
       {[
-        ['component', t('status.component')],
-        ['incident', t('status.incident')]
+        ['component', t('setting.status.components.tab')],
+        ['incident', t('setting.status.incidents.tab')]
       ].map(([key, label]) => {
         const mode = key as 'component' | 'incident';
         const active = activeMode === mode;
@@ -402,7 +402,7 @@ export function StatusSettingSurface({
   const components = data.components || [];
   const incidents = data.incidents?.content || [];
   const [openRowActionMenuId, setOpenRowActionMenuId] = React.useState<string | null>(null);
-  const publicStatusLabel = statusOrg.name ? `${statusOrg.name} ${t('menu.advanced.status')}` : t('menu.advanced.status');
+  const publicStatusLabel = statusOrg.name ? `${statusOrg.name} ${t('status.title')}` : t('status.title');
   const hasPublicStatusLink = statusOrg.id != null && statusOrg.name != null;
   const activeMode = mode === 'incident' ? 'incident' : 'component';
   const incidentTotalElements = Math.max(0, data.incidents?.totalElements ?? incidents.length);
@@ -489,7 +489,7 @@ export function StatusSettingSurface({
               <div data-status-header="cold-compact-header" className={coldStatusVisual.panel.hero}>
                 <div className="max-w-[840px]">
                   <h1 className="text-[30px] font-semibold leading-tight text-[#f5f7fb]">
-                    {t('menu.advanced.status')}
+                    {t('setting.status.title')}
                   </h1>
                   <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">
                     {t('setting.status.subtitle')}
@@ -520,12 +520,12 @@ export function StatusSettingSurface({
                     </Button>
                   </div>
                   <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
-                    <ColdField required label={t('status.org.name')} value={orgValues.name} placeholder={t('status.org.name.tip')} onChange={value => onOrgDraftChange({ name: value })} />
-                    <ColdField required label={t('status.org.desc')} value={orgValues.description} placeholder={t('status.org.desc.tip')} onChange={value => onOrgDraftChange({ description: value })} />
-                    <ColdField required label={t('status.org.home')} value={orgValues.home} placeholder={t('status.org.home.tip')} onChange={value => onOrgDraftChange({ home: value })} />
-                    <ColdField required label={t('status.org.logo')} value={orgValues.logo} placeholder={t('status.org.logo.tip')} onChange={value => onOrgDraftChange({ logo: value })} />
-                    <ColdField required label={t('status.org.feedback')} value={orgValues.feedback} placeholder={t('status.org.feedback.tip')} onChange={value => onOrgDraftChange({ feedback: value })} />
-                    <ColdField label={t('status.org.color')} value={orgValues.color} placeholder={t('status.org.color.tip')} onChange={value => onOrgDraftChange({ color: value })} />
+                    <ColdField required label={t('setting.status.org.name')} value={orgValues.name} placeholder={t('setting.status.org.name')} onChange={value => onOrgDraftChange({ name: value })} />
+                    <ColdField required label={t('setting.status.org.description')} value={orgValues.description} placeholder={t('setting.status.org.description')} onChange={value => onOrgDraftChange({ description: value })} />
+                    <ColdField required label={t('setting.status.org.home')} value={orgValues.home} placeholder={t('setting.status.org.home')} onChange={value => onOrgDraftChange({ home: value })} />
+                    <ColdField required label={t('setting.status.org.logo')} value={orgValues.logo} placeholder={t('setting.status.org.logo')} onChange={value => onOrgDraftChange({ logo: value })} />
+                    <ColdField required label={t('setting.status.org.feedback')} value={orgValues.feedback} placeholder={t('setting.status.org.feedback')} onChange={value => onOrgDraftChange({ feedback: value })} />
+                    <ColdField label={t('setting.status.org.color')} value={orgValues.color} placeholder={t('setting.status.org.color')} onChange={value => onOrgDraftChange({ color: value })} />
                   </div>
                   {orgMessage ? <div className="mt-3 text-[12px] text-[#a9b0bb]">{orgMessage}</div> : null}
                   {orgError ? <div className="mt-3 text-[12px] text-[#f87171]">{orgError}</div> : null}
@@ -536,7 +536,7 @@ export function StatusSettingSurface({
                 <div hidden={activeMode !== 'component'}>
                   <div data-status-component-toolbar="cold-table-toolbar" className={coldTableToolbarClassName}>
                     <div className="min-w-0 flex-1 text-[12px] font-semibold text-[#dbe4f0]">
-                      {t('status.component')} <span className="ml-2 text-[#6f7788]">{components.length}</span>
+                      {t('setting.status.components.tab')} <span className="ml-2 text-[#6f7788]">{components.length}</span>
                     </div>
                     <Button
                       size="sm"
@@ -550,7 +550,7 @@ export function StatusSettingSurface({
                     </Button>
                     <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onNewComponent}>
                       <Plus className="h-3.5 w-3.5" aria-hidden="true" />
-                      {t('status.component.new')}
+                      {t('setting.status.components.new')}
                     </Button>
                     {hasPublicStatusLink ? (
                       <a
@@ -572,10 +572,10 @@ export function StatusSettingSurface({
                     <table data-status-component-table="cold-component-table" className="w-full table-fixed border-collapse text-left text-[12px] text-[#a9b0bb]">
                       <thead className="border-b border-[#252b34] bg-[#101217] text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">
                         <tr>
-                          <th className="w-[22%] px-3 py-2.5">{t('status.component.name')}</th>
-                          <th className="w-[14%] px-3 py-2.5">{t('status.component.state')}</th>
-                          <th className="w-[15%] px-3 py-2.5">{t('status.component.method')}</th>
-                          <th className="w-[25%] px-3 py-2.5">{t('status.component.tag')}</th>
+                          <th className="w-[22%] px-3 py-2.5">{t('setting.status.components.name')}</th>
+                          <th className="w-[14%] px-3 py-2.5">{t('setting.status.components.state')}</th>
+                          <th className="w-[15%] px-3 py-2.5">{t('setting.status.components.method')}</th>
+                          <th className="w-[25%] px-3 py-2.5">{t('setting.status.components.labels')}</th>
                           <th className="w-[16%] px-3 py-2.5">{t('common.edit-time')}</th>
                           <th className="w-[96px] px-3 py-2.5">{t('common.edit')}</th>
                         </tr>
@@ -597,7 +597,7 @@ export function StatusSettingSurface({
                                 <Tag className={tagClassName(settingStatusComponentTagTone(component.state))}>{t(`status.component.state.${Number(component.state ?? 0)}`)}</Tag>
                               </td>
                               <td className="px-3 py-2.5">
-                                <Tag className={tagClassName('neutral')}>{t(`status.component.method.${Number(component.method ?? 0)}`)}</Tag>
+                                <Tag className={tagClassName('neutral')}>{t(`setting.status.components.method.${Number(component.method ?? 0)}`)}</Tag>
                               </td>
                               <td className="px-3 py-2.5">
                                 <div className="flex flex-wrap gap-1.5">
@@ -615,14 +615,14 @@ export function StatusSettingSurface({
                                     size="icon"
                                     variant="default"
                                     className={coldIconButtonClassName}
-                                    title={t('status.component.edit')}
+                                    title={t('setting.status.components.edit')}
                                     onClick={() => {
                                       if (component.id != null) onSelectComponent(component.id);
                                       onEditComponent(component);
                                     }}
                                   >
                                     <Pencil className="h-3.5 w-3.5" aria-hidden="true" />
-                                    <span className="sr-only">{t('status.component.edit')}</span>
+                                    <span className="sr-only">{t('setting.status.components.edit')}</span>
                                   </Button>
                                   <div
                                     className={statusActionMenuRootClassName}
@@ -665,7 +665,7 @@ export function StatusSettingSurface({
                                         className="w-full text-[#fecaca] hover:text-white"
                                       >
                                         <Trash2 className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
-                                        <span className="truncate">{t('status.component.delete')}</span>
+                                        <span className="truncate">{t('setting.status.components.delete')}</span>
                                       </HzTableRowActionButton>
                                     </div>
                                   </div>
@@ -688,7 +688,7 @@ export function StatusSettingSurface({
                     className="mb-3"
                     inputWidthClassName="w-[320px]"
                     value={incidentSearchInput}
-                    placeholder={t('status.incident.name')}
+                    placeholder={t('setting.status.incidents.name')}
                     searchLabel={t('common.search')}
                     clearLabel={t('common.reset')}
                     showClearWhenEmpty
@@ -709,7 +709,7 @@ export function StatusSettingSurface({
                         </Button>
                         <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onNewIncident}>
                           <Plus className="h-3.5 w-3.5" aria-hidden="true" />
-                          {t('status.incident.new')}
+                          {t('setting.status.incidents.new')}
                         </Button>
                         {hasPublicStatusLink ? (
                           <a
@@ -733,10 +733,10 @@ export function StatusSettingSurface({
                     <table data-status-incident-table="cold-incident-table" className="w-full table-fixed border-collapse text-left text-[12px] text-[#a9b0bb]">
                       <thead className="border-b border-[#252b34] bg-[#101217] text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">
                         <tr>
-                          <th className="w-[18%] px-3 py-2.5">{t('status.incident.name')}</th>
-                          <th className="w-[14%] px-3 py-2.5">{t('status.incident.state')}</th>
-                          <th className="w-[28%] px-3 py-2.5">{t('status.incident.message-latest')}</th>
-                          <th className="w-[18%] px-3 py-2.5">{t('status.incident.component')}</th>
+                          <th className="w-[18%] px-3 py-2.5">{t('setting.status.incidents.name')}</th>
+                          <th className="w-[14%] px-3 py-2.5">{t('setting.status.incidents.state')}</th>
+                          <th className="w-[28%] px-3 py-2.5">{t('setting.status.incidents.message-latest')}</th>
+                          <th className="w-[18%] px-3 py-2.5">{t('setting.status.incidents.components')}</th>
                           <th className="w-[14%] px-3 py-2.5">{t('common.new-time')}</th>
                           <th className="w-[96px] px-3 py-2.5">{t('common.edit')}</th>
                         </tr>
@@ -777,14 +777,14 @@ export function StatusSettingSurface({
                                     size="icon"
                                     variant="default"
                                     className={coldIconButtonClassName}
-                                    title={t('status.incident.update')}
+                                    title={t('setting.status.incidents.update')}
                                     onClick={() => {
                                       if (incident.id != null) onSelectIncident(incident.id);
                                       onEditIncident(incident);
                                     }}
                                   >
                                     <RotateCw className="h-3.5 w-3.5" aria-hidden="true" />
-                                    <span className="sr-only">{t('status.incident.update')}</span>
+                                    <span className="sr-only">{t('setting.status.incidents.update')}</span>
                                   </Button>
                                   <div
                                     className={statusActionMenuRootClassName}
@@ -827,7 +827,7 @@ export function StatusSettingSurface({
                                         className="w-full text-[#fecaca] hover:text-white"
                                       >
                                         <Trash2 className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
-                                        <span className="truncate">{t('status.incident.delete')}</span>
+                                        <span className="truncate">{t('setting.status.incidents.delete')}</span>
                                       </HzTableRowActionButton>
                                     </div>
                                   </div>
@@ -884,8 +884,8 @@ export function StatusSettingSurface({
 
       <OverlayDialog
         open={editingComponent}
-        title={componentDraft.id ? t('status.component.edit') : t('status.component.new')}
-        kicker={t('status.component')}
+        title={componentDraft.id ? t('setting.status.components.edit') : t('setting.status.components.new')}
+        kicker={t('setting.status.components.tab')}
         maxWidthClassName="max-w-2xl"
         onClose={onCancelComponent}
         footer={
@@ -900,20 +900,20 @@ export function StatusSettingSurface({
         }
       >
         <div className="grid gap-3 md:grid-cols-2">
-          <DialogField label={t('status.component.name')} value={componentDraft.name} placeholder={t('status.component.name.tip')} onChange={value => onComponentDraftChange({ name: value })} />
-          <DialogField label={t('status.component.method')} value={componentDraft.method} placeholder={t('setting.status.component.method.placeholder')} onChange={value => onComponentDraftChange({ method: value })} />
-          <DialogField label={t('status.component.state')} value={componentDraft.state} placeholder={t('setting.status.component.state.placeholder')} onChange={value => onComponentDraftChange({ state: value })} />
-          <DialogField label={t('status.component.tag')} value={componentDraft.labelsText} placeholder={t('setting.status.component.labels.placeholder')} onChange={value => onComponentDraftChange({ labelsText: value })} />
+          <DialogField label={t('setting.status.components.name')} value={componentDraft.name} placeholder={t('setting.status.components.name')} onChange={value => onComponentDraftChange({ name: value })} />
+          <DialogField label={t('setting.status.components.method')} value={componentDraft.method} placeholder={t('setting.status.component.method.placeholder')} onChange={value => onComponentDraftChange({ method: value })} />
+          <DialogField label={t('setting.status.components.state')} value={componentDraft.state} placeholder={t('setting.status.component.state.placeholder')} onChange={value => onComponentDraftChange({ state: value })} />
+          <DialogField label={t('setting.status.components.labels')} value={componentDraft.labelsText} placeholder={t('setting.status.component.labels.placeholder')} onChange={value => onComponentDraftChange({ labelsText: value })} />
           <div className="md:col-span-2">
-            <DialogField label={t('status.org.desc')} value={componentDraft.description} placeholder={t('setting.status.component.description.placeholder')} onChange={value => onComponentDraftChange({ description: value })} />
+            <DialogField label={t('setting.status.org.description')} value={componentDraft.description} placeholder={t('setting.status.component.description.placeholder')} onChange={value => onComponentDraftChange({ description: value })} />
           </div>
         </div>
       </OverlayDialog>
 
       <OverlayDialog
         open={editingIncident}
-        title={incidentDraft.id ? t('status.incident.update') : t('status.incident.new')}
-        kicker={t('status.incident')}
+        title={incidentDraft.id ? t('setting.status.incidents.update') : t('setting.status.incidents.new')}
+        kicker={t('setting.status.incidents.tab')}
         maxWidthClassName="max-w-2xl"
         onClose={onCancelIncident}
         overlayProps={{
@@ -932,7 +932,7 @@ export function StatusSettingSurface({
         }
       >
         <div className="grid gap-3 md:grid-cols-2">
-          <DialogField label={t('status.incident.name')} value={incidentDraft.name} placeholder={t('status.incident.name.tip')} onChange={value => onIncidentDraftChange({ name: value })} />
+          <DialogField label={t('setting.status.incidents.name')} value={incidentDraft.name} placeholder={t('setting.status.incidents.name')} onChange={value => onIncidentDraftChange({ name: value })} />
           {incidentDraft.id ? (
             <div
               className="md:col-span-2"
@@ -948,14 +948,14 @@ export function StatusSettingSurface({
           ) : null}
           <div className="md:col-span-2">
             <DialogIncidentStateRadioGroup
-              label={t('status.incident.state')}
+              label={t('setting.status.incidents.state')}
               value={incidentDraft.state}
               t={t}
               onChange={value => onIncidentDraftChange({ state: value })}
             />
           </div>
           <DialogComponentCheckboxGroup
-            label={t('status.incident.component')}
+            label={t('setting.status.incidents.components')}
             components={components}
             value={incidentDraft.componentIdsText}
             emptyLabel={emptyCellText}
@@ -968,7 +968,7 @@ export function StatusSettingSurface({
             data-status-incident-message-placeholder-contract="angular-state-placeholder"
           >
             <DialogTextareaField
-              label={t('status.incident.message')}
+              label={t('setting.status.incidents.message')}
               value={incidentDraft.message}
               placeholder={incidentMessagePlaceholder}
               rows={2}
@@ -988,7 +988,7 @@ export function StatusSettingSurface({
           open={deleteDialog !== null}
           tone="critical"
           title={deleteDialogTitle}
-          kicker={t('menu.advanced.status')}
+          kicker={t('status.title')}
           cancelLabel={t('common.button.cancel')}
           confirmLabel={t('setting.status.delete.confirm')}
           onClose={() => setDeleteDialog(null)}
diff --git a/web-next/components/providers/i18n-provider.test.ts b/web-next/components/providers/i18n-provider.test.ts
index 9a554510a6..09fe8deeb4 100644
--- a/web-next/components/providers/i18n-provider.test.ts
+++ b/web-next/components/providers/i18n-provider.test.ts
@@ -6,6 +6,7 @@ import {
   resolveMessageTemplate,
   shouldUseRemoteLocaleBootstrap
 } from './i18n-provider';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 
 const fetchMock = vi.fn<typeof fetch>();
 
@@ -16,6 +17,10 @@ function jsonResponse(payload: unknown) {
   });
 }
 
+function supplementalMessages(locale: 'en-US' | 'zh-CN', keys: string[]) {
+  return Object.fromEntries(keys.map(key => [key, SUPPLEMENTAL_MESSAGES[locale]?.[key]]));
+}
+
 describe('i18n provider remote overlay messages', () => {
   beforeEach(() => {
     fetchMock.mockReset();
@@ -71,92 +76,61 @@ describe('i18n provider remote overlay messages', () => {
   });
 
   it('seeds alert notice modal labels synchronously so cold dialogs never flash raw keys', () => {
+    const keys = [
+      'alert.notice.template',
+      'alert.notice.template.name',
+      'alert.notice.template.preset',
+      'alert.notice.template.preset.false',
+      'alert.notice.template.preset.true',
+      'alert.notice.receiver.people',
+      'alert.notice.receiver.people.name',
+      'alert.notice.receiver.people.placeholder',
+      'alert.notice.rule.name',
+      'alert.notice.rule.all',
+      'alert.notice.rule.tag',
+      'alert.notice.rule.period',
+      'alert.notice.rule.time',
+      'common.enable'
+    ];
+
     expect(buildInitialMessages('zh-CN')).toEqual(
-      expect.objectContaining({
-        'alert.notice.template': '通知模板',
-        'alert.notice.template.name': '模板名称',
-        'alert.notice.template.preset': '模版类型',
-        'alert.notice.template.preset.false': '用户自定义模版',
-        'alert.notice.template.preset.true': '系统内置模版',
-        'alert.notice.receiver.people': '接收对象',
-        'alert.notice.receiver.people.name': '接收对象名称',
-        'alert.notice.receiver.people.placeholder': '选择接收对象',
-        'alert.notice.rule.name': '策略名称',
-        'alert.notice.rule.all': '转发所有',
-        'alert.notice.rule.tag': '标签匹配',
-        'alert.notice.rule.period': '时间周期',
-        'alert.notice.rule.time': '通知时段',
-        'common.enable': '启用状态'
-      })
+      expect.objectContaining(supplementalMessages('zh-CN', keys))
     );
 
     expect(buildInitialMessages('en-US')).toEqual(
-      expect.objectContaining({
-        'alert.notice.template': 'Notice Template',
-        'alert.notice.template.name': 'Template Name',
-        'alert.notice.template.preset': 'Template Type',
-        'alert.notice.template.preset.false': 'User Custom',
-        'alert.notice.template.preset.true': 'System Preset',
-        'alert.notice.receiver.people': 'Receiver',
-        'alert.notice.receiver.people.name': 'Receiver Name',
-        'alert.notice.receiver.people.placeholder': 'Select a receiver',
-        'alert.notice.rule.name': 'Policy Name',
-        'alert.notice.rule.all': 'Dispatch All',
-        'alert.notice.rule.tag': 'Label Match',
-        'alert.notice.rule.period': 'Time Period',
-        'alert.notice.rule.time': 'Notification Time',
-        'common.enable': 'Enable'
-      })
+      expect.objectContaining(supplementalMessages('en-US', keys))
     );
   });
 
   it('seeds alert notice shell actions synchronously so the workbench does not paint blank controls', () => {
+    const keys = [
+      'menu.alert.dispatch',
+      'alert.notice.receiver',
+      'alert.notice.receiver.new',
+      'alert.notice.receiver.edit',
+      'alert.notice.receiver.delete',
+      'alert.notice.receiver.setting',
+      'alert.notice.send-test',
+      'alert.notice.send-test.notify.success',
+      'alert.notice.send-test.notify.failed',
+      'common.search',
+      'common.edit',
+      'common.total',
+      'common.button.delete',
+      'common.button.ok',
+      'common.button.cancel',
+      'common.confirm.delete',
+      'common.delete-success',
+      'common.delete-failed',
+      'monitor.status'
+    ];
+
     expect(buildInitialMessages('zh-CN')).toEqual(
-      expect.objectContaining({
-        'menu.alert.dispatch': '消息通知',
-        'alert.notice.receiver': '通知媒介',
-        'alert.notice.receiver.new': '新增接收对象',
-        'alert.notice.receiver.edit': '编辑接收对象',
-        'alert.notice.receiver.delete': '删除接收对象',
-        'alert.notice.receiver.setting': '配置',
-        'alert.notice.send-test': '发送告警测试',
-        'alert.notice.send-test.notify.success': '触发告警测试成功!',
-        'alert.notice.send-test.notify.failed': '触发告警测试失败!',
-        'common.search': '搜索',
-        'common.edit': '操作',
-        'common.total': '总量',
-        'common.button.delete': '删除选中项',
-        'common.button.ok': '确定',
-        'common.button.cancel': '取消',
-        'common.confirm.delete': '请确认是否删除!',
-        'common.delete-success': '删除成功',
-        'common.delete-failed': '删除失败',
-        'monitor.status': '任务状态'
-      })
+      expect.objectContaining(supplementalMessages('zh-CN', keys))
     );
 
     expect(buildInitialMessages('en-US')).toEqual(
-      expect.objectContaining({
-        'menu.alert.dispatch': 'Notification',
-        'alert.notice.receiver': 'Notice Receiver',
-        'alert.notice.receiver.new': 'New Receiver',
-        'alert.notice.receiver.edit': 'Edit Receiver',
-        'alert.notice.receiver.delete': 'Delete Receiver',
-        'alert.notice.receiver.setting': 'Setting',
-        'alert.notice.send-test': 'Send Alert Test Msg',
-        'alert.notice.send-test.notify.success': 'Send Alert Test Success!',
-        'alert.notice.send-test.notify.failed': 'Send Alert Test Failed!',
-        'common.search': 'Search',
-        'common.edit': 'Operate',
-        'common.total': 'Total',
-        'common.button.delete': 'Delete selected',
-        'common.button.ok': 'OK',
-        'common.button.cancel': 'Cancel',
-        'common.confirm.delete': 'Please confirm whether to delete!',
-        'common.delete-success': 'Deleted successfully',
-        'common.delete-failed': 'Delete failed',
-        'monitor.status': 'Task Status'
-      })
+      expect.objectContaining(supplementalMessages('en-US', keys))
     );
   });
 
@@ -179,8 +153,10 @@ describe('i18n provider remote overlay messages', () => {
     await expect(loadLocaleMessages('zh-CN', false)).resolves.toEqual(
       expect.objectContaining({
         'menu.home': 'Dashboard',
-        'monitor.detail.history-series.search.count': '个采样点',
-        'monitor.detail.history-metric.search.count': '个指标'
+        ...supplementalMessages('zh-CN', [
+          'monitor.detail.history-series.search.count',
+          'monitor.detail.history-metric.search.count'
+        ])
       })
     );
   });
diff --git a/web-next/components/settings/settings-console-shell.test.tsx b/web-next/components/settings/settings-console-shell.test.tsx
index 994da7e5d9..0cc64ce131 100644
--- a/web-next/components/settings/settings-console-shell.test.tsx
+++ b/web-next/components/settings/settings-console-shell.test.tsx
@@ -18,14 +18,14 @@ describe('settings console shell', () => {
     const html = renderToStaticMarkup(
       <SettingsConsoleShell
         activeHref="/setting/settings/config"
-        contentLabel="设置内容"
-        kicker="设置"
-        navigationLabel="设置分区"
-        title="把系统配置、消息通道和接入凭证放在一起"
-        subtitle="在同一页查看系统配置、服务器、对象存储和接入令牌。"
+        contentLabel="Settings content"
+        kicker="Settings"
+        navigationLabel="Settings sections"
+        title="Keep system configuration, message channels, and access credentials together"
+        subtitle="Review system configuration, servers, object storage, and access tokens on one page."
         items={[
-          { href: '/setting/settings/config', label: '系统配置' },
-          { href: '/setting/settings/server', label: '消息服务配置' }
+          { href: '/setting/settings/config', label: 'System configuration' },
+          { href: '/setting/settings/server', label: 'Message service configuration' }
         ]}
       >
         <div>content</div>
@@ -43,18 +43,18 @@ describe('settings console shell', () => {
     expect(html).toContain('data-settings-console-menu="cold-static-list"');
     expect(html).toContain('data-settings-console-menu-mode="angular-responsive-inline-horizontal"');
     expect(html).toContain('data-settings-console-content="cold-settings-content"');
-    expect(html).toContain('data-settings-console-active-title="系统配置"');
-    expect(html).toContain('data-settings-console-active-title-marker="系统配置"');
-    expect(html).toContain('aria-label="设置分区"');
-    expect(html).toContain('aria-label="设置内容"');
+    expect(html).toContain('data-settings-console-active-title="System configuration"');
+    expect(html).toContain('data-settings-console-active-title-marker="System configuration"');
+    expect(html).toContain('aria-label="Settings sections"');
+    expect(html).toContain('aria-label="Settings content"');
     expect(html).toContain('data-settings-console-menu-item="/setting/settings/config"');
     expect(html).toContain('data-settings-console-active="true"');
     expect(html).toContain('data-settings-console-selected-contract="angular-nz-selected"');
-    expect(html).toContain('设置');
-    expect(html).toContain('把系统配置、消息通道和接入凭证放在一起');
-    expect(html).toContain('在同一页查看系统配置、服务器、对象存储和接入令牌。');
-    expect(html).toContain('系统配置');
-    expect(html).toContain('消息服务配置');
+    expect(html).toContain('Settings');
+    expect(html).toContain('Keep system configuration, message channels, and access credentials together');
+    expect(html).toContain('Review system configuration, servers, object storage, and access tokens on one page.');
+    expect(html).toContain('System configuration');
+    expect(html).toContain('Message service configuration');
     expect(html).toContain('content');
     expect(html).toContain('href="/setting/settings/config"');
     expect(html).toContain('aria-current="page"');
diff --git a/web-next/components/settings/settings-dialog-form.test.tsx b/web-next/components/settings/settings-dialog-form.test.tsx
index 2fdfad2f35..a11be92ed2 100644
--- a/web-next/components/settings/settings-dialog-form.test.tsx
+++ b/web-next/components/settings/settings-dialog-form.test.tsx
@@ -14,10 +14,10 @@ describe('settings dialog form controls', () => {
   it('renders dialog dropdowns through the shared cold custom select owner', () => {
     const html = renderToStaticMarkup(
       <SettingsDialogForm>
-        <SettingsDialogField label="短信类型" required>
+        <SettingsDialogField label="SMS type" required>
           <SettingsDialogSelect defaultValue="tencent" defaultOpen>
-            <option value="tencent">腾讯短信</option>
-            <option value="aliyun">阿里短信</option>
+            <option value="tencent">Tencent SMS</option>
+            <option value="aliyun">Aliyun SMS</option>
           </SettingsDialogSelect>
         </SettingsDialogField>
         <SettingsDialogField label="SecretId" required>
@@ -69,13 +69,13 @@ describe('settings dialog form controls', () => {
   it('supports the Angular vertical form layout used by token generation dialogs', () => {
     const html = renderToStaticMarkup(
       <SettingsDialogForm>
-        <SettingsDialogField label="令牌名称" required layout="vertical">
+        <SettingsDialogField label="Token name" required layout="vertical">
           <SettingsDialogInput value="ops-token" readOnly />
         </SettingsDialogField>
-        <SettingsDialogField label="过期时间" layout="vertical">
+        <SettingsDialogField label="Expiration time" layout="vertical">
           <SettingsDialogSelect defaultValue="-1" defaultOpen>
-            <option value="-1">永不过期</option>
-            <option value="604800">7 天</option>
+            <option value="-1">Never expires</option>
+            <option value="604800">7 days</option>
           </SettingsDialogSelect>
         </SettingsDialogField>
       </SettingsDialogForm>
diff --git a/web-next/components/settings/settings-form.test.tsx b/web-next/components/settings/settings-form.test.tsx
index d622c8e9c2..d59e27e533 100644
--- a/web-next/components/settings/settings-form.test.tsx
+++ b/web-next/components/settings/settings-form.test.tsx
@@ -15,16 +15,16 @@ describe('settings form controls', () => {
   it('renders cold-matte form controls with low-radius dense settings rhythm', () => {
     const html = renderToStaticMarkup(
       <SettingsForm data-setting-config-form="cold-settings-form">
-        <SettingsFormField label="系统语言">
+        <SettingsFormField label="System language">
           <SettingsFormSelect value="zh-CN" onChange={() => {}}>
-            <option value="zh-CN">简体中文</option>
+            <option value="zh-CN">Simplified Chinese</option>
           </SettingsFormSelect>
         </SettingsFormField>
-        <SettingsFormField label="访问密钥">
+        <SettingsFormField label="Access key">
           <SettingsFormInput value="ak" onChange={() => {}} />
         </SettingsFormField>
         <SettingsFormActions data-setting-config-actions="standard-equal-buttons">
-          <button type="submit">确认更新</button>
+          <button type="submit">Confirm update</button>
         </SettingsFormActions>
       </SettingsForm>
     );
diff --git a/web-next/components/settings/settings-summary-list.test.tsx b/web-next/components/settings/settings-summary-list.test.tsx
index 0e53291f43..48fd3405f5 100644
--- a/web-next/components/settings/settings-summary-list.test.tsx
+++ b/web-next/components/settings/settings-summary-list.test.tsx
@@ -10,18 +10,18 @@ describe('SettingsSummaryList', () => {
         items={[
           {
             key: 'email',
-            title: '邮件服务器',
-            lines: ['邮箱服务器地址: -', '邮箱账号: -'],
-            actionLabel: '配置',
-            actionAriaLabel: '配置邮件服务器',
+            title: 'Email server',
+            lines: ['Email server address: -', 'Email account: -'],
+            actionLabel: 'Configure',
+            actionAriaLabel: 'Configure email server',
             onAction: vi.fn()
           },
           {
             key: 'sms',
-            title: '短信配置',
-            lines: ['短信类型: 腾讯短信', '启用状态: 否'],
-            actionLabel: '配置',
-            actionAriaLabel: '配置短信配置',
+            title: 'SMS settings',
+            lines: ['SMS type: Tencent SMS', 'Enabled: no'],
+            actionLabel: 'Configure',
+            actionAriaLabel: 'Configure SMS settings',
             onAction: vi.fn()
           }
         ]}
@@ -37,16 +37,16 @@ describe('SettingsSummaryList', () => {
     expect(html).toContain('data-settings-summary-action="email"');
     expect(html).toContain('data-settings-summary-action="sms"');
     expect(html).toContain('data-settings-summary-action-style="cold-compact-action"');
-    expect(html).toContain('aria-label="配置邮件服务器"');
-    expect(html).toContain('aria-label="配置短信配置"');
+    expect(html).toContain('aria-label="Configure email server"');
+    expect(html).toContain('aria-label="Configure SMS settings"');
     expect(html).toContain('rounded-[4px]');
     expect(html).toContain('border-[#2b3039]');
     expect(html).toContain('bg-[#0b0c0e]');
     expect(html).toContain('rounded-[3px]');
     expect(html).toContain('h-8');
-    expect(html).toContain('邮件服务器');
-    expect(html).toContain('短信配置');
-    expect(html).toContain('配置');
+    expect(html).toContain('Email server');
+    expect(html).toContain('SMS settings');
+    expect(html).toContain('Configure');
     expect(html).not.toContain('angular-nz-list');
     expect(html).not.toContain('border-[#d8dee9]');
   });
diff --git a/web-next/components/shell/app-frame.chrome.test.tsx b/web-next/components/shell/app-frame.chrome.test.tsx
index bff8b617d5..6b8395a5fb 100644
--- a/web-next/components/shell/app-frame.chrome.test.tsx
+++ b/web-next/components/shell/app-frame.chrome.test.tsx
@@ -76,6 +76,8 @@ vi.mock('@/lib/utils', () => ({
 }));
 
 const setupT = createTranslatorMock({ locale: 'zh-CN' });
+const setupHeadline = (percent: number) => setupT('layout.setup.progress.headline', { percent });
+const srOnlyMarkup = (label: string) => `class="sr-only">${label}</span>`;
 
 const emptySetupState = {
   monitorTotal: 0,
@@ -107,7 +109,7 @@ describe('app frame chrome', () => {
 
     expect(summary.completedCount).toBe(0);
     expect(summary.progressPercent).toBe(0);
-    expect(summary.headline).toBe('你已完成 0% 的平台配置');
+    expect(summary.headline).toBe(setupHeadline(0));
   }, 15000);
 
   it('keeps the startup setup baseline on the entity create route when the live setup state is empty', async () => {
@@ -116,7 +118,7 @@ describe('app frame chrome', () => {
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('你已完成 83% 的平台配置');
+    expect(summary.headline).toBe(setupHeadline(83));
   }, 15000);
 
   it('keeps the startup setup baseline on the entity import route when the live setup state is empty', async () => {
@@ -125,7 +127,7 @@ describe('app frame chrome', () => {
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('你已完成 83% 的平台配置');
+    expect(summary.headline).toBe(setupHeadline(83));
   });
 
   it('keeps the startup setup baseline on the entity discovery route when the live setup state is empty', async () => {
@@ -134,7 +136,7 @@ describe('app frame chrome', () => {
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('你已完成 83% 的平台配置');
+    expect(summary.headline).toBe(setupHeadline(83));
   });
 
   it('keeps the startup setup baseline on overview routes when the live setup state is empty', async () => {
@@ -145,7 +147,7 @@ describe('app frame chrome', () => {
 
       expect(summary.completedCount).toBe(5);
       expect(summary.progressPercent).toBe(83);
-      expect(summary.headline).toBe('你已完成 83% 的平台配置');
+      expect(summary.headline).toBe(setupHeadline(83));
     }
   });
 
@@ -157,7 +159,7 @@ describe('app frame chrome', () => {
 
       expect(summary.completedCount).toBe(5);
       expect(summary.progressPercent).toBe(83);
-      expect(summary.headline).toBe('你已完成 83% 的平台配置');
+      expect(summary.headline).toBe(setupHeadline(83));
     }
   });
 
@@ -167,7 +169,7 @@ describe('app frame chrome', () => {
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('你已完成 83% 的平台配置');
+    expect(summary.headline).toBe(setupHeadline(83));
   });
 
   it('keeps the startup setup baseline on the OTLP intake route when the live setup state is empty', async () => {
@@ -176,7 +178,7 @@ describe('app frame chrome', () => {
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('你已完成 83% 的平台配置');
+    expect(summary.headline).toBe(setupHeadline(83));
   });
 
   it('keeps the startup setup baseline on entity definition routes when the live setup state is empty', async () => {
@@ -185,7 +187,7 @@ describe('app frame chrome', () => {
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('你已完成 83% 的平台配置');
+    expect(summary.headline).toBe(setupHeadline(83));
   });
 
   it('keeps the live empty setup state on the entity edit route', async () => {
@@ -194,7 +196,7 @@ describe('app frame chrome', () => {
 
     expect(summary.completedCount).toBe(0);
     expect(summary.progressPercent).toBe(0);
-    expect(summary.headline).toBe('你已完成 0% 的平台配置');
+    expect(summary.headline).toBe(setupHeadline(0));
   });
 
   it('renders shared header utility triggers with icon-only parity chrome on ops routes', async () => {
@@ -234,7 +236,7 @@ describe('app frame chrome', () => {
     expect(html).toContain('data-app-frame-glyph="mute-megaphone"');
     expect(html).toContain('data-app-frame-glyph="settings-gear"');
     expect(html).toContain('data-app-frame-user-avatar="angular-circle"');
-    expect(html).toContain('开始使用');
+    expect(html).toContain(setupT('layout.setup.title'));
     expect(html).toContain('data-app-frame-setup-progress="angular-reference"');
     expect(html).toContain('style="width:83%"');
     expect(html).not.toContain('common.setup');
@@ -243,19 +245,19 @@ describe('app frame chrome', () => {
     expect(html).toContain('data-platform-main-scroll="content-flow"');
     expect(html).toContain('data-platform-footer-placement="flow-end-or-viewport-bottom"');
     expect(html).toContain('Apache HertzBeat™ v1.8.0');
-    expect(html).not.toContain('遵循 Apache License, Version 2.0 授权');
+    expect(html).not.toContain(setupT('platform.footer.license'));
     expect(html).not.toContain('Apache License, Version 2.0');
     expect(html).toContain('data-shell-ai-chat-launcher="angular-ai-chat"');
-    expect(html).toContain('class="sr-only">打开 AI 助手</span>');
-    expect(html).toContain('class="sr-only">GitHub</span>');
-    expect(html).toContain('class="sr-only">菜单</span>');
-    expect(html).toContain('class="sr-only">通知</span>');
-    expect(html).toContain('class="sr-only">取消静音</span>');
-    expect(html).toContain('class="sr-only">锁屏</span>');
-    expect(html).toContain('class="sr-only">设置</span>');
-    expect(html).toContain('class="sr-only">用户菜单:admin</span>');
-    expect(html).toContain('placeholder="询问助手"');
-    expect(html).toContain('aria-label="发送提示"');
+    expect(html).toContain(srOnlyMarkup(setupT('ai.chat.launch')));
+    expect(html).toContain(srOnlyMarkup(setupT('app.frame.utility.github')));
+    expect(html).toContain(srOnlyMarkup(setupT('app.frame.utility.menu')));
+    expect(html).toContain(srOnlyMarkup(setupT('common.notify')));
+    expect(html).toContain(srOnlyMarkup(setupT('common.unmute')));
+    expect(html).toContain(srOnlyMarkup(setupT('common.lock')));
+    expect(html).toContain(srOnlyMarkup(setupT('menu.settings')));
+    expect(html).toContain(srOnlyMarkup(setupT('app.frame.utility.user', { user: 'admin' })));
+    expect(html).toContain(`placeholder="${setupT('ai.chat.input.placeholder')}"`);
+    expect(html).toContain(`aria-label="${setupT('ai.chat.submit')}"`);
     expect(html).toContain('overview-body');
   });
 
diff --git a/web-next/components/shell/app-sidebar.test.tsx b/web-next/components/shell/app-sidebar.test.tsx
index 15a3ed4a7f..2cf6c7810b 100644
--- a/web-next/components/shell/app-sidebar.test.tsx
+++ b/web-next/components/shell/app-sidebar.test.tsx
@@ -21,23 +21,23 @@ describe('AppSidebar', () => {
     expect(html).toContain('data-app-sidebar="true"');
     expect(html).toContain('data-app-sidebar-nav="true"');
     expect(html).toContain('data-shell-nav-item="overview-home"');
-    expect(html).toContain('接入采集');
-    expect(html).toContain('对象资源');
-    expect(html).toContain('可观测排障');
-    expect(html).toContain('告警处置');
-    expect(html).toContain('仪表盘');
-    expect(html).toContain('平台设置');
+    expect(html).toContain(t('menu.section.ingestion'));
+    expect(html).toContain(t('menu.section.objects'));
+    expect(html).toContain(t('menu.section.observability'));
+    expect(html).toContain(t('menu.section.alerting'));
+    expect(html).toContain(t('menu.section.dashboards'));
+    expect(html).toContain(t('menu.section.settings'));
     expect(html).toContain('data-shell-nav-item="settings-mcp-server"');
-    expect(html).toContain('MCP服务器');
+    expect(html).toContain(t('menu.advanced.mcp-server'));
     expect(html).toContain('data-shell-nav-item="help-center"');
-    expect(html).toContain('帮助中心');
+    expect(html).toContain(t('menu.extras.help'));
     expect(html).not.toContain('data-shell-nav-item="incidents"');
     expect(html).not.toContain('data-shell-nav-item="actions"');
     expect(html).not.toContain('data-shell-nav-item="status-public"');
     expect(html).toContain('href="/overview"');
     expect(html).not.toContain('data-app-sidebar-operator="true"');
-    expect(html).not.toContain('操作员');
-    expect(html).not.toContain('已认证会话');
+    expect(html).not.toMatch(/\u64cd\u4f5c\u5458/);
+    expect(html).not.toMatch(/\u5df2\u8ba4\u8bc1\u4f1a\u8bdd/);
   });
 
   it('flattens shared nav rows so the sidebar no longer renders every item as a bordered card with a boxed icon', () => {
diff --git a/web-next/components/shell/locale-option-list.test.tsx b/web-next/components/shell/locale-option-list.test.tsx
index 4b9c74df15..b3906ecc9c 100644
--- a/web-next/components/shell/locale-option-list.test.tsx
+++ b/web-next/components/shell/locale-option-list.test.tsx
@@ -27,8 +27,8 @@ describe('LocaleOptionList', () => {
       />
     );
 
-    expect(html).toContain('英语(en_US)');
-    expect(html).toContain('简体中文(zh_CN)');
+    expect(html).toContain(t('settings.system-config.locale.en_US'));
+    expect(html).toContain(t('settings.system-config.locale.zh_CN'));
     expect(html).toContain('✓');
     expect(html).toContain('data-hz-ui="locale-menu-option"');
     expect(html).toContain('data-hz-locale-menu-option-owner="hertzbeat-ui-locale-menu-option"');
diff --git a/web-next/components/shell/platform-copyright-footer.test.tsx b/web-next/components/shell/platform-copyright-footer.test.tsx
index cebb891ee4..7a96f1f0a7 100644
--- a/web-next/components/shell/platform-copyright-footer.test.tsx
+++ b/web-next/components/shell/platform-copyright-footer.test.tsx
@@ -14,7 +14,7 @@ describe('PlatformCopyrightFooter', () => {
     expect(html).toContain('Apache HertzBeat™ 1.0.0');
     expect(html).not.toContain('Licensed under the Apache License, Version 2.0');
     expect(html).not.toContain('Apache License, Version 2.0');
-    expect(html).not.toContain('授权');
+    expect(html).not.toMatch(/\u6388\u6743/u);
     expect(html).toContain('https://hertzbeat.apache.org');
   });
 });
diff --git a/web-next/components/ui/checkbox.test.tsx b/web-next/components/ui/checkbox.test.tsx
index 5c770b10ee..22cc9f5bd3 100644
--- a/web-next/components/ui/checkbox.test.tsx
+++ b/web-next/components/ui/checkbox.test.tsx
@@ -5,7 +5,7 @@ import { Checkbox } from './checkbox';
 
 describe('Checkbox', () => {
   it('renders the shared cold checkbox owner instead of default checkbox chrome', () => {
-    const html = renderToStaticMarkup(<Checkbox name="enabled" checked readOnly label="启用状态" />);
+    const html = renderToStaticMarkup(<Checkbox name="enabled" checked readOnly label="Enabled state" />);
 
     expect(html).toContain('<label');
     expect(html).toContain('data-cold-checkbox-owner="cold-checkbox"');
diff --git a/web-next/components/ui/cold-confirm-dialog.test.tsx b/web-next/components/ui/cold-confirm-dialog.test.tsx
index b1a6ef5540..95ceed9c51 100644
... 11603 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 38/50: fix(web-next): cap topology auto-fit zoom

Posted by zh...@apache.org.
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 4e969d7bcc743edf471649b97a2c418b083feff2
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 08:41:19 2026 +0800

    fix(web-next): cap topology auto-fit zoom
---
 .../packages/hertzbeat-ui/src/topology-g6.test.tsx |  9 ++++--
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx | 32 ++++++++++++++++------
 2 files changed, 31 insertions(+), 10 deletions(-)

diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
index 8e675129e8..04a813fb86 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
@@ -365,12 +365,17 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(source).not.toContain("fitAndCenterG6Viewport(runtimeGraph, { when: 'always' }, false)");
     expect(source).not.toContain("fitAndCenterG6Viewport(runtimeGraph, { when: 'always' }, { duration: 120 })");
     expect(source).not.toContain("runtimeGraph.fitView?.({ when: 'always' }, false);\n        await runtimeGraph.fitCenter?.(false);");
-    expect(html).toContain('data-hz-topology-g6-auto-fit-zoom-bounds="0.18-1.35"');
-    expect(html).toContain('data-hz-topology-g6-auto-fit-max-zoom="1.35"');
+    expect(html).toContain('data-hz-topology-g6-auto-fit-zoom-bounds="0.18-1"');
+    expect(html).toContain('data-hz-topology-g6-auto-fit-max-zoom="1"');
     expect(html).toContain('data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"');
+    expect(html).toContain('data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range"');
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-bounds="0.18-4.8"');
     expect(html).toContain('data-hz-topology-g6-fit-mode="overflow-only-center"');
     expect(clampHzTopologyG6AutoFitZoom(4.8)).toBe(HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM);
     expect(clampHzTopologyG6AutoFitZoom(0.72)).toBe(0.72);
+    expect(source).toContain('withG6AutoFitZoomRange(runtimeGraph, async () => {');
+    expect(source).toContain('runtimeGraph.setZoomRange?.([HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM])');
+    expect(source).toContain('runtimeGraph.setZoomRange?.([HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_MAX_ZOOM])');
   });
 
   it('centers the shared G6 canvas after fit and reset view actions', () => {
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
index b590d2c5e1..d638b35501 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
@@ -222,6 +222,8 @@ type G6GraphRuntime = {
   fitCenter?: (animation?: Record<string, unknown> | boolean) => Promise<void>;
   zoomBy?: (scale: number, animation?: Record<string, unknown> | boolean) => Promise<void>;
   zoomTo?: (scale: number, animation?: Record<string, unknown> | boolean, origin?: number[]) => Promise<void>;
+  setZoomRange?: (zoomRange: [number, number]) => void;
+  getZoomRange?: () => [number, number];
   getZoom?: () => number;
   getPosition?: () => number[];
   translateTo?: (position: number[], animation?: Record<string, unknown> | boolean) => Promise<void>;
@@ -232,7 +234,7 @@ type G6GraphRuntime = {
 
 export const HZ_TOPOLOGY_G6_MIN_ZOOM = 0.18;
 export const HZ_TOPOLOGY_G6_MAX_ZOOM = 4.8;
-export const HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM = 1.35;
+export const HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM = 1;
 
 type HzTopologyG6ViewportSnapshot = {
   zoom: number;
@@ -1128,13 +1130,24 @@ async function fitAndCenterG6Viewport(
   animation: Record<string, unknown> | boolean
 ) {
   if (!runtimeGraph) return;
-  await runtimeGraph.fitView?.(fitOptions, animation);
-  await runtimeGraph.fitCenter?.(animation);
-  const currentZoom = runtimeGraph.getZoom?.();
-  const clampedZoom = clampHzTopologyG6AutoFitZoom(currentZoom);
-  if (clampedZoom !== undefined && currentZoom !== undefined && clampedZoom < currentZoom) {
-    await runtimeGraph.zoomTo?.(clampedZoom, false);
-    await runtimeGraph.fitCenter?.(false);
+  await withG6AutoFitZoomRange(runtimeGraph, async () => {
+    await runtimeGraph.fitView?.(fitOptions, animation);
+    await runtimeGraph.fitCenter?.(animation);
+    const currentZoom = runtimeGraph.getZoom?.();
+    const clampedZoom = clampHzTopologyG6AutoFitZoom(currentZoom);
+    if (clampedZoom !== undefined && currentZoom !== undefined && clampedZoom < currentZoom) {
+      await runtimeGraph.zoomTo?.(clampedZoom, false);
+      await runtimeGraph.fitCenter?.(false);
+    }
+  });
+}
+
+async function withG6AutoFitZoomRange(runtimeGraph: G6GraphRuntime, action: () => Promise<void>) {
+  runtimeGraph.setZoomRange?.([HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM]);
+  try {
+    await action();
+  } finally {
+    runtimeGraph.setZoomRange?.([HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_MAX_ZOOM]);
   }
 }
 
@@ -1531,6 +1544,7 @@ export function HzTopologyG6Canvas({
           width,
           height: graphHeight,
           autoFit: false,
+          zoomRange: [HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_MAX_ZOOM],
           background: '#08090c',
           animation: false,
           data: initialGraphData,
@@ -1831,6 +1845,8 @@ export function HzTopologyG6Canvas({
       data-hz-topology-g6-auto-fit-zoom-bounds={`${HZ_TOPOLOGY_G6_MIN_ZOOM}-${HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM}`}
       data-hz-topology-g6-auto-fit-max-zoom={HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM}
       data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"
+      data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range"
+      data-hz-topology-g6-operator-zoom-bounds={`${HZ_TOPOLOGY_G6_MIN_ZOOM}-${HZ_TOPOLOGY_G6_MAX_ZOOM}`}
       data-hz-topology-g6-fit-mode="overflow-only-center"
       data-hz-topology-g6-viewport-interaction-state={viewportInteractionState}
       data-hz-topology-g6-viewport-interaction-owner="hertzbeat-ui-g6-viewport-interaction"


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 03/50: feat(web-next): add client runtime foundation

Posted by zh...@apache.org.
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 584fca5f0d852c573d8add771cc06591b097ccb6
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 00:08:52 2026 +0800

    feat(web-next): add client runtime foundation
---
 web-next/app/layout.tsx                            |  13 +-
 .../components/providers/query-provider.test.tsx   |  25 +++
 web-next/components/providers/query-provider.tsx   |  23 +++
 .../components/workbench/client-workbench.test.tsx | 191 ++++++++++++++++++++-
 web-next/components/workbench/client-workbench.tsx |  70 +++++++-
 web-next/lib/api-client.test.ts                    | 137 +++++++++++++++
 web-next/lib/api-client.ts                         | 111 +++++++-----
 web-next/lib/i18n-runtime-messages.ts              |   4 +
 web-next/lib/query-keys.test.ts                    |  40 +++++
 web-next/lib/query-keys.ts                         |  50 ++++++
 web-next/lib/session-client.test.ts                |  86 ++++++++++
 web-next/lib/session-client.ts                     |  96 +++++++++++
 web-next/lib/workbench-load-cache.test.ts          |  31 ++++
 web-next/lib/workbench-load-cache.ts               |  62 ++++++-
 14 files changed, 863 insertions(+), 76 deletions(-)

diff --git a/web-next/app/layout.tsx b/web-next/app/layout.tsx
index 6dc6e96655..2b478d76ae 100644
--- a/web-next/app/layout.tsx
+++ b/web-next/app/layout.tsx
@@ -2,11 +2,12 @@ import 'react-datepicker/dist/react-datepicker.css';
 import './globals.css';
 import type { Metadata } from 'next';
 import { I18nProvider } from '@/components/providers/i18n-provider';
+import { HertzBeatQueryProvider } from '@/components/providers/query-provider';
 import { AppFrame } from '@/components/shell/app-frame';
 
 export const metadata: Metadata = {
-  title: 'HertzBeat Workbench Next Pilot',
-  description: 'Mixed Angular/Next.js observability workbench pilot for HertzBeat.',
+  title: 'HertzBeat Observability Workbench',
+  description: 'Private-deployable operations observability for monitors, OTLP signals, alerts, topology, and safe automation.',
   icons: {
     icon: '/assets/logo.svg',
     shortcut: '/assets/logo.svg'
@@ -17,9 +18,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
   return (
     <html lang="zh-CN">
       <body data-theme="dark-ops">
-        <I18nProvider>
-          <AppFrame>{children}</AppFrame>
-        </I18nProvider>
+        <HertzBeatQueryProvider>
+          <I18nProvider>
+            <AppFrame>{children}</AppFrame>
+          </I18nProvider>
+        </HertzBeatQueryProvider>
       </body>
     </html>
   );
diff --git a/web-next/components/providers/query-provider.test.tsx b/web-next/components/providers/query-provider.test.tsx
new file mode 100644
index 0000000000..7f6412d83c
--- /dev/null
+++ b/web-next/components/providers/query-provider.test.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it } from 'vitest';
+
+import { hertzBeatQueryDefaults, HertzBeatQueryProvider } from './query-provider';
+
+describe('HertzBeatQueryProvider', () => {
+  it('uses Horizon-style short stale query defaults', () => {
+    expect(hertzBeatQueryDefaults.queries).toMatchObject({
+      staleTime: 5000,
+      refetchOnWindowFocus: true,
+      retry: 1
+    });
+  });
+
+  it('renders children through the shared QueryClientProvider', () => {
+    const html = renderToStaticMarkup(
+      <HertzBeatQueryProvider>
+        <span data-query-provider-child="true">ready</span>
+      </HertzBeatQueryProvider>
+    );
+
+    expect(html).toContain('data-query-provider-child="true"');
+  });
+});
diff --git a/web-next/components/providers/query-provider.tsx b/web-next/components/providers/query-provider.tsx
new file mode 100644
index 0000000000..f072dab83c
--- /dev/null
+++ b/web-next/components/providers/query-provider.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import * as React from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+export const hertzBeatQueryDefaults = {
+  queries: {
+    staleTime: 5000,
+    refetchOnWindowFocus: true,
+    retry: 1
+  }
+} as const;
+
+function createHertzBeatQueryClient() {
+  return new QueryClient({
+    defaultOptions: hertzBeatQueryDefaults
+  });
+}
+
+export function HertzBeatQueryProvider({ children }: { children: React.ReactNode }) {
+  const [queryClient] = React.useState(createHertzBeatQueryClient);
+  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
+}
diff --git a/web-next/components/workbench/client-workbench.test.tsx b/web-next/components/workbench/client-workbench.test.tsx
index 57f1b58fe3..06be8dfb4e 100644
--- a/web-next/components/workbench/client-workbench.test.tsx
+++ b/web-next/components/workbench/client-workbench.test.tsx
@@ -1,11 +1,21 @@
+// @vitest-environment jsdom
+
 import React from 'react';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
-import { describe, expect, it, vi } from 'vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
 import { ClientWorkbench } from './client-workbench';
 
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+const mockTranslate = (key: string) => key;
+
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: (key: string) => key
+    t: mockTranslate
   })
 }));
 
@@ -36,19 +46,184 @@ vi.mock('@/lib/workbench-load-cache', () => ({
 }));
 
 describe('ClientWorkbench', () => {
-  it('renders a global visible pending state so slow workbenches do not look blank', () => {
+  let root: Root | null = null;
+  let container: HTMLDivElement | null = null;
+
+  afterEach(() => {
+    if (root) {
+      act(() => {
+        root?.unmount();
+      });
+    }
+    root = null;
+    container?.remove();
+    container = null;
+    vi.useRealTimers();
+  });
+
+  it('defers the global visible pending state so quick route hops do not flash Loading workspace', () => {
     const html = renderToStaticMarkup(
       <ClientWorkbench load={() => Promise.resolve({ ready: true })} loadingTitle="链路工作台" loadingCopy="正在加载链路数据">
         {() => <div>ready</div>}
       </ClientWorkbench>
     );
 
-    expect(html).toContain('data-client-workbench-loading="global-spinner"');
-    expect(html).toContain('data-client-workbench-loading-spinner="true"');
-    expect(html).toContain('role="status"');
+    expect(html).toContain('data-client-workbench-loading="deferred"');
     expect(html).toContain('aria-busy="true"');
-    expect(html).toContain('链路工作台');
-    expect(html).toContain('正在加载链路数据');
+    expect(html).not.toContain('data-client-workbench-loading="global-spinner"');
+    expect(html).not.toContain('data-client-workbench-loading-spinner="true"');
+    expect(html).not.toContain('role="status"');
+    expect(html).not.toContain('链路工作台');
+    expect(html).not.toContain('正在加载链路数据');
     expect(html).not.toContain('ready');
   });
+
+  it('reveals the route loading copy only when the load remains pending beyond the defer window', async () => {
+    let resolveLoad: ((value: { ready: boolean }) => void) | null = null;
+    const load = vi.fn(() => new Promise<{ ready: boolean }>(resolve => {
+      resolveLoad = resolve;
+    }));
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+
+    await act(async () => {
+      root?.render(
+        <ClientWorkbench load={load} loadingTitle="链路工作台" loadingCopy="正在加载链路数据" loadingDelayMs={10}>
+          {() => <div>ready</div>}
+        </ClientWorkbench>
+      );
+      await Promise.resolve();
+    });
+
+    expect(load).toHaveBeenCalledTimes(1);
+    expect(container.textContent).not.toContain('链路工作台');
+    expect(container.querySelector('[data-client-workbench-loading="deferred"]')).not.toBeNull();
+    expect(container.querySelector('[data-client-workbench-loading="global-spinner"]')).toBeNull();
+
+    await act(async () => {
+      await new Promise(resolve => window.setTimeout(resolve, 15));
+    });
+
+    expect(container.querySelector('[data-client-workbench-loading="global-spinner"]')).not.toBeNull();
+    expect(container.textContent).toContain('链路工作台');
+    expect(container.textContent).toContain('正在加载链路数据');
+
+    await act(async () => {
+      resolveLoad?.({ ready: true });
+      await Promise.resolve();
+    });
+
+    expect(container.textContent).toContain('ready');
+    expect(container.querySelector('[data-client-workbench-loading="global-spinner"]')).toBeNull();
+  });
+
+  it('keeps shared workbench loading copy hidden during the deferred first paint', () => {
+    const html = renderToStaticMarkup(
+      <ClientWorkbench load={() => Promise.resolve({ ready: true })}>
+        {() => <div>ready</div>}
+      </ClientWorkbench>
+    );
+
+    expect(html).toContain('data-client-workbench-loading="deferred"');
+    expect(html).not.toContain('common.workbench.loading.title');
+    expect(html).not.toContain('common.workbench.loading.copy');
+    expect(html).not.toContain('common.loading');
+    expect(html).not.toContain('ready');
+  });
+
+  it('forwards settled cache TTL options through the shared workbench cache', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/workbench/client-workbench.tsx'), 'utf8');
+
+    expect(source).toContain('cacheSettledTtlMs?: number');
+    expect(source).toContain('consumeWorkbenchLoad(cacheKey, load, { settledTtlMs: cacheSettledTtlMs })');
+    expect(source).toContain('[cacheKey, cacheSettledTtlMs, load, loadingDelayMs, reloadKey, t]');
+  });
+
+  it('lets route islands render a custom recoverable error state and retry the loader', async () => {
+    const load = vi.fn()
+      .mockRejectedValueOnce(new Error('monitor missing'))
+      .mockResolvedValueOnce({ ready: true });
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+
+    await act(async () => {
+      root?.render(
+        <ClientWorkbench
+          load={load}
+          renderError={(message, retry) => (
+            <section data-custom-error-state="true">
+              <span>{message}</span>
+              <button type="button" onClick={retry}>Retry</button>
+            </section>
+          )}
+        >
+          {() => <div>ready</div>}
+        </ClientWorkbench>
+      );
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(load).toHaveBeenCalledTimes(1);
+    expect(container.textContent).toContain('load failed');
+    expect(container.querySelector('[data-custom-error-state="true"]')).not.toBeNull();
+
+    await act(async () => {
+      container.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(load).toHaveBeenCalledTimes(2);
+    expect(container.textContent).toContain('ready');
+    expect(container.querySelector('[data-custom-error-state="true"]')).toBeNull();
+  });
+
+  it('lets route islands render custom loading chrome after the defer window', async () => {
+    let resolveLoad: ((value: { ready: boolean }) => void) | null = null;
+    const load = vi.fn(() => new Promise<{ ready: boolean }>(resolve => {
+      resolveLoad = resolve;
+    }));
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+
+    await act(async () => {
+      root?.render(
+        <ClientWorkbench
+          load={load}
+          loadingDelayMs={10}
+          renderLoading={visible => (
+            <section data-custom-loading-state={visible ? 'visible' : 'deferred'}>
+              {visible ? 'route loading' : null}
+            </section>
+          )}
+        >
+          {() => <div>ready</div>}
+        </ClientWorkbench>
+      );
+      await Promise.resolve();
+    });
+
+    expect(load).toHaveBeenCalledTimes(1);
+    expect(container.querySelector('[data-custom-loading-state="deferred"]')).not.toBeNull();
+    expect(container.textContent).not.toContain('route loading');
+
+    await act(async () => {
+      await new Promise(resolve => window.setTimeout(resolve, 15));
+    });
+
+    expect(container.querySelector('[data-custom-loading-state="visible"]')).not.toBeNull();
+    expect(container.textContent).toContain('route loading');
+
+    await act(async () => {
+      resolveLoad?.({ ready: true });
+      await Promise.resolve();
+    });
+
+    expect(container.textContent).toContain('ready');
+    expect(container.querySelector('[data-custom-loading-state]')).toBeNull();
+  });
 });
diff --git a/web-next/components/workbench/client-workbench.tsx b/web-next/components/workbench/client-workbench.tsx
index 21be141272..226c2084ba 100644
--- a/web-next/components/workbench/client-workbench.tsx
+++ b/web-next/components/workbench/client-workbench.tsx
@@ -4,27 +4,39 @@ import React, { useEffect, useRef, useState } from 'react';
 import { Loader2 } from 'lucide-react';
 import { ObservabilityStatusState } from '@/components/observability';
 import { useI18n } from '@/components/providers/i18n-provider';
-import { getAuthorizationToken } from '@/lib/api-client';
 import { resolveWorkbenchError } from '@/lib/client-workbench-state';
 import { buildLoginRedirectHref, buildLoginReturnTo } from '@/lib/passport-login/controller';
+import { readClientSessionState } from '@/lib/session-client';
 import { consumeWorkbenchLoad } from '@/lib/workbench-load-cache';
 
+const CLIENT_WORKBENCH_LOADING_DELAY_MS = 650;
+
 export function ClientWorkbench<T>({
   load,
   children,
+  renderError,
+  renderLoading,
   loadingTitle,
   loadingCopy,
-  cacheKey
+  cacheKey,
+  cacheSettledTtlMs,
+  loadingDelayMs = CLIENT_WORKBENCH_LOADING_DELAY_MS
 }: {
   load: () => Promise<T>;
   children: (data: T) => React.ReactNode;
+  renderError?: (message: string, retry: () => void) => React.ReactNode;
+  renderLoading?: (visible: boolean) => React.ReactNode;
   loadingTitle?: string;
   loadingCopy?: string;
   cacheKey?: string;
+  cacheSettledTtlMs?: number;
+  loadingDelayMs?: number;
 }) {
   const { t } = useI18n();
   const [data, setData] = useState<T | null>(null);
   const [error, setError] = useState<string | null>(null);
+  const [showPendingState, setShowPendingState] = useState(false);
+  const [reloadKey, setReloadKey] = useState(0);
   const loadRef = useRef<{
     load: (() => Promise<T>) | null;
     promise: Promise<T> | null;
@@ -32,8 +44,26 @@ export function ClientWorkbench<T>({
     load: null,
     promise: null
   });
+  const retry = () => {
+    loadRef.current = {
+      load: null,
+      promise: null
+    };
+    setData(null);
+    setError(null);
+    setShowPendingState(false);
+    setReloadKey(key => key + 1);
+  };
+
   useEffect(() => {
     let cancelled = false;
+    setShowPendingState(false);
+    const pendingDelay = Math.max(0, loadingDelayMs);
+    const pendingTimer = window.setTimeout(() => {
+      if (!cancelled) {
+        setShowPendingState(true);
+      }
+    }, pendingDelay);
     if (loadRef.current.load !== load) {
       loadRef.current = {
         load,
@@ -42,7 +72,7 @@ export function ClientWorkbench<T>({
     }
     if (!loadRef.current.promise) {
       loadRef.current.promise = cacheKey
-        ? consumeWorkbenchLoad(cacheKey, load)
+        ? consumeWorkbenchLoad(cacheKey, load, { settledTtlMs: cacheSettledTtlMs })
         : load().finally(() => {
             if (loadRef.current.load === load) {
               loadRef.current.promise = null;
@@ -52,33 +82,55 @@ export function ClientWorkbench<T>({
     loadRef.current.promise
       .then(result => {
         if (!cancelled) {
+          window.clearTimeout(pendingTimer);
           setData(result);
           setError(null);
         }
       })
       .catch(err => {
         if (!cancelled) {
-          const { redirectToLogin, message } = resolveWorkbenchError(err, Boolean(getAuthorizationToken()), t);
+          window.clearTimeout(pendingTimer);
+          const { redirectToLogin, message } = resolveWorkbenchError(err, false, t);
           if (redirectToLogin) {
-            const returnTo = buildLoginReturnTo(window.location);
-            window.location.href = buildLoginRedirectHref(returnTo, process.env.NEXT_PUBLIC_LOGIN_PATH);
-            return;
+            void readClientSessionState().then(session => {
+              if (cancelled || session.authenticated) return;
+              const returnTo = buildLoginReturnTo(window.location);
+              window.location.href = buildLoginRedirectHref(returnTo, process.env.NEXT_PUBLIC_LOGIN_PATH);
+            });
           }
           setError(message);
         }
       });
     return () => {
       cancelled = true;
+      window.clearTimeout(pendingTimer);
     };
-  }, [cacheKey, load, t]);
+  }, [cacheKey, cacheSettledTtlMs, load, loadingDelayMs, reloadKey, t]);
 
   if (error) {
+    if (renderError) {
+      return <>{renderError(error, retry)}</>;
+    }
     return <ObservabilityStatusState title={t('common.load-failed')} copy={error} tone="danger" />;
   }
 
   if (!data) {
+    if (renderLoading) {
+      return <>{renderLoading(showPendingState)}</>;
+    }
+
+    if (!showPendingState) {
+      return (
+        <section
+          data-client-workbench-loading="deferred"
+          aria-busy="true"
+          className="min-h-[260px]"
+        />
+      );
+    }
+
     const pendingTitle = loadingTitle ?? t('common.workbench.loading.title');
-    const pendingCopy = loadingCopy ?? t('common.loading');
+    const pendingCopy = loadingCopy ?? t('common.workbench.loading.copy');
 
     return (
       <section
diff --git a/web-next/lib/api-client.test.ts b/web-next/lib/api-client.test.ts
new file mode 100644
index 0000000000..37489775da
--- /dev/null
+++ b/web-next/lib/api-client.test.ts
@@ -0,0 +1,137 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { apiDownload, apiGet, apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from './api-client';
+
+const fetchMock = vi.fn<typeof fetch>();
+
+function mockApiMessagePayload(payload: unknown) {
+  fetchMock.mockResolvedValueOnce(
+    new Response(JSON.stringify(payload), {
+      status: 200,
+      headers: { 'Content-Type': 'application/json' }
+    })
+  );
+}
+
+function mockHttpStatus(status: number) {
+  fetchMock.mockResolvedValueOnce(new Response('unavailable', { status }));
+}
+
+describe('api client message helpers', () => {
+  afterEach(() => {
+    fetchMock.mockReset();
+    vi.unstubAllGlobals();
+  });
+
+  it('uses runtime fallback copy when the backend omits a non-zero message', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ code: 500, data: null });
+
+    await expect(apiMessageGet('/bad')).rejects.toThrow('API message returned non-zero code');
+  });
+
+  it('preserves backend-provided message text for non-zero payloads', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ code: 500, msg: 'Backend said no', data: null });
+
+    await expect(apiMessagePost('/bad', {})).rejects.toThrow('Backend said no');
+  });
+
+  it('preserves backend-provided message text for write helpers used by monitor detect and save', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ code: 1, msg: 'Detect failed from backend', data: null });
+    mockApiMessagePayload({ code: 2, msg: 'Save failed from backend', data: null });
+
+    await expect(apiMessagePost('/monitor/detect', { monitor: { app: 'website' } })).rejects.toThrow(
+      'Detect failed from backend'
+    );
+    await expect(apiMessagePut('/monitor/1001', { monitor: { app: 'website' } })).rejects.toThrow(
+      'Save failed from backend'
+    );
+  });
+
+  it('posts FormData bodies without forcing a JSON content type', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ code: 0, data: { ok: true } });
+
+    const formData = new FormData();
+    formData.append('name', 'smtp');
+    await expect(apiMessagePost('/plugin', formData)).resolves.toEqual({ ok: true });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/plugin',
+      expect.objectContaining({
+        method: 'POST',
+        body: formData
+      })
+    );
+    expect(JSON.stringify(fetchMock.mock.calls[0]?.[1]?.headers ?? {})).not.toContain('Content-Type');
+  });
+
+  it('attaches backend non-zero codes to thrown message errors', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ code: 3, msg: 'Monitor disappeared', data: null });
+
+    await expect(apiMessageGet('/monitors/manage')).rejects.toMatchObject({ code: 3 });
+  });
+
+  it('shares the non-zero fallback across write helpers', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ code: 500, data: null });
+    mockApiMessagePayload({ code: 500, data: null });
+
+    await expect(apiMessagePut('/bad', {})).rejects.toThrow('API message returned non-zero code');
+    await expect(apiMessageDelete('/bad')).rejects.toThrow('API message returned non-zero code');
+  });
+
+  it('uses runtime fallback copy for failed HTTP status responses', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockHttpStatus(503);
+
+    await expect(apiGet('/offline')).rejects.toThrow('API request failed: 503');
+  });
+
+  it('attaches HTTP status codes to thrown request errors', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockHttpStatus(404);
+
+    await expect(apiGet('/offline')).rejects.toMatchObject({ status: 404 });
+  });
+
+  it('forwards abort signals through message reads so topology timeouts can cancel fetch work', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ code: 0, data: { ok: true } });
+
+    const controller = new AbortController();
+    await expect(apiMessageGet('/topology', { signal: controller.signal })).resolves.toEqual({ ok: true });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/topology',
+      expect.objectContaining({
+        signal: controller.signal
+      })
+    );
+  });
+
+  it('returns raw download responses so export handlers can inspect headers and JSON error bodies', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    fetchMock.mockResolvedValueOnce(
+      new Response(JSON.stringify({ code: 1, msg: 'export denied' }), {
+        status: 200,
+        headers: {
+          'Content-Disposition': 'attachment; filename=monitors.json',
+          'Content-Type': 'application/json'
+        }
+      })
+    );
+
+    const response = await apiDownload('/monitors/export?ids=42&type=JSON');
+
+    expect(response.headers.get('Content-Disposition')).toBe('attachment; filename=monitors.json');
+    expect(response.headers.get('Content-Type')).toContain('application/json');
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/export?ids=42&type=JSON',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+});
diff --git a/web-next/lib/api-client.ts b/web-next/lib/api-client.ts
index 121125a329..bd72fb54e8 100644
--- a/web-next/lib/api-client.ts
+++ b/web-next/lib/api-client.ts
@@ -1,11 +1,20 @@
-export function getAuthorizationToken(): string | null {
-  if (typeof window === 'undefined') return null;
-  return window.localStorage.getItem('Authorization');
+import { SUPPLEMENTAL_MESSAGES } from './i18n-runtime-messages';
+import { clearClientSessionMarker } from './session-client';
+
+const API_MESSAGE_NON_ZERO_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['common.api.message-nonzero'] ?? 'common.api.message-nonzero';
+const API_REQUEST_FAILED_STATUS_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['common.api.request-failed-status'] ?? 'common.api.request-failed-status';
+
+function formatApiRequestFailedStatus(status: number): string {
+  return API_REQUEST_FAILED_STATUS_FALLBACK.replace('{{status}}', String(status));
 }
 
-export function getRefreshToken(): string | null {
-  if (typeof window === 'undefined') return null;
-  return window.localStorage.getItem('refresh-token');
+export type ApiClientError = Error & {
+  code?: number;
+  status?: number;
+};
+
+export function getAuthorizationToken(): string | null {
+  return null;
 }
 
 export function getCurrentLocale(): string | null {
@@ -13,59 +22,37 @@ export function getCurrentLocale(): string | null {
   return window.localStorage.getItem('hb.lang') || window.localStorage.getItem('layout.lang');
 }
 
-function setTokens(token?: string | null, refreshToken?: string | null) {
-  if (typeof window === 'undefined') return;
-  if (token) {
-    window.localStorage.setItem('Authorization', token);
-  }
-  if (refreshToken) {
-    window.localStorage.setItem('refresh-token', refreshToken);
-  }
-}
-
-function clearTokens() {
-  if (typeof window === 'undefined') return;
-  window.localStorage.removeItem('Authorization');
-  window.localStorage.removeItem('refresh-token');
-}
-
 function buildHeaders(extra?: Record<string, string>) {
-  const token = getAuthorizationToken();
   const locale = getCurrentLocale();
   return {
-    ...(token ? { Authorization: `Bearer ${token}` } : {}),
     ...(locale ? { 'Accept-Language': locale } : {}),
     ...(extra || {})
   };
 }
 
 async function refreshAuthorizationToken(): Promise<boolean> {
-  const refreshToken = getRefreshToken();
-  if (!refreshToken) return false;
   try {
     const response = await fetch('/api/account/auth/refresh', {
       method: 'POST',
-      headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify({ token: refreshToken }),
+      credentials: 'same-origin',
       cache: 'no-store'
     });
     if (!response.ok) {
-      clearTokens();
+      clearClientSessionMarker();
       return false;
     }
     const payload = (await response.json()) as {
       code: number;
       msg?: string;
-      data?: { token?: string; refreshToken?: string };
+      data?: { authenticated?: boolean };
     };
-    if (payload.code !== 0 || !payload.data?.token) {
-      clearTokens();
+    if (payload.code !== 0 || !payload.data?.authenticated) {
+      clearClientSessionMarker();
       return false;
     }
-    setTokens(payload.data.token, payload.data.refreshToken ?? refreshToken);
     return true;
   } catch {
-    clearTokens();
+    clearClientSessionMarker();
     return false;
   }
 }
@@ -74,6 +61,7 @@ async function apiFetch(path: string, init: RequestInit = {}, retryOn401 = true)
   const response = await fetch(path.startsWith('/api') ? path : `/api${path}`, {
     ...init,
     headers: buildHeaders(init.headers as Record<string, string> | undefined),
+    credentials: 'same-origin',
     cache: 'no-store'
   });
 
@@ -85,21 +73,46 @@ async function apiFetch(path: string, init: RequestInit = {}, retryOn401 = true)
   }
 
   if (!response.ok) {
-    throw new Error(`API request failed: ${response.status}`);
+    const error = new Error(formatApiRequestFailedStatus(response.status)) as ApiClientError;
+    error.status = response.status;
+    throw error;
+  }
+  return response;
+}
+
+export async function apiDownload(path: string, init: RequestInit = {}, retryOn401 = true): Promise<Response> {
+  const response = await fetch(path.startsWith('/api') ? path : `/api${path}`, {
+    ...init,
+    headers: buildHeaders(init.headers as Record<string, string> | undefined),
+    credentials: 'same-origin',
+    cache: 'no-store'
+  });
+
+  if (response.status === 401 && retryOn401) {
+    const refreshed = await refreshAuthorizationToken();
+    if (refreshed) {
+      return apiDownload(path, init, false);
+    }
   }
+
   return response;
 }
 
-export async function apiGet<T>(path: string): Promise<T> {
-  const response = await apiFetch(path);
+export async function apiGet<T>(path: string, init: RequestInit = {}): Promise<T> {
+  const response = await apiFetch(path, init);
   return response.json() as Promise<T>;
 }
 
 export async function apiPost<T>(path: string, body: unknown): Promise<T> {
+  const isFormDataBody = typeof FormData !== 'undefined' && body instanceof FormData;
   const response = await apiFetch(path, {
     method: 'POST',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify(body),
+    ...(isFormDataBody
+      ? { body }
+      : {
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify(body),
+        }),
   });
   return response.json() as Promise<T>;
 }
@@ -120,10 +133,12 @@ export async function apiDelete<T>(path: string): Promise<T> {
   return response.json() as Promise<T>;
 }
 
-export async function apiMessageGet<T>(path: string): Promise<T> {
-  const payload = await apiGet<{ code: number; msg?: string; data: T }>(path);
+export async function apiMessageGet<T>(path: string, init: RequestInit = {}): Promise<T> {
+  const payload = await apiGet<{ code: number; msg?: string; data: T }>(path, init);
   if (payload.code !== 0) {
-    throw new Error(payload.msg || 'API message returned non-zero code');
+    const error = new Error(payload.msg || API_MESSAGE_NON_ZERO_FALLBACK) as ApiClientError;
+    error.code = payload.code;
+    throw error;
   }
   return payload.data;
 }
@@ -131,7 +146,9 @@ export async function apiMessageGet<T>(path: string): Promise<T> {
 export async function apiMessagePost<T>(path: string, body: unknown): Promise<T> {
   const payload = await apiPost<{ code: number; msg?: string; data: T }>(path, body);
   if (payload.code !== 0) {
-    throw new Error(payload.msg || 'API message returned non-zero code');
+    const error = new Error(payload.msg || API_MESSAGE_NON_ZERO_FALLBACK) as ApiClientError;
+    error.code = payload.code;
+    throw error;
   }
   return payload.data;
 }
@@ -139,7 +156,9 @@ export async function apiMessagePost<T>(path: string, body: unknown): Promise<T>
 export async function apiMessagePut<T>(path: string, body: unknown): Promise<T> {
   const payload = await apiPut<{ code: number; msg?: string; data: T }>(path, body);
   if (payload.code !== 0) {
-    throw new Error(payload.msg || 'API message returned non-zero code');
+    const error = new Error(payload.msg || API_MESSAGE_NON_ZERO_FALLBACK) as ApiClientError;
+    error.code = payload.code;
+    throw error;
   }
   return payload.data;
 }
@@ -147,7 +166,9 @@ export async function apiMessagePut<T>(path: string, body: unknown): Promise<T>
 export async function apiMessageDelete<T>(path: string): Promise<T> {
   const payload = await apiDelete<{ code: number; msg?: string; data: T }>(path);
   if (payload.code !== 0) {
-    throw new Error(payload.msg || 'API message returned non-zero code');
+    const error = new Error(payload.msg || API_MESSAGE_NON_ZERO_FALLBACK) as ApiClientError;
+    error.code = payload.code;
+    throw error;
   }
   return payload.data;
 }
diff --git a/web-next/lib/i18n-runtime-messages.ts b/web-next/lib/i18n-runtime-messages.ts
index 328b432448..0fa9350fda 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -11,6 +11,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
   'en-US': {
     'common.active': 'Active',
     'common.annotation.bind': 'Annotations',
+    'common.api.message-nonzero': 'API message returned non-zero code',
+    'common.api.request-failed-status': 'API request failed: {{status}}',
     'common.attention': 'attention',
     'common.app': 'App',
     'common.application': 'application',
@@ -1061,6 +1063,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
   'zh-CN': {
     'common.active': '活跃',
     'common.annotation.bind': '注解',
+    'common.api.message-nonzero': 'API 消息返回非零状态码',
+    'common.api.request-failed-status': 'API 请求失败:{{status}}',
     'common.attention': '关注',
     'common.app': '应用',
     'common.application': 'application',
diff --git a/web-next/lib/query-keys.test.ts b/web-next/lib/query-keys.test.ts
new file mode 100644
index 0000000000..ecfe6278fd
--- /dev/null
+++ b/web-next/lib/query-keys.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from 'vitest';
+
+import { queryKeys } from './query-keys';
+
+describe('query keys', () => {
+  it('uses stable domain-scoped monitor keys', () => {
+    expect(queryKeys.monitors.list({ pageIndex: 0, pageSize: 8, status: '' })).toEqual([
+      'monitors',
+      'list',
+      { pageIndex: 0, pageSize: 8 }
+    ]);
+    expect(queryKeys.monitors.detail(42)).toEqual(['monitors', 'detail', '42']);
+  });
+
+  it('preserves topology context without empty values', () => {
+    expect(queryKeys.topology.graph({ entityId: '501', sourceKind: 'otlp-trace-call', environment: '' })).toEqual([
+      'topology',
+      'graph',
+      { entityId: '501', sourceKind: 'otlp-trace-call' }
+    ]);
+  });
+
+  it('uses a stable overview console key and strips empty refresh dimensions', () => {
+    expect(queryKeys.overview.console({ summary: '/summary', alerts: '/alerts', refreshNonce: 0, empty: '' })).toEqual([
+      'overview',
+      'console',
+      { summary: '/summary', alerts: '/alerts', refreshNonce: 0 }
+    ]);
+  });
+
+  it('uses stable monitor history keys from the monitor YAML metric catalog', () => {
+    expect(queryKeys.monitors.history(501, 'summary.responseTime', { history: '1h', interval: false, empty: '' })).toEqual([
+      'monitors',
+      'history',
+      '501',
+      'summary.responseTime',
+      { history: '1h', interval: false }
+    ]);
+  });
+});
diff --git a/web-next/lib/query-keys.ts b/web-next/lib/query-keys.ts
new file mode 100644
index 0000000000..cad58a6f0b
--- /dev/null
+++ b/web-next/lib/query-keys.ts
@@ -0,0 +1,50 @@
+import type { TopologyRouteContext } from './topology-surface/view-model';
+
+type QueryRecord = Record<string, string | number | boolean | null | undefined>;
+
+function compactRecord(record: QueryRecord = {}) {
+  return Object.fromEntries(Object.entries(record).filter(([, value]) => value != null && value !== ''));
+}
+
+export const queryKeys = {
+  session: {
+    current: ['session', 'current'] as const
+  },
+  monitors: {
+    all: ['monitors'] as const,
+    list: (query: QueryRecord = {}) => ['monitors', 'list', compactRecord(query)] as const,
+    detail: (monitorId: string | number) => ['monitors', 'detail', String(monitorId)] as const,
+    history: (monitorId: string | number, metric: string, query: QueryRecord = {}) =>
+      ['monitors', 'history', String(monitorId), metric, compactRecord(query)] as const
+  },
+  entities: {
+    all: ['entities'] as const,
+    detail: (entityId: string | number) => ['entities', 'detail', String(entityId)] as const
+  },
+  metrics: {
+    all: ['metrics'] as const,
+    monitorRealtime: (monitorId: string | number, metricName: string) =>
+      ['metrics', 'monitor-realtime', String(monitorId), metricName] as const
+  },
+  logs: {
+    all: ['logs'] as const,
+    list: (query: QueryRecord = {}) => ['logs', 'list', compactRecord(query)] as const
+  },
+  traces: {
+    all: ['traces'] as const,
+    detail: (traceId: string) => ['traces', 'detail', traceId] as const,
+    spans: (traceId: string) => ['traces', 'spans', traceId] as const
+  },
+  topology: {
+    all: ['topology'] as const,
+    graph: (context: TopologyRouteContext = {}) => ['topology', 'graph', compactRecord(context as QueryRecord)] as const
+  },
+  overview: {
+    all: ['overview'] as const,
+    console: (query: QueryRecord = {}) => ['overview', 'console', compactRecord(query)] as const
+  },
+  alerts: {
+    all: ['alerts'] as const,
+    list: (query: QueryRecord = {}) => ['alerts', 'list', compactRecord(query)] as const
+  }
+} as const;
diff --git a/web-next/lib/session-client.test.ts b/web-next/lib/session-client.test.ts
new file mode 100644
index 0000000000..12a50a989f
--- /dev/null
+++ b/web-next/lib/session-client.test.ts
@@ -0,0 +1,86 @@
+// @vitest-environment jsdom
+
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+  HB_UI_SESSION_USER_KEY,
+  clearClientSession,
+  clearClientSessionUserSnapshot,
+  readClientSessionUserSnapshot,
+  writeClientSessionUserSnapshot
+} from './session-client';
+
+describe('session client helpers', () => {
+  afterEach(() => {
+    window.sessionStorage.clear();
+    vi.unstubAllGlobals();
+  });
+
+  it('stores and reads the Angular-style post-login user snapshot without token material', () => {
+    writeClientSessionUserSnapshot({
+      name: 'ops-admin',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator',
+      role: 'ADMIN'
+    });
+
+    expect(readClientSessionUserSnapshot()).toEqual({
+      name: 'ops-admin',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator',
+      role: 'ADMIN'
+    });
+    expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).not.toContain('token');
+    expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).not.toContain('refresh');
+  });
+
+  it('clears stale user snapshots during logout cleanup', async () => {
+    writeClientSessionUserSnapshot({
+      name: 'ops-admin',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator'
+    });
+    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('{}', { status: 200 })));
+
+    await clearClientSession();
+
+    expect(readClientSessionUserSnapshot()).toBeNull();
+    expect(global.fetch).toHaveBeenCalledWith('/api/account/session', {
+      method: 'DELETE',
+      credentials: 'same-origin',
+      cache: 'no-store'
+    });
+  });
+
+  it('clears local session markers even when logout cleanup request fails', async () => {
+    writeClientSessionUserSnapshot({
+      name: 'ops-admin',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator'
+    });
+    Object.defineProperty(document, 'cookie', {
+      value: 'hb_ui_session=1',
+      writable: true,
+      configurable: true
+    });
+    vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('offline')));
+
+    await expect(clearClientSession()).rejects.toThrow('offline');
+
+    expect(readClientSessionUserSnapshot()).toBeNull();
+    expect(document.cookie).toContain('hb_ui_session=');
+    expect(global.fetch).toHaveBeenCalledWith('/api/account/session', {
+      method: 'DELETE',
+      credentials: 'same-origin',
+      cache: 'no-store'
+    });
+  });
+
+  it('drops malformed user snapshots instead of crashing the shell', () => {
+    window.sessionStorage.setItem(HB_UI_SESSION_USER_KEY, '{bad-json');
+
+    expect(readClientSessionUserSnapshot()).toBeNull();
+
+    clearClientSessionUserSnapshot();
+    expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).toBeNull();
+  });
+});
diff --git a/web-next/lib/session-client.ts b/web-next/lib/session-client.ts
new file mode 100644
index 0000000000..5f5a12b81f
--- /dev/null
+++ b/web-next/lib/session-client.ts
@@ -0,0 +1,96 @@
+export type ClientSessionState = {
+  authenticated: boolean;
+};
+
+export type ClientSessionUserSnapshot = {
+  name: string;
+  avatar: string;
+  email: string;
+  role?: string;
+};
+
+export const HB_UI_SESSION_USER_KEY = 'HB_UI_SESSION_USER';
+
+function readSessionStorage() {
+  if (typeof window === 'undefined') return null;
+  try {
+    return window.sessionStorage;
+  } catch {
+    return null;
+  }
+}
+
+export function readClientSessionUserSnapshot(): ClientSessionUserSnapshot | null {
+  const storage = readSessionStorage();
+  if (!storage) return null;
+  try {
+    const value = storage.getItem(HB_UI_SESSION_USER_KEY);
+    if (!value) return null;
+    const parsed = JSON.parse(value) as Partial<ClientSessionUserSnapshot>;
+    if (!parsed.name || typeof parsed.name !== 'string') return null;
+    return {
+      name: parsed.name,
+      avatar: typeof parsed.avatar === 'string' ? parsed.avatar : './assets/img/avatar.svg',
+      email: typeof parsed.email === 'string' ? parsed.email : 'administrator',
+      ...(typeof parsed.role === 'string' && parsed.role ? { role: parsed.role } : {})
+    };
+  } catch {
+    return null;
+  }
+}
+
+export function writeClientSessionUserSnapshot(user: ClientSessionUserSnapshot) {
+  const storage = readSessionStorage();
+  if (!storage) return;
+  try {
+    storage.setItem(HB_UI_SESSION_USER_KEY, JSON.stringify(user));
+  } catch {
+    // The user snapshot is an Angular parity convenience, not an auth boundary.
+  }
+}
+
+export function clearClientSessionUserSnapshot() {
+  const storage = readSessionStorage();
+  if (!storage) return;
+  try {
+    storage.removeItem(HB_UI_SESSION_USER_KEY);
+  } catch {
+    // Ignore storage failures during logout/session cleanup.
+  }
+}
+
+export function clearClientSessionMarker() {
+  if (typeof document === 'undefined') return;
+  document.cookie = 'hb_ui_session=; Max-Age=0; path=/; SameSite=Lax';
+}
+
+export async function readClientSessionState(): Promise<ClientSessionState> {
+  try {
+    const response = await fetch('/api/account/session', {
+      credentials: 'same-origin',
+      cache: 'no-store'
+    });
+    if (!response.ok) {
+      clearClientSessionMarker();
+      return { authenticated: false };
+    }
+    const payload = (await response.json()) as ClientSessionState;
+    return { authenticated: Boolean(payload.authenticated) };
+  } catch {
+    clearClientSessionMarker();
+    return { authenticated: false };
+  }
+}
+
+export async function clearClientSession() {
+  try {
+    await fetch('/api/account/session', {
+      method: 'DELETE',
+      credentials: 'same-origin',
+      cache: 'no-store'
+    });
+  } finally {
+    clearClientSessionMarker();
+    clearClientSessionUserSnapshot();
+  }
+}
diff --git a/web-next/lib/workbench-load-cache.test.ts b/web-next/lib/workbench-load-cache.test.ts
index 8e551333e1..0a04667fce 100644
--- a/web-next/lib/workbench-load-cache.test.ts
+++ b/web-next/lib/workbench-load-cache.test.ts
@@ -35,6 +35,37 @@ describe('workbench load cache', () => {
     expect(load).toHaveBeenCalledTimes(2);
   });
 
+  it('can keep a settled result for a short ttl when shared chrome needs the same state', async () => {
+    let now = 1_000;
+    const load = vi.fn()
+      .mockResolvedValueOnce('first')
+      .mockResolvedValueOnce('second');
+
+    await expect(
+      consumeWorkbenchLoad('app-frame:header-state:zh-CN', load, {
+        settledTtlMs: 500,
+        now: () => now,
+      })
+    ).resolves.toBe('first');
+    await expect(
+      consumeWorkbenchLoad('app-frame:header-state:zh-CN', load, {
+        settledTtlMs: 500,
+        now: () => now,
+      })
+    ).resolves.toBe('first');
+
+    now = 1_501;
+
+    await expect(
+      consumeWorkbenchLoad('app-frame:header-state:zh-CN', load, {
+        settledTtlMs: 500,
+        now: () => now,
+      })
+    ).resolves.toBe('second');
+
+    expect(load).toHaveBeenCalledTimes(2);
+  });
+
   it('clears the cache after a rejection so the next attempt can retry', async () => {
     const load = vi.fn()
       .mockRejectedValueOnce(new Error('boom'))
diff --git a/web-next/lib/workbench-load-cache.ts b/web-next/lib/workbench-load-cache.ts
index 4480e94b14..4b6ae1ef6f 100644
--- a/web-next/lib/workbench-load-cache.ts
+++ b/web-next/lib/workbench-load-cache.ts
@@ -1,18 +1,62 @@
-const workbenchLoadCache = new Map<string, Promise<unknown>>();
+type WorkbenchLoadCacheOptions = {
+  settledTtlMs?: number;
+  now?: () => number;
+};
 
-export function consumeWorkbenchLoad<T>(cacheKey: string, load: () => Promise<T>) {
-  const existing = workbenchLoadCache.get(cacheKey) as Promise<T> | undefined;
+type WorkbenchLoadCacheEntry<T> = {
+  promise: Promise<T>;
+  settled: boolean;
+  expiresAt: number;
+};
+
+const workbenchLoadCache = new Map<string, WorkbenchLoadCacheEntry<unknown>>();
+
+function resolveNow(options: WorkbenchLoadCacheOptions) {
+  return options.now ? options.now() : Date.now();
+}
+
+export function consumeWorkbenchLoad<T>(
+  cacheKey: string,
+  load: () => Promise<T>,
+  options: WorkbenchLoadCacheOptions = {}
+) {
+  const now = resolveNow(options);
+  const existing = workbenchLoadCache.get(cacheKey) as WorkbenchLoadCacheEntry<T> | undefined;
+  if (existing && (!existing.settled || existing.expiresAt > now)) {
+    return existing.promise;
+  }
   if (existing) {
-    return existing;
+    workbenchLoadCache.delete(cacheKey);
   }
 
-  const promise = load().finally(() => {
-    if (workbenchLoadCache.get(cacheKey) === promise) {
-      workbenchLoadCache.delete(cacheKey);
+  const settledTtlMs = Math.max(0, options.settledTtlMs ?? 0);
+  let entry: WorkbenchLoadCacheEntry<T> | null = null;
+  const promise = load().then(
+    value => {
+      if (!entry) return value;
+      entry.settled = true;
+      if (settledTtlMs > 0) {
+        entry.expiresAt = resolveNow(options) + settledTtlMs;
+      } else if (workbenchLoadCache.get(cacheKey) === entry) {
+        workbenchLoadCache.delete(cacheKey);
+      }
+      return value;
+    },
+    error => {
+      if (entry && workbenchLoadCache.get(cacheKey) === entry) {
+        workbenchLoadCache.delete(cacheKey);
+      }
+      throw error;
     }
-  });
+  );
+
+  entry = {
+    promise,
+    settled: false,
+    expiresAt: Number.POSITIVE_INFINITY,
+  };
 
-  workbenchLoadCache.set(cacheKey, promise);
+  workbenchLoadCache.set(cacheKey, entry);
   return promise;
 }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 13/50: feat(web-next): restore passport parity

Posted by zh...@apache.org.
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 c64801e73d6171a82c41339c1a5b77d8307c2d40
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 01:14:54 2026 +0800

    feat(web-next): restore passport parity
---
 web-next/app/api/account/auth/form/route.ts        |  35 ++
 web-next/app/api/account/auth/refresh/route.ts     |  44 +++
 web-next/app/api/account/session/route.ts          |  24 ++
 web-next/app/login/page.tsx                        |   4 +-
 .../app/passport/lock/page-submit-flow.test.tsx    | 120 ++++++
 web-next/app/passport/lock/page.test.tsx           |  39 +-
 web-next/app/passport/lock/page.tsx                |  73 +---
 web-next/app/passport/lock/passport-lock-page.tsx  |  63 +++
 web-next/app/passport/login/page.test.tsx          |  25 +-
 web-next/app/passport/login/page.tsx               |  15 +-
 .../pages/login-form-submit-flow.test.tsx          | 421 +++++++++++++++++++++
 .../components/pages/login-form.submit.test.tsx    |  15 +-
 web-next/components/pages/login-form.test.tsx      |  86 +++--
 web-next/components/pages/login-form.tsx           | 203 ++++++----
 .../pages/passport-shell-session-clear.test.tsx    | 125 ++++++
 web-next/components/pages/passport-shell.test.tsx  |  47 ++-
 web-next/components/pages/passport-shell.tsx       |  27 +-
 web-next/lib/passport-lock/view-model.test.ts      |   6 +-
 web-next/lib/passport-lock/view-model.ts           |   4 +-
 web-next/lib/passport-login/controller.test.ts     | 108 +++++-
 web-next/lib/passport-login/controller.ts          |  92 ++++-
 web-next/lib/passport-login/view-model.test.ts     |  29 +-
 web-next/lib/passport-login/view-model.ts          |  20 +-
 web-next/lib/session-bff.test.ts                   | 178 +++++++++
 web-next/lib/session-bff.ts                        | 206 ++++++++++
 25 files changed, 1771 insertions(+), 238 deletions(-)

diff --git a/web-next/app/api/account/auth/form/route.ts b/web-next/app/api/account/auth/form/route.ts
new file mode 100644
index 0000000000..2192593721
--- /dev/null
+++ b/web-next/app/api/account/auth/form/route.ts
@@ -0,0 +1,35 @@
+import { NextRequest, NextResponse } from 'next/server';
+import {
+  applySessionCookies,
+  buildBackendApiUrl,
+  clearSessionCookies,
+  readJsonPayload,
+  sanitizeSessionPayload
+} from '@/lib/session-bff';
+
+export const dynamic = 'force-dynamic';
+
+export async function POST(request: NextRequest) {
+  const upstream = await fetch(buildBackendApiUrl('/account/auth/form'), {
+    method: 'POST',
+    headers: {
+      'Content-Type': request.headers.get('Content-Type') || 'application/json',
+      ...(request.headers.get('Accept-Language') ? { 'Accept-Language': request.headers.get('Accept-Language') as string } : {})
+    },
+    body: await request.text(),
+    cache: 'no-store'
+  });
+  const payload = await readJsonPayload(upstream);
+  const response = NextResponse.json(sanitizeSessionPayload(payload), { status: upstream.status });
+
+  if (upstream.ok && payload.code === 0 && payload.data) {
+    applySessionCookies(response, {
+      token: typeof payload.data.token === 'string' ? payload.data.token : undefined,
+      refreshToken: typeof payload.data.refreshToken === 'string' ? payload.data.refreshToken : undefined
+    });
+  } else {
+    clearSessionCookies(response);
+  }
+
+  return response;
+}
diff --git a/web-next/app/api/account/auth/refresh/route.ts b/web-next/app/api/account/auth/refresh/route.ts
new file mode 100644
index 0000000000..f55252ab21
--- /dev/null
+++ b/web-next/app/api/account/auth/refresh/route.ts
@@ -0,0 +1,44 @@
+import { NextRequest, NextResponse } from 'next/server';
+import {
+  HB_UI_REFRESH_COOKIE,
+  applySessionCookies,
+  buildBackendApiUrl,
+  clearSessionCookies,
+  readJsonPayload,
+  readSessionCookieValue,
+  sanitizeSessionPayload
+} from '@/lib/session-bff';
+
+export const dynamic = 'force-dynamic';
+
+export async function POST(request: NextRequest) {
+  const refreshToken = readSessionCookieValue(request, HB_UI_REFRESH_COOKIE);
+  if (!refreshToken) {
+    const response = NextResponse.json({ code: 401, msg: 'Missing refresh session', data: null }, { status: 401 });
+    clearSessionCookies(response);
+    return response;
+  }
+
+  const upstream = await fetch(buildBackendApiUrl('/account/auth/refresh'), {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      ...(request.headers.get('Accept-Language') ? { 'Accept-Language': request.headers.get('Accept-Language') as string } : {})
+    },
+    body: JSON.stringify({ token: refreshToken }),
+    cache: 'no-store'
+  });
+  const payload = await readJsonPayload(upstream);
+  const response = NextResponse.json(sanitizeSessionPayload(payload), { status: upstream.status });
+
+  if (upstream.ok && payload.code === 0 && payload.data) {
+    applySessionCookies(response, {
+      token: typeof payload.data.token === 'string' ? payload.data.token : undefined,
+      refreshToken: typeof payload.data.refreshToken === 'string' ? payload.data.refreshToken : refreshToken
+    });
+  } else {
+    clearSessionCookies(response);
+  }
+
+  return response;
+}
diff --git a/web-next/app/api/account/session/route.ts b/web-next/app/api/account/session/route.ts
new file mode 100644
index 0000000000..dabd4ce88e
--- /dev/null
+++ b/web-next/app/api/account/session/route.ts
@@ -0,0 +1,24 @@
+import { NextRequest, NextResponse } from 'next/server';
+import {
+  HB_UI_ACCESS_COOKIE,
+  HB_UI_REFRESH_COOKIE,
+  clearSessionCookies,
+  readSessionCookieValue
+} from '@/lib/session-bff';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET(request: NextRequest) {
+  return NextResponse.json({
+    authenticated: Boolean(
+      readSessionCookieValue(request, HB_UI_ACCESS_COOKIE) ||
+      readSessionCookieValue(request, HB_UI_REFRESH_COOKIE)
+    )
+  });
+}
+
+export async function DELETE() {
+  const response = NextResponse.json({ authenticated: false });
+  clearSessionCookies(response);
+  return response;
+}
diff --git a/web-next/app/login/page.tsx b/web-next/app/login/page.tsx
index cba692aacc..6d0a0f3833 100644
--- a/web-next/app/login/page.tsx
+++ b/web-next/app/login/page.tsx
@@ -1,5 +1,5 @@
 import { redirect } from 'next/navigation';
-import { buildCompatRedirectTarget, type SearchParamsRecord } from '../../lib/compat/search-params';
+import { buildLoginCompatRouteUrl, type SearchParamsRecord } from '../../lib/passport-login/controller';
 
 export default async function LoginAliasPage({
   searchParams
@@ -7,5 +7,5 @@ export default async function LoginAliasPage({
   searchParams?: Promise<SearchParamsRecord>;
 }) {
   const resolvedSearchParams = await searchParams;
-  redirect(buildCompatRedirectTarget('/passport/login', resolvedSearchParams));
+  redirect(buildLoginCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/passport/lock/page-submit-flow.test.tsx b/web-next/app/passport/lock/page-submit-flow.test.tsx
new file mode 100644
index 0000000000..2fb20d1495
--- /dev/null
+++ b/web-next/app/passport/lock/page-submit-flow.test.tsx
@@ -0,0 +1,120 @@
+// @vitest-environment jsdom
+
+import React from 'react';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import { HB_UI_SESSION_USER_KEY } from '../../../lib/session-client';
+
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+const mockState = vi.hoisted(() => ({
+  routerReplace: vi.fn()
+}));
+
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    replace: mockState.routerReplace
+  })
+}));
+
+vi.mock('next/image', () => ({
+  default: ({ alt, src, priority: _priority, ...props }: any) => React.createElement('img', { alt, src, ...props })
+}));
+
+vi.mock('@/components/providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t: createTranslatorMock({ locale: 'zh-CN' }),
+    locale: 'zh-CN',
+    locales: [
+      { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 'gb' },
+      { code: 'zh-CN', labelKey: 'settings.system-config.locale.zh_CN', abbr: 'cn' }
+    ],
+    setLocale: vi.fn()
+  })
+}));
+
+describe('passport lock submit flow', () => {
+  let container: HTMLDivElement;
+  let root: Root;
+
+  async function mountPage() {
+    const { default: PassportLockPage } = await import('./passport-lock-page');
+    await act(async () => {
+      root.render(<PassportLockPage />);
+      await Promise.resolve();
+    });
+  }
+
+  function setPassword(value: string) {
+    const input = container.querySelector('[data-hz-passport-lock-password-input="shared"]') as HTMLInputElement | null;
+    if (!input) throw new Error('Missing lock password input');
+    const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
+    descriptor?.set?.call(input, value);
+    input.dispatchEvent(new Event('input', { bubbles: true }));
+    input.dispatchEvent(new Event('change', { bubbles: true }));
+  }
+
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+    mockState.routerReplace.mockReset();
+  });
+
+  afterEach(() => {
+    act(() => {
+      root.unmount();
+    });
+    container.remove();
+    document.cookie = 'hb_ui_session=; Max-Age=0; path=/';
+  });
+
+  it('blocks empty unlock drafts and navigates to the workbench after a password is entered', async () => {
+    window.sessionStorage.setItem(
+      HB_UI_SESSION_USER_KEY,
+      JSON.stringify({ name: 'ops-admin', avatar: './assets/img/avatar.svg', email: 'administrator', role: 'ADMIN' })
+    );
+    document.cookie = 'hb_ui_session=1; path=/';
+    await mountPage();
+
+    expect(container.querySelector('[data-hz-passport-lock-submit-lifecycle="angular-mark-dirty-required-then-dashboard"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-lock-submit-lifecycle-contract="angular-mark-dirty-required-then-dashboard"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-lock-required-mode-contract="angular-required-no-trim"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-lock-required-mode="angular-required-no-trim"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-session-clear-contract="angular-lock-preserve-session"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-session-clear-enabled="false"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-session-clear-lifecycle="angular-lock-preserve-session"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-session-clear-scope="client-marker-user-snapshot-preserved"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-lock-avatar-contract="angular-settings-user-avatar"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-lock-avatar-source="settings-user-avatar"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-lock-avatar-img="settings-user-avatar"]')).not.toBeNull();
+    expect((container.querySelector('[data-hz-passport-lock-avatar-img="settings-user-avatar"]') as HTMLImageElement).getAttribute('src')).toBe('./assets/img/avatar.svg');
+    expect((container.querySelector('[data-hz-passport-lock-avatar-img="settings-user-avatar"]') as HTMLImageElement).getAttribute('alt')).toBe('ops-admin');
+    expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).toContain('"name":"ops-admin"');
+    expect(document.cookie).toContain('hb_ui_session=1');
+    expect(container.querySelector('[data-hz-passport-lock-submit-state="disabled"]')).not.toBeNull();
+
+    await act(async () => {
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).not.toHaveBeenCalled();
+
+    await act(async () => {
+      setPassword('   ');
+      await Promise.resolve();
+    });
+
+    expect(container.querySelector('[data-hz-passport-lock-submit-state="ready"]')).not.toBeNull();
+
+    await act(async () => {
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.routerReplace).toHaveBeenCalledWith('/overview');
+  }, 30000);
+});
diff --git a/web-next/app/passport/lock/page.test.tsx b/web-next/app/passport/lock/page.test.tsx
index a58a5bf10f..d5fe6d9548 100644
--- a/web-next/app/passport/lock/page.test.tsx
+++ b/web-next/app/passport/lock/page.test.tsx
@@ -10,12 +10,12 @@ vi.mock('next/navigation', () => ({
 }));
 
 vi.mock('next/image', () => ({
-  default: ({ alt, src, priority: _priority, ...props }: any) => <img alt={alt} src={src} {...props} />
+  default: ({ alt, src, priority: _priority, ...props }: any) => React.createElement('img', { alt, src, ...props })
 }));
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
+    t: createTranslatorMock({ locale: 'zh-CN' }),
     locale: 'zh-CN',
     locales: [
       { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: '🇬🇧' },
@@ -26,11 +26,15 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 }));
 
 vi.mock('../../../components/pages/passport-shell', () => ({
-  PassportShell: ({ children, panelClassName }: any) => (
+  PassportShell: ({ children, panelClassName, sessionLifecycle }: any) => (
     <div
       data-login-shell="passport"
       data-passport-shell="true"
       data-passport-shell-panel-class={panelClassName}
+      data-passport-session-clear-contract={sessionLifecycle === 'preserve-on-lock' ? 'angular-lock-preserve-session' : 'angular-token-service-clear-on-passport-entry'}
+      data-passport-session-clear-enabled={sessionLifecycle === 'preserve-on-lock' ? 'false' : 'true'}
+      data-hz-passport-session-clear-lifecycle={sessionLifecycle === 'preserve-on-lock' ? 'angular-lock-preserve-session' : 'angular-token-service-clear-on-passport-entry'}
+      data-hz-passport-session-clear-scope={sessionLifecycle === 'preserve-on-lock' ? 'client-marker-user-snapshot-preserved' : 'client-marker-user-snapshot'}
       style={{ backgroundImage: "url('/assets/bg.png')" }}
     >
       <div>Apache HertzBeat™</div>
@@ -63,14 +67,37 @@ describe('passport lock page', () => {
     const html = renderToStaticMarkup(<PassportLockPage />);
 
     expect(html).toContain('data-passport-shell="true"');
+    expect(html).toContain('data-passport-session-clear-contract="angular-lock-preserve-session"');
+    expect(html).toContain('data-passport-session-clear-enabled="false"');
+    expect(html).toContain('data-hz-passport-session-clear-lifecycle="angular-lock-preserve-session"');
+    expect(html).toContain('data-hz-passport-session-clear-scope="client-marker-user-snapshot-preserved"');
     expect(html).toContain('data-passport-panel="true"');
     expect(html).toContain('data-passport-lock="true"');
     expect(html).toContain('data-passport-lock-panel="angular-wide"');
+    expect(html).toContain('data-passport-lock-panel-owner="hertzbeat-ui-passport-lock"');
+    expect(html).toContain('data-passport-lock-avatar-contract="angular-settings-user-avatar"');
+    expect(html).toContain('data-passport-lock-session-contract="angular-lock-preserve-session"');
+    expect(html).toContain('data-passport-lock-submit-lifecycle-contract="angular-mark-dirty-required-then-dashboard"');
+    expect(html).toContain('data-passport-lock-submit-lifecycle-owner="hertzbeat-ui-passport-lock"');
+    expect(html).toContain('data-passport-lock-redirect-contract="angular-dashboard-next-overview"');
+    expect(html).toContain('data-passport-lock-required-mode-contract="angular-required-no-trim"');
+    expect(html).toContain('data-passport-lock-required-mode-owner="hertzbeat-ui-passport-lock"');
+    expect(html).toContain('data-hz-ui="passport-lock-surface"');
+    expect(html).toContain('data-hz-passport-lock-owner="hertzbeat-ui-passport-lock"');
+    expect(html).toContain('data-hz-passport-lock-density="angular-lock-card"');
+    expect(html).toContain('data-hz-passport-lock-submit-lifecycle="angular-mark-dirty-required-then-dashboard"');
+    expect(html).toContain('data-hz-passport-lock-required-fields="password"');
+    expect(html).toContain('data-hz-passport-lock-required-mode="angular-required-no-trim"');
+    expect(html).toContain('data-hz-passport-lock-redirect="angular-dashboard-next-overview"');
+    expect(html).toContain('data-hz-passport-lock-submit-disabled="angular-invalid-disabled"');
+    expect(html).toContain('data-hz-passport-lock-avatar="angular-floating"');
+    expect(html).toContain('data-hz-passport-lock-avatar-source="fallback-user-icon"');
+    expect(html).toContain('data-hz-passport-lock-submit-state="disabled"');
     expect(html).toContain('max-w-[712px]');
     expect(html).toContain('rounded-none');
-    expect(html).toContain('Unlock');
-    expect(html).toContain('Enter Any To Unlock');
+    expect(html).toContain('解除锁定');
+    expect(html).toContain('输入任意解锁');
     expect(html).toContain('/assets/bg.png');
     expect(html).toContain('Apache HertzBeat™');
-  }, 15000);
+  }, 30000);
 });
diff --git a/web-next/app/passport/lock/page.tsx b/web-next/app/passport/lock/page.tsx
index baccd086db..25d490a279 100644
--- a/web-next/app/passport/lock/page.tsx
+++ b/web-next/app/passport/lock/page.tsx
@@ -1,71 +1,6 @@
-'use client';
+import React from 'react';
+import PassportLockPage from './passport-lock-page';
 
-import React, { FormEvent, useState } from 'react';
-import { useRouter } from 'next/navigation';
-import { LockKeyhole, UserRound } from 'lucide-react';
-import { ObservabilityStatusState } from '@/components/observability';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { validateUnlockPassword } from '../../../lib/passport-lock/view-model';
-import { PassportPanel, PassportShell } from '../../../components/pages/passport-shell';
-
-export default function PassportLockPage() {
-  const { t } = useI18n();
-  const router = useRouter();
-  const [password, setPassword] = useState('');
-  const [error, setError] = useState<string | null>(null);
-
-  function handleSubmit(event: FormEvent) {
-    event.preventDefault();
-    const validationError = validateUnlockPassword(password, t);
-    if (validationError) {
-      setError(validationError);
-      return;
-    }
-    setError(null);
-    router.replace('/overview');
-  }
-
-  return (
-    <PassportShell panelClassName="max-w-[712px] lg:max-w-[712px]">
-      <PassportPanel
-        className="mx-auto min-h-[334px] max-w-[712px] rounded-none border-[var(--ops-border-color)] bg-[#101217] px-6 py-14 shadow-none lg:px-[120px]"
-      >
-        <div data-passport-lock-panel="angular-wide" className="mx-auto flex min-h-[220px] max-w-[320px] flex-col justify-center">
-          <div className="text-center">
-            <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-white/75 text-[var(--ops-surface-base)] shadow-none">
-              <UserRound size={28} />
-            </div>
-            <div className="mt-6 text-[16px] font-semibold text-[var(--ops-text-primary)]">{t('app.lock')}</div>
-          </div>
-          <form onSubmit={handleSubmit} className="mt-6 space-y-5" data-passport-lock="true">
-            <div className="relative">
-              <LockKeyhole
-                size={16}
-                className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--ops-text-tertiary)]"
-              />
-              <Input
-                placeholder={t('app.lock.placeholder')}
-                type="password"
-                value={password}
-                onChange={e => setPassword(e.target.value)}
-                className="h-9 border-[var(--ops-border-color)] bg-[var(--ops-surface-base)] pl-10"
-              />
-            </div>
-            <div className="flex justify-end">
-              <Button className="min-w-[76px]" size="sm" variant="primary" type="submit">
-                {t('app.lock')}
-              </Button>
-            </div>
-          </form>
-          {error ? (
-            <div className="mt-4">
-              <ObservabilityStatusState title={t('common.failed')} copy={error} tone="danger" />
-            </div>
-          ) : null}
-        </div>
-      </PassportPanel>
-    </PassportShell>
-  );
+export default function PassportLockRoutePage() {
+  return <PassportLockPage />;
 }
diff --git a/web-next/app/passport/lock/passport-lock-page.tsx b/web-next/app/passport/lock/passport-lock-page.tsx
new file mode 100644
index 0000000000..a6efdaf5d0
--- /dev/null
+++ b/web-next/app/passport/lock/passport-lock-page.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import React, { FormEvent, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { HzPassportLockSurface } from '@hertzbeat/ui';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { validateUnlockPassword } from '../../../lib/passport-lock/view-model';
+import { readClientSessionUserSnapshot } from '../../../lib/session-client';
+import { PassportPanel, PassportShell } from '../../../components/pages/passport-shell';
+
+export default function PassportLockPage() {
+  const { t } = useI18n();
+  const router = useRouter();
+  const [sessionUser] = useState(() => readClientSessionUserSnapshot());
+  const [password, setPassword] = useState('');
+  const [error, setError] = useState<string | null>(null);
+
+  function handleSubmit(event: FormEvent) {
+    event.preventDefault();
+    const validationError = validateUnlockPassword(password, t);
+    if (validationError) {
+      setError(validationError);
+      return;
+    }
+    setError(null);
+    router.replace('/overview');
+  }
+
+  return (
+    <PassportShell panelClassName="max-w-[712px] lg:max-w-[712px]" sessionLifecycle="preserve-on-lock">
+      <PassportPanel
+        className="mx-auto min-h-[334px] max-w-[712px] rounded-none border-[var(--ops-border-color)] bg-[#101217] px-6 py-14 shadow-none lg:px-[120px]"
+      >
+        <HzPassportLockSurface
+          title={t('app.lock')}
+          passwordLabel={t('app.lock.placeholder')}
+          passwordPlaceholder={t('app.lock.placeholder')}
+          buttonLabel={t('app.lock')}
+          password={password}
+          avatarSrc={sessionUser?.avatar}
+          avatarAlt={sessionUser?.name || t('app.lock')}
+          error={error}
+          disabled={password.length === 0}
+          onPasswordChange={value => {
+            setPassword(value);
+            if (error) setError(null);
+          }}
+          onSubmit={handleSubmit}
+          data-passport-lock="true"
+          data-passport-lock-panel="angular-wide"
+          data-passport-lock-panel-owner="hertzbeat-ui-passport-lock"
+          data-passport-lock-avatar-contract="angular-settings-user-avatar"
+          data-passport-lock-session-contract="angular-lock-preserve-session"
+          data-passport-lock-submit-lifecycle-contract="angular-mark-dirty-required-then-dashboard"
+          data-passport-lock-submit-lifecycle-owner="hertzbeat-ui-passport-lock"
+          data-passport-lock-redirect-contract="angular-dashboard-next-overview"
+          data-passport-lock-required-mode-contract="angular-required-no-trim"
+          data-passport-lock-required-mode-owner="hertzbeat-ui-passport-lock"
+        />
+      </PassportPanel>
+    </PassportShell>
+  );
+}
diff --git a/web-next/app/passport/login/page.test.tsx b/web-next/app/passport/login/page.test.tsx
index 667793e281..369bc386a0 100644
--- a/web-next/app/passport/login/page.test.tsx
+++ b/web-next/app/passport/login/page.test.tsx
@@ -1,17 +1,38 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import type { PassportLoginRouteState } from '@/lib/passport-login/controller';
 
 vi.mock('@/components/pages/login-form', () => ({
-  LoginForm: () => <div data-login-form="true">passport login form</div>
+  LoginForm: ({ initialRouteState }: { initialRouteState?: PassportLoginRouteState }) => (
+    <div data-login-form="true" data-redirect-target={initialRouteState?.redirectTarget}>
+      passport login form
+    </div>
+  )
 }));
 
 describe('passport login page', () => {
   it('renders the shared login form surface', async () => {
     const { default: PassportLoginPage } = await import('./page');
-    const html = renderToStaticMarkup(<PassportLoginPage />);
+    const html = renderToStaticMarkup(await PassportLoginPage());
 
     expect(html).toContain('data-login-form="true"');
     expect(html).toContain('passport login form');
+    expect(html).toContain('data-redirect-target="/"');
+  });
+
+  it('passes the normalized redirect route state to the login form', async () => {
+    const { default: PassportLoginPage } = await import('./page');
+    const { LoginForm } = await import('@/components/pages/login-form');
+    const initialRouteState = { redirectTarget: '/monitors?app=website' };
+    const mockedFormHtml = renderToStaticMarkup(<LoginForm initialRouteState={initialRouteState} />);
+    const html = renderToStaticMarkup(
+      await PassportLoginPage({
+        searchParams: Promise.resolve({ redirect: '/monitors?app=website' })
+      })
+    );
+
+    expect(mockedFormHtml).toContain('data-redirect-target="/monitors?app=website"');
+    expect(html).toContain('data-redirect-target="/monitors?app=website"');
   });
 });
diff --git a/web-next/app/passport/login/page.tsx b/web-next/app/passport/login/page.tsx
index dcd4ae0942..3cc7e68f41 100644
--- a/web-next/app/passport/login/page.tsx
+++ b/web-next/app/passport/login/page.tsx
@@ -1,10 +1,21 @@
 import React, { Suspense } from 'react';
 import { LoginForm } from '@/components/pages/login-form';
+import {
+  readPassportLoginRouteState,
+  type PassportLoginSearchParams
+} from '@/lib/passport-login/controller';
+
+export default async function PassportLoginPage({
+  searchParams
+}: {
+  searchParams?: Promise<PassportLoginSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readPassportLoginRouteState(resolvedSearchParams);
 
-export default function PassportLoginPage() {
   return (
     <Suspense fallback={null}>
-      <LoginForm />
+      <LoginForm initialRouteState={routeState} />
     </Suspense>
   );
 }
diff --git a/web-next/components/pages/login-form-submit-flow.test.tsx b/web-next/components/pages/login-form-submit-flow.test.tsx
new file mode 100644
index 0000000000..4dcc42ac01
--- /dev/null
+++ b/web-next/components/pages/login-form-submit-flow.test.tsx
@@ -0,0 +1,421 @@
+// @vitest-environment jsdom
+
+import React from 'react';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+import type { PassportLoginRouteState } from '../../lib/passport-login/controller';
+
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+const mockState = vi.hoisted(() => ({
+  resetWorkbenchLoadCache: vi.fn(),
+  routerReplace: vi.fn()
+}));
+
+const setLocale = vi.fn(async () => {});
+const t = createTranslatorMock();
+
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    replace: mockState.routerReplace
+  })
+}));
+
+vi.mock('next/image', () => ({
+  default: ({ alt, src, priority: _priority, ...props }: any) => React.createElement('img', { alt, src, ...props })
+}));
+
+vi.mock('../providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t,
+    locale: 'en-US',
+    locales: [
+      { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 'gb' },
+      { code: 'zh-CN', labelKey: 'settings.system-config.locale.zh_CN', abbr: 'cn' },
+      { code: 'ja-JP', labelKey: 'settings.system-config.locale.ja-JP', abbr: 'jp' }
+    ],
+    setLocale
+  })
+}));
+
+vi.mock('../ui/button', () => ({
+  Button: ({ children, ...props }: any) => <button {...props}>{children}</button>
+}));
+
+vi.mock('../ui/input', () => ({
+  Input: (props: any) => <input {...props} />
+}));
+
+vi.mock('../../lib/workbench-load-cache', () => ({
+  resetWorkbenchLoadCache: mockState.resetWorkbenchLoadCache
+}));
+
+describe('login form submit copy', () => {
+  let container: HTMLDivElement;
+  let root: Root;
+
+  async function mountForm(initialRouteState: PassportLoginRouteState = { redirectTarget: '/' }) {
+    const { LoginForm } = await import('./login-form');
+    await act(async () => {
+      root.render(<LoginForm initialRouteState={initialRouteState} />);
+      await Promise.resolve();
+    });
+  }
+
+  function setInputValue(index: number, value: string) {
+    const input = container.querySelectorAll('input')[index] as HTMLInputElement | undefined;
+    if (!input) throw new Error(`Missing input index ${index}`);
+    const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
+    descriptor?.set?.call(input, value);
+    input.dispatchEvent(new Event('input', { bubbles: true }));
+    input.dispatchEvent(new Event('change', { bubbles: true }));
+  }
+
+  async function flushAsyncWork(turns = 8) {
+    for (let index = 0; index < turns; index += 1) {
+      await Promise.resolve();
+    }
+  }
+
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+    mockState.resetWorkbenchLoadCache.mockReset();
+    mockState.routerReplace.mockReset();
+    setLocale.mockClear();
+    window.localStorage.clear();
+    window.sessionStorage.clear();
+    vi.stubGlobal('fetch', vi.fn());
+  });
+
+  afterEach(() => {
+    act(() => {
+      root.unmount();
+    });
+    container.remove();
+    vi.unstubAllGlobals();
+  });
+
+  it('uses the BFF cookie session without writing access or refresh tokens to localStorage', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { authenticated: true, tokenBoundary: 'bff-cookie', role: 'ADMIN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { locale: 'zh_CN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: [] })
+      });
+
+    await mountForm({ redirectTarget: '/monitors?app=website' });
+
+    expect(container.querySelector('[data-hz-ui="passport-login-action-frame"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-token-boundary="bff-cookie-no-localstorage"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-session-bootstrap="angular-startup-load-after-success"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-session-user-name="angular-raw-identifier"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-startup-failure="angular-exception-500"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-login-startup-failure-contract="angular-exception-500"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-login-session-user-name-contract="angular-raw-identifier"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-required-mode="angular-required-no-trim"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-login-required-mode-contract="angular-required-no-trim"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-redirect="angular-referrer-non-passport-fallback"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-redirect-fallback="angular-root-fallback"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-login-redirect-fallback-contract="angular-root-fallback"]')).not.toBeNull();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/account/auth/form',
+      expect.objectContaining({
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ type: 0, identifier: 'ops-admin', credential: 'custom-secret' })
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/config/system',
+      expect.objectContaining({
+        cache: 'no-store',
+        credentials: 'same-origin',
+        headers: expect.not.objectContaining({
+          Authorization: expect.any(String)
+        })
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      3,
+      '/api/apps/hierarchy?lang=zh-CN',
+      expect.objectContaining({
+        cache: 'no-store',
+        credentials: 'same-origin',
+        headers: expect.not.objectContaining({
+          Authorization: expect.any(String)
+        })
+      })
+    );
+    expect(window.localStorage.getItem('Authorization')).toBeNull();
+    expect(window.localStorage.getItem('refresh-token')).toBeNull();
+    expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).toContain('"name":"ops-admin"');
+    expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).toContain('"role":"ADMIN"');
+    expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).not.toContain('token');
+    expect(window.sessionStorage.getItem('HB_ABOUT_AUTO_SHOW_AFTER_LOGIN')).toBe('true');
+    expect(mockState.resetWorkbenchLoadCache).toHaveBeenCalledTimes(1);
+    expect(mockState.routerReplace).toHaveBeenCalledWith('/monitors?app=website');
+  }, 60000);
+
+  it('routes to the Angular startup failure page when post-login hierarchy bootstrap fails', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { authenticated: true, tokenBoundary: 'bff-cookie', role: 'ADMIN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { locale: 'zh_CN' } })
+      })
+      .mockRejectedValueOnce(new Error('hierarchy failed'));
+
+    await mountForm({ redirectTarget: '/monitors?app=website' });
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      3,
+      '/api/apps/hierarchy?lang=zh-CN',
+      expect.objectContaining({
+        cache: 'no-store',
+        credentials: 'same-origin'
+      })
+    );
+    expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).toContain('"name":"ops-admin"');
+    expect(window.sessionStorage.getItem('HB_ABOUT_AUTO_SHOW_AFTER_LOGIN')).toBeNull();
+    expect(mockState.routerReplace).toHaveBeenCalledWith('/exception/500');
+    expect(mockState.routerReplace).not.toHaveBeenCalledWith('/monitors?app=website');
+  }, 60000);
+
+  it('keeps Angular default-password warning as the first submit instead of posting immediately', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'admin');
+      setInputValue(1, 'hertzbeat');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect(fetchMock).not.toHaveBeenCalled();
+    expect(container.querySelector('[data-hz-ui="passport-login-notice"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-notice-link="account-modify"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-default-password="angular-first-submit-warning"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-default-password-lifecycle="angular-sticky-until-submit"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-login-default-password-lifecycle-contract="angular-sticky-until-submit"]')).not.toBeNull();
+  }, 60000);
+
+  it('keeps the Angular default-password notice sticky after the credential changes', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock.mockResolvedValueOnce({
+      ok: false,
+      status: 401,
+      json: async () => ({ code: 1, msg: 'bad credential' })
+    });
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'admin');
+      setInputValue(1, 'hertzbeat');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect(fetchMock).not.toHaveBeenCalled();
+    expect(container.querySelector('[data-hz-ui="passport-login-notice"]')).not.toBeNull();
+
+    await act(async () => {
+      setInputValue(1, 'custom-secret');
+      await flushAsyncWork();
+    });
+
+    expect(container.querySelector('[data-hz-ui="passport-login-notice"]')).not.toBeNull();
+
+    await act(async () => {
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenCalledTimes(1);
+    expect(container.querySelector('[data-hz-ui="passport-login-notice"]')).not.toBeNull();
+    expect(container.textContent).toContain('bad credential');
+  }, 60000);
+
+  it('keeps Angular credential required-field validation on the client before posting', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+
+    await mountForm();
+
+    await act(async () => {
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect(fetchMock).not.toHaveBeenCalled();
+    expect(container.textContent).toContain('Please enter your username');
+    expect(container.querySelector('[data-hz-ui="passport-login-validation-notice"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-login-validation-density="angular-error-alert"]')).not.toBeNull();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect(fetchMock).not.toHaveBeenCalled();
+    expect(container.textContent).toContain('Please enter password');
+  }, 60000);
+
+  it('allows Angular required whitespace credentials through without trimming before posting', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock.mockResolvedValueOnce({
+      ok: false,
+      status: 401,
+      json: async () => ({ code: 1, msg: 'bad credential' })
+    });
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, '   ');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenCalledTimes(1);
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/account/auth/form',
+      expect.objectContaining({
+        body: JSON.stringify({ type: 0, identifier: 'ops-admin', credential: '   ' })
+      })
+    );
+    expect(container.textContent).toContain('bad credential');
+  }, 60000);
+
+  it('keeps Angular post-login user snapshot name as the raw submitted identifier', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { authenticated: true, tokenBoundary: 'bff-cookie', role: 'ADMIN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: { locale: 'zh_CN' } })
+      })
+      .mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => ({ code: 0, data: [] })
+      });
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, ' ops-admin ');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/account/auth/form',
+      expect.objectContaining({
+        body: JSON.stringify({ type: 0, identifier: ' ops-admin ', credential: 'custom-secret' })
+      })
+    );
+    expect(window.sessionStorage.getItem('HB_UI_SESSION_USER')).toContain('"name":" ops-admin "');
+    expect(mockState.routerReplace).toHaveBeenCalledWith('/');
+  }, 60000);
+
+  it('renders localized loading and fallback login errors', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    let resolveLoginResponse: ((value: unknown) => void) | undefined;
+    const loginResponse = new Promise(resolve => {
+      resolveLoginResponse = resolve;
+    });
+    fetchMock.mockReturnValueOnce(loginResponse);
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork();
+    });
+
+    expect((container.querySelector('button[type="submit"]') as HTMLButtonElement | null)?.textContent).toContain('Logging in');
+
+    await act(async () => {
+      resolveLoginResponse?.({
+        ok: false,
+        status: 503,
+        json: async () => ({ code: 1 })
+      });
+      await loginResponse;
+      await flushAsyncWork(12);
+    });
+
+    expect(container.textContent).toContain('Login failed: 503');
+    expect(mockState.routerReplace).not.toHaveBeenCalled();
+  }, 60000);
+
+  it('renders the localized generic fallback when login throws a non-Error value', async () => {
+    const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
+    fetchMock.mockRejectedValueOnce('network-cancelled');
+
+    await mountForm();
+
+    await act(async () => {
+      setInputValue(0, 'ops-admin');
+      setInputValue(1, 'custom-secret');
+      (container.querySelector('form') as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
+      await flushAsyncWork(12);
+    });
+
+    expect(container.textContent).toContain('Login failed. Check username or password.');
+    expect(mockState.routerReplace).not.toHaveBeenCalled();
+  }, 60000);
+});
diff --git a/web-next/components/pages/login-form.submit.test.tsx b/web-next/components/pages/login-form.submit.test.tsx
index f347798c83..28f21a60cd 100644
--- a/web-next/components/pages/login-form.submit.test.tsx
+++ b/web-next/components/pages/login-form.submit.test.tsx
@@ -90,13 +90,13 @@ describe('login form submit flow', () => {
     vi.unstubAllGlobals();
   });
 
-  it('persists tokens, warms bootstrap config, and restores the guarded return path after login', async () => {
+  it('uses the BFF cookie session, warms bootstrap config, and restores the guarded return path after login', async () => {
     const fetchMock = global.fetch as unknown as ReturnType<typeof vi.fn>;
     fetchMock
       .mockResolvedValueOnce({
         ok: true,
         status: 200,
-        json: async () => ({ code: 0, data: { token: 'access-token', refreshToken: 'refresh-token' } })
+        json: async () => ({ code: 0, data: { authenticated: true, tokenBoundary: 'bff-cookie' } })
       })
       .mockResolvedValueOnce({
         ok: true,
@@ -130,13 +130,14 @@ describe('login form submit flow', () => {
       '/api/config/system',
       expect.objectContaining({
         cache: 'no-store',
-        headers: expect.objectContaining({
-          Authorization: 'Bearer access-token'
+        credentials: 'same-origin',
+        headers: expect.not.objectContaining({
+          Authorization: expect.any(String)
         })
       })
     );
-    expect(window.localStorage.getItem('Authorization')).toBe('access-token');
-    expect(window.localStorage.getItem('refresh-token')).toBe('refresh-token');
+    expect(window.localStorage.getItem('Authorization')).toBeNull();
+    expect(window.localStorage.getItem('refresh-token')).toBeNull();
     expect(mockState.routerReplace).toHaveBeenCalledWith('/monitors?app=website');
   }, 15000);
 
@@ -146,7 +147,7 @@ describe('login form submit flow', () => {
       .mockResolvedValueOnce({
         ok: true,
         status: 200,
-        json: async () => ({ code: 0, data: { token: 'access-token', refreshToken: 'refresh-token' } })
+        json: async () => ({ code: 0, data: { authenticated: true, tokenBoundary: 'bff-cookie' } })
       })
       .mockResolvedValueOnce({
         ok: true,
diff --git a/web-next/components/pages/login-form.test.tsx b/web-next/components/pages/login-form.test.tsx
index 0b0a9bc157..fb4100d697 100644
--- a/web-next/components/pages/login-form.test.tsx
+++ b/web-next/components/pages/login-form.test.tsx
@@ -6,36 +6,35 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 
-const ZH_CN_LABEL = 'Simplified Chinese(zh_CN)';
+const ZH_CN_LABEL = '简体中文(zh_CN)';
 const JA_JP_LABEL = 'Japanese(ja_JP)';
 const LOGIN_NOTICE_COPY =
   '\u767b\u5f55\u6210\u529f\u540e\u4f1a\u81ea\u52a8\u6062\u590d\u5f53\u524d\u5de5\u4f5c\u53f0\u4f1a\u8bdd\uff0c\u5e76\u5728\u9700\u8981\u65f6\u5c1d\u8bd5\u5237\u65b0\u4ee4\u724c\u3002';
-const HERO_TITLE = 'Open-source enterprise observability for private operations';
-const HERO_LEAD = 'Collectors, monitoring templates, entities, metrics, logs, and traces';
-const HERO_FOCUS = 'Handle alerts and close issues inside HertzBeat';
+const HERO_TITLE = '开源私有化企业运维可观测平台';
+const HERO_LEAD = '采集器、监控模板、实体、指标、日志和链路';
+const HERO_FOCUS = '处理告警并关闭问题';
 const HERO_BODY =
-  'Collect metrics from applications, databases, operating systems, middleware, and network devices without sending data outside your deployment.';
-const LOGIN_HEADING = 'Sign In HertzBeat';
-const USERNAME_PROMPT = 'Please enter your username';
-const PASSWORD_PROMPT = 'Please enter password';
-const LOGIN_BUTTON = 'Login';
+  '通过采集器接入应用、数据库、操作系统、中间件和网络设备指标,数据留在私有化部署内。';
+const LOGIN_HEADING = '登入 HertzBeat';
+const USERNAME_PROMPT = '请输入用户名';
+const PASSWORD_PROMPT = '请输入密码';
+const PASSWORD_TOGGLE_LABEL = '显示密码';
+const REMEMBER_ME_LABEL = '记住我';
+const LOGIN_BUTTON = '登录';
 
 vi.mock('next/navigation', () => ({
   useRouter: () => ({
     replace: vi.fn()
-  }),
-  useSearchParams: () => ({
-    get: () => null
   })
 }));
 
 vi.mock('next/image', () => ({
-  default: ({ alt, src, priority: _priority, ...props }: any) => <img alt={alt} src={src} {...props} />
+  default: ({ alt, src, priority: _priority, ...props }: any) => React.createElement('img', { alt, src, ...props })
 }));
 
 vi.mock('../providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
+    t: createTranslatorMock({ locale: 'zh-CN' }),
     locale: 'zh-CN',
     locales: [
       { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: '🇬🇧' },
@@ -55,10 +54,9 @@ vi.mock('../ui/input', () => ({
 }));
 
 vi.mock('../../lib/passport-login/controller', () => ({
-  assertLoginSuccess: vi.fn(),
+  assertSessionLoginSuccess: vi.fn(),
   buildLoginRequestBody: vi.fn(),
   bootstrapPostLoginSession: vi.fn(),
-  persistLoginTokens: vi.fn(),
   resolvePostLoginRedirectTarget: vi.fn(() => '/overview'),
   LOGIN_REDIRECT_QUERY_KEY: 'redirect'
 }));
@@ -86,7 +84,7 @@ describe('LoginForm', () => {
     expect(html).toContain(HERO_LEAD);
     expect(html).toContain(HERO_FOCUS);
     expect(html).toContain(HERO_BODY);
-    expect(html).not.toContain('Open-source private-deployable enterprise operations observability platform');
+    expect(html).not.toContain('Open-source enterprise observability for private operations');
     expect(html).not.toContain('Unified metrics platform, agentless and supports web, db, os, mid, network etc.');
     expect(html).not.toContain('Unified logs platform');
     expect(html).not.toContain('seamlessly integrates');
@@ -95,11 +93,35 @@ describe('LoginForm', () => {
     expect(html).toContain(USERNAME_PROMPT);
     expect(html).toContain(PASSWORD_PROMPT);
     expect(html).toContain(LOGIN_BUTTON);
-    expect(html).toContain('placeholder="Please enter your username"');
-    expect(html).toContain('placeholder="Please enter password"');
+    expect(html).toContain(`placeholder="${USERNAME_PROMPT}"`);
+    expect(html).toContain(`placeholder="${PASSWORD_PROMPT}"`);
+    expect(html).toContain(`aria-label="${PASSWORD_TOGGLE_LABEL}"`);
     expect(html).toContain('data-passport-login-password-eye="true"');
     expect(html).toContain('data-passport-login-panel="angular-gray-card"');
     expect(html).toContain('data-passport-login-panel-align="angular-top"');
+    expect(html).toContain('data-passport-login-submit-lifecycle-contract="angular-required-default-warning-session-bootstrap-redirect"');
+    expect(html).toContain('data-passport-login-submit-lifecycle-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-passport-login-required-mode-contract="angular-required-no-trim"');
+    expect(html).toContain('data-passport-login-required-mode-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-passport-login-session-user-name-contract="angular-raw-identifier"');
+    expect(html).toContain('data-passport-login-session-user-name-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-hz-ui="passport-login-action-frame"');
+    expect(html).toContain('data-hz-passport-login-action-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-hz-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"');
+    expect(html).toContain('data-hz-passport-login-required-fields="identifier-credential"');
+    expect(html).toContain('data-hz-passport-login-required-mode="angular-required-no-trim"');
+    expect(html).toContain('data-hz-passport-login-default-password="angular-first-submit-warning"');
+    expect(html).toContain('data-hz-passport-login-default-password-lifecycle="angular-sticky-until-submit"');
+    expect(html).toContain('data-passport-login-default-password-lifecycle-contract="angular-sticky-until-submit"');
+    expect(html).toContain('data-hz-passport-login-token-boundary="bff-cookie-no-localstorage"');
+    expect(html).toContain('data-hz-passport-login-session-bootstrap="angular-startup-load-after-success"');
+    expect(html).toContain('data-hz-passport-login-session-user-name="angular-raw-identifier"');
+    expect(html).toContain('data-hz-passport-login-startup-failure="angular-exception-500"');
+    expect(html).toContain('data-passport-login-startup-failure-contract="angular-exception-500"');
+    expect(html).toContain('data-hz-passport-login-redirect="angular-referrer-non-passport-fallback"');
+    expect(html).toContain('data-hz-passport-login-redirect-fallback="angular-root-fallback"');
+    expect(html).toContain('data-passport-login-redirect-fallback-contract="angular-root-fallback"');
+    expect(html).toContain('data-hz-passport-login-remember-default="true"');
     expect(html).toContain('data-passport-login-accent="true"');
     expect(html).toContain('data-passport-login-remember="true"');
     expect(html).toContain('data-passport-login-remember-checkbox="cold-checkbox"');
@@ -113,21 +135,23 @@ describe('LoginForm', () => {
     expect(html).toContain('data-passport-hero-offset="angular-left-reference"');
     expect(html).toContain('data-passport-brand-lockup="angular-lowered"');
     expect(html).toContain('data-passport-intro-bullet-tone="angular-cyan"');
-    expect(html).toContain('Remember me');
+    expect(html).toContain(REMEMBER_ME_LABEL);
     expect(html).toContain('data-passport-locale-trigger="globe"');
     expect(html).toContain('data-passport-footer-tone="angular-muted"');
     expect(html).toContain('data-passport-footer-band="angular-raised"');
     expect(html).not.toContain(ZH_CN_LABEL);
     expect(html).toContain('Apache HertzBeat™');
     expect(html).toContain('Apache HertzBeat™ v1.8.0');
-    expect(html).toContain('Licensed under the Apache License, Version 2.0');
+    expect(html).toContain('Copyright ©');
+    expect(html).not.toContain('遵循 Apache License, Version 2.0 授权');
     expect(html).not.toContain(LOGIN_NOTICE_COPY);
     expect(html).not.toContain('value="admin"');
     expect(html).not.toContain('value="hertzbeat"');
-  }, 15000);
+  }, 60000);
 
   it('removes the remaining bright auth-field residue and adopts shared ops form tokens', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/login-form.tsx'), 'utf8');
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
 
     expect(source).not.toContain('text-[#424955]');
     expect(source).not.toContain('text-[#697180]');
@@ -142,17 +166,27 @@ describe('LoginForm', () => {
     expect(source).not.toContain('text-[#8f1d37]');
     expect(source).not.toContain('pt-8 lg:pt-7');
 
-    expect(source).toContain('text-[var(--ops-text-secondary)]');
+    expect(uiSource).toContain('text-[var(--ops-text-secondary)]');
     expect(source).toContain('text-[var(--ops-text-tertiary)]');
     expect(source).toContain('border-[var(--ops-border-color)]');
     expect(source).toContain('bg-[var(--ops-surface-panel)]');
-    expect(source).toContain('bg-[var(--ops-surface-elevated)]');
-    expect(source).toContain("from '../workbench/primitives'");
+    expect(uiSource).toContain('bg-[var(--ops-surface-elevated)]');
+    expect(source).toContain("import { HzPassportLoginActionFrame, HzPassportLoginNotice, HzPassportLoginValidationNotice } from '@hertzbeat/ui';");
+    expect(source).toContain('<HzPassportLoginActionFrame');
     expect(source).toContain("import { Checkbox } from '../ui/checkbox';");
-    expect(source).toContain('StatusState');
+    expect(source).toContain('<HzPassportLoginNotice copy={notice.copy} href={notice.href} />');
+    expect(source).toContain('<HzPassportLoginValidationNotice title={t(\'common.attention\')} copy={error} />');
+    expect(source).toContain('validateCredentialLoginDraft(identifier, credential, t)');
+    expect(source).toContain("import { resetWorkbenchLoadCache } from '../../lib/workbench-load-cache';");
+    expect(source).toContain('resetWorkbenchLoadCache();');
+    expect(source).toContain('await bootstrapPostLoginSession(apiGet);');
     expect(source).toContain('data-passport-login-panel-align="angular-top"');
+    expect(source).not.toContain('<AlertTriangle');
     expect(source).not.toContain('input type="checkbox"');
     expect(source).not.toContain('<input type="checkbox" className="sr-only" defaultChecked />');
+    expect(source).not.toContain('StatusState');
     expect(source).not.toContain('ObservabilityStatusState');
+    expect(source).toContain("aria-label={showCredential ? t('app.login.password-hide') : t('app.login.password-show')}");
+    expect(source).toContain("label={t('app.login.remember-me')}");
   });
 });
diff --git a/web-next/components/pages/login-form.tsx b/web-next/components/pages/login-form.tsx
index b8050f8d1c..60b1655582 100644
--- a/web-next/components/pages/login-form.tsx
+++ b/web-next/components/pages/login-form.tsx
@@ -2,23 +2,37 @@
 
 import React from 'react';
 import { FormEvent, useState } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { AlertTriangle, Eye, EyeOff, LockKeyhole, UserRound } from 'lucide-react';
+import { useRouter } from 'next/navigation';
+import { Eye, EyeOff, LockKeyhole, UserRound } from 'lucide-react';
+import { HzPassportLoginActionFrame, HzPassportLoginNotice, HzPassportLoginValidationNotice } from '@hertzbeat/ui';
 import { useI18n } from '../providers/i18n-provider';
 import { Button } from '../ui/button';
 import { Checkbox } from '../ui/checkbox';
 import { Input } from '../ui/input';
-import { StatusState } from '../workbench/primitives';
 import { apiGet } from '../../lib/api-client';
-import { assertLoginSuccess, bootstrapPostLoginSession, buildLoginRequestBody, LOGIN_REDIRECT_QUERY_KEY, persistLoginTokens, resolvePostLoginRedirectTarget } from '../../lib/passport-login/controller';
-import type { LoginMessage } from '../../lib/passport-login/controller';
-import { buildLoginNotice, shouldBlockDefaultPasswordSubmit, shouldWarnDefaultPassword } from '../../lib/passport-login/view-model';
+import {
+  assertSessionLoginSuccess,
+  bootstrapPostLoginSession,
+  buildLoginRequestBody,
+  buildPostLoginSessionUser,
+  resolvePostLoginStartupFailureTarget,
+  type LoginMessage,
+  type PassportLoginRouteState
+} from '../../lib/passport-login/controller';
+import { buildLoginNotice, shouldBlockDefaultPasswordSubmit, shouldWarnDefaultPassword, validateCredentialLoginDraft } from '../../lib/passport-login/view-model';
+import { writeClientSessionUserSnapshot } from '../../lib/session-client';
+import { markAboutAutoShowAfterLogin } from '../../lib/shell/about';
+import { resetWorkbenchLoadCache } from '../../lib/workbench-load-cache';
 import { PassportPanel, PassportShell } from './passport-shell';
 
-export function LoginForm() {
+const EMPTY_PASSPORT_LOGIN_ROUTE_STATE: PassportLoginRouteState = {
+  redirectTarget: '/'
+};
+
+export function LoginForm({ initialRouteState }: { initialRouteState?: PassportLoginRouteState } = {}) {
   const { t } = useI18n();
   const router = useRouter();
-  const searchParams = useSearchParams();
+  const passportLoginRouteState = initialRouteState ?? EMPTY_PASSPORT_LOGIN_ROUTE_STATE;
   const [identifier, setIdentifier] = useState('');
   const [credential, setCredential] = useState('');
   const [showCredential, setShowCredential] = useState(false);
@@ -32,13 +46,21 @@ export function LoginForm() {
 
   async function handleSubmit(event: FormEvent) {
     event.preventDefault();
+    const validation = validateCredentialLoginDraft(identifier, credential, t);
+    if (validation) {
+      setError(validation.message);
+      return;
+    }
+
     if (shouldBlockDefaultPasswordSubmit(needUpdatePassword, credential)) {
       setNeedUpdatePassword(true);
       setError(null);
       return;
     }
 
-    setNeedUpdatePassword(shouldWarnDefaultPassword(credential));
+    if (shouldWarnDefaultPassword(credential)) {
+      setNeedUpdatePassword(true);
+    }
     setLoading(true);
     setError(null);
     try {
@@ -48,14 +70,21 @@ export function LoginForm() {
         body: JSON.stringify(buildLoginRequestBody(identifier, credential))
       });
       const message = (await response.json()) as LoginMessage;
-      const tokens = assertLoginSuccess(
+      assertSessionLoginSuccess(
         response.status,
         message,
         t('passport.login.error.with-status')
       );
-      persistLoginTokens(window.localStorage, tokens);
-      await bootstrapPostLoginSession(apiGet);
-      router.replace(resolvePostLoginRedirectTarget(searchParams.get(LOGIN_REDIRECT_QUERY_KEY)));
+      resetWorkbenchLoadCache();
+      writeClientSessionUserSnapshot(buildPostLoginSessionUser(identifier, message));
+      try {
+        await bootstrapPostLoginSession(apiGet);
+      } catch {
+        router.replace(resolvePostLoginStartupFailureTarget());
+        return;
+      }
+      markAboutAutoShowAfterLogin();
+      router.replace(passportLoginRouteState.redirectTarget);
     } catch (err) {
       setError(err instanceof Error ? err.message : t('passport.login.error.generic'));
     } finally {
@@ -68,6 +97,11 @@ export function LoginForm() {
       <div
         data-passport-login-panel="angular-gray-card"
         data-passport-login-panel-align="angular-top"
+        data-passport-login-submit-lifecycle-contract="angular-required-default-warning-session-bootstrap-redirect"
+        data-passport-login-default-password-lifecycle-contract="angular-sticky-until-submit"
+        data-passport-login-startup-failure-contract="angular-exception-500"
+        data-passport-login-redirect-fallback-contract="angular-root-fallback"
+        data-passport-login-submit-lifecycle-owner="hertzbeat-ui-passport-login-action"
       >
         <PassportPanel
           className="rounded-none border border-[rgba(33,35,42,0.68)] border-b-[var(--ops-primary)] border-r-[var(--ops-primary)] bg-[#5f5f66] px-5 py-9 shadow-none"
@@ -83,81 +117,88 @@ export function LoginForm() {
             </div>
           )}
         >
-        <form onSubmit={handleSubmit} className="mt-4 space-y-4">
-          <label className="block space-y-2">
-            <span className={fieldLabelClassName}>
-              {t('app.login.message-need-identifier')}
-            </span>
-            <div className="relative">
-              <UserRound size={16} className={fieldIconClassName} />
-              <Input
-                value={identifier}
-                onChange={e => setIdentifier(e.target.value)}
-                placeholder={t('app.login.message-need-identifier')}
-                className={fieldInputClassName}
-              />
-            </div>
-          </label>
-          <label className="block space-y-2">
-            <span className={fieldLabelClassName}>
-              {t('app.login.message-need-credential')}
-            </span>
-            <div className="relative">
-              <LockKeyhole size={16} className={fieldIconClassName} />
-              <Input
-                value={credential}
-                onChange={e => {
-                  const nextValue = e.target.value;
-                  setCredential(nextValue);
-                  if (!shouldWarnDefaultPassword(nextValue) && needUpdatePassword) {
-                    setNeedUpdatePassword(false);
-                  }
-                }}
-                placeholder={t('app.login.message-need-credential')}
-                type={showCredential ? 'text' : 'password'}
-                className={`${fieldInputClassName} pr-10`}
+        <HzPassportLoginActionFrame
+          className="mt-4"
+          data-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"
+          data-passport-login-submit-lifecycle-owner="hertzbeat-ui-passport-login-action"
+          data-passport-login-required-mode-contract="angular-required-no-trim"
+          data-passport-login-required-mode-owner="hertzbeat-ui-passport-login-action"
+          data-passport-login-session-user-name-contract="angular-raw-identifier"
+          data-passport-login-session-user-name-owner="hertzbeat-ui-passport-login-action"
+        >
+          <form onSubmit={handleSubmit} className="space-y-4">
+            <label className="block space-y-2">
+              <span className={fieldLabelClassName}>
+                {t('app.login.message-need-identifier')}
+              </span>
+              <div className="relative">
+                <UserRound size={16} className={fieldIconClassName} />
+                <Input
+                  value={identifier}
+                  onChange={e => {
+                    setIdentifier(e.target.value);
+                    if (error) {
+                      setError(null);
+                    }
+                  }}
+                  placeholder={t('app.login.message-need-identifier')}
+                  className={fieldInputClassName}
+                />
+              </div>
+            </label>
+            <label className="block space-y-2">
+              <span className={fieldLabelClassName}>
+                {t('app.login.message-need-credential')}
+              </span>
+              <div className="relative">
+                <LockKeyhole size={16} className={fieldIconClassName} />
+                <Input
+                  value={credential}
+                  onChange={e => {
+                    const nextValue = e.target.value;
+                    setCredential(nextValue);
+                    if (error) {
+                      setError(null);
+                    }
+                  }}
+                  placeholder={t('app.login.message-need-credential')}
+                  type={showCredential ? 'text' : 'password'}
+                  className={`${fieldInputClassName} pr-10`}
+                />
+                <button
+                  aria-label={showCredential ? t('app.login.password-hide') : t('app.login.password-show')}
+                  className="absolute right-3 top-1/2 inline-flex -translate-y-1/2 items-center justify-center text-[var(--ops-text-tertiary)] transition-colors hover:text-white"
+                  data-passport-login-password-eye="true"
+                  type="button"
+                  onClick={() => setShowCredential(current => !current)}
+                >
+                  {showCredential ? <EyeOff size={16} aria-hidden="true" /> : <Eye size={16} aria-hidden="true" />}
+                </button>
+              </div>
+            </label>
+
+            <div data-passport-login-remember="true">
+              <Checkbox
+                data-passport-login-remember-checkbox="cold-checkbox"
+                defaultChecked
+                containerClassName="min-h-4 gap-2 text-[13px] font-medium text-[#17181c]"
+                label={t('app.login.remember-me')}
               />
-              <button
-                aria-label={showCredential ? 'Hide password' : 'Show password'}
-                className="absolute right-3 top-1/2 inline-flex -translate-y-1/2 items-center justify-center text-[var(--ops-text-tertiary)] transition-colors hover:text-white"
-                data-passport-login-password-eye="true"
-                type="button"
-                onClick={() => setShowCredential(current => !current)}
-              >
-                {showCredential ? <EyeOff size={16} aria-hidden="true" /> : <Eye size={16} aria-hidden="true" />}
-              </button>
             </div>
-          </label>
-
-          <div data-passport-login-remember="true">
-            <Checkbox
-              data-passport-login-remember-checkbox="cold-checkbox"
-              defaultChecked
-              containerClassName="min-h-4 gap-2 text-[13px] font-medium text-[#17181c]"
-              label="Remember me"
-            />
-          </div>
 
-          {notice.kind === 'warning' ? (
-            <a
-              className="flex items-start gap-3 rounded-[6px] border border-[rgba(216,111,91,0.28)] bg-[var(--ops-surface-elevated)] px-4 py-3 text-sm text-[var(--ops-text-secondary)] transition-colors hover:border-[var(--ops-primary)] hover:text-[var(--ops-text-primary)]"
-              href={notice.href}
-              target="_blank"
-              rel="noreferrer"
-            >
-              <AlertTriangle size={16} className="mt-0.5 shrink-0 text-[var(--ops-primary)]" />
-              <span>{notice.copy}</span>
-            </a>
-          ) : null}
+            {notice.kind === 'warning' ? (
+              <HzPassportLoginNotice copy={notice.copy} href={notice.href} />
+            ) : null}
 
-          <Button className="h-11 w-full" variant="primary" type="submit" disabled={loading} size="lg">
-            {loading ? t('passport.login.loading') : t('app.login.login')}
-          </Button>
-        </form>
+            <Button className="h-11 w-full" variant="primary" type="submit" disabled={loading} size="lg">
+              {loading ? t('passport.login.loading') : t('app.login.login')}
+            </Button>
+          </form>
+        </HzPassportLoginActionFrame>
 
         {error ? (
           <div className="mt-4">
-            <StatusState title={t('common.attention')} copy={error} tone="danger" />
+            <HzPassportLoginValidationNotice title={t('common.attention')} copy={error} />
           </div>
         ) : null}
         </PassportPanel>
diff --git a/web-next/components/pages/passport-shell-session-clear.test.tsx b/web-next/components/pages/passport-shell-session-clear.test.tsx
new file mode 100644
index 0000000000..78394261fd
--- /dev/null
+++ b/web-next/components/pages/passport-shell-session-clear.test.tsx
@@ -0,0 +1,125 @@
+// @vitest-environment jsdom
+
+import React from 'react';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { HB_UI_SESSION_USER_KEY } from '../../lib/session-client';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+vi.mock('next/image', () => ({
+  default: ({ alt, src, priority: _priority, ...props }: any) => React.createElement('img', { alt, src, ...props })
+}));
+
+vi.mock('../providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t: createTranslatorMock({ locale: 'zh-CN' }),
+    locale: 'zh-CN',
+    locales: [
+      { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: 'gb' },
+      { code: 'zh-CN', labelKey: 'settings.system-config.locale.zh_CN', abbr: 'cn' }
+    ],
+    setLocale: vi.fn()
+  })
+}));
+
+vi.mock('../shell/locale-option-list', () => ({
+  LocaleOptionList: () => <div data-locale-options="true">locale-options</div>
+}));
+
+vi.mock('../shell/platform-copyright-footer', () => ({
+  PlatformCopyrightFooter: ({
+    children,
+    headlineClassName: _headlineClassName,
+    innerClassName: _innerClassName,
+    lineClassName: _lineClassName,
+    linkClassName: _linkClassName,
+    version: _version,
+    ...props
+  }: any) => <footer {...props}>{children}</footer>
+}));
+
+describe('PassportShell session clear lifecycle', () => {
+  let container: HTMLDivElement;
+  let root: Root;
+
+  async function mountShell() {
+    const { PassportShell } = await import('./passport-shell');
+    await act(async () => {
+      root.render(
+        <PassportShell>
+          <div data-passport-child="true">child</div>
+        </PassportShell>
+      );
+      await Promise.resolve();
+    });
+  }
+
+  async function mountLockShell() {
+    const { PassportShell } = await import('./passport-shell');
+    await act(async () => {
+      root.render(
+        <PassportShell sessionLifecycle="preserve-on-lock">
+          <div data-passport-child="true">child</div>
+        </PassportShell>
+      );
+      await Promise.resolve();
+    });
+  }
+
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = createRoot(container);
+    window.sessionStorage.clear();
+    document.cookie = 'hb_ui_session=1; path=/';
+    vi.stubGlobal('fetch', vi.fn());
+  });
+
+  afterEach(() => {
+    act(() => {
+      root.unmount();
+    });
+    container.remove();
+    window.sessionStorage.clear();
+    vi.unstubAllGlobals();
+  });
+
+  it('clears Angular-style client session residue on passport entry without calling the logout API', async () => {
+    window.sessionStorage.setItem(
+      HB_UI_SESSION_USER_KEY,
+      JSON.stringify({ name: 'admin', avatar: './assets/img/avatar.svg', email: 'administrator', role: 'ADMIN' })
+    );
+
+    await mountShell();
+
+    expect(container.querySelector('[data-hz-ui="passport-session-clear-frame"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-session-clear-contract="angular-token-service-clear-on-passport-entry"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-session-clear-lifecycle="angular-token-service-clear-on-passport-entry"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-session-clear-scope="client-marker-user-snapshot"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-session-clear-boundary="no-api-logout-on-entry"]')).not.toBeNull();
+    expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).toBeNull();
+    expect(document.cookie).not.toContain('hb_ui_session=1');
+    expect(global.fetch).not.toHaveBeenCalled();
+  }, 15000);
+
+  it('preserves client session residue on the Angular lock entry', async () => {
+    window.sessionStorage.setItem(
+      HB_UI_SESSION_USER_KEY,
+      JSON.stringify({ name: 'admin', avatar: './assets/img/avatar.svg', email: 'administrator', role: 'ADMIN' })
+    );
+
+    await mountLockShell();
+
+    expect(container.querySelector('[data-passport-session-clear-contract="angular-lock-preserve-session"]')).not.toBeNull();
+    expect(container.querySelector('[data-passport-session-clear-enabled="false"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-session-clear-lifecycle="angular-lock-preserve-session"]')).not.toBeNull();
+    expect(container.querySelector('[data-hz-passport-session-clear-scope="client-marker-user-snapshot-preserved"]')).not.toBeNull();
+    expect(window.sessionStorage.getItem(HB_UI_SESSION_USER_KEY)).toContain('"name":"admin"');
+    expect(document.cookie).toContain('hb_ui_session=1');
+    expect(global.fetch).not.toHaveBeenCalled();
+  }, 15000);
+});
diff --git a/web-next/components/pages/passport-shell.test.tsx b/web-next/components/pages/passport-shell.test.tsx
index 42cb958e61..0ef944d5f0 100644
--- a/web-next/components/pages/passport-shell.test.tsx
+++ b/web-next/components/pages/passport-shell.test.tsx
@@ -7,12 +7,12 @@ import { PassportPanel, PassportShell } from './passport-shell';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 vi.mock('next/image', () => ({
-  default: ({ alt, src, priority: _priority, ...props }: any) => <img alt={alt} src={src} {...props} />
+  default: ({ alt, src, priority: _priority, ...props }: any) => React.createElement('img', { alt, src, ...props })
 }));
 
 vi.mock('../providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
+    t: createTranslatorMock({ locale: 'zh-CN' }),
     locale: 'zh-CN',
     locales: [
       { code: 'en-US', labelKey: 'settings.system-config.locale.en_US', abbr: '🇬🇧' },
@@ -39,7 +39,7 @@ vi.mock('../shell/platform-copyright-footer', () => ({
     <footer {...props}>
       <div className={headlineClassName}>Apache HertzBeat™{version ? ` ${version}` : ''}</div>
       <a className={linkClassName}>Apache HertzBeat™</a>
-      <div className={lineClassName}>{children ?? 'Licensed under the Apache License, Version 2.0'}</div>
+      {children ? <div className={lineClassName}>{children}</div> : null}
     </footer>
   )
 }));
@@ -57,6 +57,13 @@ describe('PassportShell', () => {
     );
 
     expect(html).toContain('data-passport-shell="true"');
+    expect(html).toContain('data-hz-ui="passport-session-clear-frame"');
+    expect(html).toContain('data-passport-session-clear-contract="angular-token-service-clear-on-passport-entry"');
+    expect(html).toContain('data-passport-session-clear-enabled="true"');
+    expect(html).toContain('data-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"');
+    expect(html).toContain('data-hz-passport-session-clear-lifecycle="angular-token-service-clear-on-passport-entry"');
+    expect(html).toContain('data-hz-passport-session-clear-scope="client-marker-user-snapshot"');
+    expect(html).toContain('data-hz-passport-session-clear-boundary="no-api-logout-on-entry"');
     expect(html).toContain('data-passport-shell-spacing="angular-reference"');
     expect(html).toContain('data-passport-brand-lockup="angular-lowered"');
     expect(html).toContain('data-passport-content-alignment="angular-centered"');
@@ -76,7 +83,32 @@ describe('PassportShell', () => {
     expect(html).toContain('data-passport-footer-band="angular-raised"');
     expect(html).toContain('/assets/bg.png');
     expect(html).toContain('Apache HertzBeat™ v1.8.0');
-    expect(html).toContain('Licensed under the Apache License, Version 2.0');
+    expect(html).toContain('开源私有化企业运维可观测平台');
+    expect(html).toContain('采集器、监控模板、实体、指标、日志和链路');
+    expect(html).toContain('处理告警并关闭问题');
+    expect(html).toContain('通过采集器接入应用、数据库、操作系统、中间件和网络设备指标,数据留在私有化部署内。');
+    expect(html).toContain('按网络区域扩展 Collector 集群,支撑私有化、隔离采集和状态页。');
+    expect(html).not.toContain('遵循 Apache License, Version 2.0 授权');
+    expect(html).not.toContain('Apache License, Version 2.0');
+  });
+
+  it('can preserve the active session for the Angular lock route', () => {
+    const html = renderToStaticMarkup(
+      <PassportShell panelClassName="max-w-[712px]" sessionLifecycle="preserve-on-lock">
+        <PassportPanel title="Unlock">
+          <form data-passport-lock-form="true">
+            <input />
+          </form>
+        </PassportPanel>
+      </PassportShell>
+    );
+
+    expect(html).toContain('data-passport-session-clear-contract="angular-lock-preserve-session"');
+    expect(html).toContain('data-passport-session-clear-enabled="false"');
+    expect(html).toContain('data-hz-passport-session-clear-lifecycle="angular-lock-preserve-session"');
+    expect(html).toContain('data-hz-passport-session-clear-scope="client-marker-user-snapshot-preserved"');
+    expect(html).toContain('data-hz-passport-session-clear-boundary="no-session-clear-on-lock"');
+    expect(html).toContain('data-passport-lock-form="true"');
   });
 
   it('removes the remaining bright auth shell residue and adopts ops tokens', () => {
@@ -104,9 +136,16 @@ describe('PassportShell', () => {
     expect(source).toContain('text-[var(--ops-text-secondary)]');
     expect(source).toContain('text-[var(--ops-primary)]');
     expect(source).toContain("from '../workbench/primitives'");
+    expect(source).toContain("import { HzPassportSessionClearFrame } from '@hertzbeat/ui';");
+    expect(source).toContain("import { clearClientSessionMarker, clearClientSessionUserSnapshot } from '../../lib/session-client';");
+    expect(source).toContain('clearClientSessionMarker();');
+    expect(source).toContain('clearClientSessionUserSnapshot();');
+    expect(source).not.toContain('clearClientSession();');
     expect(source).toContain('WorkbenchPanel');
     expect(source).toContain('data-passport-locale-trigger="globe"');
     expect(source).toContain('data-passport-locale-tone="angular-magenta"');
+    expect(source).toContain("aria-label={t('app.passport.language-switch')}");
+    expect(source).not.toContain('aria-label="Switch language"');
     expect(source).toContain('text-[#d11ce6]');
     expect(source).toContain('data-passport-background-overlay="angular-light"');
     expect(source).toContain('data-passport-intro-list="angular-single-column"');
diff --git a/web-next/components/pages/passport-shell.tsx b/web-next/components/pages/passport-shell.tsx
index 325dc16bd8..80a12be448 100644
--- a/web-next/components/pages/passport-shell.tsx
+++ b/web-next/components/pages/passport-shell.tsx
@@ -1,17 +1,20 @@
 'use client';
 
-import React, { type ReactNode, useState } from 'react';
+import React, { type ReactNode, useEffect, useState } from 'react';
 import Image from 'next/image';
 import { Globe2 } from 'lucide-react';
+import { HzPassportSessionClearFrame } from '@hertzbeat/ui';
 import { useI18n } from '../providers/i18n-provider';
 import { LocaleOptionList } from '../shell/locale-option-list';
 import { PlatformCopyrightFooter } from '../shell/platform-copyright-footer';
 import { WorkbenchPanel } from '../workbench/primitives';
+import { clearClientSessionMarker, clearClientSessionUserSnapshot } from '../../lib/session-client';
 import { cn } from '../../lib/utils';
 
 type PassportShellProps = {
   children: ReactNode;
   panelClassName?: string;
+  sessionLifecycle?: 'clear-on-entry' | 'preserve-on-lock';
 };
 
 type PassportPanelProps = {
@@ -21,9 +24,11 @@ type PassportPanelProps = {
   className?: string;
 };
 
-export function PassportShell({ children, panelClassName }: PassportShellProps) {
+export function PassportShell({ children, panelClassName, sessionLifecycle = 'clear-on-entry' }: PassportShellProps) {
   const { t, locale, locales, setLocale } = useI18n();
   const [localeOpen, setLocaleOpen] = useState(false);
+  const shouldClearSession = sessionLifecycle === 'clear-on-entry';
+  const sessionContract = shouldClearSession ? 'angular-token-service-clear-on-passport-entry' : 'angular-lock-preserve-session';
   const passportPoints = [
     t('about.point.1'),
     t('about.point.2'),
@@ -33,11 +38,23 @@ export function PassportShell({ children, panelClassName }: PassportShellProps)
     t('about.point.6')
   ];
 
+  useEffect(() => {
+    if (!shouldClearSession) {
+      return;
+    }
+    clearClientSessionMarker();
+    clearClientSessionUserSnapshot();
+  }, [shouldClearSession]);
+
   return (
-    <div
+    <HzPassportSessionClearFrame
       className="relative min-h-screen overflow-hidden bg-[var(--ops-background)] bg-cover bg-center bg-no-repeat text-[var(--ops-text-primary)]"
+      lifecycle={sessionLifecycle}
       data-login-shell="passport"
       data-passport-shell="true"
+      data-passport-session-clear-contract={sessionContract}
+      data-passport-session-clear-enabled={shouldClearSession ? 'true' : 'false'}
+      data-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"
       style={{ backgroundImage: "url('/assets/bg.png')" }}
     >
       <div
@@ -53,7 +70,7 @@ export function PassportShell({ children, panelClassName }: PassportShellProps)
             <div className="absolute right-0 top-0 hidden md:block">
               <div className="relative">
                 <button
-                  aria-label="Switch language"
+                  aria-label={t('app.passport.language-switch')}
                   className="inline-flex h-9 w-9 items-center justify-center text-[#d11ce6] transition-colors hover:text-[#f149ff]"
                   data-passport-locale-trigger="globe"
                   data-passport-locale-tone="angular-magenta"
@@ -149,7 +166,7 @@ export function PassportShell({ children, panelClassName }: PassportShellProps)
           />
         </div>
       </div>
-    </div>
+    </HzPassportSessionClearFrame>
   );
 }
 
diff --git a/web-next/lib/passport-lock/view-model.test.ts b/web-next/lib/passport-lock/view-model.test.ts
index a5a34bed59..9b650e5858 100644
--- a/web-next/lib/passport-lock/view-model.test.ts
+++ b/web-next/lib/passport-lock/view-model.test.ts
@@ -8,14 +8,14 @@ describe('passport lock view model', () => {
   it('builds lock page facts', () => {
     expect(buildLockFacts(t)).toEqual([
       { label: '工作区', value: 'passport/lock' },
-      { label: '状态', value: 'interactive' },
-      { label: '下一步', value: 'wire unlock validation if needed' }
+      { label: '状态', value: '可解锁' },
+      { label: '下一步', value: '按需接入解锁校验' }
     ]);
   });
 
   it('requires a non-empty password', () => {
     expect(validateUnlockPassword('', t)).toBe('请输入密码');
-    expect(validateUnlockPassword('   ', t)).toBe('请输入密码');
+    expect(validateUnlockPassword('   ', t)).toBeNull();
     expect(validateUnlockPassword('secret', t)).toBeNull();
   });
 });
diff --git a/web-next/lib/passport-lock/view-model.ts b/web-next/lib/passport-lock/view-model.ts
index 72e5114a7d..6d692170da 100644
--- a/web-next/lib/passport-lock/view-model.ts
+++ b/web-next/lib/passport-lock/view-model.ts
@@ -3,13 +3,13 @@ type Translator = (key: string, params?: Record<string, string | number | null |
 export function buildLockFacts(t: Translator) {
   return [
     { label: t('common.workspace'), value: 'passport/lock' },
-    { label: t('common.status'), value: 'interactive' },
+    { label: t('common.status'), value: t('passport.lock.status.interactive') },
     { label: t('common.next-step'), value: t('passport.lock.next-step') }
   ];
 }
 
 export function validateUnlockPassword(password: string, t: Translator) {
-  if (!password.trim()) {
+  if (password.length === 0) {
     return t('passport.lock.error.required');
   }
   return null;
diff --git a/web-next/lib/passport-login/controller.test.ts b/web-next/lib/passport-login/controller.test.ts
index 73759e5a7a..5d04bc41e3 100644
--- a/web-next/lib/passport-login/controller.test.ts
+++ b/web-next/lib/passport-login/controller.test.ts
@@ -1,13 +1,17 @@
 import { describe, expect, it, vi } from 'vitest';
 import {
   assertLoginSuccess,
+  assertSessionLoginSuccess,
   bootstrapPostLoginSession,
+  buildLoginCompatRouteUrl,
   buildLoginRedirectHref,
   buildLoginRequestBody,
   buildLoginReturnTo,
-  persistLoginTokens,
+  buildPostLoginSessionUser,
+  readPassportLoginRouteState,
   resolveLoginError,
   resolvePostLoginRedirectTarget,
+  resolvePostLoginStartupFailureTarget,
   sanitizeLoginRedirectTarget
 } from './controller';
 
@@ -36,13 +40,43 @@ describe('passport login controller', () => {
     expect(() => assertLoginSuccess(200, { code: 0, data: { token: 'access' } }, 'Login failed: {{status}}')).toThrow('Login failed: 200');
   });
 
-  it('persists access and refresh tokens', () => {
-    const storage = { setItem: vi.fn() };
+  it('accepts BFF-cookie login success without exposing tokens to the browser', () => {
+    expect(
+      assertSessionLoginSuccess(200, { code: 0, data: { authenticated: true } }, 'Login failed: {{status}}')
+    ).toBeUndefined();
+
+    expect(() => assertSessionLoginSuccess(401, { code: 1 }, 'Login failed: {{status}}')).toThrow('Login failed: 401');
+    expect(() => assertSessionLoginSuccess(200, { code: 1, msg: 'denied' }, 'Login failed: {{status}}')).toThrow('denied');
+  });
+
+  it('builds the Angular-style post-login user snapshot from the raw submitted identifier', () => {
+    expect(
+      buildPostLoginSessionUser(' ops-admin ', {
+        code: 0,
+        data: {
+          authenticated: true,
+          tokenBoundary: 'bff-cookie',
+          role: 'ADMIN'
+        }
+      })
+    ).toEqual({
+      name: ' ops-admin ',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator',
+      role: 'ADMIN'
+    });
 
-    persistLoginTokens(storage, { token: 'access', refreshToken: 'refresh' });
+    expect(buildPostLoginSessionUser('   ', { code: 0, data: { authenticated: true } })).toEqual({
+      name: '   ',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator'
+    });
 
-    expect(storage.setItem).toHaveBeenNthCalledWith(1, 'Authorization', 'access');
-    expect(storage.setItem).toHaveBeenNthCalledWith(2, 'refresh-token', 'refresh');
+    expect(buildPostLoginSessionUser('', { code: 0, data: { authenticated: true } })).toEqual({
+      name: 'admin',
+      avatar: './assets/img/avatar.svg',
+      email: 'administrator'
+    });
   });
 
   it('sanitizes login redirect targets to internal non-auth routes', () => {
@@ -87,20 +121,68 @@ describe('passport login controller', () => {
     expect(buildLoginRedirectHref('/passport/login')).toBe('/passport/login');
     expect(buildLoginRedirectHref('/passport/login', '/login')).toBe('/login');
     expect(resolvePostLoginRedirectTarget('/monitors?app=website')).toBe('/monitors?app=website');
-    expect(resolvePostLoginRedirectTarget('/passport/login')).toBe('/overview');
-    expect(resolvePostLoginRedirectTarget('https://example.com')).toBe('/overview');
+    expect(resolvePostLoginRedirectTarget('/passport/login')).toBe('/');
+    expect(resolvePostLoginRedirectTarget('https://example.com')).toBe('/');
+    expect(resolvePostLoginRedirectTarget('/passport/login', '/overview')).toBe('/overview');
+    expect(resolvePostLoginStartupFailureTarget()).toBe('/exception/500');
+  });
+
+  it('builds the compatibility login alias URL while preserving guard context', () => {
+    expect(buildLoginCompatRouteUrl()).toBe('/passport/login');
+    expect(buildLoginCompatRouteUrl({ redirect: '/monitors?app=website', source: 'guard' })).toBe(
+      '/passport/login?redirect=%2Fmonitors%3Fapp%3Dwebsite&source=guard'
+    );
+    expect(buildLoginCompatRouteUrl({ redirect: '/trace/manage?traceId=1', returnLabel: 'Login' })).toBe(
+      '/passport/login?redirect=%2Ftrace%2Fmanage%3FtraceId%3D1'
+    );
+  });
+
+  it('normalizes multi-value URL search params into the first passport login redirect target', () => {
+    expect(
+      readPassportLoginRouteState({
+        redirect: ['/monitors?app=website', '/trace/manage?traceId=1'],
+        returnLabel: ['Login', 'Ignored']
+      })
+    ).toEqual({
+      redirectTarget: '/monitors?app=website'
+    });
+
+    expect(
+      readPassportLoginRouteState({
+        redirect: ['https://evil.example', '/monitors?app=website']
+      })
+    ).toEqual({
+      redirectTarget: '/'
+    });
   });
 
-  it('warms the post-login session bootstrap without failing on bootstrap misses', async () => {
+  it('warms the post-login session bootstrap after login success', async () => {
     const apiGet = vi
       .fn()
-      .mockResolvedValueOnce({ locale: 'en-US' });
+      .mockResolvedValueOnce({ code: 0, data: { locale: 'zh_CN' } })
+      .mockResolvedValueOnce({ code: 0, data: [] });
 
     await expect(bootstrapPostLoginSession(apiGet as any)).resolves.toBeUndefined();
-    expect(apiGet).toHaveBeenCalledWith('/config/system');
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/config/system');
+    expect(apiGet).toHaveBeenNthCalledWith(2, '/apps/hierarchy?lang=zh-CN');
 
-    const failingApiGet = vi.fn().mockRejectedValueOnce(new Error('bootstrap failed'));
+    const failingApiGet = vi
+      .fn()
+      .mockRejectedValueOnce(new Error('bootstrap failed'))
+      .mockResolvedValueOnce({ code: 0, data: [] });
     await expect(bootstrapPostLoginSession(failingApiGet as any)).resolves.toBeUndefined();
-    expect(failingApiGet).toHaveBeenCalledWith('/config/system');
+    expect(failingApiGet).toHaveBeenNthCalledWith(1, '/config/system');
+    expect(failingApiGet).toHaveBeenNthCalledWith(2, '/apps/hierarchy?lang=en-US');
+  });
+
+  it('preserves Angular startup failure routing when the hierarchy bootstrap fails', async () => {
+    const apiGet = vi
+      .fn()
+      .mockResolvedValueOnce({ code: 0, data: { locale: 'zh_CN' } })
+      .mockRejectedValueOnce(new Error('hierarchy failed'));
+
+    await expect(bootstrapPostLoginSession(apiGet as any)).rejects.toThrow('hierarchy failed');
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/config/system');
+    expect(apiGet).toHaveBeenNthCalledWith(2, '/apps/hierarchy?lang=zh-CN');
   });
 });
diff --git a/web-next/lib/passport-login/controller.ts b/web-next/lib/passport-login/controller.ts
index 6eca563773..d9682b901c 100644
--- a/web-next/lib/passport-login/controller.ts
+++ b/web-next/lib/passport-login/controller.ts
@@ -1,21 +1,42 @@
+import { buildCompatRedirectTarget, createCompatSearchParamReader, type SearchParamsRecord } from '../compat/search-params';
 import { stripReturnLabelFromHref } from '../signal-route-context';
 
+export type { SearchParamsRecord } from '../compat/search-params';
+
 export type LoginTokens = {
   token: string;
   refreshToken: string;
 };
 
+export type PostLoginSessionUser = {
+  name: string;
+  avatar: string;
+  email: string;
+  role?: string;
+};
+
+export type PassportLoginSearchParams = SearchParamsRecord;
+
+export type PassportLoginRouteState = {
+  redirectTarget: string;
+};
+
 export const DEFAULT_LOGIN_ENTRY_PATH = '/passport/login';
 export const LOGIN_REDIRECT_QUERY_KEY = 'redirect';
+export const POST_LOGIN_STARTUP_FAILURE_PATH = '/exception/500';
+
+export function buildLoginCompatRouteUrl(searchParams?: SearchParamsRecord) {
+  return buildCompatRedirectTarget(DEFAULT_LOGIN_ENTRY_PATH, searchParams);
+}
 
 export type LoginMessage = {
   code?: number;
   msg?: string;
-  data?: Partial<LoginTokens>;
-};
-
-type StorageLike = {
-  setItem: (key: string, value: string) => void;
+  data?: Partial<LoginTokens> & {
+    authenticated?: boolean;
+    tokenBoundary?: string;
+    role?: unknown;
+  };
 };
 
 type LocationLike = {
@@ -26,6 +47,13 @@ type LocationLike = {
 
 type ApiGetter = <T>(path: string) => Promise<T>;
 
+type SystemConfigPayload = {
+  code?: number;
+  data?: {
+    locale?: unknown;
+  } | null;
+};
+
 export function buildLoginRequestBody(identifier: string, credential: string) {
   return {
     type: 0,
@@ -49,9 +77,24 @@ export function assertLoginSuccess(status: number, message: LoginMessage, fallba
   };
 }
 
-export function persistLoginTokens(storage: StorageLike, tokens: LoginTokens) {
-  storage.setItem('Authorization', tokens.token);
-  storage.setItem('refresh-token', tokens.refreshToken);
+export function assertSessionLoginSuccess(status: number, message: LoginMessage, fallback: string) {
+  if (status >= 400 || message.code !== 0) {
+    throw new Error(resolveLoginError(status, message, fallback));
+  }
+}
+
+export function buildPostLoginSessionUser(identifier: string, message: LoginMessage): PostLoginSessionUser {
+  const name = identifier || 'admin';
+  const role = typeof message.data?.role === 'string' && message.data.role.trim()
+    ? message.data.role.trim()
+    : undefined;
+
+  return {
+    name,
+    avatar: './assets/img/avatar.svg',
+    email: 'administrator',
+    ...(role ? { role } : {})
+  };
 }
 
 export function sanitizeLoginRedirectTarget(value?: string | null) {
@@ -91,12 +134,35 @@ export function buildLoginRedirectHref(returnTo?: string | null, loginPath?: str
   return `${resolvedLoginPath}?${params.toString()}`;
 }
 
-export function resolvePostLoginRedirectTarget(returnTo?: string | null, fallback = '/overview') {
+export function resolvePostLoginRedirectTarget(returnTo?: string | null, fallback = '/') {
   return sanitizeLoginRedirectTarget(returnTo) || fallback;
 }
 
-export async function bootstrapPostLoginSession(apiGet: ApiGetter) {
-  await Promise.allSettled([
-    apiGet('/config/system')
-  ]);
+export function resolvePostLoginStartupFailureTarget() {
+  return POST_LOGIN_STARTUP_FAILURE_PATH;
+}
+
+export function readPassportLoginRouteState(searchParams?: PassportLoginSearchParams): PassportLoginRouteState {
+  const reader = createCompatSearchParamReader(searchParams);
+  return {
+    redirectTarget: resolvePostLoginRedirectTarget(reader.get(LOGIN_REDIRECT_QUERY_KEY))
+  };
+}
+
+function normalizeStartupLocale(locale: unknown, fallback = 'en-US') {
+  if (typeof locale !== 'string') return fallback;
+  const nextLocale = locale.trim().replace('_', '-');
+  return nextLocale || fallback;
+}
+
+export async function bootstrapPostLoginSession(apiGet: ApiGetter, fallbackLocale = 'en-US') {
+  let configPayload: SystemConfigPayload | null = null;
+  try {
+    configPayload = await apiGet<SystemConfigPayload>('/config/system');
+  } catch {
+    configPayload = null;
+  }
+  const startupLocale = normalizeStartupLocale(configPayload?.data?.locale, fallbackLocale);
+
+  await apiGet(`/apps/hierarchy?lang=${encodeURIComponent(startupLocale)}`);
 }
diff --git a/web-next/lib/passport-login/view-model.test.ts b/web-next/lib/passport-login/view-model.test.ts
index de2f1c985b..8a8ee1c445 100644
--- a/web-next/lib/passport-login/view-model.test.ts
+++ b/web-next/lib/passport-login/view-model.test.ts
@@ -1,8 +1,9 @@
 import { describe, expect, it, vi } from 'vitest';
-import { buildLoginFeatureCards, buildLoginNotice, shouldBlockDefaultPasswordSubmit, shouldWarnDefaultPassword } from './view-model';
+import { buildLoginFeatureCards, buildLoginNotice, shouldBlockDefaultPasswordSubmit, shouldWarnDefaultPassword, validateCredentialLoginDraft } from './view-model';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 const t = createTranslatorMock({ locale: 'zh-CN' });
+const enT = createTranslatorMock({ locale: 'en-US' });
 
 describe('passport login view model', () => {
   it('detects the default password warning condition', () => {
@@ -16,6 +17,20 @@ describe('passport login view model', () => {
     expect(shouldBlockDefaultPasswordSubmit(false, 'custom')).toBe(false);
   });
 
+  it('validates the Angular credential-login required fields before submit', () => {
+    expect(validateCredentialLoginDraft('', 'custom', enT)).toEqual({
+      field: 'identifier',
+      message: 'Please enter your username'
+    });
+    expect(validateCredentialLoginDraft('ops-admin', '', enT)).toEqual({
+      field: 'credential',
+      message: 'Please enter password'
+    });
+    expect(validateCredentialLoginDraft(' ops-admin ', ' custom ', enT)).toBeNull();
+    expect(validateCredentialLoginDraft('   ', 'custom', enT)).toBeNull();
+    expect(validateCredentialLoginDraft('ops-admin', '   ', enT)).toBeNull();
+  });
+
   it('builds the login feature cards', () => {
     expect(buildLoginFeatureCards(t)).toEqual([
       { title: '运维入口', copy: '登录后继续查看资源、实体、遥测数据和告警。' },
@@ -27,7 +42,7 @@ describe('passport login view model', () => {
   it('builds the warning or session notice', () => {
     expect(buildLoginNotice(true, t)).toEqual({
       kind: 'warning',
-      copy: '当前使用默认密码,建议登录后尽快修改。',
+      copy: '请及时更新初始默认密码!',
       href: 'https://hertzbeat.apache.org/docs/start/account-modify'
     });
 
@@ -36,4 +51,14 @@ describe('passport login view model', () => {
       copy: '登录成功后会自动恢复当前工作台会话,并在需要时尝试刷新令牌。'
     });
   });
+
+  it('builds the English session notice without localized fallback copy', () => {
+    const notice = buildLoginNotice(false, enT);
+
+    expect(notice).toEqual({
+      kind: 'session',
+      copy: 'After login, HertzBeat will restore the current workspace session and try to refresh tokens when needed.'
+    });
+    expect(notice.copy).not.toMatch(/[\u4e00-\u9fff]/);
+  });
 });
diff --git a/web-next/lib/passport-login/view-model.ts b/web-next/lib/passport-login/view-model.ts
index 75cfb85b56..821fc01e04 100644
--- a/web-next/lib/passport-login/view-model.ts
+++ b/web-next/lib/passport-login/view-model.ts
@@ -8,6 +8,24 @@ export function shouldBlockDefaultPasswordSubmit(needUpdatePassword: boolean, cr
   return !needUpdatePassword && shouldWarnDefaultPassword(credential);
 }
 
+export function validateCredentialLoginDraft(identifier: string, credential: string, t: Translator) {
+  if (identifier.length === 0) {
+    return {
+      field: 'identifier' as const,
+      message: t('app.login.message-need-identifier')
+    };
+  }
+
+  if (credential.length === 0) {
+    return {
+      field: 'credential' as const,
+      message: t('app.login.message-need-credential')
+    };
+  }
+
+  return null;
+}
+
 export function buildLoginFeatureCards(t: Translator) {
   return [
     {
@@ -29,7 +47,7 @@ export function buildLoginNotice(needUpdatePassword: boolean, t: Translator) {
   if (needUpdatePassword) {
     return {
       kind: 'warning' as const,
-      copy: t('passport.login.default-password-warning'),
+      copy: t('app.login.need-change-password'),
       href: 'https://hertzbeat.apache.org/docs/start/account-modify'
     };
   }
diff --git a/web-next/lib/session-bff.test.ts b/web-next/lib/session-bff.test.ts
new file mode 100644
index 0000000000..634cfc6d3f
--- /dev/null
+++ b/web-next/lib/session-bff.test.ts
@@ -0,0 +1,178 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+  buildBackendApiUrl,
+  proxyBackendApiRequest,
+  sanitizeSessionPayload
+} from './session-bff';
+
+describe('session BFF helpers', () => {
+  const previousBackendOrigin = process.env.BACKEND_ORIGIN;
+
+  afterEach(() => {
+    if (previousBackendOrigin === undefined) {
+      delete process.env.BACKEND_ORIGIN;
+    } else {
+      process.env.BACKEND_ORIGIN = previousBackendOrigin;
+    }
+    vi.unstubAllGlobals();
+  });
+
+  it('builds backend API URLs from the configured private origin', () => {
+    process.env.BACKEND_ORIGIN = 'http://backend.internal:1157';
+
+    expect(buildBackendApiUrl('/config/system', '?locale=en-US')).toBe(
+      'http://backend.internal:1157/api/config/system?locale=en-US'
+    );
+    expect(buildBackendApiUrl('alerts')).toBe('http://backend.internal:1157/api/alerts');
+  });
+
+  it('strips credential material from login and refresh payloads before returning them to browser code', () => {
+    expect(
+      sanitizeSessionPayload({
+        code: 0,
+        data: {
+          token: 'access-token',
+          refreshToken: 'refresh-token',
+          user: 'admin'
+        }
+      })
+    ).toEqual({
+      code: 0,
+      data: {
+        user: 'admin',
+        authenticated: true,
+        tokenBoundary: 'bff-cookie'
+      }
+    });
+  });
+
+  it('forwards the BFF access cookie as backend authorization when the framework cookie reader misses it', async () => {
+    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
+      new Response(JSON.stringify({ code: 0, data: { ok: true } }), {
+        status: 200,
+        headers: { 'Content-Type': 'application/json' }
+      })
+    );
+    vi.stubGlobal('fetch', fetchMock);
+
+    const request = {
+      url: 'http://127.0.0.1:4200/api/config/system?locale=zh-CN',
+      method: 'GET',
+      headers: new Headers({
+        cookie: 'hb_ui_session=1; hb_ui_access=session-access-token; theme=dark',
+        'accept-language': 'zh-CN'
+      }),
+      cookies: {
+        get: () => undefined
+      },
+      arrayBuffer: vi.fn()
+    };
+
+    await proxyBackendApiRequest(request as any, '/config/system');
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      'http://127.0.0.1:1157/api/config/system?locale=zh-CN',
+      expect.objectContaining({
+        method: 'GET',
+        cache: 'no-store'
+      })
+    );
+    const forwardedHeaders = fetchMock.mock.calls[0][1]?.headers as Headers;
+    expect(forwardedHeaders.get('Authorization')).toBe('Bearer session-access-token');
+    expect(forwardedHeaders.get('cookie')).toBeNull();
+  });
+
+  it('does not forward backend auth challenges to browser-facing API responses', async () => {
+    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
+      new Response('token expired', {
+        status: 401,
+        headers: {
+          'Content-Type': 'text/plain',
+          'WWW-Authenticate': 'Digest realm=sureness_realm,nonce=abc,qop=auth'
+        }
+      })
+    );
+    vi.stubGlobal('fetch', fetchMock);
+
+    const request = {
+      url: 'http://127.0.0.1:4200/api/monitor',
+      method: 'GET',
+      headers: new Headers(),
+      cookies: {
+        get: () => undefined
+      },
+      arrayBuffer: vi.fn()
+    };
+
+    const response = await proxyBackendApiRequest(request as any, '/monitor');
+
+    expect(response.status).toBe(401);
+    expect(response.headers.get('WWW-Authenticate')).toBeNull();
+    expect(response.headers.get('Content-Type')).toContain('text/plain');
+  });
+
+  it('refreshes the BFF access cookie and retries protected API calls when only refresh session remains', async () => {
+    const fetchMock = vi.fn<typeof fetch>()
+      .mockResolvedValueOnce(
+        new Response('token expired', {
+          status: 401,
+          headers: {
+            'WWW-Authenticate': 'Digest realm=sureness_realm,nonce=abc,qop=auth'
+          }
+        })
+      )
+      .mockResolvedValueOnce(
+        new Response(
+          JSON.stringify({
+            code: 0,
+            data: {
+              token: 'new-access-token',
+              refreshToken: 'new-refresh-token'
+            }
+          }),
+          {
+            status: 200,
+            headers: { 'Content-Type': 'application/json' }
+          }
+        )
+      )
+      .mockResolvedValueOnce(
+        new Response(JSON.stringify({ code: 0, data: { ok: true } }), {
+          status: 200,
+          headers: { 'Content-Type': 'application/json' }
+        })
+      );
+    vi.stubGlobal('fetch', fetchMock);
+
+    const request = {
+      url: 'http://127.0.0.1:4200/api/config/system',
+      method: 'GET',
+      headers: new Headers({
+        cookie: 'hb_ui_session=1; hb_ui_refresh=session-refresh-token',
+        'accept-language': 'zh-CN'
+      }),
+      cookies: {
+        get: () => undefined
+      },
+      arrayBuffer: vi.fn()
+    };
+
+    const response = await proxyBackendApiRequest(request as any, '/config/system');
+
+    expect(response.status).toBe(200);
+    expect(await response.text()).toContain('"ok":true');
+    expect(fetchMock).toHaveBeenCalledTimes(3);
+    expect(fetchMock.mock.calls[1][0]).toBe('http://127.0.0.1:1157/api/account/auth/refresh');
+    expect(fetchMock.mock.calls[1][1]).toEqual(
+      expect.objectContaining({
+        method: 'POST',
+        body: JSON.stringify({ token: 'session-refresh-token' })
+      })
+    );
+    const retryHeaders = fetchMock.mock.calls[2][1]?.headers as Headers;
+    expect(retryHeaders.get('Authorization')).toBe('Bearer new-access-token');
+    expect(response.headers.get('set-cookie')).toContain('hb_ui_access=new-access-token');
+    expect(response.headers.get('set-cookie')).toContain('hb_ui_refresh=new-refresh-token');
+    expect(response.headers.get('WWW-Authenticate')).toBeNull();
+  });
+});
diff --git a/web-next/lib/session-bff.ts b/web-next/lib/session-bff.ts
new file mode 100644
index 0000000000..b26af137a3
--- /dev/null
+++ b/web-next/lib/session-bff.ts
@@ -0,0 +1,206 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+export const HB_UI_ACCESS_COOKIE = 'hb_ui_access';
+export const HB_UI_REFRESH_COOKIE = 'hb_ui_refresh';
+export const HB_UI_SESSION_MARKER_COOKIE = 'hb_ui_session';
+
+type ApiPayload = {
+  code?: number;
+  msg?: string;
+  data?: Record<string, unknown> | null;
+};
+
+type SessionTokens = {
+  token?: string;
+  refreshToken?: string;
+};
+
+const DEFAULT_BACKEND_ORIGIN = 'http://127.0.0.1:1157';
+const ACCESS_MAX_AGE_SECONDS = 60 * 60;
+const REFRESH_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
+
+function baseCookieOptions(maxAge: number) {
+  return {
+    sameSite: 'lax' as const,
+    secure: process.env.NODE_ENV === 'production',
+    path: '/',
+    maxAge
+  };
+}
+
+function secretCookieOptions(maxAge: number) {
+  return {
+    ...baseCookieOptions(maxAge),
+    httpOnly: true
+  };
+}
+
+function markerCookieOptions(maxAge: number) {
+  return {
+    ...baseCookieOptions(maxAge),
+    httpOnly: false
+  };
+}
+
+function readCookieHeaderValue(cookieHeader: string | null, name: string) {
+  if (!cookieHeader) return undefined;
+  const prefix = `${name}=`;
+  for (const segment of cookieHeader.split(';')) {
+    const trimmed = segment.trim();
+    if (!trimmed.startsWith(prefix)) continue;
+    const rawValue = trimmed.slice(prefix.length);
+    if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
+      return rawValue.slice(1, -1);
+    }
+    return rawValue;
+  }
+  return undefined;
+}
+
+export function readSessionCookieValue(request: Pick<NextRequest, 'cookies' | 'headers'>, name: string) {
+  return request.cookies.get(name)?.value ?? readCookieHeaderValue(request.headers.get('cookie'), name);
+}
+
+export function resolveBackendOrigin() {
+  return process.env.BACKEND_ORIGIN?.trim() || DEFAULT_BACKEND_ORIGIN;
+}
+
+export function buildBackendApiUrl(path: string, search = '') {
+  const cleanPath = path.startsWith('/') ? path : `/${path}`;
+  return `${resolveBackendOrigin()}/api${cleanPath}${search}`;
+}
+
+export function sanitizeSessionPayload(payload: ApiPayload): ApiPayload {
+  if (!payload.data || typeof payload.data !== 'object') {
+    return payload;
+  }
+  const { token: _token, refreshToken: _refreshToken, ...rest } = payload.data;
+  return {
+    ...payload,
+    data: {
+      ...rest,
+      authenticated: payload.code === 0,
+      tokenBoundary: 'bff-cookie'
+    }
+  };
+}
+
+export function applySessionCookies(response: NextResponse, tokens: SessionTokens) {
+  if (tokens.token) {
+    response.cookies.set(HB_UI_ACCESS_COOKIE, tokens.token, secretCookieOptions(ACCESS_MAX_AGE_SECONDS));
+  }
+  if (tokens.refreshToken) {
+    response.cookies.set(HB_UI_REFRESH_COOKIE, tokens.refreshToken, secretCookieOptions(REFRESH_MAX_AGE_SECONDS));
+  }
+  if (tokens.token || tokens.refreshToken) {
+    response.cookies.set(HB_UI_SESSION_MARKER_COOKIE, '1', markerCookieOptions(REFRESH_MAX_AGE_SECONDS));
+  }
+}
+
+export function clearSessionCookies(response: NextResponse) {
+  [HB_UI_ACCESS_COOKIE, HB_UI_REFRESH_COOKIE, HB_UI_SESSION_MARKER_COOKIE].forEach(name => {
+    const options = name === HB_UI_SESSION_MARKER_COOKIE ? markerCookieOptions(0) : secretCookieOptions(0);
+    response.cookies.set(name, '', options);
+  });
+}
+
+function copyProxyHeaders(request: NextRequest) {
+  const headers = new Headers(request.headers);
+  headers.delete('cookie');
+  headers.delete('host');
+  headers.delete('connection');
+  headers.delete('content-length');
+  return headers;
+}
+
+function copyResponseHeaders(headers: Headers) {
+  const nextHeaders = new Headers(headers);
+  nextHeaders.delete('content-encoding');
+  nextHeaders.delete('content-length');
+  nextHeaders.delete('transfer-encoding');
+  nextHeaders.delete('www-authenticate');
+  nextHeaders.delete('proxy-authenticate');
+  return nextHeaders;
+}
+
+async function readProxyBody(request: NextRequest) {
+  if (request.method === 'GET' || request.method === 'HEAD') {
+    return undefined;
+  }
+  return request.arrayBuffer();
+}
+
+async function refreshSessionTokens(request: NextRequest): Promise<SessionTokens | null> {
+  const refreshToken = readSessionCookieValue(request, HB_UI_REFRESH_COOKIE);
+  if (!refreshToken) {
+    return null;
+  }
+
+  const upstream = await fetch(buildBackendApiUrl('/account/auth/refresh'), {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      ...(request.headers.get('Accept-Language') ? { 'Accept-Language': request.headers.get('Accept-Language') as string } : {})
+    },
+    body: JSON.stringify({ token: refreshToken }),
+    cache: 'no-store'
+  });
+  const payload = await readJsonPayload(upstream);
+  if (!upstream.ok || payload.code !== 0 || !payload.data || typeof payload.data.token !== 'string') {
+    return null;
+  }
+
+  return {
+    token: payload.data.token,
+    refreshToken: typeof payload.data.refreshToken === 'string' ? payload.data.refreshToken : refreshToken
+  };
+}
+
+export async function proxyBackendApiRequest(request: NextRequest, path: string) {
+  const requestUrl = new URL(request.url);
+  const body = await readProxyBody(request);
+  const accessToken = readSessionCookieValue(request, HB_UI_ACCESS_COOKIE);
+
+  const fetchUpstream = (token?: string) => {
+    const headers = copyProxyHeaders(request);
+    if (token) {
+      headers.set('Authorization', `Bearer ${token}`);
+    }
+    return fetch(buildBackendApiUrl(path, requestUrl.search), {
+      method: request.method,
+      headers,
+      body,
+      redirect: 'manual',
+      cache: 'no-store'
+    });
+  };
+
+  let upstream = await fetchUpstream(accessToken);
+  let refreshedTokens: SessionTokens | null = null;
+  if (upstream.status === 401) {
+    refreshedTokens = await refreshSessionTokens(request);
+    if (refreshedTokens?.token) {
+      upstream = await fetchUpstream(refreshedTokens.token);
+    }
+  }
+
+  const response = new NextResponse(upstream.body, {
+    status: upstream.status,
+    statusText: upstream.statusText,
+    headers: copyResponseHeaders(upstream.headers)
+  });
+  if (refreshedTokens?.token && upstream.ok) {
+    applySessionCookies(response, refreshedTokens);
+  } else if (upstream.status === 401 && readSessionCookieValue(request, HB_UI_REFRESH_COOKIE)) {
+    clearSessionCookies(response);
+  }
+  return response;
+}
+
+export async function readJsonPayload(response: Response): Promise<ApiPayload> {
+  try {
+    return (await response.json()) as ApiPayload;
+  } catch {
+    return { code: response.ok ? 0 : response.status, msg: response.statusText, data: null };
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 42/50: fix(web-next): keep topology search input explicit

Posted by zh...@apache.org.
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 23523355b7edc46623ced8e5e275b638dd3df25d
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 09:33:25 2026 +0800

    fix(web-next): keep topology search input explicit
---
 web-next/app/topology/page.test.tsx     | 13 +++++++++----
 web-next/app/topology/topology-page.tsx |  9 +++++----
 2 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 172759c94d..f7db52bbe8 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -303,7 +303,8 @@ describe('topology page', () => {
     expect(source).toContain('overlayMode="non-occluding"');
     expect(source).toContain('const topologyLiveHoverEdge = topologyHoveredDetailEdge;');
     expect(source).toContain('topologyHoveredDetailEdge ?? topologyDetailEdge');
-    expect(source).toContain("const topologyG6SearchQuery = topologySearchQuery ?? '';");
+    expect(source).toContain("const topologyToolbarSearchQuery = topologySearchQuery ?? '';");
+    expect(source).toContain('const topologyG6SearchQuery = topologyToolbarSearchQuery;');
     expect(source).toContain('searchQuery={topologyG6SearchQuery}');
     expect(source).toContain('topologyNodeIds');
     expect(source).not.toContain('const topologyNodeHrefById');
@@ -427,6 +428,8 @@ describe('topology page', () => {
     expect(source).not.toContain('setTopologyLayout(value as TopologyLayoutMode);');
     expect(source).toContain('const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(undefined);');
     expect(source).toContain('const topologyEffectiveSearchQuery = topologySearchQuery ?? map.filterContext.search;');
+    expect(source).toContain("const topologyToolbarSearchQuery = topologySearchQuery ?? '';");
+    expect(source).toContain('const topologyG6SearchQuery = topologyToolbarSearchQuery;');
     expect(source).toContain('const handleTopologySearchChange = React.useCallback');
     expect(source).toContain('setTopologySearchQuery(event.currentTarget.value);');
     expect(source).toContain('data-topology-layout-navigation="in-page-g6-layout"');
@@ -461,10 +464,11 @@ describe('topology page', () => {
     expect(source).toContain('onSearchChange={handleTopologySearchChange}');
     expect(source).toContain('onReset={handleTopologyResetScope}');
     expect(source).not.toContain('layoutValue={topologyLayout}');
-    expect(source).toContain('searchValue={topologyEffectiveSearchQuery}');
+    expect(source).toContain('searchValue={topologyToolbarSearchQuery}');
     expect(source).toContain("layout={topologyLayout}");
     expect(source).toContain("layout={topologyG6Layout}");
     expect(source).toContain('searchQuery={topologyG6SearchQuery}');
+    expect(source).toContain("if (topologyToolbarSearchQuery) topologyG6SearchParams.set('search', topologyToolbarSearchQuery);");
     expect(source).not.toContain('setLoadedApiGraph(undefined);');
   });
 
@@ -895,7 +899,8 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-filter-control-value="none"');
     expect(html).toContain('data-hz-topology-g6-filter-control="search"');
     expect(html).toContain('data-hz-topology-g6-filter-control-kind="search"');
-    expect(html).toContain('data-hz-topology-g6-filter-control-value="checkout-api"');
+    expect(html).not.toContain('data-hz-topology-g6-filter-control-value="checkout-api"');
+    expect(html).not.toContain('value="checkout-api"');
     expect(html).toContain('data-hz-topology-g6-filter-control="reset"');
     expect(html).toContain('data-hz-topology-g6-filter-control-kind="reset"');
     expect(html).toContain('data-hz-topology-g6-overlay-mode="non-occluding"');
@@ -1874,7 +1879,7 @@ describe('topology page', () => {
     expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-focus="active"/);
     expect(html).toMatch(/data-topology-edge-source="database-middleware-connection"[^>]+data-topology-edge-focus="active-path"/);
     expect(html).toContain('data-topology-view-mode-active="service-call"');
-    expect(html).toContain('value="checkout-api"');
+    expect(html).not.toContain('value="checkout-api"');
     expect(html).toContain('last-30m');
     expect(html).toContain('timeRange=last-30m');
     expect(html).toContain('当前筛选');
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index 334aea845e..76feb0035c 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -684,7 +684,8 @@ export default function TopologyPage({
     [apiOwnedEmptyGraph, loadedApiGraph, routeContext, t]
   );
   const topologyEffectiveSearchQuery = topologySearchQuery ?? map.filterContext.search;
-  const topologyG6SearchQuery = topologySearchQuery ?? '';
+  const topologyToolbarSearchQuery = topologySearchQuery ?? '';
+  const topologyG6SearchQuery = topologyToolbarSearchQuery;
   const topologyIsPending = loadedApiGraph === undefined;
   const topologyTraceCallScope = map.filterContext.sourceKind === 'otlp-trace-call' || map.filterContext.viewMode === 'service-call';
   const topologyTraceCallMissingEdges =
@@ -847,7 +848,7 @@ export default function TopologyPage({
   const topologyG6GroupParams = new URLSearchParams(topologyG6ResetParams);
   topologyG6GroupParams.set('groupBy', 'source-kind');
   const topologyG6SearchParams = new URLSearchParams(topologyG6ResetParams);
-  if (topologyEffectiveSearchQuery) topologyG6SearchParams.set('search', topologyEffectiveSearchQuery);
+  if (topologyToolbarSearchQuery) topologyG6SearchParams.set('search', topologyToolbarSearchQuery);
   if (map.filterContext.groupBy !== 'none') topologyG6SearchParams.set('groupBy', map.filterContext.groupBy);
   const handleTopologyEnvironmentChange = React.useCallback(
     (value: string) => {
@@ -1423,7 +1424,7 @@ export default function TopologyPage({
         ]}
         searchLabel={t('topology.search.aria')}
         searchPlaceholder={t('topology.search.placeholder')}
-        searchValue={topologyEffectiveSearchQuery}
+        searchValue={topologyToolbarSearchQuery}
         sourceKindLabel={t('topology.focus-trail.filter.source')}
         sourceKindValue={map.filterContext.sourceKind ?? 'all'}
         sourceKindOptions={[
@@ -1520,7 +1521,7 @@ export default function TopologyPage({
                 id: 'search',
                 kind: 'search',
                 label: t('topology.state.focus'),
-                value: topologyEffectiveSearchQuery || t('topology.all-entities'),
+                value: topologyToolbarSearchQuery || t('topology.all-entities'),
                 href: `/topology?${topologyG6SearchParams.toString()}`
               },
               {


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 04/50: feat(web-next): restore monitor editor parity

Posted by zh...@apache.org.
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 388b76446e7444524ee058c7456fcf65d4328eca
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 00:21:14 2026 +0800

    feat(web-next): restore monitor editor parity
---
 .../[monitorId]/edit/monitor-edit-page.tsx         |  103 ++
 .../app/monitors/[monitorId]/edit/page.test.tsx    |  154 ++-
 web-next/app/monitors/[monitorId]/edit/page.tsx    |   49 +-
 web-next/app/monitors/new/monitor-new-page.tsx     |   71 ++
 web-next/app/monitors/new/page.test.tsx            |  158 ++-
 web-next/app/monitors/new/page.tsx                 |   53 +-
 .../pages/monitor-editor-surface.test.tsx          | 1280 +++++++++++++++++++-
 .../components/pages/monitor-editor-surface.tsx    | 1150 +++++++++++++-----
 web-next/lib/monitor-api-facade.test.ts            |   76 ++
 web-next/lib/monitor-api-facade.ts                 |   84 ++
 web-next/lib/monitor-editor/controller.test.ts     |  316 ++++-
 web-next/lib/monitor-editor/controller.ts          |  197 ++-
 web-next/lib/monitor-editor/navigation.test.ts     |    9 +-
 web-next/lib/monitor-editor/navigation.ts          |    4 +
 web-next/lib/monitor-editor/query-state.test.ts    |  107 ++
 web-next/lib/monitor-editor/query-state.ts         |   57 +
 16 files changed, 3396 insertions(+), 472 deletions(-)

diff --git a/web-next/app/monitors/[monitorId]/edit/monitor-edit-page.tsx b/web-next/app/monitors/[monitorId]/edit/monitor-edit-page.tsx
new file mode 100644
index 0000000000..9a1dde940e
--- /dev/null
+++ b/web-next/app/monitors/[monitorId]/edit/monitor-edit-page.tsx
@@ -0,0 +1,103 @@
+'use client';
+
+import React, { useCallback } from 'react';
+import { HzButton, HzInlineFeedback } from '@hertzbeat/ui';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { MonitorEditorSurface } from '../../../../components/pages/monitor-editor-surface';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { api } from '@/lib/monitor-api-facade';
+import { buildMonitorEditCacheKey, loadMonitorEditorDraftFromFacade } from '@/lib/monitor-editor/controller';
+import type { MonitorEditRouteState } from '@/lib/monitor-editor/query-state';
+
+const MONITOR_EDIT_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_MONITOR_EDIT_ROUTE_STATE: MonitorEditRouteState = {
+  returnContext: {}
+};
+
+export default function MonitorEditPage({
+  monitorId,
+  initialRouteState
+}: {
+  monitorId: string;
+  initialRouteState?: MonitorEditRouteState;
+}) {
+  const { t } = useI18n();
+  const monitorEditRouteState = initialRouteState ?? EMPTY_MONITOR_EDIT_ROUTE_STATE;
+  const { returnContext } = monitorEditRouteState;
+  const monitorEditCacheKey = React.useMemo(() => buildMonitorEditCacheKey(monitorId), [monitorId]);
+
+  const load = useCallback(async () => {
+    return loadMonitorEditorDraftFromFacade(
+      {
+        readMonitorDetail: api.monitors.editorDetail,
+        readCollectors: api.monitors.editorCollectors,
+        readParamDefines: api.monitors.editorParamDefines
+      },
+      'edit',
+      { monitorId }
+    );
+  }, [monitorId]);
+
+  return (
+    <div
+      data-monitor-editor-app-source-contract="angular-route-context-hidden-field"
+      data-monitor-editor-static-host-position-contract="angular-before-name"
+      data-monitor-editor-field-order-contract="angular-monitor-form-sequence"
+      data-monitor-editor-detect-payload-contract="angular-monitor-collector-params-no-grafana"
+      data-monitor-editor-save-payload-contract="angular-monitor-collector-params-grafana"
+      data-monitor-editor-payload-param-merge-contract="angular-params-advanced-sdparams"
+      data-monitor-editor-payload-host-instance-contract="angular-host-param-as-instance"
+      data-monitor-editor-service-discovery-params="angular-nonstatic-only"
+      data-monitor-editor-ssl-port-notice="angular-info-notification"
+      data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"
+      data-monitor-editor-label-selector="angular-app-label-selector"
+    >
+      <ClientWorkbench
+        load={load}
+        loadingCopy={t('monitor.editor.loading.edit')}
+        cacheKey={monitorEditCacheKey}
+        cacheSettledTtlMs={MONITOR_EDIT_SETTLED_CACHE_TTL_MS}
+        renderError={(message, retry) => (
+          <section
+            className="mx-auto w-full max-w-[1320px] px-3 py-6"
+            data-monitor-editor-route-state="error"
+            data-monitor-editor-route-state-owner="hertzbeat-ui-inline-feedback"
+            data-monitor-editor-edit-error-state="angular-route-state"
+          >
+            <HzInlineFeedback
+              tone="critical"
+              title={t('monitor.route-state.form.error.title')}
+              description={
+                <>
+                  {t('monitor.route-state.form.error.copy')}
+                  <span className="sr-only">{message}</span>
+                </>
+              }
+              variant="embedded"
+              data-monitor-editor-route-state-feedback="error"
+            />
+            <div className="mt-3 flex justify-center">
+              <HzButton
+                size="sm"
+                intent="primary"
+                onClick={retry}
+                data-monitor-editor-route-state-retry="true"
+                data-monitor-editor-route-state-retry-owner="hertzbeat-ui-button"
+              >
+                {t('monitor.route-state.action.retry')}
+              </HzButton>
+            </div>
+          </section>
+        )}
+      >
+        {data => (
+          <MonitorEditorSurface
+            initial={data}
+            mode="edit"
+            returnContext={returnContext}
+          />
+        )}
+      </ClientWorkbench>
+    </div>
+  );
+}
diff --git a/web-next/app/monitors/[monitorId]/edit/page.test.tsx b/web-next/app/monitors/[monitorId]/edit/page.test.tsx
index a34af5e414..66af81e4b5 100644
--- a/web-next/app/monitors/[monitorId]/edit/page.test.tsx
+++ b/web-next/app/monitors/[monitorId]/edit/page.test.tsx
@@ -1,11 +1,13 @@
 import React from 'react';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
-import MonitorEditPage from './page';
+import MonitorEditPage from './monitor-edit-page';
 import { createTranslatorMock } from '../../../../test/i18n-test-helper';
+import type { MonitorEditRouteState } from '../../../../lib/monitor-editor/query-state';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   routerPush: vi.fn(),
   draft: {
@@ -33,35 +35,39 @@ const mockState = vi.hoisted(() => ({
   }
 }));
 
-const loadMonitorEditorDraft = vi.hoisted(() => vi.fn(async () => mockState.draft));
+const loadMonitorEditorDraftFromFacade = vi.hoisted(() => vi.fn(async () => mockState.draft));
 
 vi.mock('next/navigation', () => ({
   useRouter: () => ({
     push: mockState.routerPush
-  }),
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
+  })
 }));
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
-    locale: 'en-US'
+    t: createTranslatorMock({ locale: 'zh-CN' }),
+    locale: 'zh-CN'
   })
 }));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    cacheKey,
+    cacheSettledTtlMs
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.draft)}</div>;
+    return (
+      <div data-client-workbench="true" data-cache-key={cacheKey} data-cache-settled-ttl={cacheSettledTtlMs}>
+        {children(mockState.draft)}
+      </div>
+    );
   }
 }));
 
@@ -118,10 +124,23 @@ vi.mock('@/lib/api-client', () => ({
   apiMessagePut: vi.fn()
 }));
 
+vi.mock('@/lib/monitor-api-facade', () => ({
+  api: {
+    monitors: {
+      editorDetail: vi.fn(),
+      editorCollectors: vi.fn(),
+      editorParamDefines: vi.fn()
+    }
+  }
+}));
+
 vi.mock('@/lib/monitor-editor/controller', () => ({
+  buildMonitorEditCacheKey: (monitorId: string) => `monitor-editor-edit:/monitor/${monitorId}:/collector`,
+  buildMonitorEditorCollectorsUrl: () => '/collector',
+  buildMonitorEditorMonitorUrl: (monitorId: string) => `/monitor/${monitorId}`,
   createMonitor: vi.fn(),
   detectMonitor: vi.fn(),
-  loadMonitorEditorDraft,
+  loadMonitorEditorDraftFromFacade,
   loadMonitorScrapeDraft: vi.fn(async () => ({
     scrapeParams: [],
     scrapeParamDefines: []
@@ -136,6 +155,7 @@ vi.mock('@/lib/monitor-editor/controller', () => ({
 }));
 
 vi.mock('@/lib/monitor-editor/navigation', () => ({
+  buildMonitorEditorCancelUrl: () => '/monitors',
   buildMonitorEditorReturnUrl: () => '/monitors?app=website'
 }));
 
@@ -170,29 +190,121 @@ vi.mock('@/lib/utils', () => ({
   cn: (...values: Array<string | undefined | null | false>) => values.filter(Boolean).join(' ')
 }));
 
+function renderMonitorEditPage(
+  initialRouteState: MonitorEditRouteState = {
+    returnContext: { returnTo: '/entities/7' }
+  }
+) {
+  return renderToStaticMarkup(<MonitorEditPage monitorId="42" initialRouteState={initialRouteState} />);
+}
+
 describe('MonitorEditPage', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams('returnTo=%2Fentities%2F7');
     mockState.lastLoad = null;
     mockState.routerPush.mockReset();
-    loadMonitorEditorDraft.mockClear().mockResolvedValue(mockState.draft);
+    loadMonitorEditorDraftFromFacade.mockClear().mockResolvedValue(mockState.draft);
   });
 
   it('renders the HertzBeat monitor form surface for the edit route', () => {
-    const html = renderToStaticMarkup(<MonitorEditPage params={Promise.resolve({ monitorId: '42' })} />);
+    const html = renderMonitorEditPage();
 
     expect(html).toContain('<form');
+    expect(html).toContain('data-cache-key="monitor-editor-edit:/monitor/42:/collector"');
+    expect(html).toContain('data-cache-settled-ttl="10000"');
+    expect(html).toContain('data-monitor-editor-app-source-contract="angular-route-context-hidden-field"');
+    expect(html).toContain('data-monitor-editor-static-host-position-contract="angular-before-name"');
+    expect(html).toContain('data-monitor-editor-field-order-contract="angular-monitor-form-sequence"');
+    expect(html).toContain('data-monitor-editor-detect-payload-contract="angular-monitor-collector-params-no-grafana"');
+    expect(html).toContain('data-monitor-editor-save-payload-contract="angular-monitor-collector-params-grafana"');
+    expect(html).toContain('data-monitor-editor-payload-param-merge-contract="angular-params-advanced-sdparams"');
+    expect(html).toContain('data-monitor-editor-payload-host-instance-contract="angular-host-param-as-instance"');
+    expect(html).toContain('data-monitor-editor-service-discovery-params="angular-nonstatic-only"');
+    expect(html).toContain('data-monitor-editor-ssl-port-notice="angular-info-notification"');
+    expect(html).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
+    expect(html).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
     expect(html).toContain('checkout');
-    expect(html).toContain('OK');
+    expect(html).toContain('确定');
   });
 
   it('loads the edit-monitor draft with the current monitor id', async () => {
-    renderToStaticMarkup(<MonitorEditPage params={Promise.resolve({ monitorId: '42' })} />);
+    renderMonitorEditPage();
 
     await mockState.lastLoad?.();
 
-    expect(loadMonitorEditorDraft).toHaveBeenCalledWith(expect.any(Function), 'edit', {
-      monitorId: '42'
-    });
+    expect(loadMonitorEditorDraftFromFacade).toHaveBeenCalledWith(
+      expect.objectContaining({
+        readMonitorDetail: expect.any(Function),
+        readCollectors: expect.any(Function),
+        readParamDefines: expect.any(Function)
+      }),
+      'edit',
+      {
+        monitorId: '42'
+      }
+    );
+  });
+
+  it('accepts typed return context from the edit route wrapper without changing the draft loader', async () => {
+    const initialRouteState: MonitorEditRouteState = {
+      returnContext: {
+        labels: 'team=platform',
+        entityId: '7',
+        entityName: 'Checkout',
+        timeRange: '1h',
+        returnTo: '/entities/7'
+      }
+    };
+    const html = renderMonitorEditPage(initialRouteState);
+
+    expect(html).toContain('data-cache-key="monitor-editor-edit:/monitor/42:/collector"');
+
+    await mockState.lastLoad?.();
+
+    expect(loadMonitorEditorDraftFromFacade).toHaveBeenCalledWith(
+      expect.objectContaining({
+        readMonitorDetail: expect.any(Function),
+        readCollectors: expect.any(Function),
+        readParamDefines: expect.any(Function)
+      }),
+      'edit',
+      {
+        monitorId: '42'
+      }
+    );
+  });
+
+  it('loads monitor editor draft through the monitor domain facade instead of a raw getter', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/edit/monitor-edit-page.tsx'), 'utf8');
+
+    expect(source).toContain('loadMonitorEditorDraftFromFacade(');
+    expect(source).toContain('readMonitorDetail: api.monitors.editorDetail');
+    expect(source).toContain('readCollectors: api.monitors.editorCollectors');
+    expect(source).toContain('readParamDefines: api.monitors.editorParamDefines');
+    expect(source).not.toContain("import { apiMessageGet } from '@/lib/api-client'");
+    expect(source).not.toContain('loadMonitorEditorDraft(apiMessageGet');
+  });
+
+  it('keeps edit load failures in the Angular-style monitor form route-state surface', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/edit/monitor-edit-page.tsx'), 'utf8');
+
+    expect(source).toContain('renderError={(message, retry) =>');
+    expect(source).toContain('data-monitor-editor-route-state="error"');
+    expect(source).toContain('data-monitor-editor-edit-error-state="angular-route-state"');
+    expect(source).toContain('data-monitor-editor-route-state-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-monitor-editor-app-source-contract="angular-route-context-hidden-field"');
+    expect(source).toContain('data-monitor-editor-static-host-position-contract="angular-before-name"');
+    expect(source).toContain('data-monitor-editor-field-order-contract="angular-monitor-form-sequence"');
+    expect(source).toContain('data-monitor-editor-detect-payload-contract="angular-monitor-collector-params-no-grafana"');
+    expect(source).toContain('data-monitor-editor-save-payload-contract="angular-monitor-collector-params-grafana"');
+    expect(source).toContain('data-monitor-editor-payload-param-merge-contract="angular-params-advanced-sdparams"');
+    expect(source).toContain('data-monitor-editor-payload-host-instance-contract="angular-host-param-as-instance"');
+    expect(source).toContain('data-monitor-editor-service-discovery-params="angular-nonstatic-only"');
+    expect(source).toContain('data-monitor-editor-ssl-port-notice="angular-info-notification"');
+    expect(source).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
+    expect(source).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(source).toContain("t('monitor.route-state.form.error.title')");
+    expect(source).toContain("t('monitor.route-state.form.error.copy')");
+    expect(source).toContain("t('monitor.route-state.action.retry')");
+    expect(source).toContain('data-monitor-editor-route-state-retry-owner="hertzbeat-ui-button"');
   });
 });
diff --git a/web-next/app/monitors/[monitorId]/edit/page.tsx b/web-next/app/monitors/[monitorId]/edit/page.tsx
index ad4ded7459..751d6afb7c 100644
--- a/web-next/app/monitors/[monitorId]/edit/page.tsx
+++ b/web-next/app/monitors/[monitorId]/edit/page.tsx
@@ -1,40 +1,17 @@
-'use client';
-
 import React from 'react';
-import { useCallback } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { MonitorEditorSurface } from '../../../../components/pages/monitor-editor-surface';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { apiMessageGet } from '@/lib/api-client';
-import { loadMonitorEditorDraft } from '@/lib/monitor-editor/controller';
-
-export default function MonitorEditPage({ params }: { params: Promise<{ monitorId: string }> }) {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const returnTo = searchParams.get('returnTo');
 
-  const load = useCallback(async () => {
-    const resolved = await params;
-    return loadMonitorEditorDraft(apiMessageGet, 'edit', { monitorId: resolved.monitorId });
-  }, [params]);
+import MonitorEditPage from './monitor-edit-page';
+import { readMonitorEditRouteState, type MonitorEditSearchParams } from '../../../../lib/monitor-editor/query-state';
 
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('monitor.editor.loading.edit')}>
-      {data => (
-        <MonitorEditorSurface
-          initial={data}
-          mode="edit"
-          returnContext={{
-            labels: searchParams.get('labels'),
-            pageIndex: searchParams.get('pageIndex'),
-            pageSize: searchParams.get('pageSize'),
-            entityId: searchParams.get('entityId'),
-            entityName: searchParams.get('entityName'),
-            returnTo
-          }}
-        />
-      )}
-    </ClientWorkbench>
-  );
+export default async function MonitorEditRoutePage({
+  params,
+  searchParams
+}: {
+  params: Promise<{ monitorId: string }>;
+  searchParams?: Promise<MonitorEditSearchParams>;
+}) {
+  const { monitorId } = await params;
+  const resolvedSearchParams = await searchParams;
+  const routeState = readMonitorEditRouteState(resolvedSearchParams);
+  return <MonitorEditPage monitorId={monitorId} initialRouteState={routeState} />;
 }
diff --git a/web-next/app/monitors/new/monitor-new-page.tsx b/web-next/app/monitors/new/monitor-new-page.tsx
new file mode 100644
index 0000000000..64bad90869
--- /dev/null
+++ b/web-next/app/monitors/new/monitor-new-page.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+import React, { useCallback } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { MonitorEditorSurface } from '../../../components/pages/monitor-editor-surface';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { api } from '@/lib/monitor-api-facade';
+import { buildMonitorEditorCollectorsUrl, buildMonitorEditorParamDefinesUrl, loadMonitorEditorDraftFromFacade } from '@/lib/monitor-editor/controller';
+import type { MonitorNewRouteState } from '@/lib/monitor-editor/query-state';
+
+const MONITOR_NEW_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_MONITOR_NEW_ROUTE_STATE: MonitorNewRouteState = {
+  app: 'website',
+  returnContext: {}
+};
+
+export default function MonitorNewPage({ initialRouteState }: { initialRouteState?: MonitorNewRouteState } = {}) {
+  const { t } = useI18n();
+  const monitorNewRouteState = initialRouteState ?? EMPTY_MONITOR_NEW_ROUTE_STATE;
+  const { app, returnContext } = monitorNewRouteState;
+  const monitorNewCollectorsUrl = React.useMemo(() => buildMonitorEditorCollectorsUrl(), []);
+  const monitorNewParamsUrl = React.useMemo(() => buildMonitorEditorParamDefinesUrl(app), [app]);
+  const monitorNewCacheKey = React.useMemo(
+    () => ['monitor-editor-new', monitorNewCollectorsUrl, monitorNewParamsUrl].join(':'),
+    [monitorNewCollectorsUrl, monitorNewParamsUrl]
+  );
+
+  const load = useCallback(
+    async () =>
+      loadMonitorEditorDraftFromFacade(
+        {
+          readCollectors: api.monitors.editorCollectors,
+          readParamDefines: api.monitors.editorParamDefines
+        },
+        'new',
+        { app }
+      ),
+    [app]
+  );
+
+  return (
+    <div
+      data-monitor-editor-app-source-contract="angular-route-context-hidden-field"
+      data-monitor-editor-static-host-position-contract="angular-before-name"
+      data-monitor-editor-field-order-contract="angular-monitor-form-sequence"
+      data-monitor-editor-detect-payload-contract="angular-monitor-collector-params-no-grafana"
+      data-monitor-editor-save-payload-contract="angular-monitor-collector-params-grafana"
+      data-monitor-editor-payload-param-merge-contract="angular-params-advanced-sdparams"
+      data-monitor-editor-payload-host-instance-contract="angular-host-param-as-instance"
+      data-monitor-editor-service-discovery-params="angular-nonstatic-only"
+      data-monitor-editor-ssl-port-notice="angular-info-notification"
+      data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"
+      data-monitor-editor-label-selector="angular-app-label-selector"
+    >
+      <ClientWorkbench
+        load={load}
+        loadingCopy={t('monitor.editor.loading.new')}
+        cacheKey={monitorNewCacheKey}
+        cacheSettledTtlMs={MONITOR_NEW_SETTLED_CACHE_TTL_MS}
+      >
+        {data => (
+          <MonitorEditorSurface
+            initial={data}
+            mode="new"
+            returnContext={returnContext}
+          />
+        )}
+      </ClientWorkbench>
+    </div>
+  );
+}
diff --git a/web-next/app/monitors/new/page.test.tsx b/web-next/app/monitors/new/page.test.tsx
index 4ca09c8e81..f90bf7fe33 100644
--- a/web-next/app/monitors/new/page.test.tsx
+++ b/web-next/app/monitors/new/page.test.tsx
@@ -1,13 +1,18 @@
 import React from 'react';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
-import MonitorNewPage from './page';
+import MonitorNewPage from './monitor-new-page';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import type { MonitorNewRouteState } from '../../../lib/monitor-editor/query-state';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   routerPush: vi.fn(),
+  redirect: vi.fn((target: string) => {
+    throw new Error(`redirect:${target}`);
+  }),
   draft: {
     monitor: {
       id: 0,
@@ -33,35 +38,40 @@ const mockState = vi.hoisted(() => ({
   }
 }));
 
-const loadMonitorEditorDraft = vi.hoisted(() => vi.fn(async () => mockState.draft));
+const loadMonitorEditorDraftFromFacade = vi.hoisted(() => vi.fn(async () => mockState.draft));
 
 vi.mock('next/navigation', () => ({
+  redirect: mockState.redirect,
   useRouter: () => ({
     push: mockState.routerPush
-  }),
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
+  })
 }));
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
-    locale: 'en-US'
+    t: createTranslatorMock({ locale: 'zh-CN' }),
+    locale: 'zh-CN'
   })
 }));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    cacheKey,
+    cacheSettledTtlMs
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.draft)}</div>;
+    return (
+      <div data-client-workbench="true" data-cache-key={cacheKey} data-cache-settled-ttl={cacheSettledTtlMs}>
+        {children(mockState.draft)}
+      </div>
+    );
   }
 }));
 
@@ -118,10 +128,21 @@ vi.mock('@/lib/api-client', () => ({
   apiMessagePut: vi.fn()
 }));
 
+vi.mock('@/lib/monitor-api-facade', () => ({
+  api: {
+    monitors: {
+      editorCollectors: vi.fn(),
+      editorParamDefines: vi.fn()
+    }
+  }
+}));
+
 vi.mock('@/lib/monitor-editor/controller', () => ({
+  buildMonitorEditorCollectorsUrl: () => '/collector',
+  buildMonitorEditorParamDefinesUrl: (app: string) => `/apps/${app}/params`,
   createMonitor: vi.fn(),
   detectMonitor: vi.fn(),
-  loadMonitorEditorDraft,
+  loadMonitorEditorDraftFromFacade,
   loadMonitorScrapeDraft: vi.fn(async () => ({
     scrapeParams: [],
     scrapeParamDefines: []
@@ -136,6 +157,7 @@ vi.mock('@/lib/monitor-editor/controller', () => ({
 }));
 
 vi.mock('@/lib/monitor-editor/navigation', () => ({
+  buildMonitorEditorCancelUrl: () => '/monitors',
   buildMonitorEditorReturnUrl: () => '/monitors?app=website'
 }));
 
@@ -170,29 +192,121 @@ vi.mock('@/lib/utils', () => ({
   cn: (...values: Array<string | undefined | null | false>) => values.filter(Boolean).join(' ')
 }));
 
+function renderMonitorNewPage(
+  initialRouteState: MonitorNewRouteState = {
+    app: 'website',
+    returnContext: { returnTo: '/entities/7' }
+  }
+) {
+  return renderToStaticMarkup(<MonitorNewPage initialRouteState={initialRouteState} />);
+}
+
 describe('MonitorNewPage', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams('app=website&returnTo=%2Fentities%2F7');
     mockState.lastLoad = null;
     mockState.routerPush.mockReset();
-    loadMonitorEditorDraft.mockClear().mockResolvedValue(mockState.draft);
+    mockState.redirect.mockClear();
+    loadMonitorEditorDraftFromFacade.mockClear().mockResolvedValue(mockState.draft);
   });
 
   it('renders the HertzBeat monitor form surface for the new route', () => {
-    const html = renderToStaticMarkup(<MonitorNewPage />);
+    const html = renderMonitorNewPage();
 
     expect(html).toContain('<form');
-    expect(html).toContain('New monitor');
-    expect(html).toContain('OK');
+    expect(html).toContain('data-cache-key="monitor-editor-new:/collector:/apps/website/params"');
+    expect(html).toContain('data-cache-settled-ttl="10000"');
+    expect(html).toContain('data-monitor-editor-app-source-contract="angular-route-context-hidden-field"');
+    expect(html).toContain('data-monitor-editor-static-host-position-contract="angular-before-name"');
+    expect(html).toContain('data-monitor-editor-field-order-contract="angular-monitor-form-sequence"');
+    expect(html).toContain('data-monitor-editor-detect-payload-contract="angular-monitor-collector-params-no-grafana"');
+    expect(html).toContain('data-monitor-editor-save-payload-contract="angular-monitor-collector-params-grafana"');
+    expect(html).toContain('data-monitor-editor-payload-param-merge-contract="angular-params-advanced-sdparams"');
+    expect(html).toContain('data-monitor-editor-payload-host-instance-contract="angular-host-param-as-instance"');
+    expect(html).toContain('data-monitor-editor-service-discovery-params="angular-nonstatic-only"');
+    expect(html).toContain('data-monitor-editor-ssl-port-notice="angular-info-notification"');
+    expect(html).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
+    expect(html).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(html).toContain('新建监控');
+    expect(html).toContain('确定');
   });
 
   it('loads the new-monitor draft with the current app context', async () => {
-    renderToStaticMarkup(<MonitorNewPage />);
+    renderMonitorNewPage();
+
+    await mockState.lastLoad?.();
+
+    expect(loadMonitorEditorDraftFromFacade).toHaveBeenCalledWith(
+      expect.objectContaining({
+        readCollectors: expect.any(Function),
+        readParamDefines: expect.any(Function)
+      }),
+      'new',
+      {
+        app: 'website'
+      }
+    );
+  });
+
+  it('uses the typed route state app when loading a new monitor from a handoff', async () => {
+    const initialRouteState: MonitorNewRouteState = {
+      app: 'prometheus',
+      returnContext: {
+        labels: 'team=platform',
+        entityId: '42',
+        entityName: 'Checkout Service',
+        timeRange: '1h',
+        returnTo: '/entities/42'
+      }
+    };
+    const html = renderMonitorNewPage(initialRouteState);
+
+    expect(html).toContain('data-cache-key="monitor-editor-new:/collector:/apps/prometheus/params"');
 
     await mockState.lastLoad?.();
 
-    expect(loadMonitorEditorDraft).toHaveBeenCalledWith(expect.any(Function), 'new', {
-      app: 'website'
-    });
+    expect(loadMonitorEditorDraftFromFacade).toHaveBeenCalledWith(
+      expect.objectContaining({
+        readCollectors: expect.any(Function),
+        readParamDefines: expect.any(Function)
+      }),
+      'new',
+      {
+        app: 'prometheus'
+      }
+    );
+  });
+
+  it('loads monitor editor draft through the monitor domain facade instead of a raw getter', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/new/monitor-new-page.tsx'), 'utf8');
+
+    expect(source).toContain('loadMonitorEditorDraftFromFacade(');
+    expect(source).toContain('readCollectors: api.monitors.editorCollectors');
+    expect(source).toContain('readParamDefines: api.monitors.editorParamDefines');
+    expect(source).not.toContain("import { apiMessageGet } from '@/lib/api-client'");
+    expect(source).not.toContain('loadMonitorEditorDraft(apiMessageGet');
+  });
+});
+
+describe('MonitorNewRoutePage', () => {
+  beforeEach(() => {
+    mockState.redirect.mockClear();
+  });
+
+  it('redirects missing app query to the Angular-style website monitor setup URL', async () => {
+    const { default: MonitorNewRoutePage } = await import('./page');
+
+    await expect(MonitorNewRoutePage({ searchParams: Promise.resolve({}) })).rejects.toThrow(
+      'redirect:/monitors/new?app=website'
+    );
+    expect(mockState.redirect).toHaveBeenCalledWith('/monitors/new?app=website');
+  });
+
+  it('does not redirect when the app query is present', async () => {
+    const { default: MonitorNewRoutePage } = await import('./page');
+    const element = await MonitorNewRoutePage({ searchParams: Promise.resolve({ app: 'prometheus' }) });
+    const html = renderToStaticMarkup(element);
+
+    expect(mockState.redirect).not.toHaveBeenCalled();
+    expect(html).toContain('data-cache-key="monitor-editor-new:/collector:/apps/prometheus/params"');
   });
 });
diff --git a/web-next/app/monitors/new/page.tsx b/web-next/app/monitors/new/page.tsx
index 2f346bead3..70d8c3dd43 100644
--- a/web-next/app/monitors/new/page.tsx
+++ b/web-next/app/monitors/new/page.tsx
@@ -1,38 +1,23 @@
-'use client';
-
 import React from 'react';
-import { useCallback } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { MonitorEditorSurface } from '../../../components/pages/monitor-editor-surface';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { apiMessageGet } from '@/lib/api-client';
-import { loadMonitorEditorDraft } from '@/lib/monitor-editor/controller';
-
-export default function MonitorNewPage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const app = searchParams.get('app') || 'website';
-  const returnTo = searchParams.get('returnTo');
+import { redirect } from 'next/navigation';
 
-  const load = useCallback(async () => loadMonitorEditorDraft(apiMessageGet, 'new', { app }), [app]);
+import MonitorNewPage from './monitor-new-page';
+import {
+  buildMonitorNewDefaultAppRedirectUrl,
+  hasMonitorNewAppParam,
+  readMonitorNewRouteState,
+  type MonitorNewSearchParams
+} from '../../../lib/monitor-editor/query-state';
 
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('monitor.editor.loading.new')}>
-      {data => (
-        <MonitorEditorSurface
-          initial={data}
-          mode="new"
-          returnContext={{
-            labels: searchParams.get('labels'),
-            pageIndex: searchParams.get('pageIndex'),
-            pageSize: searchParams.get('pageSize'),
-            entityId: searchParams.get('entityId'),
-            entityName: searchParams.get('entityName'),
-            returnTo
-          }}
-        />
-      )}
-    </ClientWorkbench>
-  );
+export default async function MonitorNewRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<MonitorNewSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  if (!hasMonitorNewAppParam(resolvedSearchParams)) {
+    redirect(buildMonitorNewDefaultAppRedirectUrl());
+  }
+  const routeState = readMonitorNewRouteState(resolvedSearchParams);
+  return <MonitorNewPage initialRouteState={routeState} />;
 }
diff --git a/web-next/components/pages/monitor-editor-surface.test.tsx b/web-next/components/pages/monitor-editor-surface.test.tsx
index fc2f56f3b4..9537e8f561 100644
--- a/web-next/components/pages/monitor-editor-surface.test.tsx
+++ b/web-next/components/pages/monitor-editor-surface.test.tsx
@@ -6,6 +6,10 @@ import { describe, expect, it, vi } from 'vitest';
 import { MonitorEditorSurface } from './monitor-editor-surface';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
+const i18nMockState = vi.hoisted(() => ({
+  locale: 'en-US' as 'en-US' | 'zh-CN'
+}));
+
 vi.mock('next/navigation', () => ({
   useRouter: () => ({
     push: vi.fn()
@@ -14,11 +18,48 @@ vi.mock('next/navigation', () => ({
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock(),
-    locale: 'en-US'
+    t: createTranslatorMock({ locale: i18nMockState.locale }),
+    locale: i18nMockState.locale
   })
 }));
 
+vi.mock('@uiw/react-codemirror', () => ({
+  default: ({ value, readOnly, extensions, height, minHeight, basicSetup, theme, onChange, ...props }: any) => (
+    <div
+      data-testid={props['data-testid']}
+      data-mocked-codemirror="true"
+      data-readonly={readOnly ? 'true' : 'false'}
+      data-height={height}
+      data-min-height={minHeight}
+      data-basic-setup={basicSetup ? 'true' : 'false'}
+      data-theme={theme ? 'custom-dark' : 'missing'}
+      data-extension-count={Array.isArray(extensions) ? String(extensions.length) : '0'}
+      onClick={() => onChange?.(`${value}\nnext`)}
+    >
+      {value}
+    </div>
+  )
+}));
+
+vi.mock('@codemirror/lang-yaml', () => ({ yaml: () => 'yaml-extension' }));
+vi.mock('@codemirror/lang-json', () => ({ json: () => 'json-extension' }));
+vi.mock('@codemirror/lang-html', () => ({ html: () => 'html-extension' }));
+vi.mock('@codemirror/lang-javascript', () => ({ javascript: () => 'javascript-extension' }));
+vi.mock('@codemirror/view', () => ({
+  EditorView: {
+    lineWrapping: 'line-wrapping-extension',
+    theme: () => 'theme-extension'
+  }
+}));
+vi.mock('@codemirror/state', () => ({
+  EditorState: {
+    readOnly: {
+      of: () => 'readonly-extension'
+    }
+  }
+}));
+vi.mock('@codemirror/theme-one-dark', () => ({ oneDark: 'one-dark-theme' }));
+
 vi.mock('@/components/ui/button', () => ({
   Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
   buttonVariants: () => ''
@@ -73,11 +114,18 @@ vi.mock('@/components/workbench/primitives', () => ({
 }));
 
 vi.mock('@/components/workbench/workbench-page', () => ({
-  WorkbenchPage: ({ kicker, title, subtitle, actions, main, side }: any) => (
+  WorkbenchPage: ({ kicker, title, subtitle, facts, actions, main, side }: any) => (
     <main>
       <div>{kicker}</div>
       <h1>{title}</h1>
       <p>{subtitle}</p>
+      <div>
+        {(facts || []).map((fact: any, index: number) => (
+          <div key={`${fact.label}-${index}`} data-monitor-editor-fact={fact.label} data-monitor-editor-fact-value={fact.value}>
+            {`${fact.label}:${fact.value}`}
+          </div>
+        ))}
+      </div>
       <div>{actions}</div>
       <div>{main}</div>
       <aside>{side}</aside>
@@ -86,7 +134,14 @@ vi.mock('@/components/workbench/workbench-page', () => ({
   RowList: ({ rows }: any) => (
     <div>
       {rows.map((row: any, index: number) => (
-        <div key={`${row.title}-${index}`}>{`${row.title}:${row.copy}`}</div>
+        <div
+          key={`${row.title}-${index}`}
+          data-monitor-editor-payload-row={row.title}
+          data-monitor-editor-payload-copy={row.copy}
+          data-monitor-editor-payload-meta={row.meta}
+        >
+          {`${row.title}:${row.copy}:${row.meta}`}
+        </div>
       ))}
     </div>
   )
@@ -98,12 +153,26 @@ vi.mock('@/lib/api-client', () => ({
   apiMessagePut: vi.fn()
 }));
 
+vi.mock('@/lib/monitor-api-facade', () => ({
+  api: {
+    monitors: {
+      create: vi.fn(),
+      detect: vi.fn(),
+      editorParamDefines: vi.fn(),
+      update: vi.fn()
+    }
+  }
+}));
+
 vi.mock('@/lib/monitor-editor/controller', () => ({
-  createMonitor: vi.fn(),
-  detectMonitor: vi.fn(),
-  loadMonitorScrapeDraft: vi.fn(),
+  applyMonitorHostNameAutofill: (draft: any) => draft,
+  createMonitorFromFacade: vi.fn(),
+  detectMonitorFromFacade: vi.fn(),
+  loadMonitorScrapeDraftFromFacade: vi.fn(async () => ({ scrapeParams: [], scrapeParamDefines: [] })),
+  shouldPreserveMonitorScrapeParamsForLoad: (previousScrape: string | null | undefined, nextScrape: string) =>
+    !previousScrape && nextScrape !== 'static',
   syncMonitorDependentDisplay: (draft: any) => draft,
-  updateMonitor: vi.fn(),
+  updateMonitorFromFacade: vi.fn(),
   updateMonitorEditorParam: (draft: any, kind: string, index: number, value: unknown) => ({
     ...draft,
     [kind]: draft[kind].map((row: any, rowIndex: number) => (rowIndex === index ? { ...row, paramValue: value } : row))
@@ -112,6 +181,7 @@ vi.mock('@/lib/monitor-editor/controller', () => ({
 }));
 
 vi.mock('@/lib/monitor-editor/navigation', () => ({
+  buildMonitorEditorCancelUrl: () => '/monitors',
   buildMonitorEditorReturnUrl: () => '/monitors?app=website'
 }));
 
@@ -180,6 +250,17 @@ describe('MonitorEditorSurface', () => {
 
     expect(html).toContain('Add label');
     expect(html).toContain('Add annotation');
+    expect(html).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-monitor-editor-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-cold-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-cold-label-selector-record-row="team:platform"');
+    expect(html).toContain('data-cold-label-selector-key-input="searchable-key"');
+    expect(html).toContain('data-cold-label-selector-value-input="searchable-value"');
+    expect(html).toContain('data-hz-ui="key-value-editor"');
+    expect(html).toContain('data-monitor-editor-key-value-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-hz-key-value-action="add"');
+    expect(html).toContain('data-hz-key-value-footer="action-row"');
+    expect(html).toContain('data-hz-key-value-action-layout="footer-command"');
     expect(html).toContain('<form');
     expect(html).toContain('team');
     expect(html).toContain('platform');
@@ -189,6 +270,216 @@ describe('MonitorEditorSurface', () => {
     expect(html).not.toContain('JSON object for monitor annotations.');
   });
 
+  it('renders monitor editor label and annotation actions in zh-CN', () => {
+    i18nMockState.locale = 'zh-CN';
+    try {
+      const html = renderToStaticMarkup(
+        <MonitorEditorSurface
+          initial={{
+            monitor: {
+              id: 42,
+              app: 'website',
+              name: 'checkout',
+              instance: 'example.com:80',
+              scrape: 'static',
+              scheduleType: 'interval',
+              intervals: 60,
+              status: 1,
+              labels: { team: 'platform' },
+              annotations: { owner: 'sre' }
+            } as any,
+            collector: '',
+            grafanaDashboard: { enabled: false },
+            params: [{ field: 'host', paramValue: 'example.com' }],
+            paramDefines: [{ field: 'host', type: 'text', name: 'Host', required: true }],
+            advancedParams: [],
+            advancedParamDefines: [],
+            scrapeParams: [],
+            scrapeParamDefines: [],
+            collectors: []
+          }}
+          mode="edit"
+          returnContext={{ returnTo: '/monitors?app=website' }}
+        />
+      );
+
+      expect(html).toContain('添加标签');
+      expect(html).toContain('添加注解');
+      expect(html).not.toContain('Add label');
+      expect(html).not.toContain('Add annotation');
+    } finally {
+      i18nMockState.locale = 'en-US';
+    }
+  });
+
+  it('renders the system default collector option instead of treating it as none', () => {
+    i18nMockState.locale = 'zh-CN';
+    try {
+      const html = renderToStaticMarkup(
+        <MonitorEditorSurface
+          initial={{
+            monitor: {
+              id: 42,
+              app: 'mysql',
+              name: 'mysql-prod',
+              instance: '127.0.0.1:3306',
+              scrape: 'static',
+              scheduleType: 'interval',
+              intervals: 10,
+              status: 2,
+              labels: {},
+              annotations: {}
+            } as any,
+            collector: '',
+            grafanaDashboard: { enabled: false },
+            params: [{ field: 'host', paramValue: '127.0.0.1' }],
+            paramDefines: [{ field: 'host', type: 'text', name: '目标Host', required: true }],
+            advancedParams: [],
+            advancedParamDefines: [],
+            scrapeParams: [],
+            scrapeParamDefines: [],
+            collectors: ['edge-a']
+          }}
+          mode="edit"
+          returnContext={{ returnTo: '/monitors?app=mysql' }}
+        />
+      );
+
+      expect(html).toContain('默认系统调度');
+      expect(html).toContain('data-monitor-editor-collector-selection="angular-collectors-selection-tags"');
+      expect(html).toContain('data-monitor-editor-collector-tags="angular-status-ip-mode-tags"');
+      expect(html).toContain('data-monitor-editor-collector-tag="system-default"');
+      expect(html).not.toContain('<option value="" selected="">无</option>');
+    } finally {
+      i18nMockState.locale = 'en-US';
+    }
+  });
+
+  it('preserves Angular collector option status, ip, and mode context', () => {
+    i18nMockState.locale = 'zh-CN';
+    try {
+      const html = renderToStaticMarkup(
+        <MonitorEditorSurface
+          initial={{
+            monitor: {
+              id: 42,
+              app: 'mysql',
+              name: 'mysql-prod',
+              instance: '127.0.0.1:3306',
+              scrape: 'static',
+              scheduleType: 'interval',
+              intervals: 10,
+              status: 2,
+              labels: {},
+              annotations: {}
+            } as any,
+            collector: 'edge-a',
+            grafanaDashboard: { enabled: false },
+            params: [{ field: 'host', paramValue: '127.0.0.1' }],
+            paramDefines: [{ field: 'host', type: 'text', name: '目标Host', required: true }],
+            advancedParams: [],
+            advancedParamDefines: [],
+            scrapeParams: [],
+            scrapeParamDefines: [],
+            collectors: [{ name: 'edge-a', ip: '10.0.0.12', status: 0, mode: 'private' }]
+          }}
+          mode="edit"
+          returnContext={{ returnTo: '/monitors?app=mysql' }}
+        />
+      );
+
+      expect(html).toContain('edge-a · 10.0.0.12');
+      expect(html).toContain('data-monitor-editor-collector-selection-owner="hertzbeat-ui-select"');
+      expect(html).toContain('data-monitor-editor-collector-tags="angular-status-ip-mode-tags"');
+      expect(html).toContain('data-monitor-editor-collector-tag="status"');
+      expect(html).toContain('data-monitor-editor-collector-tag="ip"');
+      expect(html).toContain('data-monitor-editor-collector-tag="mode"');
+      expect(html).toContain('10.0.0.12');
+    } finally {
+      i18nMockState.locale = 'en-US';
+    }
+  });
+
+  it('renders monitor editor as a linear Angular-style form instead of a split workbench', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'prometheus',
+            name: '',
+            instance: '',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'host', paramValue: '127.0.0.1' }],
+          paramDefines: [{ field: 'host', type: 'text', name: 'Host', required: true }],
+          advancedParams: [{ field: 'headers', paramValue: '{}' }],
+          advancedParamDefines: [{ field: 'headers', type: 'key-value', name: 'Headers' }],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="new"
+        returnContext={{ returnTo: '/monitors' }}
+      />
+    );
+
+    const baseIndex = html.indexOf('Basic settings');
+    const paramsIndex = html.indexOf('Runtime parameters');
+    const advancedIndex = html.indexOf('Advanced');
+    const runtimeIndex = html.indexOf('Collection schedule');
+    const labelsIndex = html.indexOf('Labels');
+    const annotationsIndex = html.indexOf('Annotations');
+    const descriptionIndex = html.indexOf('Description');
+    const actionsIndex = html.indexOf('data-monitor-editor-action-dock="bottom"');
+
+    expect(html).toContain('data-monitor-editor-layout="linear"');
+    expect(html).toContain('data-hz-ui="monitor-editor-form"');
+    expect(html).toContain('data-monitor-editor-form-owner="hertzbeat-ui-monitor-editor-form"');
+    expect(html).toContain('data-monitor-editor-linear-shell="true"');
+    expect(html).toContain('data-hz-ui="monitor-editor-header"');
+    expect(html).toContain('data-monitor-editor-linear-header="hertzbeat-ui-monitor-editor-header"');
+    expect(html).toContain('data-hz-ui="monitor-editor-field-grid"');
+    expect(html).toContain('data-monitor-editor-field-grid-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect((html.match(/data-hz-ui="monitor-editor-field-grid"/g) ?? []).length).toBe(3);
+    expect(html).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
+    expect(html).toContain('data-monitor-editor-advanced-visible-contract="angular-visible-param-only"');
+    expect(html).toContain('data-monitor-editor-advanced-collapse-state="collapsed"');
+    expect(html).toContain('data-monitor-editor-advanced-toggle="angular-dashed-collapse-trigger"');
+    expect(html).toContain('data-monitor-editor-detect-cron-validation="angular-detect-skips-cron-format"');
+    expect(html).toContain('data-monitor-editor-cron-required="angular-required-before-detect-save"');
+    expect(html).not.toContain('data-monitor-editor-advanced-fields="expanded"');
+    expect(baseIndex).toBeGreaterThan(-1);
+    expect(baseIndex).toBeLessThan(paramsIndex);
+    expect(paramsIndex).toBeLessThan(advancedIndex);
+    expect(advancedIndex).toBeLessThan(runtimeIndex);
+    expect(runtimeIndex).toBeLessThan(labelsIndex);
+    expect(labelsIndex).toBeLessThan(annotationsIndex);
+    expect(annotationsIndex).toBeLessThan(descriptionIndex);
+    expect(descriptionIndex).toBeLessThan(actionsIndex);
+    expect(html).not.toContain('data-monitor-editor-fact=');
+    expect(html).not.toContain('data-monitor-editor-payload-row=');
+    expect(html).not.toContain('Monitor Configuration');
+    expect(source).not.toContain('WorkbenchPage');
+    expect(source).not.toContain('RowList');
+    expect(source).toContain('HzMonitorEditorForm');
+    expect(source).toContain('HzMonitorEditorHeader');
+    expect(source).toContain('HzMonitorEditorFieldGrid');
+    expect(source).toContain('validateBeforeSubmit(nextDraft, { validateCronFormat: false })');
+    expect(source).not.toContain('className="mx-auto w-full max-w-[980px] space-y-3"');
+    expect(source).not.toContain('className="grid gap-3 md:grid-cols-2"');
+    expect(source).not.toContain('<div className="space-y-3" data-monitor-editor-linear-shell="true">');
+    expect(source).not.toContain('<header\n          className="border-b border-[var(--hz-ui-line-soft)] px-3 pb-3 pt-2"');
+  });
+
   it('keeps the embedded editor action posture inside one form', () => {
     const html = renderToStaticMarkup(
       <MonitorEditorSurface
@@ -232,7 +523,490 @@ describe('MonitorEditorSurface', () => {
     expect(html).toContain('data-monitor-editor-cancel-action="true"');
   });
 
-  it('uses the shared cold number stepper for interval schedules instead of native number input', () => {
+  it('uses the shared mutation bar for monitor editor actions and feedback instead of page-local action chrome', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'website',
+            name: 'checkout',
+            instance: 'example.com:80',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'host', paramValue: 'example.com' }],
+          paramDefines: [{ field: 'host', type: 'text', name: 'Host', required: true }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=website' }}
+      />
+    );
+
+    expect(html).toContain('data-hz-ui="mutation-bar"');
+    expect(html).toContain('data-hz-ui="monitor-editor-action-bar"');
+    expect(html).toContain('data-hz-ui="monitor-editor-section"');
+    expect(html).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-monitor-editor-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-monitor-editor-key-value="annotations"');
+    expect(html).toContain('data-monitor-editor-section-owner="hertzbeat-ui-editor-section"');
+    expect(html).toContain('data-monitor-editor-field-owner="hertzbeat-ui-field"');
+    expect(html).toContain('data-hz-ui="field"');
+    expect(html).toContain('data-hz-ui="input"');
+    expect(html).toContain('data-monitor-editor-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-monitor-editor-app-source="angular-route-context-hidden-field"');
+    expect(html).toContain('data-monitor-editor-hidden-app="route-or-detail-context"');
+    expect(html).not.toContain('data-monitor-editor-input="app"');
+    expect(html).toContain('data-monitor-editor-field-order="angular-monitor-form-sequence"');
+    expect(html).toContain('data-monitor-editor-detect-payload-contract="angular-monitor-collector-params-no-grafana"');
+    expect(html).toContain('data-monitor-editor-save-payload-contract="angular-monitor-collector-params-grafana"');
+    expect(html).toContain('data-monitor-editor-payload-param-merge="angular-params-advanced-sdparams"');
+    expect(html).toContain('data-monitor-editor-payload-host-instance="angular-host-param-as-instance"');
+    expect(html).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
+    expect(html).toContain('data-monitor-editor-advanced-visible-contract="angular-visible-param-only"');
+    expect(html).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-monitor-editor-static-host-position="angular-before-name"');
+    expect(html).toContain('data-monitor-editor-static-host-field="angular-before-name"');
+    expect(html).toContain('data-monitor-editor-input="name"');
+    expect(html).toContain('data-monitor-param-input="host"');
+    expect(html.indexOf('data-monitor-editor-static-host-field="angular-before-name"')).toBeLessThan(
+      html.indexOf('data-monitor-editor-input="name"')
+    );
+    expect(html).toContain('data-monitor-editor-host-name-autofill-contract="angular-new-host-change"');
+    expect(html).not.toContain('data-monitor-editor-host-name-autofill-target="monitor-name"');
+    expect(html).toContain('data-hz-ui="select"');
+    expect(html).toContain('data-monitor-editor-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-monitor-editor-select="scrape"');
+    expect(html).toContain('data-monitor-editor-select="collector"');
+    expect(html).toContain('data-monitor-editor-select="schedule-type"');
+    expect(html).toContain('data-hz-mutation-status="clean"');
+    expect(html).toContain('data-hz-mutation-issue-count="0"');
+    expect(html).toContain('data-monitor-editor-action-dock="bottom"');
+    expect(html).toContain('data-monitor-editor-mutation-bar="true"');
+    expect(html).toContain('data-monitor-editor-action-bar-owner="hertzbeat-ui-monitor-editor-action-bar"');
+    expect(html).toContain('data-monitor-editor-action-bar-layout="centered-footer"');
+    expect(html).toContain('data-monitor-editor-action="detect"');
+    expect(html).toContain('data-monitor-editor-action="submit"');
+    expect(html).toContain('data-monitor-editor-action="cancel"');
+    expect(html).toContain('data-monitor-editor-action-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-monitor-editor-detect-action="true"');
+    expect(html).toContain('data-monitor-editor-submit-action="true"');
+    expect(html).toContain('data-monitor-editor-detect-busy-label="Available Detecting"');
+    expect(html).toContain('data-monitor-editor-submit-busy-label="Loading..."');
+    expect(html).toContain('data-monitor-editor-save-return="safe-return-context-or-angular-app-list"');
+    expect(html).toContain('data-monitor-editor-save-return-target="/monitors?app=website"');
+    expect(html).toContain('data-monitor-editor-save-notification-contract="angular-success-before-return"');
+    expect(html).toContain('data-monitor-editor-cancel-action="true"');
+    expect(html).toContain('data-monitor-editor-cancel-return="safe-return-context-or-list-root"');
+    expect(html).toContain('data-monitor-editor-cancel-return-target="/monitors"');
+    expect(html.indexOf('data-monitor-editor-section-owner="hertzbeat-ui-editor-section"')).toBeLessThan(
+      html.indexOf('data-monitor-editor-action-dock="bottom"')
+    );
+    expect(source).toContain("from '@hertzbeat/ui'");
+    expect(source).toContain('HzMonitorEditorActionBar');
+    expect(source).toContain('buildMonitorEditorCancelUrl');
+    expect(source).not.toContain('HzMutationBar');
+    expect(source).not.toContain('data-monitor-editor-action-owner="hertzbeat-ui-button"');
+    expect(source).toContain('HzInlineFeedback');
+    expect(source).toContain('variant="embedded"');
+    expect(source).toContain('data-monitor-editor-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain("useState<'idle' | 'detecting' | 'saving'>('idle')");
+    expect(source).toContain("useState<'validation' | 'api' | null>(null)");
+    expect(source).toContain("useState<'detect' | 'save' | null>(null)");
+    expect(source).toContain("actionPhase === 'detecting' ? t('monitor.spinning-tip.detecting') : t('common.loading')");
+    expect(source).toContain('data-monitor-editor-feedback="busy"');
+    expect(source).toContain('data-monitor-editor-busy-contract="angular-spin-tip"');
+    expect(source).toContain('data-monitor-editor-busy-phase={actionPhase}');
+    expect(source).toContain('data-monitor-editor-validation-contract');
+    expect(source).toContain('data-monitor-editor-api-error-contract');
+    expect(source).toContain('data-monitor-editor-blocked-action');
+    expect(source).toContain('data-monitor-editor-blocked-before-api');
+    expect(source).toContain("setActionErrorSource('validation')");
+    expect(source).toContain("setActionErrorSource('api')");
+    expect(source).toContain("setActionErrorAction('detect')");
+    expect(source).toContain("setActionErrorAction('save')");
+    expect(source).toContain("setActionError(error instanceof Error ? error.message : t('monitor.detect.failed'))");
+    expect(source).toContain(
+      "setActionError(error instanceof Error ? error.message : mode === 'new' ? t('monitor.new.failed') : t('monitor.edit.failed'))"
+    );
+    expect(source).toContain('data-monitor-editor-detect-busy-label');
+    expect(source).toContain('data-monitor-editor-submit-busy-label');
+    expect(source).toContain('saveReturnUrl');
+    expect(source).toContain('cancelReturnUrl');
+    expect(source).toContain('data-monitor-editor-save-return');
+    expect(source).toContain('data-monitor-editor-save-return-target');
+    expect(source).toContain('data-monitor-editor-save-notification-contract');
+    expect(source).toContain('data-monitor-editor-cancel-return-target');
+    expect(source).toContain('data-monitor-editor-feedback="error"');
+    expect(source).toContain('data-monitor-editor-feedback="success"');
+    expect(source).not.toContain('className="border-0"');
+    expect(source).toContain('HzMonitorEditorSection');
+    expect(source).toContain('HzKeyValueEditor');
+    expect(source).toContain('LabelRecordInput');
+    expect(source).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(source).toContain('data-monitor-editor-label-selector-owner="cold-label-selector"');
+    expect(source).not.toContain('data-monitor-editor-key-value="labels"');
+    expect(source).toContain('data-monitor-editor-key-value="annotations"');
+    expect(source).toContain('HzField');
+    expect(source).toContain('applyMonitorHostNameAutofill');
+    expect(source).toContain("field: defines[index]?.field");
+    expect(source).toContain('HzInput');
+    expect(source).toContain('data-monitor-editor-input-owner="hertzbeat-ui-input"');
+    expect(source).toContain('data-monitor-editor-app-source="angular-route-context-hidden-field"');
+    expect(source).toContain('data-monitor-editor-hidden-app="route-or-detail-context"');
+    expect(source).toContain('data-monitor-editor-field-order="angular-monitor-form-sequence"');
+    expect(source).toContain('data-monitor-editor-detect-payload-contract="angular-monitor-collector-params-no-grafana"');
+    expect(source).toContain('data-monitor-editor-save-payload-contract="angular-monitor-collector-params-grafana"');
+    expect(source).toContain('data-monitor-editor-payload-param-merge="angular-params-advanced-sdparams"');
+    expect(source).toContain('data-monitor-editor-payload-host-instance="angular-host-param-as-instance"');
+    expect(source).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
+    expect(source).toContain('data-monitor-editor-advanced-visible-contract="angular-visible-param-only"');
+    expect(source).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(source).toContain('data-monitor-editor-advanced-toggle="angular-dashed-collapse-trigger"');
+    expect(source).toContain('setAdvancedOpen(open => !open)');
+    expect(source).toContain('data-monitor-editor-static-host-position="angular-before-name"');
+    expect(source).toContain('data-monitor-editor-static-host-field="angular-before-name"');
+    expect(source).toContain('data-monitor-editor-scrape-param-order="angular-after-scrape-before-name"');
+    expect(source).toContain('data-monitor-editor-runtime-order="angular-after-advanced-before-labels"');
+    expect(source).toContain("define.field === 'host'");
+    expect(source).not.toContain('data-monitor-editor-input="app"');
+    expect(source).toContain('data-monitor-editor-input="name"');
+    expect(source).toContain('data-monitor-editor-input="cron-expression"');
+    expect(source).toContain('data-monitor-editor-cron-required="angular-required-before-detect-save"');
+    expect(source).toContain('required');
+    expect(source).toContain('data-monitor-param-input={define.field}');
+    expect(source).toContain('HzSelect');
+    expect(source).toContain('data-monitor-editor-select-owner="hertzbeat-ui-select"');
+    expect(source).toContain('data-monitor-editor-select="scrape"');
+    expect(source).toContain('data-monitor-editor-select="collector"');
+    expect(source).toContain('data-monitor-editor-select="schedule-type"');
+    expect(source).toContain('data-monitor-param-radio={define.field}');
+    expect(source).toContain('data-monitor-param-radio-contract="angular-nz-radio-group-button-solid"');
+    expect(source).not.toContain('SurfaceSection');
+    expect(source).not.toContain('function KeyValueEditor');
+    expect(source).not.toContain("import { Button } from '@/components/ui/button'");
+    expect(source).not.toContain("import { Input } from '@/components/ui/input'");
+    expect(source).not.toContain("import { Select } from '@/components/ui/select'");
+    expect(source).not.toContain('const fieldLabelClassName');
+    expect(source).not.toContain('text-emerald-300');
+    expect(source).not.toContain('text-rose-300');
+  });
+
+  it('keeps scrape loading feedback owned by @hertzbeat/ui instead of page-local text chrome', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+
+    expect(source).toContain('HzLoadingState');
+    expect(source).toContain('data-monitor-editor-scrape-loading-owner="hertzbeat-ui-loading-state"');
+    expect(source).not.toContain('text-sm text-[var(--ops-text-secondary)]');
+  });
+
+  it('loads dynamic scrape params through the monitor domain facade instead of a raw getter', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+
+    expect(source).toContain('loadMonitorScrapeDraftFromFacade(api.monitors.editorParamDefines');
+    expect(source).toContain("if (scrape === 'static')");
+    expect(source).toContain('data-monitor-editor-service-discovery-params="angular-nonstatic-only"');
+    expect(source).toContain('data-monitor-editor-scrape-reload-contract="angular-reset-on-user-scrape-change"');
+    expect(source).toContain('shouldPreserveMonitorScrapeParamsForLoad(previousScrape, scrape)');
+    expect(source).toContain('shouldPreserveExistingParams ? scrapeParamsRef.current : undefined');
+    expect(source).not.toContain('loadMonitorScrapeDraft(apiMessageGet');
+    expect(source).not.toContain("import { apiMessageGet");
+  });
+
+  it('keeps Angular ssl port auto-change info notification behavior wired in the shared editor', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'api',
+            name: 'checkout',
+            instance: 'example.com:80',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [
+            { field: 'ssl', paramValue: false },
+            { field: 'port', paramValue: 80 }
+          ],
+          paramDefines: [
+            { field: 'ssl', type: 'boolean', name: 'SSL' },
+            { field: 'port', type: 'number', name: 'Port' }
+          ],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="new"
+      />
+    );
+
+    expect(source).toContain('resolveMonitorEditorParamChangeNotice(draft, kind, index, value)');
+    expect(source).toContain('setActionMessage(t(noticeKey))');
+    expect(html).toContain('data-monitor-editor-ssl-port-notice="angular-info-notification"');
+    expect(html).toContain('data-monitor-param-switch="ssl"');
+    expect(html).toContain('data-monitor-param-boolean-contract="angular-nz-switch"');
+    expect(html).toContain('data-monitor-param-number-stepper="port"');
+  });
+
+  it('submits monitor editor mutations through the monitor domain facade instead of raw API writers', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+
+    expect(source).toContain('detectMonitorFromFacade(api.monitors.detect');
+    expect(source).toContain('createMonitorFromFacade(api.monitors.create');
+    expect(source).toContain('updateMonitorFromFacade(api.monitors.update');
+    expect(source).not.toContain("import { apiMessagePost");
+    expect(source).not.toContain("import { apiMessagePut");
+    expect(source).not.toContain('detectMonitor(apiMessagePost');
+    expect(source).not.toContain('createMonitor(apiMessagePost');
+    expect(source).not.toContain('updateMonitor(apiMessagePut');
+  });
+
+  it('keeps Angular monitor form field order for service-discovery params and runtime controls', () => {
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'website',
+            name: 'checkout',
+            instance: 'example.com:80',
+            scrape: 'http_sd',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [
+            { field: 'host', paramValue: 'example.com' },
+            { field: 'port', paramValue: 80 }
+          ],
+          paramDefines: [
+            { field: 'host', type: 'text', name: 'Host', required: true },
+            { field: 'port', type: 'number', name: 'Port', required: true }
+          ],
+          advancedParams: [{ field: 'timeout', paramValue: 3 }],
+          advancedParamDefines: [{ field: 'timeout', type: 'number', name: 'Timeout', required: false }],
+          scrapeParams: [{ field: 'sd_url', paramValue: 'https://sd.local/targets' }],
+          scrapeParamDefines: [{ field: 'sd_url', type: 'text', name: 'HTTP SD URL', required: true }],
+          collectors: ['collector-a']
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=website' }}
+      />
+    );
+
+    const scrapeIndex = html.indexOf('data-monitor-editor-select="scrape"');
+    const sdParamIndex = html.indexOf('data-monitor-editor-scrape-param-order="angular-after-scrape-before-name"');
+    const nameIndex = html.indexOf('data-monitor-editor-input="name"');
+    const mainParamIndex = html.indexOf('data-monitor-param-number-stepper="port"');
+    const advancedIndex = html.indexOf('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"', mainParamIndex);
+    const advancedToggleIndex = html.indexOf('data-monitor-editor-advanced-toggle="angular-dashed-collapse-trigger"', advancedIndex);
+    const runtimeIndex = html.indexOf('data-monitor-editor-runtime-order="angular-after-advanced-before-labels"');
+    const labelIndex = html.indexOf('data-monitor-editor-label-selector-owner="cold-label-selector"', runtimeIndex);
+
+    expect(html).toContain('data-monitor-editor-field-order="angular-monitor-form-sequence"');
+    expect(scrapeIndex).toBeGreaterThan(-1);
+    expect(sdParamIndex).toBeGreaterThan(scrapeIndex);
+    expect(nameIndex).toBeGreaterThan(sdParamIndex);
+    expect(mainParamIndex).toBeGreaterThan(nameIndex);
+    expect(advancedIndex).toBeGreaterThan(mainParamIndex);
+    expect(advancedToggleIndex).toBeGreaterThan(advancedIndex);
+    expect(runtimeIndex).toBeGreaterThan(advancedIndex);
+    expect(labelIndex).toBeGreaterThan(runtimeIndex);
+    expect(html).toContain('data-monitor-editor-advanced-collapse-state="collapsed"');
+    expect(html).not.toContain('data-monitor-param-number-stepper="timeout"');
+    expect(html).not.toContain('data-monitor-editor-advanced-fields="expanded"');
+  });
+
+  it('hides the Angular advanced collapse when every advanced param is dependency-hidden', () => {
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'website',
+            name: 'checkout',
+            instance: 'example.com:80',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'host', paramValue: 'example.com' }],
+          paramDefines: [{ field: 'host', type: 'text', name: 'Host', required: true }],
+          advancedParams: [{ field: 'token', paramValue: '', display: false }],
+          advancedParamDefines: [{ field: 'token', type: 'text', name: 'Token', required: true, depend: { ssl: [true] } }],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+      />
+    );
+
+    expect(html).toContain('data-monitor-editor-advanced-visible-contract="angular-visible-param-only"');
+    expect(html).not.toContain('data-monitor-editor-advanced-collapse-state=');
+    expect(html).not.toContain('data-monitor-editor-advanced-toggle="angular-dashed-collapse-trigger"');
+    expect(html).not.toContain('Token');
+  });
+
+  it('hides service-discovery params for static scrape to match Angular nonstatic-only rendering', () => {
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'website',
+            name: 'checkout',
+            instance: 'example.com:80',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'host', paramValue: 'example.com' }],
+          paramDefines: [{ field: 'host', type: 'text', name: 'Host', required: true }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [{ field: 'sd_url', paramValue: 'https://sd.local/targets' }],
+          scrapeParamDefines: [{ field: 'sd_url', type: 'text', name: 'HTTP SD URL', required: true }],
+          collectors: []
+        }}
+        mode="edit"
+      />
+    );
+
+    expect(html).toContain('data-monitor-editor-service-discovery-params="angular-nonstatic-only"');
+    expect(html).toContain('data-monitor-editor-scrape-reload-contract="angular-reset-on-user-scrape-change"');
+    expect(html).not.toContain('data-monitor-editor-scrape-param-order="angular-after-scrape-before-name"');
+    expect(html).toContain('data-monitor-editor-static-host-field="angular-before-name"');
+  });
+
+  it('centers bottom monitor editor actions without repeating the editor title or ready badge', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'prometheus',
+            name: '',
+            instance: '',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'host', paramValue: '127.0.0.1' }],
+          paramDefines: [{ field: 'host', type: 'text', name: 'Host', required: true }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="new"
+        returnContext={{ returnTo: '/monitors' }}
+      />
+    );
+    const dockHtml = html.slice(html.indexOf('data-monitor-editor-action-dock="bottom"'));
+
+    expect(dockHtml).toContain('data-hz-mutation-summary="hidden"');
+    expect(dockHtml).toContain('data-hz-mutation-action-align="center"');
+    expect(dockHtml).toContain('data-hz-ui="monitor-editor-action-bar"');
+    expect(dockHtml).toContain('data-monitor-editor-action-bar-owner="hertzbeat-ui-monitor-editor-action-bar"');
+    expect(dockHtml).toContain('data-monitor-editor-action="detect"');
+    expect(dockHtml).toContain('data-monitor-editor-action="submit"');
+    expect(dockHtml).toContain('data-monitor-editor-action="cancel"');
+    expect(dockHtml).toContain('data-monitor-editor-detect-action="true"');
+    expect(dockHtml).toContain('data-monitor-editor-submit-action="true"');
+    expect(dockHtml).toContain('Detect');
+    expect(dockHtml).toContain('OK');
+    expect(dockHtml).toContain('Cancel');
+    expect(dockHtml).not.toContain('New monitor');
+    expect(dockHtml).not.toContain('data-hz-mutation-status-label=');
+    expect(dockHtml).not.toContain('ready');
+    expect(source).toContain('HzMonitorEditorActionBar');
+    expect(source).toContain('summaryVisible={false}');
+    expect(source).toContain('actionAlign="center"');
+  });
+
+  it('keeps missing app and name values editable without synthetic shell summaries', () => {
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: '',
+            name: '',
+            instance: '',
+            scrape: '',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'host', paramValue: '' }],
+          paramDefines: [{ field: 'host', type: 'text', name: 'Host', required: true }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors' }}
+      />
+    );
+
+    expect(html).toContain('data-monitor-editor-layout="linear"');
+    expect(html).toContain('value=""');
+    expect(html).not.toContain('data-monitor-editor-fact=');
+    expect(html).not.toContain('data-monitor-editor-payload-row=');
+    expect(html).not.toContain('App:-');
+  });
+
+  it('uses the shared HertzBeat UI number stepper for interval schedules instead of native number input', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
     const html = renderToStaticMarkup(
       <MonitorEditorSurface
@@ -264,17 +1038,67 @@ describe('MonitorEditorSurface', () => {
       />
     );
 
-    expect(html).toContain('data-monitor-interval-stepper="cold-number-stepper"');
-    expect(html).toContain('data-cold-number-stepper-owner="cold-number-stepper"');
-    expect(html).toContain('data-cold-number-stepper-input="true"');
+    expect(html).toContain('data-monitor-interval-stepper="hertzbeat-ui-number-stepper"');
+    expect(html).toContain('data-monitor-interval-stepper-contract="angular-min-step-max-by-app"');
+    expect(html).toContain('data-monitor-interval-stepper-min="10"');
+    expect(html).toContain('data-monitor-interval-stepper-max="604800"');
+    expect(html).toContain('data-monitor-interval-stepper-step="10"');
+    expect(html).toContain('min="10"');
+    expect(html).toContain('max="604800"');
+    expect(html).toContain('step="10"');
+    expect(html).toContain('data-monitor-interval-stepper-unit="common.time.unit.second"');
+    expect(html).toContain('data-monitor-editor-number-stepper-owner="hertzbeat-ui-number-stepper"');
+    expect(html).toContain('data-hz-ui="number-stepper"');
+    expect(html).toContain('data-hz-number-stepper-input="true"');
     expect(html).toContain('name="intervals"');
     expect(html).not.toContain('type="number"');
-    expect(source).toContain("from '../ui/number-stepper'");
-    expect(source).toContain('data-monitor-interval-stepper="cold-number-stepper"');
+    expect(source).toContain('HzNumberStepper');
+    expect(source).not.toContain("from '../ui/number-stepper'");
+    expect(source).toContain('data-monitor-interval-stepper="hertzbeat-ui-number-stepper"');
+    expect(source).not.toContain('data-cold-number-stepper-owner');
     expect(source).not.toContain('type="number"');
   });
 
-  it('uses the shared cold textarea for monitor descriptions instead of the old editor row', () => {
+  it('uses Angular push interval stepper bounds for push monitors', () => {
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'push',
+            name: 'push-input',
+            instance: 'push-input',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 1,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [],
+          paramDefines: [],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=push' }}
+      />
+    );
+
+    expect(html).toContain('data-monitor-interval-stepper-contract="angular-min-step-max-by-app"');
+    expect(html).toContain('data-monitor-interval-stepper-min="1"');
+    expect(html).toContain('data-monitor-interval-stepper-max="604800"');
+    expect(html).toContain('data-monitor-interval-stepper-step="1"');
+    expect(html).toContain('min="1"');
+    expect(html).toContain('step="1"');
+  });
+
+  it('uses the shared HertzBeat UI textarea for monitor descriptions instead of the old editor row', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
     const html = renderToStaticMarkup(
       <MonitorEditorSurface
@@ -307,12 +1131,22 @@ describe('MonitorEditorSurface', () => {
       />
     );
 
-    expect(source).toContain("import { Textarea } from '@/components/ui/textarea';");
-    expect(source).toContain('data-monitor-description-textarea="cold-textarea"');
+    expect(source).toContain('HzTextarea');
+    expect(source).toContain('data-monitor-description-textarea="hertzbeat-ui-textarea"');
+    expect(source).toContain('data-monitor-editor-textarea-owner="hertzbeat-ui-textarea"');
+    expect(source).toContain('data-monitor-description-textarea-limit="angular-textarea-limit-100"');
+    expect(source).toContain('maxCharacterCount={100}');
+    expect(source).not.toContain("import { Textarea } from '@/components/ui/textarea';");
     expect(source).not.toContain("import { EditorRow } from '../observability/editor-rows';");
     expect(source).not.toContain('const textAreaClassName =');
-    expect(html).toContain('data-monitor-description-textarea="cold-textarea"');
-    expect(html).toContain('data-cold-textarea-owner="cold-textarea"');
+    expect(html).toContain('data-monitor-description-textarea="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-monitor-editor-textarea-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-monitor-description-textarea-limit="angular-textarea-limit-100"');
+    expect(html).toContain('data-hz-ui="textarea"');
+    expect(html).toContain('data-hz-ui="textarea-count"');
+    expect(html).toContain('data-hz-textarea-count-max="100"');
+    expect(html).toContain('data-hz-textarea-count-value="24/100"');
+    expect(html).not.toContain('data-cold-textarea-owner="cold-textarea"');
     expect(html).toContain('Primary endpoint monitor');
   });
 
@@ -355,14 +1189,29 @@ describe('MonitorEditorSurface', () => {
     );
 
     expect(html).toContain('data-monitor-param-number-stepper="port"');
-    expect(html).toContain('data-cold-number-stepper-owner="cold-number-stepper"');
-    expect(html).toContain('data-cold-number-stepper-input="true"');
+    expect(html).toContain('data-monitor-param-number-contract="angular-nz-input-number--1000-65535-step-1"');
+    expect(html).toContain('data-monitor-param-number-min="-1000"');
+    expect(html).toContain('data-monitor-param-number-max="65535"');
+    expect(html).toContain('data-monitor-param-number-step="1"');
+    expect(html).toContain('data-monitor-editor-number-stepper-owner="hertzbeat-ui-number-stepper"');
+    expect(html).toContain('data-hz-ui="number-stepper"');
+    expect(html).toContain('data-hz-number-stepper-input="true"');
+    expect(html).toContain('min="-1000"');
+    expect(html).toContain('max="65535"');
+    expect(html).toContain('step="1"');
     expect(html).not.toContain('type="number"');
+    expect(source).toContain('HzNumberStepper');
     expect(source).toContain('data-monitor-param-number-stepper={define.field}');
+    expect(source).toContain('data-monitor-param-number-contract="angular-nz-input-number--1000-65535-step-1"');
+    expect(source).toContain('min={-1000}');
+    expect(source).toContain('max={65535}');
+    expect(source).toContain('step={1}');
+    expect(source).not.toContain("from '../ui/number-stepper'");
+    expect(source).not.toContain('data-cold-number-stepper-owner');
     expect(source).not.toContain("type={define.type === 'number' ? 'number' : 'text'}");
   });
 
-  it('uses the shared cold checkbox for boolean monitor parameters instead of page-local checkbox chrome', () => {
+  it('renders Angular boolean param definitions as shared switches instead of checkboxes', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
     const html = renderToStaticMarkup(
       <MonitorEditorSurface
@@ -394,17 +1243,25 @@ describe('MonitorEditorSurface', () => {
       />
     );
 
-    expect(html).toContain('data-monitor-param-checkbox="ssl"');
-    expect(html).toContain('data-cold-checkbox-owner="cold-checkbox"');
-    expect(html).toContain('data-cold-checkbox-control="native-hidden"');
-    expect(html).toContain('data-cold-checkbox-box="indicator"');
-    expect(html).toContain('data-cold-checkbox-label="true"');
-    expect(source).toContain("from '../ui/checkbox'");
-    expect(source).toContain('data-monitor-param-checkbox={define.field}');
+    expect(html).toContain('data-monitor-param-switch="ssl"');
+    expect(html).toContain('data-monitor-param-boolean-contract="angular-nz-switch"');
+    expect(html).toContain('data-monitor-param-field="ssl"');
+    expect(html).toContain('data-monitor-editor-switch-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-hz-ui="switch"');
+    expect(html).toContain('data-hz-switch-control="button"');
+    expect(html).toContain('data-hz-switch-checked="true"');
+    expect(html).toContain('data-hz-switch-label="true"');
+    expect(source).toContain('HzSwitch');
+    expect(source).not.toContain("from '../ui/checkbox'");
+    expect(source).not.toContain('data-cold-checkbox-owner');
+    expect(source).toContain('data-monitor-param-switch={define.field}');
+    expect(source).toContain('data-monitor-param-boolean-contract="angular-nz-switch"');
+    expect(source).toContain('data-monitor-param-field={define.field}');
+    expect(source).not.toContain('data-monitor-param-checkbox={define.field}');
     expect(source).not.toContain('<input type="checkbox" checked={Boolean(field.paramValue)}');
   });
 
-  it('uses the shared cold checkbox for the Grafana dashboard enable switch', () => {
+  it('uses the shared HertzBeat UI checkbox for the Grafana dashboard enable switch', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
     const html = renderToStaticMarkup(
       <MonitorEditorSurface
@@ -436,12 +1293,16 @@ describe('MonitorEditorSurface', () => {
       />
     );
 
-    expect(html).toContain('data-monitor-grafana-enabled-checkbox="cold-checkbox"');
-    expect(html).toContain('data-cold-checkbox-owner="cold-checkbox"');
-    expect(html).toContain('data-cold-checkbox-control="native-hidden"');
-    expect(html).toContain('data-cold-checkbox-box="indicator"');
-    expect(html).toContain('data-cold-checkbox-label="true"');
-    expect(source).toContain('data-monitor-grafana-enabled-checkbox="cold-checkbox"');
+    expect(html).toContain('data-monitor-grafana-enabled-checkbox="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-monitor-editor-checkbox="grafana-dashboard-enabled"');
+    expect(html).toContain('data-monitor-editor-checkbox-owner="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-hz-ui="checkbox"');
+    expect(html).toContain('data-hz-checkbox-control="native-hidden"');
+    expect(html).toContain('data-hz-checkbox-box="indicator"');
+    expect(html).toContain('data-hz-checkbox-label="true"');
+    expect(source).toContain('data-monitor-grafana-enabled-checkbox="hertzbeat-ui-checkbox"');
+    expect(source).toContain('data-monitor-editor-checkbox="grafana-dashboard-enabled"');
+    expect(source).not.toContain('data-cold-checkbox-owner');
     expect(source).not.toContain('type="checkbox"\n                    checked={Boolean(draft.grafanaDashboard.enabled)}');
   });
 
@@ -480,17 +1341,222 @@ describe('MonitorEditorSurface', () => {
       />
     );
 
+    expect(html).toContain('data-monitor-editor-code-editor-owner="hertzbeat-ui-code-editor"');
+    expect(html).toContain('data-monitor-editor-code-editor="grafana-dashboard-template"');
+    expect(html).toContain('data-hz-ui="code-editor-frame"');
+    expect(html).toContain('data-hz-code-editor-body="true"');
+    expect(html).toContain('data-hz-code-editor-runtime="codemirror"');
+    expect(html).toContain('data-hz-code-editor-language="json"');
+    expect(html).toContain('data-hz-code-editor-license="codemirror-mit"');
     expect(html).toContain('data-monitor-grafana-code-editor="dashboard-template"');
-    expect(html).toContain('data-cold-code-editor="codemirror"');
-    expect(html).toContain('data-cold-code-editor-language="json"');
+    expect(html).toContain('data-mocked-codemirror="true"');
+    expect(html).toContain('data-min-height="120px"');
     expect(html).toContain('name="grafana_dashboard_template"');
     expect(html).toContain('{&quot;panels&quot;:[]}');
-    expect(source).toContain("from '../ui/cold-code-editor'");
+    expect(source).not.toContain("from '../ui/cold-code-editor'");
+    expect(source).toContain('HzCodeEditor');
+    expect(source).toContain('data-monitor-editor-code-editor-owner="hertzbeat-ui-code-editor"');
+    expect(source).toContain('data-monitor-editor-code-editor="grafana-dashboard-template"');
     expect(source).toContain('data-monitor-grafana-code-editor="dashboard-template"');
     expect(source).not.toContain('grafanaDashboard.enabled ? <EditorRow');
   });
 
-  it('uses the shared CodeMirror editor for structured monitor parameter values', () => {
+  it('keeps the Angular Grafana JSON template upload wired through shared file input', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'prometheus',
+            name: 'prometheus',
+            instance: 'prometheus:9090',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: {
+            enabled: true,
+            template: '{"panels":[]}'
+          },
+          params: [{ field: 'host', paramValue: 'prometheus' }],
+          paramDefines: [{ field: 'host', type: 'text', name: 'Host', required: true }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=prometheus' }}
+      />
+    );
+
+    expect(html).toContain('data-monitor-grafana-template-input-contract="angular-json-template-upload"');
+    expect(html).toContain('data-monitor-grafana-template-upload="angular-json-template-upload"');
+    expect(html).toContain('data-monitor-grafana-template-upload-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-monitor-grafana-template-upload-input="json-template"');
+    expect(html).toContain('data-monitor-grafana-template-upload-trigger-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-monitor-grafana-template-upload-trigger="json-template"');
+    expect(html).toContain('data-hz-ui="file-input"');
+    expect(html).toContain('accept=".json,application/json"');
+    expect(html).toContain('Upload Grafana Template');
+    expect(source).toContain('HzFileInput');
+    expect(source).toContain('async function handleGrafanaTemplateUpload');
+    expect(source).toContain('file.text()');
+    expect(source).toContain('setGrafanaTemplateText(template)');
+    expect(source).toContain('template');
+    expect(source).not.toContain('data-cold-file-input-owner');
+  });
+
+  it('renders Angular password param definitions as password inputs instead of exposing secrets as text', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'mysql',
+            name: 'mysql-prod',
+            instance: '127.0.0.1:3306',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: {
+            enabled: false
+          },
+          params: [{ field: 'password', paramValue: 'secret' }],
+          paramDefines: [{ field: 'password', type: 'password', name: 'Password', required: true }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=mysql' }}
+      />
+    );
+
+    expect(html).toContain('data-monitor-param-password-input="password"');
+    expect(html).toContain('data-monitor-param-password-contract="angular-app-multi-func-password"');
+    expect(html).toContain('data-monitor-param-input="password"');
+    expect(html).toContain('data-monitor-param-field="password"');
+    expect(html).toContain('data-monitor-editor-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('type="password"');
+    expect(source).toContain("if (define.type === 'password')");
+    expect(source).toContain('data-monitor-param-password-contract="angular-app-multi-func-password"');
+  });
+
+  it('renders Angular textarea param definitions as multi-line shared textareas instead of single-line inputs', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'website',
+            name: 'website-prod',
+            instance: 'https://example.com',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: {
+            enabled: false
+          },
+          params: [{ field: 'body', paramValue: 'line one\nline two' }],
+          paramDefines: [{ field: 'body', type: 'textarea', name: 'Body', required: false }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=website' }}
+      />
+    );
+
+    expect(html).toContain('data-monitor-param-textarea="body"');
+    expect(html).toContain('data-monitor-param-textarea-contract="angular-nz-input-textarea-rows-8"');
+    expect(html).toContain('data-monitor-param-field="body"');
+    expect(html).toContain('data-monitor-editor-textarea-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-hz-ui="textarea"');
+    expect(html).toContain('data-hz-textarea-height="tall"');
+    expect(html).not.toContain('data-monitor-param-input="body"');
+    expect(source).toContain("if (define.type === 'textarea')");
+    expect(source).toContain('data-monitor-param-textarea-contract="angular-nz-input-textarea-rows-8"');
+  });
+
+  it('renders Angular radio param definitions as shared button groups instead of select menus', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'website',
+            name: 'checkout',
+            instance: 'example.com:443',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'authType', paramValue: 'basic' }],
+          paramDefines: [
+            {
+              field: 'authType',
+              type: 'radio',
+              name: 'Auth Type',
+              options: [
+                { label: 'Basic', value: 'basic' },
+                { label: 'Bearer', value: 'bearer' }
+              ]
+            }
+          ],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=website' }}
+      />
+    );
+
+    expect(html).toContain('data-monitor-param-radio="authType"');
+    expect(html).toContain('data-monitor-param-radio-contract="angular-nz-radio-group-button-solid"');
+    expect(html).toContain('data-monitor-param-field="authType"');
+    expect(html).toContain('data-monitor-editor-radio-owner="hertzbeat-ui-radio-button-group"');
+    expect(html).toContain('data-hz-ui="radio-button-group"');
+    expect(html).toContain('data-hz-radio-button-option="basic"');
+    expect(html).toContain('data-hz-radio-button-option="bearer"');
+    expect(html).toContain('data-hz-radio-button-checked="true"');
+    expect(html).not.toContain('data-monitor-param-select="authType"');
+    expect(source).toContain('HzRadioButtonGroup');
+    expect(source).toContain('data-monitor-param-radio-contract="angular-nz-radio-group-button-solid"');
+    expect(source).not.toContain('data-monitor-param-select={define.field}');
+  });
+
+  it('renders monitor key-value and array params as ordinary shared text controls instead of code editors', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
     const html = renderToStaticMarkup(
       <MonitorEditorSurface
@@ -514,7 +1580,7 @@ describe('MonitorEditorSurface', () => {
             { field: 'targets', paramValue: '[\n  "checkout",\n  "orders"\n]' }
           ],
           paramDefines: [
-            { field: 'headers', type: 'key-value', name: 'Headers' },
+            { field: 'headers', type: 'key-value', name: 'Headers', keyAlias: 'Header Name', valueAlias: 'Header Value' },
             { field: 'targets', type: 'array', name: 'Targets' }
           ],
           advancedParams: [],
@@ -528,16 +1594,128 @@ describe('MonitorEditorSurface', () => {
       />
     );
 
-    expect(html).toContain('data-monitor-param-code-editor="key-value"');
-    expect(html).toContain('data-monitor-param-code-editor="array"');
-    expect(html).toContain('data-monitor-param-field="headers"');
-    expect(html).toContain('data-monitor-param-field="targets"');
-    expect(html).toContain('data-cold-code-editor="codemirror"');
-    expect(html).toContain('data-cold-code-editor-language="json"');
-    expect(html).toContain('&quot;Authorization&quot;');
+    expect(html).toContain('data-monitor-param-key-value-editor="headers"');
+    expect(html).toContain('data-monitor-param-array-input="targets"');
+    expect(html).toContain('data-hz-ui="key-value-editor"');
+    expect(html).toContain('data-hz-key-value-input="key"');
+    expect(html).toContain('data-hz-key-value-input="value"');
+    expect(html).toContain('placeholder="Header Name"');
+    expect(html).toContain('placeholder="Header Value"');
+    expect(html).toContain('value="Authorization"');
+    expect(html).toContain('value="Bearer token"');
     expect(html).toContain('&quot;checkout&quot;');
-    expect(source).toContain('data-monitor-param-code-editor={define.type}');
-    expect(source).toContain('data-monitor-param-field={define.field}');
-    expect(source).toContain('onChange={nextValue => onChange(nextValue)}');
+    expect(html).not.toContain('data-monitor-param-code-editor="key-value"');
+    expect(html).not.toContain('data-monitor-param-code-editor="array"');
+    expect(html).not.toContain('data-mocked-codemirror="true"');
+    expect(source).toContain('HzCodeEditor');
+    expect(source).toContain('data-monitor-grafana-code-editor="dashboard-template"');
+    expect(source).toContain('data-monitor-param-key-value-editor={define.field}');
+    expect(source).toContain('data-monitor-param-array-input={define.field}');
+    expect(source).not.toContain('data-monitor-param-code-editor={define.type}');
+  });
+
+  it('renders Angular labels params as configurable key/value rows instead of a plain text input', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'custom',
+            name: 'custom monitor',
+            instance: 'custom:8080',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'labels', paramValue: { team: 'platform', env: 'prod' } }],
+          paramDefines: [{ field: 'labels', type: 'labels', name: 'Labels' }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=custom' }}
+      />
+    );
+
+    expect(html).toContain('data-monitor-param-labels-editor="labels"');
+    expect(html).toContain('data-monitor-param-labels-contract="angular-app-configurable-field-key-value"');
+    expect(html).toContain('data-monitor-param-field="labels"');
+    expect(html).toContain('data-monitor-editor-labels-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-monitor-param-labels-input="key"');
+    expect(html).toContain('data-monitor-param-labels-input="value"');
+    expect(html).toContain('data-hz-ui="key-value-editor"');
+    expect(html).toContain('value="team"');
+    expect(html).toContain('value="platform"');
+    expect(html).toContain('value="env"');
+    expect(html).toContain('value="prod"');
+    expect(html).not.toContain('data-monitor-param-input="labels"');
+    expect(source).toContain("if (define.type === 'labels')");
+    expect(source).toContain('data-monitor-param-labels-editor={define.field}');
+    expect(source).toContain('data-monitor-param-labels-contract="angular-app-configurable-field-key-value"');
+    expect(source).toContain("keyInputProps={{ 'data-monitor-param-labels-input': 'key' }}");
+    expect(source).not.toContain("define.type === 'labels' ? 'text'");
+  });
+
+  it('renders Angular metrics-field params as three-column configurable rows instead of a plain text input', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorEditorSurface
+        initial={{
+          monitor: {
+            id: 42,
+            app: 'custom',
+            name: 'custom monitor',
+            instance: 'custom:8080',
+            scrape: 'static',
+            scheduleType: 'interval',
+            intervals: 60,
+            status: 1,
+            labels: {},
+            annotations: {}
+          } as any,
+          collector: '',
+          grafanaDashboard: { enabled: false },
+          params: [{ field: 'metrics', paramValue: [{ field: 'usage', unit: '%', type: 'number' }] }],
+          paramDefines: [{ field: 'metrics', type: 'metrics-field', name: 'Metrics' }],
+          advancedParams: [],
+          advancedParamDefines: [],
+          scrapeParams: [],
+          scrapeParamDefines: [],
+          collectors: []
+        }}
+        mode="edit"
+        returnContext={{ returnTo: '/monitors?app=custom' }}
+      />
+    );
+
+    expect(html).toContain('data-monitor-param-metrics-field-editor="metrics"');
+    expect(html).toContain('data-monitor-param-metrics-field-contract="angular-app-configurable-field-field-unit-type"');
+    expect(html).toContain('data-monitor-param-field="metrics"');
+    expect(html).toContain('data-monitor-editor-metrics-field-owner="hertzbeat-ui-configurable-field-editor"');
+    expect(html).toContain('data-monitor-param-metrics-field-input="field"');
+    expect(html).toContain('data-monitor-param-metrics-field-input="unit"');
+    expect(html).toContain('data-monitor-param-metrics-field-input="type"');
+    expect(html).toContain('data-hz-ui="configurable-field-editor"');
+    expect(html).toContain('placeholder="Field"');
+    expect(html).toContain('placeholder="Unit"');
+    expect(html).toContain('placeholder="Type"');
+    expect(html).toContain('value="usage"');
+    expect(html).toContain('value="%"');
+    expect(html).toContain('value="number"');
+    expect(html).not.toContain('data-monitor-param-input="metrics"');
+    expect(source).toContain("if (define.type === 'metrics-field')");
+    expect(source).toContain('HzConfigurableFieldEditor');
+    expect(source).toContain('data-monitor-param-metrics-field-editor={define.field}');
+    expect(source).toContain('data-monitor-param-metrics-field-contract="angular-app-configurable-field-field-unit-type"');
+    expect(source).not.toContain("define.type === 'metrics-field' ? 'text'");
   });
 });
diff --git a/web-next/components/pages/monitor-editor-surface.tsx b/web-next/components/pages/monitor-editor-surface.tsx
index 604c44646f..d966da70ef 100644
--- a/web-next/components/pages/monitor-editor-surface.tsx
+++ b/web-next/components/pages/monitor-editor-surface.tsx
@@ -2,40 +2,171 @@
 
 import React, { useEffect, useRef, useState } from 'react';
 import { useRouter } from 'next/navigation';
+import { ChevronDown } from 'lucide-react';
+import { HzButton, HzCheckbox, HzCodeEditor, HzConfigurableFieldEditor, HzField, HzFileInput, HzInlineFeedback, HzInput, HzKeyValueEditor, HzLoadingState, HzMonitorEditorActionBar, HzMonitorEditorFieldGrid, HzMonitorEditorForm, HzMonitorEditorHeader, HzMonitorEditorSection, HzNumberStepper, HzRadioButtonGroup, HzSelect, HzSwitch, HzTextarea, type HzMutationStatus } from '@hertzbeat/ui';
 import { useI18n } from '@/components/providers/i18n-provider';
-import { Button } from '@/components/ui/button';
-import { Checkbox } from '../ui/checkbox';
-import { Input } from '@/components/ui/input';
-import { Select } from '@/components/ui/select';
-import { Textarea } from '@/components/ui/textarea';
-import { ColdCodeEditor } from '../ui/cold-code-editor';
-import { NumberStepper } from '../ui/number-stepper';
-import { SurfaceSection } from '@/components/workbench/primitives';
-import { RowList, WorkbenchPage } from '@/components/workbench/workbench-page';
-import { apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
+import { api } from '@/lib/monitor-api-facade';
 import {
-  createMonitor,
-  detectMonitor,
-  loadMonitorScrapeDraft,
+  createMonitorFromFacade,
+  detectMonitorFromFacade,
+  applyMonitorHostNameAutofill,
+  loadMonitorScrapeDraftFromFacade,
+  resolveMonitorEditorParamChangeNotice,
+  shouldPreserveMonitorScrapeParamsForLoad,
   syncMonitorDependentDisplay,
+  type MonitorCollectorOption,
   type MonitorEditorDraft,
   type MonitorEditorMode,
-  updateMonitor,
+  updateMonitorFromFacade,
   updateMonitorEditorParam,
   validateMonitorEditorDraft
 } from '@/lib/monitor-editor/controller';
-import { buildMonitorEditorReturnUrl, type MonitorEditorReturnContext } from '@/lib/monitor-editor/navigation';
+import { buildMonitorEditorCancelUrl, buildMonitorEditorReturnUrl, type MonitorEditorReturnContext } from '@/lib/monitor-editor/navigation';
 import { resolveLocalizedText } from '@/lib/monitor-editor/localized-text';
 import type { KeyValueDraft } from '@/lib/entity-editor/draft-utils';
 import { fromKeyValueDraft, toKeyValueDraft } from '@/lib/entity-editor/draft-utils';
-import { ensureKeyValueRows, removeRowAt, updateRowAt } from '@/lib/entity-editor/editor-state';
+import { LabelRecordInput } from '../ui/label-record-input';
 
-const fieldLabelClassName = 'text-[10px] uppercase tracking-[0.22em] text-[var(--ops-text-tertiary)]';
+function parseMonitorKeyValueRows(value: unknown): KeyValueDraft[] {
+  if (Array.isArray(value)) {
+    const rows = value.map(item => {
+      if (item && typeof item === 'object') {
+        const record = item as Record<string, unknown>;
+        return {
+          key: String(record.key ?? ''),
+          value: String(record.value ?? '')
+        };
+      }
+      return { key: String(item ?? ''), value: '' };
+    });
+    return rows.length > 0 ? rows : [{ key: '', value: '' }];
+  }
+
+  if (value && typeof value === 'object') {
+    return toKeyValueDraft(
+      Object.fromEntries(
+        Object.entries(value as Record<string, unknown>).map(([key, entryValue]) => [key, String(entryValue ?? '')])
+      )
+    );
+  }
+
+  if (typeof value !== 'string') {
+    return [{ key: '', value: '' }];
+  }
+
+  const trimmed = value.trim();
+  if (!trimmed || trimmed === '""') {
+    return [{ key: '', value: '' }];
+  }
+
+  try {
+    const parsed = JSON.parse(trimmed);
+    if (Array.isArray(parsed)) {
+      return parseMonitorKeyValueRows(parsed);
+    }
+    if (parsed && typeof parsed === 'object') {
+      return parseMonitorKeyValueRows(parsed);
+    }
+    if (typeof parsed === 'string') {
+      return parsed.trim() ? [{ key: '', value: parsed }] : [{ key: '', value: '' }];
+    }
+  } catch {
+    // Keep non-JSON legacy values visible instead of silently dropping operator input.
+  }
+
+  return [{ key: '', value: trimmed }];
+}
+
+function stringifyMonitorKeyValueRows(rows: KeyValueDraft[]) {
+  return JSON.stringify(fromKeyValueDraft(rows));
+}
+
+function parseMonitorConfigurableRows(value: unknown, keys: string[]): Record<string, string>[] {
+  if (Array.isArray(value)) {
+    const rows = value.map(item => {
+      if (item && typeof item === 'object') {
+        const record = item as Record<string, unknown>;
+        return Object.fromEntries(keys.map(key => [key, String(record[key] ?? '')]));
+      }
+      return Object.fromEntries(keys.map((key, index) => [key, index === 0 ? String(item ?? '') : '']));
+    });
+    return rows.length > 0 ? rows : [{}];
+  }
+
+  if (value && typeof value === 'object') {
+    const record = value as Record<string, unknown>;
+    return [Object.fromEntries(keys.map(key => [key, String(record[key] ?? '')]))];
+  }
+
+  return [{}];
+}
+
+function stringifyMonitorLabelSelectorValue(labels?: Record<string, unknown>) {
+  return Object.entries(labels || {})
+    .filter(([key]) => key.trim())
+    .map(([key, value]) => {
+      const normalizedKey = key.trim();
+      const normalizedValue = String(value ?? '').trim();
+      return normalizedValue ? `${normalizedKey}:${normalizedValue}` : normalizedKey;
+    })
+    .join(', ');
+}
+
+function parseMonitorLabelSelectorValue(value: string) {
+  return value
+    .split(',')
+    .map(item => item.trim())
+    .filter(Boolean)
+    .reduce<Record<string, string>>((acc, item) => {
+      const [rawKey, ...rawValueParts] = item.split(':');
+      const key = rawKey.trim();
+      if (!key) return acc;
+      const labelValue = rawValueParts.join(':').trim();
+      acc[key] = labelValue || key;
+      return acc;
+    }, {});
+}
+
+function normalizeCollectorOption(option: MonitorCollectorOption | string): MonitorCollectorOption | null {
+  if (typeof option === 'string') {
+    return option.trim() ? { name: option.trim() } : null;
+  }
+  const name = option.name?.trim();
+  return name ? { ...option, name } : null;
+}
+
+function isCollectorOnline(option: MonitorCollectorOption) {
+  if (typeof option.online === 'boolean') return option.online;
+  const status = String(option.status ?? '').toLowerCase();
+  return status === '0' || status === 'online' || status === 'true';
+}
+
+function collectorModeLabel(mode: string | null | undefined, t: (key: string) => string) {
+  return mode === 'private' ? t('collector.mode.private') : t('collector.mode.public');
+}
+
+function collectorStatusLabel(option: MonitorCollectorOption, t: (key: string) => string) {
+  return isCollectorOnline(option) ? t('monitor.collector.status.online') : t('monitor.collector.status.offline');
+}
+
+function formatCollectorOptionLabel(option: MonitorCollectorOption, t: (key: string) => string) {
+  return [option.name, option.ip, collectorStatusLabel(option, t), collectorModeLabel(option.mode, t)].filter(Boolean).join(' · ');
+}
+
+function resolveMonitorIntervalStepperBounds(app?: string | null) {
+  const fastInterval = app === 'push';
+  return {
+    min: fastInterval ? 1 : 10,
+    step: fastInterval ? 1 : 10,
+    max: 604800
+  };
+}
 
 function MonitorParamField({
   field,
   define,
   locale,
+  t,
   onChange
 }: {
   field: { paramValue?: unknown };
@@ -45,79 +176,207 @@ function MonitorParamField({
     type?: string;
     placeholder?: string | Record<string, string>;
     options?: Array<{ label?: string | Record<string, string>; value?: string }>;
+    keyAlias?: string;
+    valueAlias?: string;
   };
   locale: string;
+  t: (key: string) => string;
   onChange: (value: unknown) => void;
 }) {
   const label = resolveLocalizedText(define.name, locale, define.field);
   const placeholder = resolveLocalizedText(define.placeholder, locale, '');
   if (define.type === 'boolean') {
     return (
-      <div className="grid gap-2">
-        <span className={fieldLabelClassName}>{label}</span>
-        <Checkbox
-          data-monitor-param-checkbox={define.field}
+      <HzField as="div" label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzSwitch
+          data-monitor-param-switch={define.field}
+          data-monitor-param-boolean-contract="angular-nz-switch"
+          data-monitor-param-field={define.field}
+          data-monitor-editor-switch-owner="hertzbeat-ui-switch"
           checked={Boolean(field.paramValue)}
-          onChange={e => onChange(e.target.checked)}
+          onCheckedChange={checked => onChange(checked)}
+          aria-label={label}
           label={label}
         />
-      </div>
+      </HzField>
     );
   }
   if (define.type === 'radio' && (define.options || []).length > 0) {
+    const radioOptions = (define.options || []).map(option => ({
+      value: option.value || '',
+      label: resolveLocalizedText(option.label, locale, option.value || '')
+    }));
+    return (
+      <HzField as="div" label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzRadioButtonGroup
+          name={define.field}
+          value={String(field.paramValue ?? '')}
+          options={radioOptions}
+          data-monitor-param-radio={define.field}
+          data-monitor-param-radio-contract="angular-nz-radio-group-button-solid"
+          data-monitor-param-field={define.field}
+          data-monitor-editor-radio-owner="hertzbeat-ui-radio-button-group"
+          aria-label={label}
+          onChange={value => onChange(value)}
+        />
+      </HzField>
+    );
+  }
+  if (define.type === 'key-value') {
+    return (
+      <HzField as="div" label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzKeyValueEditor
+          data-monitor-param-key-value-editor={define.field}
+          data-monitor-param-field={define.field}
+          data-monitor-editor-key-value-owner="hertzbeat-ui-key-value-editor"
+          rows={parseMonitorKeyValueRows(field.paramValue)}
+          onChange={rows => onChange(stringifyMonitorKeyValueRows(rows))}
+          addLabel={t('common.add')}
+          removeLabel={t('common.remove')}
+          keyPlaceholder={define.keyAlias || t('monitor.editor.labels.key')}
+          valuePlaceholder={define.valueAlias || t('monitor.editor.labels.value')}
+        />
+      </HzField>
+    );
+  }
+  if (define.type === 'labels') {
+    return (
+      <HzField as="div" label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzKeyValueEditor
+          data-monitor-param-labels-editor={define.field}
+          data-monitor-param-labels-contract="angular-app-configurable-field-key-value"
+          data-monitor-param-field={define.field}
+          data-monitor-editor-labels-owner="hertzbeat-ui-key-value-editor"
+          rows={parseMonitorKeyValueRows(field.paramValue)}
+          onChange={rows => onChange(stringifyMonitorKeyValueRows(rows))}
+          addLabel={t('common.add')}
+          removeLabel={t('common.remove')}
+          keyPlaceholder={define.keyAlias || t('monitor.editor.labels.key')}
+          valuePlaceholder={define.valueAlias || t('monitor.editor.labels.value')}
+          keyInputProps={{ 'data-monitor-param-labels-input': 'key' }}
+          valueInputProps={{ 'data-monitor-param-labels-input': 'value' }}
+        />
+      </HzField>
+    );
+  }
+  if (define.type === 'metrics-field') {
+    return (
+      <HzField as="div" label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzConfigurableFieldEditor
+          rows={parseMonitorConfigurableRows(field.paramValue, ['field', 'unit', 'type'])}
+          columns={[
+            {
+              key: 'field',
+              placeholder: 'Field',
+              inputProps: { 'data-monitor-param-metrics-field-input': 'field' } as React.ComponentProps<typeof HzConfigurableFieldEditor>['columns'][number]['inputProps']
+            },
+            {
+              key: 'unit',
+              placeholder: 'Unit',
+              className: 'minmax(50px,90px)',
+              inputProps: { 'data-monitor-param-metrics-field-input': 'unit' } as React.ComponentProps<typeof HzConfigurableFieldEditor>['columns'][number]['inputProps']
+            },
+            {
+              key: 'type',
+              placeholder: 'Type',
+              inputProps: { 'data-monitor-param-metrics-field-input': 'type' } as React.ComponentProps<typeof HzConfigurableFieldEditor>['columns'][number]['inputProps']
+            }
+          ]}
+          onChange={rows => onChange(rows)}
+          addLabel={t('common.add')}
+          removeLabel={t('common.remove')}
+          data-monitor-param-metrics-field-editor={define.field}
+          data-monitor-param-metrics-field-contract="angular-app-configurable-field-field-unit-type"
+          data-monitor-param-field={define.field}
+          data-monitor-editor-metrics-field-owner="hertzbeat-ui-configurable-field-editor"
+        />
+      </HzField>
+    );
+  }
+  if (define.type === 'array') {
+    return (
+      <HzField label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzInput
+          type="text"
+          data-monitor-param-array-input={define.field}
+          data-monitor-param-input={define.field}
+          data-monitor-param-field={define.field}
+          data-monitor-editor-input-owner="hertzbeat-ui-input"
+          value={String(field.paramValue ?? '')}
+          placeholder={placeholder}
+          onChange={e => onChange(e.target.value)}
+        />
+      </HzField>
+    );
+  }
+  if (define.type === 'password') {
     return (
-      <label className="grid gap-2">
-        <span className={fieldLabelClassName}>{label}</span>
-        <Select value={String(field.paramValue ?? '')} onChange={e => onChange(e.target.value)}>
-          {(define.options || []).map(option => (
-            <option key={option.value} value={option.value}>
-              {resolveLocalizedText(option.label, locale, option.value || '')}
-            </option>
-          ))}
-        </Select>
-      </label>
+      <HzField label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzInput
+          type="password"
+          data-monitor-param-password-input={define.field}
+          data-monitor-param-password-contract="angular-app-multi-func-password"
+          data-monitor-param-input={define.field}
+          data-monitor-param-field={define.field}
+          data-monitor-editor-input-owner="hertzbeat-ui-input"
+          value={String(field.paramValue ?? '')}
+          placeholder={placeholder}
+          onChange={e => onChange(e.target.value)}
+        />
+      </HzField>
     );
   }
-  if (define.type === 'key-value' || define.type === 'array') {
+  if (define.type === 'textarea') {
     return (
-      <label className="grid gap-2">
-        <span className={fieldLabelClassName}>{label}</span>
-        <ColdCodeEditor
-          data-monitor-param-code-editor={define.type}
+      <HzField label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzTextarea
+          height="tall"
+          data-monitor-param-textarea={define.field}
+          data-monitor-param-textarea-contract="angular-nz-input-textarea-rows-8"
           data-monitor-param-field={define.field}
+          data-monitor-editor-textarea-owner="hertzbeat-ui-textarea"
           value={String(field.paramValue ?? '')}
-          language="json"
-          minHeight="120px"
           placeholder={placeholder}
-          ariaLabel={label}
-          onChange={nextValue => onChange(nextValue)}
+          onChange={e => onChange(e.target.value)}
         />
-      </label>
+      </HzField>
     );
   }
   if (define.type === 'number') {
     return (
-      <label className="grid gap-2">
-        <span className={fieldLabelClassName}>{label}</span>
-        <NumberStepper
+      <HzField as="div" label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+        <HzNumberStepper
           data-monitor-param-number-stepper={define.field}
+          data-monitor-param-number-contract="angular-nz-input-number--1000-65535-step-1"
+          data-monitor-param-number-min="-1000"
+          data-monitor-param-number-max="65535"
+          data-monitor-param-number-step="1"
+          data-monitor-editor-number-stepper-owner="hertzbeat-ui-number-stepper"
           value={String(field.paramValue ?? '')}
+          min={-1000}
+          max={65535}
+          step={1}
           placeholder={placeholder}
+          decrementLabel={t('common.decrement')}
+          incrementLabel={t('common.increment')}
           onValueChange={value => onChange(value === '' ? null : Number(value))}
         />
-      </label>
+      </HzField>
     );
   }
   return (
-    <label className="grid gap-2">
-      <span className={fieldLabelClassName}>{label}</span>
-      <Input
+    <HzField label={label} data-monitor-editor-field-owner="hertzbeat-ui-field">
+      <HzInput
         type="text"
+        data-monitor-param-input={define.field}
+        data-monitor-param-field={define.field}
+        data-monitor-editor-input-owner="hertzbeat-ui-input"
+        data-monitor-editor-host-name-autofill-contract={define.field === 'host' ? 'angular-new-host-change' : undefined}
         value={String(field.paramValue ?? '')}
         placeholder={placeholder}
         onChange={e => onChange(e.target.value)}
       />
-    </label>
+    </HzField>
   );
 }
 
@@ -134,53 +393,6 @@ function validateMetadataRows(rows: KeyValueDraft[], sectionLabel: string) {
   return null;
 }
 
-function KeyValueEditor({
-  title,
-  rows,
-  onChange,
-  addLabel,
-  removeLabel,
-  keyPlaceholder,
-  valuePlaceholder
-}: {
-  title: string;
-  rows: KeyValueDraft[];
-  onChange: (rows: KeyValueDraft[]) => void;
-  addLabel: string;
-  removeLabel: string;
-  keyPlaceholder: string;
-  valuePlaceholder: string;
-}) {
-  const nextRows = ensureKeyValueRows(rows);
-  return (
-    <div className="grid gap-3">
-      <div className={fieldLabelClassName}>{title}</div>
-      {nextRows.map((row, index) => (
-        <div key={`${title}-${index}`} className="grid gap-2 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
-          <Input
-            placeholder={keyPlaceholder}
-            value={row.key}
-            onChange={e => onChange(updateRowAt(nextRows, index, { key: e.target.value }))}
-          />
-          <Input
-            placeholder={valuePlaceholder}
-            value={row.value}
-            onChange={e => onChange(updateRowAt(nextRows, index, { value: e.target.value }))}
-          />
-          <Button type="button" size="sm" variant="subtle" onClick={() => onChange(removeRowAt(nextRows, index))}>
-            {removeLabel}
-          </Button>
-        </div>
-      ))}
-      <div className="flex flex-wrap gap-2">
-        <Button type="button" size="sm" variant="subtle" onClick={() => onChange([...nextRows, { key: '', value: '' }])}>
-          {addLabel}
-        </Button>
-      </div>
-    </div>
-  );
-}
-
 export function MonitorEditorSurface({
   initial,
   mode,
@@ -193,20 +405,55 @@ export function MonitorEditorSurface({
   const { t, locale } = useI18n();
   const router = useRouter();
   const [draft, setDraft] = useState(initial);
-  const [labelRows, setLabelRows] = useState<KeyValueDraft[]>(toKeyValueDraft(initial.monitor.labels));
+  const [labelSelectorText, setLabelSelectorText] = useState(stringifyMonitorLabelSelectorValue(initial.monitor.labels));
   const [annotationRows, setAnnotationRows] = useState<KeyValueDraft[]>(toKeyValueDraft(initial.monitor.annotations));
   const [grafanaTemplateText, setGrafanaTemplateText] = useState(initial.grafanaDashboard.template || '');
   const [actionMessage, setActionMessage] = useState<string | null>(null);
   const [actionError, setActionError] = useState<string | null>(null);
-  const [saving, setSaving] = useState(false);
+  const [actionErrorSource, setActionErrorSource] = useState<'validation' | 'api' | null>(null);
+  const [actionErrorAction, setActionErrorAction] = useState<'detect' | 'save' | null>(null);
+  const [actionPhase, setActionPhase] = useState<'idle' | 'detecting' | 'saving'>('idle');
   const [scrapeLoading, setScrapeLoading] = useState(false);
+  const [advancedOpen, setAdvancedOpen] = useState(false);
+  const grafanaTemplateInputRef = useRef<HTMLInputElement | null>(null);
   const scrapeParamsRef = useRef(draft.scrapeParams);
+  const previousScrapeRef = useRef<string | null>(null);
+  const systemDefaultCollectorLabel = t('monitor.collector.system.default');
+  const scrapeOptions = [
+    { value: 'static', label: 'static' },
+    { value: 'http_sd', label: 'http_sd' },
+    { value: 'nacos_sd', label: 'nacos_sd' },
+    { value: 'dns_sd', label: 'dns_sd' },
+    { value: 'eureka_sd', label: 'eureka_sd' },
+    { value: 'consul_sd', label: 'consul_sd' },
+    { value: 'zookeeper_sd', label: 'zookeeper_sd' }
+  ];
+  const collectorChoices = draft.collectors.map(normalizeCollectorOption).filter((option): option is MonitorCollectorOption => Boolean(option));
+  const selectedCollectorChoice = collectorChoices.find(option => option.name === draft.collector);
+  const defaultCollectorModeLabel = collectorModeLabel('public', t);
+  const intervalStepperBounds = resolveMonitorIntervalStepperBounds(draft.monitor.app);
+  const collectorOptions = [
+    { value: '', label: `${systemDefaultCollectorLabel} - ${defaultCollectorModeLabel}` },
+    ...collectorChoices.map(collector => ({ value: collector.name, label: formatCollectorOptionLabel(collector, t) }))
+  ];
+  const scheduleTypeOptions = [
+    { value: 'interval', label: t('monitor.scheduleType.interval') },
+    { value: 'cron', label: t('monitor.scheduleType.cron') }
+  ];
+  const staticHostParamIndex = draft.paramDefines.findIndex(define => define.field === 'host');
+  const shouldRenderStaticHost =
+    (draft.monitor.scrape || 'static') === 'static' &&
+    staticHostParamIndex >= 0 &&
+    draft.params[staticHostParamIndex]?.display !== false;
+  const hasVisibleAdvancedParams = draft.advancedParams.some(param => param?.display !== false);
 
   useEffect(() => {
+    previousScrapeRef.current = null;
     setDraft(syncMonitorDependentDisplay(initial));
-    setLabelRows(toKeyValueDraft(initial.monitor.labels));
+    setLabelSelectorText(stringifyMonitorLabelSelectorValue(initial.monitor.labels));
     setAnnotationRows(toKeyValueDraft(initial.monitor.annotations));
     setGrafanaTemplateText(initial.grafanaDashboard.template || '');
+    setAdvancedOpen(false);
   }, [initial]);
 
   useEffect(() => {
@@ -216,8 +463,27 @@ export function MonitorEditorSurface({
   useEffect(() => {
     const scrape = draft.monitor.scrape || 'static';
     let cancelled = false;
+    const previousScrape = previousScrapeRef.current;
+    const shouldPreserveExistingParams = shouldPreserveMonitorScrapeParamsForLoad(previousScrape, scrape);
+    previousScrapeRef.current = scrape;
+    if (scrape === 'static') {
+      setScrapeLoading(false);
+      setDraft(prev => {
+        if (prev.scrapeParams.length === 0 && prev.scrapeParamDefines.length === 0) {
+          return prev;
+        }
+        return syncMonitorDependentDisplay({
+          ...prev,
+          scrapeParams: [],
+          scrapeParamDefines: []
+        });
+      });
+      return () => {
+        cancelled = true;
+      };
+    }
     setScrapeLoading(true);
-    loadMonitorScrapeDraft(apiMessageGet, scrape, scrapeParamsRef.current)
+    loadMonitorScrapeDraftFromFacade(api.monitors.editorParamDefines, scrape, shouldPreserveExistingParams ? scrapeParamsRef.current : undefined)
       .then(result => {
         if (cancelled) return;
         setDraft(prev =>
@@ -241,7 +507,33 @@ export function MonitorEditorSurface({
   }, [draft.monitor.scrape, t]);
 
   function updateParam(kind: 'params' | 'advancedParams' | 'scrapeParams', index: number, value: unknown) {
-    setDraft(prev => updateMonitorEditorParam(prev, kind, index, value));
+    const noticeKey = resolveMonitorEditorParamChangeNotice(draft, kind, index, value);
+    setDraft(prev => {
+      const nextDraft = updateMonitorEditorParam(prev, kind, index, value);
+      const defines = kind === 'params' ? prev.paramDefines : kind === 'advancedParams' ? prev.advancedParamDefines : prev.scrapeParamDefines;
+      return applyMonitorHostNameAutofill(nextDraft, { mode, field: defines[index]?.field });
+    });
+    if (noticeKey) {
+      setActionMessage(t(noticeKey));
+      setActionError(null);
+      setActionErrorSource(null);
+      setActionErrorAction(null);
+    }
+  }
+
+  async function handleGrafanaTemplateUpload(event: React.ChangeEvent<HTMLInputElement>) {
+    const file = event.target.files?.[0];
+    event.target.value = '';
+    if (!file) return;
+    const template = await file.text();
+    setGrafanaTemplateText(template);
+    setDraft(prev => ({
+      ...prev,
+      grafanaDashboard: {
+        ...prev.grafanaDashboard,
+        template
+      }
+    }));
   }
 
   function buildDraftForSubmit() {
@@ -249,7 +541,7 @@ export function MonitorEditorSurface({
       ...draft,
       monitor: {
         ...draft.monitor,
-        labels: fromKeyValueDraft(labelRows),
+        labels: parseMonitorLabelSelectorValue(labelSelectorText),
         annotations: fromKeyValueDraft(annotationRows)
       },
       grafanaDashboard: {
@@ -259,250 +551,530 @@ export function MonitorEditorSurface({
     } satisfies MonitorEditorDraft;
   }
 
-  function validateBeforeSubmit(nextDraft: MonitorEditorDraft) {
-    const labelError = validateMetadataRows(labelRows, t('label.bind'));
+  function validateBeforeSubmit(nextDraft: MonitorEditorDraft, options: { validateCronFormat?: boolean } = {}) {
+    const labelError = validateMetadataRows(toKeyValueDraft(parseMonitorLabelSelectorValue(labelSelectorText)), t('label.bind'));
     if (labelError) return labelError;
     const annotationError = validateMetadataRows(annotationRows, t('common.annotation.bind'));
     if (annotationError) return annotationError;
-    return validateMonitorEditorDraft(nextDraft, t);
+    return validateMonitorEditorDraft(nextDraft, t, options);
   }
 
+  const actionBusy = actionPhase !== 'idle';
+  const busyActionLabel = actionPhase === 'detecting' ? t('monitor.spinning-tip.detecting') : t('common.loading');
+  const mutationStatus: HzMutationStatus = actionBusy ? 'saving' : actionError ? 'failed' : actionMessage ? 'saved' : 'clean';
+  const saveReturnUrl = buildMonitorEditorReturnUrl(draft.monitor.app, returnContext ? { ...returnContext } : undefined);
+  const cancelReturnUrl = buildMonitorEditorCancelUrl(returnContext ? { ...returnContext } : undefined);
+  const saveReturnContract = returnContext?.returnTo ? 'safe-return-context-or-angular-app-list' : 'angular-app-list';
+  const cancelReturnContract = returnContext?.returnTo ? 'safe-return-context-or-list-root' : 'legacy-list-root';
+  const mutationStatusLabel =
+    actionBusy
+      ? busyActionLabel
+      : mutationStatus === 'failed'
+        ? t('common.failed')
+        : mutationStatus === 'saved'
+          ? actionMessage || t('common.ready')
+          : t('common.ready');
+  const mutationFeedback = actionBusy ? (
+    <HzInlineFeedback
+      tone="info"
+      title={busyActionLabel}
+      variant="embedded"
+      data-monitor-editor-feedback-owner="hertzbeat-ui-inline-feedback"
+      data-monitor-editor-feedback="busy"
+      data-monitor-editor-busy-contract="angular-spin-tip"
+      data-monitor-editor-busy-phase={actionPhase}
+    />
+  ) : actionError ? (
+    <HzInlineFeedback
+      tone="critical"
+      title={t('common.failed')}
+      description={actionError}
+      variant="embedded"
+      data-monitor-editor-feedback-owner="hertzbeat-ui-inline-feedback"
+      data-monitor-editor-feedback="error"
+      data-monitor-editor-validation-contract={actionErrorSource === 'validation' ? 'angular-form-required' : undefined}
+      data-monitor-editor-api-error-contract={actionErrorSource === 'api' ? 'angular-message-msg' : undefined}
+      data-monitor-editor-blocked-action={actionErrorAction || undefined}
+      data-monitor-editor-blocked-before-api={actionErrorSource === 'validation' ? 'true' : undefined}
+    />
+  ) : actionMessage ? (
+    <HzInlineFeedback
+      tone="success"
+      title={actionMessage}
+      variant="embedded"
+      data-monitor-editor-feedback-owner="hertzbeat-ui-inline-feedback"
+      data-monitor-editor-feedback="success"
+    />
+  ) : null;
+  const mutationBar = (
+    <HzMonitorEditorActionBar
+      title={mode === 'new' ? t('monitor.new-monitor') : draft.monitor.name || t('monitor.edit-monitor')}
+      status={mutationStatus}
+      statusLabel={mutationStatusLabel}
+      summaryVisible={false}
+      actionAlign="center"
+      feedback={mutationFeedback}
+      data-monitor-editor-action-dock="bottom"
+      data-monitor-editor-mutation-bar="true"
+      actions={[
+        {
+          id: 'detect',
+          label: t('common.button.detect'),
+          intent: 'ghost',
+          disabled: actionBusy,
+          onSelect: () => void handleDetect(),
+          buttonProps: {
+            'data-monitor-editor-detect-action': 'true',
+            'data-monitor-editor-detect-busy-label': t('monitor.spinning-tip.detecting'),
+            'data-monitor-editor-detect-cron-validation': 'angular-detect-skips-cron-format'
+          }
+        },
+        {
+          id: 'submit',
+          label: t('common.button.ok'),
+          type: 'submit',
+          intent: 'primary',
+          disabled: actionBusy,
+          buttonProps: {
+            'data-monitor-editor-submit-action': 'true',
+            'data-monitor-editor-submit-busy-label': t('common.loading'),
+            'data-monitor-editor-save-return': saveReturnContract,
+            'data-monitor-editor-save-return-target': saveReturnUrl,
+            'data-monitor-editor-save-notification-contract': 'angular-success-before-return'
+          }
+        },
+        {
+          id: 'cancel',
+          label: t('common.button.cancel'),
+          intent: 'ghost',
+          onSelect: () => router.push(cancelReturnUrl),
+          buttonProps: {
+            'data-monitor-editor-cancel-action': 'true',
+            'data-monitor-editor-cancel-return': cancelReturnContract,
+            'data-monitor-editor-cancel-return-target': cancelReturnUrl
+          }
+        }
+      ]}
+    />
+  );
+
   async function handleDetect() {
-    setSaving(true);
+    setActionPhase('detecting');
+    setActionErrorSource(null);
+    setActionErrorAction(null);
     try {
       const nextDraft = buildDraftForSubmit();
-      const validationError = validateBeforeSubmit(nextDraft);
+      const validationError = validateBeforeSubmit(nextDraft, { validateCronFormat: false });
       if (validationError) {
         setActionError(validationError);
+        setActionErrorSource('validation');
+        setActionErrorAction('detect');
         setActionMessage(null);
         return;
       }
-      await detectMonitor(apiMessagePost as any, nextDraft);
+      await detectMonitorFromFacade(api.monitors.detect, nextDraft);
       setActionMessage(t('monitor.detect.success'));
       setActionError(null);
+      setActionErrorSource(null);
+      setActionErrorAction(null);
     } catch (error) {
       setActionError(error instanceof Error ? error.message : t('monitor.detect.failed'));
+      setActionErrorSource('api');
+      setActionErrorAction('detect');
     } finally {
-      setSaving(false);
+      setActionPhase('idle');
     }
   }
 
   async function handleSave() {
-    setSaving(true);
+    setActionPhase('saving');
+    setActionErrorSource(null);
+    setActionErrorAction(null);
     try {
       const nextDraft = buildDraftForSubmit();
       const validationError = validateBeforeSubmit(nextDraft);
       if (validationError) {
         setActionError(validationError);
+        setActionErrorSource('validation');
+        setActionErrorAction('save');
         setActionMessage(null);
         return;
       }
       if (mode === 'new') {
-        await createMonitor(apiMessagePost as any, nextDraft);
+        await createMonitorFromFacade(api.monitors.create, nextDraft);
         setActionMessage(t('monitor.new.success'));
       } else {
-        await updateMonitor(apiMessagePut as any, nextDraft);
+        await updateMonitorFromFacade(api.monitors.update, nextDraft);
         setActionMessage(t('monitor.edit.success'));
       }
       setActionError(null);
+      setActionErrorSource(null);
+      setActionErrorAction(null);
       router.push(buildMonitorEditorReturnUrl(nextDraft.monitor.app, returnContext ? { ...returnContext } : undefined));
     } catch (error) {
       setActionError(error instanceof Error ? error.message : mode === 'new' ? t('monitor.new.failed') : t('monitor.edit.failed'));
+      setActionErrorSource('api');
+      setActionErrorAction('save');
     } finally {
-      setSaving(false);
+      setActionPhase('idle');
     }
   }
 
   return (
-    <form
-      className="space-y-3"
+    <HzMonitorEditorForm
+      actionBar={mutationBar}
       onSubmit={event => {
         event.preventDefault();
         void handleSave();
       }}
+      data-monitor-editor-form-owner="hertzbeat-ui-monitor-editor-form"
+      data-monitor-editor-app-source="angular-route-context-hidden-field"
+      data-monitor-editor-static-host-position="angular-before-name"
+      data-monitor-editor-field-order="angular-monitor-form-sequence"
+      data-monitor-editor-detect-payload-contract="angular-monitor-collector-params-no-grafana"
+      data-monitor-editor-save-payload-contract="angular-monitor-collector-params-grafana"
+      data-monitor-editor-payload-param-merge="angular-params-advanced-sdparams"
+      data-monitor-editor-payload-host-instance="angular-host-param-as-instance"
+      data-monitor-editor-service-discovery-params="angular-nonstatic-only"
+      data-monitor-editor-scrape-reload-contract="angular-reset-on-user-scrape-change"
+      data-monitor-editor-ssl-port-notice="angular-info-notification"
+      data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"
+      data-monitor-editor-advanced-visible-contract="angular-visible-param-only"
+      data-monitor-editor-label-selector="angular-app-label-selector"
+      data-monitor-editor-collector-selection="angular-collectors-selection-tags"
+      data-monitor-editor-detect-cron-validation="angular-detect-skips-cron-format"
+      data-monitor-editor-cron-required="angular-required-before-detect-save"
     >
-      <WorkbenchPage
-        kicker={t('monitor.form.shell.kicker')}
+      <input
+        type="hidden"
+        name="app"
+        value={draft.monitor.app || ''}
+        readOnly
+        data-monitor-editor-hidden-app="route-or-detail-context"
+      />
+      <HzMonitorEditorHeader
         title={mode === 'new' ? t('monitor.new-monitor') : draft.monitor.name || t('monitor.edit-monitor')}
-        subtitle={mode === 'new' ? t('monitor.form.shell.new.copy') : t('monitor.form.shell.edit.copy')}
-        tone="operator"
-        facts={[
-          { label: t('common.app'), value: draft.monitor.app || '-' },
-          { label: t('common.scrape'), value: draft.monitor.scrape || 'static' },
-          { label: t('common.mode'), value: mode }
-        ]}
-        actions={
-          <>
-            <Button type="button" size="sm" variant="default" onClick={() => void handleDetect()} disabled={saving}>
-              {t('common.button.detect')}
-            </Button>
-            <Button type="submit" size="sm" variant="primary" disabled={saving}>
-              {t('common.button.ok')}
-            </Button>
-            <Button
+        data-monitor-editor-linear-header="hertzbeat-ui-monitor-editor-header"
+      />
+
+        <HzMonitorEditorSection data-monitor-editor-section-owner="hertzbeat-ui-editor-section" title={t('monitor.editor.section.base')}>
+          <HzMonitorEditorFieldGrid data-monitor-editor-field-grid-owner="hertzbeat-ui-monitor-editor-field-grid">
+            <HzField label={t('monitor.scrape')} data-monitor-editor-field-owner="hertzbeat-ui-field">
+              <HzSelect
+                value={draft.monitor.scrape || 'static'}
+                aria-label={t('monitor.scrape')}
+                options={scrapeOptions}
+                data-monitor-editor-select="scrape"
+                data-monitor-editor-select-owner="hertzbeat-ui-select"
+                onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, scrape: e.target.value } }))}
+              />
+            </HzField>
+            {draft.monitor.scrape !== 'static'
+              ? draft.scrapeParamDefines.map((define, index) => (
+                  draft.scrapeParams[index]?.display === false ? null : (
+                    <div
+                      key={`scrape-${define.field}`}
+                      data-monitor-editor-scrape-param-order="angular-after-scrape-before-name"
+                    >
+                      <MonitorParamField
+                        field={draft.scrapeParams[index] || {}}
+                        define={define}
+                        locale={locale}
+                        t={t}
+                        onChange={value => updateParam('scrapeParams', index, value)}
+                      />
+                    </div>
+                  )
+                ))
+              : null}
+            {shouldRenderStaticHost ? (
+              <div data-monitor-editor-static-host-field="angular-before-name">
+                <MonitorParamField
+                  field={draft.params[staticHostParamIndex] || {}}
+                  define={draft.paramDefines[staticHostParamIndex]}
+                  locale={locale}
+                  t={t}
+                  onChange={value => updateParam('params', staticHostParamIndex, value)}
+                />
+              </div>
+            ) : null}
+            <HzField label={t('monitor.name')} data-monitor-editor-field-owner="hertzbeat-ui-field">
+              <HzInput
+                value={draft.monitor.name || ''}
+                data-monitor-editor-input="name"
+                data-monitor-editor-input-owner="hertzbeat-ui-input"
+                data-monitor-editor-host-name-autofill-target={mode === 'new' ? 'monitor-name' : undefined}
+                onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, name: e.target.value } }))}
+              />
+            </HzField>
+          </HzMonitorEditorFieldGrid>
+        </HzMonitorEditorSection>
+
+        <HzMonitorEditorSection data-monitor-editor-section-owner="hertzbeat-ui-editor-section" title={t('monitor.editor.section.params')}>
+          <HzMonitorEditorFieldGrid data-monitor-editor-field-grid-owner="hertzbeat-ui-monitor-editor-field-grid">
+            {draft.paramDefines.map((define, index) => (
+              draft.params[index]?.display === false || define.field === 'host' ? null : <MonitorParamField key={`param-${define.field}`} field={draft.params[index] || {}} define={define} locale={locale} t={t} onChange={value => updateParam('params', index, value)} />
+            ))}
+          </HzMonitorEditorFieldGrid>
+          {scrapeLoading ? (
+            <HzLoadingState
+              data-monitor-editor-scrape-loading-owner="hertzbeat-ui-loading-state"
+              title={t('common.loading')}
+              rows={2}
+            />
+          ) : null}
+        </HzMonitorEditorSection>
+
+        {hasVisibleAdvancedParams ? (
+          <HzMonitorEditorSection
+            data-monitor-editor-section-owner="hertzbeat-ui-editor-section"
+            data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"
+            data-monitor-editor-advanced-collapse-state={advancedOpen ? 'expanded' : 'collapsed'}
+            title={t('monitor.advanced')}
+          >
+            <HzButton
               type="button"
               size="sm"
-              variant="subtle"
-              data-monitor-editor-cancel-action="true"
-              onClick={() => router.push(buildMonitorEditorReturnUrl(draft.monitor.app, returnContext ? { ...returnContext } : undefined))}
+              intent="secondary"
+              aria-expanded={advancedOpen}
+              aria-controls="monitor-editor-advanced-params"
+              data-monitor-editor-advanced-toggle="angular-dashed-collapse-trigger"
+              data-monitor-editor-advanced-toggle-owner="hertzbeat-ui-button"
+              onClick={() => setAdvancedOpen(open => !open)}
             >
-              {t('common.button.cancel')}
-            </Button>
-          </>
-        }
-        main={
-          <div className="space-y-4">
-            <SurfaceSection title={t('monitor.editor.section.base')} copy={t('monitor.editor.section.base.copy')}>
-              <div className="grid gap-3 md:grid-cols-2">
-                <label className="grid gap-2">
-                  <span className={fieldLabelClassName}>{t('common.app')}</span>
-                  <Input value={draft.monitor.app || ''} onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, app: e.target.value } }))} disabled={mode === 'edit'} />
-                </label>
-                <label className="grid gap-2">
-                  <span className={fieldLabelClassName}>{t('monitor.scrape')}</span>
-                  <Select value={draft.monitor.scrape || 'static'} onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, scrape: e.target.value } }))}>
-                    <option value="static">static</option>
-                    <option value="http_sd">http_sd</option>
-                    <option value="nacos_sd">nacos_sd</option>
-                    <option value="dns_sd">dns_sd</option>
-                    <option value="eureka_sd">eureka_sd</option>
-                    <option value="consul_sd">consul_sd</option>
-                    <option value="zookeeper_sd">zookeeper_sd</option>
-                  </Select>
-                </label>
-                <label className="grid gap-2">
-                  <span className={fieldLabelClassName}>{t('monitor.name')}</span>
-                  <Input value={draft.monitor.name || ''} onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, name: e.target.value } }))} />
-                </label>
-                <label className="grid gap-2">
-                  <span className={fieldLabelClassName}>{t('monitor.collector')}</span>
-                  <Select value={draft.collector || ''} onChange={e => setDraft(prev => ({ ...prev, collector: e.target.value }))}>
-                    <option value="">{t('common.none')}</option>
-                    {draft.collectors.map(collector => (
-                      <option key={collector} value={collector}>
-                        {collector}
-                      </option>
-                    ))}
-                  </Select>
-                </label>
-                <label className="grid gap-2">
-                  <span className={fieldLabelClassName}>{t('monitor.scheduleType')}</span>
-                  <Select value={draft.monitor.scheduleType || 'interval'} onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, scheduleType: e.target.value } }))}>
-                    <option value="interval">{t('monitor.scheduleType.interval')}</option>
-                    <option value="cron">{t('monitor.scheduleType.cron')}</option>
-                  </Select>
-                </label>
-                {draft.monitor.scheduleType === 'cron' ? (
-                  <label className="grid gap-2">
-                    <span className={fieldLabelClassName}>{t('monitor.cronExpression')}</span>
-                    <Input value={draft.monitor.cronExpression || ''} onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, cronExpression: e.target.value } }))} />
-                  </label>
-                ) : (
-                  <label className="grid gap-2">
-                    <span className={fieldLabelClassName}>{t('monitor.intervals')}</span>
-                    <NumberStepper
-                      name="intervals"
-                      min="1"
-                      step="1"
-                      data-monitor-interval-stepper="cold-number-stepper"
-                      value={String(draft.monitor.intervals ?? 60)}
-                      onValueChange={value => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, intervals: value === '' ? null : Number(value) } }))}
-                    />
-                  </label>
-                )}
-              </div>
-            </SurfaceSection>
-
-            <SurfaceSection title={t('monitor.editor.section.params')} copy={t('monitor.editor.section.params.copy')}>
-              <div className="grid gap-3 md:grid-cols-2">
-                {draft.paramDefines.map((define, index) => (
-                  draft.params[index]?.display === false ? null : <MonitorParamField key={`param-${define.field}`} field={draft.params[index] || {}} define={define} locale={locale} onChange={value => updateParam('params', index, value)} />
-                ))}
-                {draft.scrapeParamDefines.map((define, index) => (
-                  draft.scrapeParams[index]?.display === false ? null : <MonitorParamField key={`scrape-${define.field}`} field={draft.scrapeParams[index] || {}} define={define} locale={locale} onChange={value => updateParam('scrapeParams', index, value)} />
+              <ChevronDown
+                aria-hidden="true"
+                className={`h-3.5 w-3.5 transition-transform ${advancedOpen ? 'rotate-180' : ''}`}
+              />
+              {t('monitor.advanced')}
+            </HzButton>
+            {advancedOpen ? (
+              <HzMonitorEditorFieldGrid
+                id="monitor-editor-advanced-params"
+                className="mt-3"
+                data-monitor-editor-field-grid-owner="hertzbeat-ui-monitor-editor-field-grid"
+                data-monitor-editor-advanced-fields="expanded"
+              >
+                {draft.advancedParamDefines.map((define, index) => (
+                  draft.advancedParams[index]?.display === false ? null : <MonitorParamField key={`advanced-${define.field}`} field={draft.advancedParams[index] || {}} define={define} locale={locale} t={t} onChange={value => updateParam('advancedParams', index, value)} />
                 ))}
-              </div>
-              {scrapeLoading ? <div className="text-sm text-[var(--ops-text-secondary)]">{t('common.loading')}</div> : null}
-            </SurfaceSection>
-
-            {draft.advancedParamDefines.length > 0 ? (
-              <SurfaceSection title={t('monitor.advanced')} copy={t('monitor.editor.section.advanced.copy')}>
-                <div className="grid gap-3 md:grid-cols-2">
-                  {draft.advancedParamDefines.map((define, index) => (
-                    draft.advancedParams[index]?.display === false ? null : <MonitorParamField key={`advanced-${define.field}`} field={draft.advancedParams[index] || {}} define={define} locale={locale} onChange={value => updateParam('advancedParams', index, value)} />
-                  ))}
-                </div>
-              </SurfaceSection>
+              </HzMonitorEditorFieldGrid>
             ) : null}
-          </div>
-        }
-        side={
-          <>
-            <SurfaceSection title={t('common.description')} copy={t('monitor.description.tip')}>
-              <Textarea
-                data-monitor-description-textarea="cold-textarea"
-                className="min-h-[120px]"
-                value={draft.monitor.description || ''}
-                onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, description: e.target.value } }))}
-              />
-            </SurfaceSection>
-            <SurfaceSection title={t('label.bind')} copy={t('monitor.editor.labels.copy')}>
-              <KeyValueEditor
-                title={t('label.bind')}
-                rows={labelRows}
-                onChange={setLabelRows}
-                addLabel={t('monitor.editor.labels.add')}
-                removeLabel={t('monitor.editor.labels.remove')}
-                keyPlaceholder={t('monitor.editor.labels.key')}
-                valuePlaceholder={t('monitor.editor.labels.value')}
+          </HzMonitorEditorSection>
+        ) : null}
+
+        <HzMonitorEditorSection
+          data-monitor-editor-section-owner="hertzbeat-ui-editor-section"
+          data-monitor-editor-runtime-order="angular-after-advanced-before-labels"
+          title={t('monitor.editor.section.runtime')}
+        >
+          <HzMonitorEditorFieldGrid data-monitor-editor-field-grid-owner="hertzbeat-ui-monitor-editor-field-grid">
+            <HzField
+              label={t('monitor.collector')}
+              data-monitor-editor-field-owner="hertzbeat-ui-field"
+              data-monitor-editor-collector-selection="angular-collectors-selection-tags"
+            >
+              <HzSelect
+                value={draft.collector || ''}
+                aria-label={t('monitor.collector')}
+                options={collectorOptions}
+                data-monitor-editor-select="collector"
+                data-monitor-editor-select-owner="hertzbeat-ui-select"
+                data-monitor-editor-collector-selection-owner="hertzbeat-ui-select"
+                optionDataAttributes={option => ({
+                  'data-monitor-editor-collector-option': option.value || 'system-default',
+                  'data-monitor-editor-collector-option-contract': 'angular-status-ip-mode-tags'
+                })}
+                onChange={e => setDraft(prev => ({ ...prev, collector: e.target.value }))}
               />
-            </SurfaceSection>
-            <SurfaceSection title={t('common.annotation.bind')} copy={t('monitor.editor.annotations.copy')}>
-              <KeyValueEditor
-                title={t('common.annotation.bind')}
-                rows={annotationRows}
-                onChange={setAnnotationRows}
-                addLabel={t('monitor.editor.annotations.add')}
-                removeLabel={t('monitor.editor.annotations.remove')}
-                keyPlaceholder={t('monitor.editor.annotations.key')}
-                valuePlaceholder={t('monitor.editor.annotations.value')}
+              <div
+                className="mt-2 flex min-h-6 flex-wrap items-center gap-1.5"
+                data-monitor-editor-collector-tags="angular-status-ip-mode-tags"
+              >
+                {selectedCollectorChoice ? (
+                  <>
+                    <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="status">
+                      {collectorStatusLabel(selectedCollectorChoice, t)}
+                    </span>
+                    <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="name">
+                      {selectedCollectorChoice.name}
+                    </span>
+                    {selectedCollectorChoice.ip ? (
+                      <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="ip">
+                        {selectedCollectorChoice.ip}
+                      </span>
+                    ) : null}
+                    <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="mode">
+                      {collectorModeLabel(selectedCollectorChoice.mode, t)}
+                    </span>
+                  </>
+                ) : (
+                  <>
+                    <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="system-default">
+                      {systemDefaultCollectorLabel}
+                    </span>
+                    <span className="border border-[#1f4537] bg-[#0d2a20] px-2 py-0.5 text-[10px] font-semibold text-[#b7f7d0]" data-monitor-editor-collector-tag="mode">
+                      {defaultCollectorModeLabel}
+                    </span>
+                  </>
+                )}
+              </div>
+            </HzField>
+            <HzField label={t('monitor.scheduleType')} data-monitor-editor-field-owner="hertzbeat-ui-field">
+              <HzSelect
+                value={draft.monitor.scheduleType || 'interval'}
+                aria-label={t('monitor.scheduleType')}
+                options={scheduleTypeOptions}
+                data-monitor-editor-select="schedule-type"
+                data-monitor-editor-select-owner="hertzbeat-ui-select"
+                onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, scheduleType: e.target.value } }))}
               />
-            </SurfaceSection>
-            {draft.monitor.app === 'prometheus' ? (
-              <SurfaceSection title={t('monitor.grafana.enabled.label')} copy={t('monitor.grafana.upload.tip')}>
-                <Checkbox
-                  containerClassName="mb-3"
-                  data-monitor-grafana-enabled-checkbox="cold-checkbox"
-                  checked={Boolean(draft.grafanaDashboard.enabled)}
-                  onChange={e => setDraft(prev => ({ ...prev, grafanaDashboard: { ...prev.grafanaDashboard, enabled: e.target.checked } }))}
-                  label={t('monitor.grafana.enabled.label')}
+            </HzField>
+            {draft.monitor.scheduleType === 'cron' ? (
+              <HzField label={t('monitor.cronExpression')} data-monitor-editor-field-owner="hertzbeat-ui-field">
+                <HzInput
+                  value={draft.monitor.cronExpression || ''}
+                  required
+                  data-monitor-editor-input="cron-expression"
+                  data-monitor-editor-input-owner="hertzbeat-ui-input"
+                  data-monitor-editor-cron-required="angular-required-before-detect-save"
+                  onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, cronExpression: e.target.value } }))}
                 />
-                {draft.grafanaDashboard.enabled ? (
-                  <ColdCodeEditor
-                    name="grafana_dashboard_template"
-                    value={grafanaTemplateText}
-                    language="json"
-                    minHeight="120px"
-                    ariaLabel={t('monitor.grafana.enabled.label')}
-                    data-monitor-grafana-code-editor="dashboard-template"
-                    onChange={setGrafanaTemplateText}
+              </HzField>
+            ) : (
+              <HzField
+                as="div"
+                label={t('monitor.intervals')}
+                data-monitor-editor-field-owner="hertzbeat-ui-field"
+                data-monitor-interval-stepper-contract="angular-min-step-max-by-app"
+              >
+                <HzNumberStepper
+                  name="intervals"
+                  min={intervalStepperBounds.min}
+                  max={intervalStepperBounds.max}
+                  step={intervalStepperBounds.step}
+                  data-monitor-interval-stepper="hertzbeat-ui-number-stepper"
+                  data-monitor-interval-stepper-min={String(intervalStepperBounds.min)}
+                  data-monitor-interval-stepper-max={String(intervalStepperBounds.max)}
+                  data-monitor-interval-stepper-step={String(intervalStepperBounds.step)}
+                  data-monitor-editor-number-stepper-owner="hertzbeat-ui-number-stepper"
+                  value={String(draft.monitor.intervals ?? 60)}
+                  decrementLabel={t('common.decrement')}
+                  incrementLabel={t('common.increment')}
+                  onValueChange={value => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, intervals: value === '' ? null : Number(value) } }))}
+                />
+                <span
+                  className="mt-1 inline-flex text-[10px] font-semibold text-[#7d8798]"
+                  data-monitor-interval-stepper-unit="common.time.unit.second"
+                >
+                  {t('common.time.unit.second')}
+                </span>
+              </HzField>
+            )}
+          </HzMonitorEditorFieldGrid>
+        </HzMonitorEditorSection>
+
+        <HzMonitorEditorSection data-monitor-editor-section-owner="hertzbeat-ui-editor-section" title={t('label.bind')}>
+          <div
+            data-monitor-editor-label-selector="angular-app-label-selector"
+            data-monitor-editor-label-selector-owner="cold-label-selector"
+          >
+            <LabelRecordInput
+              name="labels"
+              value={labelSelectorText}
+              onValueChange={setLabelSelectorText}
+              keyPlaceholder={t('monitor.editor.labels.key')}
+              valuePlaceholder={t('monitor.editor.labels.value')}
+              addLabel={t('monitor.editor.labels.add')}
+              removeLabel={t('monitor.editor.labels.remove')}
+              containerClassName="min-w-0"
+            />
+          </div>
+        </HzMonitorEditorSection>
+
+        <HzMonitorEditorSection data-monitor-editor-section-owner="hertzbeat-ui-editor-section" title={t('common.annotation.bind')}>
+          <HzKeyValueEditor
+            data-monitor-editor-key-value="annotations"
+            data-monitor-editor-key-value-owner="hertzbeat-ui-key-value-editor"
+            title={t('common.annotation.bind')}
+            rows={annotationRows}
+            onChange={setAnnotationRows}
+            addLabel={t('monitor.editor.annotations.add')}
+            removeLabel={t('monitor.editor.annotations.remove')}
+            keyPlaceholder={t('monitor.editor.annotations.key')}
+            valuePlaceholder={t('monitor.editor.annotations.value')}
+          />
+        </HzMonitorEditorSection>
+
+        <HzMonitorEditorSection data-monitor-editor-section-owner="hertzbeat-ui-editor-section" title={t('common.description')}>
+          <HzTextarea
+            data-monitor-description-textarea="hertzbeat-ui-textarea"
+            data-monitor-editor-textarea-owner="hertzbeat-ui-textarea"
+            data-monitor-description-textarea-limit="angular-textarea-limit-100"
+            height="tall"
+            maxCharacterCount={100}
+            value={draft.monitor.description || ''}
+            onChange={e => setDraft(prev => ({ ...prev, monitor: { ...prev.monitor, description: e.target.value } }))}
+          />
+        </HzMonitorEditorSection>
+
+        {draft.monitor.app === 'prometheus' ? (
+          <HzMonitorEditorSection data-monitor-editor-section-owner="hertzbeat-ui-editor-section" title={t('monitor.grafana.enabled.label')}>
+            <HzCheckbox
+              containerClassName="mb-3"
+              data-monitor-grafana-enabled-checkbox="hertzbeat-ui-checkbox"
+              data-monitor-editor-checkbox="grafana-dashboard-enabled"
+              data-monitor-editor-checkbox-owner="hertzbeat-ui-checkbox"
+              checked={Boolean(draft.grafanaDashboard.enabled)}
+              onChange={e => setDraft(prev => ({ ...prev, grafanaDashboard: { ...prev.grafanaDashboard, enabled: e.target.checked } }))}
+              label={t('monitor.grafana.enabled.label')}
+            />
+            {draft.grafanaDashboard.enabled ? (
+              <div
+                className="space-y-3"
+                data-monitor-grafana-template-input-contract="angular-json-template-upload"
+              >
+                <div
+                  className="flex flex-wrap items-center gap-2"
+                  data-monitor-grafana-template-upload="angular-json-template-upload"
+                >
+                  <HzFileInput
+                    ref={grafanaTemplateInputRef}
+                    accept=".json,application/json"
+                    aria-label={t('monitor.grafana.upload.label')}
+                    data-monitor-grafana-template-upload-owner="hertzbeat-ui-file-input"
+                    data-monitor-grafana-template-upload-input="json-template"
+                    onChange={event => void handleGrafanaTemplateUpload(event)}
                   />
-                ) : null}
-              </SurfaceSection>
+                  <HzButton
+                    type="button"
+                    size="sm"
+                    intent="secondary"
+                    data-monitor-grafana-template-upload-trigger-owner="hertzbeat-ui-button"
+                    data-monitor-grafana-template-upload-trigger="json-template"
+                    onClick={() => grafanaTemplateInputRef.current?.click()}
+                  >
+                    {t('monitor.grafana.upload.label')}
+                  </HzButton>
+                  <span className="text-xs text-[var(--hz-ui-text-muted)]">
+                    {t('monitor.grafana.upload.tip')}
+                  </span>
+                </div>
+                <HzCodeEditor
+                  name="grafana_dashboard_template"
+                  value={grafanaTemplateText}
+                  language="json"
+                  minHeight="120px"
+                  ariaLabel={t('monitor.grafana.enabled.label')}
+                  data-monitor-editor-code-editor-owner="hertzbeat-ui-code-editor"
+                  data-monitor-editor-code-editor="grafana-dashboard-template"
+                  data-monitor-grafana-code-editor="dashboard-template"
+                  onChange={setGrafanaTemplateText}
+                />
+              </div>
             ) : null}
-            <SurfaceSection title={t('monitor.editor.section.payload')} copy={t('monitor.editor.section.payload.copy')}>
-              <RowList
-                rows={[
-                  { title: t('common.app'), copy: draft.monitor.app || '-', meta: draft.monitor.scrape || 'static' },
-                  { title: t('monitor.name'), copy: draft.monitor.name || '-', meta: draft.collector || '-' }
-                ]}
-              />
-            </SurfaceSection>
-            {actionMessage ? <div className="text-sm text-emerald-300">{actionMessage}</div> : null}
-            {actionError ? <div className="text-sm text-rose-300">{actionError}</div> : null}
-          </>
-        }
-      />
-    </form>
+          </HzMonitorEditorSection>
+        ) : null}
+    </HzMonitorEditorForm>
   );
 }
diff --git a/web-next/lib/monitor-api-facade.test.ts b/web-next/lib/monitor-api-facade.test.ts
new file mode 100644
index 0000000000..572733772c
--- /dev/null
+++ b/web-next/lib/monitor-api-facade.test.ts
@@ -0,0 +1,76 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { api } from './monitor-api-facade';
+
+const fetchMock = vi.fn<typeof fetch>();
+
+function mockApiMessagePayload(data: unknown) {
+  fetchMock.mockResolvedValueOnce(
+    new Response(JSON.stringify({ code: 0, data }), {
+      status: 200,
+      headers: { 'Content-Type': 'application/json' }
+    })
+  );
+}
+
+describe('monitor api facade', () => {
+  afterEach(() => {
+    fetchMock.mockReset();
+    vi.unstubAllGlobals();
+  });
+
+  it('maps monitor list reads through the monitor domain URL builder', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ content: [{ id: 42 }], totalElements: 1 });
+
+    await expect(
+      api.monitors.page({
+        search: ' mysql ',
+        app: 'mysql',
+        labels: '',
+        status: '2',
+        pageIndex: '0',
+        pageSize: '8',
+        entityId: '',
+        entityName: '',
+        returnTo: ''
+      })
+    ).resolves.toEqual({ content: [{ id: 42 }], totalElements: 1 });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors?pageIndex=0&pageSize=8&search=mysql&app=mysql&status=2',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor editor draft reads through the monitor domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ monitor: { id: 42, app: 'mysql' }, params: [], collector: null });
+    mockApiMessagePayload({ content: [{ collector: { name: 'edge-a' } }] });
+    mockApiMessagePayload([{ field: 'host', type: 'text' }]);
+
+    await expect(api.monitors.editorDetail(42)).resolves.toEqual({
+      monitor: { id: 42, app: 'mysql' },
+      params: [],
+      collector: null
+    });
+    await expect(api.monitors.editorCollectors()).resolves.toEqual({ content: [{ collector: { name: 'edge-a' } }] });
+    await expect(api.monitors.editorParamDefines('mysql')).resolves.toEqual([{ field: 'host', type: 'text' }]);
+
+    expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/monitor/42', expect.objectContaining({ credentials: 'same-origin' }));
+    expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/collector', expect.objectContaining({ credentials: 'same-origin' }));
+    expect(fetchMock).toHaveBeenNthCalledWith(3, '/api/apps/mysql/params', expect.objectContaining({ credentials: 'same-origin' }));
+  });
+
+  it('maps monitor export downloads to response readers', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    fetchMock.mockResolvedValueOnce(new Response('zip', { status: 200 }));
+
+    await expect(api.monitors.exportResponse([42, 43], 'JSON')).resolves.toBeInstanceOf(Response);
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/export?ids=42&ids=43&type=JSON',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+});
diff --git a/web-next/lib/monitor-api-facade.ts b/web-next/lib/monitor-api-facade.ts
new file mode 100644
index 0000000000..6a8db74281
--- /dev/null
+++ b/web-next/lib/monitor-api-facade.ts
@@ -0,0 +1,84 @@
+import { apiDownload, apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from './api-client';
+import {
+  buildMonitorDetailUrl,
+  buildMonitorFavoriteUrl,
+  buildMonitorGrafanaUrl,
+  buildMonitorHistoryCatalogUrl,
+  buildMonitorHistoryMetricDataUrl,
+  buildMonitorRealtimeMetricUrl,
+  type MonitorHistoryCatalogDefineResponse,
+  type MonitorHistoryMetricCatalogItem
+} from './monitor-detail/controller';
+import {
+  buildMonitorEditorCollectorsUrl,
+  buildMonitorEditorMonitorUrl,
+  buildMonitorEditorParamDefinesUrl,
+  type MonitorDetailResponse
+} from './monitor-editor/controller';
+import {
+  buildCopyMonitorUrl,
+  buildDeleteMonitorsUrl,
+  buildEnableMonitorsUrl,
+  buildExportAllMonitorsUrl,
+  buildExportMonitorsUrl,
+  buildImportMonitorsUrl,
+  buildPauseMonitorsUrl
+} from './monitor-manage/controller';
+import { buildMonitorUrl, type MonitorQueryState } from './monitor-manage/query-state';
+import type { CollectorSummary, Monitor, MonitorHistoryData, PageResult, ParamDefine } from './types';
+
+type MonitorListQuery = string | URLSearchParams | Record<string, string | number | boolean | undefined | null>;
+
+function appendQuery(path: string, query?: MonitorListQuery) {
+  if (!query) return path;
+  const params =
+    typeof query === 'string'
+      ? new URLSearchParams(query.startsWith('?') ? query.slice(1) : query)
+      : query instanceof URLSearchParams
+        ? query
+        : new URLSearchParams(
+            Object.entries(query).flatMap(([key, value]) => (value == null || value === '' ? [] : [[key, String(value)]]))
+          );
+  const queryString = params.toString();
+  return queryString ? `${path}?${queryString}` : path;
+}
+
+export const api = {
+  monitors: {
+    list: <T = PageResult<Monitor>>(query?: MonitorListQuery) => apiMessageGet<T>(appendQuery('/monitors/manage', query)),
+    page: <T = PageResult<Monitor>>(query: MonitorQueryState) => apiMessageGet<T>(buildMonitorUrl(query)),
+    detail: <T = Monitor>(monitorId: string | number) => apiMessageGet<T>(buildMonitorDetailUrl(String(monitorId))),
+    editorDetail: <T = MonitorDetailResponse>(monitorId: string | number) =>
+      apiMessageGet<T>(buildMonitorEditorMonitorUrl(String(monitorId))),
+    editorCollectors: <T = { content?: CollectorSummary[] }>() => apiMessageGet<T>(buildMonitorEditorCollectorsUrl()),
+    editorParamDefines: (app: string) => apiMessageGet<ParamDefine[]>(buildMonitorEditorParamDefinesUrl(app)),
+    favoriteMetrics: (monitorId: string | number) => apiMessageGet<string[]>(buildMonitorFavoriteUrl(String(monitorId))),
+    addFavoriteMetric: (monitorId: string | number, metricName: string) =>
+      apiMessagePost<unknown>(buildMonitorFavoriteUrl(String(monitorId), metricName), null),
+    removeFavoriteMetric: (monitorId: string | number, metricName: string) =>
+      apiMessageDelete<unknown>(buildMonitorFavoriteUrl(String(monitorId), metricName)),
+    grafanaDashboard: <T = unknown>(monitorId: string | number) => apiMessageGet<T>(buildMonitorGrafanaUrl(String(monitorId))),
+    deleteGrafanaDashboard: (monitorId: string | number) => apiMessageDelete<unknown>(buildMonitorGrafanaUrl(String(monitorId))),
+    realtimeMetric: <T = unknown>(monitorId: string | number, metricName: string) =>
+      apiMessageGet<T>(buildMonitorRealtimeMetricUrl(String(monitorId), metricName)),
+    warehouseStorageStatus: <T = unknown>() => apiMessageGet<T>('/warehouse/storage/status'),
+    historyMetricCatalogDefine: (monitor: Monitor) =>
+      apiMessageGet<MonitorHistoryCatalogDefineResponse>(buildMonitorHistoryCatalogUrl(monitor)),
+    historyMetric: (monitor: Monitor, item: MonitorHistoryMetricCatalogItem, query?: Parameters<typeof buildMonitorHistoryMetricDataUrl>[2]) =>
+      apiMessageGet<MonitorHistoryData>(buildMonitorHistoryMetricDataUrl(monitor, item, query)),
+    copy: (monitorId: string | number) => apiMessagePost<unknown>(buildCopyMonitorUrl(monitorId), null),
+    enable: (ids: Array<string | number>) => apiMessageGet<unknown>(buildEnableMonitorsUrl(ids)),
+    pause: (ids: Array<string | number>) => apiMessageDelete<unknown>(buildPauseMonitorsUrl(ids)),
+    delete: (ids: Array<string | number>) => apiMessageDelete<unknown>(buildDeleteMonitorsUrl(ids)),
+    appHierarchy: <T = unknown>(locale?: string | null) =>
+      apiMessageGet<T>(`/apps/hierarchy${locale ? `?lang=${encodeURIComponent(locale)}` : ''}`),
+    create: <T = unknown>(payload: unknown) => apiMessagePost<T>('/monitor', payload),
+    update: <T = unknown>(payload: unknown) => apiMessagePut<T>('/monitor', payload),
+    detect: <T = unknown>(payload: unknown) => apiMessagePost<T>('/monitor/detect', payload),
+    import: <T = unknown>(body: FormData) => apiMessagePost<T>(buildImportMonitorsUrl(), body),
+    export: (ids: Array<string | number>, type: 'JSON' | 'EXCEL') => apiMessageGet<Blob>(buildExportMonitorsUrl(ids, type)),
+    exportAll: (type: 'JSON' | 'EXCEL') => apiMessageGet<Blob>(buildExportAllMonitorsUrl(type)),
+    exportResponse: (ids: Array<string | number>, type: 'JSON' | 'EXCEL') => apiDownload(buildExportMonitorsUrl(ids, type)),
+    exportAllResponse: (type: 'JSON' | 'EXCEL') => apiDownload(buildExportAllMonitorsUrl(type))
+  }
+};
diff --git a/web-next/lib/monitor-editor/controller.test.ts b/web-next/lib/monitor-editor/controller.test.ts
index d26ce61851..18166cf70e 100644
--- a/web-next/lib/monitor-editor/controller.test.ts
+++ b/web-next/lib/monitor-editor/controller.test.ts
@@ -1,20 +1,40 @@
 import { describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 import {
+  applyMonitorHostNameAutofill,
+  buildMonitorEditCacheKey,
+  buildMonitorEditorCollectorsUrl,
+  buildMonitorEditorMonitorUrl,
+  buildMonitorEditorParamDefinesUrl,
   buildMonitorDetectPayload,
   buildMonitorSavePayload,
   createMonitor,
+  createMonitorFromFacade,
   detectMonitor,
+  detectMonitorFromFacade,
+  generateMonitorEditorReadableName,
   isValidMonitorCronExpression,
   loadMonitorEditorDraft,
+  loadMonitorEditorDraftFromFacade,
   loadMonitorScrapeDraft,
+  loadMonitorScrapeDraftFromFacade,
+  resolveMonitorEditorParamChangeNotice,
+  shouldPreserveMonitorScrapeParamsForLoad,
   syncMonitorDependentDisplay,
   updateMonitor,
+  updateMonitorFromFacade,
   updateMonitorEditorParam,
   validateMonitorEditorDraft
 } from './controller';
 
 describe('monitor editor controller', () => {
+  it('exposes stable editor resource URLs for workbench cache keys', () => {
+    expect(buildMonitorEditorCollectorsUrl()).toBe('/collector');
+    expect(buildMonitorEditorMonitorUrl('42')).toBe('/monitor/42');
+    expect(buildMonitorEditorParamDefinesUrl('website')).toBe('/apps/website/params');
+    expect(buildMonitorEditCacheKey(42)).toBe('monitor-editor-edit:/monitor/42:/collector');
+  });
+
   it('loads new monitor draft from app params and collectors', async () => {
     const apiGet = vi.fn()
       .mockResolvedValueOnce({ content: [{ collector: { name: 'edge-a' } }] })
@@ -23,11 +43,86 @@ describe('monitor editor controller', () => {
 
     await expect(loadMonitorEditorDraft(apiGet as any, 'new', { app: 'website' })).resolves.toMatchObject({
       monitor: { app: 'website', scrape: 'static', scheduleType: 'interval', intervals: 60 },
-      collectors: ['edge-a'],
+      collectors: [{ name: 'edge-a' }],
       params: [{ field: 'host', paramValue: '127.0.0.1' }]
     });
   });
 
+  it('loads monitor editor drafts from facade readers without exposing raw URLs to routes', async () => {
+    const readMonitorDetail = vi.fn(async () => ({
+      monitor: {
+        id: 42,
+        app: 'mysql',
+        name: 'mysql-prod',
+        instance: '127.0.0.1:3306',
+        scrape: 'static',
+        status: 2
+      },
+      params: [{ id: 25, field: 'host', paramValue: '127.0.0.1' }],
+      collector: 'edge-a',
+      grafanaDashboard: { enabled: true }
+    }));
+    const readCollectors = vi.fn(async () => ({
+      content: [
+        { collector: { name: 'main-default-collector' } },
+        { collector: { name: 'edge-a', ip: '10.0.0.12', status: 0, mode: 'private' } }
+      ]
+    }));
+    const readParamDefines = vi.fn(async (app: string) => (app === 'mysql' ? [{ field: 'host', type: 'text', defaultValue: '' }] : []));
+
+    await expect(
+      loadMonitorEditorDraftFromFacade(
+        {
+          readMonitorDetail,
+          readCollectors,
+          readParamDefines
+        },
+        'edit',
+        { monitorId: '42' }
+      )
+    ).resolves.toMatchObject({
+      monitor: { id: 42, app: 'mysql', scrape: 'static', scheduleType: 'interval', intervals: 60 },
+      collector: 'edge-a',
+      collectors: [{ name: 'edge-a', ip: '10.0.0.12', status: 0, mode: 'private' }],
+      params: [{ id: 25, field: 'host', paramValue: '127.0.0.1' }],
+      grafanaDashboard: { enabled: true }
+    });
+
+    expect(readMonitorDetail).toHaveBeenCalledWith('42');
+    expect(readCollectors).toHaveBeenCalledTimes(1);
+    expect(readParamDefines).toHaveBeenCalledWith('mysql');
+  });
+
+  it('maps the built-in main collector to system default dispatch instead of a pinned collector', async () => {
+    const apiGet = vi.fn()
+      .mockResolvedValueOnce({
+        monitor: {
+          id: 42,
+          app: 'mysql',
+          name: 'mysql-prod',
+          instance: '127.0.0.1:3306',
+          scrape: 'static',
+          status: 2
+        },
+        collector: 'main-default-collector',
+        params: [{ id: 25, field: 'host', paramValue: '127.0.0.1' }]
+      })
+      .mockResolvedValueOnce({
+        content: [
+          { collector: { name: 'main-default-collector' } },
+          { collector: { name: 'edge-a' } }
+        ]
+      })
+      .mockResolvedValueOnce([{ field: 'host', type: 'text', defaultValue: '' }])
+      .mockResolvedValueOnce([]);
+
+    await expect(loadMonitorEditorDraft(apiGet as any, 'edit', { monitorId: '42' })).resolves.toMatchObject({
+      collector: '',
+      collectors: [{ name: 'edge-a' }],
+      params: [{ id: 25, field: 'host', paramValue: '127.0.0.1' }]
+    });
+  });
+
   it('builds save/detect payloads and posts them', async () => {
     const draft = {
       monitor: { id: 1, app: 'website', name: 'demo', instance: '127.0.0.1', scrape: 'static', status: 0 },
@@ -61,6 +156,72 @@ describe('monitor editor controller', () => {
     expect(apiPost).toHaveBeenCalledWith('/monitor/detect', buildMonitorDetectPayload(draft));
   });
 
+  it('submits save and detect payloads through facade writers', async () => {
+    const draft = {
+      monitor: { id: 1, app: 'website', name: 'demo', instance: '127.0.0.1', scrape: 'static', status: 0 },
+      collector: 'edge-a',
+      grafanaDashboard: { enabled: false },
+      params: [{ field: 'host', paramValue: '127.0.0.1' }],
+      advancedParams: [],
+      scrapeParams: []
+    } as any;
+    const writeCreate = vi.fn().mockResolvedValue(undefined);
+    const writeUpdate = vi.fn().mockResolvedValue(undefined);
+    const writeDetect = vi.fn().mockResolvedValue(undefined);
+
+    await createMonitorFromFacade(writeCreate, draft);
+    await updateMonitorFromFacade(writeUpdate, draft);
+    await detectMonitorFromFacade(writeDetect, draft);
+
+    expect(writeCreate).toHaveBeenCalledWith(buildMonitorSavePayload(draft));
+    expect(writeUpdate).toHaveBeenCalledWith(buildMonitorSavePayload(draft));
+    expect(writeDetect).toHaveBeenCalledWith(buildMonitorDetectPayload(draft));
+  });
+
+  it('matches Angular new-monitor host-change auto name behavior without touching edit mode', () => {
+    const draft = {
+      monitor: { id: 0, app: 'website', name: '', instance: '', scrape: 'static', status: 0 },
+      collector: '',
+      grafanaDashboard: { enabled: false },
+      params: [{ field: 'host', paramValue: 'example.com' }],
+      paramDefines: [{ field: 'host', type: 'text' }],
+      advancedParams: [],
+      advancedParamDefines: [],
+      scrapeParams: [],
+      scrapeParamDefines: [],
+      collectors: []
+    } as any;
+
+    expect(
+      applyMonitorHostNameAutofill(draft, {
+        mode: 'new',
+        field: 'host',
+        generateName: () => 'Bright_Probe_23AB'
+      }).monitor.name
+    ).toBe('Bright_Probe_23AB');
+
+    expect(
+      applyMonitorHostNameAutofill(
+        { ...draft, monitor: { ...draft.monitor, name: 'operator-name' } },
+        {
+          mode: 'new',
+          field: 'host',
+          generateName: () => 'Bright_Probe_23AB'
+        }
+      ).monitor.name
+    ).toBe('operator-name');
+
+    expect(
+      applyMonitorHostNameAutofill(draft, {
+        mode: 'edit',
+        field: 'host',
+        generateName: () => 'Bright_Probe_23AB'
+      }).monitor.name
+    ).toBe('');
+
+    expect(generateMonitorEditorReadableName(() => 0)).toBe('Quick_Probe_22AA');
+  });
+
   it('preserves existing param ids in update payloads', () => {
     const draft = {
       monitor: { id: 42, app: 'website', name: 'demo', instance: 'example.com:80', scrape: 'static', status: 1 },
@@ -76,7 +237,7 @@ describe('monitor editor controller', () => {
 
     expect(buildMonitorSavePayload(draft)).toEqual({
       monitor: { ...draft.monitor, instance: 'example.com' },
-      collector: '',
+      collector: null,
       params: [
         { id: 1, field: 'host', paramValue: 'example.com', type: 0 },
         { id: 2, field: 'port', paramValue: '80', type: 0 }
@@ -93,6 +254,17 @@ describe('monitor editor controller', () => {
     });
   });
 
+  it('loads dynamic scrape draft params from a facade param-define reader', async () => {
+    const readParamDefines = vi.fn(async () => [{ field: 'service', type: 'text', defaultValue: 'catalog' }]);
+
+    await expect(loadMonitorScrapeDraftFromFacade(readParamDefines, 'http_sd')).resolves.toEqual({
+      scrapeParams: [{ field: 'service', type: 1, paramValue: 'catalog' }],
+      scrapeParamDefines: [{ field: 'service', type: 'text', defaultValue: 'catalog' }]
+    });
+
+    expect(readParamDefines).toHaveBeenCalledWith('http_sd');
+  });
+
   it('preserves existing scrape param values on reload', async () => {
     const apiGet = vi.fn().mockResolvedValue([{ field: 'service', type: 'text', defaultValue: 'catalog' }]);
     await expect(loadMonitorScrapeDraft(apiGet as any, 'http_sd', [{ field: 'service', paramValue: 'inventory' } as any])).resolves.toEqual({
@@ -101,6 +273,14 @@ describe('monitor editor controller', () => {
     });
   });
 
+  it('matches Angular scrape reload lifecycle by preserving only the initial edit load', () => {
+    expect(shouldPreserveMonitorScrapeParamsForLoad(null, 'http_sd')).toBe(true);
+    expect(shouldPreserveMonitorScrapeParamsForLoad(undefined, 'nacos_sd')).toBe(true);
+    expect(shouldPreserveMonitorScrapeParamsForLoad(null, 'static')).toBe(false);
+    expect(shouldPreserveMonitorScrapeParamsForLoad('http_sd', 'dns_sd')).toBe(false);
+    expect(shouldPreserveMonitorScrapeParamsForLoad('static', 'http_sd')).toBe(false);
+  });
+
   it('syncs dependent visibility and boolean side effects', () => {
     const draft = {
       monitor: { app: 'api' },
@@ -126,6 +306,36 @@ describe('monitor editor controller', () => {
     expect(next.params[2].display).toBe(true);
   });
 
+  it('resolves Angular ssl port auto-change notification keys', () => {
+    const apiDraft = {
+      monitor: { app: 'api' },
+      params: [
+        { field: 'ssl', paramValue: false },
+        { field: 'port', paramValue: 80 }
+      ],
+      advancedParams: [],
+      scrapeParams: []
+    } as any;
+    const ftpDraft = {
+      ...apiDraft,
+      monitor: { app: 'ftp' },
+      params: [
+        { field: 'ssl', paramValue: false },
+        { field: 'port', paramValue: 21 }
+      ]
+    } as any;
+
+    expect(resolveMonitorEditorParamChangeNotice(apiDraft, 'params', 0, true)).toBe('monitor.new.notify.change-to-https');
+    expect(resolveMonitorEditorParamChangeNotice({ ...apiDraft, params: [{ field: 'ssl' }, { field: 'port', paramValue: 443 }] } as any, 'params', 0, false)).toBe(
+      'monitor.new.notify.change-to-http'
+    );
+    expect(resolveMonitorEditorParamChangeNotice(ftpDraft, 'params', 0, true)).toBe('monitor.new.notify.change-to-sftp');
+    expect(resolveMonitorEditorParamChangeNotice({ ...ftpDraft, params: [{ field: 'ssl' }, { field: 'port', paramValue: 22 }] } as any, 'params', 0, false)).toBe(
+      'monitor.new.notify.change-to-ftp'
+    );
+    expect(resolveMonitorEditorParamChangeNotice({ ...apiDraft, monitor: { app: 'website' } } as any, 'params', 0, true)).toBeNull();
+  });
+
   it('trims monitor and param strings in save payloads', () => {
     const draft = {
       monitor: { id: 42, app: 'website', name: '  demo monitor  ', instance: 'example.com:80', scrape: 'static', status: 1, description: '  desc  ' },
@@ -141,7 +351,7 @@ describe('monitor editor controller', () => {
 
     expect(buildMonitorSavePayload(draft)).toEqual({
       monitor: { ...draft.monitor, name: 'demo monitor', description: 'desc', instance: 'example.com' },
-      collector: '',
+      collector: null,
       params: [
         { id: 1, field: 'host', paramValue: 'example.com', type: 1 },
         { id: 2, field: 'uri', paramValue: '/health', type: 1 }
@@ -150,6 +360,66 @@ describe('monitor editor controller', () => {
     });
   });
 
+  it('matches Angular detect/save payload shape for merged params, host instance, and Grafana', () => {
+    const draft = {
+      monitor: {
+        id: 42,
+        app: 'prometheus',
+        name: '  prometheus monitor  ',
+        instance: 'prometheus:9090',
+        scrape: 'http_sd',
+        scheduleType: 'interval',
+        intervals: 60,
+        status: 1
+      },
+      collector: ' edge-a ',
+      grafanaDashboard: { enabled: true, template: '{"panels":[]}' },
+      params: [
+        { id: 1, field: 'host', paramValue: ' prometheus.local ', type: 1 },
+        { id: 2, field: 'port', paramValue: ' 9090 ', type: 1 }
+      ],
+      paramDefines: [
+        { field: 'host', type: 'text' },
+        { field: 'port', type: 'text' }
+      ],
+      advancedParams: [{ id: 3, field: 'timeout', paramValue: ' 3 ', type: 1 }],
+      advancedParamDefines: [{ field: 'timeout', type: 'text' }],
+      scrapeParams: [{ id: 4, field: 'sd_url', paramValue: ' https://sd.local/targets ', type: 1 }],
+      scrapeParamDefines: [{ field: 'sd_url', type: 'text' }]
+    } as any;
+
+    expect(buildMonitorDetectPayload(draft)).toEqual({
+      monitor: {
+        ...draft.monitor,
+        name: 'prometheus monitor',
+        instance: 'prometheus.local'
+      },
+      collector: 'edge-a',
+      params: [
+        { id: 1, field: 'host', paramValue: 'prometheus.local', type: 1 },
+        { id: 2, field: 'port', paramValue: '9090', type: 1 },
+        { id: 3, field: 'timeout', paramValue: '3', type: 1 },
+        { id: 4, field: 'sd_url', paramValue: 'https://sd.local/targets', type: 1 }
+      ]
+    });
+    expect(buildMonitorDetectPayload(draft)).not.toHaveProperty('grafanaDashboard');
+    expect(buildMonitorSavePayload(draft)).toEqual({
+      monitor: {
+        ...draft.monitor,
+        name: 'prometheus monitor',
+        instance: 'prometheus.local'
+      },
+      collector: 'edge-a',
+      params: [
+        { id: 1, field: 'host', paramValue: 'prometheus.local', type: 1 },
+        { id: 2, field: 'port', paramValue: '9090', type: 1 },
+        { id: 3, field: 'timeout', paramValue: '3', type: 1 },
+        { id: 4, field: 'sd_url', paramValue: 'https://sd.local/targets', type: 1 }
+      ],
+      grafanaDashboard: { enabled: true, template: '{"panels":[]}' }
+    });
+  });
+
   it('validates required fields and cron expressions before detect/save', () => {
     const t = createTranslatorMock();
     const draft = {
@@ -184,6 +454,46 @@ describe('monitor editor controller', () => {
 
     draft.params[0].paramValue = 'example.com';
     expect(validateMonitorEditorDraft(draft, t)).toBe('Cron expression is invalid');
+    expect(validateMonitorEditorDraft(draft, t, { validateCronFormat: false })).toBeNull();
+
+    draft.monitor.cronExpression = '   ';
+    expect(validateMonitorEditorDraft(draft, t, { validateCronFormat: false })).toBe('Cron expression is invalid');
+  });
+
+  it('localizes monitor editor validation copy in zh-CN', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+    const draft = {
+      monitor: {
+        id: 0,
+        app: 'website',
+        name: '   ',
+        instance: '',
+        scrape: 'static',
+        status: 0,
+        scheduleType: 'cron',
+        cronExpression: 'bad cron'
+      },
+      params: [
+        { field: 'host', paramValue: '   ', display: true },
+        { field: 'port', paramValue: 80, display: true }
+      ],
+      paramDefines: [
+        { field: 'host', type: 'text', required: true, name: 'Host' },
+        { field: 'port', type: 'number', required: true, name: 'Port' }
+      ],
+      advancedParams: [],
+      advancedParamDefines: [],
+      scrapeParams: [],
+      scrapeParamDefines: []
+    } as any;
+
+    expect(validateMonitorEditorDraft(draft, t)).toBe('请输入监控名称');
+
+    draft.monitor.name = 'demo';
+    expect(validateMonitorEditorDraft(draft, t)).toBe('请填写 Host');
+
+    draft.params[0].paramValue = 'example.com';
+    expect(validateMonitorEditorDraft(draft, t)).toBe('Cron 表达式不合法');
   });
 
   it('accepts common five-field and six-field cron expressions', () => {
diff --git a/web-next/lib/monitor-editor/controller.ts b/web-next/lib/monitor-editor/controller.ts
index 782d5a7654..8d7d8f369a 100644
--- a/web-next/lib/monitor-editor/controller.ts
+++ b/web-next/lib/monitor-editor/controller.ts
@@ -16,7 +16,7 @@ export type MonitorEditorDraft = {
   advancedParamDefines: ParamDefine[];
   scrapeParams: Param[];
   scrapeParamDefines: ParamDefine[];
-  collectors: string[];
+  collectors: Array<MonitorCollectorOption | string>;
 };
 
 export type MonitorScrapeDraft = {
@@ -24,16 +24,86 @@ export type MonitorScrapeDraft = {
   scrapeParamDefines: ParamDefine[];
 };
 
+export type MonitorCollectorOption = {
+  name: string;
+  ip?: string;
+  status?: string | number;
+  online?: boolean;
+  mode?: string | null;
+};
+
 type ParamCollectionKind = 'params' | 'advancedParams' | 'scrapeParams';
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
 
-type MonitorDetailResponse = {
+export type MonitorDetailResponse = {
   monitor: Monitor;
   params?: Param[];
   collector?: string | null;
   grafanaDashboard?: GrafanaDashboard;
 };
 
+export type MonitorEditorDraftReaders = {
+  readMonitorDetail?: (monitorId: string) => Promise<MonitorDetailResponse>;
+  readCollectors: () => Promise<{ content?: CollectorSummary[] }>;
+  readParamDefines: (app: string) => Promise<ParamDefine[]>;
+};
+
+const SYSTEM_DEFAULT_COLLECTOR = 'main-default-collector';
+const MONITOR_NAME_ADJECTIVES = ['quick', 'bright', 'calm', 'brave', 'cool', 'eager', 'gentle', 'lively'];
+const MONITOR_NAME_NOUNS = ['probe', 'service', 'signal', 'watch', 'beacon', 'node', 'pulse', 'guard'];
+const MONITOR_NAME_DIGITS = '23456789';
+const MONITOR_NAME_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz';
+
+function isSystemDefaultCollector(value?: string | null) {
+  return value === SYSTEM_DEFAULT_COLLECTOR;
+}
+
+function normalizeCollectorForDraft(value?: string | null) {
+  return isSystemDefaultCollector(value) ? '' : value || '';
+}
+
+function normalizeCollectorForPayload(value?: string | null) {
+  const normalized = value?.trim();
+  return normalized && !isSystemDefaultCollector(normalized) ? normalized : null;
+}
+
+function pickRandomToken(values: string[], random: () => number) {
+  return values[Math.floor(random() * values.length)] || values[0];
+}
+
+function capitalizeMonitorNamePart(value: string) {
+  return value.charAt(0).toUpperCase() + value.slice(1);
+}
+
+export function generateMonitorEditorReadableName(random: () => number = Math.random) {
+  const adjective = capitalizeMonitorNamePart(pickRandomToken(MONITOR_NAME_ADJECTIVES, random));
+  const noun = capitalizeMonitorNamePart(pickRandomToken(MONITOR_NAME_NOUNS, random));
+  const randomDigits = Array.from({ length: 2 }, () => MONITOR_NAME_DIGITS.charAt(Math.floor(random() * MONITOR_NAME_DIGITS.length))).join('');
+  const randomChars = Array.from({ length: 2 }, () => MONITOR_NAME_CHARS.charAt(Math.floor(random() * MONITOR_NAME_CHARS.length))).join('');
+  return `${adjective}_${noun}_${randomDigits}${randomChars}`;
+}
+
+export function applyMonitorHostNameAutofill(
+  draft: MonitorEditorDraft,
+  {
+    mode,
+    field,
+    generateName = generateMonitorEditorReadableName
+  }: { mode: MonitorEditorMode; field?: string; generateName?: () => string }
+) {
+  if (mode !== 'new' || field !== 'host' || draft.monitor.name?.trim()) {
+    return draft;
+  }
+
+  return {
+    ...draft,
+    monitor: {
+      ...draft.monitor,
+      name: generateName()
+    }
+  };
+}
+
 function paramType(type?: string) {
   if (type === 'number') return 0;
   if (type === 'key-value') return 3;
@@ -126,7 +196,8 @@ export function isValidMonitorCronExpression(cronExpression: string): boolean {
   return cronRegex.test(cronExpression);
 }
 
-export function validateMonitorEditorDraft(draft: MonitorEditorDraft, t: Translator) {
+export function validateMonitorEditorDraft(draft: MonitorEditorDraft, t: Translator, options: { validateCronFormat?: boolean } = {}) {
+  const validateCronFormat = options.validateCronFormat !== false;
   const normalizedMonitor = normalizeMonitorFields(draft.monitor);
   if (!normalizedMonitor.app?.trim()) return t('monitor.editor.validation.app');
   if (!normalizedMonitor.name?.trim()) return t('monitor.editor.validation.name');
@@ -151,7 +222,8 @@ export function validateMonitorEditorDraft(draft: MonitorEditorDraft, t: Transla
   }
 
   if (normalizedMonitor.scheduleType === 'cron') {
-    if (!isValidMonitorCronExpression(normalizedMonitor.cronExpression || '')) {
+    const cronExpression = normalizedMonitor.cronExpression || '';
+    if (!cronExpression.trim() || (validateCronFormat && !isValidMonitorCronExpression(cronExpression))) {
       return t('monitor.editor.validation.cron');
     }
   } else if (normalizedMonitor.intervals == null || !Number.isFinite(Number(normalizedMonitor.intervals)) || Number(normalizedMonitor.intervals) <= 0) {
@@ -195,23 +267,60 @@ function mergeParamDrafts(defines: ParamDefine[], paramMap: Map<string, Param>)
   };
 }
 
+export function buildMonitorEditorParamDefinesUrl(app: string) {
+  return `/apps/${app}/params`;
+}
+
+export function buildMonitorEditorCollectorsUrl() {
+  return '/collector';
+}
+
+export function buildMonitorEditCacheKey(monitorId: number | string) {
+  return ['monitor-editor-edit', buildMonitorEditorMonitorUrl(String(monitorId)), buildMonitorEditorCollectorsUrl()].join(':');
+}
+
+export function buildMonitorEditorMonitorUrl(monitorId: string) {
+  return `/monitor/${monitorId}`;
+}
+
 async function loadParamDefines(apiGet: ApiGetter, app: string) {
-  return apiGet<ParamDefine[]>(`/apps/${app}/params`);
+  return apiGet<ParamDefine[]>(buildMonitorEditorParamDefinesUrl(app));
 }
 
 async function loadCollectors(apiGet: ApiGetter) {
-  const response = await apiGet<{ content?: CollectorSummary[] }>('/collector');
-  return (response.content || []).map(item => item.collector?.name).filter((value): value is string => Boolean(value));
+  const response = await apiGet<{ content?: CollectorSummary[] }>(buildMonitorEditorCollectorsUrl());
+  return normalizeCollectorSummaries(response);
+}
+
+function normalizeCollectorSummaries(response: { content?: CollectorSummary[] }) {
+  return (response.content || [])
+    .map(item => item.collector)
+    .filter((collector): collector is NonNullable<CollectorSummary['collector']> => Boolean(collector?.name) && !isSystemDefaultCollector(collector.name))
+    .map(collector => ({
+      name: String(collector.name),
+      ip: collector.ip,
+      status: collector.status,
+      online: collector.online,
+      mode: collector.mode
+    }));
 }
 
 export async function loadMonitorScrapeDraft(apiGet: ApiGetter, scrape?: string | null, existingParams?: Param[]) {
+  return loadMonitorScrapeDraftFromFacade(app => loadParamDefines(apiGet, app), scrape, existingParams);
+}
+
+export async function loadMonitorScrapeDraftFromFacade(
+  readParamDefines: (app: string) => Promise<ParamDefine[]>,
+  scrape?: string | null,
+  existingParams?: Param[]
+) {
   if (!scrape || scrape === 'static') {
     return {
       scrapeParams: [],
       scrapeParamDefines: []
     } satisfies MonitorScrapeDraft;
   }
-  const defines = await loadParamDefines(apiGet, scrape);
+  const defines = await readParamDefines(scrape);
   const paramMap = new Map((existingParams || []).map(param => [String(param.field), param]));
   const merged = mergeParamDrafts(defines, paramMap);
   return {
@@ -220,6 +329,10 @@ export async function loadMonitorScrapeDraft(apiGet: ApiGetter, scrape?: string
   } satisfies MonitorScrapeDraft;
 }
 
+export function shouldPreserveMonitorScrapeParamsForLoad(previousScrape: string | null | undefined, nextScrape: string) {
+  return !previousScrape && nextScrape !== 'static';
+}
+
 function syncDependentCollection(defines: ParamDefine[], params: Param[], fieldValues: Map<string, unknown>) {
   return params.map((param, index) => {
     const define = defines[index];
@@ -270,7 +383,46 @@ export function updateMonitorEditorParam(draft: MonitorEditorDraft, kind: ParamC
   return syncMonitorDependentDisplay(nextDraft);
 }
 
+export function resolveMonitorEditorParamChangeNotice(draft: MonitorEditorDraft, kind: ParamCollectionKind, index: number, value: unknown) {
+  const changedField = String(draft[kind][index]?.field || '');
+  if (changedField !== 'ssl' || typeof value !== 'boolean') {
+    return null;
+  }
+
+  const portParam = draft.params.find(param => param.field === 'port');
+  if (!portParam) {
+    return null;
+  }
+
+  const current = Number(portParam.paramValue);
+  if (draft.monitor.app === 'api') {
+    if (value && (!Number.isFinite(current) || current === 80)) return 'monitor.new.notify.change-to-https';
+    if (!value && (!Number.isFinite(current) || current === 443)) return 'monitor.new.notify.change-to-http';
+  }
+  if (draft.monitor.app === 'ftp') {
+    if (value && (!Number.isFinite(current) || current === 21)) return 'monitor.new.notify.change-to-sftp';
+    if (!value && (!Number.isFinite(current) || current === 22)) return 'monitor.new.notify.change-to-ftp';
+  }
+  return null;
+}
+
 export async function loadMonitorEditorDraft(apiGet: ApiGetter, mode: MonitorEditorMode, options: { app?: string; monitorId?: string }) {
+  return loadMonitorEditorDraftFromFacade(
+    {
+      readMonitorDetail: monitorId => apiGet<MonitorDetailResponse>(buildMonitorEditorMonitorUrl(monitorId)),
+      readCollectors: () => apiGet<{ content?: CollectorSummary[] }>(buildMonitorEditorCollectorsUrl()),
+      readParamDefines: app => loadParamDefines(apiGet, app)
+    },
+    mode,
+    options
+  );
+}
+
+export async function loadMonitorEditorDraftFromFacade(
+  readers: MonitorEditorDraftReaders,
+  mode: MonitorEditorMode,
+  options: { app?: string; monitorId?: string }
+) {
   const baseMonitor: Monitor = {
     id: options.monitorId ? Number(options.monitorId) : 0,
     app: options.app || 'website',
@@ -290,7 +442,10 @@ export async function loadMonitorEditorDraft(apiGet: ApiGetter, mode: MonitorEdi
   let grafanaDashboard: GrafanaDashboard = { enabled: false };
 
   if (mode === 'edit' && options.monitorId) {
-    const detail = await apiGet<MonitorDetailResponse>(`/monitor/${options.monitorId}`);
+    if (!readers.readMonitorDetail) {
+      throw new Error('Monitor detail reader is required for edit drafts');
+    }
+    const detail = await readers.readMonitorDetail(options.monitorId);
     monitor = {
       ...baseMonitor,
       ...detail.monitor,
@@ -301,14 +456,14 @@ export async function loadMonitorEditorDraft(apiGet: ApiGetter, mode: MonitorEdi
       annotations: detail.monitor.annotations || {}
     };
     paramMap = new Map((detail.params || []).map(param => [String(param.field), param]));
-    collector = detail.collector || '';
+    collector = normalizeCollectorForDraft(detail.collector);
     grafanaDashboard = detail.grafanaDashboard || { enabled: false };
   }
 
   const [collectors, mainDefines, scrapeDefines] = await Promise.all([
-    loadCollectors(apiGet),
-    loadParamDefines(apiGet, monitor.app),
-    monitor.scrape && monitor.scrape !== 'static' ? loadParamDefines(apiGet, monitor.scrape) : Promise.resolve<ParamDefine[]>([])
+    readers.readCollectors().then(normalizeCollectorSummaries),
+    readers.readParamDefines(monitor.app),
+    monitor.scrape && monitor.scrape !== 'static' ? readers.readParamDefines(monitor.scrape) : Promise.resolve<ParamDefine[]>([])
   ]);
 
   const mergedMain = mergeParamDrafts(mainDefines, paramMap);
@@ -336,7 +491,7 @@ export function buildMonitorSavePayload(draft: MonitorEditorDraft) {
   ];
   return {
     monitor: normalizeMonitorInstanceForSave(normalizeMonitorFields(draft.monitor), params),
-    collector: draft.collector,
+    collector: normalizeCollectorForPayload(draft.collector),
     params,
     grafanaDashboard: draft.grafanaDashboard
   };
@@ -350,7 +505,7 @@ export function buildMonitorDetectPayload(draft: MonitorEditorDraft) {
   ];
   return {
     monitor: normalizeMonitorInstanceForSave(normalizeMonitorFields(draft.monitor), params),
-    collector: draft.collector,
+    collector: normalizeCollectorForPayload(draft.collector),
     params
   };
 }
@@ -359,10 +514,22 @@ export async function createMonitor(apiPost: ApiPoster, draft: MonitorEditorDraf
   return apiPost<unknown>('/monitor', buildMonitorSavePayload(draft));
 }
 
+export async function createMonitorFromFacade(writeCreateMonitor: (payload: unknown) => Promise<unknown>, draft: MonitorEditorDraft) {
+  return writeCreateMonitor(buildMonitorSavePayload(draft));
+}
+
 export async function updateMonitor(apiPut: ApiPutter, draft: MonitorEditorDraft) {
   return apiPut<unknown>('/monitor', buildMonitorSavePayload(draft));
 }
 
+export async function updateMonitorFromFacade(writeUpdateMonitor: (payload: unknown) => Promise<unknown>, draft: MonitorEditorDraft) {
+  return writeUpdateMonitor(buildMonitorSavePayload(draft));
+}
+
 export async function detectMonitor(apiPost: ApiPoster, draft: MonitorEditorDraft) {
   return apiPost<unknown>('/monitor/detect', buildMonitorDetectPayload(draft));
 }
+
+export async function detectMonitorFromFacade(writeDetectMonitor: (payload: unknown) => Promise<unknown>, draft: MonitorEditorDraft) {
+  return writeDetectMonitor(buildMonitorDetectPayload(draft));
+}
diff --git a/web-next/lib/monitor-editor/navigation.test.ts b/web-next/lib/monitor-editor/navigation.test.ts
index 129460febf..38528d8b67 100644
--- a/web-next/lib/monitor-editor/navigation.test.ts
+++ b/web-next/lib/monitor-editor/navigation.test.ts
@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest';
-import { buildMonitorEditorReturnUrl } from './navigation';
+import { buildMonitorEditorCancelUrl, buildMonitorEditorReturnUrl } from './navigation';
 
 describe('monitor editor navigation', () => {
   it('builds the filtered return path when app is present', () => {
@@ -26,4 +26,11 @@ describe('monitor editor navigation', () => {
       '/monitors?app=website&labels=team%3Dplatform&pageIndex=1&pageSize=20&entityId=42&entityName=Checkout+Service'
     );
   });
+
+  it('keeps Angular-style cancel navigation on the monitor list root unless a safe return target exists', () => {
+    expect(buildMonitorEditorCancelUrl()).toBe('/monitors');
+    expect(buildMonitorEditorCancelUrl({ labels: 'team=platform', pageIndex: '2' })).toBe('/monitors');
+    expect(buildMonitorEditorCancelUrl({ returnTo: '/entities/42?returnLabel=Checkout' })).toBe('/entities/42');
+    expect(buildMonitorEditorCancelUrl({ returnTo: '//evil.example', labels: 'team=platform' })).toBe('/monitors');
+  });
 });
diff --git a/web-next/lib/monitor-editor/navigation.ts b/web-next/lib/monitor-editor/navigation.ts
index 6c158b5c49..aed5932e31 100644
--- a/web-next/lib/monitor-editor/navigation.ts
+++ b/web-next/lib/monitor-editor/navigation.ts
@@ -8,3 +8,7 @@ export function buildMonitorEditorReturnUrl(app?: string | null, returnContext?:
     app
   });
 }
+
+export function buildMonitorEditorCancelUrl(returnContext?: MonitorEditorReturnContext | null) {
+  return buildMonitorListReturnHref(returnContext?.returnTo ? { returnTo: returnContext.returnTo } : undefined);
+}
diff --git a/web-next/lib/monitor-editor/query-state.test.ts b/web-next/lib/monitor-editor/query-state.test.ts
new file mode 100644
index 0000000000..63cafe58ae
--- /dev/null
+++ b/web-next/lib/monitor-editor/query-state.test.ts
@@ -0,0 +1,107 @@
+import { describe, expect, it } from 'vitest';
+import {
+  buildMonitorNewDefaultAppRedirectUrl,
+  hasMonitorNewAppParam,
+  readMonitorEditRouteState,
+  readMonitorNewRouteState
+} from './query-state';
+
+describe('monitor editor query state', () => {
+  it('normalizes multi-value URL search params into the first monitor new setup context value', () => {
+    expect(
+      readMonitorNewRouteState({
+        app: ['prometheus', 'website'],
+        labels: ['team=platform', 'team=ops'],
+        pageIndex: ['2', '0'],
+        pageSize: ['20', '8'],
+        entityId: ['42', '99'],
+        entityName: ['Checkout Service', 'Ignored Service'],
+        timeRange: ['custom', '1h'],
+        start: ['1715200000000', 'now-1h'],
+        end: ['1715203600000', 'now'],
+        refresh: ['30s', 'off'],
+        live: ['true', 'false'],
+        tz: ['UTC', 'Asia/Shanghai'],
+        returnTo: ['/entities/42?returnLabel=Checkout', '//evil.example']
+      })
+    ).toEqual({
+      app: 'prometheus',
+      returnContext: {
+        labels: 'team=platform',
+        pageIndex: '2',
+        pageSize: '20',
+        entityId: '42',
+        entityName: 'Checkout Service',
+        timeRange: 'custom',
+        start: '1715200000000',
+        end: '1715203600000',
+        refresh: '30s',
+        live: 'true',
+        tz: 'UTC',
+        returnTo: '/entities/42'
+      }
+    });
+  });
+
+  it('defaults to the website monitor type when setup route query values are absent', () => {
+    expect(readMonitorNewRouteState()).toEqual({
+      app: 'website',
+      returnContext: {
+        labels: null,
+        pageIndex: null,
+        pageSize: null,
+        entityId: null,
+        entityName: null,
+        timeRange: null,
+        start: null,
+        end: null,
+        refresh: null,
+        live: null,
+        tz: null,
+        returnTo: null
+      }
+    });
+  });
+
+  it('identifies whether the monitor new route already carries the Angular app selector', () => {
+    expect(hasMonitorNewAppParam({ app: 'website' })).toBe(true);
+    expect(hasMonitorNewAppParam({ app: ['prometheus', 'website'] })).toBe(true);
+    expect(hasMonitorNewAppParam()).toBe(false);
+    expect(hasMonitorNewAppParam({ app: '' })).toBe(false);
+    expect(buildMonitorNewDefaultAppRedirectUrl()).toBe('/monitors/new?app=website');
+  });
+
+  it('normalizes multi-value URL search params into the first monitor edit return context value', () => {
+    expect(
+      readMonitorEditRouteState({
+        labels: ['region=us-east', 'region=eu-west'],
+        pageIndex: ['3', '0'],
+        pageSize: ['50', '8'],
+        entityId: ['101', '202'],
+        entityName: ['Payment Gateway', 'Ignored Gateway'],
+        timeRange: ['custom', '6h'],
+        start: ['1715200000000', 'now-6h'],
+        end: ['1715203600000', 'now'],
+        refresh: ['60s', 'off'],
+        live: ['false', 'true'],
+        tz: ['Asia/Shanghai', 'UTC'],
+        returnTo: ['/monitors?app=website&returnLabel=Monitors', '//evil.example']
+      })
+    ).toEqual({
+      returnContext: {
+        labels: 'region=us-east',
+        pageIndex: '3',
+        pageSize: '50',
+        entityId: '101',
+        entityName: 'Payment Gateway',
+        timeRange: 'custom',
+        start: '1715200000000',
+        end: '1715203600000',
+        refresh: '60s',
+        live: 'false',
+        tz: 'Asia/Shanghai',
+        returnTo: '/monitors?app=website'
+      }
+    });
+  });
+});
diff --git a/web-next/lib/monitor-editor/query-state.ts b/web-next/lib/monitor-editor/query-state.ts
new file mode 100644
index 0000000000..643cd10276
--- /dev/null
+++ b/web-next/lib/monitor-editor/query-state.ts
@@ -0,0 +1,57 @@
+import { createCompatSearchParamReader, type SearchParamsRecord } from '../compat/search-params';
+import type { MonitorEditorReturnContext } from './navigation';
+
+export type MonitorNewSearchParams = SearchParamsRecord;
+export type MonitorEditSearchParams = SearchParamsRecord;
+
+export type MonitorNewRouteState = {
+  app: string;
+  returnContext: MonitorEditorReturnContext;
+};
+
+export type MonitorEditRouteState = {
+  returnContext: MonitorEditorReturnContext;
+};
+
+function readMonitorEditorReturnContext(searchParams: SearchParamsRecord = {}): MonitorEditorReturnContext {
+  const reader = createCompatSearchParamReader(searchParams);
+
+  return {
+    labels: reader.get('labels'),
+    pageIndex: reader.get('pageIndex'),
+    pageSize: reader.get('pageSize'),
+    entityId: reader.get('entityId'),
+    entityName: reader.get('entityName'),
+    timeRange: reader.get('timeRange'),
+    start: reader.get('start'),
+    end: reader.get('end'),
+    refresh: reader.get('refresh'),
+    live: reader.get('live'),
+    tz: reader.get('tz'),
+    returnTo: reader.get('returnTo')
+  };
+}
+
+export function readMonitorNewRouteState(searchParams: MonitorNewSearchParams = {}): MonitorNewRouteState {
+  const reader = createCompatSearchParamReader(searchParams);
+
+  return {
+    app: reader.get('app') || 'website',
+    returnContext: readMonitorEditorReturnContext(searchParams)
+  };
+}
+
+export function hasMonitorNewAppParam(searchParams: MonitorNewSearchParams = {}) {
+  const reader = createCompatSearchParamReader(searchParams);
+  return Boolean(reader.get('app'));
+}
+
+export function buildMonitorNewDefaultAppRedirectUrl() {
+  return '/monitors/new?app=website';
+}
+
+export function readMonitorEditRouteState(searchParams: MonitorEditSearchParams = {}): MonitorEditRouteState {
+  return {
+    returnContext: readMonitorEditorReturnContext(searchParams)
+  };
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 18/50: feat(web-next): restore bulletin workbench compatibility

Posted by zh...@apache.org.
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 95c78995336d201f9600f58979a58b5fa7f4a1f4
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 02:02:27 2026 +0800

    feat(web-next): restore bulletin workbench compatibility
    
    Validation: vitest run app/bulletin/page.test.tsx components/pages/bulletin-center-surface.test.tsx components/pages/bulletin-manage-dialog.test.tsx components/pages/bulletin-metrics-table.test.tsx components/pages/bulletin-monitor-detail.chrome.test.ts lib/bulletin-center/controller.test.ts lib/bulletin-center/query-state.test.ts lib/bulletin-center/view-model.test.ts --pool=forks --maxWorkers=1 --minWorkers=1; ESLint same bulletin route/component/lib set; git diff --cached --check;  [...]
---
 web-next/app/bulletin/bulletin-page.tsx            |  37 ++++++
 web-next/app/bulletin/page.test.tsx                |  28 ++++-
 web-next/app/bulletin/page.tsx                     |  24 +---
 .../pages/bulletin-center-surface.test.tsx         |  35 ++++--
 .../components/pages/bulletin-center-surface.tsx   |  13 ++-
 .../components/pages/bulletin-manage-dialog.tsx    |   2 +-
 .../pages/bulletin-metrics-table.test.tsx          | 124 +++++++++++++++++++++
 .../components/pages/bulletin-metrics-table.tsx    |  90 ++++++++-------
 .../pages/bulletin-monitor-detail.chrome.test.ts   |  25 ++---
 web-next/lib/bulletin-center/view-model.test.ts    |  85 ++++++++++++--
 web-next/lib/bulletin-center/view-model.ts         |  37 ++++--
 11 files changed, 382 insertions(+), 118 deletions(-)

diff --git a/web-next/app/bulletin/bulletin-page.tsx b/web-next/app/bulletin/bulletin-page.tsx
new file mode 100644
index 0000000000..8883fb27ed
--- /dev/null
+++ b/web-next/app/bulletin/bulletin-page.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import React, { useCallback, useMemo, useState } from 'react';
+import { BulletinCenterSurface, type BulletinCenterData } from '../../components/pages/bulletin-center-surface';
+import { useI18n } from '../../components/providers/i18n-provider';
+import { ClientWorkbench } from '../../components/workbench/client-workbench';
+import { apiMessageGet } from '../../lib/api-client';
+import { loadBulletinData } from '../../lib/bulletin-center/controller';
+import { buildBulletinListUrl } from '../../lib/bulletin-center/query-state';
+
+const BULLETIN_CENTER_SETTLED_CACHE_TTL_MS = 10_000;
+
+export default function BulletinPage() {
+  const { t } = useI18n();
+  const [refreshTick, setRefreshTick] = useState(0);
+  const bulletinListSearch = ' '.repeat(refreshTick);
+  const bulletinListUrl = useMemo(() => buildBulletinListUrl(bulletinListSearch), [bulletinListSearch]);
+  const bulletinCenterCacheKey = useMemo(
+    () => ['bulletin-center', bulletinListUrl, refreshTick].join(':'),
+    [bulletinListUrl, refreshTick]
+  );
+
+  const load = useCallback(async (): Promise<BulletinCenterData> => {
+    return loadBulletinData(apiMessageGet, bulletinListSearch);
+  }, [bulletinListSearch]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('bulletin.loading')}
+      cacheKey={bulletinCenterCacheKey}
+      cacheSettledTtlMs={BULLETIN_CENTER_SETTLED_CACHE_TTL_MS}
+    >
+      {data => <BulletinCenterSurface data={data} refreshTick={refreshTick} onReload={() => setRefreshTick(value => value + 1)} />}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/bulletin/page.test.tsx b/web-next/app/bulletin/page.test.tsx
index e6e7fa11f1..1b342fb474 100644
--- a/web-next/app/bulletin/page.test.tsx
+++ b/web-next/app/bulletin/page.test.tsx
@@ -1,4 +1,6 @@
 import React from 'react';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
@@ -20,15 +22,24 @@ const loadBulletinData = vi.fn(async () => ({
 
 vi.mock('../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock()
+    t: createTranslatorMock({ locale: 'en-US' }),
+    locale: 'en-US'
   })
 }));
 
 vi.mock('../../components/workbench/client-workbench', () => ({
-  ClientWorkbench: ({ children, load }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown> }) => {
+  ClientWorkbench: ({
+    children,
+    load,
+    loadingCopy
+  }: {
+    children: (data: any) => React.ReactNode;
+    load: () => Promise<unknown>;
+    loadingCopy?: string;
+  }) => {
     loadState.lastLoad = load;
     return (
-      <div data-client-workbench="true">
+      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
         {children({
           list: {
             totalElements: 2,
@@ -77,7 +88,18 @@ describe('bulletin page', () => {
 
     expect(html).toContain('data-client-workbench="true"');
     expect(html).toContain('data-bulletin-center-surface="true"');
+    expect(html).toContain('data-loading-copy="Loading bulletin center"');
     expect(html).toContain('Ops board');
     expect(loadBulletinData).toHaveBeenCalledWith(apiMessageGet, '');
   }, 15000);
+
+  it('keeps bulletin center remounts on a short settled cache window while reload invalidates it', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/bulletin/bulletin-page.tsx'), 'utf8');
+
+    expect(source).toContain('BULLETIN_CENTER_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['bulletin-center', bulletinListUrl, refreshTick].join(':')");
+    expect(source).toContain('[bulletinListUrl, refreshTick]');
+    expect(source).toContain('onReload={() => setRefreshTick(value => value + 1)}');
+    expect(source).toContain('cacheSettledTtlMs={BULLETIN_CENTER_SETTLED_CACHE_TTL_MS}');
+  });
 });
diff --git a/web-next/app/bulletin/page.tsx b/web-next/app/bulletin/page.tsx
index e68889cb1f..4f2f021fb0 100644
--- a/web-next/app/bulletin/page.tsx
+++ b/web-next/app/bulletin/page.tsx
@@ -1,23 +1,7 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback, useState } from 'react';
-import { BulletinCenterSurface, type BulletinCenterData } from '../../components/pages/bulletin-center-surface';
-import { useI18n } from '../../components/providers/i18n-provider';
-import { ClientWorkbench } from '../../components/workbench/client-workbench';
-import { apiMessageGet } from '../../lib/api-client';
-import { loadBulletinData } from '../../lib/bulletin-center/controller';
+import BulletinPage from './bulletin-page';
 
-export default function BulletinPage() {
-  const { t } = useI18n();
-  const [refreshTick, setRefreshTick] = useState(0);
-
-  const load = useCallback(async (): Promise<BulletinCenterData> => {
-    return loadBulletinData(apiMessageGet, ' '.repeat(refreshTick));
-  }, [refreshTick]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('bulletin.loading')}>
-      {data => <BulletinCenterSurface data={data} refreshTick={refreshTick} onReload={() => setRefreshTick(value => value + 1)} />}
-    </ClientWorkbench>
-  );
+export default function BulletinRoutePage() {
+  return <BulletinPage />;
 }
diff --git a/web-next/components/pages/bulletin-center-surface.test.tsx b/web-next/components/pages/bulletin-center-surface.test.tsx
index be4a4f96e6..faf20bc389 100644
--- a/web-next/components/pages/bulletin-center-surface.test.tsx
+++ b/web-next/components/pages/bulletin-center-surface.test.tsx
@@ -4,10 +4,11 @@ import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
+import { BulletinCenterSurface } from './bulletin-center-surface';
 
 vi.mock('../providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock()
+    t: createTranslatorMock({ locale: 'zh-CN' })
   })
 }));
 
@@ -21,8 +22,8 @@ vi.mock('../workbench/action-toolbar', () => ({
 }));
 
 vi.mock('../workbench/workspace-tab-strip', () => ({
-  WorkspaceTabStrip: ({ tabs }: any) => (
-    <div data-bulletin-tabs="true">
+  WorkspaceTabStrip: ({ tabs, ariaLabel }: any) => (
+    <div data-bulletin-tabs="true" aria-label={ariaLabel}>
       {tabs.map((tab: any) => (
         <button key={tab.key} type="button">
           {tab.label}
@@ -59,8 +60,7 @@ vi.mock('../ui/checkbox', () => ({
 }));
 
 describe('bulletin center surface', () => {
-  it('renders the shared HertzBeat toolbar, tabs, and metrics desk shell', async () => {
-    const { BulletinCenterSurface } = await import('./bulletin-center-surface');
+  it('renders the shared HertzBeat toolbar, tabs, and metrics desk shell', () => {
     const html = renderToStaticMarkup(
       <BulletinCenterSurface
         data={{
@@ -80,11 +80,12 @@ describe('bulletin center surface', () => {
     expect(html).toContain('data-bulletin-center-surface="true"');
     expect(html).toContain('data-action-toolbar="true"');
     expect(html).toContain('data-bulletin-tabs="true"');
+    expect(html).toContain('aria-label="公告导航"');
     expect(html).toContain('data-bulletin-metrics-table="true"');
     expect(html).toContain('Ops board');
     expect(html).toContain('DB board');
-    expect(html).toContain('Refresh');
-    expect(html).toContain('New');
+    expect(html).toContain('刷新');
+    expect(html).toContain('新增');
   });
 
   it('routes current bulletin deletion through a cold modal instead of native confirm', () => {
@@ -94,9 +95,23 @@ describe('bulletin center surface', () => {
     expect(source).not.toContain('confirm(');
     expect(source).toContain('data-bulletin-delete-confirm-trigger="cold-modal"');
     expect(source).toContain('data-bulletin-delete-confirm="cold-modal"');
-    expect(source).toContain('确认删除公告');
-    expect(source).toContain('确认删除');
-    expect(source).toContain('取消');
+    expect(source).toContain("ariaLabel={t('bulletin.navigation.aria')}");
+    expect(source).not.toContain('ariaLabel="Bulletin navigation"');
+    expect(source).toContain("t('bulletin.delete.title')");
+    expect(source).toContain("t('bulletin.delete.copy')");
+    expect(source).toContain("t('bulletin.delete.confirm')");
+    expect(source).toContain("t('common.cancel')");
+    expect(source).not.toContain('确认删除公告');
+    expect(source).not.toContain('删除后该公告看板会从当前工作台移除');
+  });
+
+  it('trims blank current bulletin names to the localized empty fallback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/bulletin-center-surface.tsx'), 'utf8');
+
+    expect(source).toContain("const currentBulletinName = (activeBulletin?.name || '').trim() || t('common.none');");
+    expect(source).toContain('{currentBulletinName}');
+    expect(source).not.toContain("const currentBulletinName = activeBulletin?.name || t('common.none');");
+    expect(source).not.toContain("activeBulletin?.name || '-'");
   });
 
   it('uses the shared cold checkbox for batch delete selection instead of raw checkbox chrome', () => {
diff --git a/web-next/components/pages/bulletin-center-surface.tsx b/web-next/components/pages/bulletin-center-surface.tsx
index a603c88886..043b8d40bf 100644
--- a/web-next/components/pages/bulletin-center-surface.tsx
+++ b/web-next/components/pages/bulletin-center-surface.tsx
@@ -65,6 +65,7 @@ export function BulletinCenterSurface({
   const activeBulletin = pickSelectedBulletin(data.list.content, selectedId);
   const facts = buildBulletinFacts(data.list, activeBulletin, t);
   const activeBulletinId = activeBulletin?.id ?? null;
+  const currentBulletinName = (activeBulletin?.name || '').trim() || t('common.none');
 
   useEffect(() => {
     setBatchDeleteIds(current => current.filter(id => data.list.content.some(item => item.id === id)));
@@ -309,7 +310,7 @@ export function BulletinCenterSurface({
               }
             }))}
             variant="card"
-            ariaLabel="Bulletin navigation"
+            ariaLabel={t('bulletin.navigation.aria')}
           />
         </div>
         <div className="border-t border-[var(--ops-border-color)]">
@@ -339,23 +340,23 @@ export function BulletinCenterSurface({
         open={deleteDialogOpen}
         onClose={() => setDeleteDialogOpen(false)}
         kicker={t('menu.monitor.bulletin')}
-        title="确认删除公告"
+        title={t('bulletin.delete.title')}
         maxWidthClassName="max-w-xl"
         footer={
           <div className="flex flex-wrap justify-end gap-2">
             <Button size="sm" variant="subtle" onClick={() => setDeleteDialogOpen(false)}>
-              取消
+              {t('common.cancel')}
             </Button>
             <Button size="sm" variant="primary" onClick={() => void handleDeleteCurrent()} disabled={!activeBulletin?.id}>
-              确认删除
+              {t('bulletin.delete.confirm')}
             </Button>
           </div>
         }
       >
         <div data-bulletin-delete-confirm="cold-modal" className="space-y-3 text-[12px] leading-6 text-[var(--ops-text-secondary)]">
-          <p>删除后该公告看板会从当前工作台移除,指标刷新上下文需要重新选择公告看板。</p>
+          <p>{t('bulletin.delete.copy')}</p>
           <div className="rounded-[4px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-3 py-2 font-semibold text-[var(--ops-text-primary)]">
-            {activeBulletin?.name || '-'}
+            {currentBulletinName}
           </div>
         </div>
       </OverlayDialog>
diff --git a/web-next/components/pages/bulletin-manage-dialog.tsx b/web-next/components/pages/bulletin-manage-dialog.tsx
index 5f18a964cd..4a9077aad0 100644
--- a/web-next/components/pages/bulletin-manage-dialog.tsx
+++ b/web-next/components/pages/bulletin-manage-dialog.tsx
@@ -67,7 +67,7 @@ export function BulletinManageDialog({
         <Input
           value={draft.monitorIdsText}
           onChange={event => onDraftChange({ ...draft, monitorIdsText: event.target.value })}
-          placeholder="1, 2, 3"
+          placeholder={t('bulletin.monitor.ids.placeholder')}
           className="h-10 border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] text-[var(--ops-text-primary)] placeholder:text-[var(--ops-text-tertiary)]"
         />
 
diff --git a/web-next/components/pages/bulletin-metrics-table.test.tsx b/web-next/components/pages/bulletin-metrics-table.test.tsx
new file mode 100644
index 0000000000..0d5cf4d107
--- /dev/null
+++ b/web-next/components/pages/bulletin-metrics-table.test.tsx
@@ -0,0 +1,124 @@
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+import { BulletinMetricsTable } from './bulletin-metrics-table';
+
+describe('BulletinMetricsTable', () => {
+  it('renders fixed table labels and no-data badges through runtime i18n', () => {
+    const t = createTranslatorMock();
+    const html = renderToStaticMarkup(
+      <BulletinMetricsTable
+        app="website"
+        loading={false}
+        error={null}
+        t={t}
+        data={{
+          content: [
+            {
+              monitorId: 7,
+              monitorName: 'Checkout probe',
+              host: 'checkout.example.com',
+              metrics: [
+                {
+                  name: 'response_time',
+                  fields: [[{ key: 'p95', value: 'NO_DATA' }]]
+                }
+              ]
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('data-bulletin-metrics-table="true"');
+    expect(html).toContain('App');
+    expect(html).toContain('Host');
+    expect(html).toContain('No Data Available');
+    expect(t).toHaveBeenCalledWith('bulletin.metrics.column.app');
+    expect(t).toHaveBeenCalledWith('bulletin.metrics.column.host');
+    expect(t).toHaveBeenCalledWith('bulletin.metrics.no-data');
+  });
+
+  it('renders missing metric values with the localized empty fallback', () => {
+    const t = createTranslatorMock({
+      overrides: {
+        'common.none': '无指标值'
+      }
+    });
+    const html = renderToStaticMarkup(
+      <BulletinMetricsTable
+        app="website"
+        loading={false}
+        error={null}
+        t={t}
+        data={{
+          content: [
+            {
+              monitorId: 7,
+              monitorName: 'Checkout probe',
+              host: 'checkout.example.com',
+              metrics: [
+                {
+                  name: 'response_time',
+                  fields: [[{ key: 'p95', value: 'NO_DATA' }]]
+                }
+              ]
+            },
+            {
+              monitorId: 8,
+              monitorName: 'Cart probe',
+              host: 'cart.example.com',
+              metrics: []
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('Cart probe');
+    expect(html).toContain('无指标值');
+    expect(t).toHaveBeenCalledWith('common.none');
+  });
+
+  it('renders missing monitor identity with the localized empty fallback', () => {
+    const t = createTranslatorMock({
+      overrides: {
+        'common.none': '无监控身份'
+      }
+    });
+    const html = renderToStaticMarkup(
+      <BulletinMetricsTable
+        app="website"
+        loading={false}
+        error={null}
+        t={t}
+        data={{
+          content: [
+            {
+              monitorId: 7,
+              monitorName: 'Checkout probe',
+              host: 'checkout.example.com',
+              metrics: [
+                {
+                  name: 'response_time',
+                  fields: [[{ key: 'p95', value: '250', unit: 'ms' }]]
+                }
+              ]
+            },
+            {
+              monitorId: 8,
+              monitorName: '',
+              host: '',
+              metrics: []
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('Checkout probe');
+    expect((html.match(/无监控身份/g) ?? []).length).toBeGreaterThanOrEqual(2);
+    expect(t).toHaveBeenCalledWith('common.none');
+  });
+});
diff --git a/web-next/components/pages/bulletin-metrics-table.tsx b/web-next/components/pages/bulletin-metrics-table.tsx
index b1588ef731..672578e554 100644
--- a/web-next/components/pages/bulletin-metrics-table.tsx
+++ b/web-next/components/pages/bulletin-metrics-table.tsx
@@ -46,14 +46,15 @@ export function BulletinMetricsTable({
   }
 
   const columns = buildBulletinMetricColumns(data, app, t);
+  const emptyMetricValue = t('common.none');
 
   return (
     <div className="overflow-x-auto rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)]">
       <table className="min-w-full table-auto border-collapse text-[12px]" data-bulletin-metrics-table="true">
         <thead>
           <tr className="border-b border-[var(--ops-border-color)] text-[var(--ops-text-primary)]">
-            <th className="min-w-[180px] px-3 py-2 text-center font-semibold">App</th>
-            <th className="min-w-[180px] px-3 py-2 text-center font-semibold">Host</th>
+            <th className="min-w-[180px] px-3 py-2 text-center font-semibold">{t('bulletin.metrics.column.app')}</th>
+            <th className="min-w-[180px] px-3 py-2 text-center font-semibold">{t('bulletin.metrics.column.host')}</th>
             {columns.map(column => (
               <th
                 key={column.key}
@@ -77,46 +78,51 @@ export function BulletinMetricsTable({
           </tr>
         </thead>
         <tbody>
-          {data.content.map(row => (
-            <tr key={`${row.monitorId}-${row.host}`} className="border-b border-[var(--ops-border-color)] align-top last:border-b-0">
-              <td className="px-3 py-2 text-center">
-                <Link href={`/monitors/${row.monitorId}`} className="font-medium text-[var(--ops-primary)] transition hover:text-[var(--ops-text-primary)]">
-                  {row.monitorName}
-                </Link>
-              </td>
-              <td className="px-3 py-2 text-center text-[var(--ops-text-secondary)]">{row.host}</td>
-              {columns.flatMap(column =>
-                column.fields.map(field => {
-                  const values = buildBulletinMetricCellValues(row, column.key, field.key);
-                  return (
-                    <td key={`${row.monitorId}-${column.key}-${field.key}`} className="border-l border-[var(--ops-border-color)] px-3 py-2">
-                      {values.length === 0 ? (
-                        <div className="text-center text-[var(--ops-text-tertiary)]">-</div>
-                      ) : (
-                        <div className="space-y-1.5">
-                          {values.map((value, index) =>
-                            value.isNoData ? (
-                              <div key={`${row.monitorId}-${column.key}-${field.key}-${index}`} className="flex justify-center">
-                                <Badge variant="accent">No Data Available</Badge>
-                              </div>
-                            ) : (
-                              <div
-                                key={`${row.monitorId}-${column.key}-${field.key}-${index}`}
-                                className="flex items-center justify-between gap-2 whitespace-nowrap rounded-[4px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-2 py-1"
-                              >
-                                <span className="text-[var(--ops-text-primary)]">{value.value}</span>
-                                {value.unit ? <Badge variant="success">{value.unit}</Badge> : null}
-                              </div>
-                            )
-                          )}
-                        </div>
-                      )}
-                    </td>
-                  );
-                })
-              )}
-            </tr>
-          ))}
+          {data.content.map(row => {
+            const monitorName = row.monitorName || emptyMetricValue;
+            const hostName = row.host || emptyMetricValue;
+
+            return (
+              <tr key={`${row.monitorId}-${row.host}`} className="border-b border-[var(--ops-border-color)] align-top last:border-b-0">
+                <td className="px-3 py-2 text-center">
+                  <Link href={`/monitors/${row.monitorId}`} className="font-medium text-[var(--ops-primary)] transition hover:text-[var(--ops-text-primary)]">
+                    {monitorName}
+                  </Link>
+                </td>
+                <td className="px-3 py-2 text-center text-[var(--ops-text-secondary)]">{hostName}</td>
+                {columns.flatMap(column =>
+                  column.fields.map(field => {
+                    const values = buildBulletinMetricCellValues(row, column.key, field.key);
+                    return (
+                      <td key={`${row.monitorId}-${column.key}-${field.key}`} className="border-l border-[var(--ops-border-color)] px-3 py-2">
+                        {values.length === 0 ? (
+                          <div className="text-center text-[var(--ops-text-tertiary)]">{emptyMetricValue}</div>
+                        ) : (
+                          <div className="space-y-1.5">
+                            {values.map((value, index) =>
+                              value.isNoData ? (
+                                <div key={`${row.monitorId}-${column.key}-${field.key}-${index}`} className="flex justify-center">
+                                  <Badge variant="accent">{t('bulletin.metrics.no-data')}</Badge>
+                                </div>
+                              ) : (
+                                <div
+                                  key={`${row.monitorId}-${column.key}-${field.key}-${index}`}
+                                  className="flex items-center justify-between gap-2 whitespace-nowrap rounded-[4px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-2 py-1"
+                                >
+                                  <span className="text-[var(--ops-text-primary)]">{value.value}</span>
+                                  {value.unit ? <Badge variant="success">{value.unit}</Badge> : null}
+                                </div>
+                              )
+                            )}
+                          </div>
+                        )}
+                      </td>
+                    );
+                  })
+                )}
+              </tr>
+            );
+          })}
         </tbody>
       </table>
     </div>
diff --git a/web-next/components/pages/bulletin-monitor-detail.chrome.test.ts b/web-next/components/pages/bulletin-monitor-detail.chrome.test.ts
index 0823086cae..5dfd539c6a 100644
--- a/web-next/components/pages/bulletin-monitor-detail.chrome.test.ts
+++ b/web-next/components/pages/bulletin-monitor-detail.chrome.test.ts
@@ -53,6 +53,7 @@ describe('bulletin center and monitor-detail cold-workbench chrome', () => {
     const metricTableSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-metric-table.tsx'), 'utf8');
     const historyChartSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/history-line-chart.tsx'), 'utf8');
     const summaryCardSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-summary-card.tsx'), 'utf8');
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
 
     expect(bulletinSource).toContain('border-[var(--ops-border-color)]');
     expect(bulletinSource).toContain('bg-[var(--ops-surface-panel)]');
@@ -61,23 +62,19 @@ describe('bulletin center and monitor-detail cold-workbench chrome', () => {
     expect(bulletinSource).toContain('text-[var(--ops-text-secondary)]');
     expect(bulletinSource).toContain('text-[var(--ops-text-tertiary)]');
 
-    expect(metricTableSource).toContain('border-[var(--ops-border-color)]');
-    expect(metricTableSource).toContain('text-[var(--ops-text-primary)]');
-    expect(metricTableSource).toContain('text-[var(--ops-text-secondary)]');
-    expect(metricTableSource).toContain('text-[var(--ops-text-tertiary)]');
-    expect(metricTableSource).toContain('bg-[var(--ops-surface-panel)]');
-    expect(metricTableSource).toContain('bg-[var(--ops-surface-raised)]');
+    expect(metricTableSource).toContain('HzDataTable');
+    expect(metricTableSource).toContain('data-monitor-metric-table-owner="hertzbeat-ui-data-table"');
+    expect(uiSource).toContain('border border-[var(--ops-border-color)]');
+    expect(uiSource).toContain('bg-[var(--ops-surface-panel)]');
+    expect(uiSource).toContain('text-[var(--ops-text-secondary)]');
 
-    expect(historyChartSource).toContain('border-[var(--ops-border-color)]');
-    expect(historyChartSource).toContain('WorkbenchInsetPanel');
-    expect(historyChartSource).toContain('bg-[var(--ops-surface-raised)]');
-    expect(historyChartSource).toContain('text-[var(--ops-text-primary)]');
-    expect(historyChartSource).toContain('text-[var(--ops-text-secondary)]');
-    expect(historyChartSource).toContain('text-[var(--ops-text-tertiary)]');
+    expect(historyChartSource).toContain('HzChartSurface');
+    expect(historyChartSource).toContain('data-monitor-history-line-owner="hertzbeat-ui-chart-surface"');
+    expect(uiSource).toContain('export function HzChartSurface');
     expect(historyChartSource).not.toContain('className="rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] p-3"');
 
-    expect(summaryCardSource).toContain('text-[var(--ops-text-tertiary)]');
-    expect(summaryCardSource).toContain('WorkbenchBadge');
+    expect(summaryCardSource).toContain('HzMonitorBasicSummary');
+    expect(uiSource).toContain('export function HzMonitorBasicSummary');
     expect(summaryCardSource).not.toContain('inline-flex rounded-[2px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-2.5 py-1 text-xs text-[var(--ops-text-secondary)]');
   });
 });
diff --git a/web-next/lib/bulletin-center/view-model.test.ts b/web-next/lib/bulletin-center/view-model.test.ts
index 52331df673..364e79f5bf 100644
--- a/web-next/lib/bulletin-center/view-model.test.ts
+++ b/web-next/lib/bulletin-center/view-model.test.ts
@@ -14,6 +14,18 @@ describe('bulletin view model', () => {
     ]);
   });
 
+  it('renders missing selected fact name with the localized empty fallback', () => {
+    expect(buildBulletinFacts({ totalElements: 8, content: [] } as any, { name: ' ' } as any, t)).toContainEqual({
+      label: '当前选中',
+      value: '无'
+    });
+
+    expect(buildBulletinFacts({ totalElements: 8, content: [] } as any, null, t)).toContainEqual({
+      label: '当前选中',
+      value: '无'
+    });
+  });
+
   it('builds bulletin rows', () => {
     expect(
       buildBulletinRows(
@@ -27,8 +39,8 @@ describe('bulletin view model', () => {
       {
         key: '7',
         title: 'Ops board',
-        copy: 'checkout · monitors 2',
-        meta: '2026-04-10 18:00:00 · creator ops'
+        copy: 'checkout · 监控对象 2',
+        meta: '2026-04-10 18:00:00 · 创建人 ops'
       }
     ]);
   });
@@ -41,11 +53,60 @@ describe('bulletin view model', () => {
         () => '2026-04-10 18:00:00'
       )
     ).toEqual([
-      { title: 'Ops board', copy: 'checkout · monitors 2', meta: 'id 7' },
-      { title: 'fields', copy: '1', meta: '更新时间 2026-04-10 18:00:00' }
+      { title: 'Ops board', copy: 'checkout · 监控对象 2', meta: 'id 7' },
+      { title: '字段', copy: '1', meta: '更新时间 2026-04-10 18:00:00' }
     ]);
   });
 
+  it('renders missing bulletin facts with the localized empty fallback', () => {
+    expect(
+      buildBulletinRows(
+        [{ id: 9, name: 'Fallback board', app: ' ', monitorIds: [], creator: '' }] as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      {
+        key: '9',
+        title: 'Fallback board',
+        copy: '无 · 监控对象 0',
+        meta: '2026-04-10 18:00:00 · 创建人 无'
+      }
+    ]);
+
+    expect(
+      buildBulletinSelectionRows(
+        { id: 9, name: 'Fallback board', app: ' ', monitorIds: [], fields: {} } as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )[0]
+    ).toEqual({ title: 'Fallback board', copy: '无 · 监控对象 0', meta: 'id 9' });
+
+    expect(buildBulletinSelectionRows(null, t, () => '2026-04-10 18:00:00')[0]).toEqual({
+      title: '未选择公告',
+      copy: '从公告列表选择一个看板后查看指标上下文。',
+      meta: '无'
+    });
+  });
+
+  it('renders missing bulletin titles with the localized empty fallback', () => {
+    expect(
+      buildBulletinRows(
+        [{ id: 9, name: ' ', app: 'checkout', monitorIds: [], creator: 'ops' }] as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )[0].title
+    ).toBe('无');
+
+    expect(
+      buildBulletinSelectionRows(
+        { id: 9, name: '', app: 'checkout', monitorIds: [], fields: {} } as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )[0].title
+    ).toBe('无');
+  });
+
   it('picks the explicit selection and falls back to the first row', () => {
     expect(pickSelectedBulletin([{ id: 7 }, { id: 8 }] as any, 8)).toEqual({ id: 8 });
     expect(pickSelectedBulletin([{ id: 7 }, { id: 8 }] as any, 99)).toEqual({ id: 7 });
@@ -53,7 +114,7 @@ describe('bulletin view model', () => {
   });
 
   it('builds the current query label and selected bulletin json', () => {
-    expect(buildBulletinCurrentQueryLabel('  ', t)).toBe('all bulletins');
+    expect(buildBulletinCurrentQueryLabel('  ', t)).toBe('全部公告');
     expect(buildBulletinCurrentQueryLabel('checkout', t)).toBe('checkout');
     expect(buildSelectedBulletinJson({ id: 7, name: 'Ops board' } as any)).toBe('{\n  "id": 7,\n  "name": "Ops board"\n}');
     expect(buildSelectedBulletinJson(null)).toBeNull();
@@ -84,14 +145,14 @@ describe('bulletin view model', () => {
 
     expect(buildBulletinMetricsState(null, null, t)).toEqual({
       kind: 'empty',
-      title: 'No metrics',
-      copy: 'No metrics are available yet.',
+      title: '暂无指标',
+      copy: '暂无可用指标。',
       tone: 'default'
     });
 
     expect(buildBulletinMetricsState(null, 'boom', t)).toEqual({
       kind: 'empty',
-      title: 'Metrics unavailable',
+      title: '指标不可用',
       copy: 'boom',
       tone: 'danger'
     });
@@ -155,10 +216,10 @@ describe('bulletin view model', () => {
       fieldsJson: '{\n  "cpu": [\n    "usage"\n  ]\n}'
     });
 
-    expect(validateBulletinForm({ name: '', app: '', monitorIdsText: '', fieldsJson: '{}' }, t)).toBe('Bulletin name is required');
-    expect(validateBulletinForm({ name: 'Ops', app: '', monitorIdsText: '', fieldsJson: '{}' }, t)).toBe('App is required');
-    expect(validateBulletinForm({ name: 'Ops', app: 'mysql', monitorIdsText: '', fieldsJson: '{}' }, t)).toBe('At least one monitor id is required');
-    expect(validateBulletinForm({ name: 'Ops', app: 'mysql', monitorIdsText: '1,2', fieldsJson: '{oops' }, t)).toBe('bulletin.validation.fields');
+    expect(validateBulletinForm({ name: '', app: '', monitorIdsText: '', fieldsJson: '{}' }, t)).toBe('公告看板名称为必填项');
+    expect(validateBulletinForm({ name: 'Ops', app: '', monitorIdsText: '', fieldsJson: '{}' }, t)).toBe('应用为必填项');
+    expect(validateBulletinForm({ name: 'Ops', app: 'mysql', monitorIdsText: '', fieldsJson: '{}' }, t)).toBe('至少需要填写一个监控 ID');
+    expect(validateBulletinForm({ name: 'Ops', app: 'mysql', monitorIdsText: '1,2', fieldsJson: '{oops' }, t)).toBe('指标字段 JSON 格式不正确');
     expect(validateBulletinForm({ name: 'Ops', app: 'mysql', monitorIdsText: '1,2', fieldsJson: '{}' }, t)).toBeNull();
   });
 });
diff --git a/web-next/lib/bulletin-center/view-model.ts b/web-next/lib/bulletin-center/view-model.ts
index 004d287c59..64b06e71ab 100644
--- a/web-next/lib/bulletin-center/view-model.ts
+++ b/web-next/lib/bulletin-center/view-model.ts
@@ -6,33 +6,50 @@ type Translator = (key: string, params?: Record<string, string | number | null |
 
 const BULLETIN_REFRESH_PRESETS = [10, 30, 60, 300, -1] as const;
 
+function formatBulletinFact(value: string | number | null | undefined, emptyValue: string) {
+  const text = value == null ? '' : String(value).trim();
+  return text || emptyValue;
+}
+
 export function buildBulletinFacts(list: PageResult<Bulletin>, selected: Bulletin | null, t: Translator) {
+  const emptyValue = t('common.none');
+
   return [
     { label: t('common.workspace'), value: 'bulletin' },
     { label: t('common.total'), value: String(list.totalElements || 0) },
     { label: t('common.current-page-count'), value: String(list.content?.length || 0) },
-    { label: t('bulletin.facts.selected'), value: selected?.name || t('common.none') }
+    { label: t('bulletin.facts.selected'), value: formatBulletinFact(selected?.name, emptyValue) }
   ];
 }
 
 export function buildBulletinRows(items: Bulletin[], t: Translator, formatTime: (value?: number | string | null) => string) {
-  return items.map(item => ({
-    key: String(item.id),
-    title: item.name,
-    copy: `${item.app || '-'} · ${t('bulletin.list.monitors')} ${(item.monitorIds || []).length}`,
-    meta: `${formatTime(item.gmtUpdate || item.gmtCreate || null)} · ${t('bulletin.list.creator')} ${item.creator || '-'}`
-  }));
+  const emptyValue = t('common.none');
+
+  return items.map(item => {
+    const rowTitle = formatBulletinFact(item.name, emptyValue);
+
+    return {
+      key: String(item.id),
+      title: rowTitle,
+      copy: `${formatBulletinFact(item.app, emptyValue)} · ${t('bulletin.list.monitors')} ${(item.monitorIds || []).length}`,
+      meta: `${formatTime(item.gmtUpdate || item.gmtCreate || null)} · ${t('bulletin.list.creator')} ${formatBulletinFact(item.creator, emptyValue)}`
+    };
+  });
 }
 
 export function buildBulletinSelectionRows(selected: Bulletin | null, t: Translator, formatTime: (value?: number | string | null) => string) {
+  const emptyValue = t('common.none');
+
   if (!selected) {
-    return [{ title: t('bulletin.selected.empty.title'), copy: t('bulletin.selected.empty.copy'), meta: '-' }];
+    return [{ title: t('bulletin.selected.empty.title'), copy: t('bulletin.selected.empty.copy'), meta: emptyValue }];
   }
 
+  const selectedTitle = formatBulletinFact(selected.name, emptyValue);
+
   return [
     {
-      title: selected.name,
-      copy: `${selected.app || '-'} · ${t('bulletin.list.monitors')} ${(selected.monitorIds || []).length}`,
+      title: selectedTitle,
+      copy: `${formatBulletinFact(selected.app, emptyValue)} · ${t('bulletin.list.monitors')} ${(selected.monitorIds || []).length}`,
       meta: `id ${selected.id}`
     },
     {


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 35/50: fix(web-next): suppress empty topology legends

Posted by zh...@apache.org.
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 446b18b603a0c308474a35d57142c5168ea3f745
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 08:06:19 2026 +0800

    fix(web-next): suppress empty topology legends
---
 web-next/app/topology/page.test.tsx               |   3 +
 web-next/app/topology/topology-page.tsx           | 135 ++++++++++++----------
 web-next/packages/hertzbeat-ui/src/index.test.tsx |  13 +++
 web-next/packages/hertzbeat-ui/src/index.tsx      |   4 +
 4 files changed, 92 insertions(+), 63 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 33ffd36fbd..07fde71da5 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -1677,6 +1677,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
     expect(html).toContain('data-hz-topology-empty-source-owner="hertzbeat-ui-empty-source"');
     expect(html).toContain('data-hz-topology-empty-time-scope-owner="hertzbeat-ui-empty-time-scope"');
+    expect(html).not.toContain('data-topology-g6-legend-owner="hertzbeat-ui-g6-legend-dock"');
+    expect(html).not.toContain('data-hz-ui="topology-legend"');
+    expect(html).not.toContain('data-hz-topology-legend-section=');
     expect(html).not.toContain('data-topology-node-id="svc-checkout"');
     expect(html).not.toContain('checkout-api');
     expect(html).not.toContain('redis');
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index ea04862bef..62209c2af4 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -1152,71 +1152,80 @@ export default function TopologyPage({
     },
     [topologyG6Graph.nodes, t]
   );
+  const topologyHasVisibleNodes = topologyG6Graph.nodes.length > 0;
+  const topologyHasVisibleGraphEvidence = topologyHasVisibleNodes || topologyG6Graph.edges.length > 0;
   const topologyLegendSections = React.useMemo(
-    () => [
-      {
-        id: 'node-type',
-        label: t('topology.legend.node-type'),
-        items: topologyLegendNodeTypeItems
-      },
-      {
-        id: 'status',
-        label: t('topology.legend.status'),
-        items: [
-          {
-            id: 'healthy-node',
-            label: t('topology.legend.status.healthy'),
-            color: '#22c55e',
-            visualSource: 'hertzbeat-status-token' as const,
-            value: t('topology.legend.status.healthy-value')
-          },
-          {
-            id: 'warning-node',
-            label: t('topology.legend.status.warning'),
-            color: '#f59e0b',
-            visualSource: 'hertzbeat-status-token' as const,
-            value: t('topology.legend.status.warning-value')
-          },
-          {
-            id: 'critical-node',
-            label: t('topology.legend.status.critical'),
-            color: '#ef4444',
-            visualSource: 'hertzbeat-status-token' as const,
-            value: t('topology.legend.status.critical-value')
-          }
-        ]
-      },
-      {
-        id: 'interaction',
-        label: t('topology.legend.interaction'),
-        items: [
-          {
-            id: 'selected-node',
-            label: t('topology.legend.interaction.selected-node'),
-            color: '#e5edf8',
-            visualSource: 'hertzbeat-interaction-token' as const,
-            value: t('topology.legend.interaction.selected-node-value')
-          },
-          {
-            id: 'directional-edge',
-            label: t('topology.legend.interaction.directional-edge'),
-            color: '#94a3b8',
-            pattern: 'solid' as const,
-            visualSource: 'hertzbeat-edge-token' as const,
-            value: t('topology.legend.interaction.directional-edge-value')
-          },
-          {
-            id: 'dimmed-edge',
-            label: t('topology.legend.interaction.dimmed-edge'),
-            color: '#94a3b8',
-            pattern: 'muted' as const,
-            visualSource: 'hertzbeat-edge-token' as const,
-            value: t('topology.legend.interaction.dimmed-edge-value')
-          }
-        ]
+    () => {
+      const sections = [
+        {
+          id: 'node-type',
+          label: t('topology.legend.node-type'),
+          items: topologyLegendNodeTypeItems
+        }
+      ];
+      if (topologyHasVisibleNodes) {
+        sections.push({
+          id: 'status',
+          label: t('topology.legend.status'),
+          items: [
+            {
+              id: 'healthy-node',
+              label: t('topology.legend.status.healthy'),
+              color: '#22c55e',
+              visualSource: 'hertzbeat-status-token' as const,
+              value: t('topology.legend.status.healthy-value')
+            },
+            {
+              id: 'warning-node',
+              label: t('topology.legend.status.warning'),
+              color: '#f59e0b',
+              visualSource: 'hertzbeat-status-token' as const,
+              value: t('topology.legend.status.warning-value')
+            },
+            {
+              id: 'critical-node',
+              label: t('topology.legend.status.critical'),
+              color: '#ef4444',
+              visualSource: 'hertzbeat-status-token' as const,
+              value: t('topology.legend.status.critical-value')
+            }
+          ]
+        });
+      }
+      if (topologyHasVisibleGraphEvidence) {
+        sections.push({
+          id: 'interaction',
+          label: t('topology.legend.interaction'),
+          items: [
+            {
+              id: 'selected-node',
+              label: t('topology.legend.interaction.selected-node'),
+              color: '#e5edf8',
+              visualSource: 'hertzbeat-interaction-token' as const,
+              value: t('topology.legend.interaction.selected-node-value')
+            },
+            {
+              id: 'directional-edge',
+              label: t('topology.legend.interaction.directional-edge'),
+              color: '#94a3b8',
+              pattern: 'solid' as const,
+              visualSource: 'hertzbeat-edge-token' as const,
+              value: t('topology.legend.interaction.directional-edge-value')
+            },
+            {
+              id: 'dimmed-edge',
+              label: t('topology.legend.interaction.dimmed-edge'),
+              color: '#94a3b8',
+              pattern: 'muted' as const,
+              visualSource: 'hertzbeat-edge-token' as const,
+              value: t('topology.legend.interaction.dimmed-edge-value')
+            }
+          ]
+        });
       }
-    ],
-    [t, topologyLegendNodeTypeItems]
+      return sections;
+    },
+    [t, topologyHasVisibleGraphEvidence, topologyHasVisibleNodes, topologyLegendNodeTypeItems]
   );
   const topologyEdgeIds = React.useMemo(
     () => new Set(map.edges.map(edge => edge.id)),
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index fa75a423af..3768abb6b1 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -5285,6 +5285,19 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
   });
 
+  it('does not render a topology legend shell when every section is empty', () => {
+    const html = renderToStaticMarkup(
+      <HzTopologyLegend
+        title="Legend"
+        boundary="flush"
+        density="canvas-dock"
+        sections={[{ id: 'node-type', label: 'Node type', items: [] }]}
+      />
+    );
+
+    expect(html).toBe('');
+  });
+
   it('renders a topology detail drawer for edge evidence and cross-signal handoffs', () => {
     const html = renderToStaticMarkup(
       <HzTopologyDetailDrawer
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index 9d2cf77344..c6455ec437 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -11206,6 +11206,10 @@ export function HzTopologyLegend({
   const isCanvasDock = density === 'canvas-dock';
   const visibleSections = sections.filter(section => section.items.length > 0);
 
+  if (visibleSections.length === 0) {
+    return null;
+  }
+
   return (
     <section
       {...props}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 07/50: feat(web-next): expand runtime i18n catalog

Posted by zh...@apache.org.
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 a6723173fd61885261b994756d45f030139e3343
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 00:38:17 2026 +0800

    feat(web-next): expand runtime i18n catalog
---
 web-next/lib/i18n-runtime-messages.ts | 5030 ++++++++++++++++++++++++++++++++-
 web-next/test/i18n-test-helper.ts     | 3958 +++++++++++++++++++++++++-
 2 files changed, 8854 insertions(+), 134 deletions(-)

diff --git a/web-next/lib/i18n-runtime-messages.ts b/web-next/lib/i18n-runtime-messages.ts
index 0fa9350fda..84396b91be 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -10,29 +10,49 @@ export const PREFIX_ALIASES: Array<[string, string]> = [
 export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
   'en-US': {
     'common.active': 'Active',
+    'common.add': 'Add',
     'common.annotation.bind': 'Annotations',
     'common.api.message-nonzero': 'API message returned non-zero code',
     'common.api.request-failed-status': 'API request failed: {{status}}',
     'common.attention': 'attention',
     'common.app': 'App',
     'common.application': 'application',
+    'common.boolean.true': 'true',
+    'common.boolean.false': 'false',
     'common.button.cancel': 'Cancel',
+    'common.button.setting': 'Configure',
+    'common.button.save': 'Save',
     'common.button.delete': 'Delete selected',
+    'common.button.retry': 'Retry',
     'common.button.delete-batch': 'Bulk delete',
+    'common.button.export': 'Export',
+    'common.button.import': 'Import',
     'common.button.help': 'Help',
     'common.button.ok': 'OK',
+    'common.confirm.cancel': 'Please confirm whether to cancel monitor!',
+    'common.confirm.cancel-batch': 'Please confirm whether to cancel monitor in batches!',
     'common.confirm.delete': 'Please confirm whether to delete!',
+    'common.confirm.delete-batch': 'Delete selected items?',
+    'common.confirm.enable': 'Please confirm whether to enable monitor!',
+    'common.confirm.enable-batch': 'Please confirm whether to enable monitor in batches!',
+    'common.confirm.operation': 'Confirm operation',
+    'validation.required': 'Please fill in the required fields! ',
     'common.copy-failed': 'Copy failed',
     'common.current-page-count': 'Current page',
     'common.current-view': 'Current view',
+    'common.status': 'Status',
+    'common.dark-mode': 'Dark mode',
     'common.delete-failed': 'Delete failed',
     'common.delete-success': 'Deleted successfully',
     'common.description': 'Description',
     'common.decrement': 'Decrease',
+    'common.duration.seconds': '{{value}}s',
     'common.edit': 'Operate',
     'common.edit-time': 'Updated',
     'common.empty': 'empty',
     'common.no-data': 'No data',
+    'common.tab-navigation': 'Tab navigation',
+    'common.timeline.empty': 'No events yet',
     'common.enable': 'Enable',
     'common.enabled': 'enabled',
     'common.disabled': 'disabled',
@@ -41,36 +61,93 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'common.idle': 'Idle',
     'common.increment': 'Increase',
     'common.label': 'label',
+    'common.label.key': 'Label key',
+    'common.label.value': 'Label value',
     'common.labels': 'Labels',
     'common.load-failed': 'Load failed',
     'common.mode': 'Mode',
     'common.next': 'Next',
+    'common.next-page': 'Next page',
     'common.next-point': 'Next point',
     'common.next-series': 'Next series',
     'common.next-step': 'Next step',
     'common.none': 'None',
+    'common.yes': 'Yes',
+    'common.no': 'No',
+    'common.open': 'Open',
     'common.previous': 'Previous',
+    'common.previous-page': 'Previous page',
     'common.previous-point': 'Previous point',
     'common.previous-series': 'Previous series',
+    'common.processing': 'Processing',
+    'common.remove': 'Remove',
     'common.loading': 'Loading...',
+    'common.view-more': 'View more',
     'common.apply': 'Apply',
+    'common.locale-code': 'en-US',
     'common.refresh': 'Refresh',
     'common.reset': 'Reset',
     'common.live': 'Live',
     'common.scrape': 'Scrape',
+    'common.tag.add': 'Add tag',
+    'common.week.1': 'Monday',
+    'common.week.2': 'Tuesday',
+    'common.week.3': 'Wednesday',
+    'common.week.4': 'Thursday',
+    'common.week.5': 'Friday',
+    'common.week.6': 'Saturday',
+    'common.week.7': 'Sunday',
+    'common.month.1': 'January',
+    'common.month.2': 'February',
+    'common.month.3': 'March',
+    'common.month.4': 'April',
+    'common.month.5': 'May',
+    'common.month.6': 'June',
+    'common.month.7': 'July',
+    'common.month.8': 'August',
+    'common.month.9': 'September',
+    'common.month.10': 'October',
+    'common.month.11': 'November',
+    'common.month.12': 'December',
     '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-years': 'Previous years',
+    'time.range.next-years': 'Next years',
+    'time.range.decrease': 'Decrease',
+    'time.range.increase': 'Increase',
+    '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.preset.last-15m': 'Last 15 minutes',
     'time.range.preset.last-30m': 'Last 30 minutes',
     'time.range.preset.last-1h': 'Last 1 hour',
     'time.range.preset.last-6h': 'Last 6 hours',
     'time.range.preset.last-1d': 'Last 1 day',
+    'time.range.preset.last-24h': 'Last 24 hours',
     'time.range.preset.last-1w': 'Last 1 week',
     'time.range.preset.last-4w': 'Last 4 weeks',
     'time.range.preset.last-12w': 'Last 12 weeks',
@@ -82,24 +159,745 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'time.range.reset': 'Reset',
     'time.range.reset-aria': 'Reset time range',
     'time.range.relative-placeholder': '45m',
+    'ui-lab.time-range.simulate-zoom': 'Simulate chart zoom',
+    'ui-lab.time-range.apply-query': 'Apply as query time',
+    'ui-lab.time-range.previous-query': 'Previous query: now-1h to now',
+    'ui-lab.time-range.release-window': 'Release validation window',
     'common.search': 'Search',
+    'common.select-page': 'Select page',
+    'common.select-all-results': 'Select all',
     'common.save': 'Save',
     'common.save-failed': 'Save failed',
     'common.save-success': 'Saved successfully',
     'common.saving': 'Saving...',
     'common.cancel': 'Cancel',
+    'common.button.return': 'Return',
     'common.failed': 'Failed',
+    'common.notice': 'Notice',
+    'common.unknown': 'Unknown',
+    'menu.fullscreen': 'Fullscreen',
+    'menu.fullscreen.exit': 'Exit fullscreen',
+    'alert.notify.title': 'New Alert',
+    'alert.notify.body': 'You have a new alert, please handle it in time',
+    'common.notify.apply-success': 'Applied successfully',
+    'common.notify.apply-fail': 'Apply failed',
+    'common.notify.copy-success': 'Copy Success!',
+    'common.notify.copy-fail': 'Copy Failed!',
+    'common.notify.query-fail': 'Query Failed!',
+    'common.notify.delete-success': 'Delete Success!',
+    'common.notify.delete-fail': 'Delete Failed!',
+    'common.notify.mark-success': 'Mark Success!',
+    'common.notify.mark-fail': 'Mark Failed!',
+    'common.notify.delete-pending': 'Deleting selected monitors',
+    'common.notify.edit-success': 'Edit Success!',
+    'common.notify.edit-fail': 'Edit Failed!',
+    'common.notify.operate-success': 'Operate Success!',
+    'common.notify.operate-fail': 'Operate Failed!',
+    'common.notify.enable-success': 'Enabled successfully',
+    'common.notify.enable-fail': 'Enable failed',
+    'common.notify.enable-pending': 'Enabling selected monitors',
+    'common.notify.cancel-success': 'Paused successfully',
+    'common.notify.cancel-fail': 'Pause failed',
+    'common.notify.cancel-pending': 'Pausing selected monitors',
+    'common.notify.export-success': 'Export started',
+    'common.notify.export-fail': 'Export failed',
+    'common.notify.export-pending': 'Preparing export',
+    'common.notify.export-fail-status': 'Export failed: {{status}}',
+    'common.notify.import-submitted': 'Import [{{taskName}}] submitted',
+    'common.notify.import-fail': 'Import failed',
+    'common.notify.import-pending': 'Preparing import',
+    'common.notify.import-fail-status': 'Import failed: {{status}}',
+    'common.notify.import-success-detail': 'Import [{{taskName}}] Success!',
+    'common.notify.import-fail-detail': 'Import [{{taskName}}] Failed: {{errMsg}}',
+    'common.notify.import-progress': 'Import [{{taskName}}] progress: {{progress}}',
+    'common.notify.new-success': 'Add Success!',
+    'common.notify.new-fail': 'Add Failed!',
+    'alert.notice.receiver.next': 'Please config your [Alert Notice Policy] next step!',
+    'common.notify.no-select-cancel': 'Select monitors before pausing',
+    'common.notify.no-select-delete': 'Select monitors before deleting',
+    'common.notify.no-select-enable': 'Select monitors before enabling',
+    'common.notify.no-select-export': 'No monitors selected for export',
     'common.help': 'Help',
+    'common.dialog.close': 'Close dialog',
     'common.ready': 'ready',
     'common.signals': 'Signals',
+    'app.frame.utility.menu': 'Menu',
+    'app.frame.utility.github': 'GitHub',
+    'app.frame.utility.user': 'User menu: {{user}}',
+    'menu.extras.setting': 'System settings',
+    'menu.account.logout': 'Logout',
+    'menu.extras.about': 'About',
+    'app.login.need-change-password': 'Please update the initial default password in time!',
+    'ai.chat.launch': 'Open AI assistant',
+    'ai.chat.title': 'AI assistant',
+    'ai.chat.subtitle': 'HertzBeat operations copilot',
+    'ai.chat.conversations': 'Conversations',
+    'ai.chat.new-chat': 'New chat',
+    'ai.chat.conversation.loading': 'Loading conversations...',
+    'ai.chat.conversation.ready': 'Conversation list synced from /chat/conversations.',
+    'ai.chat.conversation.empty': 'No saved conversations yet. Start a new chat from this workspace.',
+    'ai.chat.conversation.error': 'Conversation history is unavailable; the assistant shell remains usable.',
+    'ai.chat.conversation.delete.title': 'Delete conversation',
+    'ai.chat.conversation.delete.confirm': 'Delete',
+    'ai.chat.history.idle': 'Select a conversation to load its history.',
+    'ai.chat.history.loading': 'Loading selected conversation history...',
+    'ai.chat.history.ready': 'Selected conversation history is loaded.',
+    'ai.chat.history.empty': 'This conversation has no saved messages yet.',
+    'ai.chat.history.error': 'Selected conversation history is unavailable; try another conversation.',
+    'ai.chat.user': 'User',
+    'ai.chat.system': 'System',
+    'ai.chat.welcome.title': 'Ask HertzBeat',
+    'ai.chat.welcome.description': 'Ask about monitors, alerts, logs, traces, metrics, topology, and platform configuration.',
+    'ai.chat.initial-message': 'Initial request',
+    'ai.chat.assistant': 'Assistant',
+    'ai.chat.offline.response': 'The chat shell is ready; streaming response proof is the next parity slice.',
+    'ai.chat.typing': 'Typing...',
+    'ai.chat.error.processing': 'Sorry, there was an error processing your request. Please check if the AI Agent service is running and try again.',
+    'ai.chat.input.placeholder': 'Ask assistant',
+    'ai.chat.input.hint': 'Press Enter from the header prompt to open this assistant workspace.',
+    'ai.chat.submit': 'Send prompt',
+    'ai.chat.modify-api-key': 'Modify API key',
+    'ai.chat.config.title': 'AI Provider Configuration',
+    'ai.chat.config.required.content': 'To use AI Agent Chat, please configure your Model Provider.',
+    'ai.chat.config.provider': 'AI Provider',
+    'ai.chat.config.provider.help': 'Choose your AI provider (OpenAI, ZhiPu, or ZAI)',
+    'ai.chat.config.provider.required': 'Please select an AI provider',
+    'ai.chat.config.api-key': 'API Key',
+    'ai.chat.config.api-key.help': 'Your Provider API key. The key will be validated when saved.',
+    'ai.chat.config.api-key.required': 'API Key is required',
+    'ai.chat.config.base-url': 'Base URL',
+    'ai.chat.config.base-url.help': 'Custom API endpoint URL. Leave empty to use default for selected provider.',
+    'ai.chat.config.base-url.required': 'Base URL is required',
+    'ai.chat.config.model': 'Model',
+    'ai.chat.config.model.help': 'Model name to use. Leave empty to use default for selected provider.',
+    'ai.chat.config.model.required': 'Model is required',
+    'ai.chat.config.reset': 'Reset to default values',
+    'ai.chat.config.save': 'Save',
+    'ai.chat.config.cancel': 'Cancel',
+    'ai.chat.config.idle': 'Provider configuration is ready to load.',
+    'ai.chat.config.loading': 'Loading provider configuration...',
+    'ai.chat.config.ready': 'Provider configuration loaded from /config/provider.',
+    'ai.chat.config.saving': 'Saving provider configuration...',
+    'ai.chat.config.save.success': 'Model Provider configuration saved successfully!',
+    'ai.chat.config.save.failed': 'Failed to save configuration:',
+    'ai.chat.config.validation.failed': 'API Key validation failed:',
+    'ai.chat.schedule.title': 'Schedule Configuration',
+    'ai.chat.schedule.create': 'Create Schedule',
+    'ai.chat.schedule.add': 'Add New Task',
+    'ai.chat.schedule.configured': 'Configured Schedules',
+    'ai.chat.schedule.skill': 'Skill',
+    'ai.chat.schedule.skill.select': 'Select Skill',
+    'ai.chat.schedule.skill.placeholder': 'Select skill to execute',
+    'ai.chat.schedule.cron': 'Execution Time',
+    'ai.chat.schedule.cron.label': 'Execution Time (Cron Expression)',
+    'ai.chat.schedule.cron.help': 'Format: seconds minutes hours day month week',
+    'ai.chat.schedule.cron.example.daily': 'Example: 0 0 9 * * ? (daily at 9am)',
+    'ai.chat.schedule.cron.example.weekly': 'Example: 0 0 9 ? * MON (Monday 9am)',
+    'ai.chat.schedule.cron.common': 'Common: daily 9am',
+    'ai.chat.schedule.cron.monday': 'Monday 9am',
+    'ai.chat.schedule.status': 'Status',
+    'ai.chat.schedule.action': 'Action',
+    'ai.chat.schedule.create.success': 'Schedule created successfully',
+    'ai.chat.schedule.create.failed': 'Failed to create schedule',
+    'ai.chat.schedule.delete.success': 'Schedule deleted',
+    'ai.chat.schedule.delete.failed': 'Failed to delete schedule',
+    'ai.chat.schedule.toggle.success': 'Schedule status updated',
+    'ai.chat.schedule.toggle.failed': 'Failed to toggle schedule',
+    'ai.chat.schedule.push': 'Scheduled Push',
+    'ai.chat.schedule.button': 'Schedule',
+    'ai.chat.schedule.tooltip': 'Configure scheduled push tasks',
+    'ai.chat.schedule.edit': 'Edit Schedule',
+    'ai.chat.schedule.update.success': 'Schedule updated successfully',
+    'ai.chat.schedule.update.failed': 'Failed to update schedule',
+    'ai.chat.schedule.idle': 'Schedule configuration is ready to load.',
+    'ai.chat.schedule.loading': 'Loading schedules and skills...',
+    'ai.chat.schedule.ready': 'Schedules loaded from /ai/schedule.',
+    'ai.chat.schedule.empty': 'No configured schedules for this conversation.',
+    'ai.chat.schedule.saving': 'Saving schedule...',
+    'ai.chat.schedule.error': 'Failed to load schedules.',
+    'platform.footer.license': 'Licensed under the Apache License, Version 2.0',
+    'collector.health.cluster.title': 'Collection cluster health',
+    'collector.health.cluster.copy': 'Collectors {{online}} / {{total}} online',
+    'collector.health.cluster.meta': 'Tasks {{tasks}} · offline {{offline}}',
+    'collector.health.cluster.freshness': 'Last report {{time}}',
+    'collector.health.monitor.title': 'Collection health',
+    'collector.health.monitor.copy': 'Monitors {{healthy}} / {{total}} healthy',
+    'collector.health.monitor.empty-copy': 'No bound monitors',
+    'collector.health.monitor.meta': 'Collection evidence merged',
+    'collector.health.monitor.empty-meta': 'Waiting for template binding',
+    'collector.health.monitor.freshness': 'Latest evidence {{time}}',
     'entities.detail.loading': 'Loading entity detail...',
+    'entities.detail.title.fallback': 'Entity detail',
+    'entities.detail.fallback.display-name': 'Entity {{id}}',
+    'entities.detail.fallback.draft-name': 'Entity draft',
+    'entities.detail.fallback.description': 'Temporary workspace shown while backend entity detail is unavailable.',
+    'entities.detail.subtitle.active-alerts': 'This entity has active alerts. Confirm alerts and recent changes first.',
+    'entities.detail.subtitle.down-monitors': 'Bound monitors are abnormal. Start from monitor evidence to confirm impact.',
+    'entities.detail.subtitle.has-evidence': 'Logs, traces, or monitor evidence are linked and ready for investigation.',
+    'entities.detail.subtitle.needs-context': 'Add owner, definition, and telemetry binding so this entity becomes usable.',
+    'entities.detail.metric.bound-monitors': 'Bound monitors',
+    'entities.detail.metric.active-alerts': 'Active alerts',
+    'entities.detail.metric.identities': 'Identities',
+    'entities.detail.metric.relations': 'Relations',
+    'entities.detail.fact.id': 'Entity ID',
+    'entities.detail.fact.type': 'Type',
+    'entities.detail.fact.status': 'Status',
+    'entities.detail.fact.owner': 'Owner',
+    'entities.detail.status.healthy': 'Healthy',
+    'entities.detail.status.unknown': 'Unknown',
+    'entities.detail.status.abnormal': 'Abnormal',
+    'entities.detail.status.warning': 'Warning',
+    'entities.detail.overview.status': 'Status',
+    'entities.detail.overview.owner': 'Owner',
+    'entities.detail.overview.environment': 'Environment',
+    'entities.detail.overview.description': 'Description',
+    'entities.detail.summary.metrics.title': 'Related metrics',
+    'entities.detail.summary.metrics.bound': 'bound monitors',
+    'entities.detail.summary.metrics.down': 'abnormal monitors',
+    'entities.detail.summary.metrics.no-down': 'No abnormal monitors',
+    'entities.detail.summary.logs.title': 'Related logs',
+    'entities.detail.summary.logs.hints': 'query hints',
+    'entities.detail.summary.logs.available': '{{count}} available',
+    'entities.detail.summary.logs.no-hints': 'No query hints',
+    'entities.detail.summary.traces.title': 'Related traces',
+    'entities.detail.summary.traces.recent': 'recent traces',
+    'entities.detail.summary.traces.errors': 'error traces',
+    'entities.detail.summary.traces.no-errors': 'No error traces',
+    'entities.detail.handoff.metrics.title': 'Related metrics',
+    'entities.detail.handoff.metrics.meta': 'Metrics workbench',
+    'entities.detail.handoff.logs.title': 'Related logs',
+    'entities.detail.handoff.logs.meta': 'Logs workbench',
+    'entities.detail.handoff.traces.title': 'Related traces',
+    'entities.detail.handoff.traces.meta': 'Traces workbench',
+    'entities.detail.handoff.alerts.title': 'Alert rules',
+    'entities.detail.handoff.alerts.meta': 'Threshold rules',
+    'entities.detail.handoff.monitors.title': 'Bound monitors',
+    'entities.detail.handoff.monitors.meta': 'Monitored resources',
+    'entities.detail.handoff.topology.title': 'Upstream topology',
+    'entities.detail.handoff.topology.meta': 'Topology graph',
+    'entities.detail.handoff.template.title': 'Template binding',
+    'entities.detail.handoff.template.meta': 'Monitoring template',
+    'entities.detail.health.latency.no-data': 'No trace latency',
+    'entities.detail.health.latency.waiting': 'Waiting for OTLP span',
+    'entities.detail.health.latency.meta': 'Trace latency',
+    'entities.detail.health.latency.title': 'Latency',
+    'entities.detail.health.score.title': 'Health score',
+    'entities.detail.health.score.meta': 'Lightweight health model',
+    'entities.detail.health.availability.title': 'Availability',
+    'entities.detail.health.availability.waiting': 'Waiting for monitor template',
+    'entities.detail.health.availability.meta': '{{healthy}} / {{total}} monitors healthy',
+    'entities.detail.health.availability.no-monitors': 'No bound monitors',
+    'entities.detail.health.error-rate.title': 'Error rate',
+    'entities.detail.health.error-rate.no-samples': 'No trace samples',
+    'entities.detail.health.error-rate.meta': '{{errors}} / {{total}} error traces',
+    'entities.detail.health.error-rate.waiting': 'Waiting for trace reports',
+    'entities.detail.health.alerts.title': 'Current alerts',
+    'entities.detail.health.alerts.copy': '{{count}} active alerts',
+    'entities.detail.health.alerts.meta': 'Alert closure',
+    'entities.detail.health.anomalies.title': 'Recent anomalies',
+    'entities.detail.health.anomalies.copy': '{{count}} anomaly signals',
+    'entities.detail.health.anomalies.meta': 'Monitors {{monitors}} · Traces {{traces}} · Logs {{logs}}',
+    'entities.detail.health.collector.title': 'Collection health',
+    'entities.detail.evidence.coverage.title': 'Evidence coverage',
+    'entities.detail.evidence.coverage.copy': '{{count}} active signals',
+    'entities.detail.evidence.coverage.waiting': 'Waiting for backend evidence',
+    'entities.detail.evidence.coverage.no-model': 'No unified read model',
+    'entities.detail.evidence.coverage.no-signals': 'No active signals',
+    'entities.detail.evidence.metrics.title': 'Metrics / USE',
+    'entities.detail.evidence.metrics.copy': '{{count}} metric evidence',
+    'entities.detail.evidence.metrics.meta': 'Monitor and metric read model',
+    'entities.detail.evidence.logs.title': 'Logs / RED',
+    'entities.detail.evidence.logs.copy': '{{count}} log hints',
+    'entities.detail.evidence.logs.meta': 'Log read model',
+    'entities.detail.evidence.traces.title': 'Traces / RED',
+    'entities.detail.evidence.traces.copy': '{{count}} recent traces',
+    'entities.detail.evidence.traces.meta': 'Trace read model',
+    'entities.detail.evidence.traces.errors': '{{count}} error traces',
+    'entities.detail.evidence.latest.title': 'Latest observation',
+    'entities.detail.evidence.latest.waiting': 'Waiting for evidence timestamp',
+    'entities.detail.evidence.latest.meta': 'Backend read model',
+    'entities.detail.evidence-handoff.alerts.title': 'Active alert evidence',
+    'entities.detail.evidence-handoff.alerts.copy': '{{count}} active alerts',
+    'entities.detail.evidence-handoff.alerts.meta': 'Open alert handling',
+    'entities.detail.evidence-handoff.topology.title': 'Topology context',
+    'entities.detail.evidence-handoff.topology.default-relation': 'related',
+    'entities.detail.evidence-handoff.topology.unknown-target': 'Unknown target',
+    'entities.detail.evidence-handoff.runbook.title': 'Runbook',
+    'entities.detail.evidence-handoff.runbook.meta': 'Response guidance',
+    'entities.detail.action-text.open-monitors': 'Open monitors',
+    'entities.detail.action-text.open-discovery': 'Open discovery',
+    'entities.detail.action-text.open-definition': 'Open definition',
+    'entities.detail.action-text.inspect-abnormal': 'Inspect abnormal monitors first.',
+    'entities.detail.action-text.add-evidence': 'Add more evidence before triage.',
+    'entities.detail.action-text.review-definition': 'Review the definition workspace before adding ownership or evidence.',
+    'entities.detail.action-text.next-action': 'Next action',
+    'entities.detail.action-text.server-guidance': 'Server guidance',
+    'entities.detail.drilldown.definition.title': 'Definition workspace',
+    'entities.detail.drilldown.edit.title': 'Edit entity',
+    'entities.detail.drilldown.discovery.title': 'Telemetry discovery',
+    'entities.detail.drilldown.next-route': 'Next route',
+    'entities.detail.drilldown.shared-route': 'Shared route',
+    'entities.detail.alert-row.empty.title': 'Current alerts',
+    'entities.detail.alert-row.empty.copy': 'No active alerts',
+    'entities.detail.alert-row.empty.meta': '{{count}} active',
+    'entities.detail.alert-row.active.title': 'Current alert #{{id}}',
+    'entities.detail.alert-row.pending.copy': 'Alert pending',
+    'entities.detail.relationship-row.empty.title': 'Upstream/downstream relationships',
+    'entities.detail.relationship-row.empty.copy': 'No relationship data',
+    'entities.detail.relationship-row.empty.meta': 'Waiting for topology merge',
+    'entities.detail.relationship-row.unknown-target': 'Unknown target',
+    'entities.detail.relationship-row.default-meta': 'Upstream/downstream relationship',
+    'entities.detail.attribution.traditional-monitor.title': 'Traditional monitor binding',
+    'entities.detail.attribution.monitor-bind.count': '{{count}} bindings',
+    'entities.detail.attribution.traditional-monitor.waiting-meta': 'Waiting for monitor template',
+    'entities.detail.attribution.otlp.title': 'OTLP attribution',
+    'entities.detail.attribution.identity.count': '{{count}} identities',
+    'entities.detail.attribution.identity.missing': 'Missing identity',
+    'entities.detail.attribution.otlp.waiting-meta': 'Waiting for hertzbeat.entity_id or service.name',
+    'entities.detail.attribution.candidate.title': 'Candidate confirmation',
+    'entities.detail.attribution.candidate.assigned': 'Merged into entity',
+    'entities.detail.attribution.candidate.pending': 'Pending confirmation',
+    'entities.detail.attribution.candidate.discover-meta': 'Open telemetry discovery to confirm candidate',
+    'entities.detail.attribution.diagnostic.title': 'Attribution diagnostics',
+    'entities.detail.attribution.diagnostic.complete': 'Attribution evidence complete',
+    'entities.detail.attribution.diagnostic.incomplete': 'Attribution evidence incomplete',
+    'entities.detail.attribution.diagnostic.missing': 'Attribution evidence needed',
+    'entities.detail.attribution.diagnostic.complete-meta': 'Ready for entity detail',
+    'entities.detail.attribution.diagnostic.missing-part.identity': 'identity',
+    'entities.detail.attribution.diagnostic.missing-part.monitor-bind': 'monitor binding',
+    'entities.detail.attribution.diagnostic.missing-meta': 'Missing {{parts}}',
+    'entities.detail.collection.source.title': 'Collection source',
+    'entities.detail.collection.source.meta': 'Entity source',
+    'entities.detail.collection.identity.title': 'Identities',
+    'entities.detail.collection.identity.count': '{{count}} identities',
+    'entities.detail.collection.template.title': 'Template binding',
+    'entities.detail.collection.template.count': '{{count}} bindings',
+    'entities.detail.collection.labels.title': 'Labels',
+    'entities.detail.collection.labels.count': '{{count}} labels',
+    'entities.detail.header.kicker': 'Entity-first investigation',
+    'entities.detail.header.badge': 'Entity detail',
+    'entities.detail.action.all-entities': 'All entities',
+    'entities.detail.action.edit-definition': 'Edit definition',
+    'entities.detail.action.delete': 'Delete',
+    'entities.detail.action.edit': 'Edit',
+    'entities.detail.error.title': 'Operation incomplete',
+    'entities.detail.panel.overview.title': 'Context',
+    'entities.detail.panel.overview.copy': 'Confirm identity, ownership, and runtime state before entering signal workbenches.',
+    'entities.detail.panel.context.title': 'Observability context',
+    'entities.detail.panel.context.copy': 'Every handoff carries entity, service, environment, and time range; trace links also carry trace/span.',
+    'entities.detail.panel.health.title': 'Lightweight health model',
+    'entities.detail.panel.health.copy': 'Use availability, error rate, latency, current alerts, recent anomalies, and collection health as a lightweight score without SLO authoring.',
+    'entities.detail.panel.evidence.title': 'Evidence model',
+    'entities.detail.panel.evidence.copy': 'Expose backend RED / USE read model coverage before opening signal workbenches.',
+    'entities.detail.panel.alerts.title': 'Current alerts',
+    'entities.detail.panel.alerts.copy': 'Confirm active impact before choosing silence, inhibition, grouping, or notification closure.',
+    'entities.detail.panel.next.title': 'Next step',
+    'entities.detail.panel.next.copy': 'Keep server-side suggestions, but present them in the current cold console language.',
+    'entities.detail.panel.related.title': 'Related signals',
+    'entities.detail.panel.related.copy': 'Use monitors, logs, and traces to quickly judge impact scope.',
+    'entities.detail.panel.relationships.title': 'Upstream and downstream',
+    'entities.detail.panel.relationships.copy': 'Relationships come from entity merge, calls, monitor ownership, or CMDB labels and later flow into topology impact.',
+    'entities.detail.panel.source.title': 'Collection source',
+    'entities.detail.panel.source.copy': 'Confirm whether this entity comes from HertzBeat monitors, Collector, OTLP resource attributes, or manual/CMDB labels.',
+    'entities.detail.panel.drilldown.title': 'Advanced entries',
+    'entities.detail.panel.drilldown.copy': 'Keep only useful deep entries without adding right-side statistical decoration.',
+    'entities.detail.disposition.title': 'Disposition cadence',
+    'entities.detail.disposition.copy': 'Alert, monitor, and log entries branch from here; the detail page only keeps entity context and does not copy the catalog right rail.',
+    'entities.detail.delete.title': 'Delete entity',
+    'entities.detail.delete.kicker': 'Entity catalog',
+    'entities.detail.delete.cancel': 'Cancel',
+    'entities.detail.delete.confirm': 'Confirm delete',
+    'entities.detail.delete.copy': 'After deletion, this entity is removed from the entity catalog, and related monitor bindings, telemetry context, and alert investigation entries need to be re-merged.',
     'entities.detail.delete.failed': 'Failed to delete entity.',
+    'entities.detail.attribution.ready': 'Attributed',
+    'entities.detail.attribution.review': 'Review',
+    'entities.detail.attribution.missing': 'Missing',
+    'entities.editor.submit.create': 'Create entity',
+    'entities.editor.stage.basic.label': 'Basic information',
+    'entities.editor.stage.basic.description': 'Confirm type, name, and core metadata first.',
+    'entities.editor.stage.ownership.label': 'Ownership and response',
+    'entities.editor.stage.ownership.description': 'Add owners, contacts, and response entries.',
+    'entities.editor.stage.signals.label': 'Evidence links',
+    'entities.editor.stage.signals.description': 'Bind monitors, identities, and telemetry evidence.',
+    'entities.editor.stage.relations.label': 'Relations and extensions',
+    'entities.editor.stage.relations.description': 'Record upstream/downstream relations, labels, and extended fields.',
+    'entities.editor.route-tab.detail': 'Entity detail',
+    'entities.editor.route-tab.monitor': 'Monitor center',
+    'entities.editor.route-tab.logs': 'Logs and statistics',
+    'entities.editor.route-tab.traces': 'Traces',
+    'entities.editor.shell.kicker': 'Entity',
+    'entities.editor.shell.title.new': 'Create entity',
+    'entities.editor.shell.title.edit': 'Edit entity',
+    'entities.editor.shell.copy.new': 'Create the entity record first, then complete evidence, owner, and dependencies.',
+    'entities.editor.shell.copy.edit': 'Update the entity record first, then complete evidence, owner, and dependencies.',
+    'entities.editor.shell.freshness.new': '0% recently updated',
+    'entities.editor.shell.freshness.edit': '75% recently updated',
+    'entities.editor.shell.all-entities': 'All entities',
+    'entities.editor.shell.definition-workspace': 'Definition workspace',
+    'entities.editor.shell.entity-label': 'Entity',
+    'entities.editor.shell.preview.show': 'Show snapshot',
+    'entities.editor.shell.preview.hide': 'Hide snapshot',
+    'entities.editor.shell.definition-version': 'Definition version · HertzBeat v1 / OTel resource spec',
+    'entities.editor.shell.name-label': 'Name',
+    'entities.editor.shell.name-placeholder': 'Unique entity name',
+    'entities.editor.shell.type-label': 'Type',
+    'entities.editor.shell.entry-source-label': 'Entry source',
+    'entities.editor.shell.stage-label': 'Edit stage',
+    'entities.editor.shell.preview-rail.title': 'Entity metadata',
+    'entities.editor.shell.preview-rail.copy': 'View or paste a definition and apply it to the form.',
+    'entities.editor.shell.definition-content.title': 'Definition content',
+    'entities.editor.shell.definition-content.copy': 'View or paste a definition and apply it to the form.',
+    'entities.editor.mode.editor': 'Form',
+    'entities.editor.type.system.label': 'System',
+    'entities.editor.type.system.description': 'Business domain or product line',
+    'entities.editor.type.service.label': 'Service',
+    'entities.editor.type.service.description': 'Application or API that provides business capability',
+    'entities.editor.type.host.label': 'Host',
+    'entities.editor.type.host.description': 'Runtime node that hosts services',
+    'entities.editor.type.database.label': 'Database',
+    'entities.editor.type.database.description': 'Data persistence instance',
+    'entities.editor.type.queue.label': 'Queue',
+    'entities.editor.type.queue.description': 'Asynchronous messaging channel',
+    'entities.editor.type.middleware.label': 'Middleware',
+    'entities.editor.type.middleware.description': 'Shared infrastructure component',
+    'entities.editor.type.api.label': 'API',
+    'entities.editor.type.api.description': 'Interface or gateway resource',
+    'entities.editor.type.endpoint.label': 'Endpoint',
+    'entities.editor.type.endpoint.description': 'Observable access entry',
+    'entities.editor.type.device.label': 'Device',
+    'entities.editor.type.device.description': 'Network or edge device',
+    'entities.editor.type.k8s-workload.label': 'K8s workload',
+    'entities.editor.type.k8s-workload.description': 'Kubernetes-native workload',
+    'entities.editor.entry-source.manual': 'Manual entry',
+    'entities.editor.entry-source.telemetry': 'Telemetry discovery',
+    'entities.editor.entry-source.definition': 'Definition first',
+    'entities.editor.telemetry-handoff.title': 'Telemetry discovery',
+    'entities.editor.telemetry-handoff.copy': 'Identify the entity from monitor and OTel resource fields first, then complete its basic information.',
+    'entities.editor.telemetry-handoff.monitor-count': '{{count}} monitor bindings',
+    'entities.editor.telemetry-handoff.identity-count': '{{count}} identities',
+    'entities.editor.telemetry-handoff.attribution-check': 'Attribution check',
+    'entities.editor.field.display-name': 'Display name',
+    'entities.editor.field.namespace': 'Namespace',
+    'entities.editor.field.environment': 'Environment',
+    'entities.editor.field.subtype': 'Subtype',
+    'entities.editor.field.owner': 'Owner',
+    'entities.editor.field.system': 'System',
+    'entities.editor.field.source': 'Source',
+    'entities.editor.field.description': 'Description',
+    'entities.editor.field.description-placeholder': 'For example: handles settlement and payment orchestration.',
+    'entities.editor.field.runbook': 'Runbook',
+    'entities.editor.placeholder.display-name': 'For example: Checkout API',
+    'entities.editor.placeholder.namespace': 'For example: commerce',
+    'entities.editor.placeholder.environment': 'For example: prod',
+    'entities.editor.placeholder.subtype': 'For example: http',
+    'entities.editor.placeholder.owner': 'For example: payments-team',
+    'entities.editor.placeholder.system': 'For example: website',
+    'entities.editor.placeholder.source': 'manual',
+    'entities.editor.contact.title': 'Contacts',
+    'entities.editor.contact.add': 'Add contact',
+    'entities.editor.contact.name-placeholder': 'Contact',
+    'entities.editor.contact.type-placeholder': 'Type',
+    'entities.editor.contact.value-placeholder': 'Contact value',
+    'entities.editor.contact.contact-placeholder': 'Contact field',
+    'entities.editor.link.title': 'Links',
+    'entities.editor.link.add': 'Add link',
+    'entities.editor.link.name-placeholder': 'Link name',
+    'entities.editor.link.type-placeholder': 'Type',
+    'entities.editor.link.provider-placeholder': 'Provider',
+    'entities.editor.link.url-placeholder': 'Link URL',
+    'entities.editor.collection.identities': 'Identities',
+    'entities.editor.collection.identities.add': 'Add identity',
+    'entities.editor.collection.monitor-binds': 'Monitor bindings',
+    'entities.editor.collection.monitor-binds.add': 'Add monitor binding',
+    'entities.editor.collection.relations': 'Relations',
+    'entities.editor.collection.relations.add': 'Add relation',
+    'entities.editor.relation.component-of': 'Parent components',
+    'entities.editor.relation.components': 'Child components',
+    'entities.editor.relation.implemented-by': 'Implemented by',
+    'entities.editor.relation.languages': 'Languages',
+    'entities.editor.relation.component-of-placeholder': 'payment-platform, commerce',
+    'entities.editor.relation.components-placeholder': 'checkout-ui, order-router',
+    'entities.editor.relation.implemented-by-placeholder': 'checkout-api, order-worker',
+    'entities.editor.relation.languages-placeholder': 'java, typescript',
+    'entities.editor.relation.labels': 'Labels',
+    'entities.editor.relation.label-key': 'Label key',
+    'entities.editor.relation.label-value': 'Label value',
+    'entities.editor.relation.label-add': 'Add label',
+    'entities.editor.definition.aria.yaml': 'YAML definition snapshot',
+    'entities.editor.definition.aria.json': 'JSON definition snapshot',
+    'entities.editor.attribution.state.ready': 'Ready',
+    'entities.editor.attribution.state.missing': 'Needs input',
+    'entities.editor.attribution.state.review': 'Review',
+    'entities.editor.attribution.identity.title': 'Identities',
+    'entities.editor.attribution.identity.count': '{{count}} identities',
+    'entities.editor.attribution.identity.missing': 'Missing identity',
+    'entities.editor.attribution.identity.waiting-meta': 'Waiting for service.name or hertzbeat.entity_id',
+    'entities.editor.attribution.identity.recognized-meta': 'Identity recognized',
+    'entities.editor.attribution.monitor.title': 'Monitor binding',
+    'entities.editor.attribution.monitor.count': '{{count}} monitor bindings',
+    'entities.editor.attribution.monitor.waiting-meta': 'Waiting for monitor object or template binding',
+    'entities.editor.attribution.monitor.existing-meta': 'Monitor binding present',
+    'entities.editor.attribution.owner.title': 'Owner',
+    'entities.editor.attribution.owner.missing': 'Missing owner',
+    'entities.editor.attribution.owner.ready-meta': 'Accountable',
+    'entities.editor.attribution.owner.missing-meta': 'Add owner or on-call group first',
+    'entities.editor.attribution.system-environment.title': 'System and environment',
+    'entities.editor.attribution.system-environment.missing-environment': '{{system}} · missing environment',
+    'entities.editor.attribution.system-environment.missing-system': 'missing system · {{environment}}',
+    'entities.editor.attribution.system-environment.missing-both': 'Missing system and environment',
+    'entities.editor.attribution.system-environment.meta': 'Used for alert convergence and topology',
+    'entities.editor.attribution.discovery.title': 'Discovery loop',
+    'entities.editor.attribution.discovery.copy': 'Return to telemetry discovery',
+    'entities.new.loading': 'Loading entity draft',
+    'entities.edit.loading': 'Loading entity editor',
+    'entities.definition.loading': 'Loading entity definition...',
+    'entities.definition.workspace.kicker': 'Entity definition',
+    'entities.definition.workspace.title': 'Edit entity definition',
+    'entities.definition.workspace.subtitle': 'Edit the current definition directly. Saving syncs basic information, telemetry bindings, and dependencies.',
+    'entities.definition.workspace.format-help': 'Supports YAML, JSON, and cURL request content. Preview parsing before saving.',
+    'entities.definition.workspace.attribution-check': 'Attribution check',
+    'entities.definition.workspace.attribution-state.ready': 'Ready',
+    'entities.definition.workspace.attribution-state.missing': 'Needs data',
+    'entities.definition.workspace.attribution-state.unknown': 'Needs review',
+    'entities.definition.workspace.metric.ready': 'Ready to import',
+    'entities.definition.workspace.metric.attention': 'Needs attention',
+    'entities.definition.workspace.metric.telemetry-gaps': 'Missing telemetry bindings',
+    'entities.definition.workspace.fact.workspace': 'Workspace',
+    'entities.definition.workspace.fact.format': 'Format',
+    'entities.definition.workspace.fact.activities': 'Activities',
+    'entities.definition.workspace.fact.templates': 'Templates',
+    'entities.definition.workspace.current-definition': 'Current definition',
+    'entities.definition.workspace.template-title': 'Custom templates',
+    'entities.definition.workspace.template-error-help': 'After definition loading fails, confirm whether the entity exists before continuing edits.',
+    'entities.definition.workspace.template-empty-title': 'No templates',
+    'entities.definition.workspace.template-empty-copy': 'Reusable templates appear here.',
+    'entities.definition.workspace.batch-title': 'Templates and batch editing',
+    'entities.definition.workspace.batch-copy': 'Review templates, draft source, and optional actions.',
+    'entities.definition.workspace.batch-expand': 'Expand',
+    'entities.definition.workspace.activity-title': 'Recent activity',
+    'entities.definition.workspace.activity-empty-title': 'No recent activity',
+    'entities.definition.workspace.activity-empty-copy': 'Preview and import activity appears here.',
+    'entities.definition.workspace.entry-title': 'Entry',
+    'entities.definition.workspace.entry-view-entity': 'View entity',
+    'entities.definition.workspace.entry-back-to-form': 'Back to form',
+    'entities.definition.message.load-failed': 'Failed to load entity definition.',
+    'entities.definition.message.entity-not-exist': 'Entity does not exist.',
+    'entities.definition.message.process-failed': 'Entity definition processing failed.',
+    'entities.definition.message.update-missing-entity': 'Cannot confirm which entity definition to update.',
+    'entities.definition.message.update-failed': 'Failed to save entity definition.',
+    'entities.definition.message.updated': 'Entity definition updated.',
+    'entities.definition.message.backend-fallback': 'Backend reported: {{message}}',
+    'entity.definition.format.label': 'Import format',
+    'entity.definition.format.option.yaml': 'YAML',
+    'entity.definition.format.option.json': 'JSON',
+    'entity.definition.format.option.curl': 'cURL',
+    'entity.definition.action.clear-draft': 'Clear draft',
+    'entity.definition.action.preview': 'Preview definition',
+    'entity.definition.action.save': 'Save definition',
+    'entity.definition.action.import': 'Import entities',
+    'entity.definition.import.empty-preview': 'Paste a definition before previewing.',
+    'entity.definition.import.empty-save': 'Paste or load a definition before saving.',
+    'entity.definition.import.empty-import': 'Paste a definition before importing.',
+    'entity.definition.import.activity.preview-one': 'Previewed 1 definition.',
+    'entity.definition.import.activity.preview-many': 'Previewed {{count}} definitions.',
+    'entity.definition.import.parse-failed': 'Definition parsing failed.',
+    'entity.definition.workspace.single-required': 'Editing an existing entity can only save one definition document at a time.',
+    'entity.definition.import.blocked': 'Some definitions still need attention. Review them before importing.',
+    'entity.definition.import.success-bundle': 'Imported {{count}} entities.',
+    'entity.definition.import.success-single': 'Entity definition imported.',
+    'entity.definition.import.create-failed': 'Entity import failed.',
+    'entity.definition.import.format': 'Import Format',
+    'entity.definition.import.format.unknown': 'Unknown format {{format}}',
+    'entity.definition.workspace.template.custom': 'Custom template',
+    'entity.field.owner': 'Owner',
+    'entity.field.system': 'System',
+    'entity.field.implemented-by': 'Implemented by',
+    'entity.field.runbook': 'Runbook',
+    'entity.definition.import.gap.telemetry': 'Telemetry bindings',
+    'entity.definition.import.telemetry.ready': 'Telemetry binding detected',
+    'entity.definition.import.telemetry.pending': 'Telemetry binding still missing',
+    'entity.definition.import.validation.ready': 'Ready to import',
+    'entity.definition.import.validation.needs-attention': 'Needs details before import',
+    'entity.definition.import.kind.unknown': 'Unknown type {{kind}}',
+    'entity.definition.import.source.unknown': 'Unknown source {{source}}',
+    'entity.definition.import.summary.ready': 'Ready to import',
+    'entity.definition.import.summary.attention': 'Needs attention',
+    'entity.definition.import.summary.telemetry': 'Telemetry still missing',
+    'entity.definition.import.queue.ready.title': 'Ready to import',
+    'entity.definition.import.queue.ready.copy': 'These definitions already meet the basic requirements and can move straight into import or save.',
+    'entity.definition.import.queue.ready.action': 'View ready definitions',
+    'entity.definition.import.queue.attention.title': 'Needs attention first',
+    'entity.definition.import.queue.attention.copy': 'These definitions still miss owner, system, runbook, or interface details. Fix them before import.',
+    'entity.definition.import.queue.attention.action': 'View blocked definitions',
+    'entity.definition.import.queue.telemetry.title': 'Still needs telemetry after import',
+    'entity.definition.import.queue.telemetry.copy': 'These definitions still do not have telemetry bindings and should return to telemetry discovery after import.',
+    'entity.definition.import.queue.telemetry.action': 'View telemetry-pending definitions',
+    'entity.definition.import.placeholder.yaml': 'Paste a YAML entity definition. The editor parses basic information, telemetry bindings, and dependencies.',
+    'entity.definition.import.placeholder.json': 'Paste a JSON entity definition. The editor parses basic information, telemetry bindings, and dependencies.',
+    'entity.definition.import.placeholder.curl': 'Paste a cURL request generated from preview or automation flows.',
+    'entity.definition.import.attribution.ready': 'Attribution complete',
+    'entity.definition.import.attribution.missing': 'Attribution missing',
+    'entity.definition.import.attribution.review': 'Attribution needs review',
+    'entity.definition.import.activity.status.success': 'Success',
+    'entity.definition.import.activity.status.saved': 'Saved',
+    'entity.definition.import.activity.status.starter': 'Starter',
+    'entity.definition.import.activity.status.failed': 'Failed',
+    'entity.definition.import.activity.status.preview': 'Preview',
+    'entity.definition.import.activity.status.unknown': 'Unknown status {{status}}',
+    'entity.definition.import.activity.summary.telemetry-discovery-applied': 'Telemetry discovery applied',
+    'entity.definition.import.activity.summary.catalog-entity-updated': 'Catalog entity updated',
+    'entity.definition.import.activity.summary.catalog-entity-created': 'Catalog entity created',
+    'entity.definition.import.activity.field.endpoint': 'Endpoint',
+    'entity.definition.import.activity.field.service': 'Service',
+    'entity.definition.import.activity.field.system': 'System',
+    'entity.definition.import.activity.field.source': 'Source',
+    'entity.definition.import.activity.field.owner': 'Owner',
+    'entity.definition.import.activity.field.environment': 'Environment',
+    'entity.definition.import.activity.field.evidence': 'Evidence',
+    'entity.definition.import.activity.field.monitor-binds': 'monitor binds',
+    'entity.definition.import.template.source.workspace': 'Workspace',
+    'entity.definition.import.template.source.saved': 'Saved',
+    'entity.definition.import.template.source.starter': 'Starter',
+    'entity.definition.import.template.source.custom': 'Custom',
+    'entity.definition.import.template.source.unknown': 'Unknown source {{source}}',
+    'entities.import.loading': 'Loading entity import...',
+    'entities.import.workspace.title': 'Import entity definitions',
+    'entities.import.workspace.subtitle': 'Import one or more entity definitions from YAML, JSON, or cURL request content. Preview before writing to the catalog.',
+    'entities.import.workspace.format-help': 'Supports YAML, JSON, and cURL generated from preview or automation flows. Import preview parses basic information, telemetry bindings, and dependencies first.',
+    'entities.discovery.loading': 'Loading telemetry discovery...',
+    'entities.discovery.workspace.kicker': 'Object-first investigation',
+    'entities.discovery.workspace.title': 'Telemetry discovery',
+    'entities.discovery.workspace.subtitle': 'Search monitor clues that need governance, then decide whether to merge, complete ownership, or continue closure in the definition workspace.',
+    'entities.discovery.action.import': 'Create from definition',
+    'entities.discovery.action.create': 'Create entity',
+    'entities.discovery.action.catalog': 'Object catalog',
+    'entities.discovery.action.search': 'Search',
+    'entities.discovery.action.clear': 'Clear',
+    'entities.discovery.search.placeholder': 'Search monitor name or instance',
+    'entities.discovery.search.error': 'Telemetry discovery query failed',
+    'entities.discovery.candidate.title': 'OTLP candidate entity',
+    'entities.discovery.candidate.copy': 'Preserve OTLP resource identity and write to the object catalog only after confirmation.',
+    'entities.discovery.candidate.action.create': 'Create entity draft',
+    'entities.discovery.candidate.identity': 'Resource identity',
+    'entities.discovery.candidate.namespace': 'Namespace',
+    'entities.discovery.candidate.environment': 'Environment',
+    'entities.discovery.policy.title': 'Governance filters and shared policy',
+    'entities.discovery.policy.copy': 'Complete clue context with existing owner, system, and environment presets.',
+    'entities.discovery.policy.latest-activity': 'Latest activity {{activity}}',
+    'entities.discovery.policy.empty': 'No shared policy yet. Search a monitor clue first.',
+    'entities.discovery.catalog.owner': 'Owner',
+    'entities.discovery.catalog.system': 'System',
+    'entities.discovery.catalog.environment': 'Environment',
+    'entities.discovery.metric.clues': 'Clues',
+    'entities.discovery.metric.matched': 'Matched',
+    'entities.discovery.metric.create-suggested': 'Create suggested',
+    'entities.discovery.metric.catalog-sources': 'Catalog sources',
+    'entities.discovery.table.title': 'Discovered clues',
+    'entities.discovery.table.result-count': '{{count}} results',
+    'entities.discovery.table.waiting': 'Waiting for search',
+    'entities.discovery.table.column.clue': 'Clue',
+    'entities.discovery.table.column.instance': 'Instance',
+    'entities.discovery.table.column.status': 'Status',
+    'entities.discovery.table.column.owner': 'Owner',
+    'entities.discovery.table.column.system': 'System',
+    'entities.discovery.table.column.environment': 'Environment',
+    'entities.discovery.table.column.attribution': 'Attribution',
+    'entities.discovery.table.column.action': 'Action',
+    'entities.discovery.empty.title': 'Search monitor clues that need governance',
+    'entities.discovery.empty.search.copy': 'No matching monitor clues were found.',
+    'entities.discovery.empty.idle.copy': 'Search a monitor first, then convert it into an entity draft.',
+    'entities.discovery.activity.preset-synced': 'Preset synced',
+    'entities.discovery.activity.shared-governance-updated': 'Shared governance updated',
+    'entities.discovery.activity.empty': 'No activity yet',
+    'entities.discovery.row.status.enabled': 'Enabled',
+    'entities.discovery.row.status.normal': 'Normal',
+    'entities.discovery.row.status.abnormal': 'Abnormal',
+    'entities.discovery.row.status.review': 'Needs review',
+    'entities.discovery.row.status.unknown': 'Unknown status {{status}}',
+    'entities.discovery.row.activity.search-result': 'Search result',
+    'entities.discovery.row.activity.catalog-preset': 'Catalog preset',
+    'entities.discovery.row.missing.owner': 'owner',
+    'entities.discovery.row.missing.system': 'system',
+    'entities.discovery.row.missing.environment': 'environment',
+    'entities.discovery.row.missing.separator': ', ',
+    'entities.discovery.row.missing.copy': 'Missing {{fields}}',
+    'entities.discovery.row.missing.ready': 'Attribution context needs review',
+    'entities.discovery.row.attribution.preset.label': 'Catalog preset',
+    'entities.discovery.row.attribution.preset.copy': 'Can be used as a candidate review baseline',
+    'entities.discovery.row.attribution.preset.action': 'View preset',
+    'entities.discovery.row.attribution.resolved.label': 'Attributed',
+    'entities.discovery.row.attribution.resolved.copy': 'Already folded into the object catalog',
+    'entities.discovery.row.attribution.resolved.action': 'Open entity',
+    'entities.discovery.row.attribution.merge.label': 'Suggested merge',
+    'entities.discovery.row.attribution.merge.copy': 'Candidate entity {{candidate}}',
+    'entities.discovery.row.attribution.merge.action': 'Confirm merge',
+    'entities.discovery.row.attribution.review.label': 'Attribution incomplete',
+    'entities.discovery.row.attribution.review.action': 'Complete attribution',
+    'entities.discovery.row.attribution.create.label': 'Suggested new',
+    'entities.discovery.row.attribution.create.copy': 'Monitoring clues are ready for an entity draft',
+    'entities.discovery.row.attribution.create.action': 'Create entity',
+    'entities.discovery.service-name': '{{name}} service',
+    'entities.discovery.facts.workspace': 'Workspace',
+    'entities.discovery.facts.presets': 'Presets',
+    'entities.discovery.facts.activities': 'Activities',
+    'entities.discovery.facts.owners': 'Owners',
+    'entities.discovery.metrics.owners': 'owners',
+    'entities.discovery.metrics.systems': 'systems',
+    'entities.discovery.metrics.environments': 'environments',
+    'entities.discovery.metrics.preset-coverage': 'preset coverage',
+    'entities.discovery.metrics.ready': 'ready',
+    'entities.discovery.metrics.empty': 'empty',
+    'entities.discovery.catalog.owners': 'owners',
+    'entities.discovery.catalog.systems': 'systems',
+    'entities.discovery.catalog.environments': 'environments',
+    'entities.discovery.catalog.count': 'count {{count}}',
+    'entities.discovery.catalog.none': 'none',
+    'entities.discovery.card.action.open-resolved': 'Open resolved entity',
+    'entities.discovery.card.action.open-definition': 'Open definition',
+    'entities.discovery.card.action.merge-suggested': 'Merge into suggested entity',
+    'entities.discovery.card.action.open-suggested': 'Open suggested entity',
+    'entities.discovery.card.action.adopt-draft': 'Adopt as draft',
+    'entities.discovery.card.action.review-governance': 'Review governance',
+    'entities.discovery.card.action.send-definition': 'Send to definition workspace',
+    'entities.discovery.card.action.create-draft': 'Create entity draft',
+    'entities.discovery.card.recommendation.resolved': 'This telemetry has already been folded into the catalog, so the next step is to reopen the resolved entity or its definition workspace.',
+    'entities.discovery.card.recommendation.merge': 'A matching directory candidate already exists, so this telemetry should merge before a new draft is created.',
+    'entities.discovery.card.recommendation.enrich': 'Fill the shared governance context first, then send the monitor into the definition workspace or create a draft.',
+    'entities.discovery.card.recommendation.create': 'This telemetry already has enough shared governance context to open a new entity draft right away.',
+    'entities.discovery.card.risk.low': 'Governance risk low',
+    'entities.discovery.card.risk.medium': 'Governance risk medium',
+    'entities.discovery.card.risk.high': 'Governance risk high',
+    'entities.discovery.card.next.open': 'Next step open',
+    'entities.discovery.card.next.merge': 'Next step merge',
+    'entities.discovery.card.next.enrich': 'Next step enrich',
+    'entities.discovery.card.next.create': 'Next step create',
+    'entities.discovery.card.candidate-label': 'Suggested entity · {{candidate}} · score strong',
+    'entities.discovery.scope.all': 'All',
+    'entities.discovery.scope.matched': 'Matched',
+    'entities.discovery.scope.resolved': 'Resolved',
+    'entities.discovery.scope.suggested-new': 'Suggested new',
+    'entities.discovery.queue.review.title': 'Needs governance review',
+    'entities.discovery.queue.review.summary': '{{count}} telemetry results still need governance review before they can be merged or created.',
+    'entities.discovery.queue.review.action': 'Review governance',
+    'entities.discovery.queue.merge.title': 'Ready to merge',
+    'entities.discovery.queue.merge.summary': '{{count}} telemetry results can be selected for merge immediately.',
+    'entities.discovery.queue.merge.action': 'Select merge-ready',
+    'entities.discovery.queue.create.title': 'Suggested new entities',
+    'entities.discovery.queue.create.summary': '{{count}} telemetry results are ready to open as new entity drafts or send into the definition workspace.',
+    'entities.discovery.queue.create.action': 'Select suggested new',
+    'entities.discovery.queue.resolved.title': 'Already resolved',
+    'entities.discovery.queue.resolved.summary.present': '{{count}} telemetry results already map to resolved catalog entities.',
+    'entities.discovery.queue.resolved.summary.empty': 'No discovery results are marked as resolved yet.',
+    'entities.discovery.queue.resolved.action': 'View resolved',
+    'entities.discovery.bulk.tag.owner': 'Owner',
+    'entities.discovery.bulk.tag.system': 'System',
+    'entities.discovery.bulk.preset.apply': 'Apply {{name}}',
     'entity.health.label': 'Health score {{score}}',
     'entity.health.copy.collected': 'Collected {{healthy}} / {{total}} healthy',
     'entity.health.copy.waiting': 'Waiting for collection binding',
     'entity.health.meta': 'Alerts {{alerts}} · anomalies {{anomalies}}',
-    'topology.identity': 'HertzBeat operations topology',
-    'topology.kicker': 'Topology and impact',
+    'topology.identity': 'Operations topology',
+    'topology.kicker': 'Topology',
     'topology.copy': 'Center the selected entity and correlate OTLP calls, monitor ownership, template dependencies, K8s workloads, database and middleware connections, and alert impact in one operations graph.',
     'topology.refresh': 'Refresh topology',
     'topology.environment.aria': 'Select environment',
@@ -107,14 +905,121 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.search.aria': 'Filter topology',
     'topology.search.placeholder': 'Search entities, services, resources, or labels',
     'topology.view.fit': 'Fit view',
+    'topology.view.zoom-in': 'Zoom in',
+    'topology.view.zoom-out': 'Zoom out',
+    'topology.view.focus-selected': 'Focus selected node',
     'topology.locate-entity': 'Locate entity',
     'topology.current-filter': 'Current filter',
     'topology.all-entities': 'All entities',
+    'topology.state.label': 'Investigation state',
+    'topology.state.focus': 'Focus',
+    'topology.state.depth': 'Depth',
+    'topology.state.depth.auto': 'Auto',
+    'topology.state.depth.one-hop': '1-hop',
+    'topology.state.depth.two-hop': '2-hop',
+    'topology.state.depth.n-hop': '{{depth}}-hop',
+    'topology.state.layout': 'Layout',
+    'topology.state.layout.layered-service': 'Layered service',
+    'topology.state.layout.force': 'Force',
+    'topology.state.layout.grid-table': 'Grid / table',
+    'topology.state.group': 'Group',
+    'topology.state.group.none': 'None',
+    'topology.state.group.environment': 'Environment',
+    'topology.state.group.source-kind': 'Source kind',
+    'topology.state.group.entity-type': 'Entity type',
+    'topology.toolbar.reset': 'Reset',
+    'topology.focus-trail.label': 'Focused topology',
+    'topology.focus-trail.exit': 'Exit focus',
+    'topology.focus-trail.hidden-count': '{{count}} hidden by scope',
+    'topology.focus-trail.filter.environment': 'Environment',
+    'topology.focus-trail.filter.time-range': 'Time range',
+    'topology.focus-trail.filter.source': 'Source',
+    'topology.focus-trail.filter.view': 'View',
+    'topology.focus-trail.source.all': 'All sources',
+    'topology.group-panel.title': 'Grouping summary',
+    'topology.group-panel.copy': 'Group dense topology results without hiding the worst health and collapsed scope.',
+    'topology.group-panel.group-by': 'Group by type / environment / source',
+    'topology.group-panel.entity-type': 'Entity type',
+    'topology.group-panel.environment': 'Environment',
+    'topology.group-panel.source-kind': 'Evidence source',
+    'topology.group-panel.node-count': '{{count}} visible nodes',
+    'topology.group-panel.edge-count': '{{count}} visible edges',
+    'topology.group-panel.collapsed-count': '{{count}} collapsed',
+    'topology.group-panel.active-scope': 'active scope',
+    'topology.group-panel.clear-group': 'Clear group',
+    'topology.group-panel.open-table': 'Open table',
+    'topology.path-summary.title': 'Selected path',
+    'topology.path-summary.source': 'Source',
+    'topology.path-summary.target': 'Target',
+    'topology.path-summary.relation': 'Relation',
+    'topology.path-summary.direction': 'upstream to downstream',
+    'topology.path-summary.focus-path': 'Focus path',
+    'topology.path-summary.open-trace': 'Open trace',
     'topology.fault-context.title': 'Fault context',
     'topology.fault-context.copy': 'Entity, trace, and collection fields inherited from the current entry',
+    'topology.empty.api.title': 'No topology relationships found',
+    'topology.empty.api.copy': 'The API returned no entity, monitor, or trace-call relationship evidence for the current filter.',
+    'topology.empty.trace-call.title': 'No trace-call topology evidence',
+    'topology.empty.trace-call.copy': 'Greptime has no OTLP trace-call edges with RED metrics for the selected time and environment. Relation nodes are hidden so this view does not look complete without trace evidence.',
+    'topology.empty.trace-call.source': 'Greptime trace graph',
+    'topology.degraded.api.title': 'Topology API unavailable',
+    'topology.degraded.api.copy': 'The topology graph is degraded because API, Greptime, trace, or relation evidence could not be loaded for the selected scope.',
+    'topology.degraded.api.source': 'Topology API unavailable',
+    'topology.degraded.trace-call.title': 'Trace-call topology unavailable',
+    'topology.degraded.trace-call.copy': 'Greptime trace-call topology evidence could not be loaded for the selected time and environment. The graph stays empty instead of showing stale or unrelated relations.',
+    'topology.degraded.trace-call.source': 'Greptime trace graph',
+    'topology.loading.api.title': 'Loading topology evidence',
+    'topology.loading.api.copy': 'Fetching API, Greptime, trace, and relation evidence for the selected scope.',
     'topology.edge.evidence.aria': '{{label}} relationship evidence',
     'topology.aside.view': 'View',
+    'topology.companion.jump.aria': 'Topology companion sections',
+    'topology.companion.jump.view': 'View',
+    'topology.companion.jump.legend': 'Legend',
+    'topology.companion.jump.red': 'RED',
+    'topology.companion.jump.timeline': 'Timeline',
+    'topology.companion.jump.edge': 'Edge',
+    'topology.companion.jump.node': 'Node',
+    'topology.companion.section.collapse': 'Collapse',
+    'topology.companion.section.expand': 'Expand',
     'topology.alert-impact.open': 'Open alert impact',
+    'topology.alert-impact.copy': 'Keep the selected edge, entity, and signal scope.',
+    'topology.legend.title': 'Legend',
+    'topology.legend.summary': '2 groups',
+    'topology.legend.health': 'Health',
+    'topology.legend.health.healthy': 'Healthy',
+    'topology.legend.health.healthy-value': 'good',
+    'topology.legend.health.warning': 'Warning',
+    'topology.legend.health.warning-value': 'watch',
+    'topology.legend.health.critical': 'Critical',
+    'topology.legend.health.critical-value': 'act',
+    'topology.legend.status': 'Status',
+    'topology.legend.status.healthy': 'Healthy',
+    'topology.legend.status.healthy-value': 'healthy',
+    'topology.legend.status.warning': 'Warning',
+    'topology.legend.status.warning-value': 'attention',
+    'topology.legend.status.critical': 'Critical',
+    'topology.legend.status.critical-value': 'action',
+    'topology.legend.interaction': 'Interaction',
+    'topology.legend.interaction.selected-node': 'Selected',
+    'topology.legend.interaction.selected-node-value': 'selected',
+    'topology.legend.interaction.directional-edge': 'Edge',
+    'topology.legend.interaction.directional-edge-value': 'direction',
+    'topology.legend.interaction.dimmed-edge': 'Dimmed',
+    'topology.legend.interaction.dimmed-edge-value': 'context',
+    'topology.legend.source-kind': 'Evidence source',
+    'topology.legend.source.trace': 'Trace calls',
+    'topology.legend.source.trace-value': 'trace',
+    'topology.legend.source.monitor': 'Monitor bind',
+    'topology.legend.source.monitor-value': 'monitor',
+    'topology.legend.source.relation': 'Entity relation',
+    'topology.legend.source.relation-value': 'relation',
+    'topology.legend.confidence': 'Confidence',
+    'topology.legend.confidence.live': 'Current evidence',
+    'topology.legend.confidence.stale': 'Low confidence / stale',
+    'topology.timeline.title': 'Impact timeline',
+    'topology.timeline.copy': 'Recent entity, relationship, and monitor evidence that changed this topology view.',
+    'topology.timeline.detail.fallback': 'Topology evidence changed',
+    'topology.timeline.actor.system': 'system',
     'topology.edge.evidence.title': 'Relationship evidence',
     'topology.edge.link.from-entity': 'Source entity',
     'topology.edge.link.to-entity': 'Target entity',
@@ -124,6 +1029,32 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.edge.link.traces': 'Traces evidence',
     'topology.current-entity': 'Current entity',
     'topology.current-entity.signals': 'Signals: {{signals}}',
+    'topology.current-entity.boundary': 'Signal handoffs keep the current entity, time range, environment, and topology scope.',
+    'topology.current-entity.fact.entity-id': 'Entity id',
+    'topology.current-entity.fact.namespace': 'Namespace',
+    'topology.current-entity.fact.signals': 'Signals',
+    'topology.current-entity.fact.health': 'Health',
+    'topology.current-entity.fact.upstream': 'Upstream',
+    'topology.current-entity.fact.downstream': 'Downstream',
+    'topology.current-entity.fact.neighbor-count': '{{count}} dependencies',
+    'topology.current-entity.fact.neighbor.none': 'No dependencies in scope',
+    'topology.metric-table.title': 'Edge metric ranking',
+    'topology.metric-table.empty': 'No relationship metrics in this scope',
+    'topology.metric-table.edge-count': '{{count}} edges',
+    'topology.metric-table.request-rate-unit': 'req/s',
+    'topology.metric-table.error-rate-unit': 'errors',
+    'topology.metric-table.latency-p95-unit': 'p95',
+    'topology.metric-table.row-action': 'Inspect',
+    'topology.metric-table.open-edge-aria': 'Open topology edge {{edge}}',
+    'topology.metric-table.filter.all': 'All',
+    'topology.metric-table.filter.visible': 'Visible',
+    'topology.metric-table.filter.partial': 'Partial',
+    'topology.metric-table.filter.hidden': 'Hidden',
+    'topology.metric-table.filter.unknown': 'Unknown',
+    'topology.red.request-rate': 'Request rate',
+    'topology.red.error-rate': 'Error rate',
+    'topology.red.latency-p95': 'P95 latency',
+    'topology.red.source.trace-service-graph': 'Trace-derived service graph',
     'topology.context-link.entity': 'View entity detail',
     'topology.context-link.signals': 'Open three signals',
     'topology.context-link.metrics': 'Metrics workbench',
@@ -168,12 +1099,16 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.evidence.source.alert-impact': 'Current alert projected onto service/resource dependencies',
     'topology.evidence.collector.otlp': 'OTLP Collector',
     'topology.evidence.collector.hertzbeat': 'HertzBeat Collector',
-    'topology.evidence.boundary': 'This edge only uses collected relationship evidence; dependency auto-discovery, change timelines, and root cause analysis remain roadmap capabilities.',
+    'topology.evidence.boundary': 'This edge only uses collected relationship evidence; dependency auto-discovery and root cause analysis remain roadmap capabilities.',
     'topology.evidence.alert-impact.copy': 'Open alert impact with the current edge, entity, and three-signal context.',
     'topology.evidence.row.from': 'Source entity',
     'topology.evidence.row.to': 'Target entity',
     'topology.evidence.row.source': 'Collection evidence',
     'topology.evidence.row.alert-impact': 'Alert impact',
+    'topology.hover.fact.relation-type': 'Relation',
+    'topology.hover.fact.first-seen': 'First seen',
+    'topology.hover.fact.last-seen': 'Last seen',
+    'topology.hover.fact.sample-trace': 'Sample trace',
     'topology.evidence.alert-impact.none': 'No current impact',
     'topology.evidence.alert-impact.warning': 'Impact present',
     'topology.evidence.alert-impact.critical': 'Critical impact',
@@ -182,6 +1117,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'common.stable': 'stable',
     'common.total': 'Total',
     'common.updated': 'Updated',
+    'common.workspace': 'Workspace',
+    'common.workspace-navigation': 'Workspace navigation',
     'label.bind': 'Labels',
     'menu.dashboard': 'Overview',
     'menu.alert.dispatch': 'Notification',
@@ -192,13 +1129,38 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'menu.section.dashboards': 'Dashboards',
     'menu.section.settings': 'Platform settings',
     'menu.overview': 'Dashboard',
+    'monitors.loading': 'Loading monitor center',
     'monitor.list': 'Monitors',
     'monitor.scrape': 'Collection mode',
+    'monitor.collector.system.default': 'Default System Dispatch',
     'monitor.status': 'Task Status',
     'monitor.status.up': 'Up',
     'monitor.status.down': 'Down',
     'monitor.status.paused': 'Paused',
+    'monitors.status.up': 'Up',
+    'monitors.status.down': 'Down',
+    'monitors.status.paused': 'Paused',
+    'monitors.type.all': 'All types',
     'monitor.edit-monitor': 'Edit monitor',
+    'monitor.scrape.type.static': 'Static Scrape',
+    'monitor.scrape.type.http_sd': 'Http Service Discovery',
+    'monitor.scrape.type.nacos_sd': 'Nacos Service Discovery',
+    'monitor.scrape.type.dns_sd': 'Dns Service Discovery',
+    'monitor.scrape.type.eureka_sd': 'Eureka Service Discovery',
+    'monitor.scrape.type.consul_sd': 'Consul Service Discovery',
+    'monitor.scrape.type.zookeeper_sd': 'Zookeeper Service Discovery',
+    'entity.monitor.workbench.reason.down': 'This monitor is unhealthy. Check it first.',
+    'entity.monitor.workbench.reason.fallback': 'There are no unhealthy monitors right now, so this view shows all linked monitors.',
+    'entity.monitor.workbench.reason.paused': 'This monitor is paused. Check whether it should be resumed.',
+    'entity.monitor.workbench.reason.default': 'This monitor helps validate coverage and recent state.',
+    'entity.response.open-related-logs': 'Open related logs',
+    'entity.response.open-related-traces': 'Open related traces',
+    'entity.response.open-code': 'Open code',
+    'monitors.controls.more-actions': 'More monitor operations',
+    'monitor.copy.action': 'Copy monitor',
+    'monitor.copy.success': 'Monitor copied',
+    'monitor.copy.failed': 'Copy monitor failed',
+    'monitor.item.unavailable.refresh': 'This monitor item is no longer available, the list will refresh automatically',
     'monitor.editor.labels.add': 'Add label',
     'monitor.editor.labels.copy': 'Manage monitor labels as key/value rows.',
     'monitor.editor.labels.key': 'Label key',
@@ -213,12 +1175,20 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.editor.validation.app': 'Monitor type is required',
     'monitor.editor.validation.intervals': 'Collection interval is required',
     'monitor.editor.validation.param-required': '{{field}} is required',
+    'monitor.route-state.form.error.title': 'Failed to load the monitor form',
+    'monitor.route-state.form.error.copy': 'The monitor form could not be initialized right now. Please try again.',
+    'monitor.route-state.action.retry': 'Retry',
+    'monitor.route-state.loading.title': 'Loading monitor details',
+    'monitor.route-state.loading.copy': 'Please wait while the page prepares the monitor instance, metric summary, and history trends.',
+    'monitor.route-state.error.title': 'Failed to load monitor details',
+    'monitor.route-state.error.copy': 'The monitor details could not be loaded right now. Try again later or reopen the monitor from the list.',
     'monitor.editor.loading.edit': 'Loading monitor settings...',
     'monitor.editor.loading.new': 'Loading monitor form...',
     'monitor.editor.section.base': 'Basic settings',
     'monitor.editor.section.base.copy': 'Choose the monitor type, target, and collection schedule.',
     'monitor.editor.section.params': 'Runtime parameters',
     'monitor.editor.section.params.copy': 'Configure the parameters required by the selected monitor template.',
+    'monitor.editor.section.runtime': 'Collection schedule',
     'monitor.editor.section.advanced.copy': 'Configure labels, annotations, and linked dashboards for filtering and follow-up.',
     'monitor.editor.section.payload': 'Collection payload',
     'monitor.editor.section.payload.copy': 'Configure the request body or script payload used by this monitor.',
@@ -229,6 +1199,9 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.overview.kicker': 'Overview',
     'monitor.detail.title-fallback': 'Monitor detail',
     'monitor.detail.open-grafana': 'Open Grafana',
+    'monitor.detail.auto-refresh': 'Auto Refresh After {{time}} s',
+    'monitor.detail.close-refresh': 'Close Auto Refresh',
+    'monitor.detail.config-refresh': 'Set Auto Refresh For {{time}} s',
     'monitor.detail.refresh-countdown': 'Refresh soon',
     'monitor.detail.refresh-disabled': 'Auto refresh off',
     'monitor.detail.status.up': 'UP',
@@ -273,6 +1246,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.grafana.enabled.meta': 'external dashboard',
     'monitor.detail.grafana.copy': 'Open the linked Grafana dashboard when one is configured.',
     'monitor.detail.grafana.status': 'Dashboard status',
+    'monitor.detail.grafana.configure': 'Configure dashboard',
     'monitor.detail.param.meta': 'runtime param',
     'monitor.detail.param.unknown': 'Parameter',
     'monitor.detail.schedule.empty': 'Not scheduled',
@@ -329,6 +1303,10 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.favorite.catalog.title': 'Favorite catalog',
     'monitor.detail.favorite.remove': 'Remove favorite',
     'monitor.detail.favorite.add': 'Add favorite',
+    'monitor.favorite.add.success': 'Added to favorites successfully',
+    'monitor.favorite.add.failed': 'Failed to add to favorites',
+    'monitor.favorite.remove.success': 'Removed from favorites successfully',
+    'monitor.favorite.remove.failed': 'Failed to remove from favorites',
     'monitor.detail.metric.row-label': 'Row',
     'monitor.detail.metric.row': 'Row {{index}}',
     'monitor.detail.metric.fullscreen.enter': 'Fullscreen',
@@ -399,13 +1377,64 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitors.empty-selected.title': 'No monitor selected',
     'monitors.empty-selected.copy': 'Select a monitor from the list to inspect actions and context.',
     'ops.surface.mode.entry': 'entry surface',
+    'ops.surface.kicker': 'Operator surface',
+    'ops.surface.route-contract.title': 'Route contract',
+    'ops.surface.route-contract.copy': 'Shared shell status stays visible during the landing phase.',
+    'ops.surface.launch-checklist': 'Launch checklist',
+    'ops.surface.route-posture': 'Route posture',
+    'ops.surface.entry-shell': 'Entry shell ready',
+    'ops.surface.fact.focus-label': 'Focus',
+    'ops.surface.fact.mode-label': 'Mode',
+    'ops.surface.fact.mode-entry': 'entry surface',
+    'ops.surface.fact.signals-label': 'Signals',
+    'ops.surface.route-posture.mode-label': 'Mode',
     'ops.surface.shared-shell.title': 'Shared shell',
     'ops.surface.shared-shell.copy': 'The entry surface keeps the same shell and information hierarchy.',
     'ops.surface.focused-scope.title': 'Focused scope',
     'ops.surface.focused-scope.copy': 'Only carry navigation, context, and the next hop.',
+    'overview.loading': 'Loading overview console',
+    'overview.guidance.default.start': 'Next',
+    'overview.guidance.default.reasons': 'Reasons',
+    'overview.guidance.default.next': 'After that',
+    'overview.checklist.status.ready': 'Ready',
+    'overview.checklist.status.pending': 'Pending',
+    'overview.lane.entities.title': 'Entity catalog',
+    'overview.lane.entities.eyebrow': 'Entity-first',
+    'overview.lane.entities.copy': 'Start from monitored entities and service ownership before jumping into signals.',
+    'overview.lane.entities.stat': '{{count}} entities tracked',
+    'overview.lane.entities.action': 'Open entities',
+    'overview.lane.logs.title': 'Logs',
+    'overview.lane.logs.eyebrow': 'Runtime evidence',
+    'overview.lane.logs.copy': 'Inspect log entries with the same service, entity, and time context.',
+    'overview.lane.logs.stat.available': 'Logs available',
+    'overview.lane.logs.action': 'Open logs',
+    'overview.lane.trace.title': 'Traces',
+    'overview.lane.trace.eyebrow': 'Request path',
+    'overview.lane.trace.copy': 'Follow spans when trace context is present, then return to entity evidence.',
+    'overview.lane.trace.stat.distributed': 'Distributed traces ready',
     'overview.lane.trace.stat.detected': 'Alert contains trace ID',
+    'overview.lane.trace.action': 'Open traces',
+    'overview.lane.otlp.title': 'OTLP metrics',
+    'overview.lane.otlp.eyebrow': 'Three-signal ingest',
+    'overview.lane.otlp.copy': 'Inspect incoming metric series without leaving the private HertzBeat workspace.',
+    'overview.lane.otlp.stat': '{{count}} monitored apps',
+    'overview.lane.otlp.action': 'Open metrics',
     'passport.lock.error.required': 'Please enter password',
-    'passport.lock.next-step': 'wire unlock validation if needed',
+    'passport.lock.status.interactive': 'Interactive',
+    'passport.lock.next-step': 'Wire unlock validation if needed',
+    'app.lock': 'Unlock',
+    'app.lock.placeholder': 'Enter Any To Unlock',
+    'about.title': 'HertzBeat is an open source observability platform.',
+    'about.help': 'Thanks for using HertzBeat. The following links help you follow releases, report issues, and upgrade safely.',
+    'about.copyright': 'Copyright © {{year}} | Apache HertzBeat™',
+    'about.not-show-next-login': 'Do not show next login',
+    'about.github': 'GitHub',
+    'about.issue': 'Issue',
+    'about.pr': 'PR',
+    'about.discuss': 'Discuss',
+    'about.doc': 'Doc',
+    'about.upgrade': 'Upgrade',
+    'about.star': 'Star',
     'about.point.1': 'Collect metrics from applications, databases, operating systems, middleware, and network devices without sending data outside your deployment.',
     'about.point.2': 'Receive OTLP logs with resource, entity, trace, and alert context so operators can continue troubleshooting from the log line.',
     'about.point.3': 'Handle threshold and external alerts in one place with acknowledge, silence, inhibit, recover, and close actions.',
@@ -415,10 +1444,14 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'app.login.login': 'Login',
     'app.login.message-need-credential': 'Please enter password',
     'app.login.message-need-identifier': 'Please enter your username',
+    'app.login.password-hide': 'Hide password',
+    'app.login.password-show': 'Show password',
+    'app.login.remember-me': 'Remember me',
     'app.login.tab-login-credentials': 'Sign In HertzBeat',
     'app.passport.desc': 'Open-source private-deployable enterprise operations observability platform',
     'app.passport.intro-1': 'Collectors, monitoring templates, entities, metrics, logs, and traces',
     'app.passport.intro-2': 'Handle alerts and close issues inside HertzBeat',
+    'app.passport.language-switch': 'Switch language',
     'passport.login.card.entry.title': 'Operations entry',
     'passport.login.card.entry.copy': 'Sign in to continue monitoring resources, entities, telemetry, and alerts.',
     'passport.login.card.shell.title': 'Private deployment',
@@ -426,16 +1459,178 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'passport.login.card.auth.title': 'Secure access',
     'passport.login.card.auth.copy': 'Verify your account before opening alert handling, notification, and settings tools.',
     'passport.login.default-password-warning': 'The default password is still in use. Change it after login.',
+    'passport.login.loading': 'Logging in',
+    'passport.login.error.generic': 'Login failed. Check username or password.',
+    'passport.login.error.with-status': 'Login failed: {{status}}',
     'common.workbench.loading.title': 'Loading workspace',
+    'common.workbench.loading.copy': 'Preparing page data. Please wait.',
+    'settings.system-config': 'System config',
+    'settings.system-config.locale': 'System language',
+    'settings.system-config.timezone': 'System timezone',
+    'settings.system-config.theme': 'System theme',
     'settings.system-config.locale.en_US': 'English(en_US)',
+    'settings.system-config.locale.zh_CN': 'Simplified Chinese(zh_CN)',
+    'settings.system-config.locale.zh_TW': 'Traditional Chinese(zh_TW)',
+    'settings.system-config.locale.ja-JP': 'Japanese(ja_JP)',
+    'settings.system-config.locale.pt_BR': 'Portuguese(pt_BR)',
+    'settings.system-config.theme.default': 'Default theme',
+    'settings.system-config.theme.dark': 'Dark theme',
+    'settings.system-config.theme.compact': 'Compact theme',
+    'settings.console.kicker': 'Settings',
+    'settings.console.title': 'System settings',
+    'settings.console.copy': 'Configure platform system, message, object storage, and token settings from one console.',
+    'settings.console.navigation': 'Settings sections',
+    'settings.console.content': 'Settings content',
+    'settings.surface.title': 'Settings overview',
+    'settings.surface.copy': 'Keep platform configuration entry points in one workbench.',
     'settings.surface.route-contract.title': 'Settings navigation',
     'settings.surface.route-contract.copy':
       'Reach status pages, collectors, labels, plugins, tokens, and system options from one operations console.',
+    'settings.surface.route-contract.meta': 'Route contract',
     'settings.surface.api-contract.title': 'Operations configuration',
     'settings.surface.api-contract.copy':
       'Manage collectors, labels, plugins, tokens, status pages, and notification channels without leaving settings.',
+    'settings.surface.api-contract.meta': 'API contract',
+    'settings.surface.fact.mode-label': 'Mode',
+    'settings.surface.fact.mode-value': 'Settings',
+    'settings.surface.fact.focus-label': 'Focus',
     'settings.surface.governance.title': 'Platform governance',
     'settings.surface.governance.copy': 'Show only current settings entries; future domains stay in roadmap docs.',
+    'settings.surface.governance.group.users-permissions': 'Users and permissions',
+    'settings.surface.governance.group.api-access': 'API access',
+    'settings.surface.governance.group.notifications': 'Notification channels',
+    'settings.surface.governance.group.template-marketplace': 'Templates and plugins',
+    'settings.surface.governance.group.mcp-ai-foundation': 'MCP and AI foundation',
+    'settings.surface.governance.copy.implemented-route': 'Already attached to current platform entries; keep using existing pages without adding empty navigation.',
+    'settings.surface.governance.copy.foundation-only': 'Keep the current foundation entry; full governance remains in the roadmap.',
+    'settings.surface.governance.future.title': 'Future domain boundary',
+    'settings.surface.governance.future.copy': '{{domains}} remain roadmap-only planning and are not shown as live app entries. Docs: {{docs}}',
+    'settings.surface.governance.future.meta': 'roadmap only',
+    'settings.surface.governance.future.separator': ', ',
+    'settings.surface.governance.future.security': 'Security',
+    'settings.surface.governance.future.data-observability': 'Data Observability',
+    'settings.surface.governance.future.digital-experience': 'Digital Experience',
+    'settings.surface.governance.future.software-delivery': 'Software Delivery',
+    'settings.surface.governance.future.cloud-cost': 'Cloud Cost',
+    'settings.surface.governance.future.ai-observability': 'AI Observability',
+    'settings.surface.governance.future.developer-integrations': 'Developer Integrations',
+    'setting.labels.loading': 'Loading label management',
+    'setting.labels.copy': 'Manage monitor label names, values, and descriptions in the same compact card grid used by the legacy label workspace.',
+    'setting.labels.table.name': 'Label name',
+    'setting.labels.table.type': 'Label type',
+    'setting.labels.table.description': 'Description',
+    'setting.labels.type.auto': 'Auto label',
+    'setting.labels.type.user': 'User label',
+    'setting.labels.type.preset': 'Preset label',
+    'setting.labels.type.unknown': 'Unknown label type',
+    'setting.labels.metric.auto-page': 'Auto labels in page',
+    'setting.labels.metric.user-page': 'User labels in page',
+    'setting.labels.metric.preset-page': 'Preset labels in page',
+    'setting.labels.empty.title': 'No labels',
+    'setting.labels.empty.copy': 'Create a label first.',
+    'setting.labels.action.copy-aria': 'Copy label {{name}}',
+    'setting.labels.action.edit-aria': 'Edit label {{name}}',
+    'setting.labels.action.delete-aria': 'Delete label {{name}}',
+    'setting.plugins.loading': 'Loading plugin management',
+    'setting.plugins.copy': 'Manage custom plugins, execution types, and enabled state while keeping upload, filtering, bulk delete, and parameter editing in one compact workspace.',
+    'setting.plugins.selected-count': 'Selected {{count}}',
+    'setting.plugins.table.params': 'Params',
+    'setting.plugins.metric.enabled-page': 'Enabled plugins in page',
+    'setting.plugins.metric.disabled-page': 'Disabled plugins in page',
+    'setting.plugins.metric.params-page': 'Plugin params in page',
+    'setting.plugins.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'setting.plugins.pagination.page-size': 'Page size',
+    'setting.plugins.pagination.page': 'Page',
+    'setting.plugins.row.item-fallback': 'Plugin item -',
+    'setting.plugins.row.params': 'Params {{count}}',
+    'setting.plugins.action.params-aria': 'Edit plugin params {{name}}',
+    'setting.plugins.action.delete-aria': 'Delete plugin {{name}}',
+    'setting.collector.loading': 'Loading collector management',
+    'setting.collector.copy': 'Manage registered collector nodes, runtime state, and task distribution while keeping deployment, search, online/offline, and delete actions in one dense operations workspace.',
+    'setting.collector.selected-count': 'Selected {{count}}',
+    'collector.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'collector.pagination.page-size': 'Page size',
+    'collector.pagination.page': 'Page',
+    'collector.confirm.offline': 'Please confirm whether to offline this collector!',
+    'collector.confirm.offline-batch': 'Please confirm whether to offline the collector in batches!',
+    'collector.confirm.online': 'Please confirm whether to online this collector!',
+    'collector.confirm.online-batch': 'Please confirm whether to online collector in batches!',
+    'collector.notify.no-select-offline': 'No items selected for offline collector!',
+    'collector.notify.no-select-online': 'No items selected for online collector!',
+    'collector.deploy': 'Deploy Collector',
+    'collector.deploy.close': 'Close, collector identity token saved',
+    'collector.deploy.docker': 'Deploy via Docker',
+    'collector.deploy.docker.help': '# Run this command in a Docker environment',
+    'collector.deploy.docker.help.1': '# docker run -d : Start a collector container in the background with Docker',
+    'collector.deploy.docker.help.2': "# -e IDENTITY=xxx : Set the collector's identity token",
+    'collector.deploy.docker.help.3': '# -e MANAGER_HOST=127.0.0.1 : Set the HertzBeat server address the collector connects to',
+    'collector.deploy.docker.help.4': '# -e MODE=public : Set the run mode: public collector cluster or private edge collector.',
+    'collector.deploy.docker.help.5': '# --name hertzbeat-collector : Name the container hertzbeat-collector',
+    'collector.deploy.docker.help.6': '# apache/hertzbeat-collector : Official collector image. Use quay.io/tancloud/hertzbeat-collector if the pull times out',
+    'collector.deploy.identity': 'Collector identity token (IDENTITY)',
+    'collector.deploy.identity.tip': 'This identity token is unique to the current collector. Keep it safe.',
+    'collector.deploy.ok': 'Generate identity token and deployment command',
+    'collector.deploy.package': 'Deploy via installation package',
+    'collector.deploy.package.github': 'Download package from GitHub',
+    'collector.deploy.package.help': '# Download the installation package for this operating system: hertzbeat-collector-xx.tar.gz',
+    'collector.deploy.package.help.1': '# Extract it and update hertzbeat-collector/config/application.yml',
+    'collector.deploy.package.help.2': '# Replace IDENTITY, MANAGER_HOST, and MODE in the file as shown below.',
+    'collector.deploy.package.help.3': '# Run bin/startup.sh or startup.bat (Windows) to start the collector.',
+    'collector.name.placeholder': 'Please config the unique name of the collector',
+    'common.button.copy.tip': 'Click to copy',
+    'common.copy.button': 'Copy',
+    'setting.define.loading': 'Loading definition center',
+    'setting.define.title': 'Define',
+    'setting.define.subtitle':
+      'Manage monitor template YAML for collection protocols, parameters, and metric fields in one dense operations workspace.',
+    'setting.define.action.new': 'New type',
+    'setting.define.action.save-apply': 'Save and apply',
+    'setting.define.action.delete': 'Delete {{app}}',
+    'define.save-apply.confirm': 'Please confirm whether to save and apply this monitor type definition. This will affect your monitors.',
+    'define.delete.confirm': 'Please confirm whether to delete {{app}} monitor type. This monitor type cannot be added after deletion.',
+    'define.hide-true.confirm': 'Confirm whether to display this menu?',
+    'define.hide-false.confirm': 'Confirm whether to hide this menu?',
+    'define.save-apply.no-code': 'The monitoring type definition content cannot be empty.',
+    'setting.define.action.hide': 'Hide',
+    'setting.define.action.hide-aria': 'Hide template {{app}}',
+    'setting.define.action.show': 'Show',
+    'setting.define.action.show-aria': 'Show template {{app}}',
+    'setting.define.save.pending': 'Applying definition...',
+    'setting.define.skeleton.name': 'hertzbeat-baseline-alert',
+    'setting.define.skeleton.summary': 'HertzBeat baseline alert definition seed',
+    'setting.define.current-yaml.fallback': 'current-definition',
+    'setting.define.current-delete.fallback': 'current definition',
+    'setting.define.item.fallback': 'Untitled definition',
+    'setting.define.empty.title': 'No templates',
+    'setting.define.empty.copy': 'Create a monitor template YAML first.',
+    'setting.define.empty.search-title': 'No matching templates',
+    'setting.define.empty.search-copy': 'Adjust the search keyword and try again.',
+    'setting.define.empty-selected.title': 'No definition selected',
+    'setting.define.empty-selected.copy': 'Select a definition from the list.',
+    'setting.define.preview.action': 'Preview query',
+    'setting.define.preview.title': 'Definition preview',
+    'setting.define.preview.success': 'Preview completed',
+    'setting.define.preview.failed': 'Preview failed',
+    'setting.define.save.success': 'Definition saved',
+    'setting.define.save.failed': 'Failed to save definition',
+    'setting.define.datasource.title': 'Datasource status',
+    'setting.define.fact.datasource': 'Datasource',
+    'setting.define.fact.selected-template': 'Current template',
+    'setting.define.fact.hidden': 'Hidden templates',
+    'setting.define.new-template': 'New template draft',
+    'setting.define.template.visible': 'Visible',
+    'setting.define.template.hidden': 'Hidden',
+    'setting.define.summary.yaml-lines': '{{count}} YAML lines',
+    'setting.define.summary.mode.new': 'New',
+    'setting.define.summary.mode.edit': 'Edit',
+    'setting.define.diff.original': 'Original YAML',
+    'setting.define.diff.current': 'Current edit YAML',
+    'setting.define.row.meta': 'Period {{period}}s · updated {{time}}',
+    'alert.setting.type.realtime.metric': 'Realtime metric',
+    'alert.setting.type.periodic.metric': 'Periodic metric',
+    'alert.setting.type.realtime.log': 'Realtime log',
+    'alert.setting.type.periodic.log': 'Periodic log',
+    'alert.setting.type.periodic.trace': 'Periodic trace',
     'setting.status.loading': 'Loading status settings',
     'setting.status.kicker': 'Status settings',
     'setting.status.title': 'Status page settings',
@@ -454,6 +1649,13 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'setting.status.org.logo': 'Logo URL',
     'setting.status.org.color': 'Brand color',
     'setting.status.org.state': 'Org state',
+    'setting.status.delete.component.title': 'Delete component',
+    'setting.status.delete.component.copy':
+      'After deletion, this component is removed from the public status page and related incidents need component binding review.',
+    'setting.status.delete.incident.title': 'Delete incident',
+    'setting.status.delete.incident.copy':
+      'After deletion, this incident is removed from the public status page and historical updates are no longer shown to visitors.',
+    'setting.status.delete.confirm': 'Confirm delete',
     'setting.status.components.title': 'Components',
     'setting.status.components.tab': 'Components',
     'setting.status.components.empty.title': 'No components yet',
@@ -470,14 +1672,126 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'setting.status.validation.component-description': 'Component description is required',
     'setting.status.incidents.state': 'state',
     'setting.status.incidents.components': 'components',
+    'setting.status.component.method.placeholder': '0 / 1',
+    'setting.status.component.state.placeholder': '0 / 1 / 2',
+    'setting.status.component.labels.placeholder': 'env:prod, app:api',
+    'setting.status.component.description.placeholder': 'Public status component description',
+    'setting.status.incident.state.placeholder': '0 / 1 / 2 / 3',
+    'setting.status.incident.components.placeholder': '1, 2',
+    'monitors.delete.title': 'Delete monitors',
+    'monitors.delete.copy':
+      'After deletion, the selected monitors are removed from the monitor catalog, and related entity bindings plus alert investigation entries need to be reviewed.',
+    'monitors.delete.confirm': 'Confirm delete',
+    'monitors.delete.selected': '{{count}} monitors selected',
+    'monitors.selected-count': '{{count}} monitors selected',
     'setting.settings.menu.copy': 'System, storage, notification, and token settings',
+    'setting.settings.menu.open-action': 'Open {{title}}',
     'setting.settings.config.title': 'System config',
+    'setting.settings.config.loading': 'Loading system config.',
     'setting.settings.server.title': 'Message server',
+    'setting.settings.server.loading': 'Loading message server settings.',
+    'settings.server': 'Message server settings',
+    'settings.server.email': 'Email server',
+    'settings.server.sms': 'SMS settings',
+    'settings.server.email.setting': 'Configure email server',
+    'settings.server.sms.setting': 'Configure SMS parameters',
+    'settings.server.summary.configure-action': 'Configure {{title}}',
+    'alert.notice.sender.mail.host': 'Email server address',
+    'alert.notice.sender.mail.port': 'Email port',
+    'alert.notice.sender.mail.username': 'Email account',
+    'alert.notice.sender.mail.password': 'Email password',
+    'alert.notice.sender.mail.ssl': 'Enable SSL',
+    'alert.notice.sender.mail.starttls': 'Enable STARTTLS',
+    'alert.notice.sender.mail.enable': 'Enable email configuration',
+    'alert.notice.sender.sms.type': 'SMS type',
+    'alert.notice.sender.sms.type.tencent': 'Tencent SMS',
+    'alert.notice.sender.sms.type.alibaba': 'Alibaba SMS',
+    'alert.notice.sender.sms.type.unisms': 'UniSMS',
+    'alert.notice.sender.sms.type.smslocal': 'Smslocal',
+    'alert.notice.sender.sms.type.aws': 'AWS SMS',
+    'alert.notice.sender.sms.type.twilio': 'Twilio Sms',
+    'alert.notice.sender.sms.tencent.secretId': 'Tencent SMS SecretId',
+    'alert.notice.sender.sms.tencent.secretKey': 'Tencent SMS SecretKey',
+    'alert.notice.sender.sms.tencent.signName': 'Tencent SMS SignName',
+    'alert.notice.sender.sms.tencent.appId': 'Tencent SMS AppId',
+    'alert.notice.sender.sms.tencent.templateId': 'Tencent SMS TemplateId',
+    'alert.notice.sender.sms.alibaba.accessKeyId': 'Alibaba SMS AccessKeyId',
+    'alert.notice.sender.sms.alibaba.accessKeySecret': 'Alibaba SMS AccessKeySecret',
+    'alert.notice.sender.sms.alibaba.signName': 'Alibaba SMS SignName',
+    'alert.notice.sender.sms.alibaba.templateCode': 'Alibaba SMS TemplateCode',
+    'alert.notice.sender.sms.unisms.accessKeyId': 'UniSMS AccessKeyId',
+    'alert.notice.sender.sms.unisms.authMode': 'UniSMS auth mode',
+    'alert.notice.sender.sms.unisms.accessKeySecret': 'UniSMS AccessKeySecret',
+    'alert.notice.sender.sms.unisms.signature': 'UniSMS signature',
+    'alert.notice.sender.sms.unisms.templateId': 'UniSMS TemplateId',
+    'alert.notice.sender.sms.unisms.authMode.hmac': 'HMAC',
+    'alert.notice.sender.sms.unisms.authMode.simple': 'Simple',
+    'alert.notice.sender.sms.smslocal.apiKey': 'Smslocal API key',
+    'alert.notice.sender.sms.aws.accessKeyId': 'AWS access key ID',
+    'alert.notice.sender.sms.aws.accessKeySecret': 'AWS access key secret',
+    'alert.notice.sender.sms.aws.region': 'AWS region',
+    'alert.notice.sender.sms.twilio.accountSid': 'Twilio account SID',
+    'alert.notice.sender.sms.twilio.authToken': 'Twilio auth token',
+    'alert.notice.sender.sms.twilio.twilioPhoneNumber': 'Twilio phone number',
     'setting.settings.object-store.title': 'Object storage',
+    'setting.settings.object-store.loading': 'Loading object storage settings.',
+    'settings.object-store': 'Object storage',
+    'settings.object-store.type': 'Storage provider',
+    'settings.object-store.type.database': 'Local database (default)',
+    'settings.object-store.type.file': 'Local file',
+    'settings.object-store.type.obs': 'Huawei Cloud OBS',
+    'settings.object-store.obs.accessKey': 'Access key',
+    'settings.object-store.obs.secretKey': 'Secret key',
+    'settings.object-store.obs.bucketName': 'Bucket',
+    'settings.object-store.obs.endpoint': 'Endpoint',
+    'settings.object-store.obs.savePath': 'Save path',
+    'settings.object-store.obs.accessKey.placeholder': 'Access key ID',
+    'settings.object-store.obs.secretKey.placeholder': 'Access key secret',
+    'settings.object-store.obs.bucketName.placeholder': 'Bucket name',
+    'settings.object-store.obs.endpoint.placeholder': 'OBS endpoint without bucket name',
+    'settings.object-store.obs.savePath.placeholder': 'Backup save path, defaults to hertzbeat',
+    'settings.system-config.ok': 'Confirm update',
+    'setting.object-store.fact.type': 'Type',
+    'setting.object-store.fact.bucket': 'Bucket',
+    'setting.object-store.fact.endpoint': 'Endpoint',
     'setting.token.fact.total': 'Total',
     'setting.token.fact.active': 'Active',
     'setting.token.fact.expired': 'Expired',
+    'setting.token.metric.total': 'total tokens',
+    'setting.token.metric.active': 'active tokens',
+    'setting.token.metric.expired': 'expired tokens',
+    'setting.token.row.creator': 'creator {{creator}}',
+    'setting.token.row.state.active': 'active',
+    'setting.token.row.state.expired': 'expired',
     'setting.token.title': 'Token management',
+    'setting.token.loading': 'Loading token management.',
+    'settings.token': 'Token management',
+    'settings.token.console.kicker': 'API keys',
+    'settings.token.console.title': 'Manage telemetry and automation tokens',
+    'settings.token.console.copy': 'Review token state, recent use, and revoke actions from one page.',
+    'settings.token.console.result.total': 'Total tokens',
+    'settings.token.console.result.active': 'Active tokens',
+    'settings.token.console.result.expired': 'Expired tokens',
+    'settings.token.generate': 'Generate token',
+    'settings.token.generate-title': 'Generate token',
+    'settings.token.generated-title': 'Token generated',
+    'settings.token.generate-fail': 'Generate token failed',
+    'settings.token.name-placeholder': 'Enter a token name',
+    'settings.token.notice': 'Copy this token now. It will not be shown again after you close this dialog.',
+    'settings.token.delete-confirm': 'Confirm delete token',
+    'settings.token.delete-confirm-content': 'All integrations using this token will lose access. Are you sure you want to delete "{{name}}"?',
+    'settings.token.load-fail-title': 'Token list is temporarily unavailable',
+    'settings.token.load-fail': 'The token list cannot be loaded right now. Please try again later or refresh the page.',
+    'settings.token.desc': 'Manage long-lived tokens for API integrations; create or revoke them at any time.',
+    'settings.token.name': 'Token name',
+    'settings.token.value': 'Token value',
+    'settings.token.creator': 'Creator',
+    'settings.token.create-time': 'Created at',
+    'settings.token.expire-time': 'Expires at',
+    'settings.token.expire.never': 'Never expires',
+    'settings.token.last-used': 'Last used',
+    'settings.token.empty': 'No tokens yet',
+    'settings.token.delete-action': 'Delete {{name}}',
     'setting.token.empty.title': 'No tokens yet',
     'setting.token.empty.copy': 'Generate a token first.',
     'setting.token.expiration.never': 'Never expires',
@@ -487,6 +1801,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'setting.token.expiration.180d': '180 days',
     'setting.token.expiration.365d': '365 days',
     'status.loading': 'Loading status page',
+    'status.page': 'Status Page',
     'status.kicker': 'Status',
     'status.title': 'Status page',
     'status.subtitle': 'Public component health and incident history',
@@ -508,6 +1823,21 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'status.mode.incidents': 'incidents',
     'status.tone.down': 'down',
     'status.component.default-title': 'Component',
+    'status.component.state.0': 'Normal',
+    'status.component.state.1': 'Abnormal',
+    'status.component.state.2': 'Unknown',
+    'status.incident.history': 'Incident History',
+    'status.incident.state.0': 'Investigating',
+    'status.incident.state.1': 'Identified',
+    'status.incident.state.2': 'Monitoring',
+    'status.incident.state.3': 'Resolved',
+    'status.incident.message.tip.0': 'We are investigating this incident, please check back later for updates.',
+    'status.incident.message.tip.1': 'We have confirmed the cause of the failure and are processing it.',
+    'status.incident.message.tip.2': 'We are monitoring this event and observing the processing effect',
+    'status.incident.message.tip.3': 'This incident has been resolved, thanks for understanding and support.',
+    'status.public.org.state.0': 'All Systems Operational',
+    'status.public.org.state.1': 'Some Systems Abnormal',
+    'status.public.org.state.2': 'All Systems Abnormal',
     'status.components.empty.title': 'No components yet',
     'status.components.empty.copy': 'Public components will appear here once they are configured.',
     'status.components.broadcast': 'Component broadcast',
@@ -522,10 +1852,13 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'status.org.meta': 'public org',
     'status.public.history': 'History',
     'status.public.home': 'Home',
+    'status.public.feedback': 'Feedback',
     'status.public.updated': 'Updated',
     'status.public.year': 'Year',
+    'status.public.uptime': 'Uptime',
     'status.public.component-history': 'Component history',
     'status.public.incident-range': 'Incident range',
+    'status.public.incidents.load-failed': 'Failed to load public incidents',
     'status.public.today': 'Today',
     'status.public.30-day': '30 days',
     'status.public.power-by': 'Powered by HertzBeat',
@@ -534,28 +1867,346 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'status.incident.public.start-at': 'Start At',
     'status.incident.public.update-at': 'Update At',
     'status.components.view': 'Component health view',
+    'observability.waterfall.span-label': 'Span',
+    'observability.waterfall.duration-label': 'Duration',
+    'observability.waterfall.timeline-label': 'Timeline',
+    'observability.waterfall.overview-title': '{{timeline}} overview',
+    'observability.waterfall.span-count': '{{count}} spans',
+    'observability.waterfall.event-count': '{{count}} events',
+    'trace.manage.loading': 'Loading trace workbench',
     'trace.manage.stat.error-ratio': 'Error ratio',
     'trace.manage.stat.activity': 'Activity',
     'trace.manage.stat.list-count': 'List count',
+    'trace.center.console.title': 'Traces Workbench',
     'trace.manage.workspace': 'Traces Workbench',
     'trace.manage.payloads.title': 'Raw payloads',
+    'trace.manage.empty-selected-span.title': 'Select a span',
+    'trace.manage.empty-selected-span.copy': 'Choose a span on the left to inspect service, status, and event evidence.',
     'trace.manage.selected-span.service-namespace': 'Service and namespace',
     'trace.manage.trace-state-empty': 'Trace state unavailable',
+    'trace.manage.event.fallback': 'Unnamed event',
+    'trace.manage.event.attributes.empty': 'No attributes',
+    'trace.manage.event.attributes.more': '{{count}} more',
+    'trace.manage.event.attributes.object': 'Object attributes',
+    'trace.manage.link.fallback': 'Related trace',
+    'trace.manage.link.meta': 'Related trace',
     'trace.manage.handoff.logs-disabled': 'A traceId is required before opening related logs.',
     'trace.manage.handoff.entity-disabled': 'Entity ID is required before opening entity details.',
+    'trace.manage.attribution-diagnostics.aria': 'Attribution diagnostics hertzbeat.entity_id hertzbeat.entity_name hertzbeat.workspace_id hertzbeat.collector hertzbeat.template',
+    'trace.manage.attribution-diagnostics.title': 'Attribution diagnostics',
+    'trace.manage.attribution-diagnostics.present': 'Present',
+    'trace.manage.attribution-diagnostics.missing': 'Missing',
+    'trace.manage.attribution-diagnostics.entity-id.present': 'Can open entity details',
+    'trace.manage.attribution-diagnostics.entity-id.missing': 'Missing entity ID; entity detail remains disabled',
+    'trace.manage.attribution-diagnostics.entity-name.present': 'Used for entity display name',
+    'trace.manage.attribution-diagnostics.entity-name.missing': 'Missing entity name; service name assists lookup',
+    'trace.manage.attribution-diagnostics.workspace-id.present': 'Workspace ownership',
+    'trace.manage.attribution-diagnostics.workspace-id.missing': 'Missing workspace field; current deployment context applies',
+    'trace.manage.attribution-diagnostics.collector.present': 'Collector source',
+    'trace.manage.attribution-diagnostics.collector.missing': 'Missing collector field; inspect trace service context',
+    'trace.manage.attribution-diagnostics.template.present': 'Monitoring template ownership',
+    'trace.manage.attribution-diagnostics.template.missing': 'Missing template field; resolve from entity or service',
+    'trace.manage.route.fallback.filtered-trace': 'Filtered trace',
+    'trace.manage.route.fallback.waiting-query': 'Waiting for query',
+    'trace.manage.route.fallback.unselected': 'Not selected',
+    'trace.manage.route.kicker': 'Observability',
+    'trace.manage.route.title': 'Traces Workbench',
+    'trace.manage.route.subtitle': 'Filter traces by service, trace ID, span ID, and error state, then inspect trend, list, and detail evidence.',
+    'trace.manage.route.action.return-source': 'Return to source',
+    'trace.manage.route.action.intake': 'Back to OTLP intake',
+    'trace.manage.route.action.collectors': 'Collectors',
+    'trace.manage.route.action.entity': 'Entity detail',
+    'trace.manage.route.action.alerts': 'Alert handling',
+    'trace.manage.route.action.templates': 'Monitor templates',
+    'trace.manage.route.action.catalog': 'Entity catalog',
+    'trace.manage.route.query.service': 'Service name',
+    'trace.manage.route.query.trace-id': 'Trace ID',
+    'trace.manage.route.query.span-id': 'Span ID',
+    'trace.manage.route.query.status': 'Trace status',
+    'trace.manage.route.query.status.all': 'All traces',
+    'trace.manage.route.query.status.error': 'Error traces',
+    'trace.manage.route.query.run': 'Run query',
+    'trace.manage.route.query.reset': 'Reset',
+    'trace.manage.route.stat.total': 'Total traces',
+    'trace.manage.route.stat.errors': 'Error traces',
+    'trace.manage.route.stat.list-count': 'List rows',
+    'trace.manage.route.stat.latest': 'Latest time',
+    'trace.manage.route.chart.title': 'Trend band',
+    'trace.manage.route.chart.latest-window': 'Latest 10 traces',
+    'trace.manage.route.table.title': 'Recent traces',
+    'trace.manage.route.table.subtitle': 'Current query',
+    'trace.manage.route.table.count': '{{count}} rows',
+    'trace.manage.route.table.header.start': 'Start time',
+    'trace.manage.route.table.header.service': 'Service',
+    'trace.manage.route.table.header.root-span': 'Root span',
+    'trace.manage.route.table.header.duration': 'Duration',
+    'trace.manage.route.table.header.status': 'Status',
+    'trace.manage.route.table.header.trace-id': 'Trace ID',
+    'trace.manage.route.empty.title': 'No traces',
+    'trace.manage.route.empty.copy': 'Check time range, entity attribution, collectors, and monitor templates before viewing traces.',
+    'trace.manage.route.detail.title': 'Detail panel',
+    'trace.manage.route.detail.service': 'Service',
+    'trace.manage.route.detail.namespace': 'Namespace',
+    'trace.manage.route.detail.root-span': 'Root span',
+    'trace.manage.route.detail.duration': 'Duration',
+    'trace.manage.route.evidence.aria': 'Trace evidence Trace ID Trace status Start time Latest observed',
+    'trace.manage.route.evidence.title': 'Trace evidence',
+    'trace.manage.route.evidence.current': 'Current selection',
+    'trace.manage.route.evidence.trace-id': 'Trace ID',
+    'trace.manage.route.evidence.trace-status': 'Trace status',
+    'trace.manage.route.evidence.error-only': 'Error traces only',
+    'trace.manage.route.evidence.current-filter': 'Current filter',
+    'trace.manage.route.evidence.start-time': 'Start time',
+    'trace.manage.route.evidence.root-span-start': 'Root span start',
+    'trace.manage.route.evidence.latest-observed': 'Latest observed',
+    'trace.manage.route.evidence.latest-workbench-trace': 'Latest workbench trace',
+    'trace.manage.route.entity-context.aria': 'Entity context Current entity Monitor instance Current service Trace context Current environment Time range Collection source',
+    'trace.manage.route.entity-context.title': 'Entity context',
+    'trace.manage.route.handoff.alert-hint': 'View related alerts using the current entity, service, and trace context.',
+    'trace.manage.route.handoff.signal-hint': 'The current traceId, spanId, and service context carry into logs and metrics workbenches.',
+    'trace.manage.drawer.event-prefix': 'Event',
+    'trace.manage.drawer.link-prefix': 'Link',
+    'trace.manage.drawer.event-fact.title': 'Span event',
+    'trace.manage.drawer.event-fact.fallback': 'Unnamed event',
+    'trace.manage.drawer.event-fact.meta': 'A timestamp on the current span, not a new span',
+    'trace.manage.drawer.event-fact.span': 'Owner span',
+    'trace.manage.drawer.event-fact.position': 'Event position',
+    'trace.manage.drawer.event-fact.timeline': 'Relative to trace timeline',
+    'trace.manage.drawer.event-fact.attributes': 'Event attributes',
+    'trace.manage.drawer.event-fact.no-attributes': 'No attributes',
+    'trace.manage.drawer.kicker': 'Trace detail',
+    'trace.manage.drawer.title-fallback': 'Trace waterfall',
+    'trace.manage.drawer.action.logs': 'View logs',
+    'trace.manage.drawer.action.metrics': 'View metrics',
+    'trace.manage.drawer.action.entity': 'Entity detail',
+    'trace.manage.drawer.loading.title': 'Loading trace detail',
+    'trace.manage.drawer.loading.copy': 'Loading spans, events, links, and related logs for this trace.',
+    'trace.manage.drawer.error.title': 'Failed to load trace detail',
+    'trace.manage.drawer.error.copy': 'Trace detail is temporarily unavailable. Try again or adjust the query.',
+    'trace.manage.drawer.span-count': '{{count}} spans',
+    'trace.manage.drawer.stat.current-span': 'Current span',
+    'trace.manage.drawer.stat.error-spans': 'Error spans',
+    'trace.manage.drawer.stat.events': 'Events',
+    'trace.manage.drawer.stat.links': 'Links',
+    'trace.manage.drawer.waterfall.span': 'Span',
+    'trace.manage.drawer.waterfall.duration': 'Duration',
+    'trace.manage.drawer.waterfall.timeline': 'Timeline',
+    'trace.manage.drawer.event-detail.title': 'Event detail',
+    'trace.manage.drawer.event-detail.copy': 'A timestamp on the current span, not a new span',
+    'trace.manage.drawer.event-detail.action.span': 'View span',
+    'trace.manage.drawer.empty.title': 'Select a trace',
+    'trace.manage.drawer.empty.copy': 'Open a trace from the list to inspect the waterfall and span evidence here.',
+    'explorer.kicker': 'Observability',
+    'explorer.title': 'Query Workbench',
+    'explorer.subtitle': 'Search traces, logs, and metrics together, then use the query row, trend band, table, and detail panel to locate evidence.',
+    'explorer.tags.unified-query': 'Unified query',
+    'explorer.tags.context-retention': 'Context retained',
+    'explorer.tags.cross-signal-search': 'Cross-signal search',
+    'explorer.focus': 'Cross-signal query entry',
+    'explorer.summary': 'Keep the cold workbench compact while preserving handoffs into HertzBeat traces, logs, metrics, and entity catalog.',
+    'explorer.lanes.query.title': 'Query row',
+    'explorer.lanes.query.copy': 'Capture service, status, and resource attributes before opening signal-specific evidence.',
+    'explorer.lanes.query.meta': 'Query row contract',
+    'explorer.lanes.results.title': 'Results panel',
+    'explorer.lanes.results.copy': 'Keep trace, log, and metric results in one dense table for comparison.',
+    'explorer.lanes.results.meta': 'Results panel',
+    'explorer.lanes.drilldown.title': 'Context handoff',
+    'explorer.lanes.drilldown.copy': 'Preserve links to traces, logs, metrics, and the entity catalog.',
+    'explorer.lanes.drilldown.meta': 'Context handoff',
+  'explorer.checklist.entry.title': 'Route entry',
+  'explorer.checklist.entry.copy': 'The route owns the cold workbench entry and leaves interaction in the client island.',
+  'explorer.checklist.entry.meta': 'done',
+  'explorer.checklist.adapters.title': 'Adapters',
+  'explorer.checklist.adapters.copy': 'Backend-backed adapters remain the next step after the static convergence pass.',
+  'explorer.checklist.adapters.meta': 'next',
+  'explorer.checklist.links.title': 'Signal links',
+  'explorer.checklist.links.copy': 'Handoff URLs stay stable for traces, logs, metrics, and entities.',
+  'explorer.checklist.links.meta': 'reserved',
+    'explorer.actions.save-view': 'Save view',
+    'explorer.actions.create-alert': 'Create alert',
+    'explorer.actions.add-dashboard': 'Add to dashboard',
+    'explorer.query.aria': 'Query condition',
+    'explorer.query.placeholder': 'Filter by service, status, or resource attributes',
+    'explorer.query.default': 'service.name = "checkout" AND status = "error"',
+    'explorer.query.run': 'Run query',
+    'explorer.signal.aria': 'Signal type',
+    'explorer.signal.all': 'All signals',
+    'explorer.sort.aria': 'Sort order',
+    'explorer.sort.time-desc': 'Newest first',
+    'explorer.sort.duration-desc': 'Slowest first',
+    'explorer.filters.title': 'Quick filters',
+    'explorer.filters.clear': 'Clear',
+    'explorer.filters.signal-type': 'Signal type',
+    'explorer.filters.deployment-environment': 'Deployment environment',
+    'explorer.filters.service-name': 'Service name',
+    'explorer.filters.status': 'Status',
+    'explorer.chart.title': 'Trend band',
+    'explorer.chart.subtitle': 'Cross-signal hits in the last 30 minutes',
+    'explorer.chart.result-count': '{{count}} results',
+    'explorer.results.title': 'Cross-signal results',
+    'explorer.results.count': '{{count}} rows',
+    'explorer.table.signal-type': 'Signal type',
+    'explorer.table.service': 'Service',
+    'explorer.table.operation': 'Operation',
+    'explorer.table.status': 'Status',
+    'explorer.table.duration': 'Duration',
+    'explorer.table.time': 'Time',
+    'explorer.detail.title': 'Detail panel',
+    'explorer.detail.signal': 'Signal',
+    'explorer.detail.status': 'Status',
+    'explorer.detail.duration': 'Duration',
+    'explorer.handoff.trace': 'View trace',
+    'explorer.handoff.log': 'View logs',
+    'explorer.handoff.metrics': 'View metrics',
+    'explorer.handoff.entities': 'Entity catalog',
+    'explorer.rows.trace.signal': 'Trace',
+    'explorer.rows.log.signal': 'Logs',
+    'explorer.rows.metric.signal': 'Metrics',
+    'explorer.rows.exception.signal': 'Exception',
+    'explorer.rows.log.operation': 'Payment failed: insufficient balance',
+    'explorer.status.error': 'Error',
+    'explorer.status.normal': 'Normal',
     'otlp.metrics.handoff.entity-disabled': 'Entity ID is required before opening entity details.',
     'otlp.protocol.http': 'HTTP',
     'otlp.protocol.grpc': 'gRPC',
     'otlp.guide.auth-header': 'Auth header',
     'otlp.guide.grpc-authority': 'gRPC authority',
+    'otlp.guide.fallback.metrics.summary': 'Send metrics through a Collector or SDK, then bind them to HertzBeat monitor templates.',
+    'otlp.guide.fallback.logs.summary': 'Send OTLP logs into the log workspace and keep them available for entity attribution.',
+    'otlp.guide.fallback.traces.summary': 'Send OTLP traces to generate service-call relationships and topology evidence.',
     'otlp.loading': 'Loading OTLP intake',
     'otlp.kicker': 'OTLP',
     'otlp.title': 'OTLP Intake Center',
+    'ingestion.otlp.metrics.title': 'Metrics Workbench',
     'otlp.subtitle': 'Use this page to find OTLP endpoints for metrics, logs, and traces, and check whether data is arriving.',
     'otlp.fact.active-signals': 'Connected signals',
     'otlp.fact.recent-services': 'Recent services',
     'otlp.fact.bound-entities': 'Bound entities',
     'otlp.fact.latest': 'Latest',
+    'otlp.readiness.signals.title': 'Signals connected',
+    'otlp.readiness.signals.copy': '{{active}} / {{total}} active',
+    'otlp.readiness.latest.title': 'Latest report',
+    'otlp.readiness.latest.empty': 'No reports yet',
+    'otlp.readiness.latest.received': 'Telemetry received',
+    'otlp.readiness.latest.waiting': 'Waiting for first telemetry',
+    'otlp.readiness.entity.title': 'Entity attribution',
+    'otlp.readiness.entity.copy': '{{count}} entities',
+    'otlp.readiness.entity.meta': 'Object catalog',
+    'otlp.readiness.discovery.title': 'Service discovery',
+    'otlp.readiness.discovery.copy': '{{count}} services',
+    'otlp.readiness.discovery.meta': 'Last 24 hours',
+    'otlp.collection-loop.otlp-intake.title': 'OTLP signal intake',
+    'otlp.collection-loop.otlp-intake.copy': 'Connect OpenTelemetry metrics, logs, and traces, then continue investigation in the matching workbench.',
+    'otlp.collection-loop.otlp-intake.meta': 'Signals',
+    'otlp.collection-loop.traditional-monitoring.title': 'Traditional monitors',
+    'otlp.collection-loop.traditional-monitoring.copy': 'Keep host, database, middleware, and network-device template monitoring in the same operations loop.',
+    'otlp.collection-loop.traditional-monitoring.meta': 'Resources',
+    'otlp.collection-loop.collector-cluster.title': 'Collector cluster',
+    'otlp.collection-loop.collector-cluster.copy': 'Manage collection nodes, task dispatch, and intake status for private deployments.',
+    'otlp.collection-loop.collector-cluster.meta': 'Collector',
+    'otlp.collection-loop.monitoring-template.title': 'Monitor templates',
+    'otlp.collection-loop.monitoring-template.copy': 'Maintain multi-protocol templates so traditional monitors and OTLP entities map to one object model.',
+    'otlp.collection-loop.monitoring-template.meta': 'Templates',
+    'otlp.collection-loop.service-discovery.title': 'Service discovery',
+    'otlp.collection-loop.service-discovery.copy': 'Confirm newly discovered services, resources, and telemetry identities as HertzBeat entities.',
+    'otlp.collection-loop.service-discovery.meta': 'Discovery',
+    'otlp.collection-loop.object-directory.title': 'Object catalog',
+    'otlp.collection-loop.object-directory.copy': 'Review resources, signals, topology, and alert-handling context around entities.',
+    'otlp.collection-loop.object-directory.meta': 'Entities',
+    'otlp.source.section.quickstart': 'Quick start',
+    'otlp.source.section.migrate': 'Existing intake',
+    'otlp.source.section.apm-traces': 'APM / traces',
+    'otlp.source.section.logs': 'Logs',
+    'otlp.source.section.metrics': 'Metrics',
+    'otlp.source.item.demo-data.label': 'Ingest demo data in 5 minutes',
+    'otlp.source.item.demo-data.description': 'Write a demo OTLP data stream and open the workbench immediately.',
+    'otlp.source.item.open-telemetry.label': 'Existing OpenTelemetry',
+    'otlp.source.item.open-telemetry.description': 'Reuse an existing OpenTelemetry Collector or SDK pipeline.',
+    'otlp.source.item.grafana.description': 'Bring signals from Grafana into HertzBeat.',
+    'otlp.source.item.elk.description': 'Route log sources into the log workbench.',
+    'otlp.source.item.new-relic.description': 'Map service telemetry to the object catalog.',
+    'otlp.source.item.commercial-observability.label': 'Existing observability platform',
+    'otlp.source.item.commercial-observability.description': 'Connect services, logs, and metrics, then continue in the object catalog and investigation workbench.',
+    'otlp.source.item.honeycomb.description': 'Ingest trace data into the trace workbench and correlate services with logs.',
+    'otlp.source.item.self-hosted-observability.label': 'Self-hosted OTLP pipeline',
+    'otlp.source.item.self-hosted-observability.description': 'Reuse OTLP Collector pipelines in private or offline environments.',
+    'otlp.source.item.java.description': 'Java OpenTelemetry SDK configuration.',
+    'otlp.source.item.python.description': 'Python OpenTelemetry SDK configuration.',
+    'otlp.source.item.nodejs.description': 'Node.js and browser trace configuration.',
+    'otlp.source.item.golang.description': 'Go service trace configuration.',
+    'otlp.source.item.dotnet.description': '.NET OpenTelemetry instrumentation configuration.',
+    'otlp.source.item.deno.description': 'Deno runtime trace configuration.',
+    'otlp.source.item.php.description': 'PHP application trace configuration.',
+    'otlp.source.item.ruby.description': 'Ruby service trace configuration.',
+    'otlp.source.item.rust.description': 'Rust OpenTelemetry configuration.',
+    'otlp.source.item.nginx-tracing.label': 'Nginx traces',
+    'otlp.source.item.nginx-tracing.description': 'Trace reverse proxy and edge service calls.',
+    'otlp.source.item.external-api.label': 'External API monitoring',
+    'otlp.source.item.external-api.description': 'Track external API latency and errors.',
+    'otlp.source.item.web-vitals.label': 'Web Vitals traces',
+    'otlp.source.item.web-vitals.description': 'Configure frontend traces and core experience metrics.',
+    'otlp.source.item.temporal.description': 'Trace Temporal workflows and activities.',
+    'otlp.source.item.kubernetes-pod-logs.label': 'Kubernetes Pod logs',
+    'otlp.source.item.kubernetes-pod-logs.description': 'Collect Kubernetes Pod logs through OTLP.',
+    'otlp.source.item.docker-container-logs.label': 'Docker container logs',
+    'otlp.source.item.docker-container-logs.description': 'Forward container logs to HertzBeat.',
+    'otlp.source.item.fluentbit.description': 'Send logs with Fluent Bit.',
+    'otlp.source.item.fluentd.description': 'Send logs with FluentD.',
+    'otlp.source.item.syslog.description': 'Normalize Syslog data streams.',
+    'otlp.source.item.systemd-logs.label': 'Systemd logs',
+    'otlp.source.item.systemd-logs.description': 'Collect systemd journal logs.',
+    'otlp.source.item.cloudflare-logs.label': 'Cloudflare logs',
+    'otlp.source.item.cloudflare-logs.description': 'Forward Cloudflare edge logs.',
+    'otlp.source.item.vercel-logs.label': 'Vercel logs',
+    'otlp.source.item.vercel-logs.description': 'Forward Vercel deployment logs.',
+    'otlp.source.item.heroku-logs.label': 'Heroku logs',
+    'otlp.source.item.heroku-logs.description': 'Forward Heroku application logs.',
+    'otlp.source.item.http-logs.label': 'HTTP log reporting',
+    'otlp.source.item.http-logs.description': 'Push application logs through HTTP.',
+    'otlp.source.item.deno-logs.label': 'Deno logs',
+    'otlp.source.item.deno-logs.description': 'Collect Deno runtime logs.',
+    'otlp.source.item.prometheus.description': 'Bridge Prometheus metrics to OTLP.',
+    'otlp.source.item.otel-metrics.label': 'OpenTelemetry metrics',
+    'otlp.source.item.otel-metrics.description': 'Inspect OTLP metric data streams.',
+    'otlp.source.item.host-metrics.label': 'Host metrics',
+    'otlp.source.item.host-metrics.description': 'Collect host and process metrics.',
+    'otlp.source.item.jmx-metrics.label': 'JMX metrics',
+    'otlp.source.item.jmx-metrics.description': 'Collect JVM and JMX metrics.',
+    'otlp.source.item.nginx-metrics.label': 'Nginx metrics',
+    'otlp.source.item.nginx-metrics.description': 'Collect Nginx service metrics.',
+    'otlp.source.filter.all': 'All',
+    'otlp.source.filter.title': 'Filters',
+    'otlp.source.search.placeholder': 'Search data sources, cloud services, SDKs, or runtimes',
+    'otlp.source.search.label': 'Filter',
+    'otlp.source.search.empty': 'No matching data sources',
+    'otlp.ribbon.intake.label': 'OTLP intake',
+    'otlp.ribbon.intake.detail': 'Active signals',
+    'otlp.ribbon.metrics.label': 'Metrics workbench',
+    'otlp.ribbon.metrics.detail': 'Metrics channel',
+    'otlp.ribbon.logs.label': 'Logs workbench',
+    'otlp.ribbon.logs.detail': 'Logs channel',
+    'otlp.ribbon.traces.label': 'Trace workbench',
+    'otlp.ribbon.traces.detail': 'Trace channel',
+    'otlp.stepper.source': 'Select OTLP data source',
+    'otlp.stepper.protocol': 'Configure protocol and token',
+    'otlp.stepper.workbench': 'Open signal workbench',
+    'otlp.hero.title': 'OTLP protocol intake',
+    'otlp.hero.copy': 'Select an OpenTelemetry/OTLP data source, configure the metrics, logs, or traces intake path, and bind it to HertzBeat objects.',
+    'otlp.action.return-entity': 'Back to object',
+    'otlp.action.view-entities': 'View entities',
+    'otlp.section.collection-loop.kicker': 'Collection system',
+    'otlp.section.collection-loop.title': 'Existing intake loop',
+    'otlp.section.collection-loop.copy': 'OTLP intake is one path in the collection system; traditional monitors, collectors, templates, service discovery, and the object catalog remain unified operations entries.',
+    'otlp.section.readiness.kicker': 'Intake self-check',
+    'otlp.section.readiness.title': 'Real status',
+    'otlp.section.readiness.copy': 'Show confirmed signals, entity attribution, and latest reports; health items that are not connected are not displayed as empty metrics.',
+    'otlp.section.candidates.kicker': 'Entity discovery',
+    'otlp.section.candidates.title': 'Candidate entities',
+    'otlp.section.candidates.copy': 'OTLP resource identities are waiting for confirmation with original signal evidence preserved.',
+    'otlp.section.candidates.pending': 'Pending',
+    'otlp.section.self-check.kicker': 'Runtime self-check',
+    'otlp.section.self-check.title': 'Collector, storage, and query',
+    'otlp.section.self-check.copy': 'Check collection nodes, history storage, query service, and GreptimeDB connectivity.',
     'otlp.manage-token': 'Manage Tokens',
     'otlp.open-discovery': 'Open Discovery',
     'otlp.section.overview.title': 'Recent intake',
@@ -579,6 +2230,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.section.console.title': 'Metrics workbench',
     'otlp.metrics.section.console.copy': 'Inspect OTLP metrics with the same operations context',
     'otlp.metrics.section.context.title': 'Operations context',
+    'otlp.metrics.route.action.return-source': 'Return to source',
     'otlp.metrics.empty': 'No OTLP metrics matched the current query and time range.',
     'otlp.metrics.query-ready': 'metric context ready',
     'otlp.metrics.query-context': 'operations context',
@@ -586,15 +2238,247 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.series.unknown-service': 'Unknown service',
     'otlp.metrics.series.empty': 'No metric series are available in the current result set.',
     'otlp.metrics.series.empty.copy': 'Try widening the time range or changing the query.',
+    'otlp.metrics.stats.total-series': 'Metric series',
+    'otlp.metrics.stats.non-empty-series': 'Series with data',
+    'otlp.metrics.stats.datasource': 'Datasource',
+    'otlp.metrics.stats.latest-observed': 'Latest observed',
+    'otlp.metrics.stats.series-total': 'Total series',
+    'otlp.metrics.stats.intake-state': 'Intake state',
+    'otlp.metrics.header.kicker': 'Observability / metrics',
+    'otlp.metrics.field.service': 'Service',
+    'otlp.metrics.field.service-name': 'Service name',
+    'otlp.metrics.field.namespace': 'Namespace',
+    'otlp.metrics.field.environment': 'Environment',
+    'otlp.metrics.field.group-by': 'Group by',
+    'otlp.metrics.field.trace-id': 'Trace ID',
+    'otlp.metrics.group.service': 'Service',
+    'otlp.metrics.group.namespace': 'Namespace',
+    'otlp.metrics.group.environment': 'Environment',
+    'otlp.metrics.scope.service': 'Service scope',
+    'otlp.metrics.scope.all-services': 'All services',
+    'otlp.metrics.scope.all-namespaces': 'All namespaces',
+    'otlp.metrics.scope.all-environments': 'All environments',
+    'otlp.metrics.scope.series': 'Series',
+    'otlp.metrics.scope.series-count': '{{count}} rows',
+    'otlp.metrics.query.aria': 'Metric query',
+    'otlp.metrics.query.run': 'Run query',
+    'otlp.metrics.query.unselected': 'No metric selected',
+    'otlp.metrics.aggregation.aria': 'Aggregation',
+    'otlp.metrics.aggregation.avg': 'Average',
+    'otlp.metrics.aggregation.sum': 'Sum',
+    'otlp.metrics.aggregation.max': 'Max',
+    'otlp.metrics.aggregation.min': 'Min',
+    'otlp.metrics.explorer.chart-label': '{{count}} series with data',
+    'otlp.metrics.explorer.empty-title': 'No metric series',
+    'otlp.metrics.explorer.no-metrics-title': 'Confirm the time range, entity attribution, collector, and monitor template before viewing metrics.',
+    'otlp.metrics.explorer.waiting-ingest': 'Waiting for OTLP metrics',
+    'otlp.metrics.explorer.series-count': '{{count}} series',
+    'otlp.metrics.context.current-metric': 'Current metric',
+    'otlp.metrics.context.handoff-destination': 'Troubleshooting handoff',
+    'otlp.metrics.context.handoff-copy': 'Related entity, logs, traces, and alert handling.',
+    'otlp.metrics.context.handoff-meta': 'Continue by service, template, and time range',
+    'otlp.metrics.context.current-service': 'Current service',
+    'otlp.metrics.context.time-range': 'Time range',
+    'otlp.metrics.series.entity-id': 'entityId {{entityId}}',
+    'otlp.metrics.series.entity-missing': 'Waiting for entity attribution',
+    'otlp.metrics.series.context.metric-name': 'Metric name',
+    'otlp.metrics.series.context.selected-series': 'Current selected series',
+    'otlp.metrics.series.context.entity': 'Related entity',
+    'otlp.metrics.series.context.service': 'Current service',
+    'otlp.metrics.series.context.service-context': 'Service context',
+    'otlp.metrics.series.context.template': 'Monitor template',
+    'otlp.metrics.series.context.collector': 'Collector {{collector}}',
+    'otlp.metrics.series.context.monitor-template': 'Monitor template',
+    'otlp.metrics.series.context.environment': 'Current environment',
+    'otlp.metrics.series.context.deployment-environment': 'Deployment environment',
+    'otlp.metrics.evidence.samples': 'Samples',
+    'otlp.metrics.evidence.empty-skipped': '{{count}} empty values skipped',
+    'otlp.metrics.evidence.real-samples': 'Real samples',
+    'otlp.metrics.evidence.latest-value': 'Latest value',
+    'otlp.metrics.evidence.waiting-latest': 'Waiting for latest sample',
+    'otlp.metrics.evidence.value-range': 'Value range',
+    'otlp.metrics.evidence.average': 'Average {{average}}',
+    'otlp.metrics.evidence.waiting-real-samples': 'Waiting for real samples',
+    'otlp.metrics.evidence.sample-window': 'Sample window',
+    'otlp.metrics.evidence.real-sample-time': 'Real sample time',
+    'otlp.metrics.evidence.waiting-sample-time': 'Waiting for sample time',
+    'otlp.metrics.evidence.linked-trace': 'Related trace',
+    'otlp.metrics.evidence.missing-span': 'span_id not provided',
+    'otlp.metrics.evidence.missing-trace-span': 'Missing trace_id/span_id',
+    'otlp.metrics.handoff.logs': 'Historical logs',
+    'otlp.metrics.handoff.logs-by-trace': 'View by trace',
+    'otlp.metrics.handoff.logs-by-service': 'View by service',
+    'otlp.metrics.handoff.logs-current-span': 'Logs will focus on the current span',
+    'otlp.metrics.handoff.logs-current-trace': 'Logs will focus on the current trace',
+    'otlp.metrics.handoff.logs-service-filter': 'Filter by service when trace ID is missing',
+    'otlp.metrics.handoff.logs-waiting-context': 'Waiting for service or trace context',
+    'otlp.metrics.handoff.traces': 'Trace waterfall',
+    'otlp.metrics.handoff.trace-open': 'Open',
+    'otlp.metrics.handoff.trace-waiting-id': 'Waiting for trace ID',
+    'otlp.metrics.handoff.trace-full-current-span': 'Open the full trace and keep the current span',
+    'otlp.metrics.handoff.trace-full': 'Open the full trace',
+    'otlp.metrics.handoff.trace-missing-id': 'Metric has no trace ID, so trace navigation is unavailable',
+    'otlp.metrics.handoff.alerts': 'Alert handling',
+    'otlp.metrics.handoff.alerts-by-entity': 'Handle by entity',
+    'otlp.metrics.handoff.alerts-by-service': 'Handle by service',
+    'otlp.metrics.handoff.alerts-by-entity-meta': 'Open alerts with entity, service, and metric context',
+    'otlp.metrics.handoff.alerts-by-service-meta': 'Open alerts by service when entity ID is missing',
+    'otlp.metrics.attribution.entity-id.present': 'Entity detail can be opened',
+    'otlp.metrics.attribution.entity-id.missing': 'Missing entity ID, so entity detail stays disabled',
+    'otlp.metrics.attribution.entity-name': 'Used to display the entity name',
+    'otlp.metrics.attribution.workspace-id': 'Use current deployment context when workspace is missing',
+    'otlp.metrics.attribution.collector': 'Collector source',
+    'otlp.metrics.attribution.template': 'Monitor template attribution',
     'actions.focus': 'Automation inventory and execution guardrails',
     'actions.subtitle': 'Automation catalog, execution history, and approval-aware operations all stay in one context.',
     'actions.summary': 'The actions entry page carries automation catalog, execution history, and approval semantics without requiring a page redesign for future data.',
+    'actions.tag.catalog': 'automation catalog',
+    'actions.tag.risk-aware': 'risk-aware actions',
+    'actions.tag.approval-flow': 'approval flow',
+    'actions.metric.catalog-entries': 'Catalog entries',
+    'actions.metric.pending-approvals': 'Pending approvals',
+    'actions.metric.recent-runs': 'Recent runs',
+    'actions.metric.high-risk-actions': 'High-risk actions',
+    'actions.catalog.restart.name': 'Restart checkout deployment',
+    'actions.catalog.restart.category': 'runtime recovery',
+    'actions.catalog.restart.scope': 'service checkout / prod-ap',
+    'actions.catalog.restart.owner': 'Platform operations',
+    'actions.catalog.restart.last-run': '12m ago',
+    'actions.catalog.restart.posture': 'Requires approval and rollback note',
+    'actions.catalog.replay.name': 'Replay delayed OTLP metrics',
+    'actions.catalog.replay.category': 'signal remediation',
+    'actions.catalog.replay.scope': 'otlp gateway / metrics',
+    'actions.catalog.replay.owner': 'Observability',
+    'actions.catalog.replay.last-run': '38m ago',
+    'actions.catalog.replay.posture': 'Safe within current replay window',
+    'actions.catalog.mute.name': 'Mute edge saturation alerts',
+    'actions.catalog.mute.category': 'alert hygiene',
+    'actions.catalog.mute.scope': 'entity edge-proxy',
+    'actions.catalog.mute.owner': 'SRE on-call',
+    'actions.catalog.mute.last-run': '2h ago',
+    'actions.catalog.mute.posture': 'Auto-expires after 30 minutes',
+    'actions.run.restart.name': 'Restart checkout deployment',
+    'actions.run.restart.target': 'checkout / prod-ap',
+    'actions.run.restart.actor': 'li.na',
+    'actions.run.restart.duration': 'pending approval',
+    'actions.run.replay.name': 'Replay delayed OTLP metrics',
+    'actions.run.replay.target': 'gateway metrics shard-3',
+    'actions.run.replay.actor': 'automation',
+    'actions.run.replay.duration': '06m 12s',
+    'actions.run.mute.name': 'Mute edge saturation alerts',
+    'actions.run.mute.target': 'edge-proxy / cn-hz',
+    'actions.run.mute.actor': 'lin.qi',
+    'actions.run.mute.duration': '01m 42s',
+    'actions.approval.restart.summary': 'Restart production checkout pods after saturation alert burst',
+    'actions.approval.restart.owner': 'checkout-oncall',
+    'actions.approval.restart.evidence': 'Alert storm and trace latency regression are attached.',
+    'actions.approval.replay.summary': 'Replay OTLP metrics for delayed collector queue',
+    'actions.approval.replay.owner': 'obs-admin',
+    'actions.approval.replay.evidence': 'Collector backlog has stabilized and replay window is open.',
+    'actions.entry.kicker': 'Automation entry',
+    'actions.entry.title': 'Automation actions',
+    'actions.entry.subtitle': 'Align entry context and approval semantics with the OTLP cold baseline.',
+    'actions.entry.action.overview': 'Open overview',
+    'actions.entry.action.entities': 'View entities',
+    'actions.entry.shell.eyebrow': 'Cold entry connected',
+    'actions.entry.shell.copy': 'This entry inherits the near-black canvas, compact controls, hard-edged panels, and static right rail from the OTLP intake page; execution adapters can plug in next.',
+    'actions.entry.chip.catalog': 'Automation catalog',
+    'actions.entry.chip.risk': 'Risk actions',
+    'actions.entry.chip.approval': 'Approval flow',
+    'actions.entry.checklist.title': 'Integration checklist',
+    'actions.entry.checklist.context.title': 'Unify entry context',
+    'actions.entry.checklist.context.copy': 'The route already shares the OTLP baseline page rhythm.',
+    'actions.entry.checklist.adapter.title': 'Connect execution adapter',
+    'actions.entry.checklist.adapter.copy': 'Action catalog and approval-flow data can mount when the adapter lands.',
+    'actions.entry.checklist.evidence.title': 'Keep evidence jumps',
+    'actions.entry.checklist.evidence.copy': 'Entity, alert, and trace entries stay in the same operations context.',
+    'actions.entry.empty.title': 'Waiting for execution adapter',
+    'actions.entry.empty.copy': 'The visual skeleton already matches the OTLP baseline; data can connect incrementally.',
+    'actions.entry.suggested.title': 'Suggested actions',
+    'actions.entry.suggested.copy': 'Generated from alert, entity, and three-signal context as suggestions only; nothing runs automatically.',
+    'actions.entry.suggested.evidence': 'View evidence',
+    'actions.entry.suggested.confirm': 'Run after manual confirmation',
+    'actions.approval-draft.title': 'Approval draft adapter',
+    'actions.approval-draft.copy': 'Creates a manual approval draft from the first suggested action through the safe BFF; it never executes the action.',
+    'actions.approval-draft.create': 'Create approval draft',
+    'actions.approval-draft.pending': 'Creating draft',
+    'actions.approval-draft.success': 'Approval draft created',
+    'actions.approval-draft.failed': 'Approval draft failed',
+    'actions.approval-draft.disabled': 'Alert, entity, or trace context is required before an approval draft can be created.',
+    'actions.approval-draft-queue.title': 'Approval draft queue',
+    'actions.approval-draft-queue.copy': 'Reads approval drafts through the safe BFF so operators can see manager-backed lifecycle state before any execution adapter exists.',
+    'actions.approval-draft-queue.loading': 'Loading drafts',
+    'actions.approval-draft-queue.empty': 'No approval drafts returned from manager yet.',
+    'actions.approval-decision.title': 'Approval decision adapter',
+    'actions.approval-decision.copy': 'Records an approve or reject decision for the draft through the safe BFF; it never executes the action.',
+    'actions.approval-decision.approve': 'Approve draft',
+    'actions.approval-decision.reject': 'Reject draft',
+    'actions.approval-decision.pending': 'Recording decision',
+    'actions.approval-decision.success': 'Approval decision recorded',
+    'actions.approval-decision.failed': 'Approval decision failed',
+    'actions.approval-decision.disabled': 'Create an approval draft before recording a decision.',
+    'actions.catalog-adapter.title': 'Action catalog adapter',
+    'actions.catalog-adapter.copy': 'Reads durable manual-only action catalog entries before approval drafts reference them.',
+    'actions.catalog-adapter.loading': 'Loading catalog',
+    'actions.catalog-adapter.empty': 'No manager catalog items yet.',
+    'actions.adapter-boundary.label': 'Execution boundary',
+    'actions.adapter-boundary.copy': 'Action catalog, execution history, and approval flow are roadmap sample snapshots until execution adapters land; they are not realtime run state. Alert-context suggestions are human handoffs from current evidence and never auto-execute.',
+    'actions.adapter-boundary.roadmap.workflow-automation': 'Workflow automation',
+    'actions.adapter-boundary.roadmap.action-catalog': 'Action catalog',
+    'actions.adapter-boundary.roadmap.app-builder': 'App builder',
+    'actions.adapter-boundary.roadmap.self-service-actions': 'Self-service actions',
+    'actions.adapter-boundary.roadmap.approvals': 'Approvals',
+    'actions.adapter-boundary.roadmap.scripts': 'Scripts',
+    'actions.adapter-boundary.roadmap.runbook-orchestration': 'Runbook orchestration',
+    'actions.snapshot.label': 'sample snapshot',
+    'actions.suggestion.target.current-alert': 'current alert',
+    'actions.suggestion.target.entity-id': 'entity {{entityId}}',
+    'actions.suggestion.evidence.source': 'source {{value}}',
+    'actions.suggestion.evidence.signal': 'signal {{value}}',
+    'actions.suggestion.evidence.trace': 'trace {{value}}',
+    'actions.suggestion.evidence.alert-group': 'alert group {{value}}',
+    'actions.suggestion.evidence.current-alert': 'current alert evidence',
+    'actions.suggestion.source.alert': 'alert center',
+    'actions.suggestion.source.entity': 'entity catalog',
+    'actions.suggestion.source.topology': 'topology',
+    'actions.suggestion.source.monitor': 'monitor center',
+    'actions.suggestion.source.unknown': 'unknown source {{source}}',
+    'actions.suggestion.signal.metrics': 'metrics',
+    'actions.suggestion.signal.logs': 'logs',
+    'actions.suggestion.signal.traces': 'traces',
+    'actions.suggestion.signal.alerts': 'alerts',
+    'actions.suggestion.signal.unknown': 'unknown signal {{signal}}',
+    'actions.suggestion.restart.title': 'Suggest restarting {{target}}',
+    'actions.suggestion.restart.copy': 'High-risk recovery suggestion only. Confirm blast radius, rollback window, and owner before execution.',
+    'actions.suggestion.silence.title': 'Suggest creating a temporary silence for {{target}}',
+    'actions.suggestion.silence.copy': 'Use for short-term noise reduction after ownership is confirmed and an expiry is set.',
+    'actions.suggestion.runbook.title': 'Suggest reviewing the runbook for {{target}}',
+    'actions.suggestion.runbook.copy': 'Align entity, alert, trace, and collector evidence before deciding whether to execute.',
+    'actions.suggestion.manual-required': 'Suggestion only; manual confirmation is required before execution.',
+    'actions.risk.high': 'high risk',
+    'actions.risk.medium': 'medium risk',
+    'actions.risk.low': 'low risk',
+    'actions.run-status.awaiting-approval': 'awaiting approval',
+    'actions.run-status.running': 'running now',
+    'actions.run-status.completed': 'completed',
+    'actions.approval-status.pending': 'pending owner decision',
+    'actions.approval-status.approved': 'approved',
+    'actions.surface.kicker': 'Action control plane',
+    'actions.surface.handoff.entity.title': 'Entity context handoff',
+    'actions.surface.handoff.entity.copy': 'Open entity inventory before expanding action target scope.',
+    'actions.surface.handoff.monitor.title': 'Monitor posture handoff',
+    'actions.surface.handoff.monitor.copy': 'Validate signal health before issuing recovery actions.',
+    'actions.surface.handoff.approval.title': 'Approval evidence handoff',
+    'actions.surface.handoff.approval.copy': 'Keep alerts, traces, and rollback notes together in the same operator flow.',
     'actions.checklist.entry.title': 'Entry shell ready',
     'actions.checklist.entry.copy': 'The action catalog already uses the shared workbench language.',
+    'actions.checklist.entry.meta': 'done',
     'actions.checklist.adapters.title': 'Execution adapters',
     'actions.checklist.adapters.copy': 'Action list, run result, and approval flow keep the current integration boundary.',
+    'actions.checklist.adapters.meta': 'next',
     'actions.checklist.context.title': 'Context continuity',
     'actions.checklist.context.copy': 'Jump to the actions page directly from entity, alert, or topology context.',
+    'actions.checklist.context.meta': 'reserved',
     'actions.lanes.catalog.title': 'Automation catalog',
     'actions.lanes.catalog.copy': 'Treat action definitions, target scope, and risk level as one browsable object set.',
     'actions.lanes.history.title': 'Execution history',
@@ -602,6 +2486,20 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'actions.lanes.approval.title': 'Approval semantics',
     'actions.lanes.approval.copy': 'Reserve gate and guidance copy for high-risk actions in the main CTA region.',
     'menu.dashboard.back': 'Open overview',
+    'menu.entity.center': 'Entity center',
+    'entity.detail': 'Entity Detail',
+    'menu.monitor.bigdata': 'Bigdata Monitor',
+    'menu.monitor.cache': 'Cache Monitor',
+    'menu.monitor.center': 'Monitor center',
+    'menu.monitor.custom': 'Custom Monitor',
+    'menu.monitor.db': 'DB Monitor',
+    'menu.monitor.llm': 'Ai LLM',
+    'menu.monitor.mid': 'Mid Monitor',
+    'menu.monitor.network': 'Network Monitor',
+    'menu.monitor.os': 'OS Monitor',
+    'menu.monitor.server': 'Server Monitor',
+    'menu.monitor.service': 'Service Monitor',
+    'menu.monitor.webserver': 'Webserver Monitor',
     'menu.log.manage': 'Log workbench',
     'menu.trace.manage': 'Trace workbench',
     'alert.workbench.kicker': 'Alert Center',
@@ -615,7 +2513,46 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.workbench.empty.title': 'No alerts in the current scope',
     'alert.workbench.empty.copy': 'There are no actionable alerts in the current time range and filters.',
     'alert.workbench.empty.copy.filtered': 'No alerts match the current filters. Adjust the filters and search again.',
+    'alert.center.loading': 'Loading alert center',
     'alert.center.default-title': 'Alert',
+    'alert.center.search': 'Search alerts',
+    'alert.center.filter-status': 'Filter status',
+    'alert.center.batch.select-page': 'Select current page',
+    'alert.center.batch.select-group': 'Select alert group',
+    'alert.center.batch.selection-label': 'alert groups selected',
+    'alert.center.batch.acknowledge-selected': 'Acknowledge {{count}}',
+    'alert.center.batch.unacknowledge-selected': 'Unacknowledge {{count}}',
+    'alert.center.batch.resolve-selected': 'Resolve {{count}}',
+    'alert.center.batch.reopen-selected': 'Reopen {{count}}',
+    'alert.center.batch.silence-selected': 'Create silence for {{count}}',
+    'alert.center.batch.inhibit-selected': 'Create inhibit for {{count}}',
+    'alert.center.confirm.mark-done-batch': 'Please confirm whether to mark processed in batch!',
+    'alert.center.confirm.mark-no-batch': 'Please confirm whether to mark Pending in batch!',
+    'entity.response.context.title': 'Reviewing this entity',
+    'entity.response.context.return': 'Back to entity',
+    'entity.noise-controls.management.label': 'Rule view',
+    'entity.noise-controls.management.title.matched': 'Matching rules',
+    'entity.noise-controls.management.title.global': 'All rules',
+    'entity.noise-controls.management.empty': 'There are no rules matching this entity right now. Search or create one if needed.',
+    'entity.noise-controls.management.missing': '{{count}} matching rules are missing or no longer available. Only valid rules are shown here.',
+    'entity.noise-controls.management.view-all': 'View all rules',
+    'entity.noise-controls.management.view-matched': 'Only matching rules',
+    'entity.noise-controls.authoring.label': 'Entity noise control',
+    'entity.noise-controls.authoring.prefill-warning.no-entity-id': 'The current page is missing a valid entity ID. Enter the rule conditions manually.',
+    'entity.noise-controls.authoring.silence.title': 'Create a silence rule for this entity',
+    'entity.noise-controls.authoring.silence.prefill-success': "Shared labels from the entity's visible alerts were added as the default silence condition.",
+    'entity.noise-controls.authoring.silence.prefill-warning': 'No stable shared alert labels were found for this entity. Enter the silence condition manually.',
+    'entity.noise-controls.authoring.inhibit.title': 'Create an inhibit rule for this entity',
+    'entity.noise-controls.authoring.inhibit.prefill-success': "Shared labels from the entity's visible alerts were added to source, target, and equal labels.",
+    'entity.noise-controls.authoring.inhibit.prefill-warning': 'No stable shared alert labels were found for this entity. Enter the inhibit conditions manually.',
+    'entity.noise-controls.authoring.created-outside-matched.title': 'Rule created',
+    'entity.noise-controls.authoring.created-outside-matched.copy': 'This view does not automatically include the new rule. Switch to all rules to review or edit it.',
+    'entity.alert.workbench.confirm.acknowledge-selected': 'Acknowledge the selected alerts?',
+    'entity.alert.workbench.confirm.unacknowledge-selected': 'Unacknowledge the selected alerts?',
+    'alert.center.delete': 'Delete',
+    'alert.center.last-time': 'Last time',
+    'alert.center.end-time': 'End time',
+    'alert.center.time.tip': 'Triggered {{times}} times',
     'alert.center.recent.title': 'Current alerts',
     'alert.center.routes.title': 'Alert settings',
     'alert.center.routes.setting': 'Threshold and notification configuration',
@@ -627,17 +2564,111 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.center.routes.silence-inhibit.copy': 'Silence and inhibit rules',
     'alert.center.routes.silence-inhibit.meta': 'Silence and inhibit rules',
     'alert.center.summary.title': 'Alert summary',
+    'alert.status.firing': 'Firing',
+    'alert.status.acknowledged': 'Acknowledged',
+    'alert.status.resolved': 'Resolved',
     'alert.center.status.firing': 'Firing',
     'alert.center.status.resolved': 'Resolved',
     'alert.center.status.suppressed': 'Suppressed',
     'alert.center.metrics.warning': 'Warning',
     'alert.center.metrics.critical': 'Critical',
     'alert.center.metrics.emergency': 'Emergency',
+    'alert.center.metrics.error': 'Error',
+    'alert.center.metrics.info': 'Info',
+    'alert.center.metrics.unknown': 'Unknown',
     'alert.center.selected.fingerprint': 'Fingerprint / Creator',
     'alert.center.selected.trigger-window': 'Triggers / Active window',
+    'alert.center.selected.trigger-count': '{{count}} triggers',
     'alert.center.selected.triggers': 'triggers',
     'alert.center.selected.empty.title': 'No alert selected',
     'alert.center.selected.empty.copy': 'Select an alert from the left to inspect details.',
+    'alert.center.context.status.unknown': 'Unknown status {{status}}',
+    'alert.center.context.severity.unknown': 'Unknown severity {{severity}}',
+    'alert.center.context.time.title': 'Time range',
+    'alert.center.context.time.inherited': 'Inherited time window',
+    'alert.center.context.time.refresh': 'Refresh {{refresh}}',
+    'alert.center.context.time.paused': 'paused',
+    'alert.center.context.time.live': 'live',
+    'alert.center.context.monitor.title': 'Monitor',
+    'alert.center.context.monitor.id-meta': 'Monitor ID {{monitorId}}',
+    'alert.center.context.source.title': 'Source',
+    'alert.center.context.source.monitor.copy': 'Traditional monitor',
+    'alert.center.context.source.monitor.meta': 'Monitor center context',
+    'alert.center.context.source.topology.copy': 'Topology',
+    'alert.center.context.source.topology.meta': 'Topology impact context',
+    'alert.center.context.source.otlp.copy': 'OTLP signals',
+    'alert.center.context.source.otlp.meta': 'Metric, log, and trace context',
+    'alert.center.context.source.alert.copy': 'Alert event',
+    'alert.center.context.source.alert.meta': 'Alert evidence context',
+    'alert.center.context.source.unknown.copy': 'Unknown evidence source {{source}}',
+    'alert.center.context.source.default.meta': 'Inherited evidence source',
+    'alert.center.context.trace.title': 'Trace context',
+    'alert.center.context.trace.span-meta': 'Span ID {{spanId}}',
+    'alert.center.context.trace.inherited': 'traceId inherited',
+    'alert.center.topology.context.kicker': 'Topology impact',
+    'alert.center.topology.context.fallback': 'Current topology context',
+    'alert.center.topology.context.return': 'Return to topology',
+    'alert.center.topology.view-mode.unknown': 'Unknown topology view {{viewMode}}',
+    'alert.center.topology.source.all': 'All relation sources',
+    'alert.center.topology.source.otlp-trace-call': 'OTLP call relation',
+    'alert.center.topology.source.monitor-ownership': 'Monitor ownership',
+    'alert.center.topology.source.template-dependency': 'Template dependency',
+    'alert.center.topology.source.k8s-workload': 'K8s workload',
+    'alert.center.topology.source.database-middleware-connection': 'Database / middleware connection',
+    'alert.center.topology.source.cmdb-manual-label': 'CMDB / manual labels',
+    'alert.center.topology.source.alert-impact': 'Alert impact',
+    'alert.center.topology.source.unknown': 'Unknown relation source {{sourceKind}}',
+    'alert.center.group.response.stage.firing': 'Response state: Needs acknowledgement',
+    'alert.center.group.response.stage.acknowledged': 'Response state: Acknowledged',
+    'alert.center.group.response.stage.resolved': 'Response state: Recovered',
+    'alert.center.group.response.stage.custom': 'Response state: {{status}}',
+    'alert.center.group.evidence.summary': 'Evidence: {{alertCount}} {{alertUnit}} · {{labelCount}} {{labelUnit}}',
+    'alert.center.group.evidence.alert.singular': 'alert',
+    'alert.center.group.evidence.alert.plural': 'alerts',
+    'alert.center.group.evidence.label.singular': 'label',
+    'alert.center.group.evidence.label.plural': 'labels',
+    'alert.center.group.closure.next': 'Next: {{actions}}',
+    'alert.center.evidence.closure.kicker': 'Alert evidence closure',
+    'alert.center.evidence.closure.title': 'Review evidence before handling',
+    'alert.center.evidence.closure.copy': 'Keep entity, metric, log, trace, and topology evidence in the same handling entry.',
+    'alert.center.evidence.closure.summary.evidence': 'Evidence scope',
+    'alert.center.evidence.closure.summary.operations': 'Closure actions',
+    'alert.center.evidence.entity.title': 'Entity detail',
+    'alert.center.evidence.entity.fallback': 'Search entity by service',
+    'alert.center.evidence.entity.meta': 'Open the HertzBeat entity affected by this alert',
+    'alert.center.evidence.metrics.title': 'Metric evidence',
+    'alert.center.evidence.metrics.fallback': 'Inspect metrics by alert context',
+    'alert.center.evidence.metrics.meta': 'Inspect metrics in the same service, entity, and time range',
+    'alert.center.evidence.logs.title': 'Log evidence',
+    'alert.center.evidence.logs.fallback': 'Inspect logs by alert context',
+    'alert.center.evidence.logs.meta.trace': 'Inspect logs by traceId/spanId',
+    'alert.center.evidence.logs.meta.service': 'Inspect logs and trace fields for the same service',
+    'alert.center.evidence.traces.title': 'Trace evidence',
+    'alert.center.evidence.traces.fallback': 'Inspect traces by alert context',
+    'alert.center.evidence.traces.meta.trace': 'Open trace evidence by traceId/spanId',
+    'alert.center.evidence.traces.meta.service': 'Inspect latency and error spans for the same service',
+    'alert.center.evidence.topology.title': 'Topology impact',
+    'alert.center.evidence.topology.fallback': 'Inspect dependency impact',
+    'alert.center.evidence.topology.meta': 'Return to dependencies and confirm blast radius',
+    'alert.center.operation.target.current-alert': 'current alert',
+    'alert.center.operation.acknowledge.label': 'Acknowledge',
+    'alert.center.operation.acknowledge.copy': 'Mark {{count}} alert(s) for {{target}} as owned.',
+    'alert.center.operation.recover.label': 'Mark recovered',
+    'alert.center.operation.recover.copy': 'Keep the evidence bundle after recovery for review.',
+    'alert.center.operation.threshold.label': 'Create threshold rule',
+    'alert.center.operation.threshold.copy': 'Carry this entity and signal context into threshold rules.',
+    'alert.center.operation.notice.label': 'Configure notification policy',
+    'alert.center.operation.notice.copy': 'Route notifications from the current entity and labels.',
+    'alert.center.operation.group.label': 'Configure grouping',
+    'alert.center.operation.group.copy': 'Group alerts by stable entity, service, and environment labels.',
+    'alert.center.operation.silence.label': 'Create silence',
+    'alert.center.operation.silence.copy': 'Create a temporary silence from current labels.',
+    'alert.center.operation.inhibit.label': 'Create inhibit',
+    'alert.center.operation.inhibit.copy': 'Create inhibit rules from the current labels.',
+    'alert.center.operation.automation.label': 'Suggest automation',
+    'alert.center.operation.automation.copy': 'Carry this evidence into suggested actions with human confirmation.',
+    'alert.center.operation.close.label': 'Close alert',
+    'alert.center.operation.close.copy': 'Close this alert cycle after handling the issue.',
     'alert.center.operation.success': 'Alert updated. The list is refreshing.',
     'alert.center.operation.success.acknowledge': 'Alert acknowledged. The acknowledged filter is active, and the entity, topology, and three-signal evidence context is preserved.',
     'alert.center.operation.success.recover': 'Alert marked recovered. The recovered filter is active, and the entity, topology, and three-signal evidence context is preserved.',
@@ -706,48 +2737,149 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.group.delete.confirm.single': 'Delete the selected grouping rule. This cannot be undone.',
     'alert.group.delete.confirm.batch': 'Delete {{count}} grouping rules. This cannot be undone.',
     'alert.group.evidence.labels': 'Grouping labels',
+    'alert.group.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.group.pagination.page-size': 'Page size',
+    'alert.group.pagination.page': 'Page',
+    'alert.center.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.center.pagination.page-size': 'Page size',
+    'alert.center.pagination.page': 'Page',
     'alert.silence.copy': 'Manage silence rules by name, match labels, and active period to reduce repeated alert noise.',
     'alert.silence.action.new': 'New silence rule',
     'alert.silence.notes.times.meta': 'Muted alert count',
     'alert.silence.delete.confirm.single': 'Delete the selected silence rule. This cannot be undone.',
     'alert.silence.delete.confirm.batch': 'Delete {{count}} silence rules. This cannot be undone.',
+    'alert.silence.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.silence.pagination.page-size': 'Page size',
+    'alert.silence.pagination.page': 'Page',
     'alert.inhibit.copy': 'Define source, target, and equal-label inhibition so cascading alerts do not create duplicate noise.',
     'alert.inhibit.action.new': 'New inhibit rule',
     'alert.inhibit.delete.confirm.single': 'Delete the selected inhibit rule. This cannot be undone.',
     'alert.inhibit.delete.confirm.batch': 'Delete {{count}} inhibit rules. This cannot be undone.',
+    'alert.inhibit.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.inhibit.pagination.page-size': 'Page size',
+    'alert.inhibit.pagination.page': 'Page',
     'alert.inhibit.evidence.source-labels': 'Source labels',
     'alert.inhibit.evidence.target-labels': 'Target labels',
     'alert.inhibit.evidence.equal-labels': 'Equal labels',
+    'alert.integration.kicker': 'Alert integration',
+    'alert.integration.sources': 'Integration alert sources',
+    'alert.integration.token.manage': 'Manage tokens',
+    'alert.integration.doc.fallback': 'No integration guide is available for this alert source yet.',
+    'alert.integration.source.webhook': 'Default Webhook',
+    'alert.integration.source.prometheus': 'Prometheus',
+    'alert.integration.source.alertmanager': 'Alertmanager',
+    'alert.integration.source.skywalking': 'SkyWalking',
+    'alert.integration.source.uptime-kuma': 'Uptime Kuma',
+    'alert.integration.source.zabbix': 'Zabbix',
+    'alert.integration.source.tencent': 'Tencent Cloud Monitor',
+    'alert.integration.source.alibabacloud-sls': 'Alibaba Cloud Log Service SLS',
+    'alert.integration.source.huaweicloud-ces': 'Huawei Cloud CES',
+    'alert.integration.source.volcengine': 'Volcengine Cloud Monitor',
+    'alert.integration.fact.doc-status': 'Document status',
+    'alert.integration.fact.doc-loaded': 'Loaded',
+    'alert.integration.fact.doc-fallback': 'Fallback copy',
+    'alert.integration.source.selected': 'Selected',
+    'alert.integration.posture.doc-source': 'Document source',
+    'alert.integration.posture.existing-asset': 'Existing Angular asset',
+    'alert.integration.posture.fallback-behavior': 'Fallback behavior',
+    'alert.integration.posture.doc-loaded': 'Integration document loaded',
+    'alert.integration.posture.doc-missing': 'Show fallback copy when the document is missing',
+    'alert.integration.posture.behavior-preserved': 'Behavior preserved',
+    'alert.integration.posture.token-management': 'Token management',
+    'alert.integration.posture.token-copy': 'Continue to use the current API token management entry point.',
+    'alert.integration.diagram.aria': 'Alert integration diagram',
+    'alert.integration.diagram.pending': 'Rendering diagram',
+    'alert.integration.diagram.error': 'Diagram rendering failed',
     'alert.notice.page-size': '{{count}} / page',
+    'alert.notice.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.notice.pagination.page-size': 'Page size',
+    'alert.notice.pagination.page': 'Page',
     'alert.notice.copy': 'Configure receivers, notification policies, and message templates so alerts reach the right channel.',
+    'alert.notice.metrics.visible-receivers': 'Visible receivers',
+    'alert.notice.metrics.visible-rules': 'Visible policies',
+    'alert.notice.metrics.preset-templates': 'Preset templates',
+    'alert.notice.row.updated': 'Updated',
+    'alert.notice.row.enabled': 'Enabled',
+    'alert.notice.row.disabled': 'Disabled',
+    'alert.notice.row.delivery-prefix': 'Sending to',
+    'alert.notice.row.no-receiver': 'No receiver linked',
+    'alert.notice.form.receiver-ids.placeholder': 'Receiver IDs, for example 1, 2',
+    'alert.notice.form.template-id.placeholder': 'Template ID, use -1 for default',
+    'alert.notice.form.labels.placeholder': 'Label filters, for example severity:critical',
+    'alert.notice.form.days.placeholder': 'Weekdays, for example 1,2,3,4,5',
+    'alert.notice.form.email.placeholder': 'Email address, for example ops@example.com',
+    'alert.notice.form.phone.placeholder': 'Phone number, for example +86 13800000000',
+    'alert.notice.form.webhook.placeholder': 'Webhook URL, for example https://hooks.example.com',
     'alert.notice.delete.confirm.receiver': 'Delete the selected receiver. Related notification configuration may be affected.',
     'alert.notice.delete.confirm.template': 'Delete the selected message template. This cannot be undone.',
     'alert.notice.delete.confirm.rule': 'Delete the selected notification policy. This cannot be undone.',
     'common.end': 'End',
+    'bulletin.batch.delete': 'Batch delete bulletins',
+    'bulletin.new': 'New bulletin',
+    'bulletin.edit': 'Edit bulletin',
+    'bulletin.name': 'Bulletin name',
+    'bulletin.monitor.type': 'Monitor type',
+    'bulletin.monitor.name': 'Monitor',
+    'bulletin.monitor.metrics': 'Metrics',
     'bulletin.facts.selected': 'Current selection',
     'bulletin.list.monitors': 'monitors',
     'bulletin.list.creator': 'creator',
+    'bulletin.selected.empty.title': 'No bulletin selected',
+    'bulletin.selected.empty.copy': 'Select a bulletin board from the list to inspect its metrics context.',
     'bulletin.selected.fields': 'fields',
+    'bulletin.monitor.ids.placeholder': '1, 2, 3',
+    'bulletin.delete.title': 'Delete bulletin',
+    'bulletin.delete.copy': 'After deletion, this bulletin board is removed from the current workspace and the metrics refresh context needs a bulletin board to be selected again.',
+    'bulletin.delete.confirm': 'Confirm delete',
     'bulletin.validation.app': 'App is required',
     'bulletin.validation.monitors': 'At least one monitor id is required',
+    'bulletin.validation.fields': 'Metrics fields JSON is invalid',
     'common.apply-filters': 'Apply filters',
     'common.clear': 'Clear',
     'entities.list.loading': 'Loading object directory',
     'entities.list.title': 'Object Directory',
+    'entities.list.kicker': 'Object-first triage',
+    'entities.list.subtitle': 'Use services, resources, and entities to narrow risk first, then move into logs, traces, metrics, or alerts with the same context.',
     'entities.list.environment.select': 'Select Environment/s',
     'entities.list.environment.all': 'All',
     'entities.list.resource-search.placeholder': 'Search and filter based on resource attributes.',
     'entities.list.action.new': 'New object',
+    'entities.list.action.create': 'Create entity',
+    'entities.list.action.discovery': 'Discover from telemetry',
+    'entities.list.action.import': 'Import definition',
+    'entities.list.metric.total': 'Total entities',
+    'entities.list.metric.abnormal': 'Active abnormal objects',
+    'entities.list.metric.alerting': 'High-risk objects',
+    'entities.list.metric.linked': 'Linked objects',
+    'entities.list.search.placeholder': 'Search entity name, namespace, or owner',
     'entities.list.column.object': 'Object',
     'entities.list.column.type': 'Type',
+    'entities.list.column.owner': 'Owner',
+    'entities.list.column.progress': 'Progress',
+    'entities.list.column.evidence': 'Evidence links',
     'entities.list.column.status': 'Status',
+    'entities.list.column.next-action': 'Next action',
     'entities.list.column.alerts': 'Alerts',
     'entities.list.column.monitors': 'Monitors',
     'entities.list.column.relations': 'Relations',
     'entities.list.column.updated': 'Last evidence',
+    'entities.list.table.total': '{{total}} entities',
+    'entities.list.table.range': 'Showing {{from}}-{{to}} / {{total}}',
     'entities.list.pagination.items': '{{from}}-{{to}} / {{total}} Items',
     'entities.list.empty.title': 'No objects found',
     'entities.list.empty.copy': 'Adjust the resource filter or connect telemetry to populate the object directory.',
+    'entities.list.unselected.title': 'No entity selected',
+    'entities.list.unselected.copy': 'Select an entity to inspect evidence and actions.',
+    'entities.list.row.owner.unset': 'Not set',
+    'entities.list.row.progress.incomplete': '{{percent}} · incomplete',
+    'entities.list.row.progress.missing': 'Owner and runbook are still missing.',
+    'entities.list.row.evidence.monitors': '{{count}} monitors',
+    'entities.list.row.evidence.alerts': '{{count}} alerts',
+    'entities.list.row.evidence.identities': '{{count}} identities',
+    'entities.list.row.evidence.updated': 'Updated {{time}}',
+    'entities.list.row.action.owner': 'Set owner',
+    'entities.list.row.action.logs': 'Log clues',
+    'entities.list.row.action.traces': 'Trace evidence',
     'entities.list.item.fallback': 'Untitled object',
     'entities.list.type.api': 'API',
     'entities.list.type.database': 'Database',
@@ -757,41 +2889,267 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'entities.list.type.k8s-workload': 'K8s workload',
     'entities.list.type.middleware': 'Middleware',
     'entities.list.type.queue': 'Queue',
-    'entities.list.type.service': '服务',
+    'entities.list.type.service': 'Service',
     'entities.list.type.system': 'System',
-    'entities.list.environment.local': '本地',
+    'entities.list.environment.local': 'Local',
     'entities.list.status.critical': 'Critical',
     'entities.list.status.down': 'Offline',
     'entities.list.status.healthy': 'Healthy',
     'entities.list.status.unhealthy': 'Unhealthy',
-    'entities.list.status.unknown': '未知',
+    'entities.list.status.unknown': 'Unknown',
     'entities.list.status.warning': 'Warning',
-    'entities.list.metric.alerts': '当前页告警',
-    'entities.list.metric.monitors': '当前页监控',
-    'entities.list.metric.relations': '当前页关系',
-    'entities.list.item.meta': '监控 {{monitorCount}} · 告警 {{alertCount}} · 证据 {{time}}',
-    'entities.list.selected.alerts': '告警 {{count}}',
-    'entities.list.selected.definition-activity': '定义活动 -',
-    'entities.list.rail.identity-monitor-relation': '身份 / 监控 / 关系',
+    'entities.list.metric.alerts': 'Current page alerts',
+    'entities.list.metric.monitors': 'Current page monitors',
+    'entities.list.metric.relations': 'Current page relations',
+    'entities.list.item.meta': 'Monitors {{monitorCount}} · alerts {{alertCount}} · evidence {{time}}',
+    'entities.list.selected.alerts': 'Alerts {{count}}',
+    'entities.list.selected.definition-activity': 'Definition activity -',
+    'entities.list.rail.identity-monitor-relation': 'Identity / monitors / relations',
     'monitor.editor.validation.cron': 'Cron expression is invalid',
     'setting.status.validation.incident-components': 'At least one component id is required',
     'setting.status.validation.incident-message': 'Incident message is required',
-    'log.manage.trend.count': '日志 {{count}}',
+    'log.manage.trend.count': '{{count}} logs',
+    'log.manage.console.title': 'Logs Workbench',
+    'log.manage.loading': 'Loading log workbench',
+    'log.manage.api.degraded.title': 'Log API unavailable',
+    'log.manage.api.degraded.copy': 'The workbench is showing an empty degraded state because the log read API did not respond in time. Query controls remain available.',
+    'log.manage.route.kicker': 'Observability',
+    'log.manage.route.title': 'Log workbench',
+    'log.manage.route.subtitle': 'Filter logs by collection source, entity, trace, and alert handling while keeping live stream, history search, and context triage in one workbench.',
+    'log.manage.route.action.intake': 'Back to OTLP intake',
+    'log.manage.route.action.return-source': 'Return to source',
+    'log.manage.route.action.collector': 'Collectors',
+    'log.manage.route.action.entity': 'Entity detail',
+    'log.manage.route.action.alerts': 'Alert handling',
+    'log.manage.route.action.templates': 'Monitor templates',
+    'log.manage.query.search.aria': 'Log query',
+    'log.manage.query.search.placeholder': 'service.name = "checkout"',
+    'log.manage.query.severity.aria': 'Severity',
+    'log.manage.query.severity.all': 'All severities',
+    'log.manage.query.run.stream': 'Apply to log stream',
+    'log.manage.query.run.history': 'Run query',
+    'log.manage.query.reset': 'Reset',
+    'log.manage.query.trace-id': 'Trace ID',
+    'log.manage.query.span-id': 'Span ID',
+    'log.manage.query.body.aria': 'Log body',
+    'log.manage.query.body.placeholder': 'Search log body',
+    'log.manage.summary.total': 'Total logs',
+    'log.manage.summary.errors': 'Error logs',
+    'log.manage.summary.trace-coverage': 'Trace coverage',
+    'log.manage.summary.latest': 'Latest time',
+    'log.manage.trend.title': 'Trend band',
+    'log.manage.trend.points': '{{count}} points',
+    'log.manage.list.title': 'Recent logs',
+    'log.manage.list.count': '{{count}} rows',
+    'log.manage.list.column.time': 'Time',
+    'log.manage.list.column.severity': 'Severity',
+    'log.manage.list.column.service': 'Service',
+    'log.manage.list.column.body': 'Log body',
+    'log.manage.list.column.trace': 'Trace',
+    'log.manage.empty.title': 'No logs',
+    'log.manage.empty.copy': 'Confirm the time range, entity attribution, collector, and monitor template before reviewing logs.',
+    'log.manage.detail.title': 'Detail panel',
+    'log.manage.detail.severity': 'Severity',
+    'log.manage.detail.trace-id': 'Trace ID',
+    'log.manage.detail.span-id': 'Span ID',
+    'log.manage.evidence.aria': 'Log evidence Log time Log severity Body summary Latest observed',
+    'log.manage.evidence.title': 'Log evidence',
+    'log.manage.evidence.current': 'Current selection',
+    'log.manage.evidence.time.title': 'Log time',
+    'log.manage.evidence.time.meta': 'Log write time',
+    'log.manage.evidence.severity.title': 'Log severity',
+    'log.manage.evidence.severity.current-filter': 'Current filter',
+    'log.manage.evidence.body.title': 'Body summary',
+    'log.manage.evidence.body.meta': 'Log body',
+    'log.manage.evidence.latest.title': 'Latest observed',
+    'log.manage.evidence.latest.meta': 'Workbench latest log',
+    'log.manage.context.entity.aria': 'Entity context Current entity Monitor instance Current service Trace context Current environment Time range Collection source',
+    'log.manage.context.entity.title': 'Entity context',
+    'log.manage.handoff.alert-hint': 'Review related alerts with the current entity, service, and inherited trace context.',
+    'log.manage.handoff.signal-hint': 'Current log trace, span, and service context carries into the trace and metrics workbenches.',
+    'log.manage.handoff.alerts': 'Alert handling',
+    'log.manage.handoff.traces': 'View trace',
+    'log.manage.handoff.metrics': 'View metrics',
+    'log.manage.handoff.entities': 'Entity catalog',
+    'log.manage.stream.view-switch.title': 'Log view',
+    'log.manage.stream.view.stream': 'Log stream',
+    'log.manage.stream.view.history': 'History search',
+    'log.manage.stream.detail.title': 'Log detail',
+    'log.manage.stream.detail.stream-title': 'Log stream detail',
+    'log.manage.stream.detail.attribution-title': 'Attribution diagnostics',
+    'log.manage.stream.detail.attribution-present': 'Present',
+    'log.manage.stream.detail.attribution-missing': 'Missing',
+    'log.manage.selected.attribution.aria': 'Attribution diagnostics hertzbeat.entity_id hertzbeat.collector hertzbeat.template',
+    'log.manage.selected.attribution.title': 'Attribution diagnostics',
+    'log.manage.selected.attribution.state.present': 'Present',
+    'log.manage.selected.attribution.state.missing': 'Missing',
+    'log.manage.stream.action.view-trace': 'View trace',
+    'log.manage.stream.action.view-metrics': 'View metrics',
+    'log.manage.stream.action.view-code': 'View code',
+    'log.manage.stream.stage.kicker': 'Log stream',
+    'log.manage.stream.stage.title': 'Live log stream',
+    'log.manage.stream.status.connected': 'Connected',
+    'log.manage.stream.status.connecting': 'Connecting',
+    'log.manage.stream.status.disconnected': 'Disconnected',
+    'log.manage.stream.count': '{{count}} logs',
+    'log.manage.stream.state.live': 'Live',
+    'log.manage.stream.state.paused': 'Paused',
+    'log.manage.stream.action.reconnect': 'Reconnect',
+    'log.manage.stream.action.resume': 'Resume',
+    'log.manage.stream.action.pause': 'Pause',
+    'log.manage.stream.action.clear': 'Clear',
+    'log.manage.stream.paused-copy': 'Log stream is paused; new logs remain in the collection path until you resume.',
+    'log.manage.stream.empty.title': 'Waiting for live logs',
+    'log.manage.stream.empty.copy': 'Keep the stream connected so logs written by Collectors and OTLP arrive here in real time.',
+    'log.manage.stream.selected.title': 'Selected log',
+    'log.manage.stream.selected.detached': 'Detached from buffer',
+    'log.manage.stream.selected.none': 'No log selected',
+    'log.manage.stream.selected.helper': 'Select any log to inspect full content, trace context, and entity context.',
+    'log.manage.stream.detail.detached-warning': 'This log is detached from the live buffer; verify the preserved payload before handoff.',
+    'log.manage.stream.action.view-log': 'View log',
     'log.manage.selected.resource-keys': 'Resource fields',
     'log.manage.selected.attributes': 'Log attributes {{count}}',
     'log.manage.selected.span-severity': 'Span and severity',
     'log.manage.handoff.trace-disabled': 'A traceId is required before opening the related trace.',
     'log.manage.handoff.full-trace-disabled': 'A traceId is required before opening the full trace workspace.',
     'log.manage.handoff.entity-disabled': 'Entity ID is required before opening entity details.',
-    'overview.lane.logs.stat.recommended': '无链路 ID 时优先',
-    'passport.login.session-copy': '登录成功后会自动恢复当前工作台会话,并在需要时尝试刷新令牌。',
+    'log.manage.related-trace.error': 'The trace preview is temporarily unavailable. Retry later or open the full trace workspace.',
+    'log.manage.related-trace.fallback-title': 'Related trace preview',
+    'log.manage.related-trace.badge': 'Trace preview',
+    'log.manage.related-trace.open-workspace': 'View full trace',
+    'log.manage.related-trace.spans-count': '{{count}} spans',
+    'log.manage.related-trace.fact.current-span': 'Current span',
+    'log.manage.related-trace.fact.error-spans': 'Error spans',
+    'log.manage.related-trace.fact.events': 'Events',
+    'log.manage.related-trace.fact.links': 'Links',
+    'log.manage.related-trace.empty-title': 'No related trace',
+    'log.manage.related-trace.empty-copy': 'This log has no trace details available for preview.',
+    'log.manage.related-trace.loading-title': 'Loading trace preview',
+    'log.manage.related-trace.loading-copy': 'Loading the trace preview from the current log context.',
+    'log.manage.related-trace.span-label': 'Span',
+    'log.manage.related-trace.duration-label': 'Duration',
+    'log.manage.related-trace.timeline-label': 'Timeline',
+    'log.manage.related-trace.kicker': 'Related trace',
+    'log.manage.related-trace.error-title': 'Trace detail error',
+    'log.manage.related-trace.event-fact.title': 'Span event',
+    'log.manage.related-trace.event-fact.fallback': 'Unnamed event',
+    'log.manage.related-trace.event-fact.meta': 'Not a new span; this is a point on the current span timeline.',
+    'log.manage.related-trace.event-fact.span': 'Owning span',
+    'log.manage.related-trace.event-fact.position': 'Event position',
+    'log.manage.related-trace.event-fact.timeline': 'Relative trace timeline',
+    'log.manage.related-trace.event-fact.attributes': 'Event attributes',
+    'log.manage.related-trace.event-fact.no-attributes': 'No attributes',
+    'log.manage.related-trace.event-detail.title': 'Event detail',
+    'log.manage.related-trace.event-detail.copy': 'Not a new span; this is a point on the current span timeline.',
+    'log.manage.related-trace.event-detail.action.span': 'View span',
+    'log.manage.attribution.event-id.present': 'Locate this log ingest event.',
+    'log.manage.attribution.event-id.missing': 'Use timestamp and content when event ID is missing.',
+    'log.manage.attribution.ingest-id.present': 'Trace the ingest batch.',
+    'log.manage.attribution.ingest-id.missing': 'Use Collector logs when the ingest batch is missing.',
+    'log.manage.attribution.entity-id.present': 'Entity details can be opened.',
+    'log.manage.attribution.entity-id.missing': 'Entity ID is missing; entity details stay disabled.',
+    'log.manage.attribution.entity-name.present': 'Used to display the entity name.',
+    'log.manage.attribution.entity-name.missing': 'Use the service name for fallback search when entity name is missing.',
+    'log.manage.attribution.workspace-id.present': 'Workspace ownership.',
+    'log.manage.attribution.workspace-id.missing': 'Use current deployment context when workspace field is missing.',
+    'log.manage.attribution.collector': 'Collector source.',
+    'log.manage.attribution.template': 'Monitor template ownership.',
+    'overview.lane.logs.stat.recommended': 'Prioritize when trace ID is missing',
+    'passport.login.session-copy': 'After login, HertzBeat will restore the current workspace session and try to refresh tokens when needed.',
     'setting.status.validation.incident-name': 'Incident name is required',
     'status.incidents.history': 'Incident history',
-    'status.incidents.posture': '聚焦公开事件历史入口。',
+    'status.incidents.posture': 'Focus the public incident history entry.',
+    'incidents.entry.kicker': 'Incident entry',
+    'incidents.entry.title': 'Incidents',
+    'incidents.entry.subtitle': 'Align response timeline, ownership, and evidence entry points with the OTLP cold baseline.',
+    'incidents.entry.action.overview': 'Open overview',
+    'incidents.entry.action.entities': 'View entities',
+    'incidents.entry.shell.eyebrow': 'Cold entry connected',
+    'incidents.entry.shell.copy': 'This entry inherits the near-black canvas, compact controls, hard-edged panels, and static right rail from the OTLP intake page; the incident adapter can plug in next.',
+    'incidents.entry.chip.entry': 'Incident entry',
+    'incidents.entry.chip.timeline': 'Response timeline',
+    'incidents.entry.chip.owner-first': 'Owner first',
+    'incidents.entry.checklist.title': 'Integration checklist',
+    'incidents.entry.checklist.context.title': 'Unify entry context',
+    'incidents.entry.checklist.context.copy': 'The route already shares the OTLP baseline page rhythm.',
+    'incidents.entry.checklist.adapter.title': 'Connect incident adapter',
+    'incidents.entry.checklist.adapter.copy': 'Incident list, owner, and timeline data can mount when the adapter lands.',
+    'incidents.entry.checklist.evidence.title': 'Keep evidence jumps',
+    'incidents.entry.checklist.evidence.copy': 'Log, trace, and entity entries stay in the same response context.',
+    'incidents.entry.empty.title': 'Waiting for incident adapter',
+    'incidents.entry.empty.copy': 'The visual skeleton already matches the OTLP baseline; data can connect incrementally.',
+    'incidents.subtitle': 'Response timeline, ownership, and next steps stay grouped in a single operator context.',
+    'incidents.focus': 'Incident posture and response handoff',
+    'incidents.summary': 'The incidents entry page keeps response state, owners, and related evidence aligned while the deeper domain is still landing.',
+    'incidents.tag.shell': 'incident shell',
+    'incidents.tag.timeline': 'response timeline',
+    'incidents.tag.owner-first': 'owner-first',
+    'incidents.metric.open': 'Open incidents',
+    'incidents.metric.critical': 'Critical',
+    'incidents.metric.mitigating': 'Mitigating',
+    'incidents.metric.ownership-queues': 'Ownership queues',
+    'incidents.row.checkout.title': 'Checkout latency spike across prod-ap',
+    'incidents.row.checkout.service': 'checkout',
+    'incidents.row.checkout.owner': 'checkout-oncall',
+    'incidents.row.checkout.blast-radius': '2 regions',
+    'incidents.row.collector.title': 'Collector queue delay on metrics ingestion',
+    'incidents.row.collector.service': 'otlp-gateway',
+    'incidents.row.collector.owner': 'observability',
+    'incidents.row.collector.blast-radius': 'metrics only',
+    'incidents.row.edge.title': 'Edge proxy saturation after deploy',
+    'incidents.row.edge.service': 'edge-proxy',
+    'incidents.row.edge.owner': 'edge-sre',
+    'incidents.row.edge.blast-radius': 'cn-hz ingress',
+    'incidents.timeline.checkout.title': '09:08 mitigation started',
+    'incidents.timeline.checkout.copy': 'Rollback and restart actions were opened from the alert evidence bundle.',
+    'incidents.timeline.checkout.meta': 'checkout latency incident',
+    'incidents.timeline.collector.title': '08:33 metrics replay approved',
+    'incidents.timeline.collector.copy': 'Collector backlog dropped below the replay threshold and handoff stayed in the same rail.',
+    'incidents.timeline.collector.meta': 'ingestion incident',
+    'incidents.timeline.edge.title': '07:56 edge saturation escalated',
+    'incidents.timeline.edge.copy': 'Operator routed the incident from monitor detail to trace and log evidence.',
+    'incidents.timeline.edge.meta': 'edge incident',
+    'incidents.ownership.checkout.owner': 'checkout-oncall',
+    'incidents.ownership.checkout.queue': 'primary responder',
+    'incidents.ownership.checkout.copy': 'Keeps alert, trace, and deploy context bound to the active mitigation thread.',
+    'incidents.ownership.checkout.meta': '2 active handoffs',
+    'incidents.ownership.observability.owner': 'observability',
+    'incidents.ownership.observability.queue': 'signal validation',
+    'incidents.ownership.observability.copy': 'Validates replay safety and ingestion recovery before closing the incident.',
+    'incidents.ownership.observability.meta': '1 active handoff',
+    'incidents.ownership.edge.owner': 'edge-sre',
+    'incidents.ownership.edge.queue': 'capacity review',
+    'incidents.ownership.edge.copy': 'Tracks post-deploy regressions and the next rollback checkpoint.',
+    'incidents.ownership.edge.meta': 'awaiting rollback note',
+    'incidents.checklist.shell.title': 'Shell ready',
+    'incidents.checklist.shell.copy': 'The incidents route already shares the workbench shell and routing posture.',
+    'incidents.checklist.shell.meta': 'done',
+    'incidents.checklist.adapter.title': 'Adapters next',
+    'incidents.checklist.adapter.copy': 'Incident list, owner state, and response timeline can plug into the current API boundary.',
+    'incidents.checklist.adapter.meta': 'next',
+    'incidents.checklist.drilldown.title': 'Drilldown reserved',
+    'incidents.checklist.drilldown.copy': 'Log, trace, and entity handoff stay on the planned expansion path.',
+    'incidents.checklist.drilldown.meta': 'reserved',
+    'incidents.surface.kicker': 'Incident response desk',
+    'incidents.surface.handoff.logs.title': 'Log evidence',
+    'incidents.surface.handoff.logs.copy': 'Jump into logs when mitigation needs fresh payload evidence.',
+    'incidents.surface.handoff.traces.title': 'Trace evidence',
+    'incidents.surface.handoff.traces.copy': 'Preserve trace-first drilldown for latency and dependency regressions.',
+    'incidents.surface.handoff.overview.title': 'Overview return path',
+    'incidents.surface.handoff.overview.copy': 'Keep incident status visible in the same global response rhythm.',
     'alert.group.validation.name': 'Rule name is required',
     'alert.group.validation.labels': 'At least one label is required',
+    'alert.group.validation.group-wait': 'Wait time is required',
+    'alert.group.validation.group-interval': 'Interval time is required',
+    'alert.group.validation.repeat-interval': 'Repeat interval is required',
+    'alert.group.validation.group-wait-non-negative': 'Wait time must be 0 or greater',
+    'alert.group.validation.group-interval-non-negative': 'Interval time must be 0 or greater',
+    'alert.group.validation.repeat-interval-non-negative': 'Repeat interval must be 0 or greater',
+    'alert.group.id-required': 'Alert group id is required',
     'alert.group.default-title': 'Group rule',
+    'alert.group.loading': 'Loading group rules',
     'alert.group.labels': 'labels',
+    'alert.group.labels.placeholder': 'alertname, severity, service',
     'alert.group.wait': 'wait',
     'alert.group.interval': 'interval',
     'alert.group.repeat': 'repeat',
@@ -801,14 +3159,22 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.group.selected.labels': 'labels',
     'alert.group.selected.timers': 'timers',
     'alert.group.notes.query': 'Rule order',
+    'alert.rule.notes.sort-desc-copy': 'id desc',
+    'alert.rule.notes.search-copy': 'search',
+    'alert.rule.notes.search-sort-copy': 'search · id desc',
+    'alert.rule.selected.id-meta': 'ID {{id}}',
     'alert.inhibit.validation.name': 'Rule name is required',
     'alert.inhibit.validation.source': 'Source labels are required',
     'alert.inhibit.validation.target': 'Target labels are required',
     'alert.inhibit.validation.equal': 'Equal labels are required',
     'alert.inhibit.default-title': 'Inhibit rule',
+    'alert.inhibit.loading': 'Loading inhibit rules',
     'alert.inhibit.source': 'source',
     'alert.inhibit.target': 'target',
     'alert.inhibit.equal': 'equal',
+    'alert.inhibit.label.key.placeholder': 'Label key',
+    'alert.inhibit.label.value.placeholder': 'Label value',
+    'alert.inhibit.equal.placeholder': 'alertname, severity, service',
     'alert.inhibit.empty.title': 'No inhibit rules yet',
     'alert.inhibit.selected.empty.title': 'Select an inhibit rule',
     'alert.inhibit.selected.empty.copy': 'Choose a rule to see which active alerts suppress downstream noise.',
@@ -822,9 +3188,14 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.silence.validation.time': 'Start and end time are required',
     'alert.silence.validation.days': 'Days are required',
     'alert.silence.default-title': 'Silence rule',
+    'alert.silence.loading': 'Loading silence rules',
     'alert.silence.match-all': 'match all',
     'alert.silence.labels': 'labels',
+    'alert.silence.label.key.placeholder': 'Label key',
+    'alert.silence.label.value.placeholder': 'Label value',
     'alert.silence.times': 'muted alerts',
+    'alert.silence.time.start': 'Start time',
+    'alert.silence.time.end': 'End time',
     'alert.silence.days': 'days',
     'alert.silence.empty.title': 'No silence rules yet',
     'alert.silence.selected.empty.title': 'Select a silence rule',
@@ -832,6 +3203,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.silence.selected.strategy': 'strategy',
     'alert.silence.selected.strategy.all': 'all',
     'alert.silence.selected.strategy.any': 'any',
+    'alert.silence.selected.type-meta': 'type {{type}}',
     'alert.silence.selected.labels-days': 'labels / days',
     'alert.silence.notes.times.title': 'Muted alerts',
     'alert.silence.notes.query.title': 'Rule order',
@@ -868,9 +3240,16 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.subtitle': 'Manage receivers, templates, and delivery rules in one workbench.',
     'alert.notice.kicker': 'Alert notice',
     'alert.notice.loading': 'Loading notification center',
+    'alert.setting.loading': 'Loading alert settings',
     'alert.setting.title': 'Alert settings',
     'alert.setting.copy': 'Manage threshold names, data type, expressions, and alert content while keeping rule editing aligned with alert noise reduction.',
     'alert.setting.action.new': 'New threshold rule',
+    'alert.setting.batch.selection-label': 'threshold rules selected',
+    'alert.setting.export.selected': '{{count}} threshold rules selected',
+    'alert.setting.import.input': 'Import threshold rules file',
+    'alert.setting.notify.no-select-export': 'Select threshold rules before export',
+    'alert.export.switch-type': 'Choose export file format',
+    'alert.export.use-type': 'Export threshold rules in {{type}} format',
     'alert.setting.delete.confirm.single': 'Delete the selected threshold rule. This cannot be undone.',
     'alert.setting.delete.confirm.batch': 'Delete {{count}} threshold rules. This cannot be undone.',
     'alert.setting.fact.workspace': 'Workspace',
@@ -894,6 +3273,10 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.lanes.receivers.title': 'Receivers',
     'alert.notice.lanes.templates.title': 'Templates',
     'alert.notice.lanes.rules.title': 'Rules',
+    'alert.notice.lanes.receivers.copy': 'Manage who receives each alert notice.',
+    'alert.notice.lanes.rules.copy': 'Route notices by labels, schedule, and template.',
+    'alert.notice.lanes.templates.copy': 'Preview templates as plain text without exposing raw HTML or source.',
+    'alert.notice.lanes.count-meta': '{{total}} total · {{visible}} visible',
     'alert.notice.actions.define': 'Open alert settings',
     'alert.setting.name': 'Rule name',
     'alert.setting.type': 'Threshold type',
@@ -928,6 +3311,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.receiver.people.name': 'Receiver Name',
     'alert.notice.receiver.people.placeholder': 'Select a receiver',
     'alert.notice.receiver.type': 'Receiver type',
+    'alert.notice.receiver.type.placeholder': 'Select a notice type',
+    'alert.notice.rule.receivers.empty': 'No receivers yet. Create a receiver first.',
     'alert.notice.send-test': 'Send Alert Test Msg',
     'alert.notice.send-test.notify.failed': 'Send Alert Test Failed!',
     'alert.notice.send-test.notify.success': 'Send Alert Test Success!',
@@ -944,6 +3329,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.template.show': 'View Template Content',
     'alert.notice.template.type': 'Notice Type',
     'alert.notice.template.validation.name': 'Template name is required',
+    'alert.notice.template.validation.type': 'Notice type is required',
     'alert.notice.template.validation.content': 'Template content is required',
     'alert.notice.templates.title': 'Templates',
     'alert.notice.templates.empty.title': 'No templates yet',
@@ -965,6 +3351,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.rule.priority.placeholder': 'Select Priorities',
     'alert.notice.rule.tag': 'Label Match',
     'alert.notice.rule.tag.placeholder': 'Select Labels',
+    'alert.notice.rule.label.value.placeholder': 'Label value',
     'alert.notice.rule.time': 'Notification Time',
     'alert.notice.rule.time-end': 'End time',
     'alert.notice.rule.time-start': 'Start time',
@@ -985,6 +3372,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.type.WeCom-robot': 'WeCom robot',
     'alert.notice.type.ding': 'DingTalk',
     'alert.notice.type.fei-shu': 'Feishu robot',
+    'alert.notice.type.telegram': 'Telegram',
     'alert.notice.type.telegram-bot': 'Telegram bot',
     'alert.notice.type.slack': 'Slack',
     'alert.notice.type.discord': 'Discord',
@@ -1004,6 +3392,9 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.type.telegram-message-thread-id': 'Telegram message thread ID',
     'alert.notice.type.webhook-auth-type': 'Webhook auth type',
     'alert.notice.type.webhook-auth-type.placeholder': 'Select an auth type',
+    'alert.notice.type.webhook-auth-type.none': 'None',
+    'alert.notice.type.webhook-auth-type.basic': 'Basic',
+    'alert.notice.type.webhook-auth-type.bearer': 'Bearer',
     'alert.notice.type.webhook-auth-token': 'Webhook auth token',
     'alert.notice.type.slack-webHook-url': 'Slack webhook URL',
     'alert.notice.type.discord-channel-id': 'Discord channel ID',
@@ -1039,14 +3430,79 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.receiver.phone.placeholder': 'Phone number, for example +86 13800000000',
     'alert.notice.receiver.email.placeholder': 'Email address, for example ops@example.com',
     'alert.notice.receiver.hookUrl.placeholder': 'Webhook URL, for example https://hooks.example.com',
+    'bulletin.loading': 'Loading bulletin center',
     'bulletin.search.all': 'all bulletins',
+    'bulletin.navigation.aria': 'Bulletin navigation',
     'bulletin.validation.name': 'Bulletin name is required',
+    'bulletin.metrics.column.app': 'App',
+    'bulletin.metrics.column.host': 'Host',
     'bulletin.metrics.empty.title': 'No metrics',
     'bulletin.metrics.empty.copy': 'No metrics are available yet.',
     'bulletin.metrics.error.title': 'Metrics unavailable',
     'bulletin.metrics.error-default': 'The metrics request failed.',
+    'bulletin.metrics.no-data': 'No Data Available',
     'exception.fact.workspace': 'Workspace',
     'exception.fact.status': 'Status',
+    'exception.fact.action': 'Action',
+    'exception.403.title': '403 Access denied',
+    'exception.403.subtitle': 'The current session cannot access this route. Review workspace, role, and token policy before retrying.',
+    'exception.403.fact.status': 'Permission boundary',
+    'exception.403.fact.action': 'Review access policy',
+    'exception.403.boundary.title': 'Access boundary',
+    'exception.403.boundary.copy': 'Confirm the session, role, and workspace permission before retrying.',
+    'exception.403.boundary.meta': 'Access control',
+    'exception.403.next-step.copy': 'Open access settings or return to a permitted workspace.',
+    'exception.404.title': '404 Route not found',
+    'exception.404.subtitle': 'The requested route is not registered in this HertzBeat workspace.',
+    'exception.404.fact.status': 'Route missing',
+    'exception.404.fact.action': 'Go back or redirect',
+    'exception.404.boundary.title': 'Route boundary',
+    'exception.404.boundary.copy': 'Check the navigation entry and compatibility alias before retrying.',
+    'exception.404.boundary.meta': 'Routing',
+    'exception.404.next-step.copy': 'Return to overview or use a known workspace route.',
+    'exception.500.title': '500 Runtime exception',
+    'exception.500.subtitle': 'The runtime returned an error while serving this workspace route.',
+    'exception.500.fact.status': 'Runtime failure',
+    'exception.500.fact.action': 'Retry or inspect',
+    'exception.500.boundary.title': 'Runtime boundary',
+    'exception.500.boundary.copy': 'Correlate server response, logs, and traces before retrying the operation.',
+    'exception.500.boundary.meta': 'Runtime',
+    'exception.500.next-step.copy': 'Inspect logs and traces, then retry after the backend is stable.',
+    'exception.next-step.title': 'Next step',
+    'exception.next-step.meta': 'Operator action',
+    'exception.subtitle.default': 'Group server-side exception events by service, resource, and time before opening logs or traces.',
+    'exception.chrome.title': 'Exception center',
+    'exception.chrome.filter-title': 'Filters',
+    'exception.chrome.clear-all': 'Clear all',
+    'exception.chrome.expand-more': 'Show more',
+    'exception.chrome.refresh-filters': 'Refresh filters',
+    'exception.time.last-7d': 'Last 7 days',
+    'exception.action.search': 'Search',
+    'exception.action.run-query': 'Run query',
+    'exception.action.feedback': 'Feedback',
+    'exception.action.share': 'Share',
+    'exception.query.aria': 'Exception query',
+    'exception.query.placeholder': 'Search and filter by resource attributes. Supports IN / NOT IN; press Enter to confirm.',
+    'exception.scope.aria': 'Exception scope',
+    'exception.scope.all': 'All exceptions',
+    'exception.scope.critical': 'Critical exceptions',
+    'exception.sort.aria': 'Exception sort',
+    'exception.sort.last-seen': 'Last seen',
+    'exception.sort.count': 'Count',
+    'exception.table.type': 'Exception type',
+    'exception.table.message': 'Error message',
+    'exception.table.count': 'Count',
+    'exception.table.last-seen': 'Last seen',
+    'exception.table.first-seen': 'First seen',
+    'exception.table.application': 'Application',
+    'exception.summary.groups': 'exception groups',
+    'exception.filters.deployment-environment': 'Deployment environment',
+    'exception.filters.service': 'Service',
+    'exception.filters.host': 'Host',
+    'exception.filters.k8s-cluster': 'K8s cluster',
+    'exception.filters.k8s-deployment': 'K8s deployment',
+    'exception.filters.k8s-namespace': 'K8s namespace',
+    'exception.filters.k8s-pod': 'K8s pod',
     'exception.recovery.overview.title': 'Overview',
     'exception.recovery.overview.copy': 'Return to the overview workspace.',
     'exception.recovery.back.title': 'Go back',
@@ -1054,37 +3510,57 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'exception.recovery.home.title': 'Home',
     'exception.recovery.home.copy': 'Open the default landing route.',
     'exception.rail.overview.title': 'Overview',
-    'exception.rail.overview.copy': '返回主工作台并重新选择入口。',
+    'exception.rail.overview.copy': 'Return to the main workspace and choose the entry again.',
     'exception.rail.logs.title': 'Logs',
-    'exception.rail.logs.copy': '确认后端响应和错误日志。',
+    'exception.rail.logs.copy': 'Confirm backend responses and error logs.',
     'exception.rail.traces.title': 'Traces',
-    'exception.rail.traces.copy': '排查链路异常和请求级失败。',
+    'exception.rail.traces.copy': 'Inspect trace anomalies and request-level failures.',
   },
   'zh-CN': {
     'common.active': '活跃',
+    'common.add': '添加',
     'common.annotation.bind': '注解',
     'common.api.message-nonzero': 'API 消息返回非零状态码',
     'common.api.request-failed-status': 'API 请求失败:{{status}}',
     'common.attention': '关注',
     'common.app': '应用',
     'common.application': 'application',
+    'common.boolean.true': '是',
+    'common.boolean.false': '否',
     'common.button.cancel': '取消',
+    'common.button.setting': '配置',
+    'common.button.save': '保存',
     'common.button.delete': '删除选中项',
+    'common.button.retry': '重试',
     'common.button.delete-batch': '批量删除',
+    'common.button.export': '导出',
+    'common.button.import': '导入',
     'common.button.help': '帮助',
     'common.button.ok': '确定',
+    'common.confirm.cancel': '请确认是否取消!',
+    'common.confirm.cancel-batch': '请确认是否批量取消!',
     'common.confirm.delete': '请确认是否删除!',
+    'common.confirm.delete-batch': '删除选中项?',
+    'common.confirm.enable': '请确认是否启用!',
+    'common.confirm.enable-batch': '请确认是否批量启用!',
+    'common.confirm.operation': '确认操作',
+    'validation.required': '请填充必填项! ',
     'common.copy-failed': '复制失败',
     'common.current-page-count': '当前页',
-    'common.current-view': 'Current view',
+    'common.current-view': '当前视图',
+    'common.status': '状态',
+    'common.dark-mode': '深色模式',
     'common.delete-failed': '删除失败',
     'common.delete-success': '删除成功',
     'common.description': '描述',
     'common.decrement': '减少',
+    'common.duration.seconds': '{{value}} 秒',
     'common.edit': '操作',
     'common.edit-time': '更新时间',
     'common.empty': '空',
     'common.no-data': '暂无数据',
+    'common.tab-navigation': '标签导航',
+    'common.timeline.empty': '暂无事件',
     'common.enable': '启用状态',
     'common.enabled': '已启用',
     'common.disabled': '已停用',
@@ -1093,36 +3569,93 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'common.idle': '空闲',
     'common.increment': '增加',
     'common.label': 'label',
+    'common.label.key': '标签名',
+    'common.label.value': '标签值',
     'common.labels': '标签',
     'common.load-failed': '加载失败',
     'common.mode': 'Mode',
     'common.next': '下一条',
+    'common.next-page': '下一页',
     'common.next-point': 'Next point',
     'common.next-series': 'Next series',
     'common.next-step': '下一步',
     'common.none': '无',
+    'common.yes': '是',
+    'common.no': '否',
+    'common.open': '打开',
     'common.previous': '上一条',
+    'common.previous-page': '上一页',
     'common.previous-point': 'Previous point',
     'common.previous-series': 'Previous series',
+    'common.processing': '处理中',
+    'common.remove': '删除',
     'common.loading': '加载中...',
+    'common.view-more': '查看更多',
     'common.apply': '应用',
+    'common.locale-code': 'zh-CN',
     'common.refresh': '刷新',
     'common.reset': '重置',
     'common.live': '实时',
     'common.scrape': '采集方式',
+    'common.tag.add': '添加标签',
+    'common.week.1': '星期一',
+    'common.week.2': '星期二',
+    'common.week.3': '星期三',
+    'common.week.4': '星期四',
+    'common.week.5': '星期五',
+    'common.week.6': '星期六',
+    'common.week.7': '星期日',
+    'common.month.1': '一月',
+    'common.month.2': '二月',
+    'common.month.3': '三月',
+    'common.month.4': '四月',
+    'common.month.5': '五月',
+    'common.month.6': '六月',
+    'common.month.7': '七月',
+    'common.month.8': '八月',
+    'common.month.9': '九月',
+    'common.month.10': '十月',
+    'common.month.11': '十一月',
+    'common.month.12': '十二月',
     '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-years': '上一页年份',
+    'time.range.next-years': '下一页年份',
+    'time.range.decrease': '减少',
+    'time.range.increase': '增加',
+    '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.preset.last-15m': '近 15 分钟',
     'time.range.preset.last-30m': '近 30 分钟',
     'time.range.preset.last-1h': '近 1 小时',
     'time.range.preset.last-6h': '近 6 小时',
     'time.range.preset.last-1d': '近 1 天',
+    'time.range.preset.last-24h': '近 24 小时',
     'time.range.preset.last-1w': '近 1 周',
     'time.range.preset.last-4w': '近 4 周',
     'time.range.preset.last-12w': '近 12 周',
@@ -1134,24 +3667,745 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'time.range.reset': '重置',
     'time.range.reset-aria': '重置时间范围',
     'time.range.relative-placeholder': '45m',
+    'ui-lab.time-range.simulate-zoom': '模拟图表缩放',
+    'ui-lab.time-range.apply-query': '应用为查询时间',
+    'ui-lab.time-range.previous-query': '上次查询: now-1h 到 now',
+    'ui-lab.time-range.release-window': '发布验证窗口',
     'common.search': '搜索',
+    'common.select-page': '选择当前页',
+    'common.select-all-results': '选择全部',
     'common.save': '保存',
     'common.save-failed': '保存失败',
     'common.save-success': '保存成功',
     'common.saving': '保存中...',
     'common.cancel': '取消',
+    'common.button.return': '返回',
     'common.failed': '失败',
+    'common.notice': '通知',
+    'common.unknown': '未知',
+    'menu.fullscreen': '全屏',
+    'menu.fullscreen.exit': '退出全屏',
+    'alert.notify.title': '新告警通知',
+    'alert.notify.body': '您有一条新的告警,请及时处理!',
+    'common.notify.apply-success': '应用成功',
+    'common.notify.apply-fail': '应用失败',
+    'common.notify.copy-success': '复制成功!',
+    'common.notify.copy-fail': '复制失败!',
+    'common.notify.query-fail': '查询失败!',
+    'common.notify.delete-success': '删除成功!',
+    'common.notify.delete-fail': '删除失败!',
+    'common.notify.mark-success': '标记成功!',
+    'common.notify.mark-fail': '标记失败!',
+    'common.notify.delete-pending': '正在删除选中监控',
+    'common.notify.edit-success': '修改成功!',
+    'common.notify.edit-fail': '修改失败!',
+    'common.notify.operate-success': '操作成功!',
+    'common.notify.operate-fail': '操作失败!',
+    'common.notify.enable-success': '启用成功',
+    'common.notify.enable-fail': '启用失败',
+    'common.notify.enable-pending': '正在启用选中监控',
+    'common.notify.cancel-success': '暂停成功',
+    'common.notify.cancel-fail': '暂停失败',
+    'common.notify.cancel-pending': '正在暂停选中监控',
+    'common.notify.export-success': '已开始导出',
+    'common.notify.export-fail': '导出失败',
+    'common.notify.export-pending': '正在准备导出',
+    'common.notify.export-fail-status': '导出失败:{{status}}',
+    'common.notify.import-submitted': '已提交导入 [{{taskName}}]',
+    'common.notify.import-fail': '导入失败',
+    'common.notify.import-pending': '正在准备导入',
+    'common.notify.import-fail-status': '导入失败:{{status}}',
+    'common.notify.import-success-detail': '导入 [{{taskName}}] 成功!',
+    'common.notify.import-fail-detail': '导入 [{{taskName}}] 失败:{{errMsg}}',
+    'common.notify.import-progress': '导入 [{{taskName}}] 进度:{{progress}}',
+    'common.notify.new-success': '新增成功!',
+    'common.notify.new-fail': '新增失败!',
+    'alert.notice.receiver.next': '下一步请配置您的[告警通知策略]!',
+    'common.notify.no-select-cancel': '请先选择要暂停的监控',
+    'common.notify.no-select-delete': '请先选择要删除的监控',
+    'common.notify.no-select-enable': '请先选择要启用的监控',
+    'common.notify.no-select-export': '未选择要导出的监控',
     'common.help': '帮助',
+    'common.dialog.close': '关闭对话框',
     'common.ready': '就绪',
     'common.signals': 'Signals',
+    'app.frame.utility.menu': '菜单',
+    'app.frame.utility.github': 'GitHub',
+    'app.frame.utility.user': '用户菜单:{{user}}',
+    'menu.extras.setting': '系统设置',
+    'menu.account.logout': '退出登录',
+    'menu.extras.about': '关于',
+    'app.login.need-change-password': '请及时更新初始默认密码!',
+    'ai.chat.launch': '打开 AI 助手',
+    'ai.chat.title': 'AI 助手',
+    'ai.chat.subtitle': 'HertzBeat 运维助手',
+    'ai.chat.conversations': '会话',
+    'ai.chat.new-chat': '新会话',
+    'ai.chat.conversation.loading': '正在加载会话...',
+    'ai.chat.conversation.ready': '会话列表已从 /chat/conversations 同步。',
+    'ai.chat.conversation.empty': '暂无历史会话,可在此工作台发起新对话。',
+    'ai.chat.conversation.error': '会话历史暂不可用,助手外壳仍可继续使用。',
+    'ai.chat.conversation.delete.title': '删除会话',
+    'ai.chat.conversation.delete.confirm': '删除',
+    'ai.chat.history.idle': '选择一个会话以加载历史消息。',
+    'ai.chat.history.loading': '正在加载选中会话历史...',
+    'ai.chat.history.ready': '选中会话历史已加载。',
+    'ai.chat.history.empty': '此会话暂无已保存消息。',
+    'ai.chat.history.error': '选中会话历史暂不可用,可尝试其他会话。',
+    'ai.chat.user': '用户',
+    'ai.chat.system': '系统',
+    'ai.chat.welcome.title': '询问 HertzBeat',
+    'ai.chat.welcome.description': '可以询问监控、告警、日志、链路、指标、拓扑和平台配置。',
+    'ai.chat.initial-message': '初始问题',
+    'ai.chat.assistant': '助手',
+    'ai.chat.offline.response': '对话外壳已就绪;流式响应证明会在下一迁移切片闭环。',
+    'ai.chat.typing': '正在输入...',
+    'ai.chat.error.processing': '处理您的请求时出错。请检查 AI 智能体服务是否正在运行,然后重试。',
+    'ai.chat.input.placeholder': '询问助手',
+    'ai.chat.input.hint': '在顶部输入框按 Enter 可打开助手工作台。',
+    'ai.chat.submit': '发送提示',
+    'ai.chat.modify-api-key': '修改 API 密钥',
+    'ai.chat.config.title': 'AI 提供商配置',
+    'ai.chat.config.required.content': '要使用 AI 智能体聊天,请配置您的模型提供商。',
+    'ai.chat.config.provider': 'AI 提供商',
+    'ai.chat.config.provider.help': '选择您的 AI 提供商(OpenAI、智谱或 ZAI)',
+    'ai.chat.config.provider.required': '请选择 AI 提供商',
+    'ai.chat.config.api-key': 'API 密钥',
+    'ai.chat.config.api-key.help': '您的提供商 API 密钥。保存时将验证密钥。',
+    'ai.chat.config.api-key.required': 'API 密钥是必需的',
+    'ai.chat.config.base-url': '基础 URL',
+    'ai.chat.config.base-url.help': '自定义 API 端点 URL。留空则使用所选提供商的默认值。',
+    'ai.chat.config.base-url.required': '基础 URL 是必需的',
+    'ai.chat.config.model': '模型',
+    'ai.chat.config.model.help': '要使用的模型名称。留空则使用所选提供商的默认值。',
+    'ai.chat.config.model.required': '模型是必需的',
+    'ai.chat.config.reset': '重置为默认值',
+    'ai.chat.config.save': '保存',
+    'ai.chat.config.cancel': '取消',
+    'ai.chat.config.idle': '提供商配置等待加载。',
+    'ai.chat.config.loading': '正在加载提供商配置...',
+    'ai.chat.config.ready': '提供商配置已从 /config/provider 加载。',
+    'ai.chat.config.saving': '正在保存提供商配置...',
+    'ai.chat.config.save.success': '模型提供商配置保存成功!',
+    'ai.chat.config.save.failed': '保存配置失败:',
+    'ai.chat.config.validation.failed': 'API 密钥验证失败:',
+    'ai.chat.schedule.title': '定时任务设置',
+    'ai.chat.schedule.create': '创建定时任务',
+    'ai.chat.schedule.add': '添加新任务',
+    'ai.chat.schedule.configured': '已配置的定时任务',
+    'ai.chat.schedule.skill': '技能',
+    'ai.chat.schedule.skill.select': '选择技能',
+    'ai.chat.schedule.skill.placeholder': '选择要执行的技能',
+    'ai.chat.schedule.cron': '执行时间',
+    'ai.chat.schedule.cron.label': '执行时间 (Cron表达式)',
+    'ai.chat.schedule.cron.help': '格式: 秒 分 时 日 月 周',
+    'ai.chat.schedule.cron.example.daily': '示例: 0 0 9 * * ? (每天9点)',
+    'ai.chat.schedule.cron.example.weekly': '示例: 0 0 9 ? * MON (每周一9点)',
+    'ai.chat.schedule.cron.common': '常用: 每天9点',
+    'ai.chat.schedule.cron.monday': '每周一9点',
+    'ai.chat.schedule.status': '状态',
+    'ai.chat.schedule.action': '操作',
+    'ai.chat.schedule.create.success': '定时任务创建成功',
+    'ai.chat.schedule.create.failed': '创建定时任务失败',
+    'ai.chat.schedule.delete.success': '定时任务已删除',
+    'ai.chat.schedule.delete.failed': '删除定时任务失败',
+    'ai.chat.schedule.toggle.success': '定时任务状态已更新',
+    'ai.chat.schedule.toggle.failed': '更新定时任务状态失败',
+    'ai.chat.schedule.push': '定时推送',
+    'ai.chat.schedule.button': '定时任务',
+    'ai.chat.schedule.tooltip': '配置定时推送任务',
+    'ai.chat.schedule.edit': '编辑定时任务',
+    'ai.chat.schedule.update.success': '定时任务更新成功',
+    'ai.chat.schedule.update.failed': '更新定时任务失败',
+    'ai.chat.schedule.idle': '定时任务配置等待加载。',
+    'ai.chat.schedule.loading': '正在加载定时任务和技能...',
+    'ai.chat.schedule.ready': '定时任务已从 /ai/schedule 加载。',
+    'ai.chat.schedule.empty': '此会话暂无已配置的定时任务。',
+    'ai.chat.schedule.saving': '正在保存定时任务...',
+    'ai.chat.schedule.error': '定时任务加载失败。',
+    'platform.footer.license': '遵循 Apache License, Version 2.0 授权',
+    'collector.health.cluster.title': '采集集群健康',
+    'collector.health.cluster.copy': '采集器 {{online}} / {{total}} 在线',
+    'collector.health.cluster.meta': '任务 {{tasks}} · 离线 {{offline}}',
+    'collector.health.cluster.freshness': '最近上报 {{time}}',
+    'collector.health.monitor.title': '采集健康',
+    'collector.health.monitor.copy': '监控 {{healthy}} / {{total}} 健康',
+    'collector.health.monitor.empty-copy': '暂无绑定监控',
+    'collector.health.monitor.meta': '采集证据已归并',
+    'collector.health.monitor.empty-meta': '等待模板绑定',
+    'collector.health.monitor.freshness': '最近证据 {{time}}',
     'entities.detail.loading': '加载实体详情...',
+    'entities.detail.title.fallback': '实体详情',
+    'entities.detail.fallback.display-name': '实体 {{id}}',
+    'entities.detail.fallback.draft-name': '实体草稿',
+    'entities.detail.fallback.description': '后端实体详情暂不可用时展示的临时工作台。',
+    'entities.detail.subtitle.active-alerts': '当前实体已有活跃告警,优先确认告警和最近变化。',
+    'entities.detail.subtitle.down-monitors': '已有绑定监控异常,先从监控证据确认影响面。',
+    'entities.detail.subtitle.has-evidence': '日志、链路或监控证据已关联,可继续排查。',
+    'entities.detail.subtitle.needs-context': '先补齐负责人、定义和遥测绑定,让实体成为可用工作台。',
+    'entities.detail.metric.bound-monitors': '绑定监控',
+    'entities.detail.metric.active-alerts': '活跃告警',
+    'entities.detail.metric.identities': '身份标识',
+    'entities.detail.metric.relations': '关联关系',
+    'entities.detail.fact.id': '实体 ID',
+    'entities.detail.fact.type': '类型',
+    'entities.detail.fact.status': '状态',
+    'entities.detail.fact.owner': '负责人',
+    'entities.detail.status.healthy': '健康',
+    'entities.detail.status.unknown': '未知',
+    'entities.detail.status.abnormal': '异常',
+    'entities.detail.status.warning': '告警',
+    'entities.detail.overview.status': '状态',
+    'entities.detail.overview.owner': '负责人',
+    'entities.detail.overview.environment': '环境',
+    'entities.detail.overview.description': '描述',
+    'entities.detail.summary.metrics.title': '关联指标',
+    'entities.detail.summary.metrics.bound': '个绑定监控',
+    'entities.detail.summary.metrics.down': '个异常监控',
+    'entities.detail.summary.metrics.no-down': '暂无异常监控',
+    'entities.detail.summary.logs.title': '关联日志',
+    'entities.detail.summary.logs.hints': '条查询线索',
+    'entities.detail.summary.logs.available': '{{count}}可用',
+    'entities.detail.summary.logs.no-hints': '暂无查询线索',
+    'entities.detail.summary.traces.title': '关联链路',
+    'entities.detail.summary.traces.recent': '条近期链路',
+    'entities.detail.summary.traces.errors': '条错误链路',
+    'entities.detail.summary.traces.no-errors': '暂无错误链路',
+    'entities.detail.handoff.metrics.title': '关联指标',
+    'entities.detail.handoff.metrics.meta': '指标工作台',
+    'entities.detail.handoff.logs.title': '关联日志',
+    'entities.detail.handoff.logs.meta': '日志工作台',
+    'entities.detail.handoff.traces.title': '关联链路',
+    'entities.detail.handoff.traces.meta': '链路工作台',
+    'entities.detail.handoff.alerts.title': '告警规则',
+    'entities.detail.handoff.alerts.meta': '阈值规则',
+    'entities.detail.handoff.monitors.title': '绑定监控',
+    'entities.detail.handoff.monitors.meta': '监控对象',
+    'entities.detail.handoff.topology.title': '上下游拓扑',
+    'entities.detail.handoff.topology.meta': '关系图',
+    'entities.detail.handoff.template.title': '模板绑定',
+    'entities.detail.handoff.template.meta': '监控模板',
+    'entities.detail.health.latency.no-data': '暂无链路延迟',
+    'entities.detail.health.latency.waiting': '等待 OTLP Span',
+    'entities.detail.health.latency.meta': '链路延迟',
+    'entities.detail.health.latency.title': '延迟',
+    'entities.detail.health.score.title': '健康评分',
+    'entities.detail.health.score.meta': '轻量健康模型',
+    'entities.detail.health.availability.title': '可用性',
+    'entities.detail.health.availability.waiting': '等待监控模板',
+    'entities.detail.health.availability.meta': '{{healthy}} / {{total}} 监控健康',
+    'entities.detail.health.availability.no-monitors': '暂无绑定监控',
+    'entities.detail.health.error-rate.title': '错误率',
+    'entities.detail.health.error-rate.no-samples': '暂无链路样本',
+    'entities.detail.health.error-rate.meta': '{{errors}} / {{total}} 错误链路',
+    'entities.detail.health.error-rate.waiting': '等待链路上报',
+    'entities.detail.health.alerts.title': '当前告警',
+    'entities.detail.health.alerts.copy': '{{count}} 个活跃告警',
+    'entities.detail.health.alerts.meta': '告警闭环',
+    'entities.detail.health.anomalies.title': '最近异常',
+    'entities.detail.health.anomalies.copy': '{{count}} 个异常线索',
+    'entities.detail.health.anomalies.meta': '监控 {{monitors}} · 链路 {{traces}} · 日志 {{logs}}',
+    'entities.detail.health.collector.title': '采集健康',
+    'entities.detail.evidence.coverage.title': '证据覆盖',
+    'entities.detail.evidence.coverage.copy': '{{count}} 个活跃信号',
+    'entities.detail.evidence.coverage.waiting': '等待后端证据',
+    'entities.detail.evidence.coverage.no-model': '暂无统一读模型',
+    'entities.detail.evidence.coverage.no-signals': '暂无活跃信号',
+    'entities.detail.evidence.metrics.title': '指标 / USE',
+    'entities.detail.evidence.metrics.copy': '{{count}} 条指标证据',
+    'entities.detail.evidence.metrics.meta': '监控与指标读模型',
+    'entities.detail.evidence.logs.title': '日志 / RED',
+    'entities.detail.evidence.logs.copy': '{{count}} 条日志线索',
+    'entities.detail.evidence.logs.meta': '日志读模型',
+    'entities.detail.evidence.traces.title': '链路 / RED',
+    'entities.detail.evidence.traces.copy': '{{count}} 条最近链路',
+    'entities.detail.evidence.traces.meta': '链路读模型',
+    'entities.detail.evidence.traces.errors': '{{count}} 条错误链路',
+    'entities.detail.evidence.latest.title': '最近观测',
+    'entities.detail.evidence.latest.waiting': '等待证据时间戳',
+    'entities.detail.evidence.latest.meta': '后端读模型',
+    'entities.detail.evidence-handoff.alerts.title': '活跃告警证据',
+    'entities.detail.evidence-handoff.alerts.copy': '{{count}} 个活跃告警',
+    'entities.detail.evidence-handoff.alerts.meta': '打开告警处置',
+    'entities.detail.evidence-handoff.topology.title': '拓扑上下文',
+    'entities.detail.evidence-handoff.topology.default-relation': '相关',
+    'entities.detail.evidence-handoff.topology.unknown-target': '未知目标',
+    'entities.detail.evidence-handoff.runbook.title': '处置手册',
+    'entities.detail.evidence-handoff.runbook.meta': '响应指引',
+    'entities.detail.action-text.open-monitors': '打开监控',
+    'entities.detail.action-text.open-discovery': '打开发现',
+    'entities.detail.action-text.open-definition': '打开定义',
+    'entities.detail.action-text.inspect-abnormal': '先检查异常监控。',
+    'entities.detail.action-text.add-evidence': '先补充更多证据。',
+    'entities.detail.action-text.review-definition': '先检查定义工作台,再补齐归属和证据。',
+    'entities.detail.action-text.next-action': '下一步动作',
+    'entities.detail.action-text.server-guidance': '服务端建议',
+    'entities.detail.drilldown.definition.title': '定义工作台',
+    'entities.detail.drilldown.edit.title': '编辑实体',
+    'entities.detail.drilldown.discovery.title': '遥测发现',
+    'entities.detail.drilldown.next-route': '下一步路由',
+    'entities.detail.drilldown.shared-route': '共享路由',
+    'entities.detail.alert-row.empty.title': '当前告警',
+    'entities.detail.alert-row.empty.copy': '暂无活跃告警',
+    'entities.detail.alert-row.empty.meta': '{{count}} 个活跃',
+    'entities.detail.alert-row.active.title': '当前告警 #{{id}}',
+    'entities.detail.alert-row.pending.copy': '告警待处理',
+    'entities.detail.relationship-row.empty.title': '上下游关系',
+    'entities.detail.relationship-row.empty.copy': '暂无关系数据',
+    'entities.detail.relationship-row.empty.meta': '等待拓扑归并',
+    'entities.detail.relationship-row.unknown-target': '未知目标',
+    'entities.detail.relationship-row.default-meta': '上下游关系',
+    'entities.detail.attribution.traditional-monitor.title': '传统监控绑定',
+    'entities.detail.attribution.monitor-bind.count': '{{count}} 个绑定',
+    'entities.detail.attribution.traditional-monitor.waiting-meta': '等待监控模板',
+    'entities.detail.attribution.otlp.title': 'OTLP 归因',
+    'entities.detail.attribution.identity.count': '{{count}} 个身份',
+    'entities.detail.attribution.identity.missing': '缺少身份',
+    'entities.detail.attribution.otlp.waiting-meta': '等待 hertzbeat.entity_id 或 service.name',
+    'entities.detail.attribution.candidate.title': '候选确认',
+    'entities.detail.attribution.candidate.assigned': '已归入实体',
+    'entities.detail.attribution.candidate.pending': '待确认',
+    'entities.detail.attribution.candidate.discover-meta': '进入遥测发现确认候选',
+    'entities.detail.attribution.diagnostic.title': '归因诊断',
+    'entities.detail.attribution.diagnostic.complete': '归因证据完整',
+    'entities.detail.attribution.diagnostic.incomplete': '归因证据不完整',
+    'entities.detail.attribution.diagnostic.missing': '需要补齐归因',
+    'entities.detail.attribution.diagnostic.complete-meta': '可进入对象详情',
+    'entities.detail.attribution.diagnostic.missing-part.identity': '身份标识',
+    'entities.detail.attribution.diagnostic.missing-part.monitor-bind': '监控绑定',
+    'entities.detail.attribution.diagnostic.missing-meta': '缺少{{parts}}',
+    'entities.detail.collection.source.title': '采集来源',
+    'entities.detail.collection.source.meta': '实体来源',
+    'entities.detail.collection.identity.title': '身份标识',
+    'entities.detail.collection.identity.count': '{{count}} 个身份',
+    'entities.detail.collection.template.title': '模板绑定',
+    'entities.detail.collection.template.count': '{{count}} 个绑定',
+    'entities.detail.collection.labels.title': '标签',
+    'entities.detail.collection.labels.count': '{{count}} 个标签',
+    'entities.detail.header.kicker': '对象优先调查',
+    'entities.detail.header.badge': '实体详情',
+    'entities.detail.action.all-entities': '全部实体',
+    'entities.detail.action.edit-definition': '编辑定义',
+    'entities.detail.action.delete': '删除',
+    'entities.detail.action.edit': '编辑',
+    'entities.detail.error.title': '操作未完成',
+    'entities.detail.panel.overview.title': '上下文',
+    'entities.detail.panel.overview.copy': '先确认实体身份、归属和运行状态,再进入信号工作台。',
+    'entities.detail.panel.context.title': '观测上下文',
+    'entities.detail.panel.context.copy': '所有跳转都携带实体、服务、环境和时间范围,链路相关入口会继续带 trace/span。',
+    'entities.detail.panel.health.title': '轻量健康模型',
+    'entities.detail.panel.health.copy': '用可用性、错误率、延迟、当前告警、最近异常和采集健康形成轻量评分,暂不展开 SLO 编排。',
+    'entities.detail.panel.evidence.title': '证据模型',
+    'entities.detail.panel.evidence.copy': '进入各信号工作台前,先露出后端 RED / USE 读模型覆盖。',
+    'entities.detail.panel.alerts.title': '当前告警',
+    'entities.detail.panel.alerts.copy': '先确认正在影响该实体的告警,再决定静默、抑制、分组或通知闭环。',
+    'entities.detail.panel.next.title': '下一步',
+    'entities.detail.panel.next.copy': '保留服务端建议,但用当前冷峻控制台语言收口。',
+    'entities.detail.panel.related.title': '相关信号',
+    'entities.detail.panel.related.copy': '用监控、日志和链路快速判断影响范围。',
+    'entities.detail.panel.relationships.title': '上下游关系',
+    'entities.detail.panel.relationships.copy': '关系来自实体归并、调用链、监控对象归属或 CMDB 标签,后续会进入拓扑影响面。',
+    'entities.detail.panel.source.title': '采集来源',
+    'entities.detail.panel.source.copy': '确认这个实体来自 HertzBeat 监控对象、Collector、OTLP 资源属性还是手工/CMDB 标签。',
+    'entities.detail.panel.drilldown.title': '高级入口',
+    'entities.detail.panel.drilldown.copy': '只保留真正有用的深层入口,不增加右侧统计装饰。',
+    'entities.detail.disposition.title': '处置节奏',
+    'entities.detail.disposition.copy': '告警、监控和日志入口从这里分流;详情页只做对象上下文,不复制目录页的右侧统计栏。',
+    'entities.detail.delete.title': '确认删除实体',
+    'entities.detail.delete.kicker': '对象目录',
+    'entities.detail.delete.cancel': '取消',
+    'entities.detail.delete.confirm': '确认删除',
+    'entities.detail.delete.copy': '删除后实体会从对象目录移除,相关监控绑定、遥测上下文和告警排查入口需要重新归并。',
     'entities.detail.delete.failed': '删除实体失败。',
+    'entities.detail.attribution.ready': '已归因',
+    'entities.detail.attribution.review': '待确认',
+    'entities.detail.attribution.missing': '缺失',
+    'entities.editor.submit.create': '创建实体',
+    'entities.editor.stage.basic.label': '基本信息',
+    'entities.editor.stage.basic.description': '先确认类型、名称和核心元数据。',
+    'entities.editor.stage.ownership.label': '归属与处置',
+    'entities.editor.stage.ownership.description': '补齐负责人、联系人和处置入口。',
+    'entities.editor.stage.signals.label': '证据关联',
+    'entities.editor.stage.signals.description': '绑定监控、身份标识和遥测证据。',
+    'entities.editor.stage.relations.label': '关系与扩展',
+    'entities.editor.stage.relations.description': '记录上下游关系、标签和扩展字段。',
+    'entities.editor.route-tab.detail': '实体详情',
+    'entities.editor.route-tab.monitor': '监控中心',
+    'entities.editor.route-tab.logs': '日志管理与统计',
+    'entities.editor.route-tab.traces': '链路',
+    'entities.editor.shell.kicker': '实体',
+    'entities.editor.shell.title.new': '新建实体',
+    'entities.editor.shell.title.edit': '编辑实体',
+    'entities.editor.shell.copy.new': '先在页面中建立实体记录,再逐步补齐证据、负责人和依赖关系。',
+    'entities.editor.shell.copy.edit': '先在页面中建立实体记录,再逐步补充证据、负责人和依赖关系。',
+    'entities.editor.shell.freshness.new': '0% 最近更新',
+    'entities.editor.shell.freshness.edit': '75% 最近更新',
+    'entities.editor.shell.all-entities': '全部实体',
+    'entities.editor.shell.definition-workspace': '定义工作区',
+    'entities.editor.shell.entity-label': '实体',
+    'entities.editor.shell.preview.show': '显示快照',
+    'entities.editor.shell.preview.hide': '隐藏快照',
+    'entities.editor.shell.definition-version': '定义版本 · HertzBeat v1 / OTel 资源规范',
+    'entities.editor.shell.name-label': '名称',
+    'entities.editor.shell.name-placeholder': '实体唯一名称',
+    'entities.editor.shell.type-label': '类型',
+    'entities.editor.shell.entry-source-label': '录入来源',
+    'entities.editor.shell.stage-label': '编辑阶段',
+    'entities.editor.shell.preview-rail.title': '实体元数据',
+    'entities.editor.shell.preview-rail.copy': '查看或粘贴定义,应用到表单。',
+    'entities.editor.shell.definition-content.title': '定义内容',
+    'entities.editor.shell.definition-content.copy': '查看或粘贴定义,应用到表单。',
+    'entities.editor.mode.editor': '表单',
+    'entities.editor.type.system.label': '系统',
+    'entities.editor.type.system.description': '业务域或产品线',
+    'entities.editor.type.service.label': '服务',
+    'entities.editor.type.service.description': '对外提供业务能力的应用或接口',
+    'entities.editor.type.host.label': '主机',
+    'entities.editor.type.host.description': '承载服务的运行节点',
+    'entities.editor.type.database.label': '数据库',
+    'entities.editor.type.database.description': '数据持久化实例',
+    'entities.editor.type.queue.label': '队列',
+    'entities.editor.type.queue.description': '异步消息通道',
+    'entities.editor.type.middleware.label': '中间件',
+    'entities.editor.type.middleware.description': '共享基础组件',
+    'entities.editor.type.api.label': 'API',
+    'entities.editor.type.api.description': '接口或网关资源',
+    'entities.editor.type.endpoint.label': '端点',
+    'entities.editor.type.endpoint.description': '可观测访问入口',
+    'entities.editor.type.device.label': '设备',
+    'entities.editor.type.device.description': '网络或边缘设备',
+    'entities.editor.type.k8s-workload.label': 'K8s 工作负载',
+    'entities.editor.type.k8s-workload.description': 'K8s 原生工作负载',
+    'entities.editor.entry-source.manual': '页面录入',
+    'entities.editor.entry-source.telemetry': '遥测发现',
+    'entities.editor.entry-source.definition': '定义优先',
+    'entities.editor.telemetry-handoff.title': '遥测发现',
+    'entities.editor.telemetry-handoff.copy': '先通过监控和 OTel 资源字段识别实体,再补充基础信息。',
+    'entities.editor.telemetry-handoff.monitor-count': '{{count}} 个监控绑定',
+    'entities.editor.telemetry-handoff.identity-count': '{{count}} 个身份标识',
+    'entities.editor.telemetry-handoff.attribution-check': '归因检查',
+    'entities.editor.field.display-name': '显示名称',
+    'entities.editor.field.namespace': '命名空间',
+    'entities.editor.field.environment': '环境',
+    'entities.editor.field.subtype': '子类型',
+    'entities.editor.field.owner': '负责人',
+    'entities.editor.field.system': '所属系统',
+    'entities.editor.field.source': '来源',
+    'entities.editor.field.description': '描述',
+    'entities.editor.field.description-placeholder': '例如负责交易结算和支付编排。',
+    'entities.editor.field.runbook': '运行手册',
+    'entities.editor.placeholder.display-name': '例如 Checkout API',
+    'entities.editor.placeholder.namespace': '例如 commerce',
+    'entities.editor.placeholder.environment': '例如 prod',
+    'entities.editor.placeholder.subtype': '例如 http',
+    'entities.editor.placeholder.owner': '例如 payments-team',
+    'entities.editor.placeholder.system': '例如 website',
+    'entities.editor.placeholder.source': 'manual',
+    'entities.editor.contact.title': '联系人',
+    'entities.editor.contact.add': '新增联系人',
+    'entities.editor.contact.name-placeholder': '联系人',
+    'entities.editor.contact.type-placeholder': '类型',
+    'entities.editor.contact.value-placeholder': '联系方式',
+    'entities.editor.contact.contact-placeholder': '联系字段',
+    'entities.editor.link.title': '链接',
+    'entities.editor.link.add': '新增链接',
+    'entities.editor.link.name-placeholder': '链接名称',
+    'entities.editor.link.type-placeholder': '类型',
+    'entities.editor.link.provider-placeholder': '提供方',
+    'entities.editor.link.url-placeholder': '链接地址',
+    'entities.editor.collection.identities': '身份标识',
+    'entities.editor.collection.identities.add': '新增身份标识',
+    'entities.editor.collection.monitor-binds': '监控绑定',
+    'entities.editor.collection.monitor-binds.add': '新增监控绑定',
+    'entities.editor.collection.relations': '关系',
+    'entities.editor.collection.relations.add': '新增关系',
+    'entities.editor.relation.component-of': '上级组件',
+    'entities.editor.relation.components': '下级组件',
+    'entities.editor.relation.implemented-by': '实现服务',
+    'entities.editor.relation.languages': '开发语言',
+    'entities.editor.relation.component-of-placeholder': 'payment-platform, commerce',
+    'entities.editor.relation.components-placeholder': 'checkout-ui, order-router',
+    'entities.editor.relation.implemented-by-placeholder': 'checkout-api, order-worker',
+    'entities.editor.relation.languages-placeholder': 'java, typescript',
+    'entities.editor.relation.labels': '标签',
+    'entities.editor.relation.label-key': '标签键',
+    'entities.editor.relation.label-value': '标签值',
+    'entities.editor.relation.label-add': '新增标签',
+    'entities.editor.definition.aria.yaml': 'YAML 定义快照',
+    'entities.editor.definition.aria.json': 'JSON 定义快照',
+    'entities.editor.attribution.state.ready': '已就绪',
+    'entities.editor.attribution.state.missing': '需补齐',
+    'entities.editor.attribution.state.review': '待确认',
+    'entities.editor.attribution.identity.title': '身份标识',
+    'entities.editor.attribution.identity.count': '{{count}} 个身份标识',
+    'entities.editor.attribution.identity.missing': '缺少身份标识',
+    'entities.editor.attribution.identity.waiting-meta': '等待 service.name 或 hertzbeat.entity_id',
+    'entities.editor.attribution.identity.recognized-meta': '已识别身份',
+    'entities.editor.attribution.monitor.title': '监控绑定',
+    'entities.editor.attribution.monitor.count': '{{count}} 个监控绑定',
+    'entities.editor.attribution.monitor.waiting-meta': '等待监控对象或模板绑定',
+    'entities.editor.attribution.monitor.existing-meta': '已有监控绑定',
+    'entities.editor.attribution.owner.title': '负责人',
+    'entities.editor.attribution.owner.missing': '缺少负责人',
+    'entities.editor.attribution.owner.ready-meta': '可追责',
+    'entities.editor.attribution.owner.missing-meta': '先补负责人或值班组',
+    'entities.editor.attribution.system-environment.title': '系统与环境',
+    'entities.editor.attribution.system-environment.missing-environment': '{{system}} · 缺少环境',
+    'entities.editor.attribution.system-environment.missing-system': '缺少系统 · {{environment}}',
+    'entities.editor.attribution.system-environment.missing-both': '缺少系统、环境',
+    'entities.editor.attribution.system-environment.meta': '用于告警收敛和拓扑',
+    'entities.editor.attribution.discovery.title': '发现回路',
+    'entities.editor.attribution.discovery.copy': '可回到遥测发现',
+    'entities.new.loading': '正在加载实体草稿',
+    'entities.edit.loading': '正在加载实体编辑器',
+    'entities.definition.loading': '编辑实体定义',
+    'entities.definition.workspace.kicker': '实体定义',
+    'entities.definition.workspace.title': '编辑实体定义',
+    'entities.definition.workspace.subtitle': '直接编辑当前定义。保存后会同步更新基础信息、遥测绑定和依赖。',
+    'entities.definition.workspace.format-help': '支持 YAML、JSON 和 cURL 请求内容,保存前可先预览解析结果。',
+    'entities.definition.workspace.attribution-check': '归因检查',
+    'entities.definition.workspace.attribution-state.ready': '已就绪',
+    'entities.definition.workspace.attribution-state.missing': '需补齐',
+    'entities.definition.workspace.attribution-state.unknown': '待确认',
+    'entities.definition.workspace.metric.ready': '可直接导入',
+    'entities.definition.workspace.metric.attention': '待完善',
+    'entities.definition.workspace.metric.telemetry-gaps': '缺少遥测绑定',
+    'entities.definition.workspace.fact.workspace': '工作区',
+    'entities.definition.workspace.fact.format': '格式',
+    'entities.definition.workspace.fact.activities': '活动',
+    'entities.definition.workspace.fact.templates': '模板',
+    'entities.definition.workspace.current-definition': '当前定义',
+    'entities.definition.workspace.template-title': '自定义模板',
+    'entities.definition.workspace.template-error-help': '定义加载失败后可先确认实体是否存在,再继续编辑。',
+    'entities.definition.workspace.template-empty-title': '暂无模板',
+    'entities.definition.workspace.template-empty-copy': '可复用模板会显示在这里。',
+    'entities.definition.workspace.batch-title': '模板与批量编辑',
+    'entities.definition.workspace.batch-copy': '查看模板、草稿来源和可选操作。',
+    'entities.definition.workspace.batch-expand': '展开',
+    'entities.definition.workspace.activity-title': '最近活动',
+    'entities.definition.workspace.activity-empty-title': '暂无最近活动',
+    'entities.definition.workspace.activity-empty-copy': '预览和导入活动会显示在这里。',
+    'entities.definition.workspace.entry-title': '入口',
+    'entities.definition.workspace.entry-view-entity': '查看实体',
+    'entities.definition.workspace.entry-back-to-form': '回到表单',
+    'entities.definition.message.load-failed': '加载实体定义失败。',
+    'entities.definition.message.entity-not-exist': '实体不存在。',
+    'entities.definition.message.process-failed': '实体定义处理失败。',
+    'entities.definition.message.update-missing-entity': '无法确认要更新的实体定义。',
+    'entities.definition.message.update-failed': '实体定义保存失败。',
+    'entities.definition.message.updated': '实体定义已更新。',
+    'entities.definition.message.backend-fallback': '后端返回:{{message}}',
+    'entity.definition.format.label': '导入格式',
+    'entity.definition.format.option.yaml': 'YAML',
+    'entity.definition.format.option.json': 'JSON',
+    'entity.definition.format.option.curl': 'cURL',
+    'entity.definition.action.clear-draft': '清空草稿',
+    'entity.definition.action.preview': '预览定义',
+    'entity.definition.action.save': '保存定义',
+    'entity.definition.action.import': '导入实体',
+    'entity.definition.import.empty-preview': '请先粘贴定义,再预览。',
+    'entity.definition.import.empty-save': '请先粘贴或加载定义,再保存。',
+    'entity.definition.import.empty-import': '请先粘贴定义,再导入。',
+    'entity.definition.import.activity.preview-one': '已预览 1 个定义。',
+    'entity.definition.import.activity.preview-many': '已预览 {{count}} 个定义。',
+    'entity.definition.import.parse-failed': '定义解析失败。',
+    'entity.definition.workspace.single-required': '编辑已有实体时一次只能保存一个定义文档。',
+    'entity.definition.import.blocked': '部分定义仍需处理,请检查后再导入。',
+    'entity.definition.import.success-bundle': '已导入 {{count}} 个实体。',
+    'entity.definition.import.success-single': '实体定义已导入。',
+    'entity.definition.import.create-failed': '实体导入失败。',
+    'entity.definition.import.format': '导入格式',
+    'entity.definition.import.format.unknown': '未知格式 {{format}}',
+    'entity.definition.workspace.template.custom': '自定义模板',
+    'entity.field.owner': '负责人',
+    'entity.field.system': '所属系统',
+    'entity.field.implemented-by': '由以下实现',
+    'entity.field.runbook': '处置手册',
+    'entity.definition.import.gap.telemetry': '遥测绑定',
+    'entity.definition.import.telemetry.ready': '已识别遥测绑定',
+    'entity.definition.import.telemetry.pending': '待补充遥测绑定',
+    'entity.definition.import.validation.ready': '可直接导入',
+    'entity.definition.import.validation.needs-attention': '导入前待补充',
+    'entity.definition.import.kind.unknown': '未知类型 {{kind}}',
+    'entity.definition.import.source.unknown': '未知来源 {{source}}',
+    'entity.definition.import.summary.ready': '可直接导入',
+    'entity.definition.import.summary.attention': '待补充',
+    'entity.definition.import.summary.telemetry': '缺少遥测绑定',
+    'entity.definition.import.queue.ready.title': '可直接导入',
+    'entity.definition.import.queue.ready.copy': '这批定义已经满足基本要求,可直接导入或保存。',
+    'entity.definition.import.queue.ready.action': '查看可直接导入项',
+    'entity.definition.import.queue.attention.title': '需要先补齐',
+    'entity.definition.import.queue.attention.copy': '这批定义还缺负责人、系统、处置手册或接口信息,先补充再导入。',
+    'entity.definition.import.queue.attention.action': '查看待补齐项',
+    'entity.definition.import.queue.telemetry.title': '导入后还需补充遥测',
+    'entity.definition.import.queue.telemetry.copy': '这批定义还没有遥测绑定,导入后仍需到遥测发现补充相关信息。',
+    'entity.definition.import.queue.telemetry.action': '查看缺遥测项',
+    'entity.definition.import.placeholder.yaml': '粘贴 YAML 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。',
+    'entity.definition.import.placeholder.json': '粘贴 JSON 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。',
+    'entity.definition.import.placeholder.curl': '粘贴从预览或自动化流程生成的 cURL 请求。',
+    'entity.definition.import.attribution.ready': '归因完整',
+    'entity.definition.import.attribution.missing': '归因缺失',
+    'entity.definition.import.attribution.review': '归因待确认',
+    'entity.definition.import.activity.status.success': '成功',
+    'entity.definition.import.activity.status.saved': '已保存',
+    'entity.definition.import.activity.status.starter': '初始',
+    'entity.definition.import.activity.status.failed': '失败',
+    'entity.definition.import.activity.status.preview': '预览',
+    'entity.definition.import.activity.status.unknown': '未知状态 {{status}}',
+    'entity.definition.import.activity.summary.telemetry-discovery-applied': '遥测发现已应用',
+    'entity.definition.import.activity.summary.catalog-entity-updated': '目录实体已更新',
+    'entity.definition.import.activity.summary.catalog-entity-created': '目录实体已创建',
+    'entity.definition.import.activity.field.endpoint': '端点',
+    'entity.definition.import.activity.field.service': '服务',
+    'entity.definition.import.activity.field.system': '系统',
+    'entity.definition.import.activity.field.source': '来源',
+    'entity.definition.import.activity.field.owner': '负责人',
+    'entity.definition.import.activity.field.environment': '环境',
+    'entity.definition.import.activity.field.evidence': '证据',
+    'entity.definition.import.activity.field.monitor-binds': '监控绑定',
+    'entity.definition.import.template.source.workspace': '工作区',
+    'entity.definition.import.template.source.saved': '已保存',
+    'entity.definition.import.template.source.starter': '初始',
+    'entity.definition.import.template.source.custom': '自定义',
+    'entity.definition.import.template.source.unknown': '未知来源 {{source}}',
+    'entities.import.loading': '导入实体定义',
+    'entities.import.workspace.title': '导入实体定义',
+    'entities.import.workspace.subtitle': '从 YAML、JSON 或 cURL 请求内容导入一个或多个实体定义,先预览再写入目录。',
+    'entities.import.workspace.format-help': '支持 YAML、JSON 和从预览或自动化流程生成的 cURL。导入前会先解析实体基础信息、遥测绑定和依赖关系。',
+    'entities.discovery.loading': '遥测发现',
+    'entities.discovery.workspace.kicker': '对象优先调查',
+    'entities.discovery.workspace.title': '遥测发现',
+    'entities.discovery.workspace.subtitle': '先搜索一组需要治理的监控线索,再决定归并、完善归属,还是送入定义工作台继续收口。',
+    'entities.discovery.action.import': '从定义创建',
+    'entities.discovery.action.create': '创建实体',
+    'entities.discovery.action.catalog': '对象目录',
+    'entities.discovery.action.search': '搜索',
+    'entities.discovery.action.clear': '清空',
+    'entities.discovery.search.placeholder': '搜索监控名称或实例',
+    'entities.discovery.search.error': '遥测发现查询失败',
+    'entities.discovery.candidate.title': 'OTLP 候选实体',
+    'entities.discovery.candidate.copy': '保留 OTLP 资源身份,确认后再写入对象目录。',
+    'entities.discovery.candidate.action.create': '创建实体草稿',
+    'entities.discovery.candidate.identity': '资源身份',
+    'entities.discovery.candidate.namespace': '命名空间',
+    'entities.discovery.candidate.environment': '环境',
+    'entities.discovery.policy.title': '治理筛选与共享策略',
+    'entities.discovery.policy.copy': '按已有负责人、系统和环境预设给线索完善上下文。',
+    'entities.discovery.policy.latest-activity': '最近活动 {{activity}}',
+    'entities.discovery.policy.empty': '暂无共享策略,先搜索一条监控线索。',
+    'entities.discovery.catalog.owner': '负责人',
+    'entities.discovery.catalog.system': '系统',
+    'entities.discovery.catalog.environment': '环境',
+    'entities.discovery.metric.clues': '线索',
+    'entities.discovery.metric.matched': '已匹配',
+    'entities.discovery.metric.create-suggested': '建议新建',
+    'entities.discovery.metric.catalog-sources': '目录来源',
+    'entities.discovery.table.title': '发现线索',
+    'entities.discovery.table.result-count': '{{count}} 条结果',
+    'entities.discovery.table.waiting': '等待搜索',
+    'entities.discovery.table.column.clue': '线索',
+    'entities.discovery.table.column.instance': '实例',
+    'entities.discovery.table.column.status': '状态',
+    'entities.discovery.table.column.owner': '归属',
+    'entities.discovery.table.column.system': '系统',
+    'entities.discovery.table.column.environment': '环境',
+    'entities.discovery.table.column.attribution': '归因',
+    'entities.discovery.table.column.action': '操作',
+    'entities.discovery.empty.title': '先搜索一组需要治理的监控线索',
+    'entities.discovery.empty.search.copy': '没有找到匹配的监控线索。',
+    'entities.discovery.empty.idle.copy': '先搜索一条监控,再把它转成实体草稿。',
+    'entities.discovery.activity.preset-synced': '预设已同步',
+    'entities.discovery.activity.shared-governance-updated': '共享治理已更新',
+    'entities.discovery.activity.empty': '暂无活动',
+    'entities.discovery.row.status.enabled': '已启用',
+    'entities.discovery.row.status.normal': '正常',
+    'entities.discovery.row.status.abnormal': '异常',
+    'entities.discovery.row.status.review': '待确认',
+    'entities.discovery.row.status.unknown': '未知状态 {{status}}',
+    'entities.discovery.row.activity.search-result': '搜索结果',
+    'entities.discovery.row.activity.catalog-preset': '目录预设',
+    'entities.discovery.row.missing.owner': '负责人',
+    'entities.discovery.row.missing.system': '系统',
+    'entities.discovery.row.missing.environment': '环境',
+    'entities.discovery.row.missing.separator': '、',
+    'entities.discovery.row.missing.copy': '缺少{{fields}}',
+    'entities.discovery.row.missing.ready': '归因上下文待确认',
+    'entities.discovery.row.attribution.preset.label': '目录预设',
+    'entities.discovery.row.attribution.preset.copy': '可作为候选确认基线',
+    'entities.discovery.row.attribution.preset.action': '查看预设',
+    'entities.discovery.row.attribution.resolved.label': '已归因',
+    'entities.discovery.row.attribution.resolved.copy': '已归入对象目录',
+    'entities.discovery.row.attribution.resolved.action': '打开实体',
+    'entities.discovery.row.attribution.merge.label': '建议归并',
+    'entities.discovery.row.attribution.merge.copy': '候选实体 {{candidate}}',
+    'entities.discovery.row.attribution.merge.action': '确认归并',
+    'entities.discovery.row.attribution.review.label': '归因待补齐',
+    'entities.discovery.row.attribution.review.action': '补齐归因',
+    'entities.discovery.row.attribution.create.label': '建议新建',
+    'entities.discovery.row.attribution.create.copy': '已有监控线索,可创建实体草稿',
+    'entities.discovery.row.attribution.create.action': '创建实体',
+    'entities.discovery.service-name': '{{name}} 服务',
+    'entities.discovery.facts.workspace': '工作区',
+    'entities.discovery.facts.presets': '预设',
+    'entities.discovery.facts.activities': '活动',
+    'entities.discovery.facts.owners': '负责人',
+    'entities.discovery.metrics.owners': '负责人',
+    'entities.discovery.metrics.systems': '系统',
+    'entities.discovery.metrics.environments': '环境',
+    'entities.discovery.metrics.preset-coverage': '预设覆盖',
+    'entities.discovery.metrics.ready': '就绪',
+    'entities.discovery.metrics.empty': '为空',
+    'entities.discovery.catalog.owners': '负责人',
+    'entities.discovery.catalog.systems': '系统',
+    'entities.discovery.catalog.environments': '环境',
+    'entities.discovery.catalog.count': '数量 {{count}}',
+    'entities.discovery.catalog.none': '无',
+    'entities.discovery.card.action.open-resolved': '打开已归因实体',
+    'entities.discovery.card.action.open-definition': '打开定义',
+    'entities.discovery.card.action.merge-suggested': '归并到建议实体',
+    'entities.discovery.card.action.open-suggested': '打开建议实体',
+    'entities.discovery.card.action.adopt-draft': '作为草稿采用',
+    'entities.discovery.card.action.review-governance': '复核治理',
+    'entities.discovery.card.action.send-definition': '送入定义工作台',
+    'entities.discovery.card.action.create-draft': '创建实体草稿',
+    'entities.discovery.card.recommendation.resolved': '该遥测已收口到目录,下一步打开已归因实体或定义工作台复核。',
+    'entities.discovery.card.recommendation.merge': '已存在匹配的目录候选,先归并再考虑新建草稿。',
+    'entities.discovery.card.recommendation.enrich': '先补齐共享治理上下文,再送入定义工作台或创建草稿。',
+    'entities.discovery.card.recommendation.create': '该遥测已有足够共享治理上下文,可直接打开新实体草稿。',
+    'entities.discovery.card.risk.low': '治理风险低',
+    'entities.discovery.card.risk.medium': '治理风险中',
+    'entities.discovery.card.risk.high': '治理风险高',
+    'entities.discovery.card.next.open': '下一步打开',
+    'entities.discovery.card.next.merge': '下一步归并',
+    'entities.discovery.card.next.enrich': '下一步补齐',
+    'entities.discovery.card.next.create': '下一步新建',
+    'entities.discovery.card.candidate-label': '建议实体 · {{candidate}} · 匹配强',
+    'entities.discovery.scope.all': '全部',
+    'entities.discovery.scope.matched': '已匹配',
+    'entities.discovery.scope.resolved': '已归因',
+    'entities.discovery.scope.suggested-new': '建议新建',
+    'entities.discovery.queue.review.title': '需要治理复核',
+    'entities.discovery.queue.review.summary': '{{count}} 条遥测结果在归并或新建前仍需治理复核。',
+    'entities.discovery.queue.review.action': '复核治理',
+    'entities.discovery.queue.merge.title': '可归并',
+    'entities.discovery.queue.merge.summary': '{{count}} 条遥测结果可立即选择归并。',
+    'entities.discovery.queue.merge.action': '选择可归并',
+    'entities.discovery.queue.create.title': '建议新建实体',
+    'entities.discovery.queue.create.summary': '{{count}} 条遥测结果可打开为新实体草稿或送入定义工作台。',
+    'entities.discovery.queue.create.action': '选择建议新建',
+    'entities.discovery.queue.resolved.title': '已收口',
+    'entities.discovery.queue.resolved.summary.present': '{{count}} 条遥测结果已映射到已收口目录实体。',
+    'entities.discovery.queue.resolved.summary.empty': '暂无发现结果标记为已收口。',
+    'entities.discovery.queue.resolved.action': '查看已收口',
+    'entities.discovery.bulk.tag.owner': '负责人',
+    'entities.discovery.bulk.tag.system': '系统',
+    'entities.discovery.bulk.preset.apply': '应用 {{name}}',
     'entity.health.label': '健康评分 {{score}}',
     'entity.health.copy.collected': '采集 {{healthy}} / {{total}} 健康',
     'entity.health.copy.waiting': '等待采集绑定',
     'entity.health.meta': '告警 {{alerts}} · 异常 {{anomalies}}',
-    'topology.identity': 'HertzBeat 企业运维拓扑',
-    'topology.kicker': '拓扑与影响面',
+    'topology.identity': '运维拓扑',
+    'topology.kicker': '拓扑',
     'topology.copy': '以实体中心为锚点,把 OTLP 调用、监控对象、模板依赖、K8s 工作负载、数据库/中间件连接和告警影响面收敛到同一张运维关系图。',
     'topology.refresh': '刷新拓扑',
     'topology.environment.aria': '选择环境',
@@ -1159,14 +4413,121 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.search.aria': '拓扑筛选',
     'topology.search.placeholder': '搜索实体、服务、资源或标签',
     'topology.view.fit': '适配视图',
+    'topology.view.zoom-in': '放大',
+    'topology.view.zoom-out': '缩小',
+    'topology.view.focus-selected': '聚焦选中节点',
     'topology.locate-entity': '定位实体',
     'topology.current-filter': '当前筛选',
     'topology.all-entities': '全部实体',
+    'topology.state.label': '排障状态',
+    'topology.state.focus': '焦点',
+    'topology.state.depth': '深度',
+    'topology.state.depth.auto': '自动',
+    'topology.state.depth.one-hop': '1 跳',
+    'topology.state.depth.two-hop': '2 跳',
+    'topology.state.depth.n-hop': '{{depth}} 跳',
+    'topology.state.layout': '布局',
+    'topology.state.layout.layered-service': '分层服务',
+    'topology.state.layout.force': '力导向',
+    'topology.state.layout.grid-table': '网格 / 表格',
+    'topology.state.group': '分组',
+    'topology.state.group.none': '无',
+    'topology.state.group.environment': '环境',
+    'topology.state.group.source-kind': '证据来源',
+    'topology.state.group.entity-type': '实体类型',
+    'topology.toolbar.reset': '重置',
+    'topology.focus-trail.label': '聚焦拓扑',
+    'topology.focus-trail.exit': '退出聚焦',
+    'topology.focus-trail.hidden-count': '当前范围隐藏 {{count}} 个',
+    'topology.focus-trail.filter.environment': '环境',
+    'topology.focus-trail.filter.time-range': '时间范围',
+    'topology.focus-trail.filter.source': '来源',
+    'topology.focus-trail.filter.view': '视图',
+    'topology.focus-trail.source.all': '全部来源',
+    'topology.group-panel.title': '分组摘要',
+    'topology.group-panel.copy': '压缩高密度拓扑结果,同时保留最差健康状态和折叠范围。',
+    'topology.group-panel.group-by': '按类型 / 环境 / 来源分组',
+    'topology.group-panel.entity-type': '实体类型',
+    'topology.group-panel.environment': '环境',
+    'topology.group-panel.source-kind': '证据来源',
+    'topology.group-panel.node-count': '{{count}} 个可见节点',
+    'topology.group-panel.edge-count': '{{count}} 条可见边',
+    'topology.group-panel.collapsed-count': '折叠 {{count}} 个',
+    'topology.group-panel.active-scope': '当前范围',
+    'topology.group-panel.clear-group': '清除分组',
+    'topology.group-panel.open-table': '打开表格',
+    'topology.path-summary.title': '选中路径',
+    'topology.path-summary.source': '起点',
+    'topology.path-summary.target': '终点',
+    'topology.path-summary.relation': '关系',
+    'topology.path-summary.direction': '上游到下游',
+    'topology.path-summary.focus-path': '聚焦路径',
+    'topology.path-summary.open-trace': '打开链路',
     'topology.fault-context.title': '故障上下文',
     'topology.fault-context.copy': '来自当前入口的实体、链路和采集字段',
+    'topology.empty.api.title': '未找到拓扑关系',
+    'topology.empty.api.copy': '当前筛选下 API 没有返回实体、监控对象或链路调用关系证据。',
+    'topology.empty.trace-call.title': '暂无 trace 调用拓扑证据',
+    'topology.empty.trace-call.copy': 'Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。',
+    'topology.empty.trace-call.source': 'Greptime trace graph',
+    'topology.degraded.api.title': '拓扑 API 不可用',
+    'topology.degraded.api.copy': '当前范围无法加载 API、Greptime、链路或关系证据,拓扑图处于降级状态。',
+    'topology.degraded.api.source': '拓扑 API 不可用',
+    'topology.degraded.trace-call.title': 'trace 调用拓扑不可用',
+    'topology.degraded.trace-call.copy': '当前时间和环境下无法加载 Greptime trace 调用拓扑证据。拓扑图保持空态,避免展示过期或无关的关系。',
+    'topology.degraded.trace-call.source': 'Greptime trace graph',
+    'topology.loading.api.title': '正在加载拓扑证据',
+    'topology.loading.api.copy': '正在按当前范围加载 API、Greptime、链路和关系证据。',
     'topology.edge.evidence.aria': '{{label}} 关系证据',
     'topology.aside.view': '视图',
+    'topology.companion.jump.aria': '拓扑伴随栏分区',
+    'topology.companion.jump.view': '视图',
+    'topology.companion.jump.legend': '图例',
+    'topology.companion.jump.red': 'RED',
+    'topology.companion.jump.timeline': '时间线',
+    'topology.companion.jump.edge': '边',
+    'topology.companion.jump.node': '节点',
+    'topology.companion.section.collapse': '收起',
+    'topology.companion.section.expand': '展开',
     'topology.alert-impact.open': '打开告警影响面',
+    'topology.alert-impact.copy': '保留当前边、实体和三信号范围。',
+    'topology.legend.title': '图例',
+    'topology.legend.summary': '2 组',
+    'topology.legend.health': '健康状态',
+    'topology.legend.health.healthy': '健康',
+    'topology.legend.health.healthy-value': '正常',
+    'topology.legend.health.warning': '告警',
+    'topology.legend.health.warning-value': '关注',
+    'topology.legend.health.critical': '严重',
+    'topology.legend.health.critical-value': '处理',
+    'topology.legend.status': '状态',
+    'topology.legend.status.healthy': '正常',
+    'topology.legend.status.healthy-value': '正常',
+    'topology.legend.status.warning': '告警',
+    'topology.legend.status.warning-value': '关注',
+    'topology.legend.status.critical': '严重',
+    'topology.legend.status.critical-value': '处理',
+    'topology.legend.interaction': '交互',
+    'topology.legend.interaction.selected-node': '选中',
+    'topology.legend.interaction.selected-node-value': '选中',
+    'topology.legend.interaction.directional-edge': '方向边',
+    'topology.legend.interaction.directional-edge-value': '依赖方向',
+    'topology.legend.interaction.dimmed-edge': '弱化',
+    'topology.legend.interaction.dimmed-edge-value': '上下文外',
+    'topology.legend.source-kind': '证据来源',
+    'topology.legend.source.trace': '链路调用',
+    'topology.legend.source.trace-value': 'trace',
+    'topology.legend.source.monitor': '监控归属',
+    'topology.legend.source.monitor-value': 'monitor',
+    'topology.legend.source.relation': '实体关系',
+    'topology.legend.source.relation-value': 'relation',
+    'topology.legend.confidence': '可信度',
+    'topology.legend.confidence.live': '当前窗口证据',
+    'topology.legend.confidence.stale': '低可信 / 过期',
+    'topology.timeline.title': '影响时间线',
+    'topology.timeline.copy': '最近改变当前拓扑视图的实体、关系和监控对象证据。',
+    'topology.timeline.detail.fallback': '拓扑证据已变更',
+    'topology.timeline.actor.system': 'system',
     'topology.edge.evidence.title': '关系证据',
     'topology.edge.link.from-entity': '起点实体',
     'topology.edge.link.to-entity': '终点实体',
@@ -1176,6 +4537,32 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.edge.link.traces': '链路证据',
     'topology.current-entity': '当前实体',
     'topology.current-entity.signals': '信号:{{signals}}',
+    'topology.current-entity.boundary': '信号跳转会保留当前实体、时间范围、环境和拓扑范围。',
+    'topology.current-entity.fact.entity-id': '实体 ID',
+    'topology.current-entity.fact.namespace': '命名空间',
+    'topology.current-entity.fact.signals': '信号',
+    'topology.current-entity.fact.health': '健康',
+    'topology.current-entity.fact.upstream': '上游',
+    'topology.current-entity.fact.downstream': '下游',
+    'topology.current-entity.fact.neighbor-count': '{{count}} 个依赖',
+    'topology.current-entity.fact.neighbor.none': '当前范围无依赖',
+    'topology.metric-table.title': '边指标排行',
+    'topology.metric-table.empty': '当前范围暂无关系指标',
+    'topology.metric-table.edge-count': '{{count}} 条边',
+    'topology.metric-table.request-rate-unit': '请求/秒',
+    'topology.metric-table.error-rate-unit': '错误',
+    'topology.metric-table.latency-p95-unit': 'P95',
+    'topology.metric-table.row-action': '检查',
+    'topology.metric-table.open-edge-aria': '打开拓扑边 {{edge}}',
+    'topology.metric-table.filter.all': '全部',
+    'topology.metric-table.filter.visible': '可见',
+    'topology.metric-table.filter.partial': '半可见',
+    'topology.metric-table.filter.hidden': '隐藏',
+    'topology.metric-table.filter.unknown': '未知',
+    'topology.red.request-rate': '请求速率',
+    'topology.red.error-rate': '错误率',
+    'topology.red.latency-p95': 'P95 延迟',
+    'topology.red.source.trace-service-graph': '链路派生服务图',
     'topology.context-link.entity': '查看实体详情',
     'topology.context-link.signals': '打开三信号',
     'topology.context-link.metrics': '指标工作台',
@@ -1220,12 +4607,16 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.evidence.source.alert-impact': '当前告警与服务/资源依赖的影响面投射',
     'topology.evidence.collector.otlp': 'OTLP Collector',
     'topology.evidence.collector.hertzbeat': 'HertzBeat Collector',
-    'topology.evidence.boundary': '当前边仅基于已采集的关系证据;依赖自动发现、变更时间线和根因分析仍是 roadmap 能力。',
+    'topology.evidence.boundary': '当前边仅基于已采集的关系证据;依赖自动发现和根因分析仍是 roadmap 能力。',
     'topology.evidence.alert-impact.copy': '带当前边、实体和三信号上下文进入告警影响面。',
     'topology.evidence.row.from': '起点实体',
     'topology.evidence.row.to': '终点实体',
     'topology.evidence.row.source': '采集证据',
     'topology.evidence.row.alert-impact': '告警影响',
+    'topology.hover.fact.relation-type': '关系类型',
+    'topology.hover.fact.first-seen': '首次出现',
+    'topology.hover.fact.last-seen': '最后出现',
+    'topology.hover.fact.sample-trace': '样例 Trace',
     'topology.evidence.alert-impact.none': '暂无影响',
     'topology.evidence.alert-impact.warning': '存在影响',
     'topology.evidence.alert-impact.critical': '严重影响',
@@ -1234,6 +4625,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'common.stable': 'stable',
     'common.total': '总量',
     'common.updated': '更新时间',
+    'common.workspace': '工作区',
+    'common.workspace-navigation': '工作区导航',
     'label.bind': '标签',
     'menu.dashboard': '总览',
     'menu.alert.dispatch': '消息通知',
@@ -1244,33 +4637,66 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'menu.section.dashboards': '仪表盘',
     'menu.section.settings': '平台设置',
     'menu.overview': '仪表盘',
+    'monitors.loading': '正在加载监控中心',
     'monitor.list': '监控列表',
     'monitor.scrape': '采集方式',
+    'monitor.collector.system.default': '默认系统调度',
     'monitor.status': '任务状态',
     'monitor.status.up': '正常',
     'monitor.status.down': '宕机',
     'monitor.status.paused': '暂停',
+    'monitors.status.up': '正常',
+    'monitors.status.down': '宕机',
+    'monitors.status.paused': '暂停',
+    'monitors.type.all': '全部类型',
     'monitor.edit-monitor': '编辑监控',
-    'monitor.editor.labels.add': 'Add label',
+    'monitor.scrape.type.static': '静态配置',
+    'monitor.scrape.type.http_sd': 'Http 服务发现',
+    'monitor.scrape.type.nacos_sd': 'Nacos 服务发现',
+    'monitor.scrape.type.dns_sd': 'Dns 服务发现',
+    'monitor.scrape.type.eureka_sd': 'Eureka 服务发现',
+    'monitor.scrape.type.consul_sd': 'Consul 服务发现',
+    'monitor.scrape.type.zookeeper_sd': 'Zookeeper 服务发现',
+    'entity.monitor.workbench.reason.down': '当前监控处于异常状态,先看它。',
+    'entity.monitor.workbench.reason.fallback': '当前没有异常监控,这里显示全部关联监控。',
+    'entity.monitor.workbench.reason.paused': '当前监控处于暂停状态,可确认是否应恢复。',
+    'entity.monitor.workbench.reason.default': '当前监控可用于继续核对覆盖面和最近状态。',
+    'entity.response.open-related-logs': '查看相关日志',
+    'entity.response.open-related-traces': '查看相关链路',
+    'entity.response.open-code': '打开代码',
+    'monitors.controls.more-actions': '更多监控操作',
+    'monitor.copy.action': '复制监控',
+    'monitor.copy.success': '复制监控成功',
+    'monitor.copy.failed': '复制监控失败',
+    'monitor.item.unavailable.refresh': '该监控项已不可用,列表将自动刷新',
+    'monitor.editor.labels.add': '添加标签',
     'monitor.editor.labels.copy': '以键值对形式管理监控标签。',
     'monitor.editor.labels.key': '标签键',
     'monitor.editor.labels.value': '标签值',
     'monitor.editor.labels.remove': '移除',
-    'monitor.editor.annotations.add': 'Add annotation',
+    'monitor.editor.annotations.add': '添加注解',
     'monitor.editor.annotations.copy': '以键值对形式管理监控注解。',
     'monitor.editor.annotations.key': '注解键',
     'monitor.editor.annotations.value': '注解值',
     'monitor.editor.annotations.remove': '移除',
-    'monitor.editor.validation.name': 'Monitor name is required',
+    'monitor.editor.validation.name': '请输入监控名称',
     'monitor.editor.validation.app': '请选择监控类型',
     'monitor.editor.validation.intervals': '请输入采集间隔',
-    'monitor.editor.validation.param-required': '{{field}} is required',
+    'monitor.editor.validation.param-required': '请填写 {{field}}',
+    'monitor.route-state.form.error.title': '监控表单加载失败',
+    'monitor.route-state.form.error.copy': '当前无法初始化监控表单,请稍后重试。',
+    'monitor.route-state.action.retry': '重试',
+    'monitor.route-state.loading.title': '正在加载监控详情',
+    'monitor.route-state.loading.copy': '请稍候,正在准备监控实例、指标摘要和历史趋势。',
+    'monitor.route-state.error.title': '监控详情加载失败',
+    'monitor.route-state.error.copy': '当前无法加载监控详情,请稍后重试或从监控列表重新打开。',
     'monitor.editor.loading.edit': '正在加载监控配置...',
     'monitor.editor.loading.new': '正在加载监控表单...',
     'monitor.editor.section.base': '基础配置',
     'monitor.editor.section.base.copy': '设置监控类型、目标和采集周期。',
     'monitor.editor.section.params': '运行参数',
     'monitor.editor.section.params.copy': '填写当前监控模板需要的运行参数。',
+    'monitor.editor.section.runtime': '采集调度',
     'monitor.editor.section.advanced.copy': '配置标签、注解和关联看板,用于筛选与后续联动。',
     'monitor.editor.section.payload': '采集载荷',
     'monitor.editor.section.payload.copy': '配置此监控使用的请求体或脚本载荷。',
@@ -1281,6 +4707,9 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.overview.kicker': 'Overview',
     'monitor.detail.title-fallback': '监控详情',
     'monitor.detail.open-grafana': '打开 Grafana',
+    'monitor.detail.auto-refresh': '自动刷新 {{time}} 秒',
+    'monitor.detail.close-refresh': '关闭自动刷新',
+    'monitor.detail.config-refresh': '设置 {{time}} 秒自动刷新',
     'monitor.detail.refresh-countdown': '即将刷新',
     'monitor.detail.refresh-disabled': '自动刷新关闭',
     'monitor.detail.status.up': '正常',
@@ -1325,6 +4754,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.grafana.enabled.meta': 'external dashboard',
     'monitor.detail.grafana.copy': '配置关联看板后可从这里打开 Grafana。',
     'monitor.detail.grafana.status': '看板状态',
+    'monitor.detail.grafana.configure': '配置看板',
     'monitor.detail.param.meta': 'runtime param',
     'monitor.detail.param.unknown': '参数',
     'monitor.detail.schedule.empty': '未配置周期',
@@ -1381,6 +4811,10 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.favorite.catalog.title': 'Favorite catalog',
     'monitor.detail.favorite.remove': 'Remove favorite',
     'monitor.detail.favorite.add': 'Add favorite',
+    'monitor.favorite.add.success': '收藏成功',
+    'monitor.favorite.add.failed': '收藏失败',
+    'monitor.favorite.remove.success': '取消收藏成功',
+    'monitor.favorite.remove.failed': '取消收藏失败',
     'monitor.detail.metric.row-label': '行',
     'monitor.detail.metric.row': '第 {{index}} 行',
     'monitor.detail.metric.fullscreen.enter': 'Fullscreen',
@@ -1450,14 +4884,65 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitors.metric.paused': '当前页暂停',
     'monitors.empty-selected.title': '还未选择监控',
     'monitors.empty-selected.copy': '从列表选择监控实例后,这里会显示操作和上下文。',
-    'ops.surface.mode.entry': 'entry surface',
-    'ops.surface.shared-shell.title': 'Shared shell',
+    'ops.surface.mode.entry': '入口面',
+    'ops.surface.kicker': '操作员入口',
+    'ops.surface.route-contract.title': '路由契约',
+    'ops.surface.route-contract.copy': '共享壳层状态在入口阶段保持可见。',
+    'ops.surface.launch-checklist': '启动检查清单',
+    'ops.surface.route-posture': '路由姿态',
+    'ops.surface.entry-shell': '入口壳层就绪',
+    'ops.surface.fact.focus-label': '焦点',
+    'ops.surface.fact.mode-label': '模式',
+    'ops.surface.fact.mode-entry': '入口面',
+    'ops.surface.fact.signals-label': '信号',
+    'ops.surface.route-posture.mode-label': '模式',
+    'ops.surface.shared-shell.title': '共享壳层',
     'ops.surface.shared-shell.copy': '入口页沿用同一套壳层和信息层级。',
-    'ops.surface.focused-scope.title': 'Focused scope',
+    'ops.surface.focused-scope.title': '聚焦范围',
     'ops.surface.focused-scope.copy': '只承接导航、上下文和下一跳。',
+    'overview.loading': '正在加载概览控制台',
+    'overview.guidance.default.start': '下一步',
+    'overview.guidance.default.reasons': '原因',
+    'overview.guidance.default.next': '后续',
+    'overview.checklist.status.ready': '就绪',
+    'overview.checklist.status.pending': '待处理',
+    'overview.lane.entities.title': '实体目录',
+    'overview.lane.entities.eyebrow': '实体优先',
+    'overview.lane.entities.copy': '先从监控实体和服务归属定位影响范围,再进入信号排查。',
+    'overview.lane.entities.stat': '已纳管 {{count}} 个实体',
+    'overview.lane.entities.action': '打开实体目录',
+    'overview.lane.logs.title': '日志',
+    'overview.lane.logs.eyebrow': '运行证据',
+    'overview.lane.logs.copy': '用相同服务、实体和时间上下文查看日志事件。',
+    'overview.lane.logs.stat.available': '日志可用',
+    'overview.lane.logs.action': '打开日志',
+    'overview.lane.trace.title': '链路',
+    'overview.lane.trace.eyebrow': '请求路径',
+    'overview.lane.trace.copy': '存在链路上下文时跟进跨度,再回到实体证据。',
+    'overview.lane.trace.stat.distributed': '分布式链路就绪',
     'overview.lane.trace.stat.detected': '当前事件含链路 ID',
+    'overview.lane.trace.action': '打开链路',
+    'overview.lane.otlp.title': 'OTLP 指标',
+    'overview.lane.otlp.eyebrow': '三信号接入',
+    'overview.lane.otlp.copy': '在私有部署的 HertzBeat 工作区查看进入的指标序列。',
+    'overview.lane.otlp.stat': '已接入 {{count}} 个监控应用',
+    'overview.lane.otlp.action': '打开指标',
     'passport.lock.error.required': '请输入密码',
-    'passport.lock.next-step': 'wire unlock validation if needed',
+    'passport.lock.status.interactive': '可解锁',
+    'passport.lock.next-step': '按需接入解锁校验',
+    'app.lock': '解除锁定',
+    'app.lock.placeholder': '输入任意解锁',
+    'about.title': 'HertzBeat 是一个开源可观测性平台。',
+    'about.help': '感谢使用 HertzBeat。以下链接可用于关注版本、反馈问题和安全升级。',
+    'about.copyright': 'Copyright © {{year}} | Apache HertzBeat™',
+    'about.not-show-next-login': '下次登录不再显示',
+    'about.github': 'GitHub',
+    'about.issue': 'Issue',
+    'about.pr': 'PR',
+    'about.discuss': '讨论',
+    'about.doc': '文档',
+    'about.upgrade': '升级',
+    'about.star': 'Star',
     'about.point.1': '通过采集器接入应用、数据库、操作系统、中间件和网络设备指标,数据留在私有化部署内。',
     'about.point.2': '接收 OTLP 日志,保留资源、实体、链路和告警上下文,便于从日志继续排障。',
     'about.point.3': '统一处理阈值告警和外部告警,支持确认、静默、抑制、恢复和关闭。',
@@ -1467,10 +4952,14 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'app.login.login': '登录',
     'app.login.message-need-credential': '请输入密码',
     'app.login.message-need-identifier': '请输入用户名',
+    'app.login.password-hide': '隐藏密码',
+    'app.login.password-show': '显示密码',
+    'app.login.remember-me': '记住我',
     'app.login.tab-login-credentials': '登入 HertzBeat',
     'app.passport.desc': '开源私有化企业运维可观测平台',
     'app.passport.intro-1': '采集器、监控模板、实体、指标、日志和链路',
     'app.passport.intro-2': '处理告警并关闭问题',
+    'app.passport.language-switch': '切换语言',
     'passport.login.card.entry.title': '运维入口',
     'passport.login.card.entry.copy': '登录后继续查看资源、实体、遥测数据和告警。',
     'passport.login.card.shell.title': '私有化部署',
@@ -1478,14 +4967,175 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'passport.login.card.auth.title': '安全访问',
     'passport.login.card.auth.copy': '验证账号后再进入告警处理、通知和系统设置。',
     'passport.login.default-password-warning': '当前使用默认密码,建议登录后尽快修改。',
+    'passport.login.loading': '正在登录',
+    'passport.login.error.generic': '登录失败,请检查用户名或密码。',
+    'passport.login.error.with-status': '登录失败:{{status}}',
     'common.workbench.loading.title': '正在加载工作台',
+    'common.workbench.loading.copy': '正在准备页面数据,请稍等。',
+    'settings.system-config': '系统配置',
+    'settings.system-config.locale': '系统语言',
+    'settings.system-config.timezone': '系统时区',
+    'settings.system-config.theme': '系统主题',
     'settings.system-config.locale.en_US': '英语(en_US)',
+    'settings.system-config.locale.zh_CN': '简体中文(zh_CN)',
+    'settings.system-config.locale.zh_TW': '繁體中文(zh_TW)',
+    'settings.system-config.locale.ja-JP': '日语(ja_JP)',
+    'settings.system-config.locale.pt_BR': '葡萄牙语(pt_BR)',
+    'settings.system-config.theme.default': '默认主题',
+    'settings.system-config.theme.dark': '深色主题',
+    'settings.system-config.theme.compact': '紧凑主题',
+    'settings.console.kicker': '设置',
+    'settings.console.title': '系统设置',
+    'settings.console.copy': '在同一控制台配置系统、消息服务、对象存储和令牌。',
+    'settings.console.navigation': '设置分区',
+    'settings.console.content': '设置内容',
+    'settings.surface.title': '设置总览',
+    'settings.surface.copy': '把平台配置入口收拢到一个工作台。',
     'settings.surface.route-contract.title': '设置导航',
     'settings.surface.route-contract.copy': '从这里进入状态页、采集器、标签、插件、令牌和系统配置。',
+    'settings.surface.route-contract.meta': '路由契约',
     'settings.surface.api-contract.title': '运维配置',
     'settings.surface.api-contract.copy': '统一维护采集器、标签、插件、令牌、状态页和通知通道。',
+    'settings.surface.api-contract.meta': 'API 契约',
+    'settings.surface.fact.mode-label': '模式',
+    'settings.surface.fact.mode-value': '设置',
+    'settings.surface.fact.focus-label': '焦点',
     'settings.surface.governance.title': '平台治理',
-    'settings.surface.governance.copy': '只展示当前已有入口;未来大域保留在 roadmap 文档。',
+    'settings.surface.governance.copy': '只展示当前已有入口;未来大域保留在路线图文档。',
+    'settings.surface.governance.group.users-permissions': '用户与权限',
+    'settings.surface.governance.group.api-access': 'API 访问',
+    'settings.surface.governance.group.notifications': '通知通道',
+    'settings.surface.governance.group.template-marketplace': '模板与插件',
+    'settings.surface.governance.group.mcp-ai-foundation': 'MCP 与 AI 基础',
+    'settings.surface.governance.copy.implemented-route': '已接入当前平台入口,继续沿用现有页面,不新增空菜单。',
+    'settings.surface.governance.copy.foundation-only': '保留当前基础能力入口,完整治理能力继续在路线图中推进。',
+    'settings.surface.governance.future.title': '未来大域边界',
+    'settings.surface.governance.future.copy': '{{domains}} 仅作为路线图能力规划,不展示为已上线应用入口。文档:{{docs}}',
+    'settings.surface.governance.future.meta': '仅规划',
+    'settings.surface.governance.future.separator': '、',
+    'settings.surface.governance.future.security': '安全治理',
+    'settings.surface.governance.future.data-observability': '数据可观测',
+    'settings.surface.governance.future.digital-experience': '数字体验',
+    'settings.surface.governance.future.software-delivery': '软件交付',
+    'settings.surface.governance.future.cloud-cost': '云成本',
+    'settings.surface.governance.future.ai-observability': 'AI 可观测',
+    'settings.surface.governance.future.developer-integrations': '开发者集成',
+    'setting.labels.loading': '正在加载标签管理',
+    'setting.labels.copy': '统一管理监控标签名称、标签值和描述,用旧版一致的紧凑卡片网格承载检索、复制和编辑动作。',
+    'setting.labels.table.name': '标签名称',
+    'setting.labels.table.type': '标签类型',
+    'setting.labels.table.description': '标签描述',
+    'setting.labels.type.auto': '自动标签',
+    'setting.labels.type.user': '用户标签',
+    'setting.labels.type.preset': '预置标签',
+    'setting.labels.type.unknown': '未知标签类型',
+    'setting.labels.metric.auto-page': '自动标签/当前页',
+    'setting.labels.metric.user-page': '用户标签/当前页',
+    'setting.labels.metric.preset-page': '预置标签/当前页',
+    'setting.labels.empty.title': '暂无标签',
+    'setting.labels.empty.copy': '请先新增标签。',
+    'setting.labels.action.copy-aria': '复制标签 {{name}}',
+    'setting.labels.action.edit-aria': '编辑标签 {{name}}',
+    'setting.labels.action.delete-aria': '删除标签 {{name}}',
+    'setting.plugins.loading': '加载插件管理',
+    'setting.plugins.copy': '管理自定义插件、执行类型和启用状态,保持上传、筛选、批量删除和参数编辑在同一个紧凑工作面中完成。',
+    'setting.plugins.selected-count': '已选择 {{count}}',
+    'setting.plugins.table.params': '参数',
+    'setting.plugins.metric.enabled-page': '当前页启用插件',
+    'setting.plugins.metric.disabled-page': '当前页停用插件',
+    'setting.plugins.metric.params-page': '当前页参数项',
+    'setting.plugins.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'setting.plugins.pagination.page-size': '每页条数',
+    'setting.plugins.pagination.page': '页码',
+    'setting.plugins.row.item-fallback': '插件项 -',
+    'setting.plugins.row.params': '参数 {{count}}',
+    'setting.plugins.action.params-aria': '编辑插件参数 {{name}}',
+    'setting.plugins.action.delete-aria': '删除插件 {{name}}',
+    'setting.collector.loading': '正在加载采集集群',
+    'setting.collector.copy': '管理已注册采集器节点、运行状态和任务分布,保持部署、搜索、上下线和删除操作在同一个高密度后台工作面中完成。',
+    'setting.collector.selected-count': '已选择 {{count}}',
+    'collector.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'collector.pagination.page-size': '每页条数',
+    'collector.pagination.page': '页码',
+    'collector.confirm.offline': '请确认是否下线此采集器!',
+    'collector.confirm.offline-batch': '请确认是否批量下线采集器!',
+    'collector.confirm.online': '请确认是否上线此采集器!',
+    'collector.confirm.online-batch': '请确认是否批量上线采集器!',
+    'collector.notify.no-select-offline': '未选中任何待下线项!',
+    'collector.notify.no-select-online': '未选中任何待上线项!',
+    'collector.deploy': '部署采集器',
+    'collector.deploy.close': '关闭,已保存采集器身份令牌',
+    'collector.deploy.docker': '通过 Docker 方式部署',
+    'collector.deploy.docker.help': '# 在 Docker 环境下执行如下命令',
+    'collector.deploy.docker.help.1': '# docker run -d : 使用 Docker 在后台启动采集器容器',
+    'collector.deploy.docker.help.2': '# -e IDENTITY=xxx : 设置采集器身份令牌',
+    'collector.deploy.docker.help.3': '# -e MANAGER_HOST=127.0.0.1 : 设置采集器连接的 HertzBeat 服务地址',
+    'collector.deploy.docker.help.4': '# -e MODE=public : 配置运行模式:公共采集器集群或私有边缘采集器',
+    'collector.deploy.docker.help.5': '# --name hertzbeat-collector : 将容器命名为 hertzbeat-collector',
+    'collector.deploy.docker.help.6': '# apache/hertzbeat-collector : 官方采集器镜像,拉取超时时可替换为 quay.io/tancloud/hertzbeat-collector',
+    'collector.deploy.identity': '采集器身份令牌 (IDENTITY)',
+    'collector.deploy.identity.tip': '此身份令牌只对应当前采集器,请妥善保存。',
+    'collector.deploy.ok': '生成身份令牌和部署命令',
+    'collector.deploy.package': '通过安装包部署',
+    'collector.deploy.package.github': '从 GitHub 下载安装包',
+    'collector.deploy.package.help': '# 下载当前系统对应的安装包 hertzbeat-collector-xx.tar.gz',
+    'collector.deploy.package.help.1': '# 解压后修改 hertzbeat-collector/config/application.yml',
+    'collector.deploy.package.help.2': '# 按示例替换文件中的 IDENTITY、MANAGER_HOST、MODE 参数。',
+    'collector.deploy.package.help.3': '# 运行 bin/startup.sh 或 startup.bat (Windows) 启动采集器进程。',
+    'collector.name.placeholder': '请配置待部署的采集器名称,注意需保证唯一性',
+    'common.button.copy.tip': '点击复制',
+    'common.copy.button': '复制',
+    'setting.define.loading': '加载定义中心',
+    'setting.define.title': '定义',
+    'setting.define.subtitle': '管理监控模板 YAML,直接维护采集协议、参数和指标字段,保持搜索、编辑、保存和模板显示状态在同一个高密度工作面中完成。',
+    'setting.define.action.new': '新增类型',
+    'setting.define.action.save-apply': '保存并应用',
+    'setting.define.action.delete': '删除 {{app}}',
+    'define.save-apply.confirm': '请确认是否保存修改并应用此监控类型定义? 这会影响到您的监控内容。',
+    'define.delete.confirm': '请确认是否删除 {{app}} 监控类型? 删除后无法添加此类监控。',
+    'define.hide-true.confirm': '确认是否显示此菜单?',
+    'define.hide-false.confirm': '确认是否隐藏此菜单?',
+    'setting.define.action.hide': '隐藏',
+    'setting.define.action.hide-aria': '隐藏模板 {{app}}',
+    'setting.define.action.show': '显示',
+    'setting.define.action.show-aria': '显示模板 {{app}}',
+    'setting.define.save.pending': '正在应用定义...',
+    'define.save-apply.no-code': '监控类型定义内容不能为空。',
+    'setting.define.skeleton.name': 'hertzbeat-baseline-alert',
+    'setting.define.skeleton.summary': 'HertzBeat 基线告警定义草稿',
+    'setting.define.current-yaml.fallback': 'current-definition',
+    'setting.define.current-delete.fallback': '当前定义',
+    'setting.define.item.fallback': '未命名定义',
+    'setting.define.empty.title': '暂无模板',
+    'setting.define.empty.copy': '请先新增监控模板 YAML。',
+    'setting.define.empty.search-title': '没有匹配的模板',
+    'setting.define.empty.search-copy': '调整搜索关键字后再试。',
+    'setting.define.empty-selected.title': '未选择定义',
+    'setting.define.empty-selected.copy': '从左侧列表选择一条定义。',
+    'setting.define.preview.action': '预览查询',
+    'setting.define.preview.title': '定义预览',
+    'setting.define.preview.success': '预览完成',
+    'setting.define.preview.failed': '预览失败',
+    'setting.define.save.success': '定义已保存',
+    'setting.define.save.failed': '保存定义失败',
+    'setting.define.datasource.title': '数据源状态',
+    'setting.define.fact.datasource': '数据源',
+    'setting.define.fact.selected-template': '当前模板',
+    'setting.define.fact.hidden': '隐藏模板',
+    'setting.define.new-template': '新增模板草稿',
+    'setting.define.template.visible': '可见',
+    'setting.define.template.hidden': '隐藏',
+    'setting.define.summary.yaml-lines': '{{count}} 行 YAML',
+    'setting.define.summary.mode.new': '新增',
+    'setting.define.summary.mode.edit': '编辑',
+    'setting.define.diff.original': '原始 YAML',
+    'setting.define.diff.current': '当前编辑 YAML',
+    'setting.define.row.meta': '周期 {{period}} 秒 · 更新 {{time}}',
+    'alert.setting.type.realtime.metric': '指标实时',
+    'alert.setting.type.periodic.metric': '指标周期',
+    'alert.setting.type.realtime.log': '日志实时',
+    'alert.setting.type.periodic.log': '日志周期',
+    'alert.setting.type.periodic.trace': '链路周期',
     'setting.status.loading': '正在加载状态页设置',
     'setting.status.kicker': '状态页设置',
     'setting.status.title': '状态页设置',
@@ -1504,6 +5154,11 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'setting.status.org.logo': 'Logo 地址',
     'setting.status.org.color': '品牌颜色',
     'setting.status.org.state': '组织状态',
+    'setting.status.delete.component.title': '确认删除组件',
+    'setting.status.delete.component.copy': '删除后该组件会从公开状态页移除,相关维护事件需要重新绑定组件。',
+    'setting.status.delete.incident.title': '确认删除维护事件',
+    'setting.status.delete.incident.copy': '删除后该维护事件会从公开状态页移除,历史说明不会再展示给访问者。',
+    'setting.status.delete.confirm': '确认删除',
     'setting.status.components.title': '组件',
     'setting.status.components.tab': '组件',
     'setting.status.components.empty.title': '还没有组件',
@@ -1520,14 +5175,125 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'setting.status.validation.component-description': '组件描述为必填项',
     'setting.status.incidents.state': '事件状态',
     'setting.status.incidents.components': '关联组件',
+    'setting.status.component.method.placeholder': '0 / 1',
+    'setting.status.component.state.placeholder': '0 / 1 / 2',
+    'setting.status.component.labels.placeholder': 'env:prod, app:api',
+    'setting.status.component.description.placeholder': '公开状态页的组件说明',
+    'setting.status.incident.state.placeholder': '0 / 1 / 2 / 3',
+    'setting.status.incident.components.placeholder': '1, 2',
+    'monitors.delete.title': '确认删除监控',
+    'monitors.delete.copy': '删除后选中的监控对象会从监控目录移除,相关实体绑定和告警排查入口需要重新归并。',
+    'monitors.delete.confirm': '确认删除',
+    'monitors.delete.selected': '已选择 {{count}} 个监控对象',
+    'monitors.selected-count': '已选择 {{count}} 个监控对象',
     'setting.settings.menu.copy': '系统、存储、消息和令牌设置',
+    'setting.settings.menu.open-action': '打开{{title}}',
     'setting.settings.config.title': '系统配置',
+    'setting.settings.config.loading': '正在加载系统配置。',
     'setting.settings.server.title': '消息服务器',
+    'setting.settings.server.loading': '正在加载消息服务配置。',
+    'settings.server': '消息服务配置',
+    'settings.server.email': '邮件服务器',
+    'settings.server.sms': '短信配置',
+    'settings.server.email.setting': '配置邮件服务器',
+    'settings.server.sms.setting': '配置短信参数',
+    'settings.server.summary.configure-action': '配置{{title}}',
+    'alert.notice.sender.mail.host': '邮箱服务器地址',
+    'alert.notice.sender.mail.port': '邮箱端口',
+    'alert.notice.sender.mail.username': '邮箱账号',
+    'alert.notice.sender.mail.password': '邮箱密码',
+    'alert.notice.sender.mail.ssl': '是否启用SSL',
+    'alert.notice.sender.mail.starttls': '是否启用STARTTLS',
+    'alert.notice.sender.mail.enable': '是否启用邮箱配置',
+    'alert.notice.sender.sms.type': '短信类型',
+    'alert.notice.sender.sms.type.tencent': '腾讯短信',
+    'alert.notice.sender.sms.type.alibaba': '阿里短信',
+    'alert.notice.sender.sms.type.unisms': '合一短信(UniSMS)',
+    'alert.notice.sender.sms.type.smslocal': '当地短信(Smslocal)',
+    'alert.notice.sender.sms.type.aws': 'Aws Sms',
+    'alert.notice.sender.sms.type.twilio': 'Twilio Sms',
+    'alert.notice.sender.sms.tencent.secretId': '腾讯短信SecretId',
+    'alert.notice.sender.sms.tencent.secretKey': '腾讯短信SecretKey',
+    'alert.notice.sender.sms.tencent.signName': '腾讯短信SignName',
+    'alert.notice.sender.sms.tencent.appId': '腾讯短信AppId',
+    'alert.notice.sender.sms.tencent.templateId': '腾讯短信TemplateId',
+    'alert.notice.sender.sms.alibaba.accessKeyId': '阿里短信AccessKeyId',
+    'alert.notice.sender.sms.alibaba.accessKeySecret': '阿里短信AccessKeySecret',
+    'alert.notice.sender.sms.alibaba.signName': '阿里短信SignName',
+    'alert.notice.sender.sms.alibaba.templateCode': '阿里短信TemplateCode',
+    'alert.notice.sender.sms.unisms.accessKeyId': 'UniSMS AccessKeyId',
+    'alert.notice.sender.sms.unisms.authMode': 'UniSMS 认证模式',
+    'alert.notice.sender.sms.unisms.accessKeySecret': 'UniSMS AccessKeySecret',
+    'alert.notice.sender.sms.unisms.signature': 'UniSMS 签名',
+    'alert.notice.sender.sms.unisms.templateId': 'UniSMS TemplateId',
+    'alert.notice.sender.sms.unisms.authMode.hmac': 'HMAC',
+    'alert.notice.sender.sms.unisms.authMode.simple': 'Simple',
+    'alert.notice.sender.sms.smslocal.apiKey': 'Smslocal ApiKey',
+    'alert.notice.sender.sms.aws.accessKeyId': 'AWS AccessKeyId',
+    'alert.notice.sender.sms.aws.accessKeySecret': 'AWS AccessKeySecret',
+    'alert.notice.sender.sms.aws.region': 'AWS Region',
+    'alert.notice.sender.sms.twilio.accountSid': 'Twilio AccountSid',
+    'alert.notice.sender.sms.twilio.authToken': 'Twilio AuthToken',
+    'alert.notice.sender.sms.twilio.twilioPhoneNumber': 'Twilio Phone Number',
     'setting.settings.object-store.title': '对象存储',
+    'setting.settings.object-store.loading': '正在加载文件服务配置。',
+    'settings.object-store': '文件服务配置',
+    'settings.object-store.type': '文件服务提供商',
+    'settings.object-store.type.database': '本地数据库(默认)',
+    'settings.object-store.type.file': '本地文件',
+    'settings.object-store.type.obs': '华为云 OBS',
+    'settings.object-store.obs.accessKey': 'AccessKey',
+    'settings.object-store.obs.secretKey': 'SecretKey',
+    'settings.object-store.obs.bucketName': 'Bucket',
+    'settings.object-store.obs.endpoint': 'Endpoint',
+    'settings.object-store.obs.savePath': '保存路径',
+    'settings.object-store.obs.accessKey.placeholder': '华为云 AccessKeyId',
+    'settings.object-store.obs.secretKey.placeholder': '华为云 AccessKeySecret',
+    'settings.object-store.obs.bucketName.placeholder': '华为云 OBS 中创建的 Bucket 名称',
+    'settings.object-store.obs.endpoint.placeholder': '华为云 OBS 地域域名,不包括 Bucket 名',
+    'settings.object-store.obs.savePath.placeholder': '备份文件保存路径,默认是 hertzbeat',
+    'settings.system-config.ok': '确认更新',
+    'setting.object-store.fact.type': '类型',
+    'setting.object-store.fact.bucket': 'Bucket',
+    'setting.object-store.fact.endpoint': 'Endpoint',
     'setting.token.fact.total': '令牌总数',
     'setting.token.fact.active': '可用令牌',
     'setting.token.fact.expired': '过期令牌',
+    'setting.token.metric.total': '令牌总数',
+    'setting.token.metric.active': '可用令牌',
+    'setting.token.metric.expired': '过期令牌',
+    'setting.token.row.creator': '创建人 {{creator}}',
+    'setting.token.row.state.active': '可用',
+    'setting.token.row.state.expired': '已过期',
     'setting.token.title': '令牌管理',
+    'setting.token.loading': '正在加载令牌管理。',
+    'settings.token': '令牌管理',
+    'settings.token.console.kicker': 'API 密钥',
+    'settings.token.console.title': '统一管理遥测和自动化令牌',
+    'settings.token.console.copy': '在同一页查看令牌状态、最近使用情况和撤销入口。',
+    'settings.token.console.result.total': '令牌总数',
+    'settings.token.console.result.active': '可用令牌',
+    'settings.token.console.result.expired': '过期令牌',
+    'settings.token.generate': '生成令牌',
+    'settings.token.generate-title': '生成令牌',
+    'settings.token.generated-title': '令牌已生成',
+    'settings.token.generate-fail': '生成令牌失败',
+    'settings.token.name-placeholder': '请输入令牌名称',
+    'settings.token.notice': '请立即复制此令牌。关闭弹窗后将不再显示完整令牌。',
+    'settings.token.delete-confirm': '确认删除令牌',
+    'settings.token.delete-confirm-content': '删除后使用此令牌的所有集成将无法访问,确认删除「{{name}}」?',
+    'settings.token.load-fail-title': '令牌列表暂时无法加载',
+    'settings.token.load-fail': '暂时无法读取令牌列表,请稍后重试或刷新页面。',
+    'settings.token.desc': '管理用于 API 集成访问的长期令牌,可随时创建或撤销。',
+    'settings.token.name': '令牌名称',
+    'settings.token.value': '令牌值',
+    'settings.token.creator': '创建者',
+    'settings.token.create-time': '创建时间',
+    'settings.token.expire-time': '过期时间',
+    'settings.token.expire.never': '永不过期',
+    'settings.token.last-used': '最近使用时间',
+    'settings.token.empty': '还没有令牌',
+    'settings.token.delete-action': '删除{{name}}',
     'setting.token.empty.title': '还没有令牌',
     'setting.token.empty.copy': '生成一个 API 令牌后即可接入采集、通知或自动化脚本。',
     'setting.token.expiration.never': '永不过期',
@@ -1537,6 +5303,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'setting.token.expiration.180d': '180 天',
     'setting.token.expiration.365d': '365 天',
     'status.loading': '正在加载状态页',
+    'status.page': '状态页面',
     'status.kicker': '状态页',
     'status.title': '状态页',
     'status.subtitle': '面向公开访问的组件健康和事件历史',
@@ -1558,6 +5325,21 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'status.mode.incidents': 'incidents',
     'status.tone.down': 'down',
     'status.component.default-title': '组件',
+    'status.component.state.0': '正常',
+    'status.component.state.1': '异常',
+    'status.component.state.2': '未知',
+    'status.incident.history': '事件历史',
+    'status.incident.state.0': '调查中',
+    'status.incident.state.1': '已确认',
+    'status.incident.state.2': '观察中',
+    'status.incident.state.3': '已恢复',
+    'status.incident.message.tip.0': '我们正在调查此事件问题,请稍后再查看更新。',
+    'status.incident.message.tip.1': '我们已确认故障发生的原因,正在处理中。',
+    'status.incident.message.tip.2': '我们正在监控该事件,观察处理效果',
+    'status.incident.message.tip.3': '该事件已被修复,谢谢理解支持。',
+    'status.public.org.state.0': '所有系统正常运行',
+    'status.public.org.state.1': '部分系统运行异常',
+    'status.public.org.state.2': '所有系统运行异常',
     'status.components.empty.title': '还没有组件',
     'status.components.empty.copy': '配置完成后,公开状态页组件会显示在这里。',
     'status.components.broadcast': '组件播报',
@@ -1572,10 +5354,13 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'status.org.meta': '公开组织',
     'status.public.history': '历史',
     'status.public.home': '主页',
+    'status.public.feedback': '问题反馈',
     'status.public.updated': '更新于',
     'status.public.year': '年份',
+    'status.public.uptime': '可用率',
     'status.public.component-history': '组件历史',
     'status.public.incident-range': '事件范围',
+    'status.public.incidents.load-failed': '加载公开事件失败',
     'status.public.today': '今天',
     'status.public.30-day': '30 天',
     'status.public.power-by': '由 HertzBeat 提供支持',
@@ -1584,28 +5369,346 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'status.incident.public.start-at': '开始时间',
     'status.incident.public.update-at': '更新时间',
     'status.components.view': '组件健康视图',
+    'observability.waterfall.span-label': '跨度',
+    'observability.waterfall.duration-label': '耗时',
+    'observability.waterfall.timeline-label': '时间轴',
+    'observability.waterfall.overview-title': '{{timeline}}总览',
+    'observability.waterfall.span-count': '{{count}} 个跨度',
+    'observability.waterfall.event-count': '{{count}} 个事件',
+    'trace.manage.loading': '正在加载链路工作台',
     'trace.manage.stat.error-ratio': '错误链路占比',
     'trace.manage.stat.activity': '活动状态',
     'trace.manage.stat.list-count': '列表条数',
+    'trace.center.console.title': '链路工作台',
     'trace.manage.workspace': '链路工作台',
     'trace.manage.payloads.title': '原始载荷',
+    'trace.manage.empty-selected-span.title': '选择一个跨度',
+    'trace.manage.empty-selected-span.copy': '选择左侧跨度后查看服务、状态和事件证据。',
     'trace.manage.selected-span.service-namespace': '服务与命名空间',
     'trace.manage.trace-state-empty': '暂无链路状态',
+    'trace.manage.event.fallback': '未命名事件',
+    'trace.manage.event.attributes.empty': '无属性',
+    'trace.manage.event.attributes.more': '还有 {{count}} 项',
+    'trace.manage.event.attributes.object': '对象属性',
+    'trace.manage.link.fallback': '关联链路',
+    'trace.manage.link.meta': '关联链路',
     'trace.manage.handoff.logs-disabled': '缺少 traceId,暂时不能查看关联日志。',
     'trace.manage.handoff.entity-disabled': '缺少实体 ID,暂时不能打开实体详情。',
+    'trace.manage.attribution-diagnostics.aria': '归因诊断 hertzbeat.entity_id hertzbeat.entity_name hertzbeat.workspace_id hertzbeat.collector hertzbeat.template',
+    'trace.manage.attribution-diagnostics.title': '归因诊断',
+    'trace.manage.attribution-diagnostics.present': '已提供',
+    'trace.manage.attribution-diagnostics.missing': '缺失',
+    'trace.manage.attribution-diagnostics.entity-id.present': '可打开实体详情',
+    'trace.manage.attribution-diagnostics.entity-id.missing': '缺少实体 ID,实体详情会保持禁用',
+    'trace.manage.attribution-diagnostics.entity-name.present': '用于展示实体名称',
+    'trace.manage.attribution-diagnostics.entity-name.missing': '缺少实体名称时使用服务名辅助检索',
+    'trace.manage.attribution-diagnostics.workspace-id.present': '工作区归属',
+    'trace.manage.attribution-diagnostics.workspace-id.missing': '缺少工作区字段时使用当前部署上下文',
+    'trace.manage.attribution-diagnostics.collector.present': '采集器来源',
+    'trace.manage.attribution-diagnostics.collector.missing': '缺少采集器字段时使用链路服务上下文排查',
+    'trace.manage.attribution-diagnostics.template.present': '监控模板归属',
+    'trace.manage.attribution-diagnostics.template.missing': '缺少模板字段时从实体或服务补齐模板归属',
+    'trace.manage.route.fallback.filtered-trace': '过滤链路',
+    'trace.manage.route.fallback.waiting-query': '等待查询',
+    'trace.manage.route.fallback.unselected': '未选择',
+    'trace.manage.route.kicker': '可观测',
+    'trace.manage.route.title': '链路工作台',
+    'trace.manage.route.subtitle': '按服务、链路 ID、跨度 ID 和错误状态筛选链路,直接查看趋势、列表和详情。',
+    'trace.manage.route.action.return-source': '返回来源',
+    'trace.manage.route.action.intake': '返回 OTLP 接入',
+    'trace.manage.route.action.collectors': '采集集群',
+    'trace.manage.route.action.entity': '实体详情',
+    'trace.manage.route.action.alerts': '告警处理',
+    'trace.manage.route.action.templates': '监控模板',
+    'trace.manage.route.action.catalog': '对象目录',
+    'trace.manage.route.query.service': '服务名称',
+    'trace.manage.route.query.trace-id': '链路 ID',
+    'trace.manage.route.query.span-id': '跨度 ID',
+    'trace.manage.route.query.status': '链路状态',
+    'trace.manage.route.query.status.all': '全部链路',
+    'trace.manage.route.query.status.error': '错误链路',
+    'trace.manage.route.query.run': '运行查询',
+    'trace.manage.route.query.reset': '重置',
+    'trace.manage.route.stat.total': '链路总数',
+    'trace.manage.route.stat.errors': '错误链路',
+    'trace.manage.route.stat.list-count': '列表条数',
+    'trace.manage.route.stat.latest': '最近时间',
+    'trace.manage.route.chart.title': '趋势带',
+    'trace.manage.route.chart.latest-window': '近 10 条链路',
+    'trace.manage.route.table.title': '最近链路',
+    'trace.manage.route.table.subtitle': '按当前查询条件展示',
+    'trace.manage.route.table.count': '{{count}} 条',
+    'trace.manage.route.table.header.start': '开始时间',
+    'trace.manage.route.table.header.service': '服务',
+    'trace.manage.route.table.header.root-span': '根跨度',
+    'trace.manage.route.table.header.duration': '耗时',
+    'trace.manage.route.table.header.status': '状态',
+    'trace.manage.route.table.header.trace-id': '链路 ID',
+    'trace.manage.route.empty.title': '暂无链路',
+    'trace.manage.route.empty.copy': '确认时间范围、实体归因、采集器和监控模板后再查看链路。',
+    'trace.manage.route.detail.title': '详情面板',
+    'trace.manage.route.detail.service': '服务',
+    'trace.manage.route.detail.namespace': '命名空间',
+    'trace.manage.route.detail.root-span': '根跨度',
+    'trace.manage.route.detail.duration': '耗时',
+    'trace.manage.route.evidence.aria': '链路证据 链路 ID 链路状态 开始时间 最近上报',
+    'trace.manage.route.evidence.title': '链路证据',
+    'trace.manage.route.evidence.current': '当前选中',
+    'trace.manage.route.evidence.trace-id': '链路 ID',
+    'trace.manage.route.evidence.trace-status': '链路状态',
+    'trace.manage.route.evidence.error-only': '仅看错误链路',
+    'trace.manage.route.evidence.current-filter': '当前筛选',
+    'trace.manage.route.evidence.start-time': '开始时间',
+    'trace.manage.route.evidence.root-span-start': '根跨度开始',
+    'trace.manage.route.evidence.latest-observed': '最近上报',
+    'trace.manage.route.evidence.latest-workbench-trace': '工作台最新链路',
+    'trace.manage.route.entity-context.aria': '实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源',
+    'trace.manage.route.entity-context.title': '实体上下文',
+    'trace.manage.route.handoff.alert-hint': '按当前实体、服务和已带入的链路上下文查看相关告警',
+    'trace.manage.route.handoff.signal-hint': '当前链路的 traceId、spanId 和服务上下文会带入日志与指标工作台',
+    'trace.manage.drawer.event-prefix': '事件',
+    'trace.manage.drawer.link-prefix': '关联',
+    'trace.manage.drawer.event-fact.title': '跨度事件',
+    'trace.manage.drawer.event-fact.fallback': '未命名事件',
+    'trace.manage.drawer.event-fact.meta': '不是新的跨度,是当前跨度上的时间点',
+    'trace.manage.drawer.event-fact.span': '所属跨度',
+    'trace.manage.drawer.event-fact.position': '事件位置',
+    'trace.manage.drawer.event-fact.timeline': '相对链路时间轴',
+    'trace.manage.drawer.event-fact.attributes': '事件属性',
+    'trace.manage.drawer.event-fact.no-attributes': '无属性',
+    'trace.manage.drawer.kicker': '链路详情',
+    'trace.manage.drawer.title-fallback': '链路瀑布',
+    'trace.manage.drawer.action.logs': '查看日志',
+    'trace.manage.drawer.action.metrics': '查看指标',
+    'trace.manage.drawer.action.entity': '实体详情',
+    'trace.manage.drawer.loading.title': '正在加载链路详情',
+    'trace.manage.drawer.loading.copy': '正在加载该链路的 Span、事件、关联关系和关联日志。',
+    'trace.manage.drawer.error.title': '加载链路详情失败',
+    'trace.manage.drawer.error.copy': '链路详情暂时不可用,请重试或调整查询条件。',
+    'trace.manage.drawer.span-count': '{{count}} 个跨度',
+    'trace.manage.drawer.stat.current-span': '当前跨度',
+    'trace.manage.drawer.stat.error-spans': '错误跨度',
+    'trace.manage.drawer.stat.events': '事件',
+    'trace.manage.drawer.stat.links': '关联',
+    'trace.manage.drawer.waterfall.span': '跨度',
+    'trace.manage.drawer.waterfall.duration': '耗时',
+    'trace.manage.drawer.waterfall.timeline': '时间轴',
+    'trace.manage.drawer.event-detail.title': '事件详情',
+    'trace.manage.drawer.event-detail.copy': '不是新的跨度,是当前跨度上的时间点',
+    'trace.manage.drawer.event-detail.action.span': '查看跨度',
+    'trace.manage.drawer.empty.title': '选择一条链路',
+    'trace.manage.drawer.empty.copy': '从链路列表打开详情后,会在这里展示 waterfall 和跨度证据。',
+    'explorer.kicker': '可观测',
+    'explorer.title': '查询工作台',
+    'explorer.subtitle': '统一检索链路、日志和指标结果,围绕查询栏、趋势带、表格和详情面板完成定位。',
+    'explorer.tags.unified-query': '统一查询',
+    'explorer.tags.context-retention': '上下文保留',
+    'explorer.tags.cross-signal-search': '跨信号检索',
+    'explorer.focus': '跨信号查询入口',
+    'explorer.summary': '保持冷启动工作台的紧凑密度,并保留进入 HertzBeat 链路、日志、指标和对象目录的排障跳转。',
+    'explorer.lanes.query.title': '查询栏',
+    'explorer.lanes.query.copy': '先捕获服务、状态和资源属性,再进入具体信号证据。',
+    'explorer.lanes.query.meta': '查询栏契约',
+    'explorer.lanes.results.title': '结果面板',
+    'explorer.lanes.results.copy': '把链路、日志和指标结果放在一张高密度表里对比。',
+    'explorer.lanes.results.meta': '结果面板',
+    'explorer.lanes.drilldown.title': '上下文跳转',
+    'explorer.lanes.drilldown.copy': '保留进入链路、日志、指标和对象目录的链接。',
+    'explorer.lanes.drilldown.meta': '上下文跳转',
+  'explorer.checklist.entry.title': '路由入口',
+  'explorer.checklist.entry.copy': '路由只拥有冷启动工作台入口,交互留在客户端岛内。',
+  'explorer.checklist.entry.meta': '已完成',
+  'explorer.checklist.adapters.title': '适配器',
+  'explorer.checklist.adapters.copy': '静态收敛完成后再接入真实后端查询适配器。',
+  'explorer.checklist.adapters.meta': '下一步',
+  'explorer.checklist.links.title': '信号链接',
+  'explorer.checklist.links.copy': '链路、日志、指标和对象目录跳转地址保持稳定。',
+  'explorer.checklist.links.meta': '预留',
+    'explorer.actions.save-view': '保存视图',
+    'explorer.actions.create-alert': '创建告警',
+    'explorer.actions.add-dashboard': '加入仪表盘',
+    'explorer.query.aria': '查询条件',
+    'explorer.query.placeholder': '输入服务、状态或资源属性过滤条件',
+    'explorer.query.default': 'service.name = "checkout" AND status = "错误"',
+    'explorer.query.run': '运行查询',
+    'explorer.signal.aria': '信号类型',
+    'explorer.signal.all': '全部信号',
+    'explorer.sort.aria': '排序方式',
+    'explorer.sort.time-desc': '时间倒序',
+    'explorer.sort.duration-desc': '耗时倒序',
+    'explorer.filters.title': '快速筛选',
+    'explorer.filters.clear': '清除',
+    'explorer.filters.signal-type': '信号类型',
+    'explorer.filters.deployment-environment': '部署环境',
+    'explorer.filters.service-name': '服务名称',
+    'explorer.filters.status': '状态',
+    'explorer.chart.title': '趋势带',
+    'explorer.chart.subtitle': '近 30 分钟跨信号命中量',
+    'explorer.chart.result-count': '结果 {{count}}',
+    'explorer.results.title': '跨信号结果',
+    'explorer.results.count': '{{count}} 条',
+    'explorer.table.signal-type': '信号类型',
+    'explorer.table.service': '服务',
+    'explorer.table.operation': '操作',
+    'explorer.table.status': '状态',
+    'explorer.table.duration': '耗时',
+    'explorer.table.time': '时间',
+    'explorer.detail.title': '详情面板',
+    'explorer.detail.signal': '信号',
+    'explorer.detail.status': '状态',
+    'explorer.detail.duration': '耗时',
+    'explorer.handoff.trace': '查看链路',
+    'explorer.handoff.log': '查看日志',
+    'explorer.handoff.metrics': '查看指标',
+    'explorer.handoff.entities': '对象目录',
+    'explorer.rows.trace.signal': '链路',
+    'explorer.rows.log.signal': '日志',
+    'explorer.rows.metric.signal': '指标',
+    'explorer.rows.exception.signal': '异常',
+    'explorer.rows.log.operation': '支付失败:余额不足',
+    'explorer.status.error': '错误',
+    'explorer.status.normal': '正常',
     'otlp.metrics.handoff.entity-disabled': '缺少实体 ID,暂时不能打开实体详情。',
     'otlp.protocol.http': 'HTTP',
     'otlp.protocol.grpc': 'gRPC',
     'otlp.guide.auth-header': 'Auth header',
     'otlp.guide.grpc-authority': 'gRPC authority',
+    'otlp.guide.fallback.metrics.summary': '通过 Collector 或 SDK 推送指标,再绑定到 HertzBeat 监控模板。',
+    'otlp.guide.fallback.logs.summary': '通过 OTLP 日志上报进入日志工作台,并参与实体归并。',
+    'otlp.guide.fallback.traces.summary': '通过 OTLP 链路上报生成服务调用关系和拓扑证据。',
     'otlp.loading': '加载 OTLP 接入',
     'otlp.kicker': 'OTLP',
     'otlp.title': '可观测接入',
+    'ingestion.otlp.metrics.title': '指标工作台',
     'otlp.subtitle': '在这里查看指标、日志和链路的接入方式,并确认最近是否收到数据。',
     'otlp.fact.active-signals': '已接入信号数',
     'otlp.fact.recent-services': '最近识别服务数',
     'otlp.fact.bound-entities': '已绑定实体数',
     'otlp.fact.latest': '最近观测',
+    'otlp.readiness.signals.title': '三信号接入',
+    'otlp.readiness.signals.copy': '{{active}} / {{total}} 活跃',
+    'otlp.readiness.latest.title': '最近上报',
+    'otlp.readiness.latest.empty': '暂无上报',
+    'otlp.readiness.latest.received': '已收到遥测',
+    'otlp.readiness.latest.waiting': '等待首条遥测',
+    'otlp.readiness.entity.title': '实体归因',
+    'otlp.readiness.entity.copy': '{{count}} 个实体',
+    'otlp.readiness.entity.meta': '对象目录',
+    'otlp.readiness.discovery.title': '服务发现',
+    'otlp.readiness.discovery.copy': '{{count}} 个服务',
+    'otlp.readiness.discovery.meta': '最近 24 小时',
+    'otlp.collection-loop.otlp-intake.title': 'OTLP 三信号接入',
+    'otlp.collection-loop.otlp-intake.copy': '接入 OpenTelemetry 指标、日志和链路,再进入对应工作台排查。',
+    'otlp.collection-loop.otlp-intake.meta': '三信号',
+    'otlp.collection-loop.traditional-monitoring.title': '传统监控资源',
+    'otlp.collection-loop.traditional-monitoring.copy': '继续保留主机、数据库、中间件、网络设备等模板化监控。',
+    'otlp.collection-loop.traditional-monitoring.meta': '已有资源',
+    'otlp.collection-loop.collector-cluster.title': '采集器集群',
+    'otlp.collection-loop.collector-cluster.copy': '管理私有化部署中的采集节点、任务分发和接入状态。',
+    'otlp.collection-loop.collector-cluster.meta': 'Collector',
+    'otlp.collection-loop.monitoring-template.title': '监控模板',
+    'otlp.collection-loop.monitoring-template.copy': '维护多协议采集模板,让传统监控和 OTLP 实体归到同一对象。',
+    'otlp.collection-loop.monitoring-template.meta': '模板',
+    'otlp.collection-loop.service-discovery.title': '服务发现',
+    'otlp.collection-loop.service-discovery.copy': '把新发现的服务、资源和遥测身份确认到 HertzBeat 实体。',
+    'otlp.collection-loop.service-discovery.meta': '发现',
+    'otlp.collection-loop.object-directory.title': '对象目录',
+    'otlp.collection-loop.object-directory.copy': '围绕实体查看资源、三信号、拓扑和告警处理上下文。',
+    'otlp.collection-loop.object-directory.meta': '实体',
+    'otlp.source.section.quickstart': '快速开始',
+    'otlp.source.section.migrate': '现有接入',
+    'otlp.source.section.apm-traces': 'APM / 链路',
+    'otlp.source.section.logs': '日志',
+    'otlp.source.section.metrics': '指标',
+    'otlp.source.item.demo-data.label': '5 分钟内接入演示数据',
+    'otlp.source.item.demo-data.description': '写入一条演示 OTLP 数据流,并立即查看工作台。',
+    'otlp.source.item.open-telemetry.label': '已有 OpenTelemetry',
+    'otlp.source.item.open-telemetry.description': '复用现有 OpenTelemetry Collector 或 SDK 管道。',
+    'otlp.source.item.grafana.description': '将 Grafana 侧的信号接入 HertzBeat。',
+    'otlp.source.item.elk.description': '把日志源路由到日志工作台。',
+    'otlp.source.item.new-relic.description': '把服务遥测映射到对象目录。',
+    'otlp.source.item.commercial-observability.label': '已有可观测平台',
+    'otlp.source.item.commercial-observability.description': '接入服务、日志和指标后,在对象目录和排障工作台继续处理。',
+    'otlp.source.item.honeycomb.description': '把链路数据接入链路工作台,并关联服务与日志。',
+    'otlp.source.item.self-hosted-observability.label': '自托管 OTLP 管道',
+    'otlp.source.item.self-hosted-observability.description': '复用内网或离线环境中的 OTLP Collector 管道。',
+    'otlp.source.item.java.description': 'Java OpenTelemetry SDK 配置。',
+    'otlp.source.item.python.description': 'Python OpenTelemetry SDK 配置。',
+    'otlp.source.item.nodejs.description': 'Node.js 与浏览器链路配置。',
+    'otlp.source.item.golang.description': 'Go 服务链路配置。',
+    'otlp.source.item.dotnet.description': '.NET OpenTelemetry 埋点配置。',
+    'otlp.source.item.deno.description': 'Deno 运行时链路配置。',
+    'otlp.source.item.php.description': 'PHP 应用链路配置。',
+    'otlp.source.item.ruby.description': 'Ruby 服务链路配置。',
+    'otlp.source.item.rust.description': 'Rust OpenTelemetry 配置。',
+    'otlp.source.item.nginx-tracing.label': 'Nginx 链路',
+    'otlp.source.item.nginx-tracing.description': '追踪反向代理和边缘服务调用。',
+    'otlp.source.item.external-api.label': '外部 API 监控',
+    'otlp.source.item.external-api.description': '跟踪外部 API 延迟和错误。',
+    'otlp.source.item.web-vitals.label': 'Web Vitals 链路',
+    'otlp.source.item.web-vitals.description': '前端链路和核心体验指标配置。',
+    'otlp.source.item.temporal.description': '追踪 Temporal 工作流和活动。',
+    'otlp.source.item.kubernetes-pod-logs.label': 'Kubernetes Pod 日志',
+    'otlp.source.item.kubernetes-pod-logs.description': '通过 OTLP 采集 Kubernetes Pod 日志。',
+    'otlp.source.item.docker-container-logs.label': 'Docker 容器日志',
+    'otlp.source.item.docker-container-logs.description': '把容器日志转发到 HertzBeat。',
+    'otlp.source.item.fluentbit.description': '使用 Fluent Bit 发送日志。',
+    'otlp.source.item.fluentd.description': '使用 FluentD 发送日志。',
+    'otlp.source.item.syslog.description': '规范化 Syslog 数据流。',
+    'otlp.source.item.systemd-logs.label': 'Systemd 日志',
+    'otlp.source.item.systemd-logs.description': '采集 systemd journal 日志。',
+    'otlp.source.item.cloudflare-logs.label': 'Cloudflare 日志',
+    'otlp.source.item.cloudflare-logs.description': '转发 Cloudflare 边缘日志。',
+    'otlp.source.item.vercel-logs.label': 'Vercel 日志',
+    'otlp.source.item.vercel-logs.description': '转发 Vercel 部署日志。',
+    'otlp.source.item.heroku-logs.label': 'Heroku 日志',
+    'otlp.source.item.heroku-logs.description': '转发 Heroku 应用日志。',
+    'otlp.source.item.http-logs.label': 'HTTP 日志上报',
+    'otlp.source.item.http-logs.description': '通过 HTTP 推送应用日志。',
+    'otlp.source.item.deno-logs.label': 'Deno 日志',
+    'otlp.source.item.deno-logs.description': '采集 Deno 运行时日志。',
+    'otlp.source.item.prometheus.description': '把 Prometheus 指标桥接到 OTLP。',
+    'otlp.source.item.otel-metrics.label': 'OpenTelemetry 指标',
+    'otlp.source.item.otel-metrics.description': '检查 OTLP 指标数据流。',
+    'otlp.source.item.host-metrics.label': '主机指标',
+    'otlp.source.item.host-metrics.description': '采集主机和进程指标。',
+    'otlp.source.item.jmx-metrics.label': 'JMX 指标',
+    'otlp.source.item.jmx-metrics.description': '采集 JVM 和 JMX 指标。',
+    'otlp.source.item.nginx-metrics.label': 'Nginx 指标',
+    'otlp.source.item.nginx-metrics.description': '采集 Nginx 服务指标。',
+    'otlp.source.filter.all': '全部',
+    'otlp.source.filter.title': '筛选',
+    'otlp.source.search.placeholder': '搜索数据源、云服务、SDK 或运行时',
+    'otlp.source.search.label': '筛选',
+    'otlp.source.search.empty': '未找到匹配的数据源',
+    'otlp.ribbon.intake.label': 'OTLP 接入',
+    'otlp.ribbon.intake.detail': '活跃信号',
+    'otlp.ribbon.metrics.label': '指标工作台',
+    'otlp.ribbon.metrics.detail': '指标通道',
+    'otlp.ribbon.logs.label': '日志工作台',
+    'otlp.ribbon.logs.detail': '日志通道',
+    'otlp.ribbon.traces.label': '链路工作台',
+    'otlp.ribbon.traces.detail': '链路通道',
+    'otlp.stepper.source': '选择 OTLP 数据源',
+    'otlp.stepper.protocol': '配置协议与令牌',
+    'otlp.stepper.workbench': '进入信号工作台',
+    'otlp.hero.title': 'OTLP 协议接入',
+    'otlp.hero.copy': '选择 OpenTelemetry/OTLP 数据源,配置指标、日志或链路的接入路径,并绑定到 HertzBeat 对象。',
+    'otlp.action.return-entity': '返回对象',
+    'otlp.action.view-entities': '查看实体',
+    'otlp.section.collection-loop.kicker': '采集体系',
+    'otlp.section.collection-loop.title': '现有接入闭环',
+    'otlp.section.collection-loop.copy': 'OTLP 接入只是采集体系的一条路径;传统监控、采集器、模板、服务发现和对象目录继续作为统一运维入口。',
+    'otlp.section.readiness.kicker': '接入自检',
+    'otlp.section.readiness.title': '真实状态',
+    'otlp.section.readiness.copy': '展示已确认的三信号、实体归因和最近上报;未接入的健康项不显示为空指标。',
+    'otlp.section.candidates.kicker': '实体发现',
+    'otlp.section.candidates.title': '候选实体',
+    'otlp.section.candidates.copy': 'OTLP 资源身份已进入确认队列,保留原始信号证据。',
+    'otlp.section.candidates.pending': '待确认',
+    'otlp.section.self-check.kicker': '运行自检',
+    'otlp.section.self-check.title': 'Collector、存储和查询',
+    'otlp.section.self-check.copy': '检查采集节点、历史存储、查询服务和 GreptimeDB 连接状态。',
     'otlp.manage-token': '管理令牌',
     'otlp.open-discovery': '进入遥测发现',
     'otlp.section.overview.title': '最近接入',
@@ -1629,6 +5732,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.section.console.title': '指标工作台',
     'otlp.metrics.section.console.copy': '沿用同一组调查上下文继续查看 OTLP 指标',
     'otlp.metrics.section.context.title': '排障上下文',
+    'otlp.metrics.route.action.return-source': '返回来源',
     'otlp.metrics.empty': '当前查询和时间范围内还没有匹配到 OTLP 指标。',
     'otlp.metrics.query-ready': '指标上下文已就绪',
     'otlp.metrics.query-context': '排障上下文',
@@ -1636,22 +5740,268 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.series.unknown-service': 'Unknown service',
     'otlp.metrics.series.empty': '当前结果里还没有可展示的指标序列。',
     'otlp.metrics.series.empty.copy': '请尝试扩大时间范围或修改查询。',
-    'actions.focus': 'Automation inventory and execution guardrails',
-    'actions.subtitle': 'Automation catalog, execution history, and approval-aware operations all stay in one context.',
-    'actions.summary': 'The actions entry page carries automation catalog, execution history, and approval semantics without requiring a page redesign for future data.',
-    'actions.checklist.entry.title': 'Entry shell ready',
-    'actions.checklist.entry.copy': 'The action catalog already uses the shared workbench language.',
-    'actions.checklist.adapters.title': 'Execution adapters',
-    'actions.checklist.adapters.copy': 'Action list, run result, and approval flow keep the current integration boundary.',
-    'actions.checklist.context.title': 'Context continuity',
-    'actions.checklist.context.copy': 'Jump to the actions page directly from entity, alert, or topology context.',
-    'actions.lanes.catalog.title': 'Automation catalog',
-    'actions.lanes.catalog.copy': 'Treat action definitions, target scope, and risk level as one browsable object set.',
-    'actions.lanes.history.title': 'Execution history',
-    'actions.lanes.history.copy': 'The history rail is fixed and execution records plug directly into the same evidence flow.',
-    'actions.lanes.approval.title': 'Approval semantics',
-    'actions.lanes.approval.copy': 'Reserve gate and guidance copy for high-risk actions in the main CTA region.',
+    'otlp.metrics.stats.total-series': '指标序列',
+    'otlp.metrics.stats.non-empty-series': '有数据序列',
+    'otlp.metrics.stats.datasource': '存储来源',
+    'otlp.metrics.stats.latest-observed': '最近上报',
+    'otlp.metrics.stats.series-total': '序列总数',
+    'otlp.metrics.stats.intake-state': '接入状态',
+    'otlp.metrics.header.kicker': '可观测 / 指标',
+    'otlp.metrics.field.service': '服务',
+    'otlp.metrics.field.service-name': '服务名称',
+    'otlp.metrics.field.namespace': '命名空间',
+    'otlp.metrics.field.environment': '环境',
+    'otlp.metrics.field.group-by': '分组',
+    'otlp.metrics.field.trace-id': '链路 ID',
+    'otlp.metrics.group.service': '服务',
+    'otlp.metrics.group.namespace': '命名空间',
+    'otlp.metrics.group.environment': '环境',
+    'otlp.metrics.scope.service': '服务范围',
+    'otlp.metrics.scope.all-services': '全部服务',
+    'otlp.metrics.scope.all-namespaces': '全部命名空间',
+    'otlp.metrics.scope.all-environments': '全部环境',
+    'otlp.metrics.scope.series': '序列',
+    'otlp.metrics.scope.series-count': '{{count}} 条',
+    'otlp.metrics.query.aria': '指标查询',
+    'otlp.metrics.query.run': '运行查询',
+    'otlp.metrics.query.unselected': '未选择指标',
+    'otlp.metrics.aggregation.aria': '聚合方式',
+    'otlp.metrics.aggregation.avg': '平均',
+    'otlp.metrics.aggregation.sum': '求和',
+    'otlp.metrics.aggregation.max': '最大',
+    'otlp.metrics.aggregation.min': '最小',
+    'otlp.metrics.explorer.chart-label': '{{count}} 条有数据序列',
+    'otlp.metrics.explorer.empty-title': '暂无指标序列',
+    'otlp.metrics.explorer.no-metrics-title': '确认时间范围、实体归因、采集器和监控模板后再查看指标。',
+    'otlp.metrics.explorer.waiting-ingest': '等待 OTLP 指标写入',
+    'otlp.metrics.explorer.series-count': '{{count}} 条序列',
+    'otlp.metrics.context.current-metric': '当前指标',
+    'otlp.metrics.context.handoff-destination': '排障去向',
+    'otlp.metrics.context.handoff-copy': '关联实体、日志、链路和告警处理。',
+    'otlp.metrics.context.handoff-meta': '按服务、模板和时间范围继续定位',
+    'otlp.metrics.context.current-service': '当前服务',
+    'otlp.metrics.context.time-range': '时间范围',
+    'otlp.metrics.series.entity-id': 'entityId {{entityId}}',
+    'otlp.metrics.series.entity-missing': '等待实体归因',
+    'otlp.metrics.series.context.metric-name': '指标名称',
+    'otlp.metrics.series.context.selected-series': '当前选中序列',
+    'otlp.metrics.series.context.entity': '关联实体',
+    'otlp.metrics.series.context.service': '当前服务',
+    'otlp.metrics.series.context.service-context': '服务上下文',
+    'otlp.metrics.series.context.template': '采集模板',
+    'otlp.metrics.series.context.collector': '采集器 {{collector}}',
+    'otlp.metrics.series.context.monitor-template': '监控模板',
+    'otlp.metrics.series.context.environment': '当前环境',
+    'otlp.metrics.series.context.deployment-environment': '部署环境',
+    'otlp.metrics.evidence.samples': '采样点',
+    'otlp.metrics.evidence.empty-skipped': '{{count}} 个空值已跳过',
+    'otlp.metrics.evidence.real-samples': '真实采样点',
+    'otlp.metrics.evidence.latest-value': '最新值',
+    'otlp.metrics.evidence.waiting-latest': '等待最新采样',
+    'otlp.metrics.evidence.value-range': '值域',
+    'otlp.metrics.evidence.average': '平均 {{average}}',
+    'otlp.metrics.evidence.waiting-real-samples': '等待真实采样',
+    'otlp.metrics.evidence.sample-window': '采样窗口',
+    'otlp.metrics.evidence.real-sample-time': '真实采样时间',
+    'otlp.metrics.evidence.waiting-sample-time': '等待采样时间',
+    'otlp.metrics.evidence.linked-trace': '关联链路',
+    'otlp.metrics.evidence.missing-span': '未提供 span_id',
+    'otlp.metrics.evidence.missing-trace-span': '缺少 trace_id/span_id',
+    'otlp.metrics.handoff.logs': '历史日志',
+    'otlp.metrics.handoff.logs-by-trace': '按链路查看',
+    'otlp.metrics.handoff.logs-by-service': '按服务查看',
+    'otlp.metrics.handoff.logs-current-span': '历史日志会定位到当前 span',
+    'otlp.metrics.handoff.logs-current-trace': '历史日志会定位到当前链路',
+    'otlp.metrics.handoff.logs-service-filter': '缺少链路 ID 时按服务筛选',
+    'otlp.metrics.handoff.logs-waiting-context': '等待服务或链路上下文',
+    'otlp.metrics.handoff.traces': '链路瀑布图',
+    'otlp.metrics.handoff.trace-open': '可打开',
+    'otlp.metrics.handoff.trace-waiting-id': '等待链路 ID',
+    'otlp.metrics.handoff.trace-full-current-span': '打开完整链路并保留当前 span',
+    'otlp.metrics.handoff.trace-full': '打开完整链路',
+    'otlp.metrics.handoff.trace-missing-id': '指标没有链路 ID,暂不能定位链路',
+    'otlp.metrics.handoff.alerts': '告警处理',
+    'otlp.metrics.handoff.alerts-by-entity': '带实体处理',
+    'otlp.metrics.handoff.alerts-by-service': '按服务处理',
+    'otlp.metrics.handoff.alerts-by-entity-meta': '按实体、服务和指标进入告警',
+    'otlp.metrics.handoff.alerts-by-service-meta': '缺少实体 ID 时按服务进入告警',
+    'otlp.metrics.attribution.entity-id.present': '可打开实体详情',
+    'otlp.metrics.attribution.entity-id.missing': '缺少实体 ID,实体详情会保持禁用',
+    'otlp.metrics.attribution.entity-name': '用于展示实体名称',
+    'otlp.metrics.attribution.workspace-id': '缺少工作区字段时使用当前部署上下文',
+    'otlp.metrics.attribution.collector': '采集器来源',
+    'otlp.metrics.attribution.template': '监控模板归属',
+    'actions.focus': '自动化清单与执行护栏',
+    'actions.subtitle': '自动化目录、执行历史和审批语义保持在同一上下文。',
+    'actions.summary': '自动化入口页在后续数据落地前,先承载动作目录、执行历史和审批语义。',
+    'actions.tag.catalog': '自动化目录',
+    'actions.tag.risk-aware': '风险感知动作',
+    'actions.tag.approval-flow': '审批流',
+    'actions.metric.catalog-entries': '目录动作',
+    'actions.metric.pending-approvals': '待审批',
+    'actions.metric.recent-runs': '近期执行',
+    'actions.metric.high-risk-actions': '高风险动作',
+    'actions.catalog.restart.name': '重启 checkout 部署',
+    'actions.catalog.restart.category': '运行时恢复',
+    'actions.catalog.restart.scope': '服务 checkout / prod-ap',
+    'actions.catalog.restart.owner': '平台运维',
+    'actions.catalog.restart.last-run': '12 分钟前',
+    'actions.catalog.restart.posture': '需要审批和回滚说明',
+    'actions.catalog.replay.name': '重放延迟的 OTLP 指标',
+    'actions.catalog.replay.category': '信号补救',
+    'actions.catalog.replay.scope': 'OTLP 网关 / 指标',
+    'actions.catalog.replay.owner': '可观测性团队',
+    'actions.catalog.replay.last-run': '38 分钟前',
+    'actions.catalog.replay.posture': '当前重放窗口内可安全执行',
+    'actions.catalog.mute.name': '静默边缘饱和告警',
+    'actions.catalog.mute.category': '告警治理',
+    'actions.catalog.mute.scope': '实体 edge-proxy',
+    'actions.catalog.mute.owner': 'SRE 值班',
+    'actions.catalog.mute.last-run': '2 小时前',
+    'actions.catalog.mute.posture': '30 分钟后自动过期',
+    'actions.run.restart.name': '重启 checkout 部署',
+    'actions.run.restart.target': 'checkout / prod-ap',
+    'actions.run.restart.actor': 'li.na',
+    'actions.run.restart.duration': '待审批',
+    'actions.run.replay.name': '重放延迟的 OTLP 指标',
+    'actions.run.replay.target': 'gateway metrics shard-3',
+    'actions.run.replay.actor': 'automation',
+    'actions.run.replay.duration': '06m 12s',
+    'actions.run.mute.name': '静默边缘饱和告警',
+    'actions.run.mute.target': 'edge-proxy / cn-hz',
+    'actions.run.mute.actor': 'lin.qi',
+    'actions.run.mute.duration': '01m 42s',
+    'actions.approval.restart.summary': '重启生产 checkout Pod 以处理饱和告警突增',
+    'actions.approval.restart.owner': 'checkout-oncall',
+    'actions.approval.restart.evidence': '已附加告警风暴和链路延迟回退证据。',
+    'actions.approval.replay.summary': '重放延迟采集队列的 OTLP 指标',
+    'actions.approval.replay.owner': 'obs-admin',
+    'actions.approval.replay.evidence': '采集器积压已稳定,重放窗口已打开。',
+    'actions.entry.kicker': '自动化入口',
+    'actions.entry.title': '自动化处置',
+    'actions.entry.subtitle': '按 OTLP 工作台的冷色基线统一入口、上下文和审批语义。',
+    'actions.entry.action.overview': '打开概览',
+    'actions.entry.action.entities': '查看对象',
+    'actions.entry.shell.eyebrow': '冷色入口已接入',
+    'actions.entry.shell.copy': '此入口继承 OTLP 接入页的近黑画布、小按钮、硬边面板和静态右栏节奏,后续只需要接入执行适配器。',
+    'actions.entry.chip.catalog': '自动化目录',
+    'actions.entry.chip.risk': '风险动作',
+    'actions.entry.chip.approval': '审批流',
+    'actions.entry.checklist.title': '接入清单',
+    'actions.entry.checklist.context.title': '统一入口上下文',
+    'actions.entry.checklist.context.copy': '已和 OTLP 基线共用同一套页面节奏。',
+    'actions.entry.checklist.adapter.title': '接入执行适配器',
+    'actions.entry.checklist.adapter.copy': '等动作目录和审批流数据落地后直接挂载。',
+    'actions.entry.checklist.evidence.title': '保留证据跳转',
+    'actions.entry.checklist.evidence.copy': '实体、告警和链路入口保持在同一操作上下文。',
+    'actions.entry.empty.title': '等待接入执行适配器',
+    'actions.entry.empty.copy': '视觉骨架已经统一到 OTLP 基线,后续数据接入可以增量完成。',
+    'actions.entry.suggested.title': '建议动作',
+    'actions.entry.suggested.copy': '来自告警、实体和三信号上下文,只生成建议,不自动执行。',
+    'actions.entry.suggested.evidence': '查看证据',
+    'actions.entry.suggested.confirm': '人工确认后执行',
+    'actions.approval-draft.title': '审批草稿适配器',
+    'actions.approval-draft.copy': '通过安全 BFF 从第一条建议动作生成人工审批草稿,不会执行动作。',
+    'actions.approval-draft.create': '生成审批草稿',
+    'actions.approval-draft.pending': '正在生成草稿',
+    'actions.approval-draft.success': '审批草稿已生成',
+    'actions.approval-draft.failed': '审批草稿生成失败',
+    'actions.approval-draft.disabled': '需要告警、实体或链路上下文后才能生成审批草稿。',
+    'actions.approval-draft-queue.title': '审批草稿队列',
+    'actions.approval-draft-queue.copy': '通过安全 BFF 读取审批草稿,让操作员在执行适配器出现前看到 manager 生命周期状态。',
+    'actions.approval-draft-queue.loading': '正在读取审批草稿',
+    'actions.approval-draft-queue.empty': '暂未从 manager 读取到审批草稿。',
+    'actions.approval-decision.title': '审批决策适配器',
+    'actions.approval-decision.copy': '通过安全 BFF 为草稿记录批准或拒绝决策,不会执行动作。',
+    'actions.approval-decision.approve': '批准草稿',
+    'actions.approval-decision.reject': '拒绝草稿',
+    'actions.approval-decision.pending': '正在记录决策',
+    'actions.approval-decision.success': '审批决策已记录',
+    'actions.approval-decision.failed': '审批决策记录失败',
+    'actions.approval-decision.disabled': '先生成审批草稿后才能记录决策。',
+    'actions.catalog-adapter.title': '动作目录适配器',
+    'actions.catalog-adapter.copy': '先读取只能生成人工审批草稿的动作目录,再让审批草稿引用后端持久目录。',
+    'actions.catalog-adapter.loading': '正在读取动作目录',
+    'actions.catalog-adapter.empty': '暂未读取到 manager 动作目录。',
+    'actions.adapter-boundary.label': '执行边界',
+    'actions.adapter-boundary.copy': '动作目录、执行历史和审批流当前是 roadmap 示例快照,还未接入执行适配器,不代表实时运行状态;告警上下文建议是当前证据生成的人工交接,不会自动执行。',
+    'actions.adapter-boundary.roadmap.workflow-automation': '工作流自动化',
+    'actions.adapter-boundary.roadmap.action-catalog': '动作目录',
+    'actions.adapter-boundary.roadmap.app-builder': '应用编排器',
+    'actions.adapter-boundary.roadmap.self-service-actions': '自助动作',
+    'actions.adapter-boundary.roadmap.approvals': '审批流',
+    'actions.adapter-boundary.roadmap.scripts': '脚本执行',
+    'actions.adapter-boundary.roadmap.runbook-orchestration': '处置手册编排',
+    'actions.snapshot.label': '示例快照',
+    'actions.suggestion.target.current-alert': '当前告警',
+    'actions.suggestion.target.entity-id': '实体 {{entityId}}',
+    'actions.suggestion.evidence.source': '来源 {{value}}',
+    'actions.suggestion.evidence.signal': '信号 {{value}}',
+    'actions.suggestion.evidence.trace': 'trace {{value}}',
+    'actions.suggestion.evidence.alert-group': '告警组 {{value}}',
+    'actions.suggestion.evidence.current-alert': '当前告警证据',
+    'actions.suggestion.source.alert': '告警中心',
+    'actions.suggestion.source.entity': '实体目录',
+    'actions.suggestion.source.topology': '拓扑',
+    'actions.suggestion.source.monitor': '监控中心',
+    'actions.suggestion.source.unknown': '未知来源 {{source}}',
+    'actions.suggestion.signal.metrics': '指标',
+    'actions.suggestion.signal.logs': '日志',
+    'actions.suggestion.signal.traces': '链路',
+    'actions.suggestion.signal.alerts': '告警',
+    'actions.suggestion.signal.unknown': '未知信号 {{signal}}',
+    'actions.suggestion.restart.title': '建议重启 {{target}}',
+    'actions.suggestion.restart.copy': '仅作为高风险恢复建议,执行前需要确认影响范围、回滚窗口和负责人。',
+    'actions.suggestion.silence.title': '建议为 {{target}} 创建临时静默',
+    'actions.suggestion.silence.copy': '用于短时降噪,先确认告警已接手并设置自动过期。',
+    'actions.suggestion.runbook.title': '建议查看 {{target}} 处置手册',
+    'actions.suggestion.runbook.copy': '先对齐实体、告警、链路和采集器证据,再决定是否执行动作。',
+    'actions.suggestion.manual-required': '只生成建议,人工确认后才能进入执行。',
+    'actions.risk.high': '高风险',
+    'actions.risk.medium': '中风险',
+    'actions.risk.low': '低风险',
+    'actions.run-status.awaiting-approval': '等待审批',
+    'actions.run-status.running': '运行中',
+    'actions.run-status.completed': '已完成',
+    'actions.approval-status.pending': '等待负责人决策',
+    'actions.approval-status.approved': '已批准',
+    'actions.surface.kicker': '自动化控制面',
+    'actions.surface.handoff.entity.title': '实体上下文交接',
+    'actions.surface.handoff.entity.copy': '扩大动作目标范围前,先打开实体目录核对。',
+    'actions.surface.handoff.monitor.title': '监控姿态交接',
+    'actions.surface.handoff.monitor.copy': '下发恢复动作前先校验信号健康状态。',
+    'actions.surface.handoff.approval.title': '审批证据交接',
+    'actions.surface.handoff.approval.copy': '将告警、链路和回滚说明保留在同一操作流程中。',
+    'actions.checklist.entry.title': '入口壳层就绪',
+    'actions.checklist.entry.copy': '动作目录已经沿用共享工作台语言。',
+    'actions.checklist.entry.meta': '已完成',
+    'actions.checklist.adapters.title': '执行适配器',
+    'actions.checklist.adapters.copy': '动作列表、执行结果和审批流保持当前集成边界。',
+    'actions.checklist.adapters.meta': '下一步',
+    'actions.checklist.context.title': '上下文连续性',
+    'actions.checklist.context.copy': '可直接从实体、告警或拓扑上下文跳转到动作页。',
+    'actions.checklist.context.meta': '预留',
+    'actions.lanes.catalog.title': '自动化目录',
+    'actions.lanes.catalog.copy': '将动作定义、目标范围和风险级别作为一组可浏览对象处理。',
+    'actions.lanes.history.title': '执行历史',
+    'actions.lanes.history.copy': '历史栏位置固定,执行记录接入后会进入同一证据流。',
+    'actions.lanes.approval.title': '审批语义',
+    'actions.lanes.approval.copy': '在主操作区为高风险动作保留审批门禁和引导文案。',
     'menu.dashboard.back': '打开总览',
+    'menu.entity.center': '实体中心',
+    'entity.detail': '实体详情',
+    'menu.monitor.bigdata': '大数据监控',
+    'menu.monitor.cache': '缓存监控',
+    'menu.monitor.center': '监控中心',
+    'menu.monitor.custom': '自定义监控',
+    'menu.monitor.db': '数据库监控',
+    'menu.monitor.llm': 'AI大模型',
+    'menu.monitor.mid': '中间件监控',
+    'menu.monitor.network': '网络监控',
+    'menu.monitor.os': '操作系统监控',
+    'menu.monitor.server': '服务器监控',
+    'menu.monitor.service': '应用服务监控',
+    'menu.monitor.webserver': 'Web服务器监控',
     'menu.log.manage': '日志工作台',
     'menu.trace.manage': '链路工作台',
     'alert.workbench.kicker': '告警中心',
@@ -1665,7 +6015,46 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.workbench.empty.title': '当前范围内没有告警',
     'alert.workbench.empty.copy': '当前时间范围和筛选条件下还没有可处理的告警。',
     'alert.workbench.empty.copy.filtered': '当前筛选条件下没有匹配结果,可以调整筛选条件后重新查询。',
+    'alert.center.loading': '正在加载告警中心',
     'alert.center.default-title': '告警',
+    'alert.center.search': '搜索告警',
+    'alert.center.filter-status': '告警状态',
+    'alert.center.batch.select-page': '选择当前页',
+    'alert.center.batch.select-group': '选择告警组',
+    'alert.center.batch.selection-label': '条告警组已选择',
+    'alert.center.batch.acknowledge-selected': '确认 {{count}} 条',
+    'alert.center.batch.unacknowledge-selected': '取消确认 {{count}} 条',
+    'alert.center.batch.resolve-selected': '恢复 {{count}} 条',
+    'alert.center.batch.reopen-selected': '重新打开 {{count}} 条',
+    'alert.center.batch.silence-selected': '创建静默 {{count}} 条',
+    'alert.center.batch.inhibit-selected': '创建抑制 {{count}} 条',
+    'alert.center.confirm.mark-done-batch': '请确认是否批量标记已处理!',
+    'alert.center.confirm.mark-no-batch': '请确认是否批量标记未处理!',
+    'entity.response.context.title': '当前正在查看这个实体',
+    'entity.response.context.return': '返回实体',
+    'entity.noise-controls.management.label': '规则视图',
+    'entity.noise-controls.management.title.matched': '命中的规则',
+    'entity.noise-controls.management.title.global': '全部规则',
+    'entity.noise-controls.management.empty': '当前没有命中当前实体的规则,可先搜索或新建规则。',
+    'entity.noise-controls.management.missing': '已有 {{count}} 条命中规则不可用或已不存在,这里只显示仍可用的规则。',
+    'entity.noise-controls.management.view-all': '查看全部规则',
+    'entity.noise-controls.management.view-matched': '只看命中的规则',
+    'entity.noise-controls.authoring.label': '实体降噪',
+    'entity.noise-controls.authoring.prefill-warning.no-entity-id': '当前缺少实体 ID,请手动填写规则条件。',
+    'entity.noise-controls.authoring.silence.title': '为当前实体创建静默规则',
+    'entity.noise-controls.authoring.silence.prefill-success': '已提取当前实体可见告警的共享标签,作为静默条件默认值。',
+    'entity.noise-controls.authoring.silence.prefill-warning': '当前没有稳定的共享告警标签,请手动填写静默条件。',
+    'entity.noise-controls.authoring.inhibit.title': '为当前实体创建抑制规则',
+    'entity.noise-controls.authoring.inhibit.prefill-success': '已提取当前实体可见告警的共享标签,并填入源条件、目标条件和相等标签。',
+    'entity.noise-controls.authoring.inhibit.prefill-warning': '当前没有稳定的共享告警标签,请手动填写抑制条件。',
+    'entity.noise-controls.authoring.created-outside-matched.title': '新规则已创建',
+    'entity.noise-controls.authoring.created-outside-matched.copy': '当前视图不会自动显示这条新规则。要继续查看或编辑,请切到全部规则。',
+    'entity.alert.workbench.confirm.acknowledge-selected': '确认选中的告警?',
+    'entity.alert.workbench.confirm.unacknowledge-selected': '取消确认选中的告警?',
+    'alert.center.delete': '删除',
+    'alert.center.last-time': '最后触发',
+    'alert.center.end-time': '结束时间',
+    'alert.center.time.tip': '已触发 {{times}} 次',
     'alert.center.recent.title': '当前告警',
     'alert.center.routes.title': '告警配置',
     'alert.center.routes.setting': '阈值与通知配置',
@@ -1677,17 +6066,111 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.center.routes.silence-inhibit.copy': '静默与抑制规则',
     'alert.center.routes.silence-inhibit.meta': '静默与抑制规则',
     'alert.center.summary.title': '告警摘要',
+    'alert.status.firing': '告警中',
+    'alert.status.acknowledged': '已确认',
+    'alert.status.resolved': '已恢复',
     'alert.center.status.firing': '触发中',
     'alert.center.status.resolved': '已恢复',
     'alert.center.status.suppressed': '已抑制',
     'alert.center.metrics.warning': '警告',
     'alert.center.metrics.critical': '严重',
     'alert.center.metrics.emergency': '紧急',
+    'alert.center.metrics.error': '错误',
+    'alert.center.metrics.info': '信息',
+    'alert.center.metrics.unknown': '未知',
     'alert.center.selected.fingerprint': '指纹 / 创建者',
     'alert.center.selected.trigger-window': '触发 / 活跃窗口',
+    'alert.center.selected.trigger-count': '{{count}} 次触发',
     'alert.center.selected.triggers': '次触发',
     'alert.center.selected.empty.title': '未选中告警',
     'alert.center.selected.empty.copy': '左侧选择一条告警查看详情。',
+    'alert.center.context.status.unknown': '未知状态 {{status}}',
+    'alert.center.context.severity.unknown': '未知严重级别 {{severity}}',
+    'alert.center.context.time.title': '时间范围',
+    'alert.center.context.time.inherited': '继承时间窗口',
+    'alert.center.context.time.refresh': '刷新 {{refresh}}',
+    'alert.center.context.time.paused': '已暂停',
+    'alert.center.context.time.live': '实时',
+    'alert.center.context.monitor.title': '监控实例',
+    'alert.center.context.monitor.id-meta': '监控 ID {{monitorId}}',
+    'alert.center.context.source.title': '采集来源',
+    'alert.center.context.source.monitor.copy': '传统监控',
+    'alert.center.context.source.monitor.meta': '监控中心上下文',
+    'alert.center.context.source.topology.copy': '拓扑',
+    'alert.center.context.source.topology.meta': '拓扑影响面上下文',
+    'alert.center.context.source.otlp.copy': 'OTLP 三信号',
+    'alert.center.context.source.otlp.meta': '指标、日志和链路上下文',
+    'alert.center.context.source.alert.copy': '告警事件',
+    'alert.center.context.source.alert.meta': '告警证据上下文',
+    'alert.center.context.source.unknown.copy': '未知证据来源 {{source}}',
+    'alert.center.context.source.default.meta': '继承的证据来源',
+    'alert.center.context.trace.title': '链路上下文',
+    'alert.center.context.trace.span-meta': 'Span ID {{spanId}}',
+    'alert.center.context.trace.inherited': 'traceId 继承',
+    'alert.center.topology.context.kicker': '拓扑影响面',
+    'alert.center.topology.context.fallback': '当前拓扑上下文',
+    'alert.center.topology.context.return': '返回拓扑',
+    'alert.center.topology.view-mode.unknown': '未知拓扑视图 {{viewMode}}',
+    'alert.center.topology.source.all': '全部关系来源',
+    'alert.center.topology.source.otlp-trace-call': 'OTLP 调用关系',
+    'alert.center.topology.source.monitor-ownership': '监控对象归属',
+    'alert.center.topology.source.template-dependency': '模板依赖',
+    'alert.center.topology.source.k8s-workload': 'K8s 工作负载',
+    'alert.center.topology.source.database-middleware-connection': '数据库 / 中间件连接',
+    'alert.center.topology.source.cmdb-manual-label': 'CMDB / 手工标签',
+    'alert.center.topology.source.alert-impact': '告警影响面',
+    'alert.center.topology.source.unknown': '未知关系来源 {{sourceKind}}',
+    'alert.center.group.response.stage.firing': '处置状态: 待确认',
+    'alert.center.group.response.stage.acknowledged': '处置状态: 已接手',
+    'alert.center.group.response.stage.resolved': '处置状态: 已恢复',
+    'alert.center.group.response.stage.custom': '处置状态: {{status}}',
+    'alert.center.group.evidence.summary': '证据: {{alertCount}} {{alertUnit}} · {{labelCount}} {{labelUnit}}',
+    'alert.center.group.evidence.alert.singular': '条告警',
+    'alert.center.group.evidence.alert.plural': '条告警',
+    'alert.center.group.evidence.label.singular': '个标签',
+    'alert.center.group.evidence.label.plural': '个标签',
+    'alert.center.group.closure.next': '下一步: {{actions}}',
+    'alert.center.evidence.closure.kicker': '告警证据闭环',
+    'alert.center.evidence.closure.title': '处理前先看证据',
+    'alert.center.evidence.closure.copy': '把实体、指标、日志、链路和拓扑证据放在同一个处理入口。',
+    'alert.center.evidence.closure.summary.evidence': '证据范围',
+    'alert.center.evidence.closure.summary.operations': '处理动作',
+    'alert.center.evidence.entity.title': '实体详情',
+    'alert.center.evidence.entity.fallback': '按服务名查找实体',
+    'alert.center.evidence.entity.meta': '确认告警影响的 HertzBeat 实体',
+    'alert.center.evidence.metrics.title': '指标证据',
+    'alert.center.evidence.metrics.fallback': '按告警上下文查看指标',
+    'alert.center.evidence.metrics.meta': '查看同一服务、实体和时间范围的指标',
+    'alert.center.evidence.logs.title': '日志证据',
+    'alert.center.evidence.logs.fallback': '按告警上下文查看日志',
+    'alert.center.evidence.logs.meta.trace': '按 traceId/spanId 查看相关日志',
+    'alert.center.evidence.logs.meta.service': '查看同一服务的日志和链路字段',
+    'alert.center.evidence.traces.title': '链路证据',
+    'alert.center.evidence.traces.fallback': '按告警上下文查看链路',
+    'alert.center.evidence.traces.meta.trace': '打开同一 traceId/spanId 的链路证据',
+    'alert.center.evidence.traces.meta.service': '查看同一服务的调用耗时和错误跨度',
+    'alert.center.evidence.topology.title': '拓扑影响面',
+    'alert.center.evidence.topology.fallback': '查看依赖影响',
+    'alert.center.evidence.topology.meta': '返回依赖关系,确认上下游影响范围',
+    'alert.center.operation.target.current-alert': '当前告警',
+    'alert.center.operation.acknowledge.label': '确认告警',
+    'alert.center.operation.acknowledge.copy': '先确认 {{target}} 的 {{count}} 条告警已被接手。',
+    'alert.center.operation.recover.label': '标记已恢复',
+    'alert.center.operation.recover.copy': '恢复后继续保留证据,便于复盘。',
+    'alert.center.operation.threshold.label': '创建阈值规则',
+    'alert.center.operation.threshold.copy': '把当前实体和信号上下文带入阈值规则。',
+    'alert.center.operation.notice.label': '配置通知策略',
+    'alert.center.operation.notice.copy': '按当前实体和标签收敛通知路由。',
+    'alert.center.operation.group.label': '配置分组收敛',
+    'alert.center.operation.group.copy': '按稳定实体、服务和环境标签收敛告警组。',
+    'alert.center.operation.silence.label': '创建静默',
+    'alert.center.operation.silence.copy': '为当前标签创建临时静默。',
+    'alert.center.operation.inhibit.label': '创建抑制',
+    'alert.center.operation.inhibit.copy': '用当前标签创建主从告警抑制。',
+    'alert.center.operation.automation.label': '建议自动化动作',
+    'alert.center.operation.automation.copy': '带当前证据进入动作建议,人工确认后执行。',
+    'alert.center.operation.close.label': '关闭告警',
+    'alert.center.operation.close.copy': '确认问题已处理后关闭本轮告警。',
     'alert.center.operation.success': '告警状态已更新,列表正在刷新。',
     'alert.center.operation.success.acknowledge': '已确认告警,已切换到已确认筛选,并保留当前实体、拓扑和三信号证据。',
     'alert.center.operation.success.recover': '已标记恢复,已切换到已恢复筛选,并保留当前实体、拓扑和三信号证据。',
@@ -1756,48 +6239,149 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.group.delete.confirm.single': '将删除选中的分组策略,删除后不可恢复。',
     'alert.group.delete.confirm.batch': '将删除 {{count}} 条分组策略,删除后不可恢复。',
     'alert.group.evidence.labels': '分组标签',
+    'alert.group.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'alert.group.pagination.page-size': '每页条数',
+    'alert.group.pagination.page': '页码',
+    'alert.center.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'alert.center.pagination.page-size': '每页条数',
+    'alert.center.pagination.page': '页码',
     'alert.silence.copy': '按策略名称、匹配标签和静默周期管理告警静默规则,减少重复通知和噪声。',
     'alert.silence.action.new': '新增静默',
     'alert.silence.notes.times.meta': '静默次数',
     'alert.silence.delete.confirm.single': '将删除选中的静默策略,删除后不可恢复。',
     'alert.silence.delete.confirm.batch': '将删除 {{count}} 条静默策略,删除后不可恢复。',
+    'alert.silence.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'alert.silence.pagination.page-size': '每页条数',
+    'alert.silence.pagination.page': '页码',
     'alert.inhibit.copy': '以源标签、目标标签和相等标签定义告警抑制关系,控制级联噪声和重复通知。',
     'alert.inhibit.action.new': '新增抑制',
     'alert.inhibit.delete.confirm.single': '将删除选中的抑制规则,删除后不可恢复。',
     'alert.inhibit.delete.confirm.batch': '将删除 {{count}} 条抑制规则,删除后不可恢复。',
+    'alert.inhibit.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'alert.inhibit.pagination.page-size': '每页条数',
+    'alert.inhibit.pagination.page': '页码',
     'alert.inhibit.evidence.source-labels': '源标签',
     'alert.inhibit.evidence.target-labels': '目标标签',
     'alert.inhibit.evidence.equal-labels': '相等标签',
+    'alert.integration.kicker': '集成接入',
+    'alert.integration.sources': '集成告警源',
+    'alert.integration.token.manage': '管理令牌',
+    'alert.integration.doc.fallback': '当前告警源暂未提供集成指南。',
+    'alert.integration.source.webhook': '默认Webhook',
+    'alert.integration.source.prometheus': 'Prometheus',
+    'alert.integration.source.alertmanager': 'Alertmanager',
+    'alert.integration.source.skywalking': 'SkyWalking',
+    'alert.integration.source.uptime-kuma': 'Uptime Kuma',
+    'alert.integration.source.zabbix': 'Zabbix',
+    'alert.integration.source.tencent': '腾讯云监控',
+    'alert.integration.source.alibabacloud-sls': '阿里云日志服务 SLS',
+    'alert.integration.source.huaweicloud-ces': '华为云监控服务',
+    'alert.integration.source.volcengine': '火山引擎云监控',
+    'alert.integration.fact.doc-status': '文档状态',
+    'alert.integration.fact.doc-loaded': '已加载',
+    'alert.integration.fact.doc-fallback': '回退文案',
+    'alert.integration.source.selected': '已选中',
+    'alert.integration.posture.doc-source': '文档来源',
+    'alert.integration.posture.existing-asset': '现有 Angular 资产',
+    'alert.integration.posture.fallback-behavior': '回退行为',
+    'alert.integration.posture.doc-loaded': '集成文档已加载',
+    'alert.integration.posture.doc-missing': '缺少文档时展示回退文案',
+    'alert.integration.posture.behavior-preserved': '行为保留',
+    'alert.integration.posture.token-management': '令牌管理',
+    'alert.integration.posture.token-copy': '继续使用当前 API 令牌管理入口。',
+    'alert.integration.diagram.aria': '告警集成图表',
+    'alert.integration.diagram.pending': '图表渲染中',
+    'alert.integration.diagram.error': '图表渲染失败',
     'alert.notice.page-size': '{{count}} 条/页',
+    'alert.notice.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'alert.notice.pagination.page-size': '每页条数',
+    'alert.notice.pagination.page': '页码',
     'alert.notice.copy': '统一配置告警接收对象、通知策略和消息模板,让告警按规则进入正确的通知通道。',
+    'alert.notice.metrics.visible-receivers': '当前接收对象',
+    'alert.notice.metrics.visible-rules': '当前通知策略',
+    'alert.notice.metrics.preset-templates': '预置模板',
+    'alert.notice.row.updated': '更新时间',
+    'alert.notice.row.enabled': '已启用',
+    'alert.notice.row.disabled': '已停用',
+    'alert.notice.row.delivery-prefix': '发送到',
+    'alert.notice.row.no-receiver': '未绑定接收对象',
+    'alert.notice.form.receiver-ids.placeholder': '接收对象 ID,例如 1, 2',
+    'alert.notice.form.template-id.placeholder': '模板 ID,-1 表示默认模板',
+    'alert.notice.form.labels.placeholder': '标签匹配,例如 severity:critical',
+    'alert.notice.form.days.placeholder': '日期范围,例如 1,2,3,4,5',
+    'alert.notice.form.email.placeholder': '邮箱地址,例如 ops@example.com',
+    'alert.notice.form.phone.placeholder': '手机号,例如 +86 13800000000',
+    'alert.notice.form.webhook.placeholder': 'Webhook 地址,例如 https://hooks.example.com',
     'alert.notice.delete.confirm.receiver': '将删除选中的接收人,相关通知配置会受到影响。',
     'alert.notice.delete.confirm.template': '将删除选中的消息模板,删除后不可恢复。',
     'alert.notice.delete.confirm.rule': '将删除选中的通知策略,删除后不可恢复。',
     'common.end': '结束',
+    'bulletin.batch.delete': '批量删除公告',
+    'bulletin.new': '新增公告',
+    'bulletin.edit': '编辑公告',
+    'bulletin.name': '公告名称',
+    'bulletin.monitor.type': '监控类型',
+    'bulletin.monitor.name': '监控对象',
+    'bulletin.monitor.metrics': '监控指标',
     'bulletin.facts.selected': '当前选中',
-    'bulletin.list.monitors': 'monitors',
-    'bulletin.list.creator': 'creator',
-    'bulletin.selected.fields': 'fields',
-    'bulletin.validation.app': 'App is required',
-    'bulletin.validation.monitors': 'At least one monitor id is required',
+    'bulletin.list.monitors': '监控对象',
+    'bulletin.list.creator': '创建人',
+    'bulletin.selected.empty.title': '未选择公告',
+    'bulletin.selected.empty.copy': '从公告列表选择一个看板后查看指标上下文。',
+    'bulletin.selected.fields': '字段',
+    'bulletin.monitor.ids.placeholder': '1, 2, 3',
+    'bulletin.delete.title': '确认删除公告',
+    'bulletin.delete.copy': '删除后该公告看板会从当前工作台移除,指标刷新上下文需要重新选择公告看板。',
+    'bulletin.delete.confirm': '确认删除',
+    'bulletin.validation.app': '应用为必填项',
+    'bulletin.validation.monitors': '至少需要填写一个监控 ID',
+    'bulletin.validation.fields': '指标字段 JSON 格式不正确',
     'common.apply-filters': '应用筛选',
     'common.clear': '清除',
     'entities.list.loading': '正在加载对象目录',
     'entities.list.title': '对象目录',
+    'entities.list.kicker': '对象优先调查',
+    'entities.list.subtitle': '围绕服务、资源与实体定位问题并进入调查,先按对象筛出风险,再决定进入日志、链路、指标还是告警工作台。',
     'entities.list.environment.select': '选择环境',
     'entities.list.environment.all': '全部环境',
     'entities.list.resource-search.placeholder': '搜索并过滤资源属性',
     'entities.list.action.new': '新建对象',
+    'entities.list.action.create': '创建实体',
+    'entities.list.action.discovery': '从遥测发现',
+    'entities.list.action.import': '导入定义',
+    'entities.list.metric.total': '实体总数',
+    'entities.list.metric.abnormal': '活跃异常对象',
+    'entities.list.metric.alerting': '高风险对象',
+    'entities.list.metric.linked': '有关联对象',
+    'entities.list.search.placeholder': '搜索实体名称、命名空间、负责人',
     'entities.list.column.object': '对象',
     'entities.list.column.type': '类型',
+    'entities.list.column.owner': '负责人',
+    'entities.list.column.progress': '进展',
+    'entities.list.column.evidence': '证据关联',
     'entities.list.column.status': '状态',
+    'entities.list.column.next-action': '下一步动作',
     'entities.list.column.alerts': '告警',
     'entities.list.column.monitors': '监控',
     'entities.list.column.relations': '关系',
     'entities.list.column.updated': '最近证据',
+    'entities.list.table.total': '{{total}} 个实体',
+    'entities.list.table.range': '显示 {{from}}-{{to}} / {{total}}',
     'entities.list.pagination.items': '{{from}}-{{to}} / {{total}} 项',
     'entities.list.empty.title': '暂无对象',
     'entities.list.empty.copy': '调整资源筛选条件,或接入可观测数据后生成对象目录。',
+    'entities.list.unselected.title': '还未选择实体',
+    'entities.list.unselected.copy': '从列表选择实体后,这里会显示证据和操作上下文。',
+    'entities.list.row.owner.unset': '未设置',
+    'entities.list.row.progress.incomplete': '{{percent}} · 待完善',
+    'entities.list.row.progress.missing': '当前缺少负责人、处置手册',
+    'entities.list.row.evidence.monitors': '{{count}} 监控',
+    'entities.list.row.evidence.alerts': '{{count}} 告警',
+    'entities.list.row.evidence.identities': '{{count}} 身份标识',
+    'entities.list.row.evidence.updated': '最近更新 {{time}}',
+    'entities.list.row.action.owner': '设置负责人',
+    'entities.list.row.action.logs': '日志线索',
+    'entities.list.row.action.traces': '链路证据',
     'entities.list.item.fallback': '未命名对象',
     'entities.list.type.api': 'API',
     'entities.list.type.database': '数据库',
@@ -1823,25 +6407,251 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'entities.list.selected.alerts': '告警 {{count}}',
     'entities.list.selected.definition-activity': '定义活动 -',
     'entities.list.rail.identity-monitor-relation': '身份 / 监控 / 关系',
-    'monitor.editor.validation.cron': 'Cron expression is invalid',
+    'monitor.editor.validation.cron': 'Cron 表达式不合法',
     'setting.status.validation.incident-components': '至少需要关联一个组件',
     'setting.status.validation.incident-message': '事件进展内容为必填项',
     'log.manage.trend.count': '日志 {{count}}',
+    'log.manage.console.title': '日志工作台',
+    'log.manage.loading': '正在加载日志工作台',
+    'log.manage.api.degraded.title': '日志 API 暂不可用',
+    'log.manage.api.degraded.copy': '日志读取接口未及时响应,工作台进入空结果降级状态;查询控件仍可使用。',
+    'log.manage.route.kicker': '可观测',
+    'log.manage.route.title': '日志工作台',
+    'log.manage.route.subtitle': '围绕采集来源、实体、链路和告警处理筛选日志,把实时流、历史检索和上下文排查放在同一工作台。',
+    'log.manage.route.action.intake': '返回 OTLP 接入',
+    'log.manage.route.action.return-source': '返回来源',
+    'log.manage.route.action.collector': '采集集群',
+    'log.manage.route.action.entity': '实体详情',
+    'log.manage.route.action.alerts': '告警处理',
+    'log.manage.route.action.templates': '监控模板',
+    'log.manage.query.search.aria': '日志查询',
+    'log.manage.query.search.placeholder': 'service.name = "checkout"',
+    'log.manage.query.severity.aria': '严重级别',
+    'log.manage.query.severity.all': '全部级别',
+    'log.manage.query.run.stream': '应用到日志流',
+    'log.manage.query.run.history': '运行查询',
+    'log.manage.query.reset': '重置',
+    'log.manage.query.trace-id': '链路 ID',
+    'log.manage.query.span-id': '跨度 ID',
+    'log.manage.query.body.aria': '日志正文',
+    'log.manage.query.body.placeholder': '搜索日志正文',
+    'log.manage.summary.total': '日志总数',
+    'log.manage.summary.errors': '错误日志',
+    'log.manage.summary.trace-coverage': '链路关联',
+    'log.manage.summary.latest': '最近时间',
+    'log.manage.trend.title': '趋势带',
+    'log.manage.trend.points': '{{count}} 点',
+    'log.manage.list.title': '最近日志',
+    'log.manage.list.count': '{{count}} 条',
+    'log.manage.list.column.time': '时间',
+    'log.manage.list.column.severity': '严重级别',
+    'log.manage.list.column.service': '服务',
+    'log.manage.list.column.body': '日志正文',
+    'log.manage.list.column.trace': '链路',
+    'log.manage.empty.title': '暂无日志',
+    'log.manage.empty.copy': '确认时间范围、实体归因、采集器和监控模板后再查看日志。',
+    'log.manage.detail.title': '详情面板',
+    'log.manage.detail.severity': '严重级别',
+    'log.manage.detail.trace-id': '链路 ID',
+    'log.manage.detail.span-id': '跨度 ID',
+    'log.manage.evidence.aria': '日志证据 日志时间 日志级别 正文摘要 最近上报',
+    'log.manage.evidence.title': '日志证据',
+    'log.manage.evidence.current': '当前选中',
+    'log.manage.evidence.time.title': '日志时间',
+    'log.manage.evidence.time.meta': '日志写入时间',
+    'log.manage.evidence.severity.title': '日志级别',
+    'log.manage.evidence.severity.current-filter': '当前筛选',
+    'log.manage.evidence.body.title': '正文摘要',
+    'log.manage.evidence.body.meta': '日志正文',
+    'log.manage.evidence.latest.title': '最近上报',
+    'log.manage.evidence.latest.meta': '工作台最新日志',
+    'log.manage.context.entity.aria': '实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源',
+    'log.manage.context.entity.title': '实体上下文',
+    'log.manage.handoff.alert-hint': '按当前实体、服务和已带入的链路上下文查看相关告警',
+    'log.manage.handoff.signal-hint': '当前日志的链路、跨度和服务上下文会带入链路与指标工作台',
+    'log.manage.handoff.alerts': '告警处理',
+    'log.manage.handoff.traces': '查看链路',
+    'log.manage.handoff.metrics': '查看指标',
+    'log.manage.handoff.entities': '对象目录',
+    'log.manage.stream.view-switch.title': '日志视图',
+    'log.manage.stream.view.stream': '日志流',
+    'log.manage.stream.view.history': '历史检索',
+    'log.manage.stream.detail.title': '日志详情',
+    'log.manage.stream.detail.stream-title': '日志流详情',
+    'log.manage.stream.detail.attribution-title': '归因诊断',
+    'log.manage.stream.detail.attribution-present': '已提供',
+    'log.manage.stream.detail.attribution-missing': '缺失',
+    'log.manage.selected.attribution.aria': '归因诊断 hertzbeat.entity_id hertzbeat.collector hertzbeat.template',
+    'log.manage.selected.attribution.title': '归因诊断',
+    'log.manage.selected.attribution.state.present': '已提供',
+    'log.manage.selected.attribution.state.missing': '缺失',
+    'log.manage.stream.action.view-trace': '查看链路',
+    'log.manage.stream.action.view-metrics': '查看指标',
+    'log.manage.stream.action.view-code': '查看代码',
+    'log.manage.stream.stage.kicker': '日志流',
+    'log.manage.stream.stage.title': '实时日志流',
+    'log.manage.stream.status.connected': '已连接',
+    'log.manage.stream.status.connecting': '连接中',
+    'log.manage.stream.status.disconnected': '未连接',
+    'log.manage.stream.count': '{{count}} 条日志',
+    'log.manage.stream.state.live': '实时',
+    'log.manage.stream.state.paused': '已暂停',
+    'log.manage.stream.action.reconnect': '重连',
+    'log.manage.stream.action.resume': '继续',
+    'log.manage.stream.action.pause': '暂停',
+    'log.manage.stream.action.clear': '清空',
+    'log.manage.stream.paused-copy': '日志流已暂停,新的日志会暂时留在采集链路中,继续后重新连接。',
+    'log.manage.stream.empty.title': '等待实时日志',
+    'log.manage.stream.empty.copy': '保持连接后,Collector 和 OTLP 写入的日志会实时进入这里。',
+    'log.manage.stream.selected.title': '选中日志',
+    'log.manage.stream.selected.detached': '已脱离缓冲',
+    'log.manage.stream.selected.none': '未选择日志',
+    'log.manage.stream.selected.helper': '点击任意日志查看完整内容、链路和实体上下文。',
+    'log.manage.stream.detail.detached-warning': '这条日志已脱离实时缓冲,交接前请核对保留的原始内容。',
+    'log.manage.stream.action.view-log': '查看日志',
     'log.manage.selected.resource-keys': '资源字段',
     'log.manage.selected.attributes': '日志属性 {{count}}',
     'log.manage.selected.span-severity': '跨度与级别',
     'log.manage.handoff.trace-disabled': '缺少 traceId,暂时不能查看关联链路。',
     'log.manage.handoff.full-trace-disabled': '缺少 traceId,暂时不能打开完整链路工作台。',
     'log.manage.handoff.entity-disabled': '缺少实体 ID,暂时不能打开实体详情。',
+    'log.manage.related-trace.error': '链路预览暂时不可用,请稍后重试或打开完整链路工作台。',
+    'log.manage.related-trace.fallback-title': '关联链路预览',
+    'log.manage.related-trace.badge': '链路预览',
+    'log.manage.related-trace.open-workspace': '查看完整链路',
+    'log.manage.related-trace.spans-count': '{{count}} 个跨度',
+    'log.manage.related-trace.fact.current-span': '当前跨度',
+    'log.manage.related-trace.fact.error-spans': '错误跨度',
+    'log.manage.related-trace.fact.events': '事件',
+    'log.manage.related-trace.fact.links': '关联',
+    'log.manage.related-trace.empty-title': '暂无关联链路',
+    'log.manage.related-trace.empty-copy': '该日志没有可预览的链路详情。',
+    'log.manage.related-trace.loading-title': '正在加载链路预览',
+    'log.manage.related-trace.loading-copy': '正在从当前日志上下文加载链路预览。',
+    'log.manage.related-trace.span-label': '跨度',
+    'log.manage.related-trace.duration-label': '耗时',
+    'log.manage.related-trace.timeline-label': '时间轴',
+    'log.manage.related-trace.kicker': '相关链路',
+    'log.manage.related-trace.error-title': '链路详情错误',
+    'log.manage.related-trace.event-fact.title': '跨度事件',
+    'log.manage.related-trace.event-fact.fallback': '未命名事件',
+    'log.manage.related-trace.event-fact.meta': '不是新的跨度,是当前跨度上的时间点',
+    'log.manage.related-trace.event-fact.span': '所属跨度',
+    'log.manage.related-trace.event-fact.position': '事件位置',
+    'log.manage.related-trace.event-fact.timeline': '相对链路时间轴',
+    'log.manage.related-trace.event-fact.attributes': '事件属性',
+    'log.manage.related-trace.event-fact.no-attributes': '无属性',
+    'log.manage.related-trace.event-detail.title': '事件详情',
+    'log.manage.related-trace.event-detail.copy': '不是新的跨度,是当前跨度上的时间点',
+    'log.manage.related-trace.event-detail.action.span': '查看跨度',
+    'log.manage.attribution.event-id.present': '用于定位一次日志接入事件',
+    'log.manage.attribution.event-id.missing': '缺少事件 ID 时只能按时间和内容排查',
+    'log.manage.attribution.ingest-id.present': '用于排查接入批次',
+    'log.manage.attribution.ingest-id.missing': '缺少接入批次时使用 Collector 日志排查',
+    'log.manage.attribution.entity-id.present': '可打开实体详情',
+    'log.manage.attribution.entity-id.missing': '缺少实体 ID,实体详情会保持禁用',
+    'log.manage.attribution.entity-name.present': '用于展示实体名称',
+    'log.manage.attribution.entity-name.missing': '缺少实体名称时使用服务名辅助检索',
+    'log.manage.attribution.workspace-id.present': '工作区归属',
+    'log.manage.attribution.workspace-id.missing': '缺少工作区字段时使用当前部署上下文',
+    'log.manage.attribution.collector': '采集器来源',
+    'log.manage.attribution.template': '监控模板归属',
     'overview.lane.logs.stat.recommended': '无链路 ID 时优先',
     'passport.login.session-copy': '登录成功后会自动恢复当前工作台会话,并在需要时尝试刷新令牌。',
     'setting.status.validation.incident-name': '事件名称为必填项',
     'status.incidents.history': '事件历史',
     'status.incidents.posture': '聚焦公开事件历史入口。',
+    'incidents.entry.kicker': '事件入口',
+    'incidents.entry.title': '故障事件',
+    'incidents.entry.subtitle': '按 OTLP 工作台的冷色基线统一响应时间线、责任人和证据入口。',
+    'incidents.entry.action.overview': '打开概览',
+    'incidents.entry.action.entities': '查看对象',
+    'incidents.entry.shell.eyebrow': '冷色入口已接入',
+    'incidents.entry.shell.copy': '此入口继承 OTLP 接入页的近黑画布、小按钮、硬边面板和静态右栏节奏,后续只需要接入事件适配器。',
+    'incidents.entry.chip.entry': '事件入口',
+    'incidents.entry.chip.timeline': '响应时间线',
+    'incidents.entry.chip.owner-first': '责任人优先',
+    'incidents.entry.checklist.title': '接入清单',
+    'incidents.entry.checklist.context.title': '统一入口上下文',
+    'incidents.entry.checklist.context.copy': '已和 OTLP 基线共用同一套页面节奏。',
+    'incidents.entry.checklist.adapter.title': '接入事件适配器',
+    'incidents.entry.checklist.adapter.copy': '等事件列表、责任人和时间线数据落地后直接挂载。',
+    'incidents.entry.checklist.evidence.title': '保留证据跳转',
+    'incidents.entry.checklist.evidence.copy': '日志、链路和对象入口保持在同一响应上下文。',
+    'incidents.entry.empty.title': '等待接入事件适配器',
+    'incidents.entry.empty.copy': '视觉骨架已经统一到 OTLP 基线,后续数据接入可以增量完成。',
+    'incidents.subtitle': '响应时间线、责任人和下一步保持在同一操作上下文。',
+    'incidents.focus': '事件状态与响应交接',
+    'incidents.summary': '事件入口页在更深的事件域落地前,先保持响应状态、责任人和相关证据对齐。',
+    'incidents.tag.shell': '事件入口外壳',
+    'incidents.tag.timeline': '响应时间线',
+    'incidents.tag.owner-first': '责任人优先',
+    'incidents.metric.open': '打开事件',
+    'incidents.metric.critical': '严重事件',
+    'incidents.metric.mitigating': '缓解中',
+    'incidents.metric.ownership-queues': '责任队列',
+    'incidents.row.checkout.title': 'checkout 在 prod-ap 出现延迟突增',
+    'incidents.row.checkout.service': 'checkout',
+    'incidents.row.checkout.owner': 'checkout-oncall',
+    'incidents.row.checkout.blast-radius': '2 个区域',
+    'incidents.row.collector.title': '指标摄取的采集器队列延迟',
+    'incidents.row.collector.service': 'otlp-gateway',
+    'incidents.row.collector.owner': 'observability',
+    'incidents.row.collector.blast-radius': '仅指标',
+    'incidents.row.edge.title': '发布后边缘代理饱和',
+    'incidents.row.edge.service': 'edge-proxy',
+    'incidents.row.edge.owner': 'edge-sre',
+    'incidents.row.edge.blast-radius': 'cn-hz 入口',
+    'incidents.timeline.checkout.title': '09:08 开始缓解',
+    'incidents.timeline.checkout.copy': '回滚和重启动作已从告警证据包打开。',
+    'incidents.timeline.checkout.meta': 'checkout 延迟事件',
+    'incidents.timeline.collector.title': '08:33 指标重放已批准',
+    'incidents.timeline.collector.copy': '采集器积压降到重放阈值以下,交接仍留在同一操作栏。',
+    'incidents.timeline.collector.meta': '摄取事件',
+    'incidents.timeline.edge.title': '07:56 边缘饱和已升级',
+    'incidents.timeline.edge.copy': '操作员从监控详情将事件转到链路和日志证据。',
+    'incidents.timeline.edge.meta': '边缘事件',
+    'incidents.ownership.checkout.owner': 'checkout-oncall',
+    'incidents.ownership.checkout.queue': '主响应人',
+    'incidents.ownership.checkout.copy': '保持告警、链路和发布上下文绑定到当前缓解线程。',
+    'incidents.ownership.checkout.meta': '2 个活跃交接',
+    'incidents.ownership.observability.owner': 'observability',
+    'incidents.ownership.observability.queue': '信号验证',
+    'incidents.ownership.observability.copy': '关闭事件前验证重放安全性和摄取恢复。',
+    'incidents.ownership.observability.meta': '1 个活跃交接',
+    'incidents.ownership.edge.owner': 'edge-sre',
+    'incidents.ownership.edge.queue': '容量复核',
+    'incidents.ownership.edge.copy': '跟踪发布后回归和下一次回滚检查点。',
+    'incidents.ownership.edge.meta': '等待回滚说明',
+    'incidents.checklist.shell.title': '外壳已就绪',
+    'incidents.checklist.shell.copy': '事件路由已经复用工作台外壳和路由姿态。',
+    'incidents.checklist.shell.meta': '已完成',
+    'incidents.checklist.adapter.title': '下一步接适配器',
+    'incidents.checklist.adapter.copy': '事件列表、责任人状态和响应时间线可以挂到当前 API 边界。',
+    'incidents.checklist.adapter.meta': '下一步',
+    'incidents.checklist.drilldown.title': '保留下钻',
+    'incidents.checklist.drilldown.copy': '日志、链路和对象交接保留在后续扩展路径。',
+    'incidents.checklist.drilldown.meta': '预留',
+    'incidents.surface.kicker': '事件响应台',
+    'incidents.surface.handoff.logs.title': '日志证据',
+    'incidents.surface.handoff.logs.copy': '缓解过程需要最新载荷证据时跳转到日志。',
+    'incidents.surface.handoff.traces.title': '链路证据',
+    'incidents.surface.handoff.traces.copy': '保留以链路优先的延迟和依赖回归下钻。',
+    'incidents.surface.handoff.overview.title': '回到概览',
+    'incidents.surface.handoff.overview.copy': '让事件状态继续留在同一全局响应节奏中。',
     'alert.group.validation.name': '规则名称为必填项',
     'alert.group.validation.labels': '分组标签为必填项',
+    'alert.group.validation.group-wait': '等待时间为必填项',
+    'alert.group.validation.group-interval': '间隔时间为必填项',
+    'alert.group.validation.repeat-interval': '重复间隔为必填项',
+    'alert.group.validation.group-wait-non-negative': '等待时间必须大于等于 0',
+    'alert.group.validation.group-interval-non-negative': '间隔时间必须大于等于 0',
+    'alert.group.validation.repeat-interval-non-negative': '重复间隔必须大于等于 0',
+    'alert.group.id-required': '告警分组 ID 必填',
     'alert.group.default-title': '分组规则',
+    'alert.group.loading': '正在加载分组规则',
     'alert.group.labels': '分组标签',
+    'alert.group.labels.placeholder': 'alertname, severity, service',
     'alert.group.wait': '等待时间',
     'alert.group.interval': '间隔时间',
     'alert.group.repeat': '重复间隔',
@@ -1851,14 +6661,22 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.group.selected.labels': '分组标签',
     'alert.group.selected.timers': '时间窗口',
     'alert.group.notes.query': '规则排序',
+    'alert.rule.notes.sort-desc-copy': 'ID 倒序',
+    'alert.rule.notes.search-copy': '支持搜索',
+    'alert.rule.notes.search-sort-copy': '搜索 · ID 倒序',
+    'alert.rule.selected.id-meta': '规则 ID {{id}}',
     'alert.inhibit.validation.name': '规则名称为必填项',
     'alert.inhibit.validation.source': '源标签为必填项',
     'alert.inhibit.validation.target': '目标标签为必填项',
     'alert.inhibit.validation.equal': '相等标签为必填项',
     'alert.inhibit.default-title': '抑制规则',
+    'alert.inhibit.loading': '正在加载抑制规则',
     'alert.inhibit.source': '源标签',
     'alert.inhibit.target': '目标标签',
     'alert.inhibit.equal': '相等标签',
+    'alert.inhibit.label.key.placeholder': '标签名',
+    'alert.inhibit.label.value.placeholder': '标签值',
+    'alert.inhibit.equal.placeholder': 'alertname, severity, service',
     'alert.inhibit.empty.title': '还没有抑制规则',
     'alert.inhibit.selected.empty.title': '选择一条抑制规则',
     'alert.inhibit.selected.empty.copy': '选择一条规则,查看哪些告警会抑制后续噪声。',
@@ -1872,9 +6690,14 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.silence.validation.time': '开始和结束时间为必填项',
     'alert.silence.validation.days': '静默周期为必填项',
     'alert.silence.default-title': '静默规则',
+    'alert.silence.loading': '正在加载静默规则',
     'alert.silence.match-all': '匹配全部',
     'alert.silence.labels': '匹配标签',
+    'alert.silence.label.key.placeholder': '标签名',
+    'alert.silence.label.value.placeholder': '标签值',
     'alert.silence.times': '已静默告警数',
+    'alert.silence.time.start': '静默时间开始',
+    'alert.silence.time.end': '静默时间结束',
     'alert.silence.days': '静默周期',
     'alert.silence.empty.title': '还没有静默规则',
     'alert.silence.selected.empty.title': '选择一条静默规则',
@@ -1882,6 +6705,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.silence.selected.strategy': '匹配策略',
     'alert.silence.selected.strategy.all': '全部匹配',
     'alert.silence.selected.strategy.any': '按标签匹配',
+    'alert.silence.selected.type-meta': '类型 {{type}}',
     'alert.silence.selected.labels-days': '标签 / 周期',
     'alert.silence.notes.times.title': '已静默告警',
     'alert.silence.notes.query.title': '规则排序',
@@ -1918,9 +6742,16 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.subtitle': '在一个工作台里管理接收人、模板和发送规则。',
     'alert.notice.kicker': '告警通知',
     'alert.notice.loading': '正在加载通知中心',
+    'alert.setting.loading': '正在加载告警设置',
     'alert.setting.title': '告警设置',
     'alert.setting.copy': '管理阈值名称、阈值类型、表达式和告警内容,保持规则编辑与告警降噪链路一致。',
     'alert.setting.action.new': '新增阈值',
+    'alert.setting.batch.selection-label': '条阈值规则已选择',
+    'alert.setting.export.selected': '已选择 {{count}} 条阈值规则',
+    'alert.setting.import.input': '导入阈值规则文件',
+    'alert.setting.notify.no-select-export': '请先选择要导出的阈值规则',
+    'alert.export.switch-type': '请选择导出文件格式',
+    'alert.export.use-type': '以 {{type}} 文件格式导出阈值规则',
     'alert.setting.delete.confirm.single': '将删除选中的阈值规则,删除后不可恢复。',
     'alert.setting.delete.confirm.batch': '将删除 {{count}} 条阈值规则,删除后不可恢复。',
     'alert.setting.fact.workspace': '工作区',
@@ -1944,6 +6775,10 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.lanes.receivers.title': '接收人',
     'alert.notice.lanes.templates.title': '模板',
     'alert.notice.lanes.rules.title': '规则',
+    'alert.notice.lanes.receivers.copy': '管理告警发往哪些接收对象。',
+    'alert.notice.lanes.rules.copy': '按标签、时间段和模板自动路由通知。',
+    'alert.notice.lanes.templates.copy': '以纯文本摘要预览模板,不暴露原始 HTML 或模板源码。',
+    'alert.notice.lanes.count-meta': '共 {{total}} 个 · 当前 {{visible}} 个',
     'alert.notice.actions.define': '打开告警设置',
     'alert.setting.name': '规则名称',
     'alert.setting.type': '阈值类型',
@@ -1978,6 +6813,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.receiver.people.name': '接收对象名称',
     'alert.notice.receiver.people.placeholder': '选择接收对象',
     'alert.notice.receiver.type': '接收人类型',
+    'alert.notice.receiver.type.placeholder': '选择通知方式',
+    'alert.notice.rule.receivers.empty': '暂无接收对象,请先新增接收对象',
     'alert.notice.send-test': '发送告警测试',
     'alert.notice.send-test.notify.failed': '触发告警测试失败!',
     'alert.notice.send-test.notify.success': '触发告警测试成功!',
@@ -1994,6 +6831,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.template.show': '查看模板',
     'alert.notice.template.type': '通知方式',
     'alert.notice.template.validation.name': '模板名称为必填项',
+    'alert.notice.template.validation.type': '通知方式为必填项',
     'alert.notice.template.validation.content': '模板内容为必填项',
     'alert.notice.templates.title': '模板',
     'alert.notice.templates.empty.title': '还没有模板',
@@ -2015,6 +6853,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.rule.priority.placeholder': '选择级别',
     'alert.notice.rule.tag': '标签匹配',
     'alert.notice.rule.tag.placeholder': '选择标签',
+    'alert.notice.rule.label.value.placeholder': '标签值',
     'alert.notice.rule.time': '通知时段',
     'alert.notice.rule.time-end': '结束时间',
     'alert.notice.rule.time-start': '起始时间',
@@ -2035,6 +6874,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.type.WeCom-robot': '企业微信机器人',
     'alert.notice.type.ding': '钉钉',
     'alert.notice.type.fei-shu': '飞书机器人',
+    'alert.notice.type.telegram': 'Telegram',
     'alert.notice.type.telegram-bot': 'Telegram 机器人',
     'alert.notice.type.slack': 'Slack',
     'alert.notice.type.discord': 'Discord',
@@ -2054,6 +6894,9 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.type.telegram-message-thread-id': 'Telegram 线程 ID',
     'alert.notice.type.webhook-auth-type': 'Webhook 授权类型',
     'alert.notice.type.webhook-auth-type.placeholder': '请选择授权类型',
+    'alert.notice.type.webhook-auth-type.none': 'None',
+    'alert.notice.type.webhook-auth-type.basic': 'Basic',
+    'alert.notice.type.webhook-auth-type.bearer': 'Bearer',
     'alert.notice.type.webhook-auth-token': 'Webhook 授权令牌',
     'alert.notice.type.slack-webHook-url': 'Slack webhook 地址',
     'alert.notice.type.discord-channel-id': 'Discord 频道 ID',
@@ -2089,14 +6932,79 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.receiver.phone.placeholder': '手机号,例如 +86 13800000000',
     'alert.notice.receiver.email.placeholder': '邮箱地址,例如 ops@example.com',
     'alert.notice.receiver.hookUrl.placeholder': 'Webhook 地址,例如 https://hooks.example.com',
-    'bulletin.search.all': 'all bulletins',
-    'bulletin.validation.name': 'Bulletin name is required',
-    'bulletin.metrics.empty.title': 'No metrics',
-    'bulletin.metrics.empty.copy': 'No metrics are available yet.',
-    'bulletin.metrics.error.title': 'Metrics unavailable',
-    'bulletin.metrics.error-default': 'The metrics request failed.',
+    'bulletin.loading': '正在加载公告中心',
+    'bulletin.search.all': '全部公告',
+    'bulletin.navigation.aria': '公告导航',
+    'bulletin.validation.name': '公告看板名称为必填项',
+    'bulletin.metrics.column.app': '应用',
+    'bulletin.metrics.column.host': '主机',
+    'bulletin.metrics.empty.title': '暂无指标',
+    'bulletin.metrics.empty.copy': '暂无可用指标。',
+    'bulletin.metrics.error.title': '指标不可用',
+    'bulletin.metrics.error-default': '指标请求失败。',
+    'bulletin.metrics.no-data': '暂无数据',
     'exception.fact.workspace': '工作区',
     'exception.fact.status': '状态',
+    'exception.fact.action': '动作',
+    'exception.403.title': '403 无权限访问',
+    'exception.403.subtitle': '当前会话不能访问该路由,请先复核工作区、角色和令牌策略。',
+    'exception.403.fact.status': '权限边界',
+    'exception.403.fact.action': '复核访问策略',
+    'exception.403.boundary.title': '访问边界',
+    'exception.403.boundary.copy': '重试前确认当前会话、角色和工作区权限。',
+    'exception.403.boundary.meta': '访问控制',
+    'exception.403.next-step.copy': '打开访问设置,或返回有权限的工作区。',
+    'exception.404.title': '404 路由不存在',
+    'exception.404.subtitle': '当前 HertzBeat 工作区没有注册这个路由。',
+    'exception.404.fact.status': '路由缺失',
+    'exception.404.fact.action': '返回或重定向',
+    'exception.404.boundary.title': '路由边界',
+    'exception.404.boundary.copy': '重试前检查导航入口和兼容别名。',
+    'exception.404.boundary.meta': '路由',
+    'exception.404.next-step.copy': '返回概览,或进入已知的工作区路由。',
+    'exception.500.title': '500 运行时异常',
+    'exception.500.subtitle': '服务运行时在处理当前工作区路由时返回异常。',
+    'exception.500.fact.status': '运行时故障',
+    'exception.500.fact.action': '重试或排查',
+    'exception.500.boundary.title': '运行时边界',
+    'exception.500.boundary.copy': '重试前关联后端响应、日志和链路证据。',
+    'exception.500.boundary.meta': '运行时',
+    'exception.500.next-step.copy': '先检查日志和链路,在后端稳定后重试。',
+    'exception.next-step.title': '下一步',
+    'exception.next-step.meta': '人工处置',
+    'exception.subtitle.default': '按服务、资源和时间聚合服务端异常事件,便于进入日志和链路排查。',
+    'exception.chrome.title': '异常中心',
+    'exception.chrome.filter-title': '筛选',
+    'exception.chrome.clear-all': '清除全部',
+    'exception.chrome.expand-more': '展开更多',
+    'exception.chrome.refresh-filters': '刷新筛选',
+    'exception.time.last-7d': '最近 7 天',
+    'exception.action.search': '搜索',
+    'exception.action.run-query': '运行查询',
+    'exception.action.feedback': '反馈',
+    'exception.action.share': '分享',
+    'exception.query.aria': '异常查询',
+    'exception.query.placeholder': '按资源属性搜索和筛选,支持 IN / NOT IN 条件,选择后按 Enter 确认',
+    'exception.scope.aria': '异常范围',
+    'exception.scope.all': '全部异常',
+    'exception.scope.critical': '严重异常',
+    'exception.sort.aria': '异常排序',
+    'exception.sort.last-seen': '最后出现',
+    'exception.sort.count': '次数',
+    'exception.table.type': '异常类型',
+    'exception.table.message': '错误信息',
+    'exception.table.count': '次数',
+    'exception.table.last-seen': '最后出现',
+    'exception.table.first-seen': '首次出现',
+    'exception.table.application': '应用',
+    'exception.summary.groups': '个异常分组',
+    'exception.filters.deployment-environment': '部署环境',
+    'exception.filters.service': '服务',
+    'exception.filters.host': '主机',
+    'exception.filters.k8s-cluster': 'K8s 集群',
+    'exception.filters.k8s-deployment': 'K8s Deployment',
+    'exception.filters.k8s-namespace': 'K8s 命名空间',
+    'exception.filters.k8s-pod': 'K8s Pod',
     'exception.recovery.overview.title': '概览',
     'exception.recovery.overview.copy': '返回总览工作台。',
     'exception.recovery.back.title': '返回',
diff --git a/web-next/test/i18n-test-helper.ts b/web-next/test/i18n-test-helper.ts
index 66adf6358b..ed498db025 100644
--- a/web-next/test/i18n-test-helper.ts
+++ b/web-next/test/i18n-test-helper.ts
@@ -24,19 +24,47 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
   'en-US': {
     'common.active': 'Active',
     'common.annotation.bind': 'Annotations',
+    'common.api.message-nonzero': 'API message returned non-zero code',
+    'common.api.request-failed-status': 'API request failed: {{status}}',
     'common.attention': 'attention',
     'common.app': 'App',
     'common.application': 'application',
+    'common.boolean.true': 'true',
+    'common.boolean.false': 'false',
+    'common.button.cancel': 'Cancel',
+    'common.button.ok': 'OK',
+    'common.button.setting': 'Configure',
+    'common.button.save': 'Save',
+    'common.button.delete': 'Delete selected',
+    'common.button.retry': 'Retry',
     'common.button.delete-batch': 'Bulk delete',
+    'common.button.export': 'Export',
+    'common.button.import': 'Import',
+    'common.help': 'Help',
     'common.button.help': 'Help',
+    'common.confirm.delete': 'Please confirm whether to delete!',
+    'common.confirm.delete-batch': 'Delete selected items?',
+    'common.confirm.cancel': 'Please confirm whether to cancel monitor!',
+    'common.confirm.cancel-batch': 'Please confirm whether to cancel monitor in batches!',
+    'common.confirm.enable': 'Please confirm whether to enable monitor!',
+    'common.confirm.enable-batch': 'Please confirm whether to enable monitor in batches!',
+    'validation.required': 'Please fill in the required fields! ',
     'common.copy-failed': 'Copy failed',
     'common.current-page-count': 'Current page',
     'common.current-view': 'Current view',
+    'common.status': 'Status',
+    'common.dark-mode': 'Dark mode',
     'common.description': 'Description',
+    'common.failed': 'Failed',
     'common.decrement': 'Decrease',
+    'common.duration.seconds': '{{value}}s',
+    'common.edit': 'Operate',
     'common.edit-time': 'Updated',
     'common.empty': 'empty',
     'common.no-data': 'No data',
+    'common.tab-navigation': 'Tab navigation',
+    'common.timeline.empty': 'No events yet',
+    'common.enable': 'Enable',
     'common.enabled': 'enabled',
     'common.disabled': 'disabled',
     'common.focus': 'Focus',
@@ -52,21 +80,102 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'common.next-series': 'Next series',
     'common.next-step': 'Next step',
     'common.none': 'None',
+    'common.yes': 'Yes',
+    'common.no': 'No',
+    'common.open': 'Open',
     'common.previous': 'Previous',
     'common.previous-point': 'Previous point',
     'common.previous-series': 'Previous series',
     'common.loading': 'Loading...',
+    'common.view-more': 'View more',
+    'common.workbench.loading.title': 'Loading workspace',
+    'common.workbench.loading.copy': 'Preparing page data. Please wait.',
     'common.apply': 'Apply',
     'common.scrape': 'Scrape',
+    'common.locale-code': 'en-US',
     'common.refresh': 'Refresh',
     'common.reset': 'Reset',
+    'common.search': 'Search',
+    'common.clear': 'Clear',
     'common.save': 'Save',
+    'common.save-failed': 'Save failed',
+    'common.save-success': 'Saved successfully',
     'common.saving': 'Saving...',
     'common.live': 'Live',
+    'common.notice': 'Notice',
+    'common.unknown': 'Unknown',
+    'menu.fullscreen': 'Fullscreen',
+    'menu.fullscreen.exit': 'Exit fullscreen',
+    'alert.notify.title': 'New Alert',
+    'alert.notify.body': 'You have a new alert, please handle it in time',
+    'common.notify.apply-success': 'Applied successfully',
+    'common.notify.apply-fail': 'Apply failed',
+    'common.notify.copy-success': 'Copy Success!',
+    'common.notify.copy-fail': 'Copy Failed!',
+    'common.notify.query-fail': 'Query Failed!',
+    'common.notify.delete-success': 'Delete Success!',
+    'common.notify.delete-fail': 'Delete Failed!',
+    'common.notify.mark-success': 'Mark Success!',
+    'common.notify.mark-fail': 'Mark Failed!',
+    'common.notify.delete-pending': 'Deleting selected monitors',
+    'common.notify.edit-success': 'Edit Success!',
+    'common.notify.edit-fail': 'Edit Failed!',
+    'common.notify.operate-success': 'Operate Success!',
+    'common.notify.operate-fail': 'Operate Failed!',
+    'common.notify.enable-success': 'Enabled successfully',
+    'common.notify.enable-fail': 'Enable failed',
+    'common.notify.enable-pending': 'Enabling selected monitors',
+    'common.notify.cancel-success': 'Paused successfully',
+    'common.notify.cancel-fail': 'Pause failed',
+    'common.notify.cancel-pending': 'Pausing selected monitors',
+    'common.notify.export-success': 'Export started',
+    'common.notify.export-fail': 'Export failed',
+    'common.notify.export-pending': 'Preparing export',
+    'common.notify.export-fail-status': 'Export failed: {{status}}',
+    'common.notify.import-submitted': 'Import [{{taskName}}] submitted',
+    'common.notify.import-fail': 'Import failed',
+    'common.notify.import-pending': 'Preparing import',
+    'common.notify.import-fail-status': 'Import failed: {{status}}',
+    'common.notify.import-success-detail': 'Import [{{taskName}}] Success!',
+    'common.notify.import-fail-detail': 'Import [{{taskName}}] Failed: {{errMsg}}',
+    'common.notify.import-progress': 'Import [{{taskName}}] progress: {{progress}}',
+    'common.notify.new-success': 'Add Success!',
+    'common.notify.new-fail': 'Add Failed!',
+    'alert.notice.receiver.next': 'Please config your [Alert Notice Policy] next step!',
+    'common.notify.no-select-cancel': 'Select monitors before pausing',
+    'common.notify.no-select-delete': 'Select monitors before deleting',
+    'common.notify.no-select-enable': 'Select monitors before enabling',
+    'common.notify.no-select-export': 'No monitors selected for export',
+    'common.week.1': 'Monday',
+    'common.week.2': 'Tuesday',
+    'common.week.3': 'Wednesday',
+    'common.week.4': 'Thursday',
+    '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.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',
@@ -80,16 +189,316 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'time.range.reset': 'Reset',
     'time.range.reset-aria': 'Reset time range',
     'time.range.relative-placeholder': '45m',
+    'common.dialog.close': 'Close dialog',
     'common.ready': 'ready',
     'common.signals': 'Signals',
+    'app.frame.utility.menu': 'Menu',
+    'app.frame.utility.github': 'GitHub',
+    'app.frame.utility.user': 'User menu: {{user}}',
+    'menu.extras.setting': 'System settings',
+    'menu.account.logout': 'Logout',
+    'menu.extras.about': 'About',
+    'app.login.need-change-password': 'Please update the initial default password in time!',
+    'ai.chat.launch': 'Open AI assistant',
+    'ai.chat.title': 'AI assistant',
+    'ai.chat.subtitle': 'HertzBeat operations copilot',
+    'ai.chat.conversations': 'Conversations',
+    'ai.chat.new-chat': 'New chat',
+    'ai.chat.conversation.loading': 'Loading conversations...',
+    'ai.chat.conversation.ready': 'Conversation list synced from /chat/conversations.',
+    'ai.chat.conversation.empty': 'No saved conversations yet. Start a new chat from this workspace.',
+    'ai.chat.conversation.error': 'Conversation history is unavailable; the assistant shell remains usable.',
+    'ai.chat.conversation.delete.title': 'Delete conversation',
+    'ai.chat.conversation.delete.confirm': 'Delete',
+    'ai.chat.history.idle': 'Select a conversation to load its history.',
+    'ai.chat.history.loading': 'Loading selected conversation history...',
+    'ai.chat.history.ready': 'Selected conversation history is loaded.',
+    'ai.chat.history.empty': 'This conversation has no saved messages yet.',
+    'ai.chat.history.error': 'Selected conversation history is unavailable; try another conversation.',
+    'ai.chat.user': 'User',
+    'ai.chat.system': 'System',
+    'ai.chat.welcome.title': 'Ask HertzBeat',
+    'ai.chat.welcome.description': 'Ask about monitors, alerts, logs, traces, metrics, topology, and platform configuration.',
+    'ai.chat.initial-message': 'Initial request',
+    'ai.chat.assistant': 'Assistant',
+    'ai.chat.offline.response': 'The chat shell is ready; streaming response proof is the next parity slice.',
+    'ai.chat.typing': 'Typing...',
+    'ai.chat.error.processing': 'Sorry, there was an error processing your request. Please check if the AI Agent service is running and try again.',
+    'ai.chat.input.placeholder': 'Ask assistant',
+    'ai.chat.input.hint': 'Press Enter from the header prompt to open this assistant workspace.',
+    'ai.chat.submit': 'Send prompt',
+    'ai.chat.modify-api-key': 'Modify API key',
+    'ai.chat.config.title': 'AI Provider Configuration',
+    'ai.chat.config.required.content': 'To use AI Agent Chat, please configure your Model Provider.',
+    'ai.chat.config.provider': 'AI Provider',
+    'ai.chat.config.provider.help': 'Choose your AI provider (OpenAI, ZhiPu, or ZAI)',
+    'ai.chat.config.provider.required': 'Please select an AI provider',
+    'ai.chat.config.api-key': 'API Key',
+    'ai.chat.config.api-key.help': 'Your Provider API key. The key will be validated when saved.',
+    'ai.chat.config.api-key.required': 'API Key is required',
+    'ai.chat.config.base-url': 'Base URL',
+    'ai.chat.config.base-url.help': 'Custom API endpoint URL. Leave empty to use default for selected provider.',
+    'ai.chat.config.base-url.required': 'Base URL is required',
+    'ai.chat.config.model': 'Model',
+    'ai.chat.config.model.help': 'Model name to use. Leave empty to use default for selected provider.',
+    'ai.chat.config.model.required': 'Model is required',
+    'ai.chat.config.reset': 'Reset to default values',
+    'ai.chat.config.save': 'Save',
+    'ai.chat.config.cancel': 'Cancel',
+    'ai.chat.config.idle': 'Provider configuration is ready to load.',
+    'ai.chat.config.loading': 'Loading provider configuration...',
+    'ai.chat.config.ready': 'Provider configuration loaded from /config/provider.',
+    'ai.chat.config.saving': 'Saving provider configuration...',
+    'ai.chat.config.save.success': 'Model Provider configuration saved successfully!',
+    'ai.chat.config.save.failed': 'Failed to save configuration:',
+    'ai.chat.config.validation.failed': 'API Key validation failed:',
+    'ai.chat.schedule.title': 'Schedule Configuration',
+    'ai.chat.schedule.create': 'Create Schedule',
+    'ai.chat.schedule.add': 'Add New Task',
+    'ai.chat.schedule.configured': 'Configured Schedules',
+    'ai.chat.schedule.skill': 'Skill',
+    'ai.chat.schedule.skill.select': 'Select Skill',
+    'ai.chat.schedule.skill.placeholder': 'Select skill to execute',
+    'ai.chat.schedule.cron': 'Execution Time',
+    'ai.chat.schedule.cron.label': 'Execution Time (Cron Expression)',
+    'ai.chat.schedule.cron.help': 'Format: seconds minutes hours day month week',
+    'ai.chat.schedule.cron.example.daily': 'Example: 0 0 9 * * ? (daily at 9am)',
+    'ai.chat.schedule.cron.example.weekly': 'Example: 0 0 9 ? * MON (Monday 9am)',
+    'ai.chat.schedule.cron.common': 'Common: daily 9am',
+    'ai.chat.schedule.cron.monday': 'Monday 9am',
+    'ai.chat.schedule.status': 'Status',
+    'ai.chat.schedule.action': 'Action',
+    'ai.chat.schedule.create.success': 'Schedule created successfully',
+    'ai.chat.schedule.create.failed': 'Failed to create schedule',
+    'ai.chat.schedule.delete.success': 'Schedule deleted',
+    'ai.chat.schedule.delete.failed': 'Failed to delete schedule',
+    'ai.chat.schedule.toggle.success': 'Schedule status updated',
+    'ai.chat.schedule.toggle.failed': 'Failed to toggle schedule',
+    'ai.chat.schedule.push': 'Scheduled Push',
+    'ai.chat.schedule.button': 'Schedule',
+    'ai.chat.schedule.tooltip': 'Configure scheduled push tasks',
+    'ai.chat.schedule.edit': 'Edit Schedule',
+    'ai.chat.schedule.update.success': 'Schedule updated successfully',
+    'ai.chat.schedule.update.failed': 'Failed to update schedule',
+    'ai.chat.schedule.idle': 'Schedule configuration is ready to load.',
+    'ai.chat.schedule.loading': 'Loading schedules and skills...',
+    'ai.chat.schedule.ready': 'Schedules loaded from /ai/schedule.',
+    'ai.chat.schedule.empty': 'No configured schedules for this conversation.',
+    'ai.chat.schedule.saving': 'Saving schedule...',
+    'ai.chat.schedule.error': 'Failed to load schedules.',
+    'platform.footer.license': 'Licensed under the Apache License, Version 2.0',
+    'collector.health.cluster.title': 'Collection cluster health',
+    'collector.health.cluster.copy': 'Collectors {{online}} / {{total}} online',
+    'collector.health.cluster.meta': 'Tasks {{tasks}} · offline {{offline}}',
+    'collector.health.cluster.freshness': 'Last report {{time}}',
+    'collector.health.monitor.title': 'Collection health',
+    'collector.health.monitor.copy': 'Monitors {{healthy}} / {{total}} healthy',
+    'collector.health.monitor.empty-copy': 'No bound monitors',
+    'collector.health.monitor.meta': 'Collection evidence merged',
+    'collector.health.monitor.empty-meta': 'Waiting for template binding',
+    'collector.health.monitor.freshness': 'Latest evidence {{time}}',
     'entities.detail.loading': 'Loading entity detail...',
     'entities.detail.delete.failed': 'Failed to delete entity.',
+    'entities.detail.fallback.display-name': 'Entity {{id}}',
+    'entities.detail.fallback.draft-name': 'Entity draft',
+    'entities.detail.fallback.description': 'Temporary workspace shown while backend entity detail is unavailable.',
+    'entities.detail.action-text.open-definition': 'Open definition',
+    'entities.detail.action-text.review-definition': 'Review the definition workspace before adding ownership or evidence.',
+    'entities.definition.workspace.attribution-check': 'Attribution check',
+    'entities.definition.workspace.attribution-state.ready': 'Ready',
+    'entities.definition.workspace.attribution-state.missing': 'Needs data',
+    'entities.definition.workspace.attribution-state.unknown': 'Needs review',
+    'entities.definition.workspace.metric.ready': 'Ready to import',
+    'entities.definition.workspace.metric.attention': 'Needs attention',
+    'entities.definition.workspace.metric.telemetry-gaps': 'Missing telemetry bindings',
+    'entities.definition.workspace.fact.workspace': 'Workspace',
+    'entities.definition.workspace.fact.format': 'Format',
+    'entities.definition.workspace.fact.activities': 'Activities',
+    'entities.definition.workspace.fact.templates': 'Templates',
+    'entities.definition.workspace.current-definition': 'Current definition',
+    'entities.definition.workspace.template-title': 'Custom templates',
+    'entities.definition.workspace.template-error-help': 'After definition loading fails, confirm whether the entity exists before continuing edits.',
+    'entities.definition.workspace.template-empty-title': 'No templates',
+    'entities.definition.workspace.template-empty-copy': 'Reusable templates appear here.',
+    'entities.definition.workspace.batch-title': 'Templates and batch editing',
+    'entities.definition.workspace.batch-copy': 'Review templates, draft source, and optional actions.',
+    'entities.definition.workspace.batch-expand': 'Expand',
+    'entities.definition.workspace.activity-title': 'Recent activity',
+    'entities.definition.workspace.activity-empty-title': 'No recent activity',
+    'entities.definition.workspace.activity-empty-copy': 'Preview and import activity appears here.',
+    'entities.definition.workspace.entry-title': 'Entry',
+    'entities.definition.workspace.entry-view-entity': 'View entity',
+    'entities.definition.workspace.entry-back-to-form': 'Back to form',
+    'entities.definition.message.load-failed': 'Failed to load entity definition.',
+    'entities.definition.message.entity-not-exist': 'Entity does not exist.',
+    'entities.definition.message.process-failed': 'Entity definition processing failed.',
+    'entities.definition.message.update-missing-entity': 'Cannot confirm which entity definition to update.',
+    'entities.definition.message.update-failed': 'Failed to save entity definition.',
+    'entities.definition.message.updated': 'Entity definition updated.',
+    'entities.definition.message.backend-fallback': 'Backend reported: {{message}}',
+    'entity.definition.import.empty-preview': 'Paste a definition before previewing.',
+    'entity.definition.import.empty-save': 'Paste or load a definition before saving.',
+    'entity.definition.import.empty-import': 'Paste a definition before importing.',
+    'entity.definition.import.activity.preview-one': 'Previewed 1 definition.',
+    'entity.definition.import.activity.preview-many': 'Previewed {{count}} definitions.',
+    'entity.definition.import.parse-failed': 'Definition parsing failed.',
+    'entity.definition.workspace.single-required': 'Editing an existing entity can only save one definition document at a time.',
+    'entity.definition.import.blocked': 'Some definitions still need attention. Review them before importing.',
+    'entity.definition.import.success-bundle': 'Imported {{count}} entities.',
+    'entity.definition.import.success-single': 'Entity definition imported.',
+    'entity.definition.import.create-failed': 'Entity import failed.',
+    'entity.definition.import.format': 'Import Format',
+    'entity.definition.import.format.unknown': 'Unknown format {{format}}',
+    'entity.definition.format.option.yaml': 'YAML',
+    'entity.definition.format.option.json': 'JSON',
+    'entity.definition.format.option.curl': 'cURL',
+    'entity.definition.workspace.template.custom': 'Custom template',
+    'entity.field.owner': 'Owner',
+    'entity.field.system': 'System',
+    'entity.field.implemented-by': 'Implemented by',
+    'entity.field.runbook': 'Runbook',
+    'entity.definition.import.gap.telemetry': 'Telemetry bindings',
+    'entity.definition.import.telemetry.ready': 'Telemetry binding detected',
+    'entity.definition.import.telemetry.pending': 'Telemetry binding still missing',
+    'entity.definition.import.validation.ready': 'Ready to import',
+    'entity.definition.import.validation.needs-attention': 'Needs details before import',
+    'entity.definition.import.kind.unknown': 'Unknown type {{kind}}',
+    'entity.definition.import.source.unknown': 'Unknown source {{source}}',
+    'entity.definition.import.summary.ready': 'Ready to import',
+    'entity.definition.import.summary.attention': 'Needs attention',
+    'entity.definition.import.summary.telemetry': 'Telemetry still missing',
+    'entity.definition.import.queue.ready.title': 'Ready to import',
+    'entity.definition.import.queue.ready.copy': 'These definitions already meet the basic requirements and can move straight into import or save.',
+    'entity.definition.import.queue.ready.action': 'View ready definitions',
+    'entity.definition.import.queue.attention.title': 'Needs attention first',
+    'entity.definition.import.queue.attention.copy': 'These definitions still miss owner, system, runbook, or interface details. Fix them before import.',
+    'entity.definition.import.queue.attention.action': 'View blocked definitions',
+    'entity.definition.import.queue.telemetry.title': 'Still needs telemetry after import',
+    'entity.definition.import.queue.telemetry.copy': 'These definitions still do not have telemetry bindings and should return to telemetry discovery after import.',
+    'entity.definition.import.queue.telemetry.action': 'View telemetry-pending definitions',
+    'entity.definition.import.placeholder.yaml': 'Paste a YAML entity definition. The editor parses basic information, telemetry bindings, and dependencies.',
+    'entity.definition.import.placeholder.json': 'Paste a JSON entity definition. The editor parses basic information, telemetry bindings, and dependencies.',
+    'entity.definition.import.placeholder.curl': 'Paste a cURL request generated from preview or automation flows.',
+    'entity.definition.import.attribution.ready': 'Attribution complete',
+    'entity.definition.import.attribution.missing': 'Attribution missing',
+    'entity.definition.import.attribution.review': 'Attribution needs review',
+    'entity.definition.import.activity.status.success': 'Success',
+    'entity.definition.import.activity.status.saved': 'Saved',
+    'entity.definition.import.activity.status.starter': 'Starter',
+    'entity.definition.import.activity.status.failed': 'Failed',
+    'entity.definition.import.activity.status.preview': 'Preview',
+    'entity.definition.import.activity.status.unknown': 'Unknown status {{status}}',
+    'entity.definition.import.activity.summary.telemetry-discovery-applied': 'Telemetry discovery applied',
+    'entity.definition.import.activity.summary.catalog-entity-updated': 'Catalog entity updated',
+    'entity.definition.import.activity.summary.catalog-entity-created': 'Catalog entity created',
+    'entity.definition.import.activity.field.endpoint': 'Endpoint',
+    'entity.definition.import.activity.field.service': 'Service',
+    'entity.definition.import.activity.field.system': 'System',
+    'entity.definition.import.activity.field.source': 'Source',
+    'entity.definition.import.activity.field.owner': 'Owner',
+    'entity.definition.import.activity.field.environment': 'Environment',
+    'entity.definition.import.activity.field.evidence': 'Evidence',
+    'entity.definition.import.activity.field.monitor-binds': 'monitor binds',
+    'entity.definition.import.template.source.workspace': 'Workspace',
+    'entity.definition.import.template.source.saved': 'Saved',
+    'entity.definition.import.template.source.starter': 'Starter',
+    'entity.definition.import.template.source.custom': 'Custom',
+    'entity.definition.import.template.source.unknown': 'Unknown source {{source}}',
+    'entities.new.loading': 'Loading entity draft',
+    'entities.edit.loading': 'Loading entity editor',
+    'entities.definition.loading': 'Loading entity definition...',
+    'entities.import.loading': 'Loading entity import...',
+    'entities.discovery.loading': 'Loading telemetry discovery...',
+    'entities.list.loading': 'Loading object directory',
+    'entities.discovery.row.status.enabled': 'Enabled',
+    'entities.discovery.row.status.normal': 'Normal',
+    'entities.discovery.row.status.abnormal': 'Abnormal',
+    'entities.discovery.row.status.review': 'Needs review',
+    'entities.discovery.row.status.unknown': 'Unknown status {{status}}',
+    'entities.discovery.row.activity.search-result': 'Search result',
+    'entities.discovery.row.activity.catalog-preset': 'Catalog preset',
+    'entities.discovery.row.missing.owner': 'owner',
+    'entities.discovery.row.missing.system': 'system',
+    'entities.discovery.row.missing.environment': 'environment',
+    'entities.discovery.row.missing.separator': ', ',
+    'entities.discovery.row.missing.copy': 'Missing {{fields}}',
+    'entities.discovery.row.missing.ready': 'Attribution context needs review',
+    'entities.discovery.row.attribution.preset.label': 'Catalog preset',
+    'entities.discovery.row.attribution.preset.copy': 'Can be used as a candidate review baseline',
+    'entities.discovery.row.attribution.preset.action': 'View preset',
+    'entities.discovery.row.attribution.resolved.label': 'Attributed',
+    'entities.discovery.row.attribution.resolved.copy': 'Already folded into the object catalog',
+    'entities.discovery.row.attribution.resolved.action': 'Open entity',
+    'entities.discovery.row.attribution.merge.label': 'Suggested merge',
+    'entities.discovery.row.attribution.merge.copy': 'Candidate entity {{candidate}}',
+    'entities.discovery.row.attribution.merge.action': 'Confirm merge',
+    'entities.discovery.row.attribution.review.label': 'Attribution incomplete',
+    'entities.discovery.row.attribution.review.action': 'Complete attribution',
+    'entities.discovery.row.attribution.create.label': 'Suggested new',
+    'entities.discovery.row.attribution.create.copy': 'Monitoring clues are ready for an entity draft',
+    'entities.discovery.row.attribution.create.action': 'Create entity',
+    'entities.discovery.service-name': '{{name}} service',
+    'entities.discovery.facts.workspace': 'Workspace',
+    'entities.discovery.facts.presets': 'Presets',
+    'entities.discovery.facts.activities': 'Activities',
+    'entities.discovery.facts.owners': 'Owners',
+    'entities.discovery.metrics.owners': 'owners',
+    'entities.discovery.metrics.systems': 'systems',
+    'entities.discovery.metrics.environments': 'environments',
+    'entities.discovery.metrics.preset-coverage': 'preset coverage',
+    'entities.discovery.metrics.ready': 'ready',
+    'entities.discovery.metrics.empty': 'empty',
+    'entities.discovery.catalog.owners': 'owners',
+    'entities.discovery.catalog.systems': 'systems',
+    'entities.discovery.catalog.environments': 'environments',
+    'entities.discovery.catalog.count': 'count {{count}}',
+    'entities.discovery.catalog.none': 'none',
+    'entities.discovery.card.action.open-resolved': 'Open resolved entity',
+    'entities.discovery.card.action.open-definition': 'Open definition',
+    'entities.discovery.card.action.merge-suggested': 'Merge into suggested entity',
+    'entities.discovery.card.action.open-suggested': 'Open suggested entity',
+    'entities.discovery.card.action.adopt-draft': 'Adopt as draft',
+    'entities.discovery.card.action.review-governance': 'Review governance',
+    'entities.discovery.card.action.send-definition': 'Send to definition workspace',
+    'entities.discovery.card.action.create-draft': 'Create entity draft',
+    'entities.discovery.card.recommendation.resolved': 'This telemetry has already been folded into the catalog, so the next step is to reopen the resolved entity or its definition workspace.',
+    'entities.discovery.card.recommendation.merge': 'A matching directory candidate already exists, so this telemetry should merge before a new draft is created.',
+    'entities.discovery.card.recommendation.enrich': 'Fill the shared governance context first, then send the monitor into the definition workspace or create a draft.',
+    'entities.discovery.card.recommendation.create': 'This telemetry already has enough shared governance context to open a new entity draft right away.',
+    'entities.discovery.card.risk.low': 'Governance risk low',
+    'entities.discovery.card.risk.medium': 'Governance risk medium',
+    'entities.discovery.card.risk.high': 'Governance risk high',
+    'entities.discovery.card.next.open': 'Next step open',
+    'entities.discovery.card.next.merge': 'Next step merge',
+    'entities.discovery.card.next.enrich': 'Next step enrich',
+    'entities.discovery.card.next.create': 'Next step create',
+    'entities.discovery.card.candidate-label': 'Suggested entity · {{candidate}} · score strong',
+    'entities.discovery.scope.all': 'All',
+    'entities.discovery.scope.matched': 'Matched',
+    'entities.discovery.scope.resolved': 'Resolved',
+    'entities.discovery.scope.suggested-new': 'Suggested new',
+    'entities.discovery.queue.review.title': 'Needs governance review',
+    'entities.discovery.queue.review.summary': '{{count}} telemetry results still need governance review before they can be merged or created.',
+    'entities.discovery.queue.review.action': 'Review governance',
+    'entities.discovery.queue.merge.title': 'Ready to merge',
+    'entities.discovery.queue.merge.summary': '{{count}} telemetry results can be selected for merge immediately.',
+    'entities.discovery.queue.merge.action': 'Select merge-ready',
+    'entities.discovery.queue.create.title': 'Suggested new entities',
+    'entities.discovery.queue.create.summary': '{{count}} telemetry results are ready to open as new entity drafts or send into the definition workspace.',
+    'entities.discovery.queue.create.action': 'Select suggested new',
+    'entities.discovery.queue.resolved.title': 'Already resolved',
+    'entities.discovery.queue.resolved.summary.present': '{{count}} telemetry results already map to resolved catalog entities.',
+    'entities.discovery.queue.resolved.summary.empty': 'No discovery results are marked as resolved yet.',
+    'entities.discovery.queue.resolved.action': 'View resolved',
+    'entities.discovery.bulk.tag.owner': 'Owner',
+    'entities.discovery.bulk.tag.system': 'System',
+    'entities.discovery.bulk.preset.apply': 'Apply {{name}}',
     'entity.health.label': 'Health score {{score}}',
     'entity.health.copy.collected': 'Collected {{healthy}} / {{total}} healthy',
     'entity.health.copy.waiting': 'Waiting for collection binding',
     'entity.health.meta': 'Alerts {{alerts}} · anomalies {{anomalies}}',
-    'topology.identity': 'HertzBeat operations topology',
-    'topology.kicker': 'Topology and impact',
+    'topology.identity': 'Operations topology',
+    'topology.kicker': 'Topology',
     'topology.copy': 'Center the selected entity and correlate OTLP calls, monitor ownership, template dependencies, K8s workloads, database and middleware connections, and alert impact in one operations graph.',
     'topology.refresh': 'Refresh topology',
     'topology.environment.aria': 'Select environment',
@@ -97,14 +506,121 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.search.aria': 'Filter topology',
     'topology.search.placeholder': 'Search entities, services, resources, or labels',
     'topology.view.fit': 'Fit view',
+    'topology.view.zoom-in': 'Zoom in',
+    'topology.view.zoom-out': 'Zoom out',
+    'topology.view.focus-selected': 'Focus selected node',
     'topology.locate-entity': 'Locate entity',
     'topology.current-filter': 'Current filter',
     'topology.all-entities': 'All entities',
+    'topology.state.label': 'Investigation state',
+    'topology.state.focus': 'Focus',
+    'topology.state.depth': 'Depth',
+    'topology.state.depth.auto': 'Auto',
+    'topology.state.depth.one-hop': '1-hop',
+    'topology.state.depth.two-hop': '2-hop',
+    'topology.state.depth.n-hop': '{{depth}}-hop',
+    'topology.state.layout': 'Layout',
+    'topology.state.layout.layered-service': 'Layered service',
+    'topology.state.layout.force': 'Force',
+    'topology.state.layout.grid-table': 'Grid / table',
+    'topology.state.group': 'Group',
+    'topology.state.group.none': 'None',
+    'topology.state.group.environment': 'Environment',
+    'topology.state.group.source-kind': 'Source kind',
+    'topology.state.group.entity-type': 'Entity type',
+    'topology.toolbar.reset': 'Reset',
+    'topology.focus-trail.label': 'Focused topology',
+    'topology.focus-trail.exit': 'Exit focus',
+    'topology.focus-trail.hidden-count': '{{count}} hidden by scope',
+    'topology.focus-trail.filter.environment': 'Environment',
+    'topology.focus-trail.filter.time-range': 'Time range',
+    'topology.focus-trail.filter.source': 'Source',
+    'topology.focus-trail.filter.view': 'View',
+    'topology.focus-trail.source.all': 'All sources',
+    'topology.group-panel.title': 'Grouping summary',
+    'topology.group-panel.copy': 'Group dense topology results without hiding the worst health and collapsed scope.',
+    'topology.group-panel.group-by': 'Group by type / environment / source',
+    'topology.group-panel.entity-type': 'Entity type',
+    'topology.group-panel.environment': 'Environment',
+    'topology.group-panel.source-kind': 'Evidence source',
+    'topology.group-panel.node-count': '{{count}} visible nodes',
+    'topology.group-panel.edge-count': '{{count}} visible edges',
+    'topology.group-panel.collapsed-count': '{{count}} collapsed',
+    'topology.group-panel.active-scope': 'active scope',
+    'topology.group-panel.clear-group': 'Clear group',
+    'topology.group-panel.open-table': 'Open table',
+    'topology.path-summary.title': 'Selected path',
+    'topology.path-summary.source': 'Source',
+    'topology.path-summary.target': 'Target',
+    'topology.path-summary.relation': 'Relation',
+    'topology.path-summary.direction': 'upstream to downstream',
+    'topology.path-summary.focus-path': 'Focus path',
+    'topology.path-summary.open-trace': 'Open trace',
     'topology.fault-context.title': 'Fault context',
     'topology.fault-context.copy': 'Entity, trace, and collection fields inherited from the current entry',
+    'topology.empty.api.title': 'No topology relationships found',
+    'topology.empty.api.copy': 'The API returned no entity, monitor, or trace-call relationship evidence for the current filter.',
+    'topology.empty.trace-call.title': 'No trace-call topology evidence',
+    'topology.empty.trace-call.copy': 'Greptime has no OTLP trace-call edges with RED metrics for the selected time and environment. Relation nodes are hidden so this view does not look complete without trace evidence.',
+    'topology.empty.trace-call.source': 'Greptime trace graph',
+    'topology.degraded.api.title': 'Topology API unavailable',
+    'topology.degraded.api.copy': 'The topology graph is degraded because API, Greptime, trace, or relation evidence could not be loaded for the selected scope.',
+    'topology.degraded.api.source': 'Topology API unavailable',
+    'topology.degraded.trace-call.title': 'Trace-call topology unavailable',
+    'topology.degraded.trace-call.copy': 'Greptime trace-call topology evidence could not be loaded for the selected time and environment. The graph stays empty instead of showing stale or unrelated relations.',
+    'topology.degraded.trace-call.source': 'Greptime trace graph',
+    'topology.loading.api.title': 'Loading topology evidence',
+    'topology.loading.api.copy': 'Fetching API, Greptime, trace, and relation evidence for the selected scope.',
     'topology.edge.evidence.aria': '{{label}} relationship evidence',
     'topology.aside.view': 'View',
+    'topology.companion.jump.aria': 'Topology companion sections',
+    'topology.companion.jump.view': 'View',
+    'topology.companion.jump.legend': 'Legend',
+    'topology.companion.jump.red': 'RED',
+    'topology.companion.jump.timeline': 'Timeline',
+    'topology.companion.jump.edge': 'Edge',
+    'topology.companion.jump.node': 'Node',
+    'topology.companion.section.collapse': 'Collapse',
+    'topology.companion.section.expand': 'Expand',
     'topology.alert-impact.open': 'Open alert impact',
+    'topology.alert-impact.copy': 'Keep the selected edge, entity, and signal scope.',
+    'topology.legend.title': 'Legend',
+    'topology.legend.summary': '2 groups',
+    'topology.legend.health': 'Health',
+    'topology.legend.health.healthy': 'Healthy',
+    'topology.legend.health.healthy-value': 'good',
+    'topology.legend.health.warning': 'Warning',
+    'topology.legend.health.warning-value': 'watch',
+    'topology.legend.health.critical': 'Critical',
+    'topology.legend.health.critical-value': 'act',
+    'topology.legend.status': 'Status',
+    'topology.legend.status.healthy': 'Healthy',
+    'topology.legend.status.healthy-value': 'healthy',
+    'topology.legend.status.warning': 'Warning',
+    'topology.legend.status.warning-value': 'attention',
+    'topology.legend.status.critical': 'Critical',
+    'topology.legend.status.critical-value': 'action',
+    'topology.legend.interaction': 'Interaction',
+    'topology.legend.interaction.selected-node': 'Selected',
+    'topology.legend.interaction.selected-node-value': 'selected',
+    'topology.legend.interaction.directional-edge': 'Edge',
+    'topology.legend.interaction.directional-edge-value': 'direction',
+    'topology.legend.interaction.dimmed-edge': 'Dimmed',
+    'topology.legend.interaction.dimmed-edge-value': 'context',
+    'topology.legend.source-kind': 'Evidence source',
+    'topology.legend.source.trace': 'Trace calls',
+    'topology.legend.source.trace-value': 'trace',
+    'topology.legend.source.monitor': 'Monitor bind',
+    'topology.legend.source.monitor-value': 'monitor',
+    'topology.legend.source.relation': 'Entity relation',
+    'topology.legend.source.relation-value': 'relation',
+    'topology.legend.confidence': 'Confidence',
+    'topology.legend.confidence.live': 'Current evidence',
+    'topology.legend.confidence.stale': 'Low confidence / stale',
+    'topology.timeline.title': 'Impact timeline',
+    'topology.timeline.copy': 'Recent entity, relationship, and monitor evidence that changed this topology view.',
+    'topology.timeline.detail.fallback': 'Topology evidence changed',
+    'topology.timeline.actor.system': 'system',
     'topology.edge.evidence.title': 'Relationship evidence',
     'topology.edge.link.from-entity': 'Source entity',
     'topology.edge.link.to-entity': 'Target entity',
@@ -114,6 +630,32 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.edge.link.traces': 'Traces evidence',
     'topology.current-entity': 'Current entity',
     'topology.current-entity.signals': 'Signals: {{signals}}',
+    'topology.current-entity.boundary': 'Signal handoffs keep the current entity, time range, environment, and topology scope.',
+    'topology.current-entity.fact.entity-id': 'Entity id',
+    'topology.current-entity.fact.namespace': 'Namespace',
+    'topology.current-entity.fact.signals': 'Signals',
+    'topology.current-entity.fact.health': 'Health',
+    'topology.current-entity.fact.upstream': 'Upstream',
+    'topology.current-entity.fact.downstream': 'Downstream',
+    'topology.current-entity.fact.neighbor-count': '{{count}} dependencies',
+    'topology.current-entity.fact.neighbor.none': 'No dependencies in scope',
+    'topology.metric-table.title': 'Edge metric ranking',
+    'topology.metric-table.empty': 'No relationship metrics in this scope',
+    'topology.metric-table.edge-count': '{{count}} edges',
+    'topology.metric-table.request-rate-unit': 'req/s',
+    'topology.metric-table.error-rate-unit': 'errors',
+    'topology.metric-table.latency-p95-unit': 'p95',
+    'topology.metric-table.row-action': 'Inspect',
+    'topology.metric-table.open-edge-aria': 'Open topology edge {{edge}}',
+    'topology.metric-table.filter.all': 'All',
+    'topology.metric-table.filter.visible': 'Visible',
+    'topology.metric-table.filter.partial': 'Partial',
+    'topology.metric-table.filter.hidden': 'Hidden',
+    'topology.metric-table.filter.unknown': 'Unknown',
+    'topology.red.request-rate': 'Request rate',
+    'topology.red.error-rate': 'Error rate',
+    'topology.red.latency-p95': 'P95 latency',
+    'topology.red.source.trace-service-graph': 'Trace-derived service graph',
     'topology.context-link.entity': 'View entity detail',
     'topology.context-link.signals': 'Open three signals',
     'topology.context-link.metrics': 'Metrics workbench',
@@ -158,12 +700,16 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.evidence.source.alert-impact': 'Current alert projected onto service/resource dependencies',
     'topology.evidence.collector.otlp': 'OTLP Collector',
     'topology.evidence.collector.hertzbeat': 'HertzBeat Collector',
-    'topology.evidence.boundary': 'This edge only uses collected relationship evidence; dependency auto-discovery, change timelines, and root cause analysis remain roadmap capabilities.',
+    'topology.evidence.boundary': 'This edge only uses collected relationship evidence; dependency auto-discovery and root cause analysis remain roadmap capabilities.',
     'topology.evidence.alert-impact.copy': 'Open alert impact with the current edge, entity, and three-signal context.',
     'topology.evidence.row.from': 'Source entity',
     'topology.evidence.row.to': 'Target entity',
     'topology.evidence.row.source': 'Collection evidence',
     'topology.evidence.row.alert-impact': 'Alert impact',
+    'topology.hover.fact.relation-type': 'Relation',
+    'topology.hover.fact.first-seen': 'First seen',
+    'topology.hover.fact.last-seen': 'Last seen',
+    'topology.hover.fact.sample-trace': 'Sample trace',
     'topology.evidence.alert-impact.none': 'No current impact',
     'topology.evidence.alert-impact.warning': 'Impact present',
     'topology.evidence.alert-impact.critical': 'Critical impact',
@@ -171,6 +717,8 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'common.settings-mode': 'settings',
     'common.stable': 'stable',
     'common.updated': 'Updated',
+    'common.workspace': 'Workspace',
+    'common.workspace-navigation': 'Workspace navigation',
     'label.bind': 'Labels',
     'menu.dashboard': 'Overview',
     'menu.section.ingestion': 'Ingestion & collection',
@@ -181,12 +729,36 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'menu.section.settings': 'Platform settings',
     'menu.overview': 'Dashboard',
     'menu.log.integration': 'Integration',
+    'monitors.loading': 'Loading monitor center',
     'monitor.list': 'Monitors',
     'monitor.scrape': 'Collection mode',
     'monitor.status.up': 'Up',
     'monitor.status.down': 'Down',
     'monitor.status.paused': 'Paused',
+    'monitors.status.up': 'Up',
+    'monitors.status.down': 'Down',
+    'monitors.status.paused': 'Paused',
+    'monitors.type.all': 'All types',
     'monitor.edit-monitor': 'Edit monitor',
+    'monitor.scrape.type.static': 'Static Scrape',
+    'monitor.scrape.type.http_sd': 'Http Service Discovery',
+    'monitor.scrape.type.nacos_sd': 'Nacos Service Discovery',
+    'monitor.scrape.type.dns_sd': 'Dns Service Discovery',
+    'monitor.scrape.type.eureka_sd': 'Eureka Service Discovery',
+    'monitor.scrape.type.consul_sd': 'Consul Service Discovery',
+    'monitor.scrape.type.zookeeper_sd': 'Zookeeper Service Discovery',
+    'entity.monitor.workbench.reason.down': 'This monitor is unhealthy. Check it first.',
+    'entity.monitor.workbench.reason.fallback': 'There are no unhealthy monitors right now, so this view shows all linked monitors.',
+    'entity.monitor.workbench.reason.paused': 'This monitor is paused. Check whether it should be resumed.',
+    'entity.monitor.workbench.reason.default': 'This monitor helps validate coverage and recent state.',
+    'entity.response.open-related-logs': 'Open related logs',
+    'entity.response.open-related-traces': 'Open related traces',
+    'entity.response.open-code': 'Open code',
+    'monitors.controls.more-actions': 'More monitor operations',
+    'monitor.copy.action': 'Copy monitor',
+    'monitor.copy.success': 'Monitor copied',
+    'monitor.copy.failed': 'Copy monitor failed',
+    'monitor.item.unavailable.refresh': 'This monitor item is no longer available, the list will refresh automatically',
     'monitor.editor.labels.add': 'Add label',
     'monitor.editor.labels.copy': 'Manage monitor labels as key/value rows.',
     'monitor.editor.labels.key': 'Label key',
@@ -201,12 +773,20 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.editor.validation.app': 'Monitor type is required',
     'monitor.editor.validation.intervals': 'Collection interval is required',
     'monitor.editor.validation.param-required': '{{field}} is required',
+    'monitor.route-state.form.error.title': 'Failed to load the monitor form',
+    'monitor.route-state.form.error.copy': 'The monitor form could not be initialized right now. Please try again.',
+    'monitor.route-state.action.retry': 'Retry',
+    'monitor.route-state.loading.title': 'Loading monitor details',
+    'monitor.route-state.loading.copy': 'Please wait while the page prepares the monitor instance, metric summary, and history trends.',
+    'monitor.route-state.error.title': 'Failed to load monitor details',
+    'monitor.route-state.error.copy': 'The monitor details could not be loaded right now. Try again later or reopen the monitor from the list.',
     'monitor.editor.loading.edit': 'Loading monitor settings...',
     'monitor.editor.loading.new': 'Loading monitor form...',
     'monitor.editor.section.base': 'Basic settings',
     'monitor.editor.section.base.copy': 'Choose the monitor type, target, and collection schedule.',
     'monitor.editor.section.params': 'Runtime parameters',
     'monitor.editor.section.params.copy': 'Configure the parameters required by the selected monitor template.',
+    'monitor.editor.section.runtime': 'Collection schedule',
     'monitor.editor.section.advanced.copy': 'Configure labels, annotations, and linked dashboards for filtering and follow-up.',
     'monitor.editor.section.payload': 'Collection payload',
     'monitor.editor.section.payload.copy': 'Configure the request body or script payload used by this monitor.',
@@ -217,6 +797,9 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.overview.kicker': 'Overview',
     'monitor.detail.title-fallback': 'Monitor detail',
     'monitor.detail.open-grafana': 'Open Grafana',
+    'monitor.detail.auto-refresh': 'Auto Refresh After {{time}} s',
+    'monitor.detail.close-refresh': 'Close Auto Refresh',
+    'monitor.detail.config-refresh': 'Set Auto Refresh For {{time}} s',
     'monitor.detail.refresh-countdown': 'Refresh soon',
     'monitor.detail.refresh-disabled': 'Auto refresh off',
     'monitor.detail.status.up': 'UP',
@@ -261,6 +844,7 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.grafana.enabled.meta': 'external dashboard',
     'monitor.detail.grafana.copy': 'Open the linked Grafana dashboard when one is configured.',
     'monitor.detail.grafana.status': 'Dashboard status',
+    'monitor.detail.grafana.configure': 'Configure dashboard',
     'monitor.detail.param.meta': 'runtime param',
     'monitor.detail.param.unknown': 'Parameter',
     'monitor.detail.schedule.empty': 'Not scheduled',
@@ -317,6 +901,10 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.favorite.catalog.title': 'Favorite catalog',
     'monitor.detail.favorite.remove': 'Remove favorite',
     'monitor.detail.favorite.add': 'Add favorite',
+    'monitor.favorite.add.success': 'Added to favorites successfully',
+    'monitor.favorite.add.failed': 'Failed to add to favorites',
+    'monitor.favorite.remove.success': 'Removed from favorites successfully',
+    'monitor.favorite.remove.failed': 'Failed to remove from favorites',
     'monitor.detail.metric.row-label': 'Row',
     'monitor.detail.metric.row': 'Row {{index}}',
     'monitor.detail.metric.fullscreen.enter': 'Fullscreen',
@@ -387,17 +975,323 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitors.empty-selected.title': 'No monitor selected',
     'monitors.empty-selected.copy': 'Select a monitor from the list to inspect actions and context.',
     'ops.surface.mode.entry': 'entry surface',
+    'ops.surface.kicker': 'Operator surface',
+    'ops.surface.route-contract.title': 'Route contract',
+    'ops.surface.route-contract.copy': 'Shared shell status stays visible during the landing phase.',
+    'ops.surface.launch-checklist': 'Launch checklist',
+    'ops.surface.route-posture': 'Route posture',
+    'ops.surface.entry-shell': 'Entry shell ready',
+    'ops.surface.fact.focus-label': 'Focus',
+    'ops.surface.fact.mode-label': 'Mode',
+    'ops.surface.fact.mode-entry': 'entry surface',
+    'ops.surface.fact.signals-label': 'Signals',
+    'ops.surface.route-posture.mode-label': 'Mode',
     'ops.surface.shared-shell.title': 'Shared shell',
     'ops.surface.shared-shell.copy': 'The entry surface keeps the same shell and information hierarchy.',
     'ops.surface.focused-scope.title': 'Focused scope',
     'ops.surface.focused-scope.copy': 'Only carry navigation, context, and the next hop.',
+    'overview.loading': 'Loading overview console',
+    'overview.guidance.default.start': 'Next',
+    'overview.guidance.default.reasons': 'Reasons',
+    'overview.guidance.default.next': 'After that',
+    'overview.checklist.status.ready': 'Ready',
+    'overview.checklist.status.pending': 'Pending',
+    'overview.lane.entities.title': 'Entity catalog',
+    'overview.lane.entities.eyebrow': 'Entity-first',
+    'overview.lane.entities.copy': 'Start from monitored entities and service ownership before jumping into signals.',
+    'overview.lane.entities.stat': '{{count}} entities tracked',
+    'overview.lane.entities.action': 'Open entities',
+    'overview.lane.logs.title': 'Logs',
+    'overview.lane.logs.eyebrow': 'Runtime evidence',
+    'overview.lane.logs.copy': 'Inspect log entries with the same service, entity, and time context.',
+    'overview.lane.logs.stat.available': 'Logs available',
+    'overview.lane.logs.action': 'Open logs',
+    'overview.lane.trace.title': 'Traces',
+    'overview.lane.trace.eyebrow': 'Request path',
+    'overview.lane.trace.copy': 'Follow spans when trace context is present, then return to entity evidence.',
+    'overview.lane.trace.stat.distributed': 'Distributed traces ready',
     'overview.lane.trace.stat.detected': 'Alert contains trace ID',
+    'overview.lane.trace.action': 'Open traces',
+    'overview.lane.otlp.title': 'OTLP metrics',
+    'overview.lane.otlp.eyebrow': 'Three-signal ingest',
+    'overview.lane.otlp.copy': 'Inspect incoming metric series without leaving the private HertzBeat workspace.',
+    'overview.lane.otlp.stat': '{{count}} monitored apps',
+    'overview.lane.otlp.action': 'Open metrics',
+    'explorer.kicker': 'Observability',
+    'explorer.title': 'Query Workbench',
+    'explorer.subtitle': 'Search traces, logs, and metrics together, then use the query row, trend band, table, and detail panel to locate evidence.',
+    'explorer.tags.unified-query': 'Unified query',
+    'explorer.tags.context-retention': 'Context retained',
+    'explorer.tags.cross-signal-search': 'Cross-signal search',
+    'explorer.focus': 'Cross-signal query entry',
+    'explorer.summary': 'Keep the cold workbench compact while preserving handoffs into HertzBeat traces, logs, metrics, and entity catalog.',
+    'explorer.lanes.query.title': 'Query row',
+    'explorer.lanes.query.copy': 'Capture service, status, and resource attributes before opening signal-specific evidence.',
+    'explorer.lanes.query.meta': 'Query row contract',
+    'explorer.lanes.results.title': 'Results panel',
+    'explorer.lanes.results.copy': 'Keep trace, log, and metric results in one dense table for comparison.',
+    'explorer.lanes.results.meta': 'Results panel',
+    'explorer.lanes.drilldown.title': 'Context handoff',
+    'explorer.lanes.drilldown.copy': 'Preserve links to traces, logs, metrics, and the entity catalog.',
+    'explorer.lanes.drilldown.meta': 'Context handoff',
+    'explorer.checklist.entry.title': 'Route entry',
+    'explorer.checklist.entry.copy': 'The route owns the cold workbench entry and leaves interaction in the client island.',
+    'explorer.checklist.entry.meta': 'done',
+    'explorer.checklist.adapters.title': 'Adapters',
+    'explorer.checklist.adapters.copy': 'Backend-backed adapters remain the next step after the static convergence pass.',
+    'explorer.checklist.adapters.meta': 'next',
+    'explorer.checklist.links.title': 'Signal links',
+    'explorer.checklist.links.copy': 'Handoff URLs stay stable for traces, logs, metrics, and entities.',
+    'explorer.checklist.links.meta': 'reserved',
+    'explorer.actions.save-view': 'Save view',
+    'explorer.actions.create-alert': 'Create alert',
+    'explorer.actions.add-dashboard': 'Add to dashboard',
+    'explorer.query.aria': 'Query condition',
+    'explorer.query.placeholder': 'Filter by service, status, or resource attributes',
+    'explorer.query.default': 'service.name = "checkout" AND status = "error"',
+    'explorer.query.run': 'Run query',
+    'explorer.signal.aria': 'Signal type',
+    'explorer.signal.all': 'All signals',
+    'explorer.sort.aria': 'Sort order',
+    'explorer.sort.time-desc': 'Newest first',
+    'explorer.sort.duration-desc': 'Slowest first',
+    'explorer.filters.title': 'Quick filters',
+    'explorer.filters.clear': 'Clear',
+    'explorer.filters.signal-type': 'Signal type',
+    'explorer.filters.deployment-environment': 'Deployment environment',
+    'explorer.filters.service-name': 'Service name',
+    'explorer.filters.status': 'Status',
+    'explorer.chart.title': 'Trend band',
+    'explorer.chart.subtitle': 'Cross-signal hits in the last 30 minutes',
+    'explorer.chart.result-count': '{{count}} results',
+    'explorer.results.title': 'Cross-signal results',
+    'explorer.results.count': '{{count}} rows',
+    'explorer.table.signal-type': 'Signal type',
+    'explorer.table.service': 'Service',
+    'explorer.table.operation': 'Operation',
+    'explorer.table.status': 'Status',
+    'explorer.table.duration': 'Duration',
+    'explorer.table.time': 'Time',
+    'explorer.detail.title': 'Detail panel',
+    'explorer.detail.signal': 'Signal',
+    'explorer.detail.status': 'Status',
+    'explorer.detail.duration': 'Duration',
+    'explorer.handoff.trace': 'View trace',
+    'explorer.handoff.log': 'View logs',
+    'explorer.handoff.metrics': 'View metrics',
+    'explorer.handoff.entities': 'Entity catalog',
+    'explorer.rows.trace.signal': 'Trace',
+    'explorer.rows.log.signal': 'Logs',
+    'explorer.rows.metric.signal': 'Metrics',
+    'explorer.rows.exception.signal': 'Exception',
+    'explorer.rows.log.operation': 'Payment failed: insufficient balance',
+    'explorer.status.error': 'Error',
+    'explorer.status.normal': 'Normal',
     'log.integration.redirect.title': 'Log intake now opens the HertzBeat OTLP intake center',
     'log.integration.redirect.description': 'Logs, metrics, and traces now share the HertzBeat OTLP intake path. You can still open logs to inspect received data.',
     'log.integration.redirect.cta.ingestion': 'Open HertzBeat OTLP Intake',
     'log.integration.redirect.cta.manage': 'Open Logs Workbench',
+    'actions.entry.kicker': 'Automation entry',
+    'actions.entry.title': 'Automation actions',
+    'actions.entry.subtitle': 'Align entry context and approval semantics with the OTLP cold baseline.',
+    'actions.entry.action.overview': 'Open overview',
+    'actions.entry.action.entities': 'View entities',
+    'actions.entry.shell.eyebrow': 'Cold entry connected',
+    'actions.entry.shell.copy': 'This entry inherits the near-black canvas, compact controls, hard-edged panels, and static right rail from the OTLP intake page; execution adapters can plug in next.',
+    'actions.entry.chip.catalog': 'Automation catalog',
+    'actions.entry.chip.risk': 'Risk actions',
+    'actions.entry.chip.approval': 'Approval flow',
+    'actions.entry.checklist.title': 'Integration checklist',
+    'actions.entry.checklist.context.title': 'Unify entry context',
+    'actions.entry.checklist.context.copy': 'The route already shares the OTLP baseline page rhythm.',
+    'actions.entry.checklist.adapter.title': 'Connect execution adapter',
+    'actions.entry.checklist.adapter.copy': 'Action catalog and approval-flow data can mount when the adapter lands.',
+    'actions.entry.checklist.evidence.title': 'Keep evidence jumps',
+    'actions.entry.checklist.evidence.copy': 'Entity, alert, and trace entries stay in the same operations context.',
+    'actions.entry.empty.title': 'Waiting for execution adapter',
+    'actions.entry.empty.copy': 'The visual skeleton already matches the OTLP baseline; data can connect incrementally.',
+    'actions.entry.suggested.title': 'Suggested actions',
+    'actions.entry.suggested.copy': 'Generated from alert, entity, and three-signal context as suggestions only; nothing runs automatically.',
+    'actions.entry.suggested.evidence': 'View evidence',
+    'actions.entry.suggested.confirm': 'Run after manual confirmation',
+    'actions.approval-draft.title': 'Approval draft adapter',
+    'actions.approval-draft.copy': 'Creates a manual approval draft from the first suggested action through the safe BFF; it never executes the action.',
+    'actions.approval-draft.create': 'Create approval draft',
+    'actions.approval-draft.pending': 'Creating draft',
+    'actions.approval-draft.success': 'Approval draft created',
+    'actions.approval-draft.failed': 'Approval draft failed',
+    'actions.approval-draft.disabled': 'Alert, entity, or trace context is required before an approval draft can be created.',
+    'actions.approval-draft-queue.title': 'Approval draft queue',
+    'actions.approval-draft-queue.copy': 'Reads approval drafts through the safe BFF so operators can see manager-backed lifecycle state before any execution adapter exists.',
+    'actions.approval-draft-queue.loading': 'Loading drafts',
+    'actions.approval-draft-queue.empty': 'No approval drafts returned from manager yet.',
+    'actions.approval-decision.title': 'Approval decision adapter',
+    'actions.approval-decision.copy': 'Records an approve or reject decision for the draft through the safe BFF; it never executes the action.',
+    'actions.approval-decision.approve': 'Approve draft',
+    'actions.approval-decision.reject': 'Reject draft',
+    'actions.approval-decision.pending': 'Recording decision',
+    'actions.approval-decision.success': 'Approval decision recorded',
+    'actions.approval-decision.failed': 'Approval decision failed',
+    'actions.approval-decision.disabled': 'Create an approval draft before recording a decision.',
+    'actions.catalog-adapter.title': 'Action catalog adapter',
+    'actions.catalog-adapter.copy': 'Reads durable manual-only action catalog entries before approval drafts reference them.',
+    'actions.catalog-adapter.loading': 'Loading catalog',
+    'actions.catalog-adapter.empty': 'No manager catalog items yet.',
+    'actions.adapter-boundary.label': 'Execution boundary',
+    'actions.adapter-boundary.copy': 'Action catalog, execution history, and approval flow are roadmap sample snapshots until execution adapters land; they are not realtime run state. Alert-context suggestions are human handoffs from current evidence and never auto-execute.',
+    'actions.adapter-boundary.roadmap.workflow-automation': 'Workflow automation',
+    'actions.adapter-boundary.roadmap.action-catalog': 'Action catalog',
+    'actions.adapter-boundary.roadmap.app-builder': 'App builder',
+    'actions.adapter-boundary.roadmap.self-service-actions': 'Self-service actions',
+    'actions.adapter-boundary.roadmap.approvals': 'Approvals',
+    'actions.adapter-boundary.roadmap.scripts': 'Scripts',
+    'actions.adapter-boundary.roadmap.runbook-orchestration': 'Runbook orchestration',
+    'actions.snapshot.label': 'sample snapshot',
+    'actions.suggestion.target.current-alert': 'current alert',
+    'actions.suggestion.evidence.source': 'source {{value}}',
+    'actions.suggestion.evidence.signal': 'signal {{value}}',
+    'actions.suggestion.evidence.trace': 'trace {{value}}',
+    'actions.suggestion.evidence.alert-group': 'alert group {{value}}',
+    'actions.suggestion.evidence.current-alert': 'current alert evidence',
+    'actions.suggestion.source.entity': 'entity catalog',
+    'actions.suggestion.source.topology': 'topology',
+    'actions.suggestion.source.monitor': 'monitor center',
+    'actions.suggestion.signal.metrics': 'metrics',
+    'actions.suggestion.signal.logs': 'logs',
+    'actions.suggestion.signal.alerts': 'alerts',
+    'actions.suggestion.restart.title': 'Suggest restarting {{target}}',
+    'actions.suggestion.restart.copy': 'High-risk recovery suggestion only. Confirm blast radius, rollback window, and owner before execution.',
+    'actions.suggestion.silence.title': 'Suggest creating a temporary silence for {{target}}',
+    'actions.suggestion.silence.copy': 'Use for short-term noise reduction after ownership is confirmed and an expiry is set.',
+    'actions.suggestion.runbook.title': 'Suggest reviewing the runbook for {{target}}',
+    'actions.suggestion.runbook.copy': 'Align entity, alert, trace, and collector evidence before deciding whether to execute.',
+    'actions.suggestion.manual-required': 'Suggestion only; manual confirmation is required before execution.',
+    'actions.catalog.restart.name': 'Restart checkout deployment',
+    'actions.catalog.mute.name': 'Mute edge saturation alerts',
+    'incidents.entry.kicker': 'Incident entry',
+    'incidents.entry.title': 'Incidents',
+    'incidents.entry.subtitle': 'Align response timeline, ownership, and evidence entry points with the OTLP cold baseline.',
+    'incidents.entry.action.overview': 'Open overview',
+    'incidents.entry.action.entities': 'View entities',
+    'incidents.entry.shell.eyebrow': 'Cold entry connected',
+    'incidents.entry.shell.copy': 'This entry inherits the near-black canvas, compact controls, hard-edged panels, and static right rail from the OTLP intake page; the incident adapter can plug in next.',
+    'incidents.entry.chip.entry': 'Incident entry',
+    'incidents.entry.chip.timeline': 'Response timeline',
+    'incidents.entry.chip.owner-first': 'Owner first',
+    'incidents.entry.checklist.title': 'Integration checklist',
+    'incidents.entry.checklist.context.title': 'Unify entry context',
+    'incidents.entry.checklist.context.copy': 'The route already shares the OTLP baseline page rhythm.',
+    'incidents.entry.checklist.adapter.title': 'Connect incident adapter',
+    'incidents.entry.checklist.adapter.copy': 'Incident list, owner, and timeline data can mount when the adapter lands.',
+    'incidents.entry.checklist.evidence.title': 'Keep evidence jumps',
+    'incidents.entry.checklist.evidence.copy': 'Log, trace, and entity entries stay in the same response context.',
+    'incidents.entry.empty.title': 'Waiting for incident adapter',
+    'incidents.entry.empty.copy': 'The visual skeleton already matches the OTLP baseline; data can connect incrementally.',
+    'incidents.subtitle': 'Response timeline, ownership, and next steps stay grouped in a single operator context.',
+    'incidents.focus': 'Incident posture and response handoff',
+    'incidents.summary': 'The incidents entry page keeps response state, owners, and related evidence aligned while the deeper domain is still landing.',
+    'incidents.tag.shell': 'incident shell',
+    'incidents.tag.timeline': 'response timeline',
+    'incidents.tag.owner-first': 'owner-first',
+    'incidents.metric.open': 'Open incidents',
+    'incidents.metric.critical': 'Critical',
+    'incidents.metric.mitigating': 'Mitigating',
+    'incidents.metric.ownership-queues': 'Ownership queues',
+    'incidents.row.checkout.title': 'Checkout latency spike across prod-ap',
+    'incidents.row.checkout.service': 'checkout',
+    'incidents.row.checkout.owner': 'checkout-oncall',
+    'incidents.row.checkout.blast-radius': '2 regions',
+    'incidents.row.collector.title': 'Collector queue delay on metrics ingestion',
+    'incidents.row.collector.service': 'otlp-gateway',
+    'incidents.row.collector.owner': 'observability',
+    'incidents.row.collector.blast-radius': 'metrics only',
+    'incidents.row.edge.title': 'Edge proxy saturation after deploy',
+    'incidents.row.edge.service': 'edge-proxy',
+    'incidents.row.edge.owner': 'edge-sre',
+    'incidents.row.edge.blast-radius': 'cn-hz ingress',
+    'incidents.timeline.checkout.title': '09:08 mitigation started',
+    'incidents.timeline.checkout.copy': 'Rollback and restart actions were opened from the alert evidence bundle.',
+    'incidents.timeline.checkout.meta': 'checkout latency incident',
+    'incidents.timeline.collector.title': '08:33 metrics replay approved',
+    'incidents.timeline.collector.copy': 'Collector backlog dropped below the replay threshold and handoff stayed in the same rail.',
+    'incidents.timeline.collector.meta': 'ingestion incident',
+    'incidents.timeline.edge.title': '07:56 edge saturation escalated',
+    'incidents.timeline.edge.copy': 'Operator routed the incident from monitor detail to trace and log evidence.',
+    'incidents.timeline.edge.meta': 'edge incident',
+    'incidents.ownership.checkout.owner': 'checkout-oncall',
+    'incidents.ownership.checkout.queue': 'primary responder',
+    'incidents.ownership.checkout.copy': 'Keeps alert, trace, and deploy context bound to the active mitigation thread.',
+    'incidents.ownership.checkout.meta': '2 active handoffs',
+    'incidents.ownership.observability.owner': 'observability',
+    'incidents.ownership.observability.queue': 'signal validation',
+    'incidents.ownership.observability.copy': 'Validates replay safety and ingestion recovery before closing the incident.',
+    'incidents.ownership.observability.meta': '1 active handoff',
+    'incidents.ownership.edge.owner': 'edge-sre',
+    'incidents.ownership.edge.queue': 'capacity review',
+    'incidents.ownership.edge.copy': 'Tracks post-deploy regressions and the next rollback checkpoint.',
+    'incidents.ownership.edge.meta': 'awaiting rollback note',
+    'incidents.checklist.shell.title': 'Shell ready',
+    'incidents.checklist.shell.copy': 'The incidents route already shares the workbench shell and routing posture.',
+    'incidents.checklist.shell.meta': 'done',
+    'incidents.checklist.adapter.title': 'Adapters next',
+    'incidents.checklist.adapter.copy': 'Incident list, owner state, and response timeline can plug into the current API boundary.',
+    'incidents.checklist.adapter.meta': 'next',
+    'incidents.checklist.drilldown.title': 'Drilldown reserved',
+    'incidents.checklist.drilldown.copy': 'Log, trace, and entity handoff stay on the planned expansion path.',
+    'incidents.checklist.drilldown.meta': 'reserved',
+    'incidents.surface.kicker': 'Incident response desk',
+    'incidents.surface.handoff.logs.title': 'Log evidence',
+    'incidents.surface.handoff.logs.copy': 'Jump into logs when mitigation needs fresh payload evidence.',
+    'incidents.surface.handoff.traces.title': 'Trace evidence',
+    'incidents.surface.handoff.traces.copy': 'Preserve trace-first drilldown for latency and dependency regressions.',
+    'incidents.surface.handoff.overview.title': 'Overview return path',
+    'incidents.surface.handoff.overview.copy': 'Keep incident status visible in the same global response rhythm.',
+    'actions.suggestion.target.entity-id': 'entity {{entityId}}',
+    'actions.suggestion.source.alert': 'alert center',
+    'actions.suggestion.source.unknown': 'unknown source {{source}}',
+    'actions.suggestion.signal.traces': 'traces',
+    'actions.suggestion.signal.unknown': 'unknown signal {{signal}}',
+    'actions.risk.high': 'high risk',
+    'actions.risk.medium': 'medium risk',
+    'actions.risk.low': 'low risk',
+    'actions.run-status.awaiting-approval': 'awaiting approval',
+    'actions.run-status.running': 'running now',
+    'actions.run-status.completed': 'completed',
+    'actions.approval-status.pending': 'pending owner decision',
+    'actions.approval-status.approved': 'approved',
     'passport.lock.error.required': 'Please enter password',
-    'passport.lock.next-step': 'wire unlock validation if needed',
+    'passport.lock.status.interactive': 'Interactive',
+    'passport.lock.next-step': 'Wire unlock validation if needed',
+    'app.lock': 'Unlock',
+    'app.lock.placeholder': 'Enter Any To Unlock',
+    'app.passport.desc': 'Open-source private-deployable enterprise operations observability platform',
+    'app.passport.intro-1': 'Collectors, monitoring templates, entities, metrics, logs, and traces',
+    'app.passport.intro-2': 'Handle alerts and close issues inside HertzBeat',
+    'about.title': 'HertzBeat is an open source observability platform.',
+    'about.help': 'Thanks for using HertzBeat. The following links help you follow releases, report issues, and upgrade safely.',
+    'about.copyright': 'Copyright © {{year}} | Apache HertzBeat™',
+    'about.not-show-next-login': 'Do not show next login',
+    'about.github': 'GitHub',
+    'about.issue': 'Issue',
+    'about.pr': 'PR',
+    'about.discuss': 'Discuss',
+    'about.doc': 'Doc',
+    'about.upgrade': 'Upgrade',
+    'about.star': 'Star',
+    'about.point.1': 'Collect metrics from applications, databases, operating systems, middleware, and network devices without sending data outside your deployment.',
+    'about.point.2': 'Receive OTLP logs with resource, entity, trace, and alert context so operators can continue troubleshooting from the log line.',
+    'about.point.3': 'Handle threshold and external alerts in one place with acknowledge, silence, inhibit, recover, and close actions.',
+    'about.point.4': 'Notify the channels your operations team uses and keep the handling record in HertzBeat.',
+    'about.point.5': 'Use monitoring templates to define collection protocols, metrics, and thresholds for new resource types.',
+    'about.point.6': 'Scale collector clusters across network zones for private deployment, isolated collection, and status pages.',
+    'app.login.login': 'Login',
+    'app.login.message-need-credential': 'Please enter password',
+    'app.login.message-need-identifier': 'Please enter your username',
+    'app.login.tab-login-credentials': 'Sign In HertzBeat',
+    'app.login.password-hide': 'Hide password',
+    'app.login.password-show': 'Show password',
+    'app.login.remember-me': 'Remember me',
+    'app.passport.language-switch': 'Switch language',
     'passport.login.card.entry.title': 'Operations entry',
     'passport.login.card.entry.copy': 'Sign in to continue monitoring resources, entities, telemetry, and alerts.',
     'passport.login.card.shell.title': 'Private deployment',
@@ -405,26 +1299,318 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'passport.login.card.auth.title': 'Secure access',
     'passport.login.card.auth.copy': 'Verify your account before opening alert handling, notification, and settings tools.',
     'passport.login.default-password-warning': 'The default password is still in use. Change it after login.',
+    'passport.login.loading': 'Logging in',
+    'passport.login.error.generic': 'Login failed. Check username or password.',
+    'passport.login.error.with-status': 'Login failed: {{status}}',
+    'settings.console.kicker': 'Settings',
+    'settings.console.title': 'System settings',
+    'settings.console.copy': 'Configure platform system, message, object storage, and token settings from one console.',
+    'settings.console.navigation': 'Settings sections',
+    'settings.console.content': 'Settings content',
+    'settings.surface.title': 'Settings overview',
+    'settings.surface.copy': 'Keep platform configuration entry points in one workbench.',
     'settings.surface.route-contract.title': 'Settings navigation',
     'settings.surface.route-contract.copy':
       'Reach status pages, collectors, labels, plugins, tokens, and system options from one operations console.',
+    'settings.surface.route-contract.meta': 'Route contract',
     'settings.surface.api-contract.title': 'Operations configuration',
     'settings.surface.api-contract.copy':
       'Manage collectors, labels, plugins, tokens, status pages, and notification channels without leaving settings.',
+    'settings.surface.api-contract.meta': 'API contract',
+    'settings.surface.fact.mode-label': 'Mode',
+    'settings.surface.fact.mode-value': 'Settings',
+    'settings.surface.fact.focus-label': 'Focus',
+    'settings.surface.governance.title': 'Platform governance',
+    'settings.surface.governance.copy': 'Show only current settings entries; future domains stay in roadmap docs.',
+    'settings.surface.governance.group.users-permissions': 'Users and permissions',
+    'settings.surface.governance.group.api-access': 'API access',
+    'settings.surface.governance.group.notifications': 'Notification channels',
+    'settings.surface.governance.group.template-marketplace': 'Templates and plugins',
+    'settings.surface.governance.group.mcp-ai-foundation': 'MCP and AI foundation',
+    'settings.surface.governance.copy.implemented-route': 'Already attached to current platform entries; keep using existing pages without adding empty navigation.',
+    'settings.surface.governance.copy.foundation-only': 'Keep the current foundation entry; full governance remains in the roadmap.',
+    'settings.surface.governance.future.title': 'Future domain boundary',
+    'settings.surface.governance.future.copy': '{{domains}} remain roadmap-only planning and are not shown as live app entries. Docs: {{docs}}',
+    'settings.surface.governance.future.meta': 'roadmap only',
+    'settings.surface.governance.future.separator': ', ',
+    'settings.surface.governance.future.security': 'Security',
+    'settings.surface.governance.future.data-observability': 'Data Observability',
+    'settings.surface.governance.future.digital-experience': 'Digital Experience',
+    'settings.surface.governance.future.software-delivery': 'Software Delivery',
+    'settings.surface.governance.future.cloud-cost': 'Cloud Cost',
+    'settings.surface.governance.future.ai-observability': 'AI Observability',
+    'settings.surface.governance.future.developer-integrations': 'Developer Integrations',
+    'setting.labels.loading': 'Loading label management',
+    'setting.labels.copy': 'Manage monitor label names, values, and descriptions in the same compact card grid used by the legacy label workspace.',
+    'setting.labels.table.name': 'Label name',
+    'setting.labels.table.type': 'Label type',
+    'setting.labels.table.description': 'Description',
+    'setting.labels.type.auto': 'Auto label',
+    'setting.labels.type.user': 'User label',
+    'setting.labels.type.preset': 'Preset label',
+    'setting.labels.type.unknown': 'Unknown label type',
+    'setting.labels.metric.auto-page': 'Auto labels in page',
+    'setting.labels.metric.user-page': 'User labels in page',
+    'setting.labels.metric.preset-page': 'Preset labels in page',
+    'setting.labels.empty.title': 'No labels',
+    'setting.labels.empty.copy': 'Create a label first.',
+    'setting.labels.action.copy-aria': 'Copy label {{name}}',
+    'setting.labels.action.edit-aria': 'Edit label {{name}}',
+    'setting.labels.action.delete-aria': 'Delete label {{name}}',
+    'setting.plugins.loading': 'Loading plugin management',
+    'setting.plugins.copy': 'Manage custom plugins, execution types, and enabled state while keeping upload, filtering, bulk delete, and parameter editing in one compact workspace.',
+    'setting.plugins.selected-count': 'Selected {{count}}',
+    'setting.plugins.table.params': 'Params',
+    'setting.plugins.metric.enabled-page': 'Enabled plugins in page',
+    'setting.plugins.metric.disabled-page': 'Disabled plugins in page',
+    'setting.plugins.metric.params-page': 'Plugin params in page',
+    'setting.plugins.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'setting.plugins.pagination.page-size': 'Page size',
+    'setting.plugins.pagination.page': 'Page',
+    'setting.plugins.row.item-fallback': 'Plugin item -',
+    'setting.plugins.row.params': 'Params {{count}}',
+    'setting.plugins.action.params-aria': 'Edit plugin params {{name}}',
+    'setting.plugins.action.delete-aria': 'Delete plugin {{name}}',
+    'setting.collector.loading': 'Loading collector management',
+    'setting.collector.copy': 'Manage registered collector nodes, runtime state, and task distribution while keeping deployment, search, online/offline, and delete actions in one dense operations workspace.',
+    'setting.collector.selected-count': 'Selected {{count}}',
+    'collector.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'collector.pagination.page-size': 'Page size',
+    'collector.pagination.page': 'Page',
+    'collector.confirm.offline': 'Please confirm whether to offline this collector!',
+    'collector.confirm.offline-batch': 'Please confirm whether to offline the collector in batches!',
+    'collector.confirm.online': 'Please confirm whether to online this collector!',
+    'collector.confirm.online-batch': 'Please confirm whether to online collector in batches!',
+    'collector.notify.no-select-offline': 'No items selected for offline collector!',
+    'collector.notify.no-select-online': 'No items selected for online collector!',
+    'collector.deploy': 'Deploy Collector',
+    'collector.deploy.close': 'Close, collector identity token saved',
+    'collector.deploy.docker': 'Deploy via Docker',
+    'collector.deploy.docker.help': '# Run this command in a Docker environment',
+    'collector.deploy.docker.help.1': '# docker run -d : Start a collector container in the background with Docker',
+    'collector.deploy.docker.help.2': "# -e IDENTITY=xxx : Set the collector's identity token",
+    'collector.deploy.docker.help.3': '# -e MANAGER_HOST=127.0.0.1 : Set the HertzBeat server address the collector connects to',
+    'collector.deploy.docker.help.4': '# -e MODE=public : Set the run mode: public collector cluster or private edge collector.',
+    'collector.deploy.docker.help.5': '# --name hertzbeat-collector : Name the container hertzbeat-collector',
+    'collector.deploy.docker.help.6': '# apache/hertzbeat-collector : Official collector image. Use quay.io/tancloud/hertzbeat-collector if the pull times out',
+    'collector.deploy.identity': 'Collector identity token (IDENTITY)',
+    'collector.deploy.identity.tip': 'This identity token is unique to the current collector. Keep it safe.',
+    'collector.deploy.ok': 'Generate identity token and deployment command',
+    'collector.deploy.package': 'Deploy via installation package',
+    'collector.deploy.package.github': 'Download package from GitHub',
+    'collector.deploy.package.help': '# Download the installation package for this operating system: hertzbeat-collector-xx.tar.gz',
+    'collector.deploy.package.help.1': '# Extract it and update hertzbeat-collector/config/application.yml',
+    'collector.deploy.package.help.2': '# Replace IDENTITY, MANAGER_HOST, and MODE in the file as shown below.',
+    'collector.deploy.package.help.3': '# Run bin/startup.sh or startup.bat (Windows) to start the collector.',
+    'collector.name.placeholder': 'Please config the unique name of the collector',
+    'common.button.copy.tip': 'Click to copy',
+    'common.copy.button': 'Copy',
+    'setting.define.loading': 'Loading definition center',
+    'setting.define.title': 'Define',
+    'setting.define.subtitle':
+      'Manage monitor template YAML for collection protocols, parameters, and metric fields in one dense operations workspace.',
+    'setting.define.action.new': 'New type',
+    'setting.define.action.save-apply': 'Save and apply',
+    'setting.define.action.delete': 'Delete {{app}}',
+    'define.save-apply.confirm': 'Please confirm whether to save and apply this monitor type definition. This will affect your monitors.',
+    'define.delete.confirm': 'Please confirm whether to delete {{app}} monitor type. This monitor type cannot be added after deletion.',
+    'define.hide-true.confirm': 'Confirm whether to display this menu?',
+    'define.hide-false.confirm': 'Confirm whether to hide this menu?',
+    'define.save-apply.no-code': 'The monitoring type definition content cannot be empty.',
+    'setting.define.action.hide': 'Hide',
+    'setting.define.action.hide-aria': 'Hide template {{app}}',
+    'setting.define.action.show': 'Show',
+    'setting.define.action.show-aria': 'Show template {{app}}',
+    'setting.define.save.pending': 'Applying definition...',
+    'setting.define.skeleton.name': 'hertzbeat-baseline-alert',
+    'setting.define.skeleton.summary': 'HertzBeat baseline alert definition seed',
+    'setting.define.current-yaml.fallback': 'current-definition',
+    'setting.define.current-delete.fallback': 'current definition',
+    'setting.define.item.fallback': 'Untitled definition',
+    'setting.define.empty.title': 'No templates',
+    'setting.define.empty.copy': 'Create a monitor template YAML first.',
+    'setting.define.empty.search-title': 'No matching templates',
+    'setting.define.empty.search-copy': 'Adjust the search keyword and try again.',
+    'setting.define.empty-selected.title': 'No definition selected',
+    'setting.define.empty-selected.copy': 'Select a definition from the list.',
+    'setting.define.preview.action': 'Preview query',
+    'setting.define.preview.title': 'Definition preview',
+    'setting.define.preview.success': 'Preview completed',
+    'setting.define.preview.failed': 'Preview failed',
+    'setting.define.save.success': 'Definition saved',
+    'setting.define.save.failed': 'Failed to save definition',
+    'setting.define.datasource.title': 'Datasource status',
+    'setting.define.fact.datasource': 'Datasource',
+    'setting.define.fact.selected-template': 'Current template',
+    'setting.define.fact.hidden': 'Hidden templates',
+    'setting.define.new-template': 'New template draft',
+    'setting.define.template.visible': 'Visible',
+    'setting.define.template.hidden': 'Hidden',
+    'setting.define.summary.yaml-lines': '{{count}} YAML lines',
+    'setting.define.summary.mode.new': 'New',
+    'setting.define.summary.mode.edit': 'Edit',
+    'setting.define.diff.original': 'Original YAML',
+    'setting.define.diff.current': 'Current edit YAML',
+    'setting.define.row.meta': 'Period {{period}}s · updated {{time}}',
+    'alert.setting.type.realtime.metric': 'Realtime metric',
+    'alert.setting.type.periodic.metric': 'Periodic metric',
+    'alert.setting.type.realtime.log': 'Realtime log',
+    'alert.setting.type.periodic.log': 'Periodic log',
+    'alert.setting.type.periodic.trace': 'Periodic trace',
+    'setting.status.loading': 'Loading status settings',
     'setting.status.validation.name': 'Org name is required',
     'setting.status.validation.description': 'Org description is required',
     'setting.status.validation.component-name': 'Component name is required',
     'setting.status.validation.component-description': 'Component description is required',
+    'setting.status.title': 'Status page settings',
+    'setting.status.org.title': 'Org profile',
+    'setting.status.org.copy': 'Configure the public org information shown on the status page.',
+    'setting.status.org.fallback': 'Unnamed status org',
+    'setting.status.org.state': 'Org state',
+    'setting.status.org.feedback': 'Feedback',
+    'setting.status.components.title': 'Components',
+    'setting.status.incidents.title': 'Incidents',
+    'setting.status.delete.component.title': 'Delete component',
+    'setting.status.delete.component.copy':
+      'After deletion, this component is removed from the public status page and related incidents need component binding review.',
+    'setting.status.delete.incident.title': 'Delete incident',
+    'setting.status.delete.incident.copy':
+      'After deletion, this incident is removed from the public status page and historical updates are no longer shown to visitors.',
+    'setting.status.delete.confirm': 'Confirm delete',
     'setting.status.incidents.state': 'state',
     'setting.status.incidents.components': 'components',
+    'setting.status.component.method.placeholder': '0 / 1',
+    'setting.status.component.state.placeholder': '0 / 1 / 2',
+    'setting.status.component.labels.placeholder': 'env:prod, app:api',
+    'setting.status.component.description.placeholder': 'Public status component description',
+    'setting.status.incident.state.placeholder': '0 / 1 / 2 / 3',
+    'setting.status.incident.components.placeholder': '1, 2',
+    'monitors.delete.title': 'Delete monitors',
+    'monitors.delete.copy':
+      'After deletion, the selected monitors are removed from the monitor catalog, and related entity bindings plus alert investigation entries need to be reviewed.',
+    'monitors.delete.confirm': 'Confirm delete',
+    'monitors.delete.selected': '{{count}} monitors selected',
+    'monitors.selected-count': '{{count}} monitors selected',
     'setting.settings.menu.copy': 'System, storage, notification, and token settings',
+    'setting.settings.menu.open-action': 'Open {{title}}',
     'setting.settings.config.title': 'System config',
     'setting.settings.server.title': 'Message server',
+    'settings.server': 'Message server settings',
+    'settings.server.email': 'Email server',
+    'settings.server.sms': 'SMS settings',
+    'settings.server.email.setting': 'Configure email server',
+    'settings.server.sms.setting': 'Configure SMS parameters',
+    'settings.server.summary.configure-action': 'Configure {{title}}',
+    'alert.notice.sender.mail.host': 'Email server address',
+    'alert.notice.sender.mail.port': 'Email port',
+    'alert.notice.sender.mail.username': 'Email account',
+    'alert.notice.sender.mail.password': 'Email password',
+    'alert.notice.sender.mail.ssl': 'Enable SSL',
+    'alert.notice.sender.mail.starttls': 'Enable STARTTLS',
+    'alert.notice.sender.mail.enable': 'Enable email configuration',
+    'alert.notice.sender.sms.type': 'SMS type',
+    'alert.notice.sender.sms.type.tencent': 'Tencent SMS',
+    'alert.notice.sender.sms.type.alibaba': 'Alibaba SMS',
+    'alert.notice.sender.sms.type.unisms': 'UniSMS',
+    'alert.notice.sender.sms.type.smslocal': 'Smslocal',
+    'alert.notice.sender.sms.type.aws': 'AWS SMS',
+    'alert.notice.sender.sms.type.twilio': 'Twilio Sms',
+    'alert.notice.sender.sms.tencent.secretId': 'Tencent SMS SecretId',
+    'alert.notice.sender.sms.tencent.secretKey': 'Tencent SMS SecretKey',
+    'alert.notice.sender.sms.tencent.signName': 'Tencent SMS SignName',
+    'alert.notice.sender.sms.tencent.appId': 'Tencent SMS AppId',
+    'alert.notice.sender.sms.tencent.templateId': 'Tencent SMS TemplateId',
+    'alert.notice.sender.sms.alibaba.accessKeyId': 'Alibaba SMS AccessKeyId',
+    'alert.notice.sender.sms.alibaba.accessKeySecret': 'Alibaba SMS AccessKeySecret',
+    'alert.notice.sender.sms.alibaba.signName': 'Alibaba SMS SignName',
+    'alert.notice.sender.sms.alibaba.templateCode': 'Alibaba SMS TemplateCode',
+    'alert.notice.sender.sms.unisms.accessKeyId': 'UniSMS AccessKeyId',
+    'alert.notice.sender.sms.unisms.authMode': 'UniSMS auth mode',
+    'alert.notice.sender.sms.unisms.accessKeySecret': 'UniSMS AccessKeySecret',
+    'alert.notice.sender.sms.unisms.signature': 'UniSMS signature',
+    'alert.notice.sender.sms.unisms.templateId': 'UniSMS TemplateId',
+    'alert.notice.sender.sms.unisms.authMode.hmac': 'HMAC',
+    'alert.notice.sender.sms.unisms.authMode.simple': 'Simple',
+    'alert.notice.sender.sms.smslocal.apiKey': 'Smslocal API key',
+    'alert.notice.sender.sms.aws.accessKeyId': 'AWS access key ID',
+    'alert.notice.sender.sms.aws.accessKeySecret': 'AWS access key secret',
+    'alert.notice.sender.sms.aws.region': 'AWS region',
+    'alert.notice.sender.sms.twilio.accountSid': 'Twilio account SID',
+    'alert.notice.sender.sms.twilio.authToken': 'Twilio auth token',
+    'alert.notice.sender.sms.twilio.twilioPhoneNumber': 'Twilio phone number',
     'setting.settings.object-store.title': 'Object storage',
+    'setting.settings.object-store.loading': 'Loading object storage settings.',
+    'setting.settings.server.loading': 'Loading message server settings.',
+    'settings.object-store': 'Object storage',
+    'settings.object-store.type': 'Storage provider',
+    'settings.object-store.type.database': 'Local database (default)',
+    'settings.object-store.type.file': 'Local file',
+    'settings.object-store.type.obs': 'Huawei Cloud OBS',
+    'settings.object-store.obs.accessKey': 'Access key',
+    'settings.object-store.obs.secretKey': 'Secret key',
+    'settings.object-store.obs.bucketName': 'Bucket',
+    'settings.object-store.obs.endpoint': 'Endpoint',
+    'settings.object-store.obs.savePath': 'Save path',
+    'settings.object-store.obs.accessKey.placeholder': 'Access key ID',
+    'settings.object-store.obs.secretKey.placeholder': 'Access key secret',
+    'settings.object-store.obs.bucketName.placeholder': 'Bucket name',
+    'settings.object-store.obs.endpoint.placeholder': 'OBS endpoint without bucket name',
+    'settings.object-store.obs.savePath.placeholder': 'Backup save path, defaults to hertzbeat',
+    'settings.system-config': 'System config',
+    'settings.system-config.locale': 'System language',
+    'settings.system-config.timezone': 'System timezone',
+    'settings.system-config.theme': 'System theme',
+    'settings.system-config.locale.en_US': 'English(en_US)',
+    'settings.system-config.locale.zh_CN': 'Simplified Chinese(zh_CN)',
+    'settings.system-config.locale.zh_TW': 'Traditional Chinese(zh_TW)',
+    'settings.system-config.locale.ja-JP': 'Japanese(ja_JP)',
+    'settings.system-config.locale.pt_BR': 'Portuguese(pt_BR)',
+    'settings.system-config.theme.default': 'Default theme',
+    'settings.system-config.theme.dark': 'Dark theme',
+    'settings.system-config.theme.compact': 'Compact theme',
+    'setting.settings.config.loading': 'Loading system config.',
+    'settings.system-config.ok': 'Confirm update',
+    'setting.object-store.fact.type': 'Type',
+    'setting.object-store.fact.bucket': 'Bucket',
+    'setting.object-store.fact.endpoint': 'Endpoint',
+    'settings.token.delete-action': 'Delete {{name}}',
     'setting.token.fact.total': 'Total',
     'setting.token.fact.active': 'Active',
     'setting.token.fact.expired': 'Expired',
+    'setting.token.metric.total': 'total tokens',
+    'setting.token.metric.active': 'active tokens',
+    'setting.token.metric.expired': 'expired tokens',
+    'setting.token.row.creator': 'creator {{creator}}',
+    'setting.token.row.state.active': 'active',
+    'setting.token.row.state.expired': 'expired',
     'setting.token.title': 'Token management',
+    'setting.token.loading': 'Loading token management.',
+    'settings.token': 'Token management',
+    'settings.token.console.kicker': 'API keys',
+    'settings.token.console.title': 'Manage telemetry and automation tokens',
+    'settings.token.console.copy': 'Review token state, recent use, and revoke actions from one page.',
+    'settings.token.console.result.total': 'Total tokens',
+    'settings.token.console.result.active': 'Active tokens',
+    'settings.token.console.result.expired': 'Expired tokens',
+    'settings.token.generate': 'Generate token',
+    'settings.token.generate-title': 'Generate token',
+    'settings.token.generated-title': 'Token generated',
+    'settings.token.generate-fail': 'Generate token failed',
+    'settings.token.name-placeholder': 'Enter a token name',
+    'settings.token.notice': 'Copy this token now. It will not be shown again after you close this dialog.',
+    'settings.token.delete-confirm': 'Confirm delete token',
+    'settings.token.delete-confirm-content': 'All integrations using this token will lose access. Are you sure you want to delete "{{name}}"?',
+    'settings.token.load-fail-title': 'Token list is temporarily unavailable',
+    'settings.token.load-fail': 'The token list cannot be loaded right now. Please try again later or refresh the page.',
+    'settings.token.desc': 'Manage long-lived tokens for API integrations; create or revoke them at any time.',
+    'settings.token.name': 'Token name',
+    'settings.token.value': 'Token value',
+    'settings.token.creator': 'Creator',
+    'settings.token.create-time': 'Created at',
+    'settings.token.expire-time': 'Expires at',
+    'settings.token.expire.never': 'Never expires',
+    'settings.token.last-used': 'Last used',
+    'settings.token.empty': 'No tokens yet',
     'setting.token.empty.title': 'No tokens yet',
     'setting.token.empty.copy': 'Generate a token first.',
     'setting.token.expiration.never': 'Never expires',
@@ -433,6 +1619,13 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'setting.token.expiration.90d': '90 days',
     'setting.token.expiration.180d': '180 days',
     'setting.token.expiration.365d': '365 days',
+    'status.loading': 'Loading status page',
+    'status.page': 'Status Page',
+    'status.org.title': 'Organization',
+    'status.org.copy': 'Publish your public org profile to explain who owns this status page.',
+    'status.org.empty': 'Status organization',
+    'status.components.title': 'Components',
+    'status.incidents.title': 'Incidents',
     'status.metric.components': 'components',
     'status.metric.operational': 'operational',
     'status.metric.incidents': 'incidents',
@@ -443,19 +1636,160 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'status.tone.down': 'down',
     'status.incident.status': 'status',
     'status.org.meta': 'public org',
+    'status.component.state.0': 'Normal',
+    'status.component.state.1': 'Abnormal',
+    'status.component.state.2': 'Unknown',
+    'status.incident.history': 'Incident History',
+    'status.incident.state.0': 'Investigating',
+    'status.incident.state.1': 'Identified',
+    'status.incident.state.2': 'Monitoring',
+    'status.incident.state.3': 'Resolved',
+    'status.incident.message.tip.0': 'We are investigating this incident, please check back later for updates.',
+    'status.incident.message.tip.1': 'We have confirmed the cause of the failure and are processing it.',
+    'status.incident.message.tip.2': 'We are monitoring this event and observing the processing effect',
+    'status.incident.message.tip.3': 'This incident has been resolved, thanks for understanding and support.',
+    'status.public.org.state.0': 'All Systems Operational',
+    'status.public.org.state.1': 'Some Systems Abnormal',
+    'status.public.org.state.2': 'All Systems Abnormal',
     'status.public.history': 'History',
     'status.public.home': 'Home',
+    'status.public.feedback': 'Feedback',
     'status.public.updated': 'Updated',
     'status.public.year': 'Year',
+    'status.public.uptime': 'Uptime',
     'status.public.component-history': 'Component history',
     'status.public.incident-range': 'Incident range',
+    'status.public.incidents.load-failed': 'Failed to load public incidents',
+    'status.public.today': 'Today',
+    'status.public.30-day': '30 days',
+    'status.public.power-by': 'Powered by HertzBeat',
+    'status.public.to-incident': 'Incident History',
+    'status.public.to-component': 'Status Page',
+    'status.incident.public.start-at': 'Start At',
+    'status.incident.public.update-at': 'Update At',
     'status.components.view': 'Component health view',
+    'trace.manage.loading': 'Loading trace workbench',
     'trace.manage.stat.error-ratio': 'Error ratio',
     'trace.manage.stat.activity': 'Activity',
     'trace.manage.stat.list-count': 'List count',
+    'trace.center.console.title': 'Traces Workbench',
+    'trace.manage.workspace': 'Traces Workbench',
     'trace.manage.payloads.title': 'Raw payloads',
+    'trace.manage.empty-selected-span.title': 'Select a span',
+    'trace.manage.empty-selected-span.copy': 'Choose a span on the left to inspect service, status, and event evidence.',
     'trace.manage.selected-span.service-namespace': 'Service and namespace',
     'trace.manage.trace-state-empty': 'Trace state unavailable',
+    'trace.manage.event.fallback': 'Unnamed event',
+    'trace.manage.event.attributes.empty': 'No attributes',
+    'trace.manage.event.attributes.more': '{{count}} more',
+    'trace.manage.event.attributes.object': 'Object attributes',
+    'trace.manage.link.fallback': 'Related trace',
+    'trace.manage.link.meta': 'Related trace',
+    'trace.manage.attribution-diagnostics.aria':
+      'Attribution diagnostics hertzbeat.entity_id hertzbeat.entity_name hertzbeat.workspace_id hertzbeat.collector hertzbeat.template',
+    'trace.manage.attribution-diagnostics.title': 'Attribution diagnostics',
+    'trace.manage.attribution-diagnostics.present': 'Present',
+    'trace.manage.attribution-diagnostics.missing': 'Missing',
+    'trace.manage.attribution-diagnostics.entity-id.present': 'Can open entity details',
+    'trace.manage.attribution-diagnostics.entity-id.missing': 'Missing entity ID; entity detail remains disabled',
+    'trace.manage.attribution-diagnostics.entity-name.present': 'Used for entity display name',
+    'trace.manage.attribution-diagnostics.entity-name.missing': 'Missing entity name; service name assists lookup',
+    'trace.manage.attribution-diagnostics.workspace-id.present': 'Workspace ownership',
+    'trace.manage.attribution-diagnostics.workspace-id.missing': 'Missing workspace field; current deployment context applies',
+    'trace.manage.attribution-diagnostics.collector.present': 'Collector source',
+    'trace.manage.attribution-diagnostics.collector.missing': 'Missing collector field; inspect trace service context',
+    'trace.manage.attribution-diagnostics.template.present': 'Monitoring template ownership',
+    'trace.manage.attribution-diagnostics.template.missing': 'Missing template field; resolve from entity or service',
+    'trace.manage.route.fallback.filtered-trace': 'Filtered trace',
+    'trace.manage.route.fallback.waiting-query': 'Waiting for query',
+    'trace.manage.route.fallback.unselected': 'Not selected',
+    'trace.manage.route.kicker': 'Observability',
+    'trace.manage.route.title': 'Traces Workbench',
+    'trace.manage.route.subtitle': 'Filter traces by service, trace ID, span ID, and error state, then inspect trend, list, and detail evidence.',
+    'trace.manage.route.action.return-source': 'Return to source',
+    'trace.manage.route.action.intake': 'Back to OTLP intake',
+    'trace.manage.route.action.collectors': 'Collectors',
+    'trace.manage.route.action.entity': 'Entity detail',
+    'trace.manage.route.action.alerts': 'Alert handling',
+    'trace.manage.route.action.templates': 'Monitor templates',
+    'trace.manage.route.action.catalog': 'Entity catalog',
+    'trace.manage.route.query.service': 'Service name',
+    'trace.manage.route.query.trace-id': 'Trace ID',
+    'trace.manage.route.query.span-id': 'Span ID',
+    'trace.manage.route.query.status': 'Trace status',
+    'trace.manage.route.query.status.all': 'All traces',
+    'trace.manage.route.query.status.error': 'Error traces',
+    'trace.manage.route.query.run': 'Run query',
+    'trace.manage.route.query.reset': 'Reset',
+    'trace.manage.route.stat.total': 'Total traces',
+    'trace.manage.route.stat.errors': 'Error traces',
+    'trace.manage.route.stat.list-count': 'List rows',
+    'trace.manage.route.stat.latest': 'Latest time',
+    'trace.manage.route.chart.title': 'Trend band',
+    'trace.manage.route.chart.latest-window': 'Latest 10 traces',
+    'trace.manage.route.table.title': 'Recent traces',
+    'trace.manage.route.table.subtitle': 'Current query',
+    'trace.manage.route.table.count': '{{count}} rows',
+    'trace.manage.route.table.header.start': 'Start time',
+    'trace.manage.route.table.header.service': 'Service',
+    'trace.manage.route.table.header.root-span': 'Root span',
+    'trace.manage.route.table.header.duration': 'Duration',
+    'trace.manage.route.table.header.status': 'Status',
+    'trace.manage.route.table.header.trace-id': 'Trace ID',
+    'trace.manage.route.empty.title': 'No traces',
+    'trace.manage.route.empty.copy': 'Check time range, entity attribution, collectors, and monitor templates before viewing traces.',
+    'trace.manage.route.detail.title': 'Detail panel',
+    'trace.manage.route.detail.service': 'Service',
+    'trace.manage.route.detail.namespace': 'Namespace',
+    'trace.manage.route.detail.root-span': 'Root span',
+    'trace.manage.route.detail.duration': 'Duration',
+    'trace.manage.route.evidence.aria': 'Trace evidence Trace ID Trace status Start time Latest observed',
+    'trace.manage.route.evidence.title': 'Trace evidence',
+    'trace.manage.route.evidence.current': 'Current selection',
+    'trace.manage.route.evidence.trace-id': 'Trace ID',
+    'trace.manage.route.evidence.trace-status': 'Trace status',
+    'trace.manage.route.evidence.error-only': 'Error traces only',
+    'trace.manage.route.evidence.current-filter': 'Current filter',
+    'trace.manage.route.evidence.start-time': 'Start time',
+    'trace.manage.route.evidence.root-span-start': 'Root span start',
+    'trace.manage.route.evidence.latest-observed': 'Latest observed',
+    'trace.manage.route.evidence.latest-workbench-trace': 'Latest workbench trace',
+    'trace.manage.route.entity-context.aria': 'Entity context Current entity Monitor instance Current service Trace context Current environment Time range Collection source',
+    'trace.manage.route.entity-context.title': 'Entity context',
+    'trace.manage.route.handoff.alert-hint': 'View related alerts using the current entity, service, and trace context.',
+    'trace.manage.route.handoff.signal-hint': 'The current traceId, spanId, and service context carry into logs and metrics workbenches.',
+    'trace.manage.drawer.event-prefix': 'Event',
+    'trace.manage.drawer.link-prefix': 'Link',
+    'trace.manage.drawer.event-fact.title': 'Span event',
+    'trace.manage.drawer.event-fact.fallback': 'Unnamed event',
+    'trace.manage.drawer.event-fact.meta': 'A timestamp on the current span, not a new span',
+    'trace.manage.drawer.event-fact.span': 'Owner span',
+    'trace.manage.drawer.event-fact.position': 'Event position',
+    'trace.manage.drawer.event-fact.timeline': 'Relative to trace timeline',
+    'trace.manage.drawer.event-fact.attributes': 'Event attributes',
+    'trace.manage.drawer.event-fact.no-attributes': 'No attributes',
+    'trace.manage.drawer.kicker': 'Trace detail',
+    'trace.manage.drawer.title-fallback': 'Trace waterfall',
+    'trace.manage.drawer.action.logs': 'View logs',
+    'trace.manage.drawer.action.metrics': 'View metrics',
+    'trace.manage.drawer.action.entity': 'Entity detail',
+    'trace.manage.drawer.loading.title': 'Loading trace detail',
+    'trace.manage.drawer.loading.copy': 'Loading spans, events, links, and related logs for this trace.',
+    'trace.manage.drawer.error.title': 'Failed to load trace detail',
+    'trace.manage.drawer.error.copy': 'Trace detail is temporarily unavailable. Try again or adjust the query.',
+    'trace.manage.drawer.span-count': '{{count}} spans',
+    'trace.manage.drawer.stat.current-span': 'Current span',
+    'trace.manage.drawer.stat.error-spans': 'Error spans',
+    'trace.manage.drawer.stat.events': 'Events',
+    'trace.manage.drawer.stat.links': 'Links',
+    'trace.manage.drawer.waterfall.span': 'Span',
+    'trace.manage.drawer.waterfall.duration': 'Duration',
+    'trace.manage.drawer.waterfall.timeline': 'Timeline',
+    'trace.manage.drawer.event-detail.title': 'Event detail',
+    'trace.manage.drawer.event-detail.copy': 'A timestamp on the current span, not a new span',
+    'trace.manage.drawer.event-detail.action.span': 'View span',
+    'trace.manage.drawer.empty.title': 'Select a trace',
+    'trace.manage.drawer.empty.copy': 'Open a trace from the list to inspect the waterfall and span evidence here.',
     'otlp.protocol.http': 'HTTP',
     'otlp.protocol.grpc': 'gRPC',
     'otlp.guide.auth-header': 'Auth header',
@@ -463,6 +1797,7 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'otlp.loading': 'Loading OTLP intake',
     'otlp.kicker': 'OTLP',
     'otlp.title': 'OTLP Intake Center',
+    'ingestion.otlp.metrics.title': 'Metrics Workbench',
     'otlp.subtitle': 'Use this page to find OTLP endpoints for metrics, logs, and traces, and check whether data is arriving.',
     'otlp.fact.active-signals': 'Connected signals',
     'otlp.fact.recent-services': 'Recent services',
@@ -494,6 +1829,7 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.section.console.title': 'Metrics workbench',
     'otlp.metrics.section.console.copy': 'Inspect OTLP metrics with the same operations context',
     'otlp.metrics.section.context.title': 'Operations context',
+    'otlp.metrics.route.action.return-source': 'Return to source',
     'otlp.metrics.empty': 'No OTLP metrics matched the current query and time range.',
     'otlp.metrics.query-ready': 'metric context ready',
     'otlp.metrics.query-context': 'operations context',
@@ -501,22 +1837,154 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.series.unknown-service': 'Unknown service',
     'otlp.metrics.series.empty': 'No metric series are available in the current result set.',
     'otlp.metrics.series.empty.copy': 'Try widening the time range or changing the query.',
+    'otlp.metrics.stats.total-series': 'Metric series',
+    'otlp.metrics.stats.non-empty-series': 'Series with data',
+    'otlp.metrics.stats.datasource': 'Datasource',
+    'otlp.metrics.stats.latest-observed': 'Latest observed',
+    'otlp.metrics.stats.series-total': 'Total series',
+    'otlp.metrics.stats.intake-state': 'Intake state',
+    'otlp.metrics.explorer.chart-label': '{{count}} series with data',
+    'otlp.metrics.explorer.empty-title': 'No metric series',
+    'otlp.metrics.explorer.no-metrics-title': 'Confirm the time range, entity attribution, collector, and monitor template before viewing metrics.',
+    'otlp.metrics.explorer.waiting-ingest': 'Waiting for OTLP metrics',
+    'otlp.metrics.explorer.series-count': '{{count}} series',
+    'otlp.metrics.context.current-metric': 'Current metric',
+    'otlp.metrics.context.handoff-destination': 'Troubleshooting handoff',
+    'otlp.metrics.context.handoff-copy': 'Related entity, logs, traces, and alert handling.',
+    'otlp.metrics.context.handoff-meta': 'Continue by service, template, and time range',
+    'otlp.metrics.context.current-service': 'Current service',
+    'otlp.metrics.context.time-range': 'Time range',
+    'otlp.metrics.series.entity-id': 'entityId {{entityId}}',
+    'otlp.metrics.series.entity-missing': 'Waiting for entity attribution',
+    'otlp.metrics.series.context.metric-name': 'Metric name',
+    'otlp.metrics.series.context.selected-series': 'Current selected series',
+    'otlp.metrics.series.context.entity': 'Related entity',
+    'otlp.metrics.series.context.service': 'Current service',
+    'otlp.metrics.series.context.service-context': 'Service context',
+    'otlp.metrics.series.context.template': 'Monitor template',
+    'otlp.metrics.series.context.collector': 'Collector {{collector}}',
+    'otlp.metrics.series.context.monitor-template': 'Monitor template',
+    'otlp.metrics.series.context.environment': 'Current environment',
+    'otlp.metrics.series.context.deployment-environment': 'Deployment environment',
+    'otlp.metrics.evidence.samples': 'Samples',
+    'otlp.metrics.evidence.empty-skipped': '{{count}} empty values skipped',
+    'otlp.metrics.evidence.real-samples': 'Real samples',
+    'otlp.metrics.evidence.latest-value': 'Latest value',
+    'otlp.metrics.evidence.waiting-latest': 'Waiting for latest sample',
+    'otlp.metrics.evidence.value-range': 'Value range',
+    'otlp.metrics.evidence.average': 'Average {{average}}',
+    'otlp.metrics.evidence.waiting-real-samples': 'Waiting for real samples',
+    'otlp.metrics.evidence.sample-window': 'Sample window',
+    'otlp.metrics.evidence.real-sample-time': 'Real sample time',
+    'otlp.metrics.evidence.waiting-sample-time': 'Waiting for sample time',
+    'otlp.metrics.evidence.linked-trace': 'Related trace',
+    'otlp.metrics.evidence.missing-span': 'span_id not provided',
+    'otlp.metrics.evidence.missing-trace-span': 'Missing trace_id/span_id',
+    'otlp.metrics.handoff.logs': 'Historical logs',
+    'otlp.metrics.handoff.logs-by-trace': 'View by trace',
+    'otlp.metrics.handoff.logs-by-service': 'View by service',
+    'otlp.metrics.handoff.logs-current-span': 'Logs will focus on the current span',
+    'otlp.metrics.handoff.logs-current-trace': 'Logs will focus on the current trace',
+    'otlp.metrics.handoff.logs-service-filter': 'Filter by service when trace ID is missing',
+    'otlp.metrics.handoff.logs-waiting-context': 'Waiting for service or trace context',
+    'otlp.metrics.handoff.traces': 'Trace waterfall',
+    'otlp.metrics.handoff.trace-open': 'Open',
+    'otlp.metrics.handoff.trace-waiting-id': 'Waiting for trace ID',
+    'otlp.metrics.handoff.trace-full-current-span': 'Open the full trace and keep the current span',
+    'otlp.metrics.handoff.trace-full': 'Open the full trace',
+    'otlp.metrics.handoff.trace-missing-id': 'Metric has no trace ID, so trace navigation is unavailable',
+    'otlp.metrics.handoff.alerts': 'Alert handling',
+    'otlp.metrics.handoff.alerts-by-entity': 'Handle by entity',
+    'otlp.metrics.handoff.alerts-by-service': 'Handle by service',
+    'otlp.metrics.handoff.alerts-by-entity-meta': 'Open alerts with entity, service, and metric context',
+    'otlp.metrics.handoff.alerts-by-service-meta': 'Open alerts by service when entity ID is missing',
+    'otlp.metrics.attribution.entity-id.present': 'Entity detail can be opened',
+    'otlp.metrics.attribution.entity-id.missing': 'Missing entity ID, so entity detail stays disabled',
+    'otlp.metrics.attribution.entity-name': 'Used to display the entity name',
+    'otlp.metrics.attribution.workspace-id': 'Use current deployment context when workspace is missing',
+    'otlp.metrics.attribution.collector': 'Collector source',
+    'otlp.metrics.attribution.template': 'Monitor template attribution',
     'actions.focus': 'Automation inventory and execution guardrails',
     'actions.subtitle': 'Automation catalog, execution history, and approval-aware operations all stay in one context.',
     'actions.summary': 'The actions entry page carries automation catalog, execution history, and approval semantics without requiring a page redesign for future data.',
+    'actions.tag.catalog': 'automation catalog',
+    'actions.tag.risk-aware': 'risk-aware actions',
+    'actions.tag.approval-flow': 'approval flow',
+    'actions.metric.catalog-entries': 'Catalog entries',
+    'actions.metric.pending-approvals': 'Pending approvals',
+    'actions.metric.recent-runs': 'Recent runs',
+    'actions.metric.high-risk-actions': 'High-risk actions',
+    'actions.catalog.restart.category': 'runtime recovery',
+    'actions.catalog.restart.scope': 'service checkout / prod-ap',
+    'actions.catalog.restart.owner': 'Platform operations',
+    'actions.catalog.restart.last-run': '12m ago',
+    'actions.catalog.restart.posture': 'Requires approval and rollback note',
+    'actions.catalog.replay.name': 'Replay delayed OTLP metrics',
+    'actions.catalog.replay.category': 'signal remediation',
+    'actions.catalog.replay.scope': 'otlp gateway / metrics',
+    'actions.catalog.replay.owner': 'Observability',
+    'actions.catalog.replay.last-run': '38m ago',
+    'actions.catalog.replay.posture': 'Safe within current replay window',
+    'actions.catalog.mute.category': 'alert hygiene',
+    'actions.catalog.mute.scope': 'entity edge-proxy',
+    'actions.catalog.mute.owner': 'SRE on-call',
+    'actions.catalog.mute.last-run': '2h ago',
+    'actions.catalog.mute.posture': 'Auto-expires after 30 minutes',
+    'actions.run.restart.name': 'Restart checkout deployment',
+    'actions.run.restart.target': 'checkout / prod-ap',
+    'actions.run.restart.actor': 'li.na',
+    'actions.run.restart.duration': 'pending approval',
+    'actions.run.replay.name': 'Replay delayed OTLP metrics',
+    'actions.run.replay.target': 'gateway metrics shard-3',
+    'actions.run.replay.actor': 'automation',
+    'actions.run.replay.duration': '06m 12s',
+    'actions.run.mute.name': 'Mute edge saturation alerts',
+    'actions.run.mute.target': 'edge-proxy / cn-hz',
+    'actions.run.mute.actor': 'lin.qi',
+    'actions.run.mute.duration': '01m 42s',
+    'actions.approval.restart.summary': 'Restart production checkout pods after saturation alert burst',
+    'actions.approval.restart.owner': 'checkout-oncall',
+    'actions.approval.restart.evidence': 'Alert storm and trace latency regression are attached.',
+    'actions.approval.replay.summary': 'Replay OTLP metrics for delayed collector queue',
+    'actions.approval.replay.owner': 'obs-admin',
+    'actions.approval.replay.evidence': 'Collector backlog has stabilized and replay window is open.',
+    'actions.surface.kicker': 'Action control plane',
+    'actions.surface.handoff.entity.title': 'Entity context handoff',
+    'actions.surface.handoff.entity.copy': 'Open entity inventory before expanding action target scope.',
+    'actions.surface.handoff.monitor.title': 'Monitor posture handoff',
+    'actions.surface.handoff.monitor.copy': 'Validate signal health before issuing recovery actions.',
+    'actions.surface.handoff.approval.title': 'Approval evidence handoff',
+    'actions.surface.handoff.approval.copy': 'Keep alerts, traces, and rollback notes together in the same operator flow.',
     'actions.checklist.entry.title': 'Entry shell ready',
     'actions.checklist.entry.copy': 'The action catalog already uses the shared workbench language.',
+    'actions.checklist.entry.meta': 'done',
     'actions.checklist.adapters.title': 'Execution adapters',
     'actions.checklist.adapters.copy': 'Action list, run result, and approval flow keep the current integration boundary.',
+    'actions.checklist.adapters.meta': 'next',
     'actions.checklist.context.title': 'Context continuity',
     'actions.checklist.context.copy': 'Jump to the actions page directly from entity, alert, or topology context.',
+    'actions.checklist.context.meta': 'reserved',
     'actions.lanes.catalog.title': 'Automation catalog',
     'actions.lanes.catalog.copy': 'Treat action definitions, target scope, and risk level as one browsable object set.',
     'actions.lanes.history.title': 'Execution history',
     'actions.lanes.history.copy': 'The history rail is fixed and execution records plug directly into the same evidence flow.',
     'actions.lanes.approval.title': 'Approval semantics',
     'actions.lanes.approval.copy': 'Reserve gate and guidance copy for high-risk actions in the main CTA region.',
-    'menu.dashboard.back': '打开总览',
+    'menu.dashboard.back': 'Open overview',
+    'menu.entity.center': 'Entity center',
+    'entity.detail': 'Entity Detail',
+    'menu.monitor.bigdata': 'Bigdata Monitor',
+    'menu.monitor.cache': 'Cache Monitor',
+    'menu.monitor.center': 'Monitor center',
+    'menu.monitor.custom': 'Custom Monitor',
+    'menu.monitor.db': 'DB Monitor',
+    'menu.monitor.llm': 'Ai LLM',
+    'menu.monitor.mid': 'Mid Monitor',
+    'menu.monitor.network': 'Network Monitor',
+    'menu.monitor.os': 'OS Monitor',
+    'menu.monitor.server': 'Server Monitor',
+    'menu.monitor.service': 'Service Monitor',
+    'menu.monitor.webserver': 'Webserver Monitor',
     'alert.workbench.kicker': 'Alert Center',
     'alert.workbench.title': 'Review and handle current alerts',
     'alert.workbench.copy': 'Filter alerts by time, status, and target, then acknowledge, resolve, or open details here.',
@@ -528,12 +1996,34 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.workbench.empty.title': 'No alerts in the current scope',
     'alert.workbench.empty.copy': 'There are no actionable alerts in the current time range and filters.',
     'alert.workbench.empty.copy.filtered': 'No alerts match the current filters. Adjust the filters and search again.',
+    'alert.center.loading': 'Loading alert center',
     'alert.center.default-title': 'Alert',
+    'alert.center.search': 'Search alerts',
+    'alert.center.filter-status': 'Filter status',
+    'alert.center.batch.select-page': 'Select current page',
+    'alert.center.batch.select-group': 'Select alert group',
+    'alert.center.batch.selection-label': 'alert groups selected',
+    'alert.center.batch.acknowledge-selected': 'Acknowledge {{count}}',
+    'alert.center.batch.unacknowledge-selected': 'Unacknowledge {{count}}',
+    'alert.center.batch.resolve-selected': 'Resolve {{count}}',
+    'alert.center.batch.reopen-selected': 'Reopen {{count}}',
+    'alert.center.batch.silence-selected': 'Create silence for {{count}}',
+    'alert.center.batch.inhibit-selected': 'Create inhibit for {{count}}',
+    'alert.center.delete': 'Delete',
+    'alert.center.last-time': 'Last time',
+    'alert.center.end-time': 'End time',
+    'alert.center.time.tip': 'Triggered {{times}} times',
     'alert.center.recent.title': 'Current alerts',
     'alert.center.routes.title': 'Alert settings',
     'alert.setting.title': 'Alert settings',
     'alert.setting.copy': 'Manage threshold names, data type, expressions, and alert content while keeping rule editing aligned with alert noise reduction.',
     'alert.setting.action.new': 'New threshold rule',
+    'alert.setting.batch.selection-label': 'threshold rules selected',
+    'alert.setting.export.selected': '{{count}} threshold rules selected',
+    'alert.setting.import.input': 'Import threshold rules file',
+    'alert.setting.notify.no-select-export': 'Select threshold rules before export',
+    'alert.export.switch-type': 'Choose export file format',
+    'alert.export.use-type': 'Export threshold rules in {{type}} format',
     'alert.setting.delete.confirm.single': 'Delete the selected threshold rule. This cannot be undone.',
     'alert.setting.delete.confirm.batch': 'Delete {{count}} threshold rules. This cannot be undone.',
     'alert.setting.fact.workspace': 'Workspace',
@@ -577,17 +2067,111 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.center.routes.silence-inhibit.copy': 'Silence and inhibit rules',
     'alert.center.routes.silence-inhibit.meta': 'Silence and inhibit rules',
     'alert.center.summary.title': 'Alert summary',
+    'alert.status.firing': 'Firing',
+    'alert.status.acknowledged': 'Acknowledged',
+    'alert.status.resolved': 'Resolved',
     'alert.center.status.firing': 'Firing',
     'alert.center.status.resolved': 'Resolved',
     'alert.center.status.suppressed': 'Suppressed',
     'alert.center.metrics.warning': 'Warning',
     'alert.center.metrics.critical': 'Critical',
     'alert.center.metrics.emergency': 'Emergency',
+    'alert.center.metrics.error': 'Error',
+    'alert.center.metrics.info': 'Info',
+    'alert.center.metrics.unknown': 'Unknown',
     'alert.center.selected.fingerprint': 'Fingerprint / Creator',
     'alert.center.selected.trigger-window': 'Triggers / Active window',
+    'alert.center.selected.trigger-count': '{{count}} triggers',
     'alert.center.selected.triggers': 'triggers',
     'alert.center.selected.empty.title': 'No alert selected',
     'alert.center.selected.empty.copy': 'Select an alert from the left to inspect details.',
+    'alert.center.context.status.unknown': 'Unknown status {{status}}',
+    'alert.center.context.severity.unknown': 'Unknown severity {{severity}}',
+    'alert.center.context.time.title': 'Time range',
+    'alert.center.context.time.inherited': 'Inherited time window',
+    'alert.center.context.time.refresh': 'Refresh {{refresh}}',
+    'alert.center.context.time.paused': 'paused',
+    'alert.center.context.time.live': 'live',
+    'alert.center.context.monitor.title': 'Monitor',
+    'alert.center.context.monitor.id-meta': 'Monitor ID {{monitorId}}',
+    'alert.center.context.source.title': 'Source',
+    'alert.center.context.source.monitor.copy': 'Traditional monitor',
+    'alert.center.context.source.monitor.meta': 'Monitor center context',
+    'alert.center.context.source.topology.copy': 'Topology',
+    'alert.center.context.source.topology.meta': 'Topology impact context',
+    'alert.center.context.source.otlp.copy': 'OTLP signals',
+    'alert.center.context.source.otlp.meta': 'Metric, log, and trace context',
+    'alert.center.context.source.alert.copy': 'Alert event',
+    'alert.center.context.source.alert.meta': 'Alert evidence context',
+    'alert.center.context.source.unknown.copy': 'Unknown evidence source {{source}}',
+    'alert.center.context.source.default.meta': 'Inherited evidence source',
+    'alert.center.context.trace.title': 'Trace context',
+    'alert.center.context.trace.span-meta': 'Span ID {{spanId}}',
+    'alert.center.context.trace.inherited': 'traceId inherited',
+    'alert.center.topology.context.kicker': 'Topology impact',
+    'alert.center.topology.context.fallback': 'Current topology context',
+    'alert.center.topology.context.return': 'Return to topology',
+    'alert.center.topology.view-mode.unknown': 'Unknown topology view {{viewMode}}',
+    'alert.center.topology.source.all': 'All relation sources',
+    'alert.center.topology.source.otlp-trace-call': 'OTLP call relation',
+    'alert.center.topology.source.monitor-ownership': 'Monitor ownership',
+    'alert.center.topology.source.template-dependency': 'Template dependency',
+    'alert.center.topology.source.k8s-workload': 'K8s workload',
+    'alert.center.topology.source.database-middleware-connection': 'Database / middleware connection',
+    'alert.center.topology.source.cmdb-manual-label': 'CMDB / manual labels',
+    'alert.center.topology.source.alert-impact': 'Alert impact',
+    'alert.center.topology.source.unknown': 'Unknown relation source {{sourceKind}}',
+    'alert.center.group.response.stage.firing': 'Response state: Needs acknowledgement',
+    'alert.center.group.response.stage.acknowledged': 'Response state: Acknowledged',
+    'alert.center.group.response.stage.resolved': 'Response state: Recovered',
+    'alert.center.group.response.stage.custom': 'Response state: {{status}}',
+    'alert.center.group.evidence.summary': 'Evidence: {{alertCount}} {{alertUnit}} · {{labelCount}} {{labelUnit}}',
+    'alert.center.group.evidence.alert.singular': 'alert',
+    'alert.center.group.evidence.alert.plural': 'alerts',
+    'alert.center.group.evidence.label.singular': 'label',
+    'alert.center.group.evidence.label.plural': 'labels',
+    'alert.center.group.closure.next': 'Next: {{actions}}',
+    'alert.center.evidence.closure.kicker': 'Alert evidence closure',
+    'alert.center.evidence.closure.title': 'Review evidence before handling',
+    'alert.center.evidence.closure.copy': 'Keep entity, metric, log, trace, and topology evidence in the same handling entry.',
+    'alert.center.evidence.closure.summary.evidence': 'Evidence scope',
+    'alert.center.evidence.closure.summary.operations': 'Closure actions',
+    'alert.center.evidence.entity.title': 'Entity detail',
+    'alert.center.evidence.entity.fallback': 'Search entity by service',
+    'alert.center.evidence.entity.meta': 'Open the HertzBeat entity affected by this alert',
+    'alert.center.evidence.metrics.title': 'Metric evidence',
+    'alert.center.evidence.metrics.fallback': 'Inspect metrics by alert context',
+    'alert.center.evidence.metrics.meta': 'Inspect metrics in the same service, entity, and time range',
+    'alert.center.evidence.logs.title': 'Log evidence',
+    'alert.center.evidence.logs.fallback': 'Inspect logs by alert context',
+    'alert.center.evidence.logs.meta.trace': 'Inspect logs by traceId/spanId',
+    'alert.center.evidence.logs.meta.service': 'Inspect logs and trace fields for the same service',
+    'alert.center.evidence.traces.title': 'Trace evidence',
+    'alert.center.evidence.traces.fallback': 'Inspect traces by alert context',
+    'alert.center.evidence.traces.meta.trace': 'Open trace evidence by traceId/spanId',
+    'alert.center.evidence.traces.meta.service': 'Inspect latency and error spans for the same service',
+    'alert.center.evidence.topology.title': 'Topology impact',
+    'alert.center.evidence.topology.fallback': 'Inspect dependency impact',
+    'alert.center.evidence.topology.meta': 'Return to dependencies and confirm blast radius',
+    'alert.center.operation.target.current-alert': 'current alert',
+    'alert.center.operation.acknowledge.label': 'Acknowledge',
+    'alert.center.operation.acknowledge.copy': 'Mark {{count}} alert(s) for {{target}} as owned.',
+    'alert.center.operation.recover.label': 'Mark recovered',
+    'alert.center.operation.recover.copy': 'Keep the evidence bundle after recovery for review.',
+    'alert.center.operation.threshold.label': 'Create threshold rule',
+    'alert.center.operation.threshold.copy': 'Carry this entity and signal context into threshold rules.',
+    'alert.center.operation.notice.label': 'Configure notification policy',
+    'alert.center.operation.notice.copy': 'Route notifications from the current entity and labels.',
+    'alert.center.operation.group.label': 'Configure grouping',
+    'alert.center.operation.group.copy': 'Group alerts by stable entity, service, and environment labels.',
+    'alert.center.operation.silence.label': 'Create silence',
+    'alert.center.operation.silence.copy': 'Create a temporary silence from current labels.',
+    'alert.center.operation.inhibit.label': 'Create inhibit',
+    'alert.center.operation.inhibit.copy': 'Create inhibit rules from the current labels.',
+    'alert.center.operation.automation.label': 'Suggest automation',
+    'alert.center.operation.automation.copy': 'Carry this evidence into suggested actions with human confirmation.',
+    'alert.center.operation.close.label': 'Close alert',
+    'alert.center.operation.close.copy': 'Close this alert cycle after handling the issue.',
     'alert.center.operation.success': 'Alert updated. The list is refreshing.',
     'alert.center.operation.success.acknowledge': 'Alert acknowledged. The acknowledged filter is active, and the entity, topology, and three-signal evidence context is preserved.',
     'alert.center.operation.success.recover': 'Alert marked recovered. The recovered filter is active, and the entity, topology, and three-signal evidence context is preserved.',
@@ -597,12 +2181,57 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.center.operation.failed': 'Alert operation was not completed: {{reason}}',
     'alert.center.open-full-workspace': 'Open full workspace',
     'common.end': 'End',
+    'bulletin.batch.delete': 'Batch delete bulletins',
+    'bulletin.new': 'New bulletin',
+    'bulletin.edit': 'Edit bulletin',
+    'bulletin.name': 'Bulletin name',
+    'bulletin.monitor.type': 'Monitor type',
+    'bulletin.monitor.name': 'Monitor',
+    'bulletin.monitor.metrics': 'Metrics',
     'bulletin.facts.selected': 'Current selection',
     'bulletin.list.monitors': 'monitors',
     'bulletin.list.creator': 'creator',
+    'bulletin.selected.empty.title': 'No bulletin selected',
+    'bulletin.selected.empty.copy': 'Select a bulletin board from the list to inspect its metrics context.',
     'bulletin.selected.fields': 'fields',
+    'bulletin.monitor.ids.placeholder': '1, 2, 3',
+    'bulletin.delete.title': 'Delete bulletin',
+    'bulletin.delete.copy': 'After deletion, this bulletin board is removed from the current workspace and the metrics refresh context needs a bulletin board to be selected again.',
+    'bulletin.delete.confirm': 'Confirm delete',
     'bulletin.validation.app': 'App is required',
     'bulletin.validation.monitors': 'At least one monitor id is required',
+    'bulletin.validation.fields': 'Metrics fields JSON is invalid',
+    'entities.list.title': 'Object Directory',
+    'entities.list.kicker': 'Object-first triage',
+    'entities.list.subtitle': 'Use services, resources, and entities to narrow risk first, then move into logs, traces, metrics, or alerts with the same context.',
+    'entities.list.action.create': 'Create entity',
+    'entities.list.action.discovery': 'Discover from telemetry',
+    'entities.list.action.import': 'Import definition',
+    'entities.list.metric.total': 'Total entities',
+    'entities.list.metric.abnormal': 'Active abnormal objects',
+    'entities.list.metric.alerting': 'High-risk objects',
+    'entities.list.metric.linked': 'Linked objects',
+    'entities.list.search.placeholder': 'Search entity name, namespace, or owner',
+    'entities.list.table.total': '{{total}} entities',
+    'entities.list.table.range': 'Showing {{from}}-{{to}} / {{total}}',
+    'entities.list.column.owner': 'Owner',
+    'entities.list.column.progress': 'Progress',
+    'entities.list.column.evidence': 'Evidence links',
+    'entities.list.column.next-action': 'Next action',
+    'entities.list.row.owner.unset': 'Not set',
+    'entities.list.row.progress.incomplete': '{{percent}} · incomplete',
+    'entities.list.row.progress.missing': 'Owner and runbook are still missing.',
+    'entities.list.row.evidence.monitors': '{{count}} monitors',
+    'entities.list.row.evidence.alerts': '{{count}} alerts',
+    'entities.list.row.evidence.identities': '{{count}} identities',
+    'entities.list.row.evidence.updated': 'Updated {{time}}',
+    'entities.list.row.action.owner': 'Set owner',
+    'entities.list.row.action.logs': 'Log clues',
+    'entities.list.row.action.traces': 'Trace evidence',
+    'entities.list.empty.title': 'No objects found',
+    'entities.list.empty.copy': 'Adjust the resource filter or connect telemetry to populate the object directory.',
+    'entities.list.unselected.title': 'No entity selected',
+    'entities.list.unselected.copy': 'Select an entity to inspect evidence and actions.',
     'entities.list.type.api': 'API',
     'entities.list.type.database': 'Database',
     'entities.list.type.device': 'Device',
@@ -611,34 +2240,171 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'entities.list.type.k8s-workload': 'K8s workload',
     'entities.list.type.middleware': 'Middleware',
     'entities.list.type.queue': 'Queue',
-    'entities.list.type.service': '服务',
+    'entities.list.type.service': 'Service',
     'entities.list.type.system': 'System',
-    'entities.list.environment.local': '本地',
+    'entities.list.environment.local': 'Local',
     'entities.list.status.critical': 'Critical',
     'entities.list.status.down': 'Offline',
     'entities.list.status.healthy': 'Healthy',
     'entities.list.status.unhealthy': 'Unhealthy',
-    'entities.list.status.unknown': '未知',
+    'entities.list.status.unknown': 'Unknown',
     'entities.list.status.warning': 'Warning',
-    'entities.list.metric.alerts': '当前页告警',
-    'entities.list.metric.monitors': '当前页监控',
-    'entities.list.metric.relations': '当前页关系',
-    'entities.list.item.meta': '监控 {{monitorCount}} · 告警 {{alertCount}} · 证据 {{time}}',
-    'entities.list.selected.alerts': '告警 {{count}}',
-    'entities.list.selected.definition-activity': '定义活动 -',
-    'entities.list.rail.identity-monitor-relation': '身份 / 监控 / 关系',
+    'entities.list.metric.alerts': 'Current page alerts',
+    'entities.list.metric.monitors': 'Current page monitors',
+    'entities.list.metric.relations': 'Current page relations',
+    'entities.list.item.meta': 'Monitors {{monitorCount}} · alerts {{alertCount}} · evidence {{time}}',
+    'entities.list.selected.alerts': 'Alerts {{count}}',
+    'entities.list.selected.definition-activity': 'Definition activity -',
+    'entities.list.rail.identity-monitor-relation': 'Identity / monitors / relations',
     'monitor.editor.validation.cron': 'Cron expression is invalid',
     'setting.status.validation.incident-components': 'At least one component id is required',
     'setting.status.validation.incident-message': 'Incident message is required',
-    'log.manage.trend.count': '日志 {{count}}',
+    'log.manage.trend.count': '{{count}} logs',
+    'log.manage.console.title': 'Logs Workbench',
+    'log.manage.loading': 'Loading log workbench',
+    'log.manage.route.kicker': 'Observability',
+    'log.manage.route.title': 'Log workbench',
+    'log.manage.route.subtitle': 'Filter logs by collection source, entity, trace, and alert handling while keeping live stream, history search, and context triage in one workbench.',
+    'log.manage.route.action.intake': 'Back to OTLP intake',
+    'log.manage.route.action.return-source': 'Return to source',
+    'log.manage.route.action.collector': 'Collectors',
+    'log.manage.route.action.entity': 'Entity detail',
+    'log.manage.route.action.alerts': 'Alert handling',
+    'log.manage.route.action.templates': 'Monitor templates',
+    'log.manage.query.search.aria': 'Log query',
+    'log.manage.query.search.placeholder': 'service.name = "checkout"',
+    'log.manage.query.severity.aria': 'Severity',
+    'log.manage.query.severity.all': 'All severities',
+    'log.manage.query.run.stream': 'Apply to log stream',
+    'log.manage.query.run.history': 'Run query',
+    'log.manage.query.reset': 'Reset',
+    'log.manage.query.trace-id': 'Trace ID',
+    'log.manage.query.span-id': 'Span ID',
+    'log.manage.query.body.aria': 'Log body',
+    'log.manage.query.body.placeholder': 'Search log body',
+    'log.manage.summary.total': 'Total logs',
+    'log.manage.summary.errors': 'Error logs',
+    'log.manage.summary.trace-coverage': 'Trace coverage',
+    'log.manage.summary.latest': 'Latest time',
+    'log.manage.trend.title': 'Trend band',
+    'log.manage.trend.points': '{{count}} points',
+    'log.manage.list.title': 'Recent logs',
+    'log.manage.list.count': '{{count}} rows',
+    'log.manage.list.column.time': 'Time',
+    'log.manage.list.column.severity': 'Severity',
+    'log.manage.list.column.service': 'Service',
+    'log.manage.list.column.body': 'Log body',
+    'log.manage.list.column.trace': 'Trace',
+    'log.manage.empty.title': 'No logs',
+    'log.manage.empty.copy': 'Confirm the time range, entity attribution, collector, and monitor template before reviewing logs.',
+    'log.manage.detail.title': 'Detail panel',
+    'log.manage.detail.severity': 'Severity',
+    'log.manage.detail.trace-id': 'Trace ID',
+    'log.manage.detail.span-id': 'Span ID',
+    'log.manage.evidence.aria': 'Log evidence Log time Log severity Body summary Latest observed',
+    'log.manage.evidence.title': 'Log evidence',
+    'log.manage.evidence.current': 'Current selection',
+    'log.manage.evidence.time.title': 'Log time',
+    'log.manage.evidence.time.meta': 'Log write time',
+    'log.manage.evidence.severity.title': 'Log severity',
+    'log.manage.evidence.severity.current-filter': 'Current filter',
+    'log.manage.evidence.body.title': 'Body summary',
+    'log.manage.evidence.body.meta': 'Log body',
+    'log.manage.evidence.latest.title': 'Latest observed',
+    'log.manage.evidence.latest.meta': 'Workbench latest log',
+    'log.manage.context.entity.aria': 'Entity context Current entity Monitor instance Current service Trace context Current environment Time range Collection source',
+    'log.manage.context.entity.title': 'Entity context',
+    'log.manage.handoff.alert-hint': 'Review related alerts with the current entity, service, and inherited trace context.',
+    'log.manage.handoff.signal-hint': 'Current log trace, span, and service context carries into the trace and metrics workbenches.',
+    'log.manage.handoff.alerts': 'Alert handling',
+    'log.manage.handoff.traces': 'View trace',
+    'log.manage.handoff.metrics': 'View metrics',
+    'log.manage.handoff.entities': 'Entity catalog',
+    'log.manage.stream.view-switch.title': 'Log view',
+    'log.manage.stream.view.stream': 'Log stream',
+    'log.manage.stream.view.history': 'History search',
+    'log.manage.stream.detail.title': 'Log detail',
+    'log.manage.stream.detail.stream-title': 'Log stream detail',
+    'log.manage.stream.detail.attribution-title': 'Attribution diagnostics',
+    'log.manage.stream.detail.attribution-present': 'Present',
+    'log.manage.stream.detail.attribution-missing': 'Missing',
+    'log.manage.selected.attribution.aria': 'Attribution diagnostics hertzbeat.entity_id hertzbeat.collector hertzbeat.template',
+    'log.manage.selected.attribution.title': 'Attribution diagnostics',
+    'log.manage.selected.attribution.state.present': 'Present',
+    'log.manage.selected.attribution.state.missing': 'Missing',
+    'log.manage.stream.action.view-trace': 'View trace',
+    'log.manage.stream.action.view-metrics': 'View metrics',
+    'log.manage.stream.action.view-code': 'View code',
+    'log.manage.stream.stage.kicker': 'Log stream',
+    'log.manage.stream.stage.title': 'Live log stream',
+    'log.manage.stream.status.connected': 'Connected',
+    'log.manage.stream.status.connecting': 'Connecting',
+    'log.manage.stream.status.disconnected': 'Disconnected',
+    'log.manage.stream.count': '{{count}} logs',
+    'log.manage.stream.state.live': 'Live',
+    'log.manage.stream.state.paused': 'Paused',
+    'log.manage.stream.action.reconnect': 'Reconnect',
+    'log.manage.stream.action.resume': 'Resume',
+    'log.manage.stream.action.pause': 'Pause',
+    'log.manage.stream.action.clear': 'Clear',
+    'log.manage.stream.paused-copy': 'Log stream is paused; new logs remain in the collection path until you resume.',
+    'log.manage.stream.empty.title': 'Waiting for live logs',
+    'log.manage.stream.empty.copy': 'Keep the stream connected so logs written by Collectors and OTLP arrive here in real time.',
+    'log.manage.stream.selected.title': 'Selected log',
+    'log.manage.stream.selected.detached': 'Detached from buffer',
+    'log.manage.stream.selected.none': 'No log selected',
+    'log.manage.stream.selected.helper': 'Select any log to inspect full content, trace context, and entity context.',
+    'log.manage.stream.detail.detached-warning': 'This log is detached from the live buffer; verify the preserved payload before handoff.',
+    'log.manage.stream.action.view-log': 'View log',
     'log.manage.selected.resource-keys': 'Resource fields',
     'log.manage.selected.attributes': 'Log attributes {{count}}',
     'log.manage.selected.span-severity': 'Span and severity',
-    'overview.lane.logs.stat.recommended': '无链路 ID 时优先',
-    'passport.login.session-copy': '登录成功后会自动恢复当前工作台会话,并在需要时尝试刷新令牌。',
+    'log.manage.related-trace.error': 'The trace preview is temporarily unavailable. Retry later or open the full trace workspace.',
+    'log.manage.related-trace.fallback-title': 'Related trace preview',
+    'log.manage.related-trace.badge': 'Trace preview',
+    'log.manage.related-trace.open-workspace': 'View full trace',
+    'log.manage.related-trace.spans-count': '{{count}} spans',
+    'log.manage.related-trace.fact.current-span': 'Current span',
+    'log.manage.related-trace.fact.error-spans': 'Error spans',
+    'log.manage.related-trace.fact.events': 'Events',
+    'log.manage.related-trace.fact.links': 'Links',
+    'log.manage.related-trace.empty-title': 'No related trace',
+    'log.manage.related-trace.empty-copy': 'This log has no trace details available for preview.',
+    'log.manage.related-trace.loading-title': 'Loading trace preview',
+    'log.manage.related-trace.loading-copy': 'Loading the trace preview from the current log context.',
+    'log.manage.related-trace.span-label': 'Span',
+    'log.manage.related-trace.duration-label': 'Duration',
+    'log.manage.related-trace.timeline-label': 'Timeline',
+    'log.manage.related-trace.kicker': 'Related trace',
+    'log.manage.related-trace.error-title': 'Trace detail error',
+    'log.manage.related-trace.event-fact.title': 'Span event',
+    'log.manage.related-trace.event-fact.fallback': 'Unnamed event',
+    'log.manage.related-trace.event-fact.meta': 'Not a new span; this is a point on the current span timeline.',
+    'log.manage.related-trace.event-fact.span': 'Owning span',
+    'log.manage.related-trace.event-fact.position': 'Event position',
+    'log.manage.related-trace.event-fact.timeline': 'Relative trace timeline',
+    'log.manage.related-trace.event-fact.attributes': 'Event attributes',
+    'log.manage.related-trace.event-fact.no-attributes': 'No attributes',
+    'log.manage.related-trace.event-detail.title': 'Event detail',
+    'log.manage.related-trace.event-detail.copy': 'Not a new span; this is a point on the current span timeline.',
+    'log.manage.related-trace.event-detail.action.span': 'View span',
+    'log.manage.attribution.event-id.present': 'Locate this log ingest event.',
+    'log.manage.attribution.event-id.missing': 'Use timestamp and content when event ID is missing.',
+    'log.manage.attribution.ingest-id.present': 'Trace the ingest batch.',
+    'log.manage.attribution.ingest-id.missing': 'Use Collector logs when the ingest batch is missing.',
+    'log.manage.attribution.entity-id.present': 'Entity details can be opened.',
+    'log.manage.attribution.entity-id.missing': 'Entity ID is missing; entity details stay disabled.',
+    'log.manage.attribution.entity-name.present': 'Used to display the entity name.',
+    'log.manage.attribution.entity-name.missing': 'Use the service name for fallback search when entity name is missing.',
+    'log.manage.attribution.workspace-id.present': 'Workspace ownership.',
+    'log.manage.attribution.workspace-id.missing': 'Use current deployment context when workspace field is missing.',
+    'log.manage.attribution.collector': 'Collector source.',
+    'log.manage.attribution.template': 'Monitor template ownership.',
+    'overview.lane.logs.stat.recommended': 'Prioritize when trace ID is missing',
+    'passport.login.session-copy': 'After login, HertzBeat will restore the current workspace session and try to refresh tokens when needed.',
     'setting.status.validation.incident-name': 'Incident name is required',
     'status.incidents.history': 'Incident history',
-    'status.incidents.posture': '聚焦公开事件历史入口。',
+    'status.incidents.posture': 'Focus the public incident history entry.',
     'signal.context.entity.label': 'Current entity',
     'signal.context.entity.meta': 'Entity catalog',
     'signal.context.monitor.label': 'Monitor instance',
@@ -699,27 +2465,70 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.group.delete.confirm.single': 'Delete the selected grouping rule. This cannot be undone.',
     'alert.group.delete.confirm.batch': 'Delete {{count}} grouping rules. This cannot be undone.',
     'alert.group.evidence.labels': 'Grouping labels',
+    'alert.group.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.group.pagination.page-size': 'Page size',
+    'alert.group.pagination.page': 'Page',
+    'alert.center.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.center.pagination.page-size': 'Page size',
+    'alert.center.pagination.page': 'Page',
     'alert.silence.copy': 'Manage silence rules by name, match labels, and active period to reduce repeated alert noise.',
     'alert.silence.action.new': 'New silence rule',
     'alert.silence.notes.times.meta': 'Muted alert count',
     'alert.silence.delete.confirm.single': 'Delete the selected silence rule. This cannot be undone.',
     'alert.silence.delete.confirm.batch': 'Delete {{count}} silence rules. This cannot be undone.',
+    'alert.silence.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.silence.pagination.page-size': 'Page size',
+    'alert.silence.pagination.page': 'Page',
     'alert.inhibit.copy': 'Define source, target, and equal-label inhibition so cascading alerts do not create duplicate noise.',
     'alert.inhibit.action.new': 'New inhibit rule',
     'alert.inhibit.delete.confirm.single': 'Delete the selected inhibit rule. This cannot be undone.',
     'alert.inhibit.delete.confirm.batch': 'Delete {{count}} inhibit rules. This cannot be undone.',
+    'alert.inhibit.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.inhibit.pagination.page-size': 'Page size',
+    'alert.inhibit.pagination.page': 'Page',
     'alert.inhibit.evidence.source-labels': 'Source labels',
     'alert.inhibit.evidence.target-labels': 'Target labels',
     'alert.inhibit.evidence.equal-labels': 'Equal labels',
     'alert.notice.page-size': '{{count}} / page',
+    'alert.notice.pagination.summary': 'Page {{page}} / {{totalPages}} · {{from}}-{{to}} / {{total}}',
+    'alert.notice.pagination.page-size': 'Page size',
+    'alert.notice.pagination.page': 'Page',
     'alert.notice.copy': 'Configure receivers, notification policies, and message templates so alerts reach the right channel.',
+    'alert.notice.metrics.visible-receivers': 'Visible receivers',
+    'alert.notice.metrics.visible-rules': 'Visible policies',
+    'alert.notice.metrics.preset-templates': 'Preset templates',
+    'alert.notice.row.updated': 'Updated',
+    'alert.notice.row.enabled': 'Enabled',
+    'alert.notice.row.disabled': 'Disabled',
+    'alert.notice.row.delivery-prefix': 'Sending to',
+    'alert.notice.row.no-receiver': 'No receiver linked',
+    'alert.notice.form.receiver-ids.placeholder': 'Receiver IDs, for example 1, 2',
+    'alert.notice.form.template-id.placeholder': 'Template ID, use -1 for default',
+    'alert.notice.form.labels.placeholder': 'Label filters, for example severity:critical',
+    'alert.notice.form.days.placeholder': 'Weekdays, for example 1,2,3,4,5',
+    'alert.notice.form.email.placeholder': 'Email address, for example ops@example.com',
+    'alert.notice.form.phone.placeholder': 'Phone number, for example +86 13800000000',
+    'alert.notice.form.webhook.placeholder': 'Webhook URL, for example https://hooks.example.com',
+    'alert.notice.lanes.receivers.copy': 'Manage who receives each alert notice.',
+    'alert.notice.lanes.rules.copy': 'Route notices by labels, schedule, and template.',
+    'alert.notice.lanes.templates.copy': 'Preview templates as plain text without exposing raw HTML or source.',
+    'alert.notice.lanes.count-meta': '{{total}} total · {{visible}} visible',
     'alert.notice.delete.confirm.receiver': 'Delete the selected receiver. Related notification configuration may be affected.',
     'alert.notice.delete.confirm.template': 'Delete the selected message template. This cannot be undone.',
     'alert.notice.delete.confirm.rule': 'Delete the selected notification policy. This cannot be undone.',
     'alert.group.validation.name': 'Rule name is required',
     'alert.group.validation.labels': 'At least one label is required',
+    'alert.group.validation.group-wait': 'Wait time is required',
+    'alert.group.validation.group-interval': 'Interval time is required',
+    'alert.group.validation.repeat-interval': 'Repeat interval is required',
+    'alert.group.validation.group-wait-non-negative': 'Wait time must be 0 or greater',
+    'alert.group.validation.group-interval-non-negative': 'Interval time must be 0 or greater',
+    'alert.group.validation.repeat-interval-non-negative': 'Repeat interval must be 0 or greater',
+    'alert.group.id-required': 'Alert group id is required',
     'alert.group.default-title': 'Group rule',
+    'alert.group.loading': 'Loading group rules',
     'alert.group.labels': 'labels',
+    'alert.group.labels.placeholder': 'alertname, severity, service',
     'alert.group.wait': 'wait',
     'alert.group.interval': 'interval',
     'alert.group.repeat': 'repeat',
@@ -729,14 +2538,22 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.group.selected.labels': 'labels',
     'alert.group.selected.timers': 'timers',
     'alert.group.notes.query': 'Rule order',
+    'alert.rule.notes.sort-desc-copy': 'id desc',
+    'alert.rule.notes.search-copy': 'search',
+    'alert.rule.notes.search-sort-copy': 'search · id desc',
+    'alert.rule.selected.id-meta': 'ID {{id}}',
     'alert.inhibit.validation.name': 'Rule name is required',
     'alert.inhibit.validation.source': 'Source labels are required',
     'alert.inhibit.validation.target': 'Target labels are required',
     'alert.inhibit.validation.equal': 'Equal labels are required',
     'alert.inhibit.default-title': 'Inhibit rule',
+    'alert.inhibit.loading': 'Loading inhibit rules',
     'alert.inhibit.source': 'source',
     'alert.inhibit.target': 'target',
     'alert.inhibit.equal': 'equal',
+    'alert.inhibit.label.key.placeholder': 'Label key',
+    'alert.inhibit.label.value.placeholder': 'Label value',
+    'alert.inhibit.equal.placeholder': 'alertname, severity, service',
     'alert.inhibit.empty.title': 'No inhibit rules yet',
     'alert.inhibit.selected.empty.title': 'Select an inhibit rule',
     'alert.inhibit.selected.empty.copy': 'Choose a rule to see which active alerts suppress downstream noise.',
@@ -750,9 +2567,14 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.silence.validation.time': 'Start and end time are required',
     'alert.silence.validation.days': 'Days are required',
     'alert.silence.default-title': 'Silence rule',
+    'alert.silence.loading': 'Loading silence rules',
     'alert.silence.match-all': 'match all',
     'alert.silence.labels': 'labels',
+    'alert.silence.label.key.placeholder': 'Label key',
+    'alert.silence.label.value.placeholder': 'Label value',
     'alert.silence.times': 'muted alerts',
+    'alert.silence.time.start': 'Start time',
+    'alert.silence.time.end': 'End time',
     'alert.silence.days': 'days',
     'alert.silence.empty.title': 'No silence rules yet',
     'alert.silence.selected.empty.title': 'Select a silence rule',
@@ -760,6 +2582,7 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.silence.selected.strategy': 'strategy',
     'alert.silence.selected.strategy.all': 'all',
     'alert.silence.selected.strategy.any': 'any',
+    'alert.silence.selected.type-meta': 'type {{type}}',
     'alert.silence.selected.labels-days': 'labels / days',
     'alert.silence.notes.times.title': 'Muted alerts',
     'alert.silence.notes.query.title': 'Rule order',
@@ -792,19 +2615,35 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.validation.userId': 'User ID is required',
     'alert.notice.validation.chatId': 'Chat ID is required',
     'alert.notice.validation.partyId': 'Party ID is required',
+    'alert.notice.loading': 'Loading notification center',
+    'alert.setting.loading': 'Loading alert settings',
     'alert.notice.receivers.default': 'Receiver',
     'alert.notice.receivers.type': 'type',
     'alert.notice.receiver.type': 'Receiver type',
+    'alert.notice.receiver.people': 'Receiver',
+    'alert.notice.template': 'Notice Template',
+    'alert.notice.template.preset.true': 'System Preset',
     'alert.notice.template.validation.name': 'Template name is required',
     'alert.notice.template.validation.content': 'Template content is required',
     'alert.notice.templates.default': 'Template',
     'alert.notice.templates.preset': 'preset',
     'alert.notice.templates.custom': 'custom',
+    'alert.notice.rule.all': 'Dispatch All',
+    'alert.notice.rule.name': 'Policy Name',
+    'alert.notice.rule.period': 'Time Period',
+    'alert.notice.rule.period-chose': 'Choose Date',
+    'alert.notice.rule.period.custom': 'Custom',
+    'alert.notice.rule.period.no-limit': 'Unlimited',
+    'alert.notice.rule.tag': 'Label Match',
+    'alert.notice.rule.time': 'Notification Time',
+    'alert.notice.rule.time-end': 'End time',
+    'alert.notice.rule.time-start': 'Start time',
     'alert.notice.rule.validation.name': 'Rule name is required',
     'alert.notice.rule.validation.receivers': 'Receivers are required',
     'alert.notice.rule.validation.labels': 'Labels are required',
     'alert.notice.rule.validation.days': 'Days are required',
     'alert.notice.rule.validation.period': 'Period is required',
+    'alert.notice.rule.label.value.placeholder': 'Label value',
     'alert.notice.rules.default': 'Rule',
     'alert.notice.rules.receivers': 'receivers',
     'alert.notice.type.sms': 'SMS',
@@ -814,6 +2653,7 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.type.WeCom-robot': 'WeCom robot',
     'alert.notice.type.ding': 'DingTalk',
     'alert.notice.type.fei-shu': 'Feishu robot',
+    'alert.notice.type.telegram': 'Telegram',
     'alert.notice.type.telegram-bot': 'Telegram bot',
     'alert.notice.type.slack': 'Slack',
     'alert.notice.type.discord': 'Discord',
@@ -833,6 +2673,9 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.type.telegram-message-thread-id': 'Telegram message thread ID',
     'alert.notice.type.webhook-auth-type': 'Webhook auth type',
     'alert.notice.type.webhook-auth-type.placeholder': 'Select an auth type',
+    'alert.notice.type.webhook-auth-type.none': 'None',
+    'alert.notice.type.webhook-auth-type.basic': 'Basic',
+    'alert.notice.type.webhook-auth-type.bearer': 'Bearer',
     'alert.notice.type.webhook-auth-token': 'Webhook auth token',
     'alert.notice.type.slack-webHook-url': 'Slack webhook URL',
     'alert.notice.type.discord-channel-id': 'Discord channel ID',
@@ -868,14 +2711,79 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'alert.notice.receiver.phone.placeholder': 'Phone number, for example +86 13800000000',
     'alert.notice.receiver.email.placeholder': 'Email address, for example ops@example.com',
     'alert.notice.receiver.hookUrl.placeholder': 'Webhook URL, for example https://hooks.example.com',
+    'bulletin.loading': 'Loading bulletin center',
     'bulletin.search.all': 'all bulletins',
+    'bulletin.navigation.aria': 'Bulletin navigation',
     'bulletin.validation.name': 'Bulletin name is required',
+    'bulletin.metrics.column.app': 'App',
+    'bulletin.metrics.column.host': 'Host',
     'bulletin.metrics.empty.title': 'No metrics',
     'bulletin.metrics.empty.copy': 'No metrics are available yet.',
     'bulletin.metrics.error.title': 'Metrics unavailable',
     'bulletin.metrics.error-default': 'The metrics request failed.',
+    'bulletin.metrics.no-data': 'No Data Available',
     'exception.fact.workspace': 'Workspace',
     'exception.fact.status': 'Status',
+    'exception.fact.action': 'Action',
+    'exception.403.title': '403 Access denied',
+    'exception.403.subtitle': 'The current session cannot access this route. Review workspace, role, and token policy before retrying.',
+    'exception.403.fact.status': 'Permission boundary',
+    'exception.403.fact.action': 'Review access policy',
+    'exception.403.boundary.title': 'Access boundary',
+    'exception.403.boundary.copy': 'Confirm the session, role, and workspace permission before retrying.',
+    'exception.403.boundary.meta': 'Access control',
+    'exception.403.next-step.copy': 'Open access settings or return to a permitted workspace.',
+    'exception.404.title': '404 Route not found',
+    'exception.404.subtitle': 'The requested route is not registered in this HertzBeat workspace.',
+    'exception.404.fact.status': 'Route missing',
+    'exception.404.fact.action': 'Go back or redirect',
+    'exception.404.boundary.title': 'Route boundary',
+    'exception.404.boundary.copy': 'Check the navigation entry and compatibility alias before retrying.',
+    'exception.404.boundary.meta': 'Routing',
+    'exception.404.next-step.copy': 'Return to overview or use a known workspace route.',
+    'exception.500.title': '500 Runtime exception',
+    'exception.500.subtitle': 'The runtime returned an error while serving this workspace route.',
+    'exception.500.fact.status': 'Runtime failure',
+    'exception.500.fact.action': 'Retry or inspect',
+    'exception.500.boundary.title': 'Runtime boundary',
+    'exception.500.boundary.copy': 'Correlate server response, logs, and traces before retrying the operation.',
+    'exception.500.boundary.meta': 'Runtime',
+    'exception.500.next-step.copy': 'Inspect logs and traces, then retry after the backend is stable.',
+    'exception.next-step.title': 'Next step',
+    'exception.next-step.meta': 'Operator action',
+    'exception.subtitle.default': 'Group server-side exception events by service, resource, and time before opening logs or traces.',
+    'exception.chrome.title': 'Exception center',
+    'exception.chrome.filter-title': 'Filters',
+    'exception.chrome.clear-all': 'Clear all',
+    'exception.chrome.expand-more': 'Show more',
+    'exception.chrome.refresh-filters': 'Refresh filters',
+    'exception.time.last-7d': 'Last 7 days',
+    'exception.action.search': 'Search',
+    'exception.action.run-query': 'Run query',
+    'exception.action.feedback': 'Feedback',
+    'exception.action.share': 'Share',
+    'exception.query.aria': 'Exception query',
+    'exception.query.placeholder': 'Search and filter by resource attributes. Supports IN / NOT IN; press Enter to confirm.',
+    'exception.scope.aria': 'Exception scope',
+    'exception.scope.all': 'All exceptions',
+    'exception.scope.critical': 'Critical exceptions',
+    'exception.sort.aria': 'Exception sort',
+    'exception.sort.last-seen': 'Last seen',
+    'exception.sort.count': 'Count',
+    'exception.table.type': 'Exception type',
+    'exception.table.message': 'Error message',
+    'exception.table.count': 'Count',
+    'exception.table.last-seen': 'Last seen',
+    'exception.table.first-seen': 'First seen',
+    'exception.table.application': 'Application',
+    'exception.summary.groups': 'exception groups',
+    'exception.filters.deployment-environment': 'Deployment environment',
+    'exception.filters.service': 'Service',
+    'exception.filters.host': 'Host',
+    'exception.filters.k8s-cluster': 'K8s cluster',
+    'exception.filters.k8s-deployment': 'K8s deployment',
+    'exception.filters.k8s-namespace': 'K8s namespace',
+    'exception.filters.k8s-pod': 'K8s pod',
     'exception.recovery.overview.title': 'Overview',
     'exception.recovery.overview.copy': 'Return to the overview workspace.',
     'exception.recovery.back.title': 'Go back',
@@ -883,11 +2791,11 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'exception.recovery.home.title': 'Home',
     'exception.recovery.home.copy': 'Open the default landing route.',
     'exception.rail.overview.title': 'Overview',
-    'exception.rail.overview.copy': '返回主工作台并重新选择入口。',
+    'exception.rail.overview.copy': 'Return to the main workspace and choose the entry again.',
     'exception.rail.logs.title': 'Logs',
-    'exception.rail.logs.copy': '确认后端响应和错误日志。',
+    'exception.rail.logs.copy': 'Confirm backend responses and error logs.',
     'exception.rail.traces.title': 'Traces',
-    'exception.rail.traces.copy': '排查链路异常和请求级失败。',
+    'exception.rail.traces.copy': 'Inspect trace anomalies and request-level failures.',
   },
   'zh-CN': {
     'menu.log.integration': '集成接入',
@@ -897,19 +2805,47 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'log.integration.redirect.cta.manage': '前往日志工作台',
     'common.active': '活跃',
     'common.annotation.bind': '注解',
+    'common.api.message-nonzero': 'API 消息返回非零状态码',
+    'common.api.request-failed-status': 'API 请求失败:{{status}}',
     'common.attention': '关注',
     'common.app': '应用',
     'common.application': 'application',
+    'common.boolean.true': '是',
+    'common.boolean.false': '否',
+    'common.button.cancel': '取消',
+    'common.button.ok': '确定',
+    'common.button.setting': '配置',
+    'common.button.save': '保存',
+    'common.button.delete': '删除选中项',
+    'common.button.retry': '重试',
     'common.button.delete-batch': '批量删除',
+    'common.button.export': '导出',
+    'common.button.import': '导入',
+    'common.help': '帮助',
     'common.button.help': '帮助',
+    'common.confirm.delete': '请确认是否删除!',
+    'common.confirm.delete-batch': '删除选中项?',
+    'common.confirm.cancel': '请确认是否取消!',
+    'common.confirm.cancel-batch': '请确认是否批量取消!',
+    'common.confirm.enable': '请确认是否启用!',
+    'common.confirm.enable-batch': '请确认是否批量启用!',
+    'validation.required': '请填充必填项! ',
     'common.copy-failed': '复制失败',
     'common.current-page-count': '当前页',
-    'common.current-view': 'Current view',
+    'common.current-view': '当前视图',
+    'common.status': '状态',
+    'common.dark-mode': '深色模式',
     'common.description': '描述',
+    'common.failed': '失败',
     'common.decrement': '减少',
+    'common.duration.seconds': '{{value}} 秒',
+    'common.edit': '操作',
     'common.edit-time': '更新时间',
     'common.empty': '空',
     'common.no-data': '暂无数据',
+    'common.tab-navigation': '标签导航',
+    'common.timeline.empty': '暂无事件',
+    'common.enable': '启用状态',
     'common.enabled': '已启用',
     'common.disabled': '已停用',
     'common.focus': 'Focus',
@@ -925,21 +2861,102 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'common.next-series': 'Next series',
     'common.next-step': '下一步',
     'common.none': '无',
+    'common.yes': '是',
+    'common.no': '否',
+    'common.open': '打开',
     'common.previous': '上一条',
     'common.previous-point': 'Previous point',
     'common.previous-series': 'Previous series',
     'common.loading': '加载中...',
+    'common.view-more': '查看更多',
+    'common.workbench.loading.title': '正在加载工作台',
+    'common.workbench.loading.copy': '正在准备页面数据,请稍等。',
     'common.apply': '应用',
     'common.scrape': '采集方式',
+    'common.locale-code': 'zh-CN',
     'common.refresh': '刷新',
     'common.reset': '重置',
+    'common.search': '搜索',
+    'common.clear': '清除',
     'common.save': '保存',
+    'common.save-failed': '保存失败',
+    'common.save-success': '保存成功',
     'common.saving': '保存中...',
     'common.live': '实时',
+    'common.notice': '通知',
+    'common.unknown': '未知',
+    'menu.fullscreen': '全屏',
+    'menu.fullscreen.exit': '退出全屏',
+    'alert.notify.title': '新告警通知',
+    'alert.notify.body': '您有一条新的告警,请及时处理!',
+    'common.notify.apply-success': '应用成功',
+    'common.notify.apply-fail': '应用失败',
+    'common.notify.copy-success': '复制成功!',
+    'common.notify.copy-fail': '复制失败!',
+    'common.notify.query-fail': '查询失败!',
+    'common.notify.delete-success': '删除成功!',
+    'common.notify.delete-fail': '删除失败!',
+    'common.notify.mark-success': '标记成功!',
+    'common.notify.mark-fail': '标记失败!',
+    'common.notify.delete-pending': '正在删除选中监控',
+    'common.notify.edit-success': '修改成功!',
+    'common.notify.edit-fail': '修改失败!',
+    'common.notify.operate-success': '操作成功!',
+    'common.notify.operate-fail': '操作失败!',
+    'common.notify.enable-success': '启用成功',
+    'common.notify.enable-fail': '启用失败',
+    'common.notify.enable-pending': '正在启用选中监控',
+    'common.notify.cancel-success': '暂停成功',
+    'common.notify.cancel-fail': '暂停失败',
+    'common.notify.cancel-pending': '正在暂停选中监控',
+    'common.notify.export-success': '已开始导出',
+    'common.notify.export-fail': '导出失败',
+    'common.notify.export-pending': '正在准备导出',
+    'common.notify.export-fail-status': '导出失败:{{status}}',
+    'common.notify.import-submitted': '已提交导入 [{{taskName}}]',
+    'common.notify.import-fail': '导入失败',
+    'common.notify.import-pending': '正在准备导入',
+    'common.notify.import-fail-status': '导入失败:{{status}}',
+    'common.notify.import-success-detail': '导入 [{{taskName}}] 成功!',
+    'common.notify.import-fail-detail': '导入 [{{taskName}}] 失败:{{errMsg}}',
+    'common.notify.import-progress': '导入 [{{taskName}}] 进度:{{progress}}',
+    'common.notify.new-success': '新增成功!',
+    'common.notify.new-fail': '新增失败!',
+    'alert.notice.receiver.next': '下一步请配置您的[告警通知策略]!',
+    'common.notify.no-select-cancel': '请先选择要暂停的监控',
+    'common.notify.no-select-delete': '请先选择要删除的监控',
+    'common.notify.no-select-enable': '请先选择要启用的监控',
+    'common.notify.no-select-export': '未选择要导出的监控',
+    'common.week.1': '星期一',
+    'common.week.2': '星期二',
+    'common.week.3': '星期三',
+    'common.week.4': '星期四',
+    '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.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': '实时',
@@ -953,16 +2970,316 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'time.range.reset': '重置',
     'time.range.reset-aria': '重置时间范围',
     'time.range.relative-placeholder': '45m',
+    'common.dialog.close': '关闭对话框',
     'common.ready': '就绪',
     'common.signals': 'Signals',
+    'app.frame.utility.menu': '菜单',
+    'app.frame.utility.github': 'GitHub',
+    'app.frame.utility.user': '用户菜单:{{user}}',
+    'menu.extras.setting': '系统设置',
+    'menu.account.logout': '退出登录',
+    'menu.extras.about': '关于',
+    'app.login.need-change-password': '请及时更新初始默认密码!',
+    'ai.chat.launch': '打开 AI 助手',
+    'ai.chat.title': 'AI 助手',
+    'ai.chat.subtitle': 'HertzBeat 运维助手',
+    'ai.chat.conversations': '会话',
+    'ai.chat.new-chat': '新会话',
+    'ai.chat.conversation.loading': '正在加载会话...',
+    'ai.chat.conversation.ready': '会话列表已从 /chat/conversations 同步。',
+    'ai.chat.conversation.empty': '暂无历史会话,可在此工作台发起新对话。',
+    'ai.chat.conversation.error': '会话历史暂不可用,助手外壳仍可继续使用。',
+    'ai.chat.conversation.delete.title': '删除会话',
+    'ai.chat.conversation.delete.confirm': '删除',
+    'ai.chat.history.idle': '选择一个会话以加载历史消息。',
+    'ai.chat.history.loading': '正在加载选中会话历史...',
+    'ai.chat.history.ready': '选中会话历史已加载。',
+    'ai.chat.history.empty': '此会话暂无已保存消息。',
+    'ai.chat.history.error': '选中会话历史暂不可用,可尝试其他会话。',
+    'ai.chat.user': '用户',
+    'ai.chat.system': '系统',
+    'ai.chat.welcome.title': '询问 HertzBeat',
+    'ai.chat.welcome.description': '可以询问监控、告警、日志、链路、指标、拓扑和平台配置。',
+    'ai.chat.initial-message': '初始问题',
+    'ai.chat.assistant': '助手',
+    'ai.chat.offline.response': '对话外壳已就绪;流式响应证明会在下一迁移切片闭环。',
+    'ai.chat.typing': '正在输入...',
+    'ai.chat.error.processing': '处理您的请求时出错。请检查 AI 智能体服务是否正在运行,然后重试。',
+    'ai.chat.input.placeholder': '询问助手',
+    'ai.chat.input.hint': '在顶部输入框按 Enter 可打开助手工作台。',
+    'ai.chat.submit': '发送提示',
+    'ai.chat.modify-api-key': '修改 API 密钥',
+    'ai.chat.config.title': 'AI 提供商配置',
+    'ai.chat.config.required.content': '要使用 AI 智能体聊天,请配置您的模型提供商。',
+    'ai.chat.config.provider': 'AI 提供商',
+    'ai.chat.config.provider.help': '选择您的 AI 提供商(OpenAI、智谱或 ZAI)',
+    'ai.chat.config.provider.required': '请选择 AI 提供商',
+    'ai.chat.config.api-key': 'API 密钥',
+    'ai.chat.config.api-key.help': '您的提供商 API 密钥。保存时将验证密钥。',
+    'ai.chat.config.api-key.required': 'API 密钥是必需的',
+    'ai.chat.config.base-url': '基础 URL',
+    'ai.chat.config.base-url.help': '自定义 API 端点 URL。留空则使用所选提供商的默认值。',
+    'ai.chat.config.base-url.required': '基础 URL 是必需的',
+    'ai.chat.config.model': '模型',
+    'ai.chat.config.model.help': '要使用的模型名称。留空则使用所选提供商的默认值。',
+    'ai.chat.config.model.required': '模型是必需的',
+    'ai.chat.config.reset': '重置为默认值',
+    'ai.chat.config.save': '保存',
+    'ai.chat.config.cancel': '取消',
+    'ai.chat.config.idle': '提供商配置等待加载。',
+    'ai.chat.config.loading': '正在加载提供商配置...',
+    'ai.chat.config.ready': '提供商配置已从 /config/provider 加载。',
+    'ai.chat.config.saving': '正在保存提供商配置...',
+    'ai.chat.config.save.success': '模型提供商配置保存成功!',
+    'ai.chat.config.save.failed': '保存配置失败:',
+    'ai.chat.config.validation.failed': 'API 密钥验证失败:',
+    'ai.chat.schedule.title': '定时任务设置',
+    'ai.chat.schedule.create': '创建定时任务',
+    'ai.chat.schedule.add': '添加新任务',
+    'ai.chat.schedule.configured': '已配置的定时任务',
+    'ai.chat.schedule.skill': '技能',
+    'ai.chat.schedule.skill.select': '选择技能',
+    'ai.chat.schedule.skill.placeholder': '选择要执行的技能',
+    'ai.chat.schedule.cron': '执行时间',
+    'ai.chat.schedule.cron.label': '执行时间 (Cron表达式)',
+    'ai.chat.schedule.cron.help': '格式: 秒 分 时 日 月 周',
+    'ai.chat.schedule.cron.example.daily': '示例: 0 0 9 * * ? (每天9点)',
+    'ai.chat.schedule.cron.example.weekly': '示例: 0 0 9 ? * MON (每周一9点)',
+    'ai.chat.schedule.cron.common': '常用: 每天9点',
+    'ai.chat.schedule.cron.monday': '每周一9点',
+    'ai.chat.schedule.status': '状态',
+    'ai.chat.schedule.action': '操作',
+    'ai.chat.schedule.create.success': '定时任务创建成功',
+    'ai.chat.schedule.create.failed': '创建定时任务失败',
+    'ai.chat.schedule.delete.success': '定时任务已删除',
+    'ai.chat.schedule.delete.failed': '删除定时任务失败',
+    'ai.chat.schedule.toggle.success': '定时任务状态已更新',
+    'ai.chat.schedule.toggle.failed': '更新定时任务状态失败',
+    'ai.chat.schedule.push': '定时推送',
+    'ai.chat.schedule.button': '定时任务',
+    'ai.chat.schedule.tooltip': '配置定时推送任务',
+    'ai.chat.schedule.edit': '编辑定时任务',
+    'ai.chat.schedule.update.success': '定时任务更新成功',
+    'ai.chat.schedule.update.failed': '更新定时任务失败',
+    'ai.chat.schedule.idle': '定时任务配置等待加载。',
+    'ai.chat.schedule.loading': '正在加载定时任务和技能...',
+    'ai.chat.schedule.ready': '定时任务已从 /ai/schedule 加载。',
+    'ai.chat.schedule.empty': '此会话暂无已配置的定时任务。',
+    'ai.chat.schedule.saving': '正在保存定时任务...',
+    'ai.chat.schedule.error': '定时任务加载失败。',
+    'platform.footer.license': '遵循 Apache License, Version 2.0 授权',
+    'collector.health.cluster.title': '采集集群健康',
+    'collector.health.cluster.copy': '采集器 {{online}} / {{total}} 在线',
+    'collector.health.cluster.meta': '任务 {{tasks}} · 离线 {{offline}}',
+    'collector.health.cluster.freshness': '最近上报 {{time}}',
+    'collector.health.monitor.title': '采集健康',
+    'collector.health.monitor.copy': '监控 {{healthy}} / {{total}} 健康',
+    'collector.health.monitor.empty-copy': '暂无绑定监控',
+    'collector.health.monitor.meta': '采集证据已归并',
+    'collector.health.monitor.empty-meta': '等待模板绑定',
+    'collector.health.monitor.freshness': '最近证据 {{time}}',
     'entities.detail.loading': '加载实体详情...',
     'entities.detail.delete.failed': '删除实体失败。',
+    'entities.detail.fallback.display-name': '实体 {{id}}',
+    'entities.detail.fallback.draft-name': '实体草稿',
+    'entities.detail.fallback.description': '后端实体详情暂不可用时展示的临时工作台。',
+    'entities.detail.action-text.open-definition': '打开定义',
+    'entities.detail.action-text.review-definition': '先检查定义工作台,再补齐归属和证据。',
+    'entities.definition.workspace.attribution-check': '归因检查',
+    'entities.definition.workspace.attribution-state.ready': '已就绪',
+    'entities.definition.workspace.attribution-state.missing': '需补齐',
+    'entities.definition.workspace.attribution-state.unknown': '待确认',
+    'entities.definition.workspace.metric.ready': '可直接导入',
+    'entities.definition.workspace.metric.attention': '待完善',
+    'entities.definition.workspace.metric.telemetry-gaps': '缺少遥测绑定',
+    'entities.definition.workspace.fact.workspace': '工作区',
+    'entities.definition.workspace.fact.format': '格式',
+    'entities.definition.workspace.fact.activities': '活动',
+    'entities.definition.workspace.fact.templates': '模板',
+    'entities.definition.workspace.current-definition': '当前定义',
+    'entities.definition.workspace.template-title': '自定义模板',
+    'entities.definition.workspace.template-error-help': '定义加载失败后可先确认实体是否存在,再继续编辑。',
+    'entities.definition.workspace.template-empty-title': '暂无模板',
+    'entities.definition.workspace.template-empty-copy': '可复用模板会显示在这里。',
+    'entities.definition.workspace.batch-title': '模板与批量编辑',
+    'entities.definition.workspace.batch-copy': '查看模板、草稿来源和可选操作。',
+    'entities.definition.workspace.batch-expand': '展开',
+    'entities.definition.workspace.activity-title': '最近活动',
+    'entities.definition.workspace.activity-empty-title': '暂无最近活动',
+    'entities.definition.workspace.activity-empty-copy': '预览和导入活动会显示在这里。',
+    'entities.definition.workspace.entry-title': '入口',
+    'entities.definition.workspace.entry-view-entity': '查看实体',
+    'entities.definition.workspace.entry-back-to-form': '回到表单',
+    'entities.definition.message.load-failed': '加载实体定义失败。',
+    'entities.definition.message.entity-not-exist': '实体不存在。',
+    'entities.definition.message.process-failed': '实体定义处理失败。',
+    'entities.definition.message.update-missing-entity': '无法确认要更新的实体定义。',
+    'entities.definition.message.update-failed': '实体定义保存失败。',
+    'entities.definition.message.updated': '实体定义已更新。',
+    'entities.definition.message.backend-fallback': '后端返回:{{message}}',
+    'entity.definition.import.empty-preview': '请先粘贴定义,再预览。',
+    'entity.definition.import.empty-save': '请先粘贴或加载定义,再保存。',
+    'entity.definition.import.empty-import': '请先粘贴定义,再导入。',
+    'entity.definition.import.activity.preview-one': '已预览 1 个定义。',
+    'entity.definition.import.activity.preview-many': '已预览 {{count}} 个定义。',
+    'entity.definition.import.parse-failed': '定义解析失败。',
+    'entity.definition.workspace.single-required': '编辑已有实体时一次只能保存一个定义文档。',
+    'entity.definition.import.blocked': '部分定义仍需处理,请检查后再导入。',
+    'entity.definition.import.success-bundle': '已导入 {{count}} 个实体。',
+    'entity.definition.import.success-single': '实体定义已导入。',
+    'entity.definition.import.create-failed': '实体导入失败。',
+    'entity.definition.import.format': '导入格式',
+    'entity.definition.import.format.unknown': '未知格式 {{format}}',
+    'entity.definition.format.option.yaml': 'YAML',
+    'entity.definition.format.option.json': 'JSON',
+    'entity.definition.format.option.curl': 'cURL',
+    'entity.definition.workspace.template.custom': '自定义模板',
+    'entity.field.owner': '负责人',
+    'entity.field.system': '所属系统',
+    'entity.field.implemented-by': '由以下实现',
+    'entity.field.runbook': '处置手册',
+    'entity.definition.import.gap.telemetry': '遥测绑定',
+    'entity.definition.import.telemetry.ready': '已识别遥测绑定',
+    'entity.definition.import.telemetry.pending': '待补充遥测绑定',
+    'entity.definition.import.validation.ready': '可直接导入',
+    'entity.definition.import.validation.needs-attention': '导入前待补充',
+    'entity.definition.import.kind.unknown': '未知类型 {{kind}}',
+    'entity.definition.import.source.unknown': '未知来源 {{source}}',
+    'entity.definition.import.summary.ready': '可直接导入',
+    'entity.definition.import.summary.attention': '待补充',
+    'entity.definition.import.summary.telemetry': '缺少遥测绑定',
+    'entity.definition.import.queue.ready.title': '可直接导入',
+    'entity.definition.import.queue.ready.copy': '这批定义已经满足基本要求,可直接导入或保存。',
+    'entity.definition.import.queue.ready.action': '查看可直接导入项',
+    'entity.definition.import.queue.attention.title': '需要先补齐',
+    'entity.definition.import.queue.attention.copy': '这批定义还缺负责人、系统、处置手册或接口信息,先补充再导入。',
+    'entity.definition.import.queue.attention.action': '查看待补齐项',
+    'entity.definition.import.queue.telemetry.title': '导入后还需补充遥测',
+    'entity.definition.import.queue.telemetry.copy': '这批定义还没有遥测绑定,导入后仍需到遥测发现补充相关信息。',
+    'entity.definition.import.queue.telemetry.action': '查看缺遥测项',
+    'entity.definition.import.placeholder.yaml': '粘贴 YAML 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。',
+    'entity.definition.import.placeholder.json': '粘贴 JSON 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。',
+    'entity.definition.import.placeholder.curl': '粘贴从预览或自动化流程生成的 cURL 请求。',
+    'entity.definition.import.attribution.ready': '归因完整',
+    'entity.definition.import.attribution.missing': '归因缺失',
+    'entity.definition.import.attribution.review': '归因待确认',
+    'entity.definition.import.activity.status.success': '成功',
+    'entity.definition.import.activity.status.saved': '已保存',
+    'entity.definition.import.activity.status.starter': '初始',
+    'entity.definition.import.activity.status.failed': '失败',
+    'entity.definition.import.activity.status.preview': '预览',
+    'entity.definition.import.activity.status.unknown': '未知状态 {{status}}',
+    'entity.definition.import.activity.summary.telemetry-discovery-applied': '遥测发现已应用',
+    'entity.definition.import.activity.summary.catalog-entity-updated': '目录实体已更新',
+    'entity.definition.import.activity.summary.catalog-entity-created': '目录实体已创建',
+    'entity.definition.import.activity.field.endpoint': '端点',
+    'entity.definition.import.activity.field.service': '服务',
+    'entity.definition.import.activity.field.system': '系统',
+    'entity.definition.import.activity.field.source': '来源',
+    'entity.definition.import.activity.field.owner': '负责人',
+    'entity.definition.import.activity.field.environment': '环境',
+    'entity.definition.import.activity.field.evidence': '证据',
+    'entity.definition.import.activity.field.monitor-binds': '监控绑定',
+    'entity.definition.import.template.source.workspace': '工作区',
+    'entity.definition.import.template.source.saved': '已保存',
+    'entity.definition.import.template.source.starter': '初始',
+    'entity.definition.import.template.source.custom': '自定义',
+    'entity.definition.import.template.source.unknown': '未知来源 {{source}}',
+    'entities.new.loading': '正在加载实体草稿',
+    'entities.edit.loading': '正在加载实体编辑器',
+    'entities.definition.loading': '编辑实体定义',
+    'entities.import.loading': '导入实体定义',
+    'entities.discovery.loading': '遥测发现',
+    'entities.list.loading': '正在加载对象目录',
+    'entities.discovery.row.status.enabled': '已启用',
+    'entities.discovery.row.status.normal': '正常',
+    'entities.discovery.row.status.abnormal': '异常',
+    'entities.discovery.row.status.review': '待确认',
+    'entities.discovery.row.status.unknown': '未知状态 {{status}}',
+    'entities.discovery.row.activity.search-result': '搜索结果',
+    'entities.discovery.row.activity.catalog-preset': '目录预设',
+    'entities.discovery.row.missing.owner': '负责人',
+    'entities.discovery.row.missing.system': '系统',
+    'entities.discovery.row.missing.environment': '环境',
+    'entities.discovery.row.missing.separator': '、',
+    'entities.discovery.row.missing.copy': '缺少{{fields}}',
+    'entities.discovery.row.missing.ready': '归因上下文待确认',
+    'entities.discovery.row.attribution.preset.label': '目录预设',
+    'entities.discovery.row.attribution.preset.copy': '可作为候选确认基线',
+    'entities.discovery.row.attribution.preset.action': '查看预设',
+    'entities.discovery.row.attribution.resolved.label': '已归因',
+    'entities.discovery.row.attribution.resolved.copy': '已归入对象目录',
+    'entities.discovery.row.attribution.resolved.action': '打开实体',
+    'entities.discovery.row.attribution.merge.label': '建议归并',
+    'entities.discovery.row.attribution.merge.copy': '候选实体 {{candidate}}',
+    'entities.discovery.row.attribution.merge.action': '确认归并',
+    'entities.discovery.row.attribution.review.label': '归因待补齐',
+    'entities.discovery.row.attribution.review.action': '补齐归因',
+    'entities.discovery.row.attribution.create.label': '建议新建',
+    'entities.discovery.row.attribution.create.copy': '已有监控线索,可创建实体草稿',
+    'entities.discovery.row.attribution.create.action': '创建实体',
+    'entities.discovery.service-name': '{{name}} 服务',
+    'entities.discovery.facts.workspace': '工作区',
+    'entities.discovery.facts.presets': '预设',
+    'entities.discovery.facts.activities': '活动',
+    'entities.discovery.facts.owners': '负责人',
+    'entities.discovery.metrics.owners': '负责人',
+    'entities.discovery.metrics.systems': '系统',
+    'entities.discovery.metrics.environments': '环境',
+    'entities.discovery.metrics.preset-coverage': '预设覆盖',
+    'entities.discovery.metrics.ready': '就绪',
+    'entities.discovery.metrics.empty': '为空',
+    'entities.discovery.catalog.owners': '负责人',
+    'entities.discovery.catalog.systems': '系统',
+    'entities.discovery.catalog.environments': '环境',
+    'entities.discovery.catalog.count': '数量 {{count}}',
+    'entities.discovery.catalog.none': '无',
+    'entities.discovery.card.action.open-resolved': '打开已归因实体',
+    'entities.discovery.card.action.open-definition': '打开定义',
+    'entities.discovery.card.action.merge-suggested': '归并到建议实体',
+    'entities.discovery.card.action.open-suggested': '打开建议实体',
+    'entities.discovery.card.action.adopt-draft': '作为草稿采用',
+    'entities.discovery.card.action.review-governance': '复核治理',
+    'entities.discovery.card.action.send-definition': '送入定义工作台',
+    'entities.discovery.card.action.create-draft': '创建实体草稿',
+    'entities.discovery.card.recommendation.resolved': '该遥测已收口到目录,下一步打开已归因实体或定义工作台复核。',
+    'entities.discovery.card.recommendation.merge': '已存在匹配的目录候选,先归并再考虑新建草稿。',
+    'entities.discovery.card.recommendation.enrich': '先补齐共享治理上下文,再送入定义工作台或创建草稿。',
+    'entities.discovery.card.recommendation.create': '该遥测已有足够共享治理上下文,可直接打开新实体草稿。',
+    'entities.discovery.card.risk.low': '治理风险低',
+    'entities.discovery.card.risk.medium': '治理风险中',
+    'entities.discovery.card.risk.high': '治理风险高',
+    'entities.discovery.card.next.open': '下一步打开',
+    'entities.discovery.card.next.merge': '下一步归并',
+    'entities.discovery.card.next.enrich': '下一步补齐',
+    'entities.discovery.card.next.create': '下一步新建',
+    'entities.discovery.card.candidate-label': '建议实体 · {{candidate}} · 匹配强',
+    'entities.discovery.scope.all': '全部',
+    'entities.discovery.scope.matched': '已匹配',
+    'entities.discovery.scope.resolved': '已归因',
+    'entities.discovery.scope.suggested-new': '建议新建',
+    'entities.discovery.queue.review.title': '需要治理复核',
+    'entities.discovery.queue.review.summary': '{{count}} 条遥测结果在归并或新建前仍需治理复核。',
+    'entities.discovery.queue.review.action': '复核治理',
+    'entities.discovery.queue.merge.title': '可归并',
+    'entities.discovery.queue.merge.summary': '{{count}} 条遥测结果可立即选择归并。',
+    'entities.discovery.queue.merge.action': '选择可归并',
+    'entities.discovery.queue.create.title': '建议新建实体',
+    'entities.discovery.queue.create.summary': '{{count}} 条遥测结果可打开为新实体草稿或送入定义工作台。',
+    'entities.discovery.queue.create.action': '选择建议新建',
+    'entities.discovery.queue.resolved.title': '已收口',
+    'entities.discovery.queue.resolved.summary.present': '{{count}} 条遥测结果已映射到已收口目录实体。',
+    'entities.discovery.queue.resolved.summary.empty': '暂无发现结果标记为已收口。',
+    'entities.discovery.queue.resolved.action': '查看已收口',
+    'entities.discovery.bulk.tag.owner': '负责人',
+    'entities.discovery.bulk.tag.system': '系统',
+    'entities.discovery.bulk.preset.apply': '应用 {{name}}',
     'entity.health.label': '健康评分 {{score}}',
     'entity.health.copy.collected': '采集 {{healthy}} / {{total}} 健康',
     'entity.health.copy.waiting': '等待采集绑定',
     'entity.health.meta': '告警 {{alerts}} · 异常 {{anomalies}}',
-    'topology.identity': 'HertzBeat 企业运维拓扑',
-    'topology.kicker': '拓扑与影响面',
+    'topology.identity': '运维拓扑',
+    'topology.kicker': '拓扑',
     'topology.copy': '以实体中心为锚点,把 OTLP 调用、监控对象、模板依赖、K8s 工作负载、数据库/中间件连接和告警影响面收敛到同一张运维关系图。',
     'topology.refresh': '刷新拓扑',
     'topology.environment.aria': '选择环境',
@@ -970,14 +3287,121 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.search.aria': '拓扑筛选',
     'topology.search.placeholder': '搜索实体、服务、资源或标签',
     'topology.view.fit': '适配视图',
+    'topology.view.zoom-in': '放大',
+    'topology.view.zoom-out': '缩小',
+    'topology.view.focus-selected': '聚焦选中节点',
     'topology.locate-entity': '定位实体',
     'topology.current-filter': '当前筛选',
     'topology.all-entities': '全部实体',
+    'topology.state.label': '排障状态',
+    'topology.state.focus': '焦点',
+    'topology.state.depth': '深度',
+    'topology.state.depth.auto': '自动',
+    'topology.state.depth.one-hop': '1 跳',
+    'topology.state.depth.two-hop': '2 跳',
+    'topology.state.depth.n-hop': '{{depth}} 跳',
+    'topology.state.layout': '布局',
+    'topology.state.layout.layered-service': '分层服务',
+    'topology.state.layout.force': '力导向',
+    'topology.state.layout.grid-table': '网格 / 表格',
+    'topology.state.group': '分组',
+    'topology.state.group.none': '无',
+    'topology.state.group.environment': '环境',
+    'topology.state.group.source-kind': '证据来源',
+    'topology.state.group.entity-type': '实体类型',
+    'topology.toolbar.reset': '重置',
+    'topology.focus-trail.label': '聚焦拓扑',
+    'topology.focus-trail.exit': '退出聚焦',
+    'topology.focus-trail.hidden-count': '当前范围隐藏 {{count}} 个',
+    'topology.focus-trail.filter.environment': '环境',
+    'topology.focus-trail.filter.time-range': '时间范围',
+    'topology.focus-trail.filter.source': '来源',
+    'topology.focus-trail.filter.view': '视图',
+    'topology.focus-trail.source.all': '全部来源',
+    'topology.group-panel.title': '分组摘要',
+    'topology.group-panel.copy': '压缩高密度拓扑结果,同时保留最差健康状态和折叠范围。',
+    'topology.group-panel.group-by': '按类型 / 环境 / 来源分组',
+    'topology.group-panel.entity-type': '实体类型',
+    'topology.group-panel.environment': '环境',
+    'topology.group-panel.source-kind': '证据来源',
+    'topology.group-panel.node-count': '{{count}} 个可见节点',
+    'topology.group-panel.edge-count': '{{count}} 条可见边',
+    'topology.group-panel.collapsed-count': '折叠 {{count}} 个',
+    'topology.group-panel.active-scope': '当前范围',
+    'topology.group-panel.clear-group': '清除分组',
+    'topology.group-panel.open-table': '打开表格',
+    'topology.path-summary.title': '选中路径',
+    'topology.path-summary.source': '起点',
+    'topology.path-summary.target': '终点',
+    'topology.path-summary.relation': '关系',
+    'topology.path-summary.direction': '上游到下游',
+    'topology.path-summary.focus-path': '聚焦路径',
+    'topology.path-summary.open-trace': '打开链路',
     'topology.fault-context.title': '故障上下文',
     'topology.fault-context.copy': '来自当前入口的实体、链路和采集字段',
+    'topology.empty.api.title': '未找到拓扑关系',
+    'topology.empty.api.copy': '当前筛选下 API 没有返回实体、监控对象或链路调用关系证据。',
+    'topology.empty.trace-call.title': '暂无 trace 调用拓扑证据',
+    'topology.empty.trace-call.copy': 'Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。',
+    'topology.empty.trace-call.source': 'Greptime trace graph',
+    'topology.degraded.api.title': '拓扑 API 不可用',
+    'topology.degraded.api.copy': '当前范围无法加载 API、Greptime、链路或关系证据,拓扑图处于降级状态。',
+    'topology.degraded.api.source': '拓扑 API 不可用',
+    'topology.degraded.trace-call.title': 'trace 调用拓扑不可用',
+    'topology.degraded.trace-call.copy': '当前时间和环境下无法加载 Greptime trace 调用拓扑证据。拓扑图保持空态,避免展示过期或无关的关系。',
+    'topology.degraded.trace-call.source': 'Greptime trace graph',
+    'topology.loading.api.title': '正在加载拓扑证据',
+    'topology.loading.api.copy': '正在按当前范围加载 API、Greptime、链路和关系证据。',
     'topology.edge.evidence.aria': '{{label}} 关系证据',
     'topology.aside.view': '视图',
+    'topology.companion.jump.aria': '拓扑伴随栏分区',
+    'topology.companion.jump.view': '视图',
+    'topology.companion.jump.legend': '图例',
+    'topology.companion.jump.red': 'RED',
+    'topology.companion.jump.timeline': '时间线',
+    'topology.companion.jump.edge': '边',
+    'topology.companion.jump.node': '节点',
+    'topology.companion.section.collapse': '收起',
+    'topology.companion.section.expand': '展开',
     'topology.alert-impact.open': '打开告警影响面',
+    'topology.alert-impact.copy': '保留当前边、实体和三信号范围。',
+    'topology.legend.title': '图例',
+    'topology.legend.summary': '2 组',
+    'topology.legend.health': '健康状态',
+    'topology.legend.health.healthy': '健康',
+    'topology.legend.health.healthy-value': '正常',
+    'topology.legend.health.warning': '告警',
+    'topology.legend.health.warning-value': '关注',
+    'topology.legend.health.critical': '严重',
+    'topology.legend.health.critical-value': '处理',
+    'topology.legend.status': '状态',
+    'topology.legend.status.healthy': '正常',
+    'topology.legend.status.healthy-value': '正常',
+    'topology.legend.status.warning': '告警',
+    'topology.legend.status.warning-value': '关注',
+    'topology.legend.status.critical': '严重',
+    'topology.legend.status.critical-value': '处理',
+    'topology.legend.interaction': '交互',
+    'topology.legend.interaction.selected-node': '选中',
+    'topology.legend.interaction.selected-node-value': '选中',
+    'topology.legend.interaction.directional-edge': '方向边',
+    'topology.legend.interaction.directional-edge-value': '依赖方向',
+    'topology.legend.interaction.dimmed-edge': '弱化',
+    'topology.legend.interaction.dimmed-edge-value': '上下文外',
+    'topology.legend.source-kind': '证据来源',
+    'topology.legend.source.trace': '链路调用',
+    'topology.legend.source.trace-value': 'trace',
+    'topology.legend.source.monitor': '监控归属',
+    'topology.legend.source.monitor-value': 'monitor',
+    'topology.legend.source.relation': '实体关系',
+    'topology.legend.source.relation-value': 'relation',
+    'topology.legend.confidence': '可信度',
+    'topology.legend.confidence.live': '当前窗口证据',
+    'topology.legend.confidence.stale': '低可信 / 过期',
+    'topology.timeline.title': '影响时间线',
+    'topology.timeline.copy': '最近改变当前拓扑视图的实体、关系和监控对象证据。',
+    'topology.timeline.detail.fallback': '拓扑证据已变更',
+    'topology.timeline.actor.system': 'system',
     'topology.edge.evidence.title': '关系证据',
     'topology.edge.link.from-entity': '起点实体',
     'topology.edge.link.to-entity': '终点实体',
@@ -987,6 +3411,32 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.edge.link.traces': '链路证据',
     'topology.current-entity': '当前实体',
     'topology.current-entity.signals': '信号:{{signals}}',
+    'topology.current-entity.boundary': '信号跳转会保留当前实体、时间范围、环境和拓扑范围。',
+    'topology.current-entity.fact.entity-id': '实体 ID',
+    'topology.current-entity.fact.namespace': '命名空间',
+    'topology.current-entity.fact.signals': '信号',
+    'topology.current-entity.fact.health': '健康',
+    'topology.current-entity.fact.upstream': '上游',
+    'topology.current-entity.fact.downstream': '下游',
+    'topology.current-entity.fact.neighbor-count': '{{count}} 个依赖',
+    'topology.current-entity.fact.neighbor.none': '当前范围无依赖',
+    'topology.metric-table.title': '边指标排行',
+    'topology.metric-table.empty': '当前范围暂无关系指标',
+    'topology.metric-table.edge-count': '{{count}} 条边',
+    'topology.metric-table.request-rate-unit': '请求/秒',
+    'topology.metric-table.error-rate-unit': '错误',
+    'topology.metric-table.latency-p95-unit': 'P95',
+    'topology.metric-table.row-action': '检查',
+    'topology.metric-table.open-edge-aria': '打开拓扑边 {{edge}}',
+    'topology.metric-table.filter.all': '全部',
+    'topology.metric-table.filter.visible': '可见',
+    'topology.metric-table.filter.partial': '半可见',
+    'topology.metric-table.filter.hidden': '隐藏',
+    'topology.metric-table.filter.unknown': '未知',
+    'topology.red.request-rate': '请求速率',
+    'topology.red.error-rate': '错误率',
+    'topology.red.latency-p95': 'P95 延迟',
+    'topology.red.source.trace-service-graph': '链路派生服务图',
     'topology.context-link.entity': '查看实体详情',
     'topology.context-link.signals': '打开三信号',
     'topology.context-link.metrics': '指标工作台',
@@ -1031,12 +3481,16 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.evidence.source.alert-impact': '当前告警与服务/资源依赖的影响面投射',
     'topology.evidence.collector.otlp': 'OTLP Collector',
     'topology.evidence.collector.hertzbeat': 'HertzBeat Collector',
-    'topology.evidence.boundary': '当前边仅基于已采集的关系证据;依赖自动发现、变更时间线和根因分析仍是 roadmap 能力。',
+    'topology.evidence.boundary': '当前边仅基于已采集的关系证据;依赖自动发现和根因分析仍是 roadmap 能力。',
     'topology.evidence.alert-impact.copy': '带当前边、实体和三信号上下文进入告警影响面。',
     'topology.evidence.row.from': '起点实体',
     'topology.evidence.row.to': '终点实体',
     'topology.evidence.row.source': '采集证据',
     'topology.evidence.row.alert-impact': '告警影响',
+    'topology.hover.fact.relation-type': '关系类型',
+    'topology.hover.fact.first-seen': '首次出现',
+    'topology.hover.fact.last-seen': '最后出现',
+    'topology.hover.fact.sample-trace': '样例 Trace',
     'topology.evidence.alert-impact.none': '暂无影响',
     'topology.evidence.alert-impact.warning': '存在影响',
     'topology.evidence.alert-impact.critical': '严重影响',
@@ -1044,6 +3498,8 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'common.settings-mode': 'settings',
     'common.stable': 'stable',
     'common.updated': '更新时间',
+    'common.workspace': '工作区',
+    'common.workspace-navigation': '工作区导航',
     'label.bind': '标签',
     'menu.dashboard': '总览',
     'menu.section.ingestion': '接入采集',
@@ -1053,32 +3509,64 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'menu.section.dashboards': '仪表盘',
     'menu.section.settings': '平台设置',
     'menu.overview': '仪表盘',
+    'monitors.loading': '正在加载监控中心',
     'monitor.list': '监控列表',
     'monitor.scrape': '采集方式',
     'monitor.status.up': '正常',
     'monitor.status.down': '宕机',
     'monitor.status.paused': '暂停',
+    'monitors.status.up': '正常',
+    'monitors.status.down': '宕机',
+    'monitors.status.paused': '暂停',
+    'monitors.type.all': '全部类型',
     'monitor.edit-monitor': '编辑监控',
-    'monitor.editor.labels.add': 'Add label',
+    'monitor.scrape.type.static': '静态配置',
+    'monitor.scrape.type.http_sd': 'Http 服务发现',
+    'monitor.scrape.type.nacos_sd': 'Nacos 服务发现',
+    'monitor.scrape.type.dns_sd': 'Dns 服务发现',
+    'monitor.scrape.type.eureka_sd': 'Eureka 服务发现',
+    'monitor.scrape.type.consul_sd': 'Consul 服务发现',
+    'monitor.scrape.type.zookeeper_sd': 'Zookeeper 服务发现',
+    'entity.monitor.workbench.reason.down': '当前监控处于异常状态,先看它。',
+    'entity.monitor.workbench.reason.fallback': '当前没有异常监控,这里显示全部关联监控。',
+    'entity.monitor.workbench.reason.paused': '当前监控处于暂停状态,可确认是否应恢复。',
+    'entity.monitor.workbench.reason.default': '当前监控可用于继续核对覆盖面和最近状态。',
+    'entity.response.open-related-logs': '查看相关日志',
+    'entity.response.open-related-traces': '查看相关链路',
+    'entity.response.open-code': '打开代码',
+    'monitors.controls.more-actions': '更多监控操作',
+    'monitor.copy.action': '复制监控',
+    'monitor.copy.success': '复制监控成功',
+    'monitor.copy.failed': '复制监控失败',
+    'monitor.item.unavailable.refresh': '该监控项已不可用,列表将自动刷新',
+    'monitor.editor.labels.add': '添加标签',
     'monitor.editor.labels.copy': '以键值对形式管理监控标签。',
     'monitor.editor.labels.key': '标签键',
     'monitor.editor.labels.value': '标签值',
     'monitor.editor.labels.remove': '移除',
-    'monitor.editor.annotations.add': 'Add annotation',
+    'monitor.editor.annotations.add': '添加注解',
     'monitor.editor.annotations.copy': '以键值对形式管理监控注解。',
     'monitor.editor.annotations.key': '注解键',
     'monitor.editor.annotations.value': '注解值',
     'monitor.editor.annotations.remove': '移除',
-    'monitor.editor.validation.name': 'Monitor name is required',
+    'monitor.editor.validation.name': '请输入监控名称',
     'monitor.editor.validation.app': '请选择监控类型',
     'monitor.editor.validation.intervals': '请输入采集间隔',
-    'monitor.editor.validation.param-required': '{{field}} is required',
+    'monitor.editor.validation.param-required': '请填写 {{field}}',
+    'monitor.route-state.form.error.title': '监控表单加载失败',
+    'monitor.route-state.form.error.copy': '当前无法初始化监控表单,请稍后重试。',
+    'monitor.route-state.action.retry': '重试',
+    'monitor.route-state.loading.title': '正在加载监控详情',
+    'monitor.route-state.loading.copy': '请稍候,正在准备监控实例、指标摘要和历史趋势。',
+    'monitor.route-state.error.title': '监控详情加载失败',
+    'monitor.route-state.error.copy': '当前无法加载监控详情,请稍后重试或从监控列表重新打开。',
     'monitor.editor.loading.edit': '正在加载监控配置...',
     'monitor.editor.loading.new': '正在加载监控表单...',
     'monitor.editor.section.base': '基础配置',
     'monitor.editor.section.base.copy': '设置监控类型、目标和采集周期。',
     'monitor.editor.section.params': '运行参数',
     'monitor.editor.section.params.copy': '填写当前监控模板需要的运行参数。',
+    'monitor.editor.section.runtime': '采集调度',
     'monitor.editor.section.advanced.copy': '配置标签、注解和关联看板,用于筛选与后续联动。',
     'monitor.editor.section.payload': '采集载荷',
     'monitor.editor.section.payload.copy': '配置此监控使用的请求体或脚本载荷。',
@@ -1089,6 +3577,9 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.overview.kicker': 'Overview',
     'monitor.detail.title-fallback': '监控详情',
     'monitor.detail.open-grafana': '打开 Grafana',
+    'monitor.detail.auto-refresh': '自动刷新 {{time}} 秒',
+    'monitor.detail.close-refresh': '关闭自动刷新',
+    'monitor.detail.config-refresh': '设置 {{time}} 秒自动刷新',
     'monitor.detail.refresh-countdown': '即将刷新',
     'monitor.detail.refresh-disabled': '自动刷新关闭',
     'monitor.detail.status.up': '正常',
@@ -1133,6 +3624,7 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.grafana.enabled.meta': 'external dashboard',
     'monitor.detail.grafana.copy': '配置关联看板后可从这里打开 Grafana。',
     'monitor.detail.grafana.status': '看板状态',
+    'monitor.detail.grafana.configure': '配置看板',
     'monitor.detail.param.meta': 'runtime param',
     'monitor.detail.param.unknown': '参数',
     'monitor.detail.schedule.empty': '未配置周期',
@@ -1189,6 +3681,10 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitor.detail.favorite.catalog.title': 'Favorite catalog',
     'monitor.detail.favorite.remove': 'Remove favorite',
     'monitor.detail.favorite.add': 'Add favorite',
+    'monitor.favorite.add.success': '收藏成功',
+    'monitor.favorite.add.failed': '收藏失败',
+    'monitor.favorite.remove.success': '取消收藏成功',
+    'monitor.favorite.remove.failed': '取消收藏失败',
     'monitor.detail.metric.row-label': '行',
     'monitor.detail.metric.row': '第 {{index}} 行',
     'monitor.detail.metric.fullscreen.enter': 'Fullscreen',
@@ -1258,14 +3754,320 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'monitors.metric.paused': '当前页暂停',
     'monitors.empty-selected.title': '还未选择监控',
     'monitors.empty-selected.copy': '从列表选择监控实例后,这里会显示操作和上下文。',
-    'ops.surface.mode.entry': 'entry surface',
-    'ops.surface.shared-shell.title': 'Shared shell',
+    'ops.surface.mode.entry': '入口面',
+    'ops.surface.kicker': '操作员入口',
+    'ops.surface.route-contract.title': '路由契约',
+    'ops.surface.route-contract.copy': '共享壳层状态在入口阶段保持可见。',
+    'ops.surface.launch-checklist': '启动检查清单',
+    'ops.surface.route-posture': '路由姿态',
+    'ops.surface.entry-shell': '入口壳层就绪',
+    'ops.surface.fact.focus-label': '焦点',
+    'ops.surface.fact.mode-label': '模式',
+    'ops.surface.fact.mode-entry': '入口面',
+    'ops.surface.fact.signals-label': '信号',
+    'ops.surface.route-posture.mode-label': '模式',
+    'ops.surface.shared-shell.title': '共享壳层',
     'ops.surface.shared-shell.copy': '入口页沿用同一套壳层和信息层级。',
-    'ops.surface.focused-scope.title': 'Focused scope',
+    'ops.surface.focused-scope.title': '聚焦范围',
     'ops.surface.focused-scope.copy': '只承接导航、上下文和下一跳。',
+    'overview.loading': '正在加载概览控制台',
+    'overview.guidance.default.start': '下一步',
+    'overview.guidance.default.reasons': '原因',
+    'overview.guidance.default.next': '后续',
+    'overview.checklist.status.ready': '就绪',
+    'overview.checklist.status.pending': '待处理',
+    'overview.lane.entities.title': '实体目录',
+    'overview.lane.entities.eyebrow': '实体优先',
+    'overview.lane.entities.copy': '先从监控实体和服务归属定位影响范围,再进入信号排查。',
+    'overview.lane.entities.stat': '已纳管 {{count}} 个实体',
+    'overview.lane.entities.action': '打开实体目录',
+    'overview.lane.logs.title': '日志',
+    'overview.lane.logs.eyebrow': '运行证据',
+    'overview.lane.logs.copy': '用相同服务、实体和时间上下文查看日志事件。',
+    'overview.lane.logs.stat.available': '日志可用',
+    'overview.lane.logs.action': '打开日志',
+    'overview.lane.trace.title': '链路',
+    'overview.lane.trace.eyebrow': '请求路径',
+    'overview.lane.trace.copy': '存在链路上下文时跟进跨度,再回到实体证据。',
+    'overview.lane.trace.stat.distributed': '分布式链路就绪',
     'overview.lane.trace.stat.detected': '当前事件含链路 ID',
+    'overview.lane.trace.action': '打开链路',
+    'overview.lane.otlp.title': 'OTLP 指标',
+    'overview.lane.otlp.eyebrow': '三信号接入',
+    'overview.lane.otlp.copy': '在私有部署的 HertzBeat 工作区查看进入的指标序列。',
+    'overview.lane.otlp.stat': '已接入 {{count}} 个监控应用',
+    'overview.lane.otlp.action': '打开指标',
+    'explorer.kicker': '可观测',
+    'explorer.title': '查询工作台',
+    'explorer.subtitle': '统一检索链路、日志和指标结果,围绕查询栏、趋势带、表格和详情面板完成定位。',
+    'explorer.tags.unified-query': '统一查询',
+    'explorer.tags.context-retention': '上下文保留',
+    'explorer.tags.cross-signal-search': '跨信号检索',
+    'explorer.focus': '跨信号查询入口',
+    'explorer.summary': '保持冷启动工作台的紧凑密度,并保留进入 HertzBeat 链路、日志、指标和对象目录的排障跳转。',
+    'explorer.lanes.query.title': '查询栏',
+    'explorer.lanes.query.copy': '先捕获服务、状态和资源属性,再进入具体信号证据。',
+    'explorer.lanes.query.meta': '查询栏契约',
+    'explorer.lanes.results.title': '结果面板',
+    'explorer.lanes.results.copy': '把链路、日志和指标结果放在一张高密度表里对比。',
+    'explorer.lanes.results.meta': '结果面板',
+    'explorer.lanes.drilldown.title': '上下文跳转',
+    'explorer.lanes.drilldown.copy': '保留进入链路、日志、指标和对象目录的链接。',
+    'explorer.lanes.drilldown.meta': '上下文跳转',
+    'explorer.checklist.entry.title': '路由入口',
+    'explorer.checklist.entry.copy': '路由只拥有冷启动工作台入口,交互留在客户端岛内。',
+    'explorer.checklist.entry.meta': '已完成',
+    'explorer.checklist.adapters.title': '适配器',
+    'explorer.checklist.adapters.copy': '静态收敛完成后再接入真实后端查询适配器。',
+    'explorer.checklist.adapters.meta': '下一步',
+    'explorer.checklist.links.title': '信号链接',
+    'explorer.checklist.links.copy': '链路、日志、指标和对象目录跳转地址保持稳定。',
+    'explorer.checklist.links.meta': '预留',
+    'explorer.actions.save-view': '保存视图',
+    'explorer.actions.create-alert': '创建告警',
+    'explorer.actions.add-dashboard': '加入仪表盘',
+    'explorer.query.aria': '查询条件',
+    'explorer.query.placeholder': '输入服务、状态或资源属性过滤条件',
+    'explorer.query.default': 'service.name = "checkout" AND status = "错误"',
+    'explorer.query.run': '运行查询',
+    'explorer.signal.aria': '信号类型',
+    'explorer.signal.all': '全部信号',
+    'explorer.sort.aria': '排序方式',
+    'explorer.sort.time-desc': '时间倒序',
+    'explorer.sort.duration-desc': '耗时倒序',
+    'explorer.filters.title': '快速筛选',
+    'explorer.filters.clear': '清除',
+    'explorer.filters.signal-type': '信号类型',
+    'explorer.filters.deployment-environment': '部署环境',
+    'explorer.filters.service-name': '服务名称',
+    'explorer.filters.status': '状态',
+    'explorer.chart.title': '趋势带',
+    'explorer.chart.subtitle': '近 30 分钟跨信号命中量',
+    'explorer.chart.result-count': '结果 {{count}}',
+    'explorer.results.title': '跨信号结果',
+    'explorer.results.count': '{{count}} 条',
+    'explorer.table.signal-type': '信号类型',
+    'explorer.table.service': '服务',
+    'explorer.table.operation': '操作',
+    'explorer.table.status': '状态',
+    'explorer.table.duration': '耗时',
+    'explorer.table.time': '时间',
+    'explorer.detail.title': '详情面板',
+    'explorer.detail.signal': '信号',
+    'explorer.detail.status': '状态',
+    'explorer.detail.duration': '耗时',
+    'explorer.handoff.trace': '查看链路',
+    'explorer.handoff.log': '查看日志',
+    'explorer.handoff.metrics': '查看指标',
+    'explorer.handoff.entities': '对象目录',
+    'explorer.rows.trace.signal': '链路',
+    'explorer.rows.log.signal': '日志',
+    'explorer.rows.metric.signal': '指标',
+    'explorer.rows.exception.signal': '异常',
+    'explorer.rows.log.operation': '支付失败:余额不足',
+    'explorer.status.error': '错误',
+    'explorer.status.normal': '正常',
+    'actions.entry.kicker': '自动化入口',
+    'actions.entry.title': '自动化处置',
+    'actions.entry.subtitle': '按 OTLP 工作台的冷色基线统一入口、上下文和审批语义。',
+    'actions.entry.action.overview': '打开概览',
+    'actions.entry.action.entities': '查看对象',
+    'actions.entry.shell.eyebrow': '冷色入口已接入',
+    'actions.entry.shell.copy': '此入口继承 OTLP 接入页的近黑画布、小按钮、硬边面板和静态右栏节奏,后续只需要接入执行适配器。',
+    'actions.entry.chip.catalog': '自动化目录',
+    'actions.entry.chip.risk': '风险动作',
+    'actions.entry.chip.approval': '审批流',
+    'actions.entry.checklist.title': '接入清单',
+    'actions.entry.checklist.context.title': '统一入口上下文',
+    'actions.entry.checklist.context.copy': '已和 OTLP 基线共用同一套页面节奏。',
+    'actions.entry.checklist.adapter.title': '接入执行适配器',
+    'actions.entry.checklist.adapter.copy': '等动作目录和审批流数据落地后直接挂载。',
+    'actions.entry.checklist.evidence.title': '保留证据跳转',
+    'actions.entry.checklist.evidence.copy': '实体、告警和链路入口保持在同一操作上下文。',
+    'actions.entry.empty.title': '等待接入执行适配器',
+    'actions.entry.empty.copy': '视觉骨架已经统一到 OTLP 基线,后续数据接入可以增量完成。',
+    'actions.entry.suggested.title': '建议动作',
+    'actions.entry.suggested.copy': '来自告警、实体和三信号上下文,只生成建议,不自动执行。',
+    'actions.entry.suggested.evidence': '查看证据',
+    'actions.entry.suggested.confirm': '人工确认后执行',
+    'actions.approval-draft.title': '审批草稿适配器',
+    'actions.approval-draft.copy': '通过安全 BFF 从第一条建议动作生成人工审批草稿,不会执行动作。',
+    'actions.approval-draft.create': '生成审批草稿',
+    'actions.approval-draft.pending': '正在生成草稿',
+    'actions.approval-draft.success': '审批草稿已生成',
+    'actions.approval-draft.failed': '审批草稿生成失败',
+    'actions.approval-draft.disabled': '需要告警、实体或链路上下文后才能生成审批草稿。',
+    'actions.approval-draft-queue.title': '审批草稿队列',
+    'actions.approval-draft-queue.copy': '通过安全 BFF 读取审批草稿,让操作员在执行适配器出现前看到 manager 生命周期状态。',
+    'actions.approval-draft-queue.loading': '正在读取审批草稿',
+    'actions.approval-draft-queue.empty': '暂未从 manager 读取到审批草稿。',
+    'actions.approval-decision.title': '审批决策适配器',
+    'actions.approval-decision.copy': '通过安全 BFF 为草稿记录批准或拒绝决策,不会执行动作。',
+    'actions.approval-decision.approve': '批准草稿',
+    'actions.approval-decision.reject': '拒绝草稿',
+    'actions.approval-decision.pending': '正在记录决策',
+    'actions.approval-decision.success': '审批决策已记录',
+    'actions.approval-decision.failed': '审批决策记录失败',
+    'actions.approval-decision.disabled': '先生成审批草稿后才能记录决策。',
+    'actions.catalog-adapter.title': '动作目录适配器',
+    'actions.catalog-adapter.copy': '先读取只能生成人工审批草稿的动作目录,再让审批草稿引用后端持久目录。',
+    'actions.catalog-adapter.loading': '正在读取动作目录',
+    'actions.catalog-adapter.empty': '暂未读取到 manager 动作目录。',
+    'actions.adapter-boundary.label': '执行边界',
+    'actions.adapter-boundary.copy': '动作目录、执行历史和审批流当前是 roadmap 示例快照,还未接入执行适配器,不代表实时运行状态;告警上下文建议是当前证据生成的人工交接,不会自动执行。',
+    'actions.adapter-boundary.roadmap.workflow-automation': '工作流自动化',
+    'actions.adapter-boundary.roadmap.action-catalog': '动作目录',
+    'actions.adapter-boundary.roadmap.app-builder': '应用编排器',
+    'actions.adapter-boundary.roadmap.self-service-actions': '自助动作',
+    'actions.adapter-boundary.roadmap.approvals': '审批流',
+    'actions.adapter-boundary.roadmap.scripts': '脚本执行',
+    'actions.adapter-boundary.roadmap.runbook-orchestration': '处置手册编排',
+    'actions.snapshot.label': '示例快照',
+    'actions.suggestion.target.current-alert': '当前告警',
+    'actions.suggestion.evidence.source': '来源 {{value}}',
+    'actions.suggestion.evidence.signal': '信号 {{value}}',
+    'actions.suggestion.evidence.trace': 'trace {{value}}',
+    'actions.suggestion.evidence.alert-group': '告警组 {{value}}',
+    'actions.suggestion.evidence.current-alert': '当前告警证据',
+    'actions.suggestion.source.entity': '实体目录',
+    'actions.suggestion.source.topology': '拓扑',
+    'actions.suggestion.source.monitor': '监控中心',
+    'actions.suggestion.signal.metrics': '指标',
+    'actions.suggestion.signal.logs': '日志',
+    'actions.suggestion.signal.alerts': '告警',
+    'actions.suggestion.restart.title': '建议重启 {{target}}',
+    'actions.suggestion.restart.copy': '仅作为高风险恢复建议,执行前需要确认影响范围、回滚窗口和负责人。',
+    'actions.suggestion.silence.title': '建议为 {{target}} 创建临时静默',
+    'actions.suggestion.silence.copy': '用于短时降噪,先确认告警已接手并设置自动过期。',
+    'actions.suggestion.runbook.title': '建议查看 {{target}} 处置手册',
+    'actions.suggestion.runbook.copy': '先对齐实体、告警、链路和采集器证据,再决定是否执行动作。',
+    'actions.suggestion.manual-required': '只生成建议,人工确认后才能进入执行。',
+    'actions.catalog.restart.name': '重启 checkout 部署',
+    'actions.catalog.mute.name': '静默边缘饱和告警',
+    'incidents.entry.kicker': '事件入口',
+    'incidents.entry.title': '故障事件',
+    'incidents.entry.subtitle': '按 OTLP 工作台的冷色基线统一响应时间线、责任人和证据入口。',
+    'incidents.entry.action.overview': '打开概览',
+    'incidents.entry.action.entities': '查看对象',
+    'incidents.entry.shell.eyebrow': '冷色入口已接入',
+    'incidents.entry.shell.copy': '此入口继承 OTLP 接入页的近黑画布、小按钮、硬边面板和静态右栏节奏,后续只需要接入事件适配器。',
+    'incidents.entry.chip.entry': '事件入口',
+    'incidents.entry.chip.timeline': '响应时间线',
+    'incidents.entry.chip.owner-first': '责任人优先',
+    'incidents.entry.checklist.title': '接入清单',
+    'incidents.entry.checklist.context.title': '统一入口上下文',
+    'incidents.entry.checklist.context.copy': '已和 OTLP 基线共用同一套页面节奏。',
+    'incidents.entry.checklist.adapter.title': '接入事件适配器',
+    'incidents.entry.checklist.adapter.copy': '等事件列表、责任人和时间线数据落地后直接挂载。',
+    'incidents.entry.checklist.evidence.title': '保留证据跳转',
+    'incidents.entry.checklist.evidence.copy': '日志、链路和对象入口保持在同一响应上下文。',
+    'incidents.entry.empty.title': '等待接入事件适配器',
+    'incidents.entry.empty.copy': '视觉骨架已经统一到 OTLP 基线,后续数据接入可以增量完成。',
+    'incidents.subtitle': '响应时间线、责任人和下一步保持在同一操作上下文。',
+    'incidents.focus': '事件状态与响应交接',
+    'incidents.summary': '事件入口页在更深的事件域落地前,先保持响应状态、责任人和相关证据对齐。',
+    'incidents.tag.shell': '事件入口外壳',
+    'incidents.tag.timeline': '响应时间线',
+    'incidents.tag.owner-first': '责任人优先',
+    'incidents.metric.open': '打开事件',
+    'incidents.metric.critical': '严重事件',
+    'incidents.metric.mitigating': '缓解中',
+    'incidents.metric.ownership-queues': '责任队列',
+    'incidents.row.checkout.title': 'checkout 在 prod-ap 出现延迟突增',
+    'incidents.row.checkout.service': 'checkout',
+    'incidents.row.checkout.owner': 'checkout-oncall',
+    'incidents.row.checkout.blast-radius': '2 个区域',
+    'incidents.row.collector.title': '指标摄取的采集器队列延迟',
+    'incidents.row.collector.service': 'otlp-gateway',
+    'incidents.row.collector.owner': 'observability',
+    'incidents.row.collector.blast-radius': '仅指标',
+    'incidents.row.edge.title': '发布后边缘代理饱和',
+    'incidents.row.edge.service': 'edge-proxy',
+    'incidents.row.edge.owner': 'edge-sre',
+    'incidents.row.edge.blast-radius': 'cn-hz 入口',
+    'incidents.timeline.checkout.title': '09:08 开始缓解',
+    'incidents.timeline.checkout.copy': '回滚和重启动作已从告警证据包打开。',
+    'incidents.timeline.checkout.meta': 'checkout 延迟事件',
+    'incidents.timeline.collector.title': '08:33 指标重放已批准',
+    'incidents.timeline.collector.copy': '采集器积压降到重放阈值以下,交接仍留在同一操作栏。',
+    'incidents.timeline.collector.meta': '摄取事件',
+    'incidents.timeline.edge.title': '07:56 边缘饱和已升级',
+    'incidents.timeline.edge.copy': '操作员从监控详情将事件转到链路和日志证据。',
+    'incidents.timeline.edge.meta': '边缘事件',
+    'incidents.ownership.checkout.owner': 'checkout-oncall',
+    'incidents.ownership.checkout.queue': '主响应人',
+    'incidents.ownership.checkout.copy': '保持告警、链路和发布上下文绑定到当前缓解线程。',
+    'incidents.ownership.checkout.meta': '2 个活跃交接',
+    'incidents.ownership.observability.owner': 'observability',
+    'incidents.ownership.observability.queue': '信号验证',
+    'incidents.ownership.observability.copy': '关闭事件前验证重放安全性和摄取恢复。',
+    'incidents.ownership.observability.meta': '1 个活跃交接',
+    'incidents.ownership.edge.owner': 'edge-sre',
+    'incidents.ownership.edge.queue': '容量复核',
+    'incidents.ownership.edge.copy': '跟踪发布后回归和下一次回滚检查点。',
+    'incidents.ownership.edge.meta': '等待回滚说明',
+    'incidents.checklist.shell.title': '外壳已就绪',
+    'incidents.checklist.shell.copy': '事件路由已经复用工作台外壳和路由姿态。',
+    'incidents.checklist.shell.meta': '已完成',
+    'incidents.checklist.adapter.title': '下一步接适配器',
+    'incidents.checklist.adapter.copy': '事件列表、责任人状态和响应时间线可以挂到当前 API 边界。',
+    'incidents.checklist.adapter.meta': '下一步',
+    'incidents.checklist.drilldown.title': '保留下钻',
+    'incidents.checklist.drilldown.copy': '日志、链路和对象交接保留在后续扩展路径。',
+    'incidents.checklist.drilldown.meta': '预留',
+    'incidents.surface.kicker': '事件响应台',
+    'incidents.surface.handoff.logs.title': '日志证据',
+    'incidents.surface.handoff.logs.copy': '缓解过程需要最新载荷证据时跳转到日志。',
+    'incidents.surface.handoff.traces.title': '链路证据',
+    'incidents.surface.handoff.traces.copy': '保留以链路优先的延迟和依赖回归下钻。',
+    'incidents.surface.handoff.overview.title': '回到概览',
+    'incidents.surface.handoff.overview.copy': '让事件状态继续留在同一全局响应节奏中。',
+    'actions.suggestion.target.entity-id': '实体 {{entityId}}',
+    'actions.suggestion.source.alert': '告警中心',
+    'actions.suggestion.source.unknown': '未知来源 {{source}}',
+    'actions.suggestion.signal.traces': '链路',
+    'actions.suggestion.signal.unknown': '未知信号 {{signal}}',
+    'actions.risk.high': '高风险',
+    'actions.risk.medium': '中风险',
+    'actions.risk.low': '低风险',
+    'actions.run-status.awaiting-approval': '等待审批',
+    'actions.run-status.running': '运行中',
+    'actions.run-status.completed': '已完成',
+    'actions.approval-status.pending': '等待负责人决策',
+    'actions.approval-status.approved': '已批准',
     'passport.lock.error.required': '请输入密码',
-    'passport.lock.next-step': 'wire unlock validation if needed',
+    'passport.lock.status.interactive': '可解锁',
+    'passport.lock.next-step': '按需接入解锁校验',
+    'app.lock': '解除锁定',
+    'app.lock.placeholder': '输入任意解锁',
+    'app.passport.desc': '开源私有化企业运维可观测平台',
+    'app.passport.intro-1': '采集器、监控模板、实体、指标、日志和链路',
+    'app.passport.intro-2': '处理告警并关闭问题',
+    'about.title': 'HertzBeat 是一个开源可观测性平台。',
+    'about.help': '感谢使用 HertzBeat。以下链接可用于关注版本、反馈问题和安全升级。',
+    'about.copyright': 'Copyright © {{year}} | Apache HertzBeat™',
+    'about.not-show-next-login': '下次登录不再显示',
+    'about.github': 'GitHub',
+    'about.issue': 'Issue',
+    'about.pr': 'PR',
+    'about.discuss': '讨论',
+    'about.doc': '文档',
+    'about.upgrade': '升级',
+    'about.star': 'Star',
+    'about.point.1': '通过采集器接入应用、数据库、操作系统、中间件和网络设备指标,数据留在私有化部署内。',
+    'about.point.2': '接收 OTLP 日志,保留资源、实体、链路和告警上下文,便于从日志继续排障。',
+    'about.point.3': '统一处理阈值告警和外部告警,支持确认、静默、抑制、恢复和关闭。',
+    'about.point.4': '通过运维团队使用的通知渠道送达告警,并把处理记录留在 HertzBeat。',
+    'about.point.5': '用监控模板定义采集协议、指标和阈值,快速覆盖新的资源类型。',
+    'about.point.6': '按网络区域扩展 Collector 集群,支撑私有化、隔离采集和状态页。',
+    'app.login.login': '登录',
+    'app.login.message-need-credential': '请输入密码',
+    'app.login.message-need-identifier': '请输入用户名',
+    'app.login.tab-login-credentials': '登入 HertzBeat',
+    'app.passport.language-switch': '切换语言',
+    'app.login.password-hide': '隐藏密码',
+    'app.login.password-show': '显示密码',
+    'app.login.remember-me': '记住我',
     'passport.login.card.entry.title': '运维入口',
     'passport.login.card.entry.copy': '登录后继续查看资源、实体、遥测数据和告警。',
     'passport.login.card.shell.title': '私有化部署',
@@ -1273,24 +4075,312 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'passport.login.card.auth.title': '安全访问',
     'passport.login.card.auth.copy': '验证账号后再进入告警处理、通知和系统设置。',
     'passport.login.default-password-warning': '当前使用默认密码,建议登录后尽快修改。',
+    'passport.login.loading': '正在登录',
+    'passport.login.error.generic': '登录失败,请检查用户名或密码。',
+    'passport.login.error.with-status': '登录失败:{{status}}',
+    'settings.surface.title': '设置总览',
+    'settings.surface.copy': '把平台配置入口收拢到一个工作台。',
     'settings.surface.route-contract.title': '设置导航',
     'settings.surface.route-contract.copy': '从这里进入状态页、采集器、标签、插件、令牌和系统配置。',
+    'settings.surface.route-contract.meta': '路由契约',
     'settings.surface.api-contract.title': '运维配置',
     'settings.surface.api-contract.copy': '统一维护采集器、标签、插件、令牌、状态页和通知通道。',
+    'settings.surface.api-contract.meta': 'API 契约',
+    'settings.surface.fact.mode-label': '模式',
+    'settings.surface.fact.mode-value': '设置',
+    'settings.surface.fact.focus-label': '焦点',
+    'settings.surface.governance.title': '平台治理',
+    'settings.surface.governance.copy': '只展示当前已有入口;未来大域保留在路线图文档。',
+    'settings.surface.governance.group.users-permissions': '用户与权限',
+    'settings.surface.governance.group.api-access': 'API 访问',
+    'settings.surface.governance.group.notifications': '通知通道',
+    'settings.surface.governance.group.template-marketplace': '模板与插件',
+    'settings.surface.governance.group.mcp-ai-foundation': 'MCP 与 AI 基础',
+    'settings.surface.governance.copy.implemented-route': '已接入当前平台入口,继续沿用现有页面,不新增空菜单。',
+    'settings.surface.governance.copy.foundation-only': '保留当前基础能力入口,完整治理能力继续在路线图中推进。',
+    'settings.surface.governance.future.title': '未来大域边界',
+    'settings.surface.governance.future.copy': '{{domains}} 仅作为路线图能力规划,不展示为已上线应用入口。文档:{{docs}}',
+    'settings.surface.governance.future.meta': '仅规划',
+    'settings.surface.governance.future.separator': '、',
+    'settings.surface.governance.future.security': '安全治理',
+    'settings.surface.governance.future.data-observability': '数据可观测',
+    'settings.surface.governance.future.digital-experience': '数字体验',
+    'settings.surface.governance.future.software-delivery': '软件交付',
+    'settings.surface.governance.future.cloud-cost': '云成本',
+    'settings.surface.governance.future.ai-observability': 'AI 可观测',
+    'settings.surface.governance.future.developer-integrations': '开发者集成',
+    'settings.console.kicker': '设置',
+    'settings.console.title': '系统设置',
+    'settings.console.copy': '在同一控制台配置系统、消息服务、对象存储和令牌。',
+    'settings.console.navigation': '设置分区',
+    'settings.console.content': '设置内容',
+    'setting.labels.loading': '正在加载标签管理',
+    'setting.labels.copy': '统一管理监控标签名称、标签值和描述,用旧版一致的紧凑卡片网格承载检索、复制和编辑动作。',
+    'setting.labels.table.name': '标签名称',
+    'setting.labels.table.type': '标签类型',
+    'setting.labels.table.description': '标签描述',
+    'setting.labels.type.auto': '自动标签',
+    'setting.labels.type.user': '用户标签',
+    'setting.labels.type.preset': '预置标签',
+    'setting.labels.type.unknown': '未知标签类型',
+    'setting.labels.metric.auto-page': '自动标签/当前页',
+    'setting.labels.metric.user-page': '用户标签/当前页',
+    'setting.labels.metric.preset-page': '预置标签/当前页',
+    'setting.labels.empty.title': '暂无标签',
+    'setting.labels.empty.copy': '请先新增标签。',
+    'setting.labels.action.copy-aria': '复制标签 {{name}}',
+    'setting.labels.action.edit-aria': '编辑标签 {{name}}',
+    'setting.labels.action.delete-aria': '删除标签 {{name}}',
+    'setting.plugins.loading': '加载插件管理',
+    'setting.plugins.copy': '管理自定义插件、执行类型和启用状态,保持上传、筛选、批量删除和参数编辑在同一个紧凑工作面中完成。',
+    'setting.plugins.selected-count': '已选择 {{count}}',
+    'setting.plugins.table.params': '参数',
+    'setting.plugins.metric.enabled-page': '当前页启用插件',
+    'setting.plugins.metric.disabled-page': '当前页停用插件',
+    'setting.plugins.metric.params-page': '当前页参数项',
+    'setting.plugins.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'setting.plugins.pagination.page-size': '每页条数',
+    'setting.plugins.pagination.page': '页码',
+    'setting.plugins.row.item-fallback': '插件项 -',
+    'setting.plugins.row.params': '参数 {{count}}',
+    'setting.plugins.action.params-aria': '编辑插件参数 {{name}}',
+    'setting.plugins.action.delete-aria': '删除插件 {{name}}',
+    'setting.collector.loading': '正在加载采集集群',
+    'setting.collector.copy': '管理已注册采集器节点、运行状态和任务分布,保持部署、搜索、上下线和删除操作在同一个高密度后台工作面中完成。',
+    'setting.collector.selected-count': '已选择 {{count}}',
+    'collector.pagination.summary': '第 {{page}} / {{totalPages}} 页 · {{from}}-{{to}} / {{total}}',
+    'collector.pagination.page-size': '每页条数',
+    'collector.pagination.page': '页码',
+    'collector.confirm.offline': '请确认是否下线此采集器!',
+    'collector.confirm.offline-batch': '请确认是否批量下线采集器!',
+    'collector.confirm.online': '请确认是否上线此采集器!',
+    'collector.confirm.online-batch': '请确认是否批量上线采集器!',
+    'collector.notify.no-select-offline': '未选中任何待下线项!',
+    'collector.notify.no-select-online': '未选中任何待上线项!',
+    'collector.deploy': '部署采集器',
+    'collector.deploy.close': '关闭,已保存采集器身份令牌',
+    'collector.deploy.docker': '通过 Docker 方式部署',
+    'collector.deploy.docker.help': '# 在 Docker 环境下执行如下命令',
+    'collector.deploy.docker.help.1': '# docker run -d : 使用 Docker 在后台启动采集器容器',
+    'collector.deploy.docker.help.2': '# -e IDENTITY=xxx : 设置采集器身份令牌',
+    'collector.deploy.docker.help.3': '# -e MANAGER_HOST=127.0.0.1 : 设置采集器连接的 HertzBeat 服务地址',
+    'collector.deploy.docker.help.4': '# -e MODE=public : 配置运行模式:公共采集器集群或私有边缘采集器',
+    'collector.deploy.docker.help.5': '# --name hertzbeat-collector : 将容器命名为 hertzbeat-collector',
+    'collector.deploy.docker.help.6': '# apache/hertzbeat-collector : 官方采集器镜像,拉取超时时可替换为 quay.io/tancloud/hertzbeat-collector',
+    'collector.deploy.identity': '采集器身份令牌 (IDENTITY)',
+    'collector.deploy.identity.tip': '此身份令牌只对应当前采集器,请妥善保存。',
+    'collector.deploy.ok': '生成身份令牌和部署命令',
+    'collector.deploy.package': '通过安装包部署',
+    'collector.deploy.package.github': '从 GitHub 下载安装包',
+    'collector.deploy.package.help': '# 下载当前系统对应的安装包 hertzbeat-collector-xx.tar.gz',
+    'collector.deploy.package.help.1': '# 解压后修改 hertzbeat-collector/config/application.yml',
+    'collector.deploy.package.help.2': '# 按示例替换文件中的 IDENTITY、MANAGER_HOST、MODE 参数。',
+    'collector.deploy.package.help.3': '# 运行 bin/startup.sh 或 startup.bat (Windows) 启动采集器进程。',
+    'collector.name.placeholder': '请配置待部署的采集器名称,注意需保证唯一性',
+    'common.button.copy.tip': '点击复制',
+    'common.copy.button': '复制',
+    'setting.define.loading': '加载定义中心',
+    'setting.define.title': '定义',
+    'setting.define.subtitle': '管理监控模板 YAML,直接维护采集协议、参数和指标字段,保持搜索、编辑、保存和模板显示状态在同一个高密度工作面中完成。',
+    'setting.define.action.new': '新增类型',
+    'setting.define.action.save-apply': '保存并应用',
+    'setting.define.action.delete': '删除 {{app}}',
+    'define.save-apply.confirm': '请确认是否保存修改并应用此监控类型定义? 这会影响到您的监控内容。',
+    'define.delete.confirm': '请确认是否删除 {{app}} 监控类型? 删除后无法添加此类监控。',
+    'define.hide-true.confirm': '确认是否显示此菜单?',
+    'define.hide-false.confirm': '确认是否隐藏此菜单?',
+    'define.save-apply.no-code': '监控类型定义内容不能为空。',
+    'setting.define.action.hide': '隐藏',
+    'setting.define.action.hide-aria': '隐藏模板 {{app}}',
+    'setting.define.action.show': '显示',
+    'setting.define.action.show-aria': '显示模板 {{app}}',
+    'setting.define.save.pending': '正在应用定义...',
+    'setting.define.skeleton.name': 'hertzbeat-baseline-alert',
+    'setting.define.skeleton.summary': 'HertzBeat 基线告警定义草稿',
+    'setting.define.current-yaml.fallback': 'current-definition',
+    'setting.define.current-delete.fallback': '当前定义',
+    'setting.define.item.fallback': '未命名定义',
+    'setting.define.empty.title': '暂无模板',
+    'setting.define.empty.copy': '请先新增监控模板 YAML。',
+    'setting.define.empty.search-title': '没有匹配的模板',
+    'setting.define.empty.search-copy': '调整搜索关键字后再试。',
+    'setting.define.empty-selected.title': '未选择定义',
+    'setting.define.empty-selected.copy': '从左侧列表选择一条定义。',
+    'setting.define.preview.action': '预览查询',
+    'setting.define.preview.title': '定义预览',
+    'setting.define.preview.success': '预览完成',
+    'setting.define.preview.failed': '预览失败',
+    'setting.define.save.success': '定义已保存',
+    'setting.define.save.failed': '保存定义失败',
+    'setting.define.datasource.title': '数据源状态',
+    'setting.define.fact.datasource': '数据源',
+    'setting.define.fact.selected-template': '当前模板',
+    'setting.define.fact.hidden': '隐藏模板',
+    'setting.define.new-template': '新增模板草稿',
+    'setting.define.template.visible': '可见',
+    'setting.define.template.hidden': '隐藏',
+    'setting.define.summary.yaml-lines': '{{count}} 行 YAML',
+    'setting.define.summary.mode.new': '新增',
+    'setting.define.summary.mode.edit': '编辑',
+    'setting.define.diff.original': '原始 YAML',
+    'setting.define.diff.current': '当前编辑 YAML',
+    'setting.define.row.meta': '周期 {{period}} 秒 · 更新 {{time}}',
+    'alert.setting.type.realtime.metric': '指标实时',
+    'alert.setting.type.periodic.metric': '指标周期',
+    'alert.setting.type.realtime.log': '日志实时',
+    'alert.setting.type.periodic.log': '日志周期',
+    'alert.setting.type.periodic.trace': '链路周期',
+    'setting.status.loading': '正在加载状态页设置',
     'setting.status.validation.name': '组织名称为必填项',
     'setting.status.validation.description': '组织描述为必填项',
     'setting.status.validation.component-name': '组件名称为必填项',
     'setting.status.validation.component-description': '组件描述为必填项',
+    'setting.status.title': '状态页设置',
+    'setting.status.org.title': '组织档案',
+    'setting.status.org.copy': '配置公开状态页展示的组织信息。',
+    'setting.status.org.fallback': '未命名状态组织',
+    'setting.status.org.state': '组织状态',
+    'setting.status.org.feedback': '反馈入口',
+    'setting.status.components.title': '组件',
+    'setting.status.incidents.title': '事件',
+    'setting.status.delete.component.title': '确认删除组件',
+    'setting.status.delete.component.copy': '删除后该组件会从公开状态页移除,相关维护事件需要重新绑定组件。',
+    'setting.status.delete.incident.title': '确认删除维护事件',
+    'setting.status.delete.incident.copy': '删除后该维护事件会从公开状态页移除,历史说明不会再展示给访问者。',
+    'setting.status.delete.confirm': '确认删除',
     'setting.status.incidents.state': '事件状态',
     'setting.status.incidents.components': '关联组件',
+    'setting.status.component.method.placeholder': '0 / 1',
+    'setting.status.component.state.placeholder': '0 / 1 / 2',
+    'setting.status.component.labels.placeholder': 'env:prod, app:api',
+    'setting.status.component.description.placeholder': '公开状态页的组件说明',
+    'setting.status.incident.state.placeholder': '0 / 1 / 2 / 3',
+    'setting.status.incident.components.placeholder': '1, 2',
+    'monitors.delete.title': '确认删除监控',
+    'monitors.delete.copy': '删除后选中的监控对象会从监控目录移除,相关实体绑定和告警排查入口需要重新归并。',
+    'monitors.delete.confirm': '确认删除',
+    'monitors.delete.selected': '已选择 {{count}} 个监控对象',
+    'monitors.selected-count': '已选择 {{count}} 个监控对象',
     'setting.settings.menu.copy': '系统、存储、消息和令牌设置',
+    'setting.settings.menu.open-action': '打开{{title}}',
     'setting.settings.config.title': '系统配置',
     'setting.settings.server.title': '消息服务器',
+    'settings.server': '消息服务配置',
+    'settings.server.email': '邮件服务器',
+    'settings.server.sms': '短信配置',
+    'settings.server.email.setting': '配置邮件服务器',
+    'settings.server.sms.setting': '配置短信参数',
+    'settings.server.summary.configure-action': '配置{{title}}',
+    'alert.notice.sender.mail.host': '邮箱服务器地址',
+    'alert.notice.sender.mail.port': '邮箱端口',
+    'alert.notice.sender.mail.username': '邮箱账号',
+    'alert.notice.sender.mail.password': '邮箱密码',
+    'alert.notice.sender.mail.ssl': '是否启用SSL',
+    'alert.notice.sender.mail.starttls': '是否启用STARTTLS',
+    'alert.notice.sender.mail.enable': '是否启用邮箱配置',
+    'alert.notice.sender.sms.type': '短信类型',
+    'alert.notice.sender.sms.type.tencent': '腾讯短信',
+    'alert.notice.sender.sms.type.alibaba': '阿里短信',
+    'alert.notice.sender.sms.type.unisms': '合一短信(UniSMS)',
+    'alert.notice.sender.sms.type.smslocal': '当地短信(Smslocal)',
+    'alert.notice.sender.sms.type.aws': 'Aws Sms',
+    'alert.notice.sender.sms.type.twilio': 'Twilio Sms',
+    'alert.notice.sender.sms.tencent.secretId': '腾讯短信SecretId',
+    'alert.notice.sender.sms.tencent.secretKey': '腾讯短信SecretKey',
+    'alert.notice.sender.sms.tencent.signName': '腾讯短信SignName',
+    'alert.notice.sender.sms.tencent.appId': '腾讯短信AppId',
+    'alert.notice.sender.sms.tencent.templateId': '腾讯短信TemplateId',
+    'alert.notice.sender.sms.alibaba.accessKeyId': '阿里短信AccessKeyId',
+    'alert.notice.sender.sms.alibaba.accessKeySecret': '阿里短信AccessKeySecret',
+    'alert.notice.sender.sms.alibaba.signName': '阿里短信SignName',
+    'alert.notice.sender.sms.alibaba.templateCode': '阿里短信TemplateCode',
+    'alert.notice.sender.sms.unisms.accessKeyId': 'UniSMS AccessKeyId',
+    'alert.notice.sender.sms.unisms.authMode': 'UniSMS 认证模式',
+    'alert.notice.sender.sms.unisms.accessKeySecret': 'UniSMS AccessKeySecret',
+    'alert.notice.sender.sms.unisms.signature': 'UniSMS 签名',
+    'alert.notice.sender.sms.unisms.templateId': 'UniSMS TemplateId',
+    'alert.notice.sender.sms.unisms.authMode.hmac': 'HMAC',
+    'alert.notice.sender.sms.unisms.authMode.simple': 'Simple',
+    'alert.notice.sender.sms.smslocal.apiKey': 'Smslocal ApiKey',
+    'alert.notice.sender.sms.aws.accessKeyId': 'AWS AccessKeyId',
+    'alert.notice.sender.sms.aws.accessKeySecret': 'AWS AccessKeySecret',
+    'alert.notice.sender.sms.aws.region': 'AWS Region',
+    'alert.notice.sender.sms.twilio.accountSid': 'Twilio AccountSid',
+    'alert.notice.sender.sms.twilio.authToken': 'Twilio AuthToken',
+    'alert.notice.sender.sms.twilio.twilioPhoneNumber': 'Twilio Phone Number',
     'setting.settings.object-store.title': '对象存储',
+    'setting.settings.object-store.loading': '正在加载文件服务配置。',
+    'setting.settings.server.loading': '正在加载消息服务配置。',
+    'settings.object-store': '文件服务配置',
+    'settings.object-store.type': '文件服务提供商',
+    'settings.object-store.type.database': '本地数据库(默认)',
+    'settings.object-store.type.file': '本地文件',
+    'settings.object-store.type.obs': '华为云 OBS',
+    'settings.object-store.obs.accessKey': 'AccessKey',
+    'settings.object-store.obs.secretKey': 'SecretKey',
+    'settings.object-store.obs.bucketName': 'Bucket',
+    'settings.object-store.obs.endpoint': 'Endpoint',
+    'settings.object-store.obs.savePath': '保存路径',
+    'settings.object-store.obs.accessKey.placeholder': '华为云 AccessKeyId',
+    'settings.object-store.obs.secretKey.placeholder': '华为云 AccessKeySecret',
+    'settings.object-store.obs.bucketName.placeholder': '华为云 OBS 中创建的 Bucket 名称',
+    'settings.object-store.obs.endpoint.placeholder': '华为云 OBS 地域域名,不包括 Bucket 名',
+    'settings.object-store.obs.savePath.placeholder': '备份文件保存路径,默认是 hertzbeat',
+    'settings.system-config': '系统配置',
+    'settings.system-config.locale': '系统语言',
+    'settings.system-config.timezone': '系统时区',
+    'settings.system-config.theme': '系统主题',
+    'settings.system-config.locale.en_US': '英语(en_US)',
+    'settings.system-config.locale.zh_CN': '简体中文(zh_CN)',
+    'settings.system-config.locale.zh_TW': '繁體中文(zh_TW)',
+    'settings.system-config.locale.ja-JP': '日语(ja_JP)',
+    'settings.system-config.locale.pt_BR': '葡萄牙语(pt_BR)',
+    'settings.system-config.theme.default': '默认主题',
+    'settings.system-config.theme.dark': '深色主题',
+    'settings.system-config.theme.compact': '紧凑主题',
+    'setting.settings.config.loading': '正在加载系统配置。',
+    'settings.system-config.ok': '确认更新',
+    'setting.object-store.fact.type': '类型',
+    'setting.object-store.fact.bucket': 'Bucket',
+    'setting.object-store.fact.endpoint': 'Endpoint',
+    'settings.token.delete-action': '删除{{name}}',
     'setting.token.fact.total': '令牌总数',
     'setting.token.fact.active': '可用令牌',
     'setting.token.fact.expired': '过期令牌',
+    'setting.token.metric.total': '令牌总数',
+    'setting.token.metric.active': '可用令牌',
+    'setting.token.metric.expired': '过期令牌',
+    'setting.token.row.creator': '创建人 {{creator}}',
+    'setting.token.row.state.active': '可用',
+    'setting.token.row.state.expired': '已过期',
     'setting.token.title': '令牌管理',
+    'setting.token.loading': '正在加载令牌管理。',
+    'settings.token': '令牌管理',
+    'settings.token.console.kicker': 'API 密钥',
+    'settings.token.console.title': '统一管理遥测和自动化令牌',
+    'settings.token.console.copy': '在同一页查看令牌状态、最近使用情况和撤销入口。',
+    'settings.token.console.result.total': '令牌总数',
+    'settings.token.console.result.active': '可用令牌',
+    'settings.token.console.result.expired': '过期令牌',
+    'settings.token.generate': '生成令牌',
+    'settings.token.generate-title': '生成令牌',
+    'settings.token.generated-title': '令牌已生成',
+    'settings.token.generate-fail': '生成令牌失败',
+    'settings.token.name-placeholder': '请输入令牌名称',
+    'settings.token.notice': '请立即复制此令牌。关闭弹窗后将不再显示完整令牌。',
+    'settings.token.delete-confirm': '确认删除令牌',
+    'settings.token.delete-confirm-content': '删除后使用此令牌的所有集成将无法访问,确认删除「{{name}}」?',
+    'settings.token.load-fail-title': '令牌列表暂时无法加载',
+    'settings.token.load-fail': '暂时无法读取令牌列表,请稍后重试或刷新页面。',
+    'settings.token.desc': '管理用于 API 集成访问的长期令牌,可随时创建或撤销。',
+    'settings.token.name': '令牌名称',
+    'settings.token.value': '令牌值',
+    'settings.token.creator': '创建者',
+    'settings.token.create-time': '创建时间',
+    'settings.token.expire-time': '过期时间',
+    'settings.token.expire.never': '永不过期',
+    'settings.token.last-used': '最近使用时间',
+    'settings.token.empty': '还没有令牌',
     'setting.token.empty.title': '还没有令牌',
     'setting.token.empty.copy': '生成一个 API 令牌后即可接入采集、通知或自动化脚本。',
     'setting.token.expiration.never': '永不过期',
@@ -1299,6 +4389,13 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'setting.token.expiration.90d': '90 天',
     'setting.token.expiration.180d': '180 天',
     'setting.token.expiration.365d': '365 天',
+    'status.loading': '正在加载状态页',
+    'status.page': '状态页面',
+    'status.org.title': '组织信息',
+    'status.org.copy': '发布公开组织信息,说明这个状态页由谁维护。',
+    'status.org.empty': '状态页组织',
+    'status.components.title': '组件状态',
+    'status.incidents.title': '事件历史',
     'status.metric.components': 'components',
     'status.metric.operational': 'operational',
     'status.metric.incidents': 'incidents',
@@ -1309,19 +4406,160 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'status.tone.down': 'down',
     'status.incident.status': 'status',
     'status.org.meta': '公开组织',
+    'status.component.state.0': '正常',
+    'status.component.state.1': '异常',
+    'status.component.state.2': '未知',
+    'status.incident.history': '事件历史',
+    'status.incident.state.0': '调查中',
+    'status.incident.state.1': '已确认',
+    'status.incident.state.2': '观察中',
+    'status.incident.state.3': '已恢复',
+    'status.incident.message.tip.0': '我们正在调查此事件问题,请稍后再查看更新。',
+    'status.incident.message.tip.1': '我们已确认故障发生的原因,正在处理中。',
+    'status.incident.message.tip.2': '我们正在监控该事件,观察处理效果',
+    'status.incident.message.tip.3': '该事件已被修复,谢谢理解支持。',
+    'status.public.org.state.0': '所有系统正常运行',
+    'status.public.org.state.1': '部分系统运行异常',
+    'status.public.org.state.2': '所有系统运行异常',
     'status.public.history': '历史',
     'status.public.home': '主页',
+    'status.public.feedback': '问题反馈',
     'status.public.updated': '更新于',
     'status.public.year': '年份',
+    'status.public.uptime': '可用率',
     'status.public.component-history': '组件历史',
     'status.public.incident-range': '事件范围',
+    'status.public.incidents.load-failed': '加载公开事件失败',
+    'status.public.today': '今天',
+    'status.public.30-day': '30 天',
+    'status.public.power-by': '由 HertzBeat 提供支持',
+    'status.public.to-incident': '事故历史',
+    'status.public.to-component': '状态页面',
+    'status.incident.public.start-at': '开始时间',
+    'status.incident.public.update-at': '更新时间',
     'status.components.view': '组件健康视图',
+    'trace.manage.loading': '正在加载链路工作台',
     'trace.manage.stat.error-ratio': '错误链路占比',
     'trace.manage.stat.activity': '活动状态',
     'trace.manage.stat.list-count': '列表条数',
+    'trace.center.console.title': '链路工作台',
+    'trace.manage.workspace': '链路工作台',
     'trace.manage.payloads.title': '原始载荷',
+    'trace.manage.empty-selected-span.title': '选择一个跨度',
+    'trace.manage.empty-selected-span.copy': '选择左侧跨度后查看服务、状态和事件证据。',
     'trace.manage.selected-span.service-namespace': '服务与命名空间',
     'trace.manage.trace-state-empty': '暂无链路状态',
+    'trace.manage.event.fallback': '未命名事件',
+    'trace.manage.event.attributes.empty': '无属性',
+    'trace.manage.event.attributes.more': '还有 {{count}} 项',
+    'trace.manage.event.attributes.object': '对象属性',
+    'trace.manage.link.fallback': '关联链路',
+    'trace.manage.link.meta': '关联链路',
+    'trace.manage.attribution-diagnostics.aria':
+      '归因诊断 hertzbeat.entity_id hertzbeat.entity_name hertzbeat.workspace_id hertzbeat.collector hertzbeat.template',
+    'trace.manage.attribution-diagnostics.title': '归因诊断',
+    'trace.manage.attribution-diagnostics.present': '已提供',
+    'trace.manage.attribution-diagnostics.missing': '缺失',
+    'trace.manage.attribution-diagnostics.entity-id.present': '可打开实体详情',
+    'trace.manage.attribution-diagnostics.entity-id.missing': '缺少实体 ID,实体详情会保持禁用',
+    'trace.manage.attribution-diagnostics.entity-name.present': '用于展示实体名称',
+    'trace.manage.attribution-diagnostics.entity-name.missing': '缺少实体名称时使用服务名辅助检索',
+    'trace.manage.attribution-diagnostics.workspace-id.present': '工作区归属',
+    'trace.manage.attribution-diagnostics.workspace-id.missing': '缺少工作区字段时使用当前部署上下文',
+    'trace.manage.attribution-diagnostics.collector.present': '采集器来源',
+    'trace.manage.attribution-diagnostics.collector.missing': '缺少采集器字段时使用链路服务上下文排查',
+    'trace.manage.attribution-diagnostics.template.present': '监控模板归属',
+    'trace.manage.attribution-diagnostics.template.missing': '缺少模板字段时从实体或服务补齐模板归属',
+    'trace.manage.route.fallback.filtered-trace': '过滤链路',
+    'trace.manage.route.fallback.waiting-query': '等待查询',
+    'trace.manage.route.fallback.unselected': '未选择',
+    'trace.manage.route.kicker': '可观测',
+    'trace.manage.route.title': '链路工作台',
+    'trace.manage.route.subtitle': '按服务、链路 ID、跨度 ID 和错误状态筛选链路,直接查看趋势、列表和详情。',
+    'trace.manage.route.action.return-source': '返回来源',
+    'trace.manage.route.action.intake': '返回 OTLP 接入',
+    'trace.manage.route.action.collectors': '采集集群',
+    'trace.manage.route.action.entity': '实体详情',
+    'trace.manage.route.action.alerts': '告警处理',
+    'trace.manage.route.action.templates': '监控模板',
+    'trace.manage.route.action.catalog': '对象目录',
+    'trace.manage.route.query.service': '服务名称',
+    'trace.manage.route.query.trace-id': '链路 ID',
+    'trace.manage.route.query.span-id': '跨度 ID',
+    'trace.manage.route.query.status': '链路状态',
+    'trace.manage.route.query.status.all': '全部链路',
+    'trace.manage.route.query.status.error': '错误链路',
+    'trace.manage.route.query.run': '运行查询',
+    'trace.manage.route.query.reset': '重置',
+    'trace.manage.route.stat.total': '链路总数',
+    'trace.manage.route.stat.errors': '错误链路',
+    'trace.manage.route.stat.list-count': '列表条数',
+    'trace.manage.route.stat.latest': '最近时间',
+    'trace.manage.route.chart.title': '趋势带',
+    'trace.manage.route.chart.latest-window': '近 10 条链路',
+    'trace.manage.route.table.title': '最近链路',
+    'trace.manage.route.table.subtitle': '按当前查询条件展示',
+    'trace.manage.route.table.count': '{{count}} 条',
+    'trace.manage.route.table.header.start': '开始时间',
+    'trace.manage.route.table.header.service': '服务',
+    'trace.manage.route.table.header.root-span': '根跨度',
+    'trace.manage.route.table.header.duration': '耗时',
+    'trace.manage.route.table.header.status': '状态',
+    'trace.manage.route.table.header.trace-id': '链路 ID',
+    'trace.manage.route.empty.title': '暂无链路',
+    'trace.manage.route.empty.copy': '确认时间范围、实体归因、采集器和监控模板后再查看链路。',
+    'trace.manage.route.detail.title': '详情面板',
+    'trace.manage.route.detail.service': '服务',
+    'trace.manage.route.detail.namespace': '命名空间',
+    'trace.manage.route.detail.root-span': '根跨度',
+    'trace.manage.route.detail.duration': '耗时',
+    'trace.manage.route.evidence.aria': '链路证据 链路 ID 链路状态 开始时间 最近上报',
+    'trace.manage.route.evidence.title': '链路证据',
+    'trace.manage.route.evidence.current': '当前选中',
+    'trace.manage.route.evidence.trace-id': '链路 ID',
+    'trace.manage.route.evidence.trace-status': '链路状态',
+    'trace.manage.route.evidence.error-only': '仅看错误链路',
+    'trace.manage.route.evidence.current-filter': '当前筛选',
+    'trace.manage.route.evidence.start-time': '开始时间',
+    'trace.manage.route.evidence.root-span-start': '根跨度开始',
+    'trace.manage.route.evidence.latest-observed': '最近上报',
+    'trace.manage.route.evidence.latest-workbench-trace': '工作台最新链路',
+    'trace.manage.route.entity-context.aria': '实体上下文 当前实体 监控实例 当前服务 链路上下文 当前环境 时间范围 采集来源',
+    'trace.manage.route.entity-context.title': '实体上下文',
+    'trace.manage.route.handoff.alert-hint': '按当前实体、服务和已带入的链路上下文查看相关告警',
+    'trace.manage.route.handoff.signal-hint': '当前链路的 traceId、spanId 和服务上下文会带入日志与指标工作台',
+    'trace.manage.drawer.event-prefix': '事件',
+    'trace.manage.drawer.link-prefix': '关联',
+    'trace.manage.drawer.event-fact.title': '跨度事件',
+    'trace.manage.drawer.event-fact.fallback': '未命名事件',
+    'trace.manage.drawer.event-fact.meta': '不是新的跨度,是当前跨度上的时间点',
+    'trace.manage.drawer.event-fact.span': '所属跨度',
+    'trace.manage.drawer.event-fact.position': '事件位置',
... 852 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 27/50: feat(manager): add entity workbench backend models

Posted by zh...@apache.org.
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 16dd2828b4c4390f47abebaa94363e9af7522936
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 10:14:20 2026 +0800

    feat(manager): add entity workbench backend models
    
    Validation:
    
    - ./mvnw -pl hertzbeat-manager -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests validate
    
    - ./mvnw -pl hertzbeat-manager -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests clean compile
    
    - ./mvnw -pl hertzbeat-observability,hertzbeat-manager -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests compile
    
    - ./mvnw -pl hertzbeat-observability,hertzbeat-manager -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dtest='ObserveEntityServiceTest,MonitorServiceTest,MonitorControllerTest,MonitorsControllerTest,ManagerObservabilityWorkspaceQueryGatewayTest,ManagerObservabilityInventoryQueryServiceTest,*Entity*Test,*OldMonitor*Test,*TraceCallTopology*Test,ActionCatalogServiceTest,ActionApprovalDraftServiceTest,TopologyControllerTest, [...]
    
    - git diff --cached --check
    
    - cached trailing-whitespace scan
---
 .../controller/ActionApprovalDraftController.java  |   96 +
 .../controller/ActionCatalogController.java        |   75 +
 .../manager/controller/MonitorController.java      |   62 +-
 .../manager/controller/MonitorsController.java     |  119 +-
 .../manager/controller/TopologyController.java     |   76 +
 .../manager/dao/EntityDefinitionActivityDao.java   |    2 +
 .../manager/dao/EntityGovernanceStateDao.java      |    9 +
 .../hertzbeat/manager/dao/ObserveEntityDao.java    |    9 +
 .../ManagerObservabilityInventoryQueryService.java |   52 +
 .../ManagerObservabilityWorkspaceQueryGateway.java |   42 +-
 .../dto/ActionApprovalDraftDecisionInfo.java}      |   39 +-
 .../manager/pojo/dto/ActionApprovalDraftInfo.java  |   63 +
 .../dto/ActionCatalogItemInfo.java}                |   46 +-
 .../hertzbeat/manager/pojo/dto/EntityInfo.java     |    5 +
 .../manager/pojo/dto/EntityTopologyGraphInfo.java  |  168 +
 .../service/action/ActionApprovalDraftService.java |  264 ++
 .../service/action/ActionCatalogService.java       |  191 +
 .../service/entity/EntityActivityQueryService.java |  119 +
 .../entity/EntityActivityReadModelService.java     |   84 +
 .../EntityActivityRecordWriteModelService.java     |   80 +
 .../entity/EntityActivityWriteModelService.java    |  313 ++
 .../entity/EntityAlertEvidenceQueryService.java    |  193 ++
 .../EntityAlertEvidenceReadModelService.java       |  189 +
 .../entity/EntityCatalogProfileService.java        |  152 +
 .../service/entity/EntityCatalogQueryService.java  |  203 ++
 .../entity/EntityCoreWriteModelService.java        |  145 +
 ...efinitionDocumentFieldNormalizationService.java |   99 +
 .../EntityDefinitionDocumentParserService.java     |  144 +
 .../EntityDefinitionDocumentRendererService.java   |  308 ++
 .../entity/EntityDefinitionDraftService.java       |   89 +
 .../entity/EntityDefinitionExportService.java      |   49 +
 ...ityDefinitionExtensionNormalizationService.java |   65 +
 ...ityDefinitionHertzbeatNormalizationService.java |  217 ++
 .../entity/EntityDefinitionMappingService.java     |  517 +++
 ...tityDefinitionMetadataNormalizationService.java |  360 ++
 .../EntityDefinitionNormalizationService.java      |   87 +
 ...tityDefinitionRelationNormalizationService.java |  249 ++
 .../EntityDefinitionSpecNormalizationService.java  |  199 ++
 ...ityDefinitionTelemetryNormalizationService.java |  209 ++
 .../EntityDefinitionTypeResolverService.java       |  136 +
 .../entity/EntityDeletionWriteModelService.java    |   56 +
 .../EntityDetailObservabilityReadModelService.java |  169 +
 .../entity/EntityDetailReadModelService.java       |   67 +
 .../entity/EntityEvidenceReadModelService.java     |   78 +
 .../entity/EntityGovernanceStateQueryService.java  |   73 +
 .../entity/EntityGovernanceStateService.java       |  661 ++++
 .../EntityGovernanceStateWriteModelService.java    |   94 +
 .../entity/EntityGovernanceWorkflowService.java    |   96 +
 .../service/entity/EntityIdentityQueryService.java |   53 +
 .../entity/EntityIdentityReadModelService.java     |   48 +
 .../entity/EntityIdentityResolutionService.java    |  288 ++
 .../entity/EntityIdentityWriteModelService.java    |  184 +
 .../entity/EntityIntegrationHintService.java       |   48 +
 .../service/entity/EntityListReadModelService.java |  101 +
 .../entity/EntityMonitorBindQueryService.java      |   52 +
 .../service/entity/EntityMonitorBindService.java   |  128 +
 .../entity/EntityMonitorBindWriteModelService.java |   58 +
 .../EntityMonitorEvidenceReadModelService.java     |   87 +
 .../service/entity/EntityMonitorQueryService.java  |   65 +
 .../entity/EntityMutationWorkflowService.java      |  177 +
 .../entity/EntityNoiseControlReadModelService.java |  277 ++
 .../entity/EntityNoiseControlRuleQueryService.java |  127 +
 .../service/entity/EntityRelationQueryService.java |   44 +
 .../service/entity/EntityRelationService.java      |  290 ++
 .../entity/EntityRelationWriteModelService.java    |   49 +
 .../EntityResponseHandoffReadModelService.java     |   82 +
 .../service/entity/EntityRuntimeHealthService.java |   96 +
 .../EntityRuntimeHealthWriteModelService.java}     |   21 +-
 .../service/entity/EntityStatusRefreshService.java |   79 +
 .../entity/EntitySummaryReadModelService.java      |  114 +
 .../service/entity/EntityTopologyQueryService.java |  900 +++++
 .../service/entity/EntityValidationService.java    |   83 +
 .../entity/EntityWorkspaceAccessService.java       |  184 +
 .../entity/EntityWorkspaceQueryService.java        |   80 +
 .../OldMonitorAlertBindWriteModelService.java}     |   27 +-
 .../entity/OldMonitorCatalogQueryService.java      |   65 +
 .../entity/OldMonitorCatalogWriteModelService.java |   56 +
 .../OldMonitorCollectorBindQueryService.java       |   57 +
 .../OldMonitorCollectorBindWriteModelService.java  |   65 +
 .../entity/OldMonitorCollectorQueryService.java}   |   27 +-
 .../entity/OldMonitorLabelWriteModelService.java   |   52 +
 .../service/entity/OldMonitorPageQueryService.java |  121 +
 .../entity/OldMonitorParamQueryService.java}       |   28 +-
 .../entity/OldMonitorParamWriteModelService.java   |   52 +
 ...nitorServiceDiscoveryBindWriteModelService.java |   50 +
 ...OldMonitorServiceDiscoveryExpansionService.java |   59 +
 .../entity/OldMonitorStatusWriteModelService.java  |   81 +
 .../service/entity/TraceCallTopologyEdgeInfo.java  |   45 +
 .../entity/TraceCallTopologyQueryService.java      |  722 ++++
 .../entity/TraceCallTopologyReadModel.java}        |   19 +-
 .../manager/service/impl/AppServiceImpl.java       |    8 +-
 .../manager/service/impl/MonitorServiceImpl.java   |  295 +-
 .../service/impl/ObserveEntityServiceImpl.java     | 3642 +-------------------
 .../MonitorRouteAuthorizationConfigTest.java       |  169 +
 .../manager/controller/MonitorControllerTest.java  |   90 +
 .../manager/controller/MonitorsControllerTest.java |  223 +-
 .../manager/controller/TopologyControllerTest.java |   98 +
 ...agerObservabilityInventoryQueryServiceTest.java |   70 +
 ...abilityWorkspaceGatewaySourceOwnershipTest.java |   63 +
 ...ilityWorkspaceInventorySourceOwnershipTest.java |   54 +
 ...agerObservabilityWorkspaceQueryGatewayTest.java |   73 +-
 .../manager/service/MonitorServiceTest.java        |  297 +-
 .../manager/service/ObserveEntityServiceTest.java  | 1034 +++++-
 .../action/ActionApprovalDraftServiceTest.java     |  236 ++
 .../service/action/ActionCatalogServiceTest.java   |  164 +
 ...ityActivityEntityLookupSourceOwnershipTest.java |   64 +
 ...ActivityMonitorEvidenceSourceOwnershipTest.java |   58 +
 .../entity/EntityActivityQueryServiceTest.java     |  180 +
 .../EntityActivityQuerySourceOwnershipTest.java    |   77 +
 .../entity/EntityActivityReadModelServiceTest.java |  149 +
 .../EntityActivityRecordWriteModelServiceTest.java |   92 +
 ...tityActivityRecordWriteSourceOwnershipTest.java |   71 +
 ...tivityTimelineWorkspaceSourceOwnershipTest.java |   75 +
 .../EntityActivityWriteModelServiceTest.java       |  176 +
 .../EntityAlertEvidenceQueryServiceTest.java       |  224 ++
 ...ntityAlertEvidenceQuerySourceOwnershipTest.java |   75 +
 .../EntityAlertEvidenceReadModelServiceTest.java   |  220 ++
 .../entity/EntityCatalogProfileServiceTest.java    |  108 +
 .../entity/EntityCatalogQueryServiceTest.java      |   93 +
 ...logSuggestionsWorkspaceSourceOwnershipTest.java |   63 +
 .../entity/EntityCoreWriteModelServiceTest.java    |  294 ++
 ...itionDocumentFieldNormalizationServiceTest.java |  100 +
 ...umentFieldNormalizationSourceOwnershipTest.java |   77 +
 .../EntityDefinitionDocumentParserServiceTest.java |  137 +
 ...ntityDefinitionDocumentRendererServiceTest.java |  135 +
 .../entity/EntityDefinitionDraftServiceTest.java   |  134 +
 ...efinitionDraftWorkspaceSourceOwnershipTest.java |   57 +
 .../entity/EntityDefinitionExportServiceTest.java  |   86 +
 ...finitionExportWorkspaceSourceOwnershipTest.java |   49 +
 ...efinitionExtensionNormalizationServiceTest.java |   78 +
 ...nExtensionNormalizationSourceOwnershipTest.java |   68 +
 ...efinitionHertzbeatNormalizationServiceTest.java |   94 +
 ...nHertzbeatNormalizationSourceOwnershipTest.java |   79 +
 .../entity/EntityDefinitionMappingServiceTest.java |  294 ++
 ...DefinitionMetadataNormalizationServiceTest.java |   91 +
 ...onMetadataNormalizationSourceOwnershipTest.java |   75 +
 .../EntityDefinitionNormalizationServiceTest.java  |  144 +
 ...DefinitionRelationNormalizationServiceTest.java |  126 +
 ...onRelationNormalizationSourceOwnershipTest.java |   90 +
 .../EntityDefinitionSourceOwnershipTest.java       |  120 +
 ...tityDefinitionSpecNormalizationServiceTest.java |  104 +
 ...nitionSpecNormalizationSourceOwnershipTest.java |   68 +
 ...efinitionTelemetryNormalizationServiceTest.java |  112 +
 ...nTelemetryNormalizationSourceOwnershipTest.java |   78 +
 .../EntityDefinitionTypeResolverServiceTest.java   |   60 +
 ...yDefinitionTypeResolverSourceOwnershipTest.java |   70 +
 ...ntityDeletionCatalogRowSourceOwnershipTest.java |   52 +
 .../EntityDeletionCleanupSourceOwnershipTest.java  |   69 +
 .../EntityDeletionWriteModelServiceTest.java       |  104 +
 ...tityDetailChildEvidenceSourceOwnershipTest.java |   68 +
 ...ntityDetailEntityAccessSourceOwnershipTest.java |   64 +
 ...tityDetailFacadeHandoffSourceOwnershipTest.java |   55 +
 ...ityDetailObservabilityReadModelServiceTest.java |  259 ++
 ...lObservabilityWorkspaceSourceOwnershipTest.java |   59 +
 .../entity/EntityDetailReadModelServiceTest.java   |  143 +
 ...tyDetailRuntimeEvidenceSourceOwnershipTest.java |   65 +
 .../EntityEvidenceFacadeSourceOwnershipTest.java   |   61 +
 .../entity/EntityEvidenceReadModelServiceTest.java |  126 +
 ...EntityEvidenceWorkspaceSourceOwnershipTest.java |   65 +
 .../EntityFacadeWiringSourceOwnershipTest.java     |   69 +
 .../EntityFacadeWorkspaceSourceOwnershipTest.java  |   52 +
 .../EntityGovernanceStateQueryServiceTest.java     |  135 +
 ...ityGovernanceStateQuerySourceOwnershipTest.java |  120 +
 .../entity/EntityGovernanceStateServiceTest.java   |  230 ++
 ...EntityGovernanceStateWriteModelServiceTest.java |  122 +
 .../EntityGovernanceWorkflowServiceTest.java       |  100 +
 ...ntityGovernanceWorkflowSourceOwnershipTest.java |   49 +
 ...rnanceWorkflowWorkspaceSourceOwnershipTest.java |   57 +
 ...yIdentityCandidateMatchSourceOwnershipTest.java |   68 +
 ...ntityCandidateWorkspaceSourceOwnershipTest.java |   66 +
 ...tityIdentityMonitorBindSourceOwnershipTest.java |   55 +
 .../entity/EntityIdentityQueryServiceTest.java     |   94 +
 .../EntityIdentityQuerySourceOwnershipTest.java    |   69 +
 ...ityIdentityReadEvidenceSourceOwnershipTest.java |   77 +
 .../entity/EntityIdentityReadModelServiceTest.java |   97 +
 .../EntityIdentityResolutionServiceTest.java       |  181 +
 .../EntityIdentityWriteModelServiceTest.java       |  159 +
 .../entity/EntityIntegrationHintServiceTest.java   |   96 +
 ...ntegrationHintWorkspaceSourceOwnershipTest.java |   57 +
 .../EntityListCatalogQuerySourceOwnershipTest.java |   81 +
 .../entity/EntityListReadModelServiceTest.java     |  158 +
 ...tityListRuntimeEvidenceSourceOwnershipTest.java |   64 +
 .../EntityListWorkspaceSourceOwnershipTest.java    |   61 +
 .../entity/EntityMonitorBindQueryServiceTest.java  |   87 +
 .../EntityMonitorBindQuerySourceOwnershipTest.java |   63 +
 .../entity/EntityMonitorBindServiceTest.java       |  161 +
 .../EntityMonitorBindWriteModelServiceTest.java    |   99 +
 ...tyMonitorBindWriteModelSourceOwnershipTest.java |   60 +
 .../EntityMonitorEvidenceReadModelServiceTest.java |  114 +
 .../entity/EntityMonitorQueryServiceTest.java      |   93 +
 .../EntityMonitorQuerySourceOwnershipTest.java     |   60 +
 .../entity/EntityMutationWorkflowServiceTest.java  |  215 ++
 .../EntityMutationWorkflowSourceOwnershipTest.java |   62 +
 ...tationWorkflowWorkspaceSourceOwnershipTest.java |   56 +
 .../EntityNoiseControlReadModelServiceTest.java    |  198 ++
 .../EntityNoiseControlRuleQueryServiceTest.java    |  134 +
 ...tyNoiseControlRuleQuerySourceOwnershipTest.java |   67 +
 .../entity/EntityRelationQueryServiceTest.java     |   67 +
 .../EntityRelationQuerySourceOwnershipTest.java    |   60 +
 ...elationRequestWorkspaceSourceOwnershipTest.java |   51 +
 .../service/entity/EntityRelationServiceTest.java  |  195 ++
 ...RelationTargetWorkspaceSourceOwnershipTest.java |   62 +
 .../EntityRelationWriteModelServiceTest.java       |   67 +
 ...ntityRelationWriteModelSourceOwnershipTest.java |   60 +
 .../EntityResponseHandoffReadModelServiceTest.java |  185 +
 .../entity/EntityRuntimeHealthServiceTest.java     |  165 +
 .../EntityRuntimeHealthWriteModelServiceTest.java  |   64 +
 ...RuntimeHealthWriteModelSourceOwnershipTest.java |   73 +
 .../entity/EntityStatusRefreshServiceTest.java     |  116 +
 .../EntityStatusRefreshSourceOwnershipTest.java    |   73 +
 ...itySummaryCountEvidenceSourceOwnershipTest.java |   68 +
 ...mmaryDefinitionActivitySourceOwnershipTest.java |   58 +
 .../entity/EntitySummaryReadModelServiceTest.java  |  167 +
 .../entity/EntityTopologyQueryServiceTest.java     |  462 +++
 .../entity/EntityValidationServiceTest.java        |  140 +
 .../EntityValidationSourceOwnershipTest.java       |   59 +
 .../entity/EntityWorkspaceAccessServiceTest.java   |  289 ++
 .../entity/EntityWorkspaceQueryServiceTest.java    |  161 +
 .../EntityWorkspaceQuerySourceOwnershipTest.java   |   80 +
 .../OldMonitorAlertBindWriteModelServiceTest.java  |   66 +
 ...itorAlertBindWriteModelSourceOwnershipTest.java |   61 +
 .../entity/OldMonitorCatalogQueryServiceTest.java  |  112 +
 .../OldMonitorCatalogQuerySourceOwnershipTest.java |   78 +
 .../OldMonitorCatalogWriteModelServiceTest.java    |   95 +
 ...onitorCatalogWriteModelSourceOwnershipTest.java |   92 +
 ...MonitorCollectorBindAddSourceOwnershipTest.java |   59 +
 ...lectorBindDeleteCleanupSourceOwnershipTest.java |   57 +
 ...itorCollectorBindModifySourceOwnershipTest.java |   62 +
 .../OldMonitorCollectorBindQueryServiceTest.java   |   97 +
 ...nitorCollectorBindQuerySourceOwnershipTest.java |   69 +
 ...dMonitorCollectorBindWriteModelServiceTest.java |  116 +
 .../OldMonitorCollectorQueryServiceTest.java       |   71 +
 ...ldMonitorCollectorQuerySourceOwnershipTest.java |   56 +
 ...nitorDeletionEntityBindSourceOwnershipTest.java |   60 +
 .../OldMonitorLabelWriteModelServiceTest.java      |   87 +
 ...dMonitorLabelWriteModelSourceOwnershipTest.java |   74 +
 .../entity/OldMonitorPageQueryServiceTest.java     |   92 +
 .../OldMonitorPageQuerySourceOwnershipTest.java    |   60 +
 ...nitorParamDeleteCleanupSourceOwnershipTest.java |   51 +
 .../entity/OldMonitorParamQueryServiceTest.java    |   65 +
 .../OldMonitorParamQuerySourceOwnershipTest.java   |   66 +
 .../OldMonitorParamWriteModelServiceTest.java      |   90 +
 ...dMonitorParamWriteModelSourceOwnershipTest.java |   66 +
 ...iceDiscoveryBindCleanupSourceOwnershipTest.java |   67 +
 ...rServiceDiscoveryBindWriteModelServiceTest.java |   72 +
 ...onitorServiceDiscoveryExpansionServiceTest.java |   80 +
 ...rviceDiscoveryExpansionSourceOwnershipTest.java |   83 +
 .../OldMonitorStatusWriteModelServiceTest.java     |  112 +
 ...MonitorStatusWriteModelSourceOwnershipTest.java |  106 +
 .../entity/TraceCallTopologyQueryServiceTest.java  |  472 +++
 .../impl/AppServiceStartupGuardSourceTest.java     |   44 +
 .../service/impl/TelemetryIntakeServiceImpl.java   |   11 +-
 252 files changed, 29736 insertions(+), 3931 deletions(-)

diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ActionApprovalDraftController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ActionApprovalDraftController.java
new file mode 100644
index 0000000000..8a99bbb2d5
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ActionApprovalDraftController.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.controller;
+
+import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.dto.Message;
+import org.apache.hertzbeat.manager.pojo.dto.ActionApprovalDraftDecisionInfo;
+import org.apache.hertzbeat.manager.pojo.dto.ActionApprovalDraftInfo;
+import org.apache.hertzbeat.manager.service.action.ActionApprovalDraftService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Non-executing action approval draft controller.
+ */
+@Slf4j
+@RestController
+@RequestMapping(value = "/api/actions/approval-drafts", produces = {APPLICATION_JSON_VALUE})
+public class ActionApprovalDraftController {
+
+    private final ActionApprovalDraftService actionApprovalDraftService;
+
+    public ActionApprovalDraftController(ActionApprovalDraftService actionApprovalDraftService) {
+        this.actionApprovalDraftService = actionApprovalDraftService;
+    }
+
+    @PostMapping
+    @Operation(summary = "Create action approval draft", description = "Create a non-executing manual approval draft")
+    public ResponseEntity<Message<ActionApprovalDraftInfo>> createApprovalDraft(
+            @RequestBody ActionApprovalDraftInfo request,
+            @Parameter(description = "Workspace boundary") @RequestParam(required = false) String workspaceId) {
+        try {
+            return ResponseEntity.ok(Message.success(
+                    actionApprovalDraftService.createApprovalDraft(request, workspaceId)));
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, e.getMessage()));
+        } catch (Exception e) {
+            log.error("create action approval draft error", e);
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "Create action approval draft error"));
+        }
+    }
+
+    @GetMapping
+    @Operation(summary = "List action approval drafts", description = "List recent non-executing approval drafts")
+    public ResponseEntity<Message<List<ActionApprovalDraftInfo>>> listApprovalDrafts(
+            @Parameter(description = "Result limit") @RequestParam(defaultValue = "8") Integer limit,
+            @Parameter(description = "Workspace boundary") @RequestParam(required = false) String workspaceId) {
+        return ResponseEntity.ok(Message.success(
+                actionApprovalDraftService.listApprovalDrafts(limit == null ? 8 : limit, workspaceId)));
+    }
+
+    @PostMapping("/{draftId}/decision")
+    @Operation(summary = "Decide action approval draft",
+            description = "Record a non-executing approve/reject decision for an approval draft")
+    public ResponseEntity<Message<ActionApprovalDraftDecisionInfo>> decideApprovalDraft(
+            @PathVariable String draftId,
+            @RequestBody ActionApprovalDraftDecisionInfo request,
+            @Parameter(description = "Workspace boundary") @RequestParam(required = false) String workspaceId) {
+        try {
+            return ResponseEntity.ok(Message.success(
+                    actionApprovalDraftService.decideApprovalDraft(draftId, request, workspaceId)));
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, e.getMessage()));
+        } catch (Exception e) {
+            log.error("decide action approval draft error", e);
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "Decide action approval draft error"));
+        }
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ActionCatalogController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ActionCatalogController.java
new file mode 100644
index 0000000000..a8957ae320
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ActionCatalogController.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.controller;
+
+import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.dto.Message;
+import org.apache.hertzbeat.manager.pojo.dto.ActionCatalogItemInfo;
+import org.apache.hertzbeat.manager.service.action.ActionCatalogService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Durable non-executing action catalog controller.
+ */
+@Slf4j
+@RestController
+@RequestMapping(value = "/api/actions/catalog", produces = {APPLICATION_JSON_VALUE})
+public class ActionCatalogController {
+
+    private final ActionCatalogService actionCatalogService;
+
+    public ActionCatalogController(ActionCatalogService actionCatalogService) {
+        this.actionCatalogService = actionCatalogService;
+    }
+
+    @PostMapping
+    @Operation(summary = "Save action catalog item", description = "Save a manual-approval-only action catalog item")
+    public ResponseEntity<Message<ActionCatalogItemInfo>> saveCatalogItem(
+            @RequestBody ActionCatalogItemInfo request,
+            @Parameter(description = "Workspace boundary") @RequestParam(required = false) String workspaceId) {
+        try {
+            return ResponseEntity.ok(Message.success(actionCatalogService.saveCatalogItem(request, workspaceId)));
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, e.getMessage()));
+        } catch (Exception e) {
+            log.error("save action catalog item error", e);
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "Save action catalog item error"));
+        }
+    }
+
+    @GetMapping
+    @Operation(summary = "List action catalog items", description = "List durable manual-approval-only catalog items")
+    public ResponseEntity<Message<List<ActionCatalogItemInfo>>> listCatalogItems(
+            @Parameter(description = "Result limit") @RequestParam(defaultValue = "8") Integer limit,
+            @Parameter(description = "Workspace boundary") @RequestParam(required = false) String workspaceId) {
+        return ResponseEntity.ok(Message.success(
+                actionCatalogService.listCatalogItems(limit == null ? 8 : limit, workspaceId)));
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorController.java
index e44ac75c4e..116d17d15d 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorController.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorController.java
@@ -34,6 +34,7 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
@@ -60,7 +61,7 @@ public class MonitorController {
         return ResponseEntity.ok(Message.success("Add success"));
     }
 
-    @PutMapping
+    @PutMapping(params = "!id")
     @Operation(summary = "Modify an existing monitoring application", description = "Modify an existing monitoring application")
     public ResponseEntity<Message<Void>> modifyMonitor(@Valid @RequestBody MonitorDto monitorDto) {
         // Verify request data
@@ -69,11 +70,46 @@ public class MonitorController {
         return ResponseEntity.ok(Message.success("Modify success"));
     }
 
+    @PutMapping(params = "id")
+    @Operation(summary = "Modify an existing monitoring application", description = "Modify an existing monitoring application")
+    public ResponseEntity<Message<Void>> modifyMonitorByQueryId(
+            @Parameter(description = "Legacy monitoring task ID", example = "6565463543") @RequestParam("id") final long id,
+            @RequestBody MonitorDto monitorDto) {
+        return modifyMonitorWithId(id, monitorDto);
+    }
+
+    @PutMapping(path = "/{id}")
+    @Operation(summary = "Modify an existing monitoring application", description = "Modify an existing monitoring application")
+    public ResponseEntity<Message<Void>> modifyMonitorByPathId(
+            @Parameter(description = "Monitoring task ID", example = "6565463543") @PathVariable("id") final long id,
+            @RequestBody MonitorDto monitorDto) {
+        return modifyMonitorWithId(id, monitorDto);
+    }
+
+    private ResponseEntity<Message<Void>> modifyMonitorWithId(final long id, MonitorDto monitorDto) {
+        Monitor monitor = monitorDto.getMonitor();
+        if (monitor != null) {
+            monitor.setId(id);
+            monitorDto.setMonitor(monitor);
+        }
+        return modifyMonitor(monitorDto);
+    }
+
     @GetMapping(path = "/{id}")
     @Operation(summary = "Obtain monitoring information based on monitoring ID", description = "Obtain monitoring information based on monitoring ID")
     public ResponseEntity<Message<MonitorDto>> getMonitor(
             @Parameter(description = "Monitoring task ID", example = "6565463543") @PathVariable("id") final long id) {
-        // Get monitoring information
+        return getMonitorById(id);
+    }
+
+    @GetMapping
+    @Operation(summary = "Obtain monitoring information based on monitoring ID", description = "Obtain monitoring information based on monitoring ID")
+    public ResponseEntity<Message<MonitorDto>> getMonitorByQueryId(
+            @Parameter(description = "Legacy monitoring task ID", example = "6565463543") @RequestParam("id") final long id) {
+        return getMonitorById(id);
+    }
+
+    private ResponseEntity<Message<MonitorDto>> getMonitorById(final long id) {
         MonitorDto monitorDto = monitorService.getMonitorDto(id);
         if (monitorDto == null) {
             return ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "Monitor not exist."));
@@ -86,7 +122,17 @@ public class MonitorController {
     @Operation(summary = "Delete monitoring application based on monitoring ID", description = "Delete monitoring application based on monitoring ID")
     public ResponseEntity<Message<Void>> deleteMonitor(
             @Parameter(description = "en: Monitor ID", example = "6565463543") @PathVariable("id") final long id) {
-        // delete monitor
+        return deleteMonitorById(id);
+    }
+
+    @DeleteMapping
+    @Operation(summary = "Delete monitoring application based on monitoring ID", description = "Delete monitoring application based on monitoring ID")
+    public ResponseEntity<Message<Void>> deleteMonitorByQueryId(
+            @Parameter(description = "en: Legacy monitor ID", example = "6565463543") @RequestParam("id") final long id) {
+        return deleteMonitorById(id);
+    }
+
+    private ResponseEntity<Message<Void>> deleteMonitorById(final long id) {
         Monitor monitor = monitorService.getMonitor(id);
         if (monitor == null) {
             return ResponseEntity.ok(Message.success("The specified monitoring was not queried, please check whether the parameters are correct"));
@@ -107,6 +153,16 @@ public class MonitorController {
     @PostMapping("/copy/{id}")
     @Operation(summary = "Copy Monitor", description = "Copy an existing monitor")
     public ResponseEntity<Message<Void>> copyMonitor(@PathVariable("id") final Long id) {
+        return copyMonitorById(id);
+    }
+
+    @PostMapping("/copy")
+    @Operation(summary = "Copy Monitor", description = "Copy an existing monitor")
+    public ResponseEntity<Message<Void>> copyMonitorByQueryId(@RequestParam("id") final Long id) {
+        return copyMonitorById(id);
+    }
+
+    private ResponseEntity<Message<Void>> copyMonitorById(final Long id) {
         try {
             monitorService.copyMonitor(id);
             return ResponseEntity.ok(Message.success("Copy monitor success"));
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorsController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorsController.java
index 7ed4fac2e6..ce2924c4a1 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorsController.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorsController.java
@@ -18,13 +18,16 @@
 package org.apache.hertzbeat.manager.controller;
 
 import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+import com.google.common.primitives.Longs;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import org.apache.hertzbeat.common.entity.dto.Message;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.observability.dto.entity.MonitorInfo;
@@ -32,10 +35,14 @@ import org.apache.hertzbeat.manager.service.MonitorService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
@@ -57,15 +64,20 @@ public class MonitorsController {
             description = "Obtain a list of monitoring information based on query filter items")
     public ResponseEntity<Message<Page<MonitorInfo>>> getMonitors(
             @Parameter(description = "Monitor ID", example = "6565463543") @RequestParam(required = false) final List<Long> ids,
+            @Parameter(description = "Legacy Monitor ID", example = "6565463543") @RequestParam(required = false) final String id,
             @Parameter(description = "Monitor Type", example = "linux") @RequestParam(required = false) final String app,
             @Parameter(description = "Monitor Status 0:no monitor,1:usable,2:disabled,9:all status", example = "1") @RequestParam(required = false) final Byte status,
             @Parameter(description = "Monitor Host support fuzzy query", example = "127.0.0.1") @RequestParam(required = false) final String search,
+            @Parameter(description = "Legacy monitor host fuzzy query", example = "127.0.0.1") @RequestParam(required = false) final String host,
             @Parameter(description = "Monitor labels ", example = "env:prod,instance:22") @RequestParam(required = false) final String labels,
             @Parameter(description = "Sort Field ", example = "name") @RequestParam(defaultValue = "gmtCreate") final String sort,
             @Parameter(description = "Sort mode eg:asc desc", example = "desc") @RequestParam(defaultValue = "desc") final String order,
             @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex,
             @Parameter(description = "Number of list pagination ", example = "8") @RequestParam(defaultValue = "8") int pageSize) {
-        Page<Monitor> monitorPage = monitorService.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
+        List<Long> effectiveIds = resolveMonitorIds(ids, id);
+        String effectiveSearch = StringUtils.hasText(search) ? search : host;
+        Page<Monitor> monitorPage = monitorService.getMonitors(
+                effectiveIds, app, effectiveSearch, status, sort, order, pageIndex, pageSize, labels);
         Page<MonitorInfo> responsePage = monitorPage == null ? Page.empty() : monitorPage.map(MonitorInfo::fromEntity);
         return ResponseEntity.ok(Message.success(responsePage));
     }
@@ -86,10 +98,13 @@ public class MonitorsController {
     @Operation(summary = "Delete monitoring items in batches according to the monitoring ID list",
             description = "Delete monitoring items in batches according to the monitoring ID list")
     public ResponseEntity<Message<Void>> deleteMonitors(
-            @Parameter(description = "Monitoring ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids
+            @Parameter(description = "Monitoring ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids,
+            @Parameter(description = "Legacy monitoring ID", example = "6565463543") @RequestParam(required = false) String id,
+            @Parameter(description = "Legacy monitoring ID JSON body", example = "[6565463543]") @RequestBody(required = false) List<Long> bodyIds
     ) {
-        if (ids != null && !ids.isEmpty()) {
-            monitorService.deleteMonitors(new HashSet<>(ids));
+        Set<Long> effectiveIds = resolveBatchIds(ids, id, bodyIds);
+        if (!effectiveIds.isEmpty()) {
+            monitorService.deleteMonitors(effectiveIds);
         }
         return ResponseEntity.ok(Message.success());
     }
@@ -98,10 +113,13 @@ public class MonitorsController {
     @Operation(summary = "Unmanaged monitoring items in batches according to the monitoring ID list",
             description = "Unmanaged monitoring items in batches according to the monitoring ID list")
     public ResponseEntity<Message<Void>> cancelManageMonitors(
-            @Parameter(description = "Monitoring ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids
+            @Parameter(description = "Monitoring ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids,
+            @Parameter(description = "Legacy monitoring ID", example = "6565463543") @RequestParam(required = false) String id,
+            @Parameter(description = "Legacy monitoring ID JSON body", example = "[6565463543]") @RequestBody(required = false) List<Long> bodyIds
     ) {
-        if (ids != null && !ids.isEmpty()) {
-            monitorService.cancelManageMonitors(new HashSet<>(ids));
+        Set<Long> effectiveIds = resolveBatchIds(ids, id, bodyIds);
+        if (!effectiveIds.isEmpty()) {
+            monitorService.cancelManageMonitors(effectiveIds);
         }
         return ResponseEntity.ok(Message.success());
     }
@@ -110,21 +128,84 @@ public class MonitorsController {
     @Operation(summary = "Start the managed monitoring items in batches according to the monitoring ID list",
             description = "Start the managed monitoring items in batches according to the monitoring ID list")
     public ResponseEntity<Message<Void>> enableManageMonitors(
-            @Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids
+            @Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids,
+            @Parameter(description = "Legacy monitor ID", example = "6565463543") @RequestParam(required = false) String id,
+            @Parameter(description = "Legacy monitor ID JSON body", example = "[6565463543]") @RequestBody(required = false) List<Long> bodyIds
     ) {
-        if (ids != null && !ids.isEmpty()) {
-            monitorService.enableManageMonitors(new HashSet<>(ids));
+        return enableManageMonitorsByIds(ids, id, bodyIds);
+    }
+
+    @PostMapping("manage")
+    @Operation(summary = "Start the managed monitoring items in batches according to the monitoring ID list",
+            description = "Start the managed monitoring items in batches according to the monitoring ID list")
+    public ResponseEntity<Message<Void>> enableManageMonitorsByPost(
+            @Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids,
+            @Parameter(description = "Legacy monitor ID", example = "6565463543") @RequestParam(required = false) String id,
+            @Parameter(description = "Legacy monitor ID JSON body", example = "[6565463543]") @RequestBody(required = false) List<Long> bodyIds
+    ) {
+        return enableManageMonitorsByIds(ids, id, bodyIds);
+    }
+
+    @PutMapping("manage")
+    @Operation(summary = "Start the managed monitoring items in batches according to the monitoring ID list",
+            description = "Start the managed monitoring items in batches according to the monitoring ID list")
+    public ResponseEntity<Message<Void>> enableManageMonitorsByPut(
+            @Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids,
+            @Parameter(description = "Legacy monitor ID", example = "6565463543") @RequestParam(required = false) String id,
+            @Parameter(description = "Legacy monitor ID JSON body", example = "[6565463543]") @RequestBody(required = false) List<Long> bodyIds
+    ) {
+        return enableManageMonitorsByIds(ids, id, bodyIds);
+    }
+
+    private ResponseEntity<Message<Void>> enableManageMonitorsByIds(List<Long> ids, String id, List<Long> bodyIds) {
+        Set<Long> effectiveIds = resolveBatchIds(ids, id, bodyIds);
+        if (!effectiveIds.isEmpty()) {
+            monitorService.enableManageMonitors(effectiveIds);
         }
         return ResponseEntity.ok(Message.success());
     }
 
+    private static List<Long> resolveMonitorIds(List<Long> ids, String legacyId) {
+        if (ids != null && !ids.isEmpty()) {
+            return ids;
+        }
+        List<Long> legacyIds = resolveLegacyIds(legacyId);
+        return legacyIds.isEmpty() ? ids : legacyIds;
+    }
+
+    private static Set<Long> resolveBatchIds(List<Long> queryIds, String legacyId, List<Long> bodyIds) {
+        List<Long> effectiveIds = queryIds != null && !queryIds.isEmpty() ? queryIds : resolveLegacyIds(legacyId);
+        if (effectiveIds == null || effectiveIds.isEmpty()) {
+            effectiveIds = bodyIds;
+        }
+        if (effectiveIds == null || effectiveIds.isEmpty()) {
+            return Collections.emptySet();
+        }
+        return new LinkedHashSet<>(effectiveIds);
+    }
+
+    private static List<Long> resolveLegacyIds(String legacyId) {
+        if (!StringUtils.hasText(legacyId)) {
+            return Collections.emptyList();
+        }
+        List<Long> parsedIds = new ArrayList<>();
+        for (String candidate : StringUtils.commaDelimitedListToStringArray(legacyId)) {
+            Long parsedId = Longs.tryParse(candidate.trim());
+            if (parsedId != null) {
+                parsedIds.add(parsedId);
+            }
+        }
+        return parsedIds;
+    }
+
     @GetMapping("/export")
     @Operation(summary = "export monitor config", description = "export monitor config")
     public void export(
-            @Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam List<Long> ids,
+            @Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids,
+            @Parameter(description = "Legacy Monitor ID", example = "6565463543") @RequestParam(required = false) String id,
             @Parameter(description = "Export Type:JSON,EXCEL") @RequestParam(defaultValue = "JSON") String type,
             HttpServletResponse res) throws Exception {
-        monitorService.export(ids, type, res);
+        monitorService.export(resolveRequiredMonitorIds(ids, id), type, res);
     }
 
     @GetMapping("/export/all")
@@ -137,8 +218,18 @@ public class MonitorsController {
 
     @PostMapping("/import")
     @Operation(summary = "import monitor config", description = "import monitor config")
-    public ResponseEntity<Message<Void>> export(MultipartFile file) throws Exception {
+    public ResponseEntity<Message<Void>> importMonitors(
+            @Parameter(description = "Monitor config file") @RequestParam("file") MultipartFile file) throws Exception {
         monitorService.importConfig(file);
         return ResponseEntity.ok(Message.success("Import success"));
     }
+
+    private static List<Long> resolveRequiredMonitorIds(List<Long> ids, String legacyId)
+            throws MissingServletRequestParameterException {
+        List<Long> effectiveIds = resolveMonitorIds(ids, legacyId);
+        if (effectiveIds == null || effectiveIds.isEmpty()) {
+            throw new MissingServletRequestParameterException("ids", "List");
+        }
+        return effectiveIds;
+    }
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/TopologyController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/TopologyController.java
new file mode 100644
index 0000000000..5694d6a6bf
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/TopologyController.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.controller;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.hertzbeat.common.entity.dto.Message;
+import org.apache.hertzbeat.manager.pojo.dto.EntityTopologyGraphInfo;
+import org.apache.hertzbeat.manager.service.entity.EntityTopologyQueryService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Topology read API.
+ */
+@Tag(name = "Topology API")
+@RestController
+@RequestMapping(path = "/api/topology", produces = {APPLICATION_JSON_VALUE})
+public class TopologyController {
+
+    private final EntityTopologyQueryService entityTopologyQueryService;
+
+    public TopologyController(EntityTopologyQueryService entityTopologyQueryService) {
+        this.entityTopologyQueryService = entityTopologyQueryService;
+    }
+
+    @GetMapping
+    @Operation(summary = "Get topology graph", description = "Get an API-backed entity topology graph")
+    public ResponseEntity<Message<EntityTopologyGraphInfo>> getTopologyGraph(
+            @Parameter(description = "Focus entity ID", example = "87584674384")
+            @RequestParam(required = false) Long focusEntityId,
+            @Parameter(description = "Topology traversal depth", example = "1")
+            @RequestParam(defaultValue = "1") int depth,
+            @Parameter(description = "Deployment environment", example = "prod")
+            @RequestParam(required = false) String environment,
+            @Parameter(description = "Topology source kind", example = "entity-relation")
+            @RequestParam(required = false) String sourceKind,
+            @Parameter(description = "Start time in epoch milliseconds", example = "1710000000000")
+            @RequestParam(required = false) Long start,
+            @Parameter(description = "End time in epoch milliseconds", example = "1710003600000")
+            @RequestParam(required = false) Long end,
+            @Parameter(description = "Relationship type filter", example = "depends_on")
+            @RequestParam(required = false) String relationType,
+            @Parameter(description = "Hide internal telemetry relationships", example = "true")
+            @RequestParam(required = false) Boolean hideInternal,
+            @Parameter(description = "Edge page index", example = "0")
+            @RequestParam(required = false) Integer pageIndex,
+            @Parameter(description = "Edge page size", example = "50")
+            @RequestParam(required = false) Integer pageSize) {
+        EntityTopologyGraphInfo graph = entityTopologyQueryService.buildFocusedTopology(
+                focusEntityId, depth, environment, sourceKind, start, end,
+                relationType, hideInternal, pageIndex, pageSize);
+        return ResponseEntity.ok(Message.success(graph));
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityDefinitionActivityDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityDefinitionActivityDao.java
index 356564614a..7c9fe048da 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityDefinitionActivityDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityDefinitionActivityDao.java
@@ -31,5 +31,7 @@ public interface EntityDefinitionActivityDao extends JpaRepository<EntityDefinit
 
     List<EntityDefinitionActivity> findAllByEntityId(Long entityId, Pageable pageable);
 
+    List<EntityDefinitionActivity> findAllByWorkspaceId(String workspaceId, Pageable pageable);
+
     List<EntityDefinitionActivity> findAllByEntityIdIn(Collection<Long> entityIds, Sort sort);
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityGovernanceStateDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityGovernanceStateDao.java
index 728a75b9ce..75744d7562 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityGovernanceStateDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/EntityGovernanceStateDao.java
@@ -31,8 +31,17 @@ public interface EntityGovernanceStateDao extends JpaRepository<EntityGovernance
     List<EntityGovernanceState> findAllByStateScopeAndStateKindOrderByGmtUpdateDescIdDesc(
             String stateScope, String stateKind, Pageable pageable);
 
+    List<EntityGovernanceState> findAllByStateScopeAndStateKindAndWorkspaceIdOrderByGmtUpdateDescIdDesc(
+            String stateScope, String stateKind, String workspaceId, Pageable pageable);
+
     Optional<EntityGovernanceState> findByStateScopeAndStateKindAndStateKey(
             String stateScope, String stateKind, String stateKey);
 
+    Optional<EntityGovernanceState> findByStateScopeAndStateKindAndStateKeyAndWorkspaceId(
+            String stateScope, String stateKind, String stateKey, String workspaceId);
+
     void deleteByStateScopeAndStateKindAndStateKey(String stateScope, String stateKind, String stateKey);
+
+    void deleteByStateScopeAndStateKindAndStateKeyAndWorkspaceId(
+            String stateScope, String stateKind, String stateKey, String workspaceId);
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
index 87fc1ba19a..a004b00c4a 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
@@ -17,8 +17,10 @@
 
 package org.apache.hertzbeat.manager.dao;
 
+import java.util.List;
 import java.util.Optional;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
@@ -30,4 +32,11 @@ public interface ObserveEntityDao extends JpaRepository<ObserveEntity, Long>, Jp
     Optional<ObserveEntity> findFirstByTypeAndNamespaceAndName(String type, String namespace, String name);
 
     Optional<ObserveEntity> findFirstByTypeAndName(String type, String name);
+
+    Optional<ObserveEntity> findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+            String workspaceId, String type, String namespace, String name);
+
+    Optional<ObserveEntity> findFirstByWorkspaceIdAndTypeAndName(String workspaceId, String type, String name);
+
+    List<ObserveEntity> findAllByWorkspaceId(String workspaceId, Sort sort);
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/gateway/observability/ManagerObservabilityInventoryQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/gateway/observability/ManagerObservabilityInventoryQueryService.java
new file mode 100644
index 0000000000..48cfb6e717
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/gateway/observability/ManagerObservabilityInventoryQueryService.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.gateway.observability;
+
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.manager.dao.CollectorDao;
+import org.apache.hertzbeat.manager.dao.MonitorDao;
+import org.springframework.stereotype.Service;
+
+/**
+ * Query boundary for monitor and collector inventory reads used by observability orchestration.
+ */
+@Service
+@RequiredArgsConstructor
+public class ManagerObservabilityInventoryQueryService {
+
+    private final MonitorDao monitorDao;
+    private final CollectorDao collectorDao;
+
+    public long countMonitors() {
+        return monitorDao.count();
+    }
+
+    public long countCollectors() {
+        return collectorDao.count();
+    }
+
+    public long countCollectorsByStatus(byte status) {
+        return collectorDao.countByStatus(status);
+    }
+
+    public Optional<Monitor> findLatestMonitor() {
+        return monitorDao.findFirstByOrderByGmtUpdateDesc();
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/gateway/observability/ManagerObservabilityWorkspaceQueryGateway.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/gateway/observability/ManagerObservabilityWorkspaceQueryGateway.java
index 8b253e9abf..9f61790112 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/gateway/observability/ManagerObservabilityWorkspaceQueryGateway.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/gateway/observability/ManagerObservabilityWorkspaceQueryGateway.java
@@ -28,59 +28,55 @@ import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
 import org.apache.hertzbeat.common.observability.gateway.ObservabilityWorkspaceQueryGateway;
-import org.apache.hertzbeat.manager.dao.CollectorDao;
-import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
-import org.apache.hertzbeat.manager.dao.EntityMonitorBindDao;
-import org.apache.hertzbeat.manager.dao.MonitorDao;
-import org.apache.hertzbeat.manager.dao.ObserveEntityDao;
+import org.apache.hertzbeat.manager.service.entity.EntityIdentityQueryService;
+import org.apache.hertzbeat.manager.service.entity.EntityMonitorBindQueryService;
+import org.apache.hertzbeat.manager.service.entity.EntityWorkspaceQueryService;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
 
 /**
  * Manager-backed observability workspace read gateway.
  *
- * <p>This bridge intentionally remains in the manager module because it is backed by
- * manager-owned DAOs. Moving it into the observability module would invert the dependency
- * direction and force observability to depend on manager internals.
+ * <p>This gateway remains in the manager module while entity lookups pass through the
+ * manager entity query boundaries instead of exposing raw entity DAOs to observability.
  */
 @Component
 @RequiredArgsConstructor
 public class ManagerObservabilityWorkspaceQueryGateway implements ObservabilityWorkspaceQueryGateway {
 
-    private final EntityIdentityDao entityIdentityDao;
-    private final ObserveEntityDao observeEntityDao;
-    private final EntityMonitorBindDao entityMonitorBindDao;
-    private final MonitorDao monitorDao;
-    private final CollectorDao collectorDao;
+    private final EntityIdentityQueryService entityIdentityQueryService;
+    private final EntityWorkspaceQueryService entityWorkspaceQueryService;
+    private final EntityMonitorBindQueryService entityMonitorBindQueryService;
+    private final ManagerObservabilityInventoryQueryService inventoryQueryService;
 
     @Override
     public long countMonitors() {
-        return monitorDao.count();
+        return inventoryQueryService.countMonitors();
     }
 
     @Override
     public long countCollectors() {
-        return collectorDao.count();
+        return inventoryQueryService.countCollectors();
     }
 
     @Override
     public long countCollectorsByStatus(byte status) {
-        return collectorDao.countByStatus(status);
+        return inventoryQueryService.countCollectorsByStatus(status);
     }
 
     @Override
     public Optional<Monitor> findLatestMonitor() {
-        return monitorDao.findFirstByOrderByGmtUpdateDesc();
+        return inventoryQueryService.findLatestMonitor();
     }
 
     @Override
     public long countDistinctBoundEntityIdsByIdentityKeys(Set<String> identityKeys) {
-        return entityIdentityDao.countDistinctEntityIdsByIdentityKeyIn(identityKeys);
+        return entityIdentityQueryService.countDistinctEntityIdsByIdentityKeys(identityKeys);
     }
 
     @Override
     public List<EntityIdentity> findIdentitiesByKeysAndNormalizedValues(Set<String> identityKeys, Set<String> normalizedValues) {
-        return entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(identityKeys, normalizedValues);
+        return entityIdentityQueryService.findMatchingIdentities(identityKeys, normalizedValues);
     }
 
     @Override
@@ -88,22 +84,22 @@ public class ManagerObservabilityWorkspaceQueryGateway implements ObservabilityW
         if (CollectionUtils.isEmpty(entityIds)) {
             return Collections.emptyMap();
         }
-        return observeEntityDao.findAllById(entityIds).stream()
+        return entityWorkspaceQueryService.findEntitiesByIds(entityIds).stream()
                 .collect(LinkedHashMap::new, (map, entity) -> map.put(entity.getId(), entity), Map::putAll);
     }
 
     @Override
     public long countMonitorBindsByEntityId(Long entityId) {
-        return entityMonitorBindDao.countByEntityId(entityId);
+        return entityMonitorBindQueryService.countMonitorBinds(entityId);
     }
 
     @Override
     public Optional<ObserveEntity> findEntityById(Long entityId) {
-        return observeEntityDao.findById(entityId);
+        return entityWorkspaceQueryService.findEntityById(entityId);
     }
 
     @Override
     public List<EntityIdentity> findIdentitiesByEntityId(Long entityId) {
-        return entityIdentityDao.findAllByEntityIdOrderByPriorityDescIdAsc(entityId);
+        return entityIdentityQueryService.findIdentities(entityId);
     }
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionApprovalDraftDecisionInfo.java
similarity index 55%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
copy to hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionApprovalDraftDecisionInfo.java
index 87fc1ba19a..181dafa19c 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionApprovalDraftDecisionInfo.java
@@ -15,19 +15,40 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.dao;
+package org.apache.hertzbeat.manager.pojo.dto;
 
-import java.util.Optional;
-import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import java.time.LocalDateTime;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
 /**
- * ObserveEntity database operation.
+ * Non-executing approval draft lifecycle decision.
  */
-public interface ObserveEntityDao extends JpaRepository<ObserveEntity, Long>, JpaSpecificationExecutor<ObserveEntity> {
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ActionApprovalDraftDecisionInfo {
 
-    Optional<ObserveEntity> findFirstByTypeAndNamespaceAndName(String type, String namespace, String name);
+    private String draftId;
 
-    Optional<ObserveEntity> findFirstByTypeAndName(String type, String name);
+    private String decision;
+
+    private String reviewer;
+
+    private String reason;
+
+    private String state;
+
+    private String executionState;
+
+    private Boolean executionAllowed;
+
+    private String adapterOwner;
+
+    private String workspaceId;
+
+    private String creator;
+
+    private LocalDateTime gmtUpdate;
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionApprovalDraftInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionApprovalDraftInfo.java
new file mode 100644
index 0000000000..902ef9a5ce
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionApprovalDraftInfo.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.pojo.dto;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Non-executing approval draft created from a suggested operations action.
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ActionApprovalDraftInfo {
+
+    private String draftId;
+
+    private String state;
+
+    private String executionState;
+
+    private Boolean executionAllowed;
+
+    private String adapterOwner;
+
+    private String actionId;
+
+    private String catalogId;
+
+    private String risk;
+
+    private String confirmation;
+
+    private String executionMode;
+
+    private String evidenceHref;
+
+    private Map<String, Object> context;
+
+    private String workspaceId;
+
+    private String creator;
+
+    private LocalDateTime gmtUpdate;
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionCatalogItemInfo.java
similarity index 51%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
copy to hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionCatalogItemInfo.java
index 87fc1ba19a..805fda311f 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ActionCatalogItemInfo.java
@@ -15,19 +15,47 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.dao;
+package org.apache.hertzbeat.manager.pojo.dto;
 
-import java.util.Optional;
-import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import java.time.LocalDateTime;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
 /**
- * ObserveEntity database operation.
+ * Durable non-executing action catalog entry.
  */
-public interface ObserveEntityDao extends JpaRepository<ObserveEntity, Long>, JpaSpecificationExecutor<ObserveEntity> {
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ActionCatalogItemInfo {
 
-    Optional<ObserveEntity> findFirstByTypeAndNamespaceAndName(String type, String namespace, String name);
+    private String catalogId;
 
-    Optional<ObserveEntity> findFirstByTypeAndName(String type, String name);
+    private String name;
+
+    private String category;
+
+    private String scope;
+
+    private String owner;
+
+    private String risk;
+
+    private String executionMode;
+
+    private Boolean executionAllowed;
+
+    private String adapterOwner;
+
+    private String status;
+
+    private Map<String, Object> metadata;
+
+    private String workspaceId;
+
+    private String creator;
+
+    private LocalDateTime gmtUpdate;
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityInfo.java
index 80d3755730..4cf9a8f6c8 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityInfo.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityInfo.java
@@ -116,6 +116,9 @@ public class EntityInfo {
 
     private List<String> tags;
 
+    @Size(max = 64)
+    private String workspaceId;
+
     private String creator;
 
     private String modifier;
@@ -159,6 +162,7 @@ public class EntityInfo {
         info.setDescription(entity.getDescription());
         info.setLabels(entity.getLabels());
         info.setTags(entity.getTags());
+        info.setWorkspaceId(entity.getWorkspaceId());
         info.setCreator(entity.getCreator());
         info.setModifier(entity.getModifier());
         info.setGmtCreate(entity.getGmtCreate());
@@ -213,6 +217,7 @@ public class EntityInfo {
         entity.setDescription(description);
         entity.setLabels(labels);
         entity.setTags(tags);
+        entity.setWorkspaceId(workspaceId);
         entity.setCreator(creator);
         entity.setModifier(modifier);
         entity.setGmtCreate(gmtCreate);
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityTopologyGraphInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityTopologyGraphInfo.java
new file mode 100644
index 0000000000..0507874b12
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityTopologyGraphInfo.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.pojo.dto;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * API-backed topology graph payload.
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class EntityTopologyGraphInfo {
+
+    private boolean apiBacked;
+
+    private Long focusEntityId;
+
+    private int depth;
+
+    private List<String> sourceKinds = new ArrayList<>();
+
+    private List<Node> nodes = new ArrayList<>();
+
+    private List<Edge> edges = new ArrayList<>();
+
+    private List<TimelineEvent> impactTimeline = new ArrayList<>();
+
+    /**
+     * RED metrics attached to topology nodes and edges.
+     */
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RedMetrics {
+
+        private Double requestRatePerSecond;
+
+        private Long requestCount;
+
+        private Double errorRate;
+
+        private Long errorCount;
+
+        private Double latencyP95Ms;
+
+        private Double latencyAvgMs;
+    }
+
+    /**
+     * Topology node derived from an observable entity.
+     */
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Node {
+
+        private String id;
+
+        private Long entityId;
+
+        private String entityName;
+
+        private String entityType;
+
+        private String namespace;
+
+        private String environment;
+
+        private String health;
+
+        private boolean focus;
+
+        private List<String> evidenceBadges = new ArrayList<>();
+
+        private RedMetrics redMetrics = new RedMetrics();
+    }
+
+    /**
+     * Topology edge derived from persisted relation evidence.
+     */
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Edge {
+
+        private String id;
+
+        private Long relationId;
+
+        private String sourceNodeId;
+
+        private String targetNodeId;
+
+        private Long sourceEntityId;
+
+        private Long targetEntityId;
+
+        private String targetRef;
+
+        private String sampleTraceId;
+
+        private String sampleSpanId;
+
+        private String firstSeen;
+
+        private String lastSeen;
+
+        private String relationType;
+
+        private String relationSource;
+
+        private String status;
+
+        private Integer score;
+
+        private List<String> evidenceBadges = new ArrayList<>();
+
+        private RedMetrics redMetrics = new RedMetrics();
+    }
+
+    /**
+     * Timeline evidence used to explain topology impact changes.
+     */
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class TimelineEvent {
+
+        private String id;
+
+        private String edgeId;
+
+        private Long entityId;
+
+        private String sourceKind;
+
+        private String eventType;
+
+        private String title;
+
+        private String detail;
+
+        private String actor;
+
+        private LocalDateTime occurredAt;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/action/ActionApprovalDraftService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/action/ActionApprovalDraftService.java
new file mode 100644
index 0000000000..aa838a559f
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/action/ActionApprovalDraftService.java
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.action;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.common.entity.manager.EntityGovernanceState;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
+import org.apache.hertzbeat.manager.pojo.dto.ActionApprovalDraftDecisionInfo;
+import org.apache.hertzbeat.manager.pojo.dto.ActionApprovalDraftInfo;
+import org.apache.hertzbeat.manager.service.entity.EntityGovernanceStateQueryService;
+import org.apache.hertzbeat.manager.service.entity.EntityGovernanceStateWriteModelService;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+
+/**
+ * Persists suggested-action approval drafts without executing actions.
+ */
+@Service
+public class ActionApprovalDraftService {
+
+    public static final String ACTION_SCOPE = "actions";
+    public static final String APPROVAL_DRAFT_KIND = "approval-draft";
+    public static final String ADAPTER_OWNER = "manager-action-approval-draft";
+    public static final String MANUAL_CONFIRMATION = "manual-required";
+    public static final String APPROVAL_DRAFT_EXECUTION_MODE = "manual-approval-draft-only";
+    public static final String CREATED_STATE = "approval-draft-created";
+    public static final String APPROVED_STATE = "approval-draft-approved";
+    public static final String REJECTED_STATE = "approval-draft-rejected";
+    public static final String NOT_EXECUTED_STATE = "not-executed";
+    public static final String APPROVED_DECISION = "approved";
+    public static final String REJECTED_DECISION = "rejected";
+
+    private static final ObjectMapper JSON_MAPPER = JsonMapper.builder().build();
+
+    private final EntityGovernanceStateQueryService entityGovernanceStateQueryService;
+    private final EntityGovernanceStateWriteModelService entityGovernanceStateWriteModelService;
+
+    public ActionApprovalDraftService(EntityGovernanceStateQueryService entityGovernanceStateQueryService,
+                                      EntityGovernanceStateWriteModelService entityGovernanceStateWriteModelService) {
+        this.entityGovernanceStateQueryService = entityGovernanceStateQueryService;
+        this.entityGovernanceStateWriteModelService = entityGovernanceStateWriteModelService;
+    }
+
+    public ActionApprovalDraftInfo createApprovalDraft(ActionApprovalDraftInfo request) {
+        return createApprovalDraft(request, null);
+    }
+
+    public ActionApprovalDraftInfo createApprovalDraft(ActionApprovalDraftInfo request, String requestWorkspaceId) {
+        validateDraftRequest(request);
+        String draftId = defaultText(
+                trimToNull(request.getDraftId()),
+                "approval-draft-" + trimToNull(request.getActionId()) + "-" + SnowFlakeIdGenerator.generateId());
+        EntityGovernanceState state = entityGovernanceStateWriteModelService.findGovernanceStateForWrite(
+                ACTION_SCOPE, APPROVAL_DRAFT_KIND, draftId, requestWorkspaceId);
+        state.setStateScope(ACTION_SCOPE);
+        state.setStateKind(APPROVAL_DRAFT_KIND);
+        state.setStateKey(draftId);
+        state.setStateName(limitLength(trimToNull(request.getActionId()), 128));
+        state.setStatus(CREATED_STATE);
+        state.setContent(JSON_MAPPER.valueToTree(toContent(request)));
+        return toApprovalDraftInfo(entityGovernanceStateWriteModelService.saveGovernanceState(state));
+    }
+
+    public List<ActionApprovalDraftInfo> listApprovalDrafts(int limit) {
+        return listApprovalDrafts(limit, null);
+    }
+
+    public List<ActionApprovalDraftInfo> listApprovalDrafts(int limit, String requestWorkspaceId) {
+        return entityGovernanceStateQueryService.findGovernanceStates(
+                        ACTION_SCOPE, APPROVAL_DRAFT_KIND, PageRequest.of(0, normalizeLimit(limit)),
+                        requestWorkspaceId)
+                .stream()
+                .map(this::toApprovalDraftInfo)
+                .toList();
+    }
+
+    public ActionApprovalDraftDecisionInfo decideApprovalDraft(String draftId,
+                                                               ActionApprovalDraftDecisionInfo request,
+                                                               String requestWorkspaceId) {
+        String normalizedDraftId = trimToNull(draftId);
+        validateDecisionRequest(normalizedDraftId, request);
+        EntityGovernanceState state = entityGovernanceStateWriteModelService.findGovernanceStateForWrite(
+                ACTION_SCOPE, APPROVAL_DRAFT_KIND, normalizedDraftId, requestWorkspaceId);
+        if (state.getContent() == null || state.getContent().isNull()) {
+            throw new IllegalArgumentException("Approval draft does not exist.");
+        }
+        Map<String, Object> content = jsonMap(state.getContent());
+        content.put("decision", trimToNull(request.getDecision()));
+        putIfPresent(content, "decisionReason", trimToNull(request.getReason()));
+        putIfPresent(content, "reviewer", trimToNull(request.getReviewer()));
+        content.put("executionAllowed", false);
+        content.put("executionState", NOT_EXECUTED_STATE);
+        content.put("adapterOwner", ADAPTER_OWNER);
+        state.setStateScope(ACTION_SCOPE);
+        state.setStateKind(APPROVAL_DRAFT_KIND);
+        state.setStateKey(normalizedDraftId);
+        state.setStatus(decisionState(trimToNull(request.getDecision())));
+        state.setContent(JSON_MAPPER.valueToTree(content));
+        return toDecisionInfo(entityGovernanceStateWriteModelService.saveGovernanceState(state));
+    }
+
+    private void validateDraftRequest(ActionApprovalDraftInfo request) {
+        if (request == null) {
+            throw new IllegalArgumentException("Approval draft request can not be null.");
+        }
+        if (!StringUtils.hasText(request.getActionId())) {
+            throw new IllegalArgumentException("Approval draft actionId can not be blank.");
+        }
+        if (!StringUtils.hasText(request.getCatalogId())) {
+            throw new IllegalArgumentException("Approval draft catalogId can not be blank.");
+        }
+        if (!MANUAL_CONFIRMATION.equals(request.getConfirmation())) {
+            throw new IllegalArgumentException("Approval draft confirmation must be manual-required.");
+        }
+        if (!APPROVAL_DRAFT_EXECUTION_MODE.equals(request.getExecutionMode())) {
+            throw new IllegalArgumentException("Approval draft executionMode must stay manual-only.");
+        }
+        if (!Boolean.FALSE.equals(request.getExecutionAllowed())) {
+            throw new IllegalArgumentException("Approval draft can not execute actions.");
+        }
+    }
+
+    private void validateDecisionRequest(String draftId, ActionApprovalDraftDecisionInfo request) {
+        if (!StringUtils.hasText(draftId)) {
+            throw new IllegalArgumentException("Approval draft draftId can not be blank.");
+        }
+        if (request == null) {
+            throw new IllegalArgumentException("Approval draft decision request can not be null.");
+        }
+        String decision = trimToNull(request.getDecision());
+        if (!APPROVED_DECISION.equals(decision) && !REJECTED_DECISION.equals(decision)) {
+            throw new IllegalArgumentException("Approval draft decision must be approved or rejected.");
+        }
+        if (Boolean.TRUE.equals(request.getExecutionAllowed())) {
+            throw new IllegalArgumentException("Approval draft decision can not execute actions.");
+        }
+    }
+
+    private Map<String, Object> toContent(ActionApprovalDraftInfo request) {
+        Map<String, Object> content = new LinkedHashMap<>();
+        putIfPresent(content, "actionId", trimToNull(request.getActionId()));
+        putIfPresent(content, "catalogId", trimToNull(request.getCatalogId()));
+        putIfPresent(content, "risk", trimToNull(request.getRisk()));
+        putIfPresent(content, "confirmation", MANUAL_CONFIRMATION);
+        putIfPresent(content, "executionMode", APPROVAL_DRAFT_EXECUTION_MODE);
+        content.put("executionAllowed", false);
+        putIfPresent(content, "executionState", NOT_EXECUTED_STATE);
+        putIfPresent(content, "adapterOwner", ADAPTER_OWNER);
+        putIfPresent(content, "evidenceHref", trimToNull(request.getEvidenceHref()));
+        if (request.getContext() != null && !request.getContext().isEmpty()) {
+            content.put("context", request.getContext());
+        }
+        return content;
+    }
+
+    private ActionApprovalDraftInfo toApprovalDraftInfo(EntityGovernanceState state) {
+        JsonNode content = state.getContent();
+        ActionApprovalDraftInfo info = new ActionApprovalDraftInfo();
+        info.setDraftId(state.getStateKey());
+        info.setState(defaultText(state.getStatus(), CREATED_STATE));
+        info.setExecutionState(defaultText(jsonText(content, "executionState"), NOT_EXECUTED_STATE));
+        info.setExecutionAllowed(false);
+        info.setAdapterOwner(defaultText(jsonText(content, "adapterOwner"), ADAPTER_OWNER));
+        info.setActionId(jsonText(content, "actionId"));
+        info.setCatalogId(jsonText(content, "catalogId"));
+        info.setRisk(jsonText(content, "risk"));
+        info.setConfirmation(defaultText(jsonText(content, "confirmation"), MANUAL_CONFIRMATION));
+        info.setExecutionMode(defaultText(jsonText(content, "executionMode"), APPROVAL_DRAFT_EXECUTION_MODE));
+        info.setEvidenceHref(jsonText(content, "evidenceHref"));
+        info.setContext(jsonMap(content == null ? null : content.get("context")));
+        info.setWorkspaceId(defaultText(state.getWorkspaceId(), AuthTokenScopes.DEFAULT_WORKSPACE_ID));
+        info.setCreator(state.getCreator());
+        info.setGmtUpdate(state.getGmtUpdate());
+        return info;
+    }
+
+    private ActionApprovalDraftDecisionInfo toDecisionInfo(EntityGovernanceState state) {
+        JsonNode content = state.getContent();
+        ActionApprovalDraftDecisionInfo info = new ActionApprovalDraftDecisionInfo();
+        info.setDraftId(state.getStateKey());
+        info.setDecision(jsonText(content, "decision"));
+        info.setReviewer(jsonText(content, "reviewer"));
+        info.setReason(jsonText(content, "decisionReason"));
+        info.setState(defaultText(state.getStatus(), CREATED_STATE));
+        info.setExecutionState(defaultText(jsonText(content, "executionState"), NOT_EXECUTED_STATE));
+        info.setExecutionAllowed(false);
+        info.setAdapterOwner(defaultText(jsonText(content, "adapterOwner"), ADAPTER_OWNER));
+        info.setWorkspaceId(defaultText(state.getWorkspaceId(), AuthTokenScopes.DEFAULT_WORKSPACE_ID));
+        info.setCreator(state.getCreator());
+        info.setGmtUpdate(state.getGmtUpdate());
+        return info;
+    }
+
+    private String decisionState(String decision) {
+        return APPROVED_DECISION.equals(decision) ? APPROVED_STATE : REJECTED_STATE;
+    }
+
+    private Map<String, Object> jsonMap(JsonNode node) {
+        if (node == null || node.isNull() || !node.isObject()) {
+            return Map.of();
+        }
+        return JSON_MAPPER.convertValue(node, new TypeReference<Map<String, Object>>() { });
+    }
+
+    private String jsonText(JsonNode node, String fieldName) {
+        if (node == null || node.isNull() || node.get(fieldName) == null || node.get(fieldName).isNull()) {
+            return null;
+        }
+        return trimToNull(node.get(fieldName).asText());
+    }
+
+    private int normalizeLimit(int limit) {
+        if (limit <= 0) {
+            return 8;
+        }
+        return Math.min(limit, 50);
+    }
+
+    private void putIfPresent(Map<String, Object> target, String key, Object value) {
+        if (value != null) {
+            target.put(key, value);
+        }
+    }
+
+    private String defaultText(String value, String defaultValue) {
+        String normalized = trimToNull(value);
+        return normalized == null ? defaultValue : normalized;
+    }
+
+    private String limitLength(String value, int limit) {
+        String normalized = trimToNull(value);
+        if (normalized == null || normalized.length() <= limit) {
+            return normalized;
+        }
+        return normalized.substring(0, limit);
+    }
+
+    private String trimToNull(String value) {
+        return StringUtils.hasText(value) ? value.trim() : null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/action/ActionCatalogService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/action/ActionCatalogService.java
new file mode 100644
index 0000000000..9f664772fc
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/action/ActionCatalogService.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.action;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.common.entity.manager.EntityGovernanceState;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.pojo.dto.ActionCatalogItemInfo;
+import org.apache.hertzbeat.manager.service.entity.EntityGovernanceStateQueryService;
+import org.apache.hertzbeat.manager.service.entity.EntityGovernanceStateWriteModelService;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+
+/**
+ * Persists action catalog entries without enabling direct execution.
+ */
+@Service
+public class ActionCatalogService {
+
+    public static final String ACTION_SCOPE = "actions";
+    public static final String CATALOG_KIND = "catalog-item";
+    public static final String ADAPTER_OWNER = "manager-action-catalog";
+    public static final String CATALOG_READY_STATE = "catalog-item-ready";
+    public static final String APPROVAL_DRAFT_EXECUTION_MODE = "manual-approval-draft-only";
+
+    private static final ObjectMapper JSON_MAPPER = JsonMapper.builder().build();
+
+    private final EntityGovernanceStateQueryService entityGovernanceStateQueryService;
+    private final EntityGovernanceStateWriteModelService entityGovernanceStateWriteModelService;
+
+    public ActionCatalogService(EntityGovernanceStateQueryService entityGovernanceStateQueryService,
+                                EntityGovernanceStateWriteModelService entityGovernanceStateWriteModelService) {
+        this.entityGovernanceStateQueryService = entityGovernanceStateQueryService;
+        this.entityGovernanceStateWriteModelService = entityGovernanceStateWriteModelService;
+    }
+
+    public ActionCatalogItemInfo saveCatalogItem(ActionCatalogItemInfo request) {
+        return saveCatalogItem(request, null);
+    }
+
+    public ActionCatalogItemInfo saveCatalogItem(ActionCatalogItemInfo request, String requestWorkspaceId) {
+        validateCatalogItem(request);
+        String catalogId = trimToNull(request.getCatalogId());
+        EntityGovernanceState state = entityGovernanceStateWriteModelService.findGovernanceStateForWrite(
+                ACTION_SCOPE, CATALOG_KIND, catalogId, requestWorkspaceId);
+        state.setStateScope(ACTION_SCOPE);
+        state.setStateKind(CATALOG_KIND);
+        state.setStateKey(catalogId);
+        state.setStateName(limitLength(trimToNull(request.getName()), 128));
+        state.setStatus(CATALOG_READY_STATE);
+        state.setContent(JSON_MAPPER.valueToTree(toContent(request)));
+        return toCatalogItemInfo(entityGovernanceStateWriteModelService.saveGovernanceState(state));
+    }
+
+    public List<ActionCatalogItemInfo> listCatalogItems(int limit) {
+        return listCatalogItems(limit, null);
+    }
+
+    public List<ActionCatalogItemInfo> listCatalogItems(int limit, String requestWorkspaceId) {
+        return entityGovernanceStateQueryService.findGovernanceStates(
+                        ACTION_SCOPE, CATALOG_KIND, PageRequest.of(0, normalizeLimit(limit)), requestWorkspaceId)
+                .stream()
+                .map(this::toCatalogItemInfo)
+                .toList();
+    }
+
+    private void validateCatalogItem(ActionCatalogItemInfo request) {
+        if (request == null) {
+            throw new IllegalArgumentException("Action catalog item request can not be null.");
+        }
+        if (!StringUtils.hasText(request.getCatalogId())) {
+            throw new IllegalArgumentException("Action catalog item catalogId can not be blank.");
+        }
+        if (!StringUtils.hasText(request.getName())) {
+            throw new IllegalArgumentException("Action catalog item name can not be blank.");
+        }
+        if (!StringUtils.hasText(request.getRisk())) {
+            throw new IllegalArgumentException("Action catalog item risk can not be blank.");
+        }
+        String executionMode = trimToNull(request.getExecutionMode());
+        if (executionMode != null && !APPROVAL_DRAFT_EXECUTION_MODE.equals(executionMode)) {
+            throw new IllegalArgumentException("Action catalog item executionMode must stay manual-only.");
+        }
+        if (Boolean.TRUE.equals(request.getExecutionAllowed())) {
+            throw new IllegalArgumentException("Action catalog item can not execute actions directly.");
+        }
+    }
+
+    private Map<String, Object> toContent(ActionCatalogItemInfo request) {
+        Map<String, Object> content = new LinkedHashMap<>();
+        putIfPresent(content, "catalogId", trimToNull(request.getCatalogId()));
+        putIfPresent(content, "name", trimToNull(request.getName()));
+        putIfPresent(content, "category", trimToNull(request.getCategory()));
+        putIfPresent(content, "scope", trimToNull(request.getScope()));
+        putIfPresent(content, "owner", trimToNull(request.getOwner()));
+        putIfPresent(content, "risk", trimToNull(request.getRisk()));
+        putIfPresent(content, "executionMode", APPROVAL_DRAFT_EXECUTION_MODE);
+        content.put("executionAllowed", false);
+        putIfPresent(content, "adapterOwner", ADAPTER_OWNER);
+        if (request.getMetadata() != null && !request.getMetadata().isEmpty()) {
+            content.put("metadata", request.getMetadata());
+        }
+        return content;
+    }
+
+    private ActionCatalogItemInfo toCatalogItemInfo(EntityGovernanceState state) {
+        JsonNode content = state.getContent();
+        ActionCatalogItemInfo info = new ActionCatalogItemInfo();
+        info.setCatalogId(defaultText(jsonText(content, "catalogId"), state.getStateKey()));
+        info.setName(defaultText(jsonText(content, "name"), state.getStateName()));
+        info.setCategory(jsonText(content, "category"));
+        info.setScope(jsonText(content, "scope"));
+        info.setOwner(jsonText(content, "owner"));
+        info.setRisk(jsonText(content, "risk"));
+        info.setExecutionMode(defaultText(jsonText(content, "executionMode"), APPROVAL_DRAFT_EXECUTION_MODE));
+        info.setExecutionAllowed(false);
+        info.setAdapterOwner(defaultText(jsonText(content, "adapterOwner"), ADAPTER_OWNER));
+        info.setStatus(defaultText(state.getStatus(), CATALOG_READY_STATE));
+        info.setMetadata(jsonMap(content == null ? null : content.get("metadata")));
+        info.setWorkspaceId(defaultText(state.getWorkspaceId(), AuthTokenScopes.DEFAULT_WORKSPACE_ID));
+        info.setCreator(state.getCreator());
+        info.setGmtUpdate(state.getGmtUpdate());
+        return info;
+    }
+
+    private Map<String, Object> jsonMap(JsonNode node) {
+        if (node == null || node.isNull() || !node.isObject()) {
+            return Map.of();
+        }
+        return JSON_MAPPER.convertValue(node, new TypeReference<Map<String, Object>>() { });
+    }
+
+    private String jsonText(JsonNode node, String fieldName) {
+        if (node == null || node.isNull() || node.get(fieldName) == null || node.get(fieldName).isNull()) {
+            return null;
+        }
+        return trimToNull(node.get(fieldName).asText());
+    }
+
+    private int normalizeLimit(int limit) {
+        if (limit <= 0) {
+            return 8;
+        }
+        return Math.min(limit, 50);
+    }
+
+    private void putIfPresent(Map<String, Object> target, String key, Object value) {
+        if (value != null) {
+            target.put(key, value);
+        }
+    }
+
+    private String defaultText(String value, String defaultValue) {
+        String normalized = trimToNull(value);
+        return normalized == null ? defaultValue : normalized;
+    }
+
+    private String limitLength(String value, int limit) {
+        String normalized = trimToNull(value);
+        if (normalized == null || normalized.length() <= limit) {
+            return normalized;
+        }
+        return normalized.substring(0, limit);
+    }
+
+    private String trimToNull(String value) {
+        return StringUtils.hasText(value) ? value.trim() : null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityQueryService.java
new file mode 100644
index 0000000000..83b9eda728
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityQueryService.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.common.entity.manager.EntityDefinitionActivity;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.dao.EntityDefinitionActivityDao;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Query boundary for raw entity definition activity rows.
+ */
+@Service
+public class EntityActivityQueryService {
+
+    private static final String ACTIVITY_TYPE_DEFINITION_IMPORT = "definition_import";
+    private static final String ACTIVITY_TYPE_DEFINITION_UPDATE = "definition_update";
+
+    private final EntityDefinitionActivityDao entityDefinitionActivityDao;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityActivityQueryService(EntityDefinitionActivityDao entityDefinitionActivityDao,
+                                      EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityDefinitionActivityDao = entityDefinitionActivityDao;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public List<EntityDefinitionActivity> findDefinitionActivities(Long entityId, Pageable pageRequest) {
+        return findDefinitionActivities(entityId, pageRequest, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public List<EntityDefinitionActivity> findDefinitionActivities(Long entityId, Pageable pageRequest,
+                                                                    String requestWorkspaceId) {
+        String workspaceId = normalizeWorkspaceId(requestWorkspaceId);
+        if (entityId != null) {
+            if (!entityWorkspaceAccessService.isEntityAccessibleForRequestWorkspace(entityId, workspaceId)) {
+                return Collections.emptyList();
+            }
+            return entityDefinitionActivityDao.findAllByEntityId(entityId, pageRequest).stream()
+                    .filter(activity -> matchesActivityWorkspace(activity, workspaceId))
+                    .toList();
+        }
+        if (StringUtils.hasText(workspaceId)) {
+            return entityDefinitionActivityDao.findAllByWorkspaceId(workspaceId, pageRequest).stream()
+                    .filter(activity -> matchesActivityWorkspace(activity, workspaceId))
+                    .toList();
+        }
+        return entityDefinitionActivityDao.findAll(pageRequest).stream()
+                .toList();
+    }
+
+    public Map<Long, EntityDefinitionActivity> findLatestDefinitionActivities(List<Long> entityIds) {
+        if (CollectionUtils.isEmpty(entityIds)) {
+            return Collections.emptyMap();
+        }
+        List<Long> acceptedEntityIds = entityIds.stream()
+                .filter(id -> id != null)
+                .toList();
+        if (CollectionUtils.isEmpty(acceptedEntityIds)) {
+            return Collections.emptyMap();
+        }
+        List<EntityDefinitionActivity> activities = entityDefinitionActivityDao.findAllByEntityIdIn(
+                acceptedEntityIds, Sort.by(Sort.Order.desc("gmtCreate"), Sort.Order.desc("id"))
+        );
+        Map<Long, EntityDefinitionActivity> latestActivityMap = new LinkedHashMap<>();
+        for (EntityDefinitionActivity activity : activities) {
+            if (activity != null
+                    && activity.getEntityId() != null
+                    && isDefinitionActivityType(activity.getActivityType())
+                    && !latestActivityMap.containsKey(activity.getEntityId())) {
+                latestActivityMap.put(activity.getEntityId(), activity);
+            }
+        }
+        return latestActivityMap;
+    }
+
+    private boolean matchesActivityWorkspace(EntityDefinitionActivity activity, String requestWorkspaceId) {
+        if (!StringUtils.hasText(requestWorkspaceId)) {
+            return true;
+        }
+        String activityWorkspaceId = activity == null ? null : activity.getWorkspaceId();
+        if (!StringUtils.hasText(activityWorkspaceId)) {
+            return AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(requestWorkspaceId);
+        }
+        return requestWorkspaceId.equals(AuthTokenScopes.normalizeWorkspaceId(activityWorkspaceId));
+    }
+
+    private boolean isDefinitionActivityType(String activityType) {
+        return ACTIVITY_TYPE_DEFINITION_IMPORT.equals(activityType)
+                || ACTIVITY_TYPE_DEFINITION_UPDATE.equals(activityType);
+    }
+
+    private String normalizeWorkspaceId(String requestWorkspaceId) {
+        return StringUtils.hasText(requestWorkspaceId) ? AuthTokenScopes.normalizeWorkspaceId(requestWorkspaceId) : null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityReadModelService.java
new file mode 100644
index 0000000000..f26dde0ffb
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityReadModelService.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.common.entity.manager.EntityDefinitionActivity;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionActivityInfo;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Builds entity activity timeline read models behind a workspace boundary.
+ */
+@Service
+public class EntityActivityReadModelService {
+
+    private final EntityActivityQueryService entityActivityQueryService;
+
+    public EntityActivityReadModelService(EntityActivityQueryService entityActivityQueryService) {
+        this.entityActivityQueryService = entityActivityQueryService;
+    }
+
+    public List<EntityDefinitionActivityInfo> getDefinitionActivities(Long entityId, int limit) {
+        PageRequest pageRequest = pageRequest(limit);
+        return entityActivityQueryService.findDefinitionActivities(entityId, pageRequest).stream()
+                .map(this::toDefinitionActivityInfo)
+                .toList();
+    }
+
+    public List<EntityDefinitionActivityInfo> getDefinitionActivities(Long entityId, int limit,
+                                                                       String requestWorkspaceId) {
+        PageRequest pageRequest = pageRequest(limit);
+        return entityActivityQueryService.findDefinitionActivities(entityId, pageRequest, requestWorkspaceId).stream()
+                .map(this::toDefinitionActivityInfo)
+                .toList();
+    }
+
+    public Map<Long, EntityDefinitionActivity> findLatestDefinitionActivities(List<Long> entityIds) {
+        if (CollectionUtils.isEmpty(entityIds)) {
+            return Collections.emptyMap();
+        }
+        return entityActivityQueryService.findLatestDefinitionActivities(entityIds);
+    }
+
+    private PageRequest pageRequest(int limit) {
+        int pageSize = limit <= 0 ? 12 : Math.min(limit, 50);
+        return PageRequest.of(
+                0, pageSize, Sort.by(Sort.Order.desc("gmtCreate"), Sort.Order.desc("id")));
+    }
+
+    private EntityDefinitionActivityInfo toDefinitionActivityInfo(EntityDefinitionActivity activity) {
+        return new EntityDefinitionActivityInfo(
+                activity.getId(),
+                activity.getEntityId(),
+                activity.getActivityType(),
+                activity.getFormat(),
+                activity.getStatus(),
+                activity.getSummary(),
+                activity.getDetail(),
+                activity.getCreator(),
+                activity.getGmtCreate()
+        );
+    }
+
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityRecordWriteModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityRecordWriteModelService.java
new file mode 100644
index 0000000000..ff9558287a
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityRecordWriteModelService.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import org.apache.hertzbeat.common.entity.manager.EntityDefinitionActivity;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.dao.EntityDefinitionActivityDao;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * Persists entity activity rows and resolves their workspace scope at the final write boundary.
+ */
+@Service
+public class EntityActivityRecordWriteModelService {
+
+    private static final String FORMAT_YAML = "yaml";
+
+    private final EntityDefinitionActivityDao entityDefinitionActivityDao;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityActivityRecordWriteModelService(EntityDefinitionActivityDao entityDefinitionActivityDao,
+                                                 EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityDefinitionActivityDao = entityDefinitionActivityDao;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public void recordActivityForCurrentWorkspace(Long entityId, String activityType, String format, String status,
+                                                  String summary, String detail, ObserveEntity entity) {
+        recordActivity(entityId, activityType, format, status, summary, detail, entity,
+                entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public void recordActivity(Long entityId, String activityType, String format, String status, String summary,
+                               String detail, ObserveEntity entity, String requestWorkspaceId) {
+        String normalizedFormat = StringUtils.hasText(format) ? format : FORMAT_YAML;
+        entityDefinitionActivityDao.saveAndFlush(EntityDefinitionActivity.builder()
+                .entityId(entityId)
+                .workspaceId(resolveActivityWorkspaceId(entityId, entity, requestWorkspaceId))
+                .activityType(activityType)
+                .format(normalizedFormat)
+                .status(status)
+                .summary(summary)
+                .detail(detail)
+                .build());
+    }
+
+    private String resolveActivityWorkspaceId(Long entityId, ObserveEntity entity, String requestWorkspaceId) {
+        if (StringUtils.hasText(requestWorkspaceId)) {
+            return AuthTokenScopes.normalizeWorkspaceId(requestWorkspaceId);
+        }
+        if (entity != null && StringUtils.hasText(entity.getWorkspaceId())) {
+            return AuthTokenScopes.normalizeWorkspaceId(entity.getWorkspaceId());
+        }
+        if (entityId != null) {
+            return entityWorkspaceAccessService.findEntityById(entityId)
+                    .map(ObserveEntity::getWorkspaceId)
+                    .filter(StringUtils::hasText)
+                    .map(AuthTokenScopes::normalizeWorkspaceId)
+                    .orElse(AuthTokenScopes.DEFAULT_WORKSPACE_ID);
+        }
+        return AuthTokenScopes.DEFAULT_WORKSPACE_ID;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityWriteModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityWriteModelService.java
new file mode 100644
index 0000000000..bf15c24369
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityActivityWriteModelService.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Persists entity definition and lifecycle activity rows behind the entity-domain write boundary.
+ */
+@Service
+public class EntityActivityWriteModelService {
+
+    private static final String SOURCE_MANUAL = "manual";
+    private static final String SOURCE_OTEL_RESOURCE = "otel_resource";
+    private static final String SOURCE_DERIVED = "derived";
+    private static final String FORMAT_JSON = "json";
+    private static final String FORMAT_YAML = "yaml";
+    private static final String FORMAT_CURL = "curl";
+    private static final String ACTIVITY_SUCCESS = "success";
+    private static final String ACTIVITY_ERROR = "error";
+    private static final String ACTIVITY_TYPE_DEFINITION_UPDATE = "definition_update";
+    private static final String ACTIVITY_TYPE_CATALOG_CREATE = "catalog_create";
+    private static final String ACTIVITY_TYPE_CATALOG_UPDATE = "catalog_update";
+    private static final String ACTIVITY_TYPE_DISCOVERY_GOVERNANCE = "discovery_governance";
+    private static final String ACTIVITY_TYPE_SOURCE_UPDATE = "source_update";
+    private static final String BIND_SOURCE_TELEMETRY_DISCOVERY = "telemetry_discovery";
+    private static final String TYPE_DATABASE = "database";
+    private static final String TYPE_API = "api";
+    private static final String KIND_DATASTORE = "datastore";
+    private static final String KIND_API = "api";
+
+    private final EntityActivityRecordWriteModelService entityActivityRecordWriteModelService;
+    private final EntityMonitorBindService entityMonitorBindService;
+
+    public EntityActivityWriteModelService(
+            EntityActivityRecordWriteModelService entityActivityRecordWriteModelService,
+            EntityMonitorBindService entityMonitorBindService) {
+        this.entityActivityRecordWriteModelService = entityActivityRecordWriteModelService;
+        this.entityMonitorBindService = entityMonitorBindService;
+    }
+
+    public void recordDefinitionActivity(Long entityId, String activityType, String format, ObserveEntity entity) {
+        if (entityId == null) {
+            return;
+        }
+        entityActivityRecordWriteModelService.recordActivityForCurrentWorkspace(
+                entityId,
+                activityType,
+                normalizeDefinitionActivityFormat(format),
+                ACTIVITY_SUCCESS,
+                buildDefinitionActivitySummary(activityType, ACTIVITY_SUCCESS),
+                buildDefinitionActivityDetail(entity, null),
+                entity);
+    }
+
+    public void recordDefinitionActivity(Long entityId, String activityType, String format,
+                                         ObserveEntity entity, String requestWorkspaceId) {
+        if (entityId == null) {
+            return;
+        }
+        entityActivityRecordWriteModelService.recordActivity(
+                entityId,
+                activityType,
+                normalizeDefinitionActivityFormat(format),
+                ACTIVITY_SUCCESS,
+                buildDefinitionActivitySummary(activityType, ACTIVITY_SUCCESS),
+                buildDefinitionActivityDetail(entity, null),
+                entity,
+                requestWorkspaceId);
+    }
+
+    public void recordDefinitionActivityFailure(Long entityId, String activityType, String format,
+                                                RuntimeException exception) {
+        if (entityId == null) {
+            return;
+        }
+        entityActivityRecordWriteModelService.recordActivityForCurrentWorkspace(
+                entityId,
+                activityType,
+                normalizeDefinitionActivityFormat(format),
+                ACTIVITY_ERROR,
+                buildDefinitionActivitySummary(activityType, ACTIVITY_ERROR),
+                buildDefinitionActivityDetail(null, exception),
+                null);
+    }
+
+    public void recordDefinitionActivityFailure(Long entityId, String activityType, String format,
+                                                RuntimeException exception, String requestWorkspaceId) {
+        if (entityId == null) {
+            return;
+        }
+        entityActivityRecordWriteModelService.recordActivity(
+                entityId,
+                activityType,
+                normalizeDefinitionActivityFormat(format),
+                ACTIVITY_ERROR,
+                buildDefinitionActivitySummary(activityType, ACTIVITY_ERROR),
+                buildDefinitionActivityDetail(null, exception),
+                null,
+                requestWorkspaceId);
+    }
+
+    public void recordEntityLifecycleActivity(Long entityId, String activityType, ObserveEntity entity) {
+        if (entityId == null || !StringUtils.hasText(activityType)) {
+            return;
+        }
+        entityActivityRecordWriteModelService.recordActivityForCurrentWorkspace(
+                entityId,
+                activityType,
+                null,
+                ACTIVITY_SUCCESS,
+                buildEntityLifecycleActivitySummary(activityType),
+                buildEntityLifecycleActivityDetail(entity, activityType),
+                entity);
+    }
+
+    public void recordEntityLifecycleActivity(Long entityId, String activityType,
+                                              ObserveEntity entity, String requestWorkspaceId) {
+        if (entityId == null || !StringUtils.hasText(activityType)) {
+            return;
+        }
+        entityActivityRecordWriteModelService.recordActivity(
+                entityId,
+                activityType,
+                null,
+                ACTIVITY_SUCCESS,
+                buildEntityLifecycleActivitySummary(activityType),
+                buildEntityLifecycleActivityDetail(entity, activityType),
+                entity,
+                requestWorkspaceId);
+    }
+
+    public String resolveCreateLifecycleActivityType(EntityDto entityDto) {
+        ObserveEntity entity = entityDto == null ? null : entityDto.getEntity();
+        if (isTelemetryLifecycleSource(entity == null ? null : entity.getSource())
+                || hasTelemetryDiscoveryBind(Collections.emptyList(),
+                        entityDto == null ? null : entityDto.getMonitorBinds())) {
+            return ACTIVITY_TYPE_DISCOVERY_GOVERNANCE;
+        }
+        return ACTIVITY_TYPE_CATALOG_CREATE;
+    }
+
+    public String resolveModifyLifecycleActivityType(ObserveEntity currentEntity, ObserveEntity updateEntity,
+                                                     List<EntityMonitorBind> existingBinds,
+                                                     List<EntityMonitorBind> nextBinds) {
+        String currentSource = defaultText(currentEntity == null ? null : currentEntity.getSource(), SOURCE_MANUAL);
+        String nextSource = defaultText(updateEntity == null ? null : updateEntity.getSource(), currentSource, SOURCE_MANUAL);
+        if (hasTelemetryDiscoveryBind(existingBinds, nextBinds)) {
+            return ACTIVITY_TYPE_DISCOVERY_GOVERNANCE;
+        }
+        if (!Objects.equals(currentSource, nextSource)) {
+            return ACTIVITY_TYPE_SOURCE_UPDATE;
+        }
+        return ACTIVITY_TYPE_CATALOG_UPDATE;
+    }
+
+    public String resolveModifyLifecycleActivityType(ObserveEntity currentEntity, ObserveEntity updateEntity,
+                                                     List<EntityMonitorBind> nextBinds) {
+        Long entityId = currentEntity == null ? null : currentEntity.getId();
+        if (entityId == null && updateEntity != null) {
+            entityId = updateEntity.getId();
+        }
+        List<EntityMonitorBind> existingBinds = entityId == null
+                ? Collections.emptyList()
+                : entityMonitorBindService.findMonitorBinds(entityId);
+        return resolveModifyLifecycleActivityType(currentEntity, updateEntity, existingBinds, nextBinds);
+    }
+
+    private String buildEntityLifecycleActivitySummary(String activityType) {
+        return switch (activityType) {
+            case ACTIVITY_TYPE_DISCOVERY_GOVERNANCE -> "Telemetry discovery applied";
+            case ACTIVITY_TYPE_SOURCE_UPDATE -> "Entity source updated";
+            case ACTIVITY_TYPE_CATALOG_UPDATE -> "Catalog entity updated";
+            default -> "Catalog entity created";
+        };
+    }
+
+    private String buildEntityLifecycleActivityDetail(ObserveEntity entity, String activityType) {
+        String entityKind = defaultText(entity == null ? null : toDefinitionKind(entity.getType()), "entity");
+        String entityName = defaultText(
+                entity == null ? null : entity.getDisplayName(),
+                entity == null ? null : entity.getName(),
+                entityKind
+        );
+        List<String> parts = new ArrayList<>();
+        parts.add(entityKind + ": " + entityName);
+        addLifecycleDetailPart(parts, "source", entity == null ? null : entity.getSource());
+        addLifecycleDetailPart(parts, "owner", entity == null ? null : entity.getOwner());
+        addLifecycleDetailPart(parts, "system", entity == null ? null : entity.getSystem());
+        addLifecycleDetailPart(parts, "environment", entity == null ? null : entity.getEnvironment());
+        if (ACTIVITY_TYPE_DISCOVERY_GOVERNANCE.equals(activityType)) {
+            addLifecycleDetailPart(parts, "evidence",
+                    entity == null ? null : entityMonitorBindService.countMonitorBinds(entity.getId()) + " monitor binds");
+        }
+        return String.join(" · ", parts);
+    }
+
+    private void addLifecycleDetailPart(List<String> parts, String label, String value) {
+        String normalized = trimToNull(value);
+        if (normalized != null) {
+            parts.add(label + ": " + normalized);
+        }
+    }
+
+    private String buildDefinitionActivitySummary(String activityType, String status) {
+        if (ACTIVITY_TYPE_DEFINITION_UPDATE.equals(activityType)) {
+            return ACTIVITY_ERROR.equals(status) ? "Definition update failed" : "Definition updated";
+        }
+        return ACTIVITY_ERROR.equals(status) ? "Definition import failed" : "Definition imported";
+    }
+
+    private String buildDefinitionActivityDetail(ObserveEntity entity, RuntimeException exception) {
+        if (exception != null) {
+            return defaultText(exception.getMessage(), exception.getClass().getSimpleName(), "Definition validation failed");
+        }
+        String entityKind = defaultText(entity == null ? null : toDefinitionKind(entity.getType()), "entity");
+        String detail = defaultText(
+                entity == null ? null : entity.getDisplayName(),
+                entity == null ? null : entity.getName(),
+                entityKind
+        );
+        return entityKind + ": " + detail;
+    }
+
+    private String normalizeDefinitionActivityFormat(String format) {
+        if (!StringUtils.hasText(format)) {
+            return null;
+        }
+        if (FORMAT_CURL.equalsIgnoreCase(defaultText(format, ""))) {
+            return FORMAT_CURL;
+        }
+        if (FORMAT_JSON.equalsIgnoreCase(defaultText(format, ""))) {
+            return FORMAT_JSON;
+        }
+        return FORMAT_YAML;
+    }
+
+    private boolean hasTelemetryDiscoveryBind(List<EntityMonitorBind> existingBinds, List<EntityMonitorBind> nextBinds) {
+        if (CollectionUtils.isEmpty(nextBinds)) {
+            return false;
+        }
+        Set<Long> existingTelemetryDiscoveryMonitors = CollectionUtils.isEmpty(existingBinds)
+                ? Collections.emptySet()
+                : existingBinds.stream()
+                        .filter(bind -> BIND_SOURCE_TELEMETRY_DISCOVERY.equals(bind.getBindSource()))
+                        .map(EntityMonitorBind::getMonitorId)
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toSet());
+        return nextBinds.stream()
+                .filter(bind -> BIND_SOURCE_TELEMETRY_DISCOVERY.equals(bind.getBindSource()))
+                .map(EntityMonitorBind::getMonitorId)
+                .filter(Objects::nonNull)
+                .anyMatch(monitorId -> !existingTelemetryDiscoveryMonitors.contains(monitorId));
+    }
+
+    private boolean isTelemetryLifecycleSource(String source) {
+        return SOURCE_OTEL_RESOURCE.equals(source) || SOURCE_DERIVED.equals(source);
+    }
+
+    private String toDefinitionKind(String entityType) {
+        if (!StringUtils.hasText(entityType)) {
+            return null;
+        }
+        return switch (entityType.trim().toLowerCase()) {
+            case TYPE_DATABASE -> KIND_DATASTORE;
+            case TYPE_API -> KIND_API;
+            default -> entityType.trim().toLowerCase();
+        };
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return "";
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityAlertEvidenceQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityAlertEvidenceQueryService.java
new file mode 100644
index 0000000000..703b488121
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityAlertEvidenceQueryService.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.Expression;
+import jakarta.persistence.criteria.Predicate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hertzbeat.alert.dao.SingleAlertDao;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Query boundary for monitor-linked alert evidence rows.
+ */
+@Service
+public class EntityAlertEvidenceQueryService {
+
+    private static final Set<String> ALERT_WORKSPACE_LABEL_KEYS = Set.of(
+            "hertzbeat.workspace_id",
+            AuthTokenScopes.CLAIM_WORKSPACE_ID,
+            "workspace.id"
+    );
+
+    private final SingleAlertDao singleAlertDao;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityAlertEvidenceQueryService(SingleAlertDao singleAlertDao,
+                                           EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.singleAlertDao = singleAlertDao;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public List<SingleAlert> findActiveAlerts(List<Monitor> monitors, int limit) {
+        return findActiveAlerts(monitors, limit, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public List<SingleAlert> findActiveAlerts(List<Monitor> monitors, int limit, String requestWorkspaceId) {
+        if (CollectionUtils.isEmpty(monitors)) {
+            return Collections.emptyList();
+        }
+        int safeLimit = limit <= 0 ? 20 : limit;
+        PageRequest pageRequest = PageRequest.of(0, safeLimit, Sort.by(Sort.Direction.DESC, "gmtUpdate"));
+        return singleAlertDao.findAll(
+                        buildAlertSpecification(monitors, CommonConstants.ALERT_STATUS_FIRING, requestWorkspaceId),
+                        pageRequest)
+                .getContent()
+                .stream()
+                .filter(alert -> matchesAlertRequestWorkspace(alert, requestWorkspaceId))
+                .toList();
+    }
+
+    public List<SingleAlert> findAlerts(List<Monitor> monitors, String status) {
+        return findAlerts(monitors, status, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public List<SingleAlert> findAlerts(List<Monitor> monitors, String status, String requestWorkspaceId) {
+        if (CollectionUtils.isEmpty(monitors)) {
+            return Collections.emptyList();
+        }
+        return singleAlertDao.findAll(
+                        buildAlertSpecification(monitors, status, requestWorkspaceId),
+                        Sort.by(Sort.Direction.DESC, "gmtUpdate"))
+                .stream()
+                .filter(alert -> matchesAlertRequestWorkspace(alert, requestWorkspaceId))
+                .toList();
+    }
+
+    private Specification<SingleAlert> buildAlertSpecification(List<Monitor> monitors, String status,
+                                                               String requestWorkspaceId) {
+        return (root, query, criteriaBuilder) -> {
+            List<Predicate> andList = new ArrayList<>();
+            if (StringUtils.hasText(status)) {
+                andList.add(criteriaBuilder.equal(root.get("status"), status));
+            }
+            if (StringUtils.hasText(requestWorkspaceId)) {
+                andList.add(buildAlertWorkspacePredicate(criteriaBuilder, root.get("labels"), requestWorkspaceId));
+            }
+            List<Predicate> orList = new ArrayList<>();
+            for (Monitor monitor : monitors) {
+                addJsonLikePredicate(orList, criteriaBuilder, root.get("labels"),
+                        CommonConstants.LABEL_INSTANCE, monitor.getInstance());
+                addJsonLikePredicate(orList, criteriaBuilder, root.get("labels"),
+                        CommonConstants.LABEL_INSTANCE_NAME, monitor.getName());
+                addTextLikePredicate(orList, criteriaBuilder, root.get("content"), monitor.getName());
+                addTextLikePredicate(orList, criteriaBuilder, root.get("content"), monitor.getInstance());
+            }
+            if (orList.isEmpty()) {
+                return criteriaBuilder.and(andList.toArray(new Predicate[0]));
+            }
+            return criteriaBuilder.and(
+                    criteriaBuilder.and(andList.toArray(new Predicate[0])),
+                    criteriaBuilder.or(orList.toArray(new Predicate[0]))
+            );
+        };
+    }
+
+    private Predicate buildAlertWorkspacePredicate(CriteriaBuilder criteriaBuilder, Expression<String> labelsExpression,
+                                                   String requestWorkspaceId) {
+        Expression<String> normalizedLabels = criteriaBuilder.lower(labelsExpression);
+        List<Predicate> workspaceMatches = new ArrayList<>();
+        for (String key : ALERT_WORKSPACE_LABEL_KEYS) {
+            workspaceMatches.add(criteriaBuilder.like(normalizedLabels, jsonKeyValueLikePattern(key, requestWorkspaceId)));
+        }
+        if (!AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(requestWorkspaceId)) {
+            return criteriaBuilder.or(workspaceMatches.toArray(new Predicate[0]));
+        }
+        List<Predicate> workspaceKeyMissing = new ArrayList<>();
+        for (String key : ALERT_WORKSPACE_LABEL_KEYS) {
+            workspaceKeyMissing.add(criteriaBuilder.notLike(normalizedLabels, jsonKeyLikePattern(key)));
+        }
+        workspaceMatches.add(criteriaBuilder.isNull(labelsExpression));
+        workspaceMatches.add(criteriaBuilder.and(workspaceKeyMissing.toArray(new Predicate[0])));
+        return criteriaBuilder.or(workspaceMatches.toArray(new Predicate[0]));
+    }
+
+    private boolean matchesAlertRequestWorkspace(SingleAlert alert, String requestWorkspaceId) {
+        if (!StringUtils.hasText(requestWorkspaceId)) {
+            return true;
+        }
+        Map<String, String> labels = alert == null ? null : alert.getLabels();
+        if (CollectionUtils.isEmpty(labels)) {
+            return AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(requestWorkspaceId);
+        }
+        for (String key : ALERT_WORKSPACE_LABEL_KEYS) {
+            String workspaceId = trimToNull(labels.get(key));
+            if (workspaceId != null) {
+                return requestWorkspaceId.equals(AuthTokenScopes.normalizeWorkspaceId(workspaceId));
+            }
+        }
+        return AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(requestWorkspaceId);
+    }
+
+    private String jsonKeyValueLikePattern(String key, String value) {
+        return String.format("%%\"%s\":\"%s\"%%", key, value).toLowerCase(Locale.ROOT);
+    }
+
+    private String jsonKeyLikePattern(String key) {
+        return String.format("%%\"%s\":%%", key).toLowerCase(Locale.ROOT);
+    }
+
+    private void addJsonLikePredicate(List<Predicate> predicates, CriteriaBuilder criteriaBuilder,
+                                      Expression<String> expression, String key, String value) {
+        if (!StringUtils.hasText(value)) {
+            return;
+        }
+        String pattern = String.format("%%\"%s\":\"%s\"%%", key, value);
+        predicates.add(criteriaBuilder.like(criteriaBuilder.lower(expression), pattern.toLowerCase(Locale.ROOT)));
+    }
+
+    private void addTextLikePredicate(List<Predicate> predicates, CriteriaBuilder criteriaBuilder,
+                                      Expression<String> expression, String value) {
+        if (!StringUtils.hasText(value)) {
+            return;
+        }
+        predicates.add(criteriaBuilder.like(criteriaBuilder.lower(expression), "%" + value.toLowerCase(Locale.ROOT) + "%"));
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityAlertEvidenceReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityAlertEvidenceReadModelService.java
new file mode 100644
index 0000000000..345dffb12a
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityAlertEvidenceReadModelService.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Reads monitor-linked alert evidence for observed entities.
+ */
+@Service
+public class EntityAlertEvidenceReadModelService {
+
+    private static final String ALERT_STATUS_ACKNOWLEDGED = "acknowledged";
+
+    private final EntityAlertEvidenceQueryService entityAlertEvidenceQueryService;
+
+    public EntityAlertEvidenceReadModelService(EntityAlertEvidenceQueryService entityAlertEvidenceQueryService) {
+        this.entityAlertEvidenceQueryService = entityAlertEvidenceQueryService;
+    }
+
+    public List<SingleAlert> queryActiveAlerts(List<Monitor> monitors, int limit) {
+        if (CollectionUtils.isEmpty(monitors)) {
+            return Collections.emptyList();
+        }
+        return entityAlertEvidenceQueryService.findActiveAlerts(monitors, limit);
+    }
+
+    public List<SingleAlert> queryActiveAlerts(List<Monitor> monitors, int limit, String requestWorkspaceId) {
+        if (CollectionUtils.isEmpty(monitors)) {
+            return Collections.emptyList();
+        }
+        return entityAlertEvidenceQueryService.findActiveAlerts(monitors, limit, requestWorkspaceId);
+    }
+
+    public Page<SingleAlert> buildEntityAlertPage(List<Monitor> monitors, String status, String severity,
+                                                  int pageIndex, int pageSize) {
+        PageRequest pageRequest = normalizePageRequest(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "gmtUpdate"));
+        if (CollectionUtils.isEmpty(monitors)) {
+            return new PageImpl<>(Collections.emptyList(), pageRequest, 0);
+        }
+        String statusFilter = normalizeAlertStatusFilter(status);
+        String severityFilter = normalizeAlertSeverityFilter(severity);
+        List<SingleAlert> filteredAlerts = entityAlertEvidenceQueryService.findAlerts(monitors, statusFilter)
+                .stream()
+                .filter(alert -> severityFilter == null || severityFilter.equals(resolveAlertSeverity(alert)))
+                .sorted(alertEvidenceSort())
+                .toList();
+        return slicePage(filteredAlerts, pageRequest);
+    }
+
+    public Page<SingleAlert> buildEntityAlertPage(List<Monitor> monitors, String status, String severity,
+                                                  int pageIndex, int pageSize, String requestWorkspaceId) {
+        PageRequest pageRequest = normalizePageRequest(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "gmtUpdate"));
+        if (CollectionUtils.isEmpty(monitors)) {
+            return new PageImpl<>(Collections.emptyList(), pageRequest, 0);
+        }
+        String statusFilter = normalizeAlertStatusFilter(status);
+        String severityFilter = normalizeAlertSeverityFilter(severity);
+        List<SingleAlert> filteredAlerts = entityAlertEvidenceQueryService.findAlerts(
+                        monitors, statusFilter, requestWorkspaceId)
+                .stream()
+                .filter(alert -> severityFilter == null || severityFilter.equals(resolveAlertSeverity(alert)))
+                .sorted(alertEvidenceSort())
+                .toList();
+        return slicePage(filteredAlerts, pageRequest);
+    }
+
+    private Comparator<SingleAlert> alertEvidenceSort() {
+        return Comparator
+                .comparingInt((SingleAlert alert) -> severityPriority(resolveAlertSeverity(alert))).reversed()
+                .thenComparing(SingleAlert::getGmtUpdate, Comparator.nullsLast(Comparator.reverseOrder()))
+                .thenComparing(SingleAlert::getId, Comparator.nullsLast(Comparator.reverseOrder()));
+    }
+
+    private String normalizeAlertStatusFilter(String status) {
+        String normalized = trimToNull(status);
+        if (normalized == null) {
+            return CommonConstants.ALERT_STATUS_FIRING;
+        }
+        String lowered = normalized.toLowerCase(Locale.ROOT);
+        if (ALERT_STATUS_ACKNOWLEDGED.equals(lowered)) {
+            return ALERT_STATUS_ACKNOWLEDGED;
+        }
+        if (CommonConstants.ALERT_STATUS_RESOLVED.equals(lowered)) {
+            return CommonConstants.ALERT_STATUS_RESOLVED;
+        }
+        return CommonConstants.ALERT_STATUS_FIRING;
+    }
+
+    private String normalizeAlertSeverityFilter(String severity) {
+        String normalized = trimToNull(severity);
+        if (normalized == null || "all".equalsIgnoreCase(normalized)) {
+            return null;
+        }
+        return switch (normalized.toLowerCase(Locale.ROOT)) {
+            case "warn" -> "warning";
+            case "err" -> "error";
+            default -> normalized.toLowerCase(Locale.ROOT);
+        };
+    }
+
+    private String resolveAlertSeverity(SingleAlert alert) {
+        if (alert == null) {
+            return "unknown";
+        }
+        String severity = defaultText(
+                trimToNull(alert.getLabels() == null ? null : alert.getLabels().get("severity")),
+                defaultText(
+                        trimToNull(alert.getLabels() == null ? null : alert.getLabels().get("priority")),
+                        trimToNull(alert.getAnnotations() == null ? null : alert.getAnnotations().get("severity"))
+                )
+        );
+        return severity == null ? "unknown" : severity.toLowerCase(Locale.ROOT);
+    }
+
+    private int severityPriority(String severity) {
+        String normalized = trimToNull(severity);
+        if (normalized == null) {
+            return 0;
+        }
+        return switch (normalized.toLowerCase(Locale.ROOT)) {
+            case "critical", "fatal", "emergency", "severe" -> 5;
+            case "error", "high" -> 4;
+            case "warning", "warn", "medium" -> 3;
+            case "info", "low" -> 2;
+            case "debug", "trace" -> 1;
+            default -> 0;
+        };
+    }
+
+    private PageRequest normalizePageRequest(int pageIndex, int pageSize, Sort sort) {
+        int safePageIndex = Math.max(pageIndex, 0);
+        int safePageSize = pageSize <= 0 ? 10 : Math.min(pageSize, 100);
+        return PageRequest.of(safePageIndex, safePageSize, sort);
+    }
+
+    private <T> Page<T> slicePage(List<T> items, PageRequest pageRequest) {
+        if (CollectionUtils.isEmpty(items)) {
+            return new PageImpl<>(Collections.emptyList(), pageRequest, 0);
+        }
+        int start = Math.min((int) pageRequest.getOffset(), items.size());
+        int end = Math.min(start + pageRequest.getPageSize(), items.size());
+        return new PageImpl<>(items.subList(start, end), pageRequest, items.size());
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCatalogProfileService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCatalogProfileService.java
new file mode 100644
index 0000000000..df49148b45
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCatalogProfileService.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.EntityCatalogLink;
+import org.apache.hertzbeat.common.entity.manager.EntityOwnerRef;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.manager.pojo.dto.EntityCatalogSuggestionsInfo;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Builds catalog profile suggestions from existing entity rows.
+ */
+@Service
+public class EntityCatalogProfileService {
+
+    private static final String TYPE_DATABASE = "database";
+    private static final String TYPE_API = "api";
+    private static final String KIND_DATASTORE = "datastore";
+    private static final String KIND_API = "api";
+
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityCatalogProfileService(EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public EntityCatalogSuggestionsInfo getCatalogSuggestions(int limit) {
+        int suggestionLimit = limit <= 0 ? 12 : Math.min(limit, 200);
+        Sort sort = Sort.by(Sort.Order.desc("gmtUpdate"), Sort.Order.desc("id"));
+        return buildSuggestions(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(sort),
+                suggestionLimit);
+    }
+
+    public EntityCatalogSuggestionsInfo getCatalogSuggestions(int limit, String requestWorkspaceId) {
+        int suggestionLimit = limit <= 0 ? 12 : Math.min(limit, 200);
+        Sort sort = Sort.by(Sort.Order.desc("gmtUpdate"), Sort.Order.desc("id"));
+        return buildSuggestions(entityWorkspaceAccessService.findAccessibleEntities(requestWorkspaceId, sort),
+                suggestionLimit);
+    }
+
+    private EntityCatalogSuggestionsInfo buildSuggestions(List<ObserveEntity> entities, int suggestionLimit) {
+        LinkedHashSet<String> owners = new LinkedHashSet<>();
+        LinkedHashSet<String> namespaces = new LinkedHashSet<>();
+        LinkedHashSet<String> environments = new LinkedHashSet<>();
+        LinkedHashSet<String> systems = new LinkedHashSet<>();
+        LinkedHashSet<String> lifecycles = new LinkedHashSet<>();
+        LinkedHashSet<String> tiers = new LinkedHashSet<>();
+        LinkedHashSet<String> inheritFromRefs = new LinkedHashSet<>();
+        LinkedHashSet<String> entityRefs = new LinkedHashSet<>();
+        LinkedHashSet<String> languages = new LinkedHashSet<>();
+        LinkedHashSet<String> linkProviders = new LinkedHashSet<>();
+        for (ObserveEntity entity : entities) {
+            addSuggestion(owners, entity.getOwner(), suggestionLimit);
+            addSuggestion(namespaces, entity.getNamespace(), suggestionLimit);
+            addSuggestion(environments, entity.getEnvironment(), suggestionLimit);
+            addSuggestion(systems, entity.getSystem(), suggestionLimit);
+            addSuggestion(lifecycles, entity.getLifecycle(), suggestionLimit);
+            addSuggestion(tiers, entity.getTier(), suggestionLimit);
+            addSuggestion(inheritFromRefs, entity.getInheritFrom(), suggestionLimit);
+            addSuggestion(entityRefs, buildEntityReference(entity), suggestionLimit);
+            if (!CollectionUtils.isEmpty(entity.getAdditionalOwners())) {
+                entity.getAdditionalOwners().stream()
+                        .map(EntityOwnerRef::getName)
+                        .forEach(value -> addSuggestion(owners, value, suggestionLimit));
+            }
+            if (!CollectionUtils.isEmpty(entity.getLanguages())) {
+                entity.getLanguages().forEach(value -> addSuggestion(languages, value, suggestionLimit));
+            }
+            if (!CollectionUtils.isEmpty(entity.getLinks())) {
+                entity.getLinks().stream()
+                        .map(EntityCatalogLink::getProvider)
+                        .forEach(value -> addSuggestion(linkProviders, value, suggestionLimit));
+            }
+        }
+        return new EntityCatalogSuggestionsInfo(
+                List.copyOf(owners),
+                List.copyOf(namespaces),
+                List.copyOf(environments),
+                List.copyOf(systems),
+                List.copyOf(lifecycles),
+                List.copyOf(tiers),
+                List.copyOf(inheritFromRefs),
+                List.copyOf(entityRefs),
+                List.copyOf(languages),
+                List.copyOf(linkProviders)
+        );
+    }
+
+    private String buildEntityReference(ObserveEntity entity) {
+        if (entity == null || !StringUtils.hasText(entity.getType()) || !StringUtils.hasText(entity.getName())) {
+            return null;
+        }
+        String namespace = defaultText(entity.getNamespace(), "default");
+        return toDefinitionKind(entity.getType()) + ":" + namespace + "/" + entity.getName();
+    }
+
+    private String toDefinitionKind(String entityType) {
+        if (!StringUtils.hasText(entityType)) {
+            return null;
+        }
+        return switch (entityType.trim().toLowerCase()) {
+            case TYPE_DATABASE -> KIND_DATASTORE;
+            case TYPE_API -> KIND_API;
+            default -> entityType.trim().toLowerCase();
+        };
+    }
+
+    private void addSuggestion(LinkedHashSet<String> values, String value, int limit) {
+        String normalizedValue = trimToNull(value);
+        if (!StringUtils.hasText(normalizedValue) || values.size() >= limit) {
+            return;
+        }
+        values.add(normalizedValue);
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCatalogQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCatalogQueryService.java
new file mode 100644
index 0000000000..b513097539
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCatalogQueryService.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import com.google.common.primitives.Longs;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.Predicate;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Catalog query boundary for entity list pages and old API compatibility filters.
+ */
+@Service
+public class EntityCatalogQueryService {
+
+    private static final String TYPE_API = "api";
+    private static final String TYPE_ENDPOINT = "endpoint";
+
+    private final EntityWorkspaceQueryService entityWorkspaceQueryService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityCatalogQueryService(EntityWorkspaceQueryService entityWorkspaceQueryService,
+                                     EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityWorkspaceQueryService = entityWorkspaceQueryService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public Page<ObserveEntity> findEntityPage(List<Long> entityIds, String type, String status, String search,
+                                              String owner, String source, String environment, String lifecycle,
+                                              String tier, String system, String sort, String order,
+                                              int pageIndex, int pageSize) {
+        return findEntityPage(entityIds, type, status, search, owner, source, environment, lifecycle, tier, system,
+                sort, order, pageIndex, pageSize, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public Page<ObserveEntity> findEntityPage(List<Long> entityIds, String type, String status, String search,
+                                              String owner, String source, String environment, String lifecycle,
+                                              String tier, String system, String sort, String order,
+                                              int pageIndex, int pageSize, String requestWorkspaceId) {
+        PageRequest pageRequest = buildPageRequest(sort, order, pageIndex, pageSize);
+        Page<ObserveEntity> entityPage = entityWorkspaceQueryService.findEntityPage(
+                buildEntitySpecification(entityIds, type, status, search, owner, source, environment,
+                        lifecycle, tier, system, requestWorkspaceId),
+                pageRequest
+        );
+        return filterPageByRequestWorkspace(entityPage, requestWorkspaceId);
+    }
+
+    private PageRequest buildPageRequest(String sort, String order, int pageIndex, int pageSize) {
+        return PageRequest.of(
+                pageIndex,
+                pageSize,
+                Sort.by(new Sort.Order(
+                        Sort.Direction.fromString(StringUtils.hasText(order) ? order : "desc"),
+                        StringUtils.hasText(sort) ? sort : "gmtUpdate"))
+        );
+    }
+
+    private Page<ObserveEntity> filterPageByRequestWorkspace(Page<ObserveEntity> entityPage,
+                                                             String requestWorkspaceId) {
+        if (!StringUtils.hasText(requestWorkspaceId)) {
+            return entityPage;
+        }
+        List<ObserveEntity> scopedEntities = entityWorkspaceAccessService.filterEntitiesByRequestWorkspace(
+                entityPage.getContent(), requestWorkspaceId);
+        if (scopedEntities.size() == entityPage.getContent().size()) {
+            return entityPage;
+        }
+        return new PageImpl<>(scopedEntities, entityPage.getPageable(), scopedEntities.size());
+    }
+
+    private Specification<ObserveEntity> buildEntitySpecification(List<Long> entityIds, String type, String status,
+                                                                  String search, String owner, String source,
+                                                                  String environment, String lifecycle,
+                                                                  String tier, String system,
+                                                                  String requestWorkspaceId) {
+        return (root, query, criteriaBuilder) -> {
+            List<Predicate> andList = new ArrayList<>();
+            if (StringUtils.hasText(requestWorkspaceId)) {
+                andList.add(criteriaBuilder.equal(root.get("workspaceId"), requestWorkspaceId));
+            }
+            if (!CollectionUtils.isEmpty(entityIds)) {
+                CriteriaBuilder.In<Long> inPredicate = criteriaBuilder.in(root.get("id"));
+                for (Long entityId : entityIds) {
+                    inPredicate.value(entityId);
+                }
+                andList.add(inPredicate);
+            }
+            if (StringUtils.hasText(type)) {
+                if (TYPE_API.equals(type)) {
+                    Predicate apiType = criteriaBuilder.equal(root.get("type"), TYPE_API);
+                    Predicate legacyApiEndpoint = criteriaBuilder.and(
+                            criteriaBuilder.equal(root.get("type"), TYPE_ENDPOINT),
+                            criteriaBuilder.or(
+                                    criteriaBuilder.isNotNull(root.get("implementedBy")),
+                                    criteriaBuilder.isNotNull(root.get("apiInterface")),
+                                    criteriaBuilder.like(criteriaBuilder.lower(root.get("subtype")), "%api%")
+                            )
+                    );
+                    andList.add(criteriaBuilder.or(apiType, legacyApiEndpoint));
+                } else {
+                    andList.add(criteriaBuilder.equal(root.get("type"), type));
+                }
+            }
+            if (StringUtils.hasText(status)) {
+                andList.add(criteriaBuilder.equal(root.get("status"), status));
+            }
+            if (StringUtils.hasText(owner)) {
+                andList.add(criteriaBuilder.equal(root.get("owner"), owner));
+            }
+            if (StringUtils.hasText(source)) {
+                andList.add(criteriaBuilder.equal(root.get("source"), source));
+            }
+            if (StringUtils.hasText(environment)) {
+                andList.add(criteriaBuilder.equal(root.get("environment"), environment));
+            }
+            if (StringUtils.hasText(lifecycle)) {
+                andList.add(criteriaBuilder.equal(root.get("lifecycle"), lifecycle));
+            }
+            if (StringUtils.hasText(tier)) {
+                andList.add(criteriaBuilder.equal(root.get("tier"), tier));
+            }
+            if (StringUtils.hasText(system)) {
+                andList.add(criteriaBuilder.equal(root.get("system"), system));
+            }
+            List<Predicate> orList = buildSearchPredicates(search, root, criteriaBuilder);
+            Predicate andPredicate = andList.isEmpty() ? null : criteriaBuilder.and(andList.toArray(new Predicate[0]));
+            Predicate orPredicate = orList.isEmpty() ? null : criteriaBuilder.or(orList.toArray(new Predicate[0]));
+            if (andPredicate == null && orPredicate == null) {
+                return query.where().getRestriction();
+            }
+            if (andPredicate == null) {
+                return orPredicate;
+            }
+            if (orPredicate == null) {
+                return andPredicate;
+            }
+            return query.where(andPredicate, orPredicate).getRestriction();
+        };
+    }
+
+    private List<Predicate> buildSearchPredicates(String search,
+                                                  jakarta.persistence.criteria.Root<ObserveEntity> root,
+                                                  CriteriaBuilder criteriaBuilder) {
+        List<Predicate> orList = new ArrayList<>();
+        if (!StringUtils.hasText(search)) {
+            return orList;
+        }
+        String lower = "%" + search.toLowerCase() + "%";
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("displayName")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("subtype")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("namespace")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("environment")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("description")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("owner")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("lifecycle")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("tier")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("system")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("runbook")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("inheritFrom")), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("labels").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("tags").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("additionalOwners").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("componentOf").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("components").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("implementedBy").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("apiInterface").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("languages").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("links").as(String.class)), lower));
+        orList.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("contacts").as(String.class)), lower));
+        Long id = Longs.tryParse(search);
+        if (id != null) {
+            orList.add(criteriaBuilder.equal(root.get("id"), id));
+        }
+        return orList;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCoreWriteModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCoreWriteModelService.java
new file mode 100644
index 0000000000..7f4998b4f8
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityCoreWriteModelService.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
+import org.apache.hertzbeat.manager.dao.ObserveEntityDao;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Owns core catalog field application for entity writes.
+ */
+@Service
+public class EntityCoreWriteModelService {
+
+    private static final String STATUS_UNKNOWN = "unknown";
+    private static final String SOURCE_MANUAL = "manual";
+
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+    private final ObserveEntityDao observeEntityDao;
+
+    public EntityCoreWriteModelService(EntityWorkspaceAccessService entityWorkspaceAccessService,
+                                       ObserveEntityDao observeEntityDao) {
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+        this.observeEntityDao = observeEntityDao;
+    }
+
+    public ObserveEntity createEntity(ObserveEntity input, String fallbackSource) {
+        ObserveEntity entity = new ObserveEntity();
+        entity.setId(input.getId() == null ? SnowFlakeIdGenerator.generateId() : input.getId());
+        return saveEntityCore(entity, input, fallbackSource);
+    }
+
+    public List<ObserveEntity> createEntities(List<ObserveEntity> inputs, String fallbackSource) {
+        if (CollectionUtils.isEmpty(inputs)) {
+            return Collections.emptyList();
+        }
+        List<ObserveEntity> entities = inputs.stream()
+                .map(input -> {
+                    ObserveEntity entity = new ObserveEntity();
+                    entity.setId(input.getId() == null ? SnowFlakeIdGenerator.generateId() : input.getId());
+                    applyEntityCore(entity, input, fallbackSource);
+                    return entity;
+                })
+                .toList();
+        return observeEntityDao.saveAll(entities);
+    }
+
+    public ObserveEntity saveEntityCore(ObserveEntity target, ObserveEntity source, String fallbackSource) {
+        applyEntityCore(target, source, fallbackSource);
+        return observeEntityDao.save(target);
+    }
+
+    public void deleteEntityById(long entityId) {
+        observeEntityDao.deleteById(entityId);
+    }
+
+    public ObserveEntity persistStatus(ObserveEntity entity, String status) {
+        entity.setStatus(status);
+        return observeEntityDao.save(entity);
+    }
+
+    public void applyEntityCore(ObserveEntity target, ObserveEntity source, String fallbackSource) {
+        target.setType(source.getType());
+        target.setName(source.getName());
+        target.setDisplayName(source.getDisplayName());
+        target.setSubtype(source.getSubtype());
+        target.setNamespace(source.getNamespace());
+        target.setEnvironment(source.getEnvironment());
+        target.setStatus(defaultText(source.getStatus(), STATUS_UNKNOWN));
+        target.setCriticality(source.getCriticality());
+        target.setOwner(source.getOwner());
+        target.setAdditionalOwners(source.getAdditionalOwners());
+        target.setRunbook(source.getRunbook());
+        target.setLifecycle(source.getLifecycle());
+        target.setTier(source.getTier());
+        target.setSystem(source.getSystem());
+        target.setComponentOf(source.getComponentOf());
+        target.setComponents(source.getComponents());
+        target.setImplementedBy(source.getImplementedBy());
+        target.setApiInterface(source.getApiInterface());
+        target.setInheritFrom(source.getInheritFrom());
+        target.setLanguages(source.getLanguages());
+        target.setLinks(source.getLinks());
+        target.setContacts(source.getContacts());
+        target.setIntegrations(source.getIntegrations());
+        target.setExtensions(source.getExtensions());
+        target.setHertzbeat(source.getHertzbeat());
+        target.setSource(defaultText(source.getSource(), fallbackSource, SOURCE_MANUAL));
+        target.setDescription(source.getDescription());
+        target.setLabels(source.getLabels());
+        target.setTags(normalizeTags(source.getTags(), source.getLabels()));
+        target.setWorkspaceId(entityWorkspaceAccessService.resolveWriteWorkspaceId(
+                source.getWorkspaceId(), target.getWorkspaceId()));
+    }
+
+    private List<String> normalizeTags(List<String> tags, Map<String, String> labels) {
+        List<String> normalized = (tags == null ? Collections.<String>emptyList() : tags).stream()
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .toList();
+        if (!CollectionUtils.isEmpty(normalized)) {
+            return normalized;
+        }
+        if (CollectionUtils.isEmpty(labels)) {
+            return Collections.emptyList();
+        }
+        return labels.entrySet().stream()
+                .filter(entry -> StringUtils.hasText(entry.getKey()))
+                .map(entry -> StringUtils.hasText(entry.getValue())
+                        ? entry.getKey().trim() + ":" + entry.getValue().trim()
+                        : entry.getKey().trim())
+                .toList();
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentFieldNormalizationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentFieldNormalizationService.java
new file mode 100644
index 0000000000..dc63de3b7a
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentFieldNormalizationService.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * Normalizes common entity definition document field fallbacks.
+ */
+@Service
+public class EntityDefinitionDocumentFieldNormalizationService {
+
+    public Map<String, Object> toObjectMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    public Map<String, Object> resolveDefinitionSpecMap(Map<String, Object> root) {
+        if (root == null) {
+            return Collections.emptyMap();
+        }
+        return toObjectMap(root.get("spec"));
+    }
+
+    public Map<String, Object> resolveDefinitionTelemetryMap(Map<String, Object> specMap) {
+        if (specMap == null) {
+            return Collections.emptyMap();
+        }
+        return toObjectMap(specMap.get("telemetry"));
+    }
+
+    public String asText(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value);
+        return StringUtils.hasText(text) ? text.trim() : null;
+    }
+
+    public Object firstNonNull(Object... values) {
+        if (values == null) {
+            return null;
+        }
+        for (Object value : values) {
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    public String defaultText(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return null;
+    }
+
+    public String resolveDefinitionApiVersion(Map<String, Object> root, String defaultApiVersion) {
+        return defaultText(
+                asText(root.get("apiVersion")),
+                asText(root.get("schema-version")),
+                asText(root.get("schema_version")),
+                defaultApiVersion
+        );
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentParserService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentParserService.java
new file mode 100644
index 0000000000..378c349829
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentParserService.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Parses raw entity definition requests into ordered document records.
+ */
+@Service
+public class EntityDefinitionDocumentParserService {
+
+    private static final String FORMAT_JSON = "json";
+    private static final String FORMAT_CURL = "curl";
+
+    public List<Map<String, Object>> parseDefinitionRecords(EntityDefinitionRequest definitionRequest) {
+        String payload = extractDefinitionPayload(definitionRequest.getContent(), definitionRequest.getFormat());
+        String format = normalizeDefinitionFormat(definitionRequest.getFormat(), payload);
+        return parseDefinitionDocuments(payload, format);
+    }
+
+    private List<Map<String, Object>> parseDefinitionDocuments(String payload, String format) {
+        if (FORMAT_JSON.equals(format)) {
+            return toDefinitionRecords(JsonUtil.fromJson(payload, Object.class));
+        }
+        List<Map<String, Object>> documents = new ArrayList<>();
+        for (Object document : new Yaml().loadAll(payload)) {
+            if (document == null) {
+                continue;
+            }
+            documents.addAll(toDefinitionRecords(document));
+        }
+        return documents;
+    }
+
+    private List<Map<String, Object>> toDefinitionRecords(Object value) {
+        if (value instanceof List<?> items) {
+            List<Map<String, Object>> documents = new ArrayList<>();
+            for (Object item : items) {
+                if (item == null) {
+                    continue;
+                }
+                documents.addAll(toDefinitionRecords(item));
+            }
+            return documents;
+        }
+        if (value instanceof Map<?, ?> rawMap && isKubernetesList(rawMap)) {
+            return toDefinitionRecords(rawMap.get("items"));
+        }
+        return List.of(toDefinitionRecord(value));
+    }
+
+    private String extractDefinitionPayload(String content, String format) {
+        String trimmed = content == null ? null : content.trim();
+        if (!StringUtils.hasText(trimmed)) {
+            throw new IllegalArgumentException("Entity definition content can not be blank.");
+        }
+        if (!FORMAT_CURL.equalsIgnoreCase(defaultText(format, ""))) {
+            return trimmed;
+        }
+        int singleQuoteIndex = trimmed.indexOf("-d '");
+        if (singleQuoteIndex >= 0) {
+            int payloadStart = singleQuoteIndex + 4;
+            int payloadEnd = trimmed.lastIndexOf('\'');
+            if (payloadEnd > payloadStart) {
+                return trimmed.substring(payloadStart, payloadEnd)
+                        .replace("'\\\\''", "'")
+                        .replace("\\\\", "\\");
+            }
+        }
+        int doubleQuoteIndex = trimmed.indexOf("-d \"");
+        if (doubleQuoteIndex >= 0) {
+            int payloadStart = doubleQuoteIndex + 4;
+            int payloadEnd = trimmed.lastIndexOf('"');
+            if (payloadEnd > payloadStart) {
+                return trimmed.substring(payloadStart, payloadEnd)
+                        .replace("\\\"", "\"")
+                        .replace("\\\\", "\\");
+            }
+        }
+        return trimmed;
+    }
+
+    private String normalizeDefinitionFormat(String format, String payload) {
+        String normalizedFormat = defaultText(format, "").toLowerCase(Locale.ROOT);
+        if (FORMAT_JSON.equals(normalizedFormat) || JsonUtil.isJsonStr(payload)) {
+            return FORMAT_JSON;
+        }
+        return "yaml";
+    }
+
+    private Map<String, Object> toDefinitionRecord(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            throw new IllegalArgumentException("Entity definition must be a yaml or json object.");
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    private boolean isKubernetesList(Map<?, ?> rawMap) {
+        return "List".equals(rawMap.get("kind")) && rawMap.containsKey("items");
+    }
+
+    private String defaultText(String... values) {
+        if (values == null) {
+            return "";
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return "";
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentRendererService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentRendererService.java
new file mode 100644
index 0000000000..e86eb06a4d
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDocumentRendererService.java
@@ -0,0 +1,308 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+
+/**
+ * Renders canonical entity definition documents.
+ */
+@Service
+public class EntityDefinitionDocumentRendererService {
+
+    private static final String FORMAT_JSON = "json";
+    private static final ObjectMapper PRETTY_JSON_MAPPER = JsonMapper.builder().build();
+
+    public String renderDefinition(EntityDefinition definition, String format) {
+        String normalizedFormat = normalizeDefinitionFormat(format);
+        if (FORMAT_JSON.equals(normalizedFormat)) {
+            try {
+                return PRETTY_JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(toDefinitionMap(definition));
+            } catch (Exception e) {
+                throw new IllegalArgumentException("Fail to render entity definition json.", e);
+            }
+        }
+        DumperOptions dumperOptions = new DumperOptions();
+        dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+        dumperOptions.setPrettyFlow(true);
+        dumperOptions.setIndent(2);
+        dumperOptions.setIndicatorIndent(1);
+        dumperOptions.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN);
+        return new Yaml(dumperOptions).dumpAsMap(toDefinitionMap(definition));
+    }
+
+    private String normalizeDefinitionFormat(String format) {
+        if (FORMAT_JSON.equalsIgnoreCase(defaultText(format, ""))) {
+            return FORMAT_JSON;
+        }
+        return "yaml";
+    }
+
+    private Map<String, Object> toDefinitionMap(EntityDefinition definition) {
+        Map<String, Object> root = new LinkedHashMap<>();
+        root.put("apiVersion", definition.getApiVersion());
+        root.put("kind", definition.getKind());
+
+        Map<String, Object> metadata = new LinkedHashMap<>();
+        if (definition.getMetadata() != null) {
+            putIfPresent(metadata, "name", definition.getMetadata().getName());
+            putIfPresent(metadata, "namespace", definition.getMetadata().getNamespace());
+            putIfPresent(metadata, "owner", definition.getMetadata().getOwner());
+            if (!CollectionUtils.isEmpty(definition.getMetadata().getAdditionalOwners())) {
+                metadata.put("additionalOwners", definition.getMetadata().getAdditionalOwners().stream().map(owner -> {
+                    Map<String, Object> item = new LinkedHashMap<>();
+                    putIfPresent(item, "name", owner.getName());
+                    putIfPresent(item, "type", owner.getType());
+                    return item;
+                }).toList());
+            }
+            putIfPresent(metadata, "inheritFrom", definition.getMetadata().getInheritFrom());
+            putIfPresent(metadata, "displayName", definition.getMetadata().getDisplayName());
+            putIfPresent(metadata, "description", definition.getMetadata().getDescription());
+            if (!CollectionUtils.isEmpty(definition.getMetadata().getTags())) {
+                metadata.put("tags", definition.getMetadata().getTags());
+            }
+            if (!CollectionUtils.isEmpty(definition.getMetadata().getLabels())) {
+                metadata.put("labels", definition.getMetadata().getLabels());
+            }
+            if (!CollectionUtils.isEmpty(definition.getMetadata().getLinks())) {
+                metadata.put("links", definition.getMetadata().getLinks().stream().map(link -> {
+                    Map<String, Object> item = new LinkedHashMap<>();
+                    putIfPresent(item, "name", link.getName());
+                    putIfPresent(item, "type", link.getType());
+                    putIfPresent(item, "provider", link.getProvider());
+                    putIfPresent(item, "url", link.getUrl());
+                    return item;
+                }).toList());
+            }
+            if (!CollectionUtils.isEmpty(definition.getMetadata().getContacts())) {
+                metadata.put("contacts", definition.getMetadata().getContacts().stream().map(contact -> {
+                    Map<String, Object> item = new LinkedHashMap<>();
+                    putIfPresent(item, "name", contact.getName());
+                    putIfPresent(item, "type", contact.getType());
+                    putIfPresent(item, "contact", defaultText(contact.getContact(), contact.getValue()));
+                    return item;
+                }).toList());
+            }
+        }
+        root.put("metadata", metadata);
+
+        Map<String, Object> spec = new LinkedHashMap<>();
+        if (definition.getSpec() != null) {
+            putIfPresent(spec, "type", definition.getSpec().getType());
+            putIfPresent(spec, "source", definition.getSpec().getSource());
+            putIfPresent(spec, "ownedBy", definition.getSpec().getOwnedBy());
+            putIfPresent(spec, "environment", definition.getSpec().getEnvironment());
+            putIfPresent(spec, "criticality", definition.getSpec().getCriticality());
+            putIfPresent(spec, "runbook", definition.getSpec().getRunbook());
+            putIfPresent(spec, "lifecycle", definition.getSpec().getLifecycle());
+            putIfPresent(spec, "tier", definition.getSpec().getTier());
+            putIfPresent(spec, "partOf", defaultText(definition.getSpec().getPartOf(), definition.getSpec().getSystem()));
+            if (!CollectionUtils.isEmpty(definition.getSpec().getComponentOf())) {
+                spec.put("componentOf", definition.getSpec().getComponentOf());
+            }
+            if (!CollectionUtils.isEmpty(definition.getSpec().getComponents())) {
+                spec.put("components", definition.getSpec().getComponents());
+            }
+            if (!CollectionUtils.isEmpty(definition.getSpec().getImplementedBy())) {
+                spec.put("implementedBy", definition.getSpec().getImplementedBy());
+            }
+            if (definition.getSpec().getApiInterface() != null) {
+                Map<String, Object> apiInterface = new LinkedHashMap<>();
+                if (definition.getSpec().getApiInterface().getDefinition() != null) {
+                    apiInterface.put("definition", definition.getSpec().getApiInterface().getDefinition());
+                }
+                putIfPresent(apiInterface, "fileRef", definition.getSpec().getApiInterface().getFileRef());
+                if (!apiInterface.isEmpty()) {
+                    spec.put("interface", apiInterface);
+                }
+            }
+            if (!CollectionUtils.isEmpty(definition.getSpec().getLanguages())) {
+                spec.put("languages", definition.getSpec().getLanguages());
+            }
+            putTelemetryIfPresent(spec, definition.getSpec().getTelemetry());
+            if (!CollectionUtils.isEmpty(definition.getSpec().getRelations())) {
+                if (!CollectionUtils.isEmpty(definition.getSpec().getDependsOn())) {
+                    spec.put("dependsOn", definition.getSpec().getDependsOn());
+                }
+                spec.put("relations", definition.getSpec().getRelations().stream().map(relation -> {
+                    Map<String, Object> item = new LinkedHashMap<>();
+                    if (relation.getTargetEntityId() != null) {
+                        item.put("targetEntityId", relation.getTargetEntityId());
+                    }
+                    putIfPresent(item, "targetRef", relation.getTargetRef());
+                    putIfPresent(item, "relationType", relation.getRelationType());
+                    putIfPresent(item, "relationSource", relation.getRelationSource());
+                    putIfPresent(item, "status", relation.getStatus());
+                    if (relation.getScore() != null) {
+                        item.put("score", relation.getScore());
+                    }
+                    putIfPresent(item, "description", relation.getDescription());
+                    if (!CollectionUtils.isEmpty(relation.getAttributes())) {
+                        item.put("attributes", relation.getAttributes());
+                    }
+                    return item;
+                }).toList());
+            }
+        }
+        root.put("spec", spec);
+        if (!CollectionUtils.isEmpty(definition.getIntegrations())) {
+            root.put("integrations", definition.getIntegrations());
+        }
+        if (!CollectionUtils.isEmpty(definition.getExtensions())) {
+            root.put("extensions", definition.getExtensions());
+        }
+        Map<String, Object> hertzbeat = toDefinitionHertzbeatMap(definition.getHertzbeat());
+        if (!hertzbeat.isEmpty()) {
+            root.put("hertzbeat", hertzbeat);
+        }
+        return root;
+    }
+
+    private void putTelemetryIfPresent(Map<String, Object> spec, EntityDefinition.Telemetry telemetryDefinition) {
+        if (telemetryDefinition == null) {
+            return;
+        }
+        Map<String, Object> telemetry = new LinkedHashMap<>();
+        if (!CollectionUtils.isEmpty(telemetryDefinition.getIdentities())) {
+            telemetry.put("identities", telemetryDefinition.getIdentities().stream().map(identity -> {
+                Map<String, Object> item = new LinkedHashMap<>();
+                putIfPresent(item, "key", identity.getKey());
+                putIfPresent(item, "value", identity.getValue());
+                putIfPresent(item, "type", identity.getType());
+                if (identity.getPriority() != null) {
+                    item.put("priority", identity.getPriority());
+                }
+                if (identity.getPrimary() != null) {
+                    item.put("primary", identity.getPrimary());
+                }
+                return item;
+            }).toList());
+        }
+        if (!CollectionUtils.isEmpty(telemetryDefinition.getMonitors())) {
+            telemetry.put("monitors", telemetryDefinition.getMonitors().stream().map(bind -> {
+                Map<String, Object> item = new LinkedHashMap<>();
+                if (bind.getMonitorId() != null) {
+                    item.put("monitorId", bind.getMonitorId());
+                }
+                putIfPresent(item, "bindType", bind.getBindType());
+                putIfPresent(item, "bindSource", bind.getBindSource());
+                putIfPresent(item, "status", bind.getStatus());
+                if (bind.getScore() != null) {
+                    item.put("score", bind.getScore());
+                }
+                if (!CollectionUtils.isEmpty(bind.getMatchContext())) {
+                    item.put("matchContext", bind.getMatchContext());
+                }
+                return item;
+            }).toList());
+        }
+        if (!telemetry.isEmpty()) {
+            spec.put("telemetry", telemetry);
+        }
+    }
+
+    private Map<String, Object> toDefinitionHertzbeatMap(EntityDefinition.Hertzbeat hertzbeat) {
+        if (!hasHertzbeatContent(hertzbeat)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        if (!CollectionUtils.isEmpty(hertzbeat.getCodeLocations())) {
+            result.put("codeLocations", hertzbeat.getCodeLocations().stream().map(codeLocation -> {
+                Map<String, Object> item = new LinkedHashMap<>();
+                putIfPresent(item, "repositoryURL", codeLocation.getRepositoryURL());
+                if (!CollectionUtils.isEmpty(codeLocation.getPaths())) {
+                    item.put("paths", codeLocation.getPaths());
+                }
+                return item;
+            }).toList());
+        }
+        if (!CollectionUtils.isEmpty(hertzbeat.getEvents())) {
+            result.put("events", hertzbeat.getEvents().stream().map(query -> {
+                Map<String, Object> item = new LinkedHashMap<>();
+                putIfPresent(item, "name", query.getName());
+                putIfPresent(item, "query", query.getQuery());
+                return item;
+            }).toList());
+        }
+        if (!CollectionUtils.isEmpty(hertzbeat.getLogs())) {
+            result.put("logs", hertzbeat.getLogs().stream().map(query -> {
+                Map<String, Object> item = new LinkedHashMap<>();
+                putIfPresent(item, "name", query.getName());
+                putIfPresent(item, "query", query.getQuery());
+                return item;
+            }).toList());
+        }
+        if (hertzbeat.getPerformanceData() != null && !CollectionUtils.isEmpty(hertzbeat.getPerformanceData().getTags())) {
+            Map<String, Object> performanceData = new LinkedHashMap<>();
+            performanceData.put("tags", hertzbeat.getPerformanceData().getTags());
+            result.put("performanceData", performanceData);
+        }
+        if (hertzbeat.getPipelines() != null && !CollectionUtils.isEmpty(hertzbeat.getPipelines().getFingerprints())) {
+            Map<String, Object> pipelines = new LinkedHashMap<>();
+            pipelines.put("fingerprints", hertzbeat.getPipelines().getFingerprints());
+            result.put("pipelines", pipelines);
+        }
+        return result;
+    }
+
+    private boolean hasHertzbeatContent(EntityDefinition.Hertzbeat hertzbeat) {
+        if (hertzbeat == null) {
+            return false;
+        }
+        return !CollectionUtils.isEmpty(hertzbeat.getCodeLocations())
+                || !CollectionUtils.isEmpty(hertzbeat.getEvents())
+                || !CollectionUtils.isEmpty(hertzbeat.getLogs())
+                || (hertzbeat.getPerformanceData() != null && !CollectionUtils.isEmpty(hertzbeat.getPerformanceData().getTags()))
+                || (hertzbeat.getPipelines() != null && !CollectionUtils.isEmpty(hertzbeat.getPipelines().getFingerprints()));
+    }
+
+    private void putIfPresent(Map<String, Object> target, String key, Object value) {
+        if (value instanceof String stringValue) {
+            if (StringUtils.hasText(stringValue)) {
+                target.put(key, stringValue);
+            }
+            return;
+        }
+        if (value != null) {
+            target.put(key, value);
+        }
+    }
+
+    private String defaultText(String... values) {
+        if (values == null) {
+            return "";
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return "";
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDraftService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDraftService.java
new file mode 100644
index 0000000000..1dfeb23811
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionDraftService.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionRequest;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Builds entity DTO drafts from submitted definition documents.
+ */
+@Service
+public class EntityDefinitionDraftService {
+
+    private final EntityDefinitionDocumentParserService entityDefinitionDocumentParserService;
+    private final EntityDefinitionNormalizationService entityDefinitionNormalizationService;
+    private final EntityDefinitionMappingService entityDefinitionMappingService;
+
+    public EntityDefinitionDraftService(EntityDefinitionDocumentParserService entityDefinitionDocumentParserService,
+                                        EntityDefinitionNormalizationService entityDefinitionNormalizationService,
+                                        EntityDefinitionMappingService entityDefinitionMappingService) {
+        this.entityDefinitionDocumentParserService = entityDefinitionDocumentParserService;
+        this.entityDefinitionNormalizationService = entityDefinitionNormalizationService;
+        this.entityDefinitionMappingService = entityDefinitionMappingService;
+    }
+
+    public EntityDto parseEntityDefinition(EntityDefinitionRequest definitionRequest, Long entityId) {
+        return parseEntityDefinition(definitionRequest, entityId, null, true);
+    }
+
+    public EntityDto parseEntityDefinition(EntityDefinitionRequest definitionRequest,
+                                           Long entityId,
+                                           String requestWorkspaceId) {
+        return parseEntityDefinition(definitionRequest, entityId, requestWorkspaceId, false);
+    }
+
+    private EntityDto parseEntityDefinition(EntityDefinitionRequest definitionRequest,
+                                            Long entityId,
+                                            String requestWorkspaceId,
+                                            boolean resolveRequestWorkspaceAtMappingBoundary) {
+        List<EntityDto> entityDtos = parseEntityDefinitionBundle(
+                definitionRequest, entityId, requestWorkspaceId, resolveRequestWorkspaceAtMappingBoundary);
+        if (CollectionUtils.isEmpty(entityDtos)) {
+            throw new IllegalArgumentException("Entity definition content can not be blank.");
+        }
+        if (entityDtos.size() > 1) {
+            throw new IllegalArgumentException("Entity definition bundle contains multiple entities. Use the bundle API.");
+        }
+        return entityDtos.getFirst();
+    }
+
+    public List<EntityDto> parseEntityDefinitionBundle(EntityDefinitionRequest definitionRequest) {
+        return parseEntityDefinitionBundle(definitionRequest, null, null, true);
+    }
+
+    public List<EntityDto> parseEntityDefinitionBundle(EntityDefinitionRequest definitionRequest,
+                                                       String requestWorkspaceId) {
+        return parseEntityDefinitionBundle(definitionRequest, null, requestWorkspaceId, false);
+    }
+
+    private List<EntityDto> parseEntityDefinitionBundle(EntityDefinitionRequest definitionRequest,
+                                                       Long entityId,
+                                                       String requestWorkspaceId,
+                                                       boolean resolveRequestWorkspaceAtMappingBoundary) {
+        return entityDefinitionDocumentParserService.parseDefinitionRecords(definitionRequest).stream()
+                .map(entityDefinitionNormalizationService::normalizeDefinition)
+                .map(definition -> resolveRequestWorkspaceAtMappingBoundary
+                        ? entityDefinitionMappingService.toEntityDto(definition, entityId)
+                        : entityDefinitionMappingService.toEntityDto(definition, entityId, requestWorkspaceId))
+                .toList();
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionExportService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionExportService.java
new file mode 100644
index 0000000000..edf50823c9
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionExportService.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
+import org.springframework.stereotype.Service;
+
+/**
+ * Renders workspace-scoped entity definition exports.
+ */
+@Service
+public class EntityDefinitionExportService {
+
+    private final EntityDetailReadModelService entityDetailReadModelService;
+    private final EntityDefinitionMappingService entityDefinitionMappingService;
+    private final EntityDefinitionDocumentRendererService entityDefinitionDocumentRendererService;
+
+    public EntityDefinitionExportService(EntityDetailReadModelService entityDetailReadModelService,
+                                         EntityDefinitionMappingService entityDefinitionMappingService,
+                                         EntityDefinitionDocumentRendererService entityDefinitionDocumentRendererService) {
+        this.entityDetailReadModelService = entityDetailReadModelService;
+        this.entityDefinitionMappingService = entityDefinitionMappingService;
+        this.entityDefinitionDocumentRendererService = entityDefinitionDocumentRendererService;
+    }
+
+    public String getEntityDefinition(long entityId, String format) {
+        EntityDto entityDto = entityDetailReadModelService.loadEntityDto(entityId);
+        if (entityDto == null) {
+            return null;
+        }
+        return entityDefinitionDocumentRendererService.renderDefinition(
+                entityDefinitionMappingService.toEntityDefinition(entityDto), format);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionExtensionNormalizationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionExtensionNormalizationService.java
new file mode 100644
index 0000000000..98f583ec02
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionExtensionNormalizationService.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.springframework.stereotype.Service;
+
+/**
+ * Normalizes integration and extension object nodes in entity definition documents.
+ */
+@Service
+public class EntityDefinitionExtensionNormalizationService {
+
+    public void attachDefinitionAddOns(EntityDefinition definition, Map<String, Object> root, Map<String, Object> specMap) {
+        if (definition == null) {
+            return;
+        }
+        Map<String, Object> rootMap = root == null ? Collections.emptyMap() : root;
+        Map<String, Object> addOnSpecMap = specMap == null ? Collections.emptyMap() : specMap;
+        definition.setIntegrations(extractDefinitionObjectNodeMap(firstNonNull(
+                rootMap.get("integrations"), addOnSpecMap.get("integrations"))));
+        definition.setExtensions(extractDefinitionObjectNodeMap(firstNonNull(
+                rootMap.get("extensions"), addOnSpecMap.get("extensions"))));
+    }
+
+    public Map<String, Object> extractDefinitionObjectNodeMap(Object value) {
+        Map<String, Object> objectMap = toObjectMap(value);
+        return objectMap.isEmpty() ? Collections.emptyMap() : objectMap;
+    }
+
+    private Object firstNonNull(Object primary, Object fallback) {
+        return primary != null ? primary : fallback;
+    }
+
+    private Map<String, Object> toObjectMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionHertzbeatNormalizationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionHertzbeatNormalizationService.java
new file mode 100644
index 0000000000..0d56c52c78
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionHertzbeatNormalizationService.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Normalizes HertzBeat-specific evidence blocks inside entity definition documents.
+ */
+@Service
+public class EntityDefinitionHertzbeatNormalizationService {
+
+    private static final String LEGACY_CI_PIPELINE_FINGERPRINTS = "ci-pipeline-fingerprints";
+
+    public void attachDefinitionHertzbeat(EntityDefinition definition, Map<String, Object> root, Map<String, Object> specMap) {
+        if (definition == null) {
+            return;
+        }
+        Map<String, Object> rootMap = root == null ? Collections.emptyMap() : root;
+        Map<String, Object> evidenceSpecMap = specMap == null ? Collections.emptyMap() : specMap;
+        definition.setHertzbeat(extractDefinitionHertzbeat(firstNonNull(
+                rootMap.get("hertzbeat"), evidenceSpecMap.get("hertzbeat")),
+                rootMap.get(LEGACY_CI_PIPELINE_FINGERPRINTS)));
+    }
+
+    public EntityDefinition.Hertzbeat extractDefinitionHertzbeat(Object value, Object legacyPipelineFingerprints) {
+        Map<String, Object> hertzbeatMap = toObjectMap(value);
+        EntityDefinition.Hertzbeat hertzbeat = new EntityDefinition.Hertzbeat();
+        if (!hertzbeatMap.isEmpty()) {
+            hertzbeat.setCodeLocations(extractDefinitionCodeLocations(hertzbeatMap.get("codeLocations")));
+            hertzbeat.setEvents(extractDefinitionSavedQueries(hertzbeatMap.get("events")));
+            hertzbeat.setLogs(extractDefinitionSavedQueries(hertzbeatMap.get("logs")));
+            hertzbeat.setPerformanceData(extractDefinitionPerformanceData(hertzbeatMap.get("performanceData")));
+            hertzbeat.setPipelines(extractDefinitionPipelines(hertzbeatMap.get("pipelines")));
+        }
+        if (hertzbeat.getPipelines() == null
+                || CollectionUtils.isEmpty(hertzbeat.getPipelines().getFingerprints())) {
+            EntityDefinition.Pipelines pipelines = extractLegacyPipelineFingerprints(legacyPipelineFingerprints);
+            if (pipelines != null) {
+                hertzbeat.setPipelines(pipelines);
+            }
+        }
+        return hasHertzbeatContent(hertzbeat) ? hertzbeat : null;
+    }
+
+    private Object firstNonNull(Object primary, Object fallback) {
+        return primary != null ? primary : fallback;
+    }
+
+    private EntityDefinition.Pipelines extractLegacyPipelineFingerprints(Object legacyPipelineFingerprints) {
+        if (legacyPipelineFingerprints == null) {
+            return null;
+        }
+        Map<String, Object> pipelinesMap = new LinkedHashMap<>();
+        pipelinesMap.put("fingerprints", legacyPipelineFingerprints);
+        pipelinesMap.put("legacyKey", LEGACY_CI_PIPELINE_FINGERPRINTS);
+        return extractDefinitionPipelines(pipelinesMap);
+    }
+
+    private List<EntityDefinition.CodeLocation> extractDefinitionCodeLocations(Object value) {
+        if (!(value instanceof List<?> items)) {
+            return Collections.emptyList();
+        }
+        List<EntityDefinition.CodeLocation> results = new ArrayList<>();
+        for (Object item : items) {
+            Map<String, Object> codeLocationMap = toObjectMap(item);
+            String repositoryUrl = defaultText(asText(codeLocationMap.get("repositoryURL")), asText(codeLocationMap.get("repositoryUrl")));
+            List<String> paths = extractDefinitionStringList("paths", codeLocationMap);
+            if (!StringUtils.hasText(repositoryUrl) && CollectionUtils.isEmpty(paths)) {
+                continue;
+            }
+            EntityDefinition.CodeLocation codeLocation = new EntityDefinition.CodeLocation();
+            codeLocation.setRepositoryURL(trimToNull(repositoryUrl));
+            codeLocation.setPaths(paths);
+            results.add(codeLocation);
+        }
+        return results;
+    }
+
+    private List<EntityDefinition.SavedQuery> extractDefinitionSavedQueries(Object value) {
+        if (!(value instanceof List<?> items)) {
+            return Collections.emptyList();
+        }
+        List<EntityDefinition.SavedQuery> results = new ArrayList<>();
+        for (Object item : items) {
+            Map<String, Object> queryMap = toObjectMap(item);
+            String query = defaultText(asText(queryMap.get("query")), asText(queryMap.get("search")));
+            if (!StringUtils.hasText(query)) {
+                continue;
+            }
+            EntityDefinition.SavedQuery savedQuery = new EntityDefinition.SavedQuery();
+            savedQuery.setName(defaultText(asText(queryMap.get("name")), asText(queryMap.get("label"))));
+            savedQuery.setQuery(query.trim());
+            results.add(savedQuery);
+        }
+        return results;
+    }
+
+    private EntityDefinition.PerformanceData extractDefinitionPerformanceData(Object value) {
+        Map<String, Object> performanceMap = toObjectMap(value);
+        if (performanceMap.isEmpty()) {
+            return null;
+        }
+        List<String> tags = extractDefinitionStringList("tags", performanceMap);
+        if (CollectionUtils.isEmpty(tags)) {
+            return null;
+        }
+        EntityDefinition.PerformanceData performanceData = new EntityDefinition.PerformanceData();
+        performanceData.setTags(tags);
+        return performanceData;
+    }
+
+    private EntityDefinition.Pipelines extractDefinitionPipelines(Object value) {
+        Map<String, Object> pipelinesMap = toObjectMap(value);
+        if (pipelinesMap.isEmpty()) {
+            return null;
+        }
+        List<String> fingerprints = extractDefinitionStringList("fingerprints", pipelinesMap);
+        if (CollectionUtils.isEmpty(fingerprints)) {
+            return null;
+        }
+        EntityDefinition.Pipelines pipelines = new EntityDefinition.Pipelines();
+        pipelines.setFingerprints(fingerprints);
+        return pipelines;
+    }
+
+    private boolean hasHertzbeatContent(EntityDefinition.Hertzbeat hertzbeat) {
+        if (hertzbeat == null) {
+            return false;
+        }
+        return !CollectionUtils.isEmpty(hertzbeat.getCodeLocations())
+                || !CollectionUtils.isEmpty(hertzbeat.getEvents())
+                || !CollectionUtils.isEmpty(hertzbeat.getLogs())
+                || (hertzbeat.getPerformanceData() != null && !CollectionUtils.isEmpty(hertzbeat.getPerformanceData().getTags()))
+                || (hertzbeat.getPipelines() != null && !CollectionUtils.isEmpty(hertzbeat.getPipelines().getFingerprints()));
+    }
+
+    private List<String> extractDefinitionStringList(String key, Map<String, Object> source) {
+        if (!StringUtils.hasText(key) || source == null || source.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Object rawValue = source.get(key);
+        if (rawValue instanceof List<?> items) {
+            return items.stream()
+                    .map(this::asText)
+                    .filter(StringUtils::hasText)
+                    .map(String::trim)
+                    .distinct()
+                    .toList();
+        }
+        String value = asText(rawValue);
+        if (StringUtils.hasText(value)) {
+            return List.of(value.trim());
+        }
+        return Collections.emptyList();
+    }
+
+    private Map<String, Object> toObjectMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    private String asText(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value);
+        return StringUtils.hasText(text) ? text.trim() : null;
+    }
+
+    private String defaultText(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return null;
+    }
+
+    private String trimToNull(String value) {
+        return StringUtils.hasText(value) ? value.trim() : null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionMappingService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionMappingService.java
new file mode 100644
index 0000000000..5de30de564
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionMappingService.java
@@ -0,0 +1,517 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.hertzbeat.common.entity.manager.EntityCatalogContact;
+import org.apache.hertzbeat.common.entity.manager.EntityCatalogLink;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
+import org.apache.hertzbeat.common.entity.manager.EntityOwnerRef;
+import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
+import org.apache.hertzbeat.manager.pojo.dto.EntityInfo;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import tools.jackson.databind.JsonNode;
+
+/**
+ * Maps canonical entity definitions to internal entity DTOs and back.
+ */
+@Service
+public class EntityDefinitionMappingService {
+
+    private static final String SOURCE_MANUAL = "manual";
+    private static final String BIND_ACTIVE = "active";
+    private static final String RELATION_CONFIRMED = "confirmed";
+    private static final String ENTITY_DEFINITION_API_VERSION = "hertzbeat/v1";
+    private static final String LEGACY_ENTITY_DEFINITION_KIND = "Entity";
+    private static final String TYPE_DATABASE = "database";
+    private static final String TYPE_API = "api";
+    private static final String KIND_DATASTORE = "datastore";
+    private static final String KIND_API = "api";
+
+    private final EntityRelationService entityRelationService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityDefinitionMappingService(EntityRelationService entityRelationService,
+                                          EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityRelationService = entityRelationService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public EntityDto toEntityDto(EntityDefinition definition, Long entityId) {
+        return toEntityDto(definition, entityId, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public EntityDto toEntityDto(EntityDefinition definition, Long entityId, String requestWorkspaceId) {
+        EntityDto entityDto = new EntityDto();
+        EntityInfo entityInfo = new EntityInfo();
+        entityInfo.setId(entityId);
+        entityInfo.setType(defaultText(
+                normalizeEntityTypeFromKind(definition.getKind()),
+                "service"
+        ));
+        entityInfo.setName(definition.getMetadata() == null ? null : definition.getMetadata().getName());
+        entityInfo.setDisplayName(definition.getMetadata() == null ? null : definition.getMetadata().getDisplayName());
+        entityInfo.setSubtype(definition.getSpec() == null ? null : definition.getSpec().getType());
+        entityInfo.setDescription(definition.getMetadata() == null ? null : definition.getMetadata().getDescription());
+        entityInfo.setLabels(definition.getMetadata() == null ? null : definition.getMetadata().getLabels());
+        entityInfo.setTags(definition.getMetadata() == null ? Collections.emptyList() : definition.getMetadata().getTags());
+        entityInfo.setAdditionalOwners(toEntityOwnerRefs(definition.getMetadata() == null ? null : definition.getMetadata().getAdditionalOwners()));
+        entityInfo.setInheritFrom(definition.getMetadata() == null ? null : definition.getMetadata().getInheritFrom());
+        entityInfo.setLinks(toEntityLinks(
+                definition.getMetadata() == null ? null : definition.getMetadata().getLinks(),
+                definition.getSpec() == null ? null : definition.getSpec().getRunbook()
+        ));
+        entityInfo.setContacts(toEntityContacts(definition.getMetadata() == null ? null : definition.getMetadata().getContacts()));
+        entityInfo.setIntegrations(toJsonNode(definition.getIntegrations()));
+        entityInfo.setExtensions(toJsonNode(definition.getExtensions()));
+        entityInfo.setHertzbeat(toJsonNode(definition.getHertzbeat()));
+        if (definition.getSpec() != null) {
+            entityInfo.setSource(definition.getSpec().getSource());
+            entityInfo.setOwner(defaultText(
+                    definition.getMetadata() == null ? null : definition.getMetadata().getOwner(),
+                    definition.getSpec().getOwner(),
+                    definition.getSpec().getOwnedBy()
+            ));
+            entityInfo.setNamespace(defaultText(
+                    definition.getMetadata() == null ? null : definition.getMetadata().getNamespace(),
+                    definition.getSpec().getNamespace()
+            ));
+            entityInfo.setEnvironment(definition.getSpec().getEnvironment());
+            entityInfo.setCriticality(definition.getSpec().getCriticality());
+            entityInfo.setRunbook(definition.getSpec().getRunbook());
+            entityInfo.setLifecycle(definition.getSpec().getLifecycle());
+            entityInfo.setTier(definition.getSpec().getTier());
+            entityInfo.setSystem(defaultText(definition.getSpec().getPartOf(), definition.getSpec().getSystem()));
+            entityInfo.setComponentOf(definition.getSpec().getComponentOf());
+            entityInfo.setComponents(definition.getSpec().getComponents());
+            entityInfo.setImplementedBy(definition.getSpec().getImplementedBy());
+            entityInfo.setApiInterface(toJsonNode(definition.getSpec().getApiInterface()));
+            entityInfo.setLanguages(definition.getSpec().getLanguages());
+        }
+        entityDto.setEntityInfo(entityInfo);
+        entityDto.setIdentities(toEntityIdentities(definition.getSpec() == null ? null : definition.getSpec().getTelemetry()));
+        entityDto.setMonitorBinds(toEntityMonitorBinds(definition.getSpec() == null ? null : definition.getSpec().getTelemetry()));
+        entityDto.setRelations(toEntityRelations(
+                definition.getSpec() == null ? null : definition.getSpec().getRelations(),
+                entityId,
+                requestWorkspaceId));
+        return entityDto;
+    }
+
+    public EntityDefinition toEntityDefinition(EntityDto entityDto) {
+        EntityDefinition definition = new EntityDefinition();
+        definition.setApiVersion(ENTITY_DEFINITION_API_VERSION);
+        definition.setKind(defaultText(toDefinitionKind(entityDto.getEntity().getType()), "service"));
+
+        ObserveEntity entity = entityDto.getEntity();
+        EntityDefinition.Metadata metadata = new EntityDefinition.Metadata();
+        metadata.setName(entity.getName());
+        metadata.setNamespace(entity.getNamespace());
+        metadata.setOwner(entity.getOwner());
+        metadata.setAdditionalOwners(toDefinitionOwnerRefs(entity.getAdditionalOwners()));
+        metadata.setInheritFrom(entity.getInheritFrom());
+        metadata.setDisplayName(entity.getDisplayName());
+        metadata.setDescription(entity.getDescription());
+        metadata.setLabels(entity.getLabels());
+        metadata.setTags(normalizeTags(entity.getTags(), entity.getLabels()));
+        metadata.setLinks(toDefinitionLinks(entity.getLinks(), entity.getRunbook()));
+        metadata.setContacts(toDefinitionContacts(entity.getContacts()));
+        definition.setMetadata(metadata);
+        definition.setIntegrations(toObjectNodeMap(entity.getIntegrations()));
+        definition.setExtensions(toObjectNodeMap(entity.getExtensions()));
+        definition.setHertzbeat(toDefinitionHertzbeat(entity.getHertzbeat()));
+
+        EntityDefinition.Spec spec = new EntityDefinition.Spec();
+        spec.setSource(entity.getSource());
+        spec.setOwner(entity.getOwner());
+        spec.setOwnedBy(entity.getOwner());
+        spec.setNamespace(entity.getNamespace());
+        spec.setEnvironment(entity.getEnvironment());
+        spec.setCriticality(entity.getCriticality());
+        spec.setRunbook(entity.getRunbook());
+        spec.setLifecycle(entity.getLifecycle());
+        spec.setTier(entity.getTier());
+        spec.setType(entity.getSubtype());
+        spec.setSystem(entity.getSystem());
+        spec.setPartOf(entity.getSystem());
+        spec.setComponentOf(entity.getComponentOf());
+        spec.setComponents(entity.getComponents());
+        spec.setImplementedBy(entity.getImplementedBy());
+        spec.setApiInterface(toDefinitionApiInterface(entity.getApiInterface()));
+        spec.setLanguages(entity.getLanguages());
+
+        EntityDefinition.Telemetry telemetry = new EntityDefinition.Telemetry();
+        telemetry.setIdentities((entityDto.getIdentities() == null ? Collections.<EntityIdentity>emptyList() : entityDto.getIdentities()).stream()
+                .map(identity -> {
+                    EntityDefinition.Identity item = new EntityDefinition.Identity();
+                    item.setKey(identity.getIdentityKey());
+                    item.setValue(identity.getIdentityValue());
+                    item.setType(identity.getIdentityType());
+                    item.setPriority(identity.getPriority());
+                    item.setPrimary(identity.isPrimaryIdentity());
+                    return item;
+                }).toList());
+        telemetry.setMonitors((entityDto.getMonitorBinds() == null ? Collections.<EntityMonitorBind>emptyList() : entityDto.getMonitorBinds()).stream()
+                .map(bind -> {
+                    EntityDefinition.MonitorBind item = new EntityDefinition.MonitorBind();
+                    item.setMonitorId(bind.getMonitorId());
+                    item.setBindType(bind.getBindType());
+                    item.setBindSource(bind.getBindSource());
+                    item.setStatus(bind.getStatus());
+                    item.setScore(bind.getScore());
+                    item.setMatchContext(bind.getMatchContext());
+                    return item;
+                }).toList());
+        if (!CollectionUtils.isEmpty(telemetry.getIdentities()) || !CollectionUtils.isEmpty(telemetry.getMonitors())) {
+            spec.setTelemetry(telemetry);
+        }
+
+        Long entityId = entity.getId();
+        List<EntityDefinition.Relation> relations = (entityDto.getRelations() == null ? Collections.<EntityRelation>emptyList() : entityDto.getRelations()).stream()
+                .filter(relation -> entityId == null || Objects.equals(relation.getSourceEntityId(), entityId))
+                .map(relation -> {
+                    EntityDefinition.Relation item = new EntityDefinition.Relation();
+                    item.setTargetEntityId(relation.getTargetEntityId());
+                    item.setTargetRef(defaultText(relation.getTargetRef(),
+                            entityRelationService.buildEntityReference(relation.getTargetEntityId())));
+                    item.setRelationType(relation.getRelationType());
+                    item.setRelationSource(relation.getRelationSource());
+                    item.setStatus(relation.getStatus());
+                    item.setScore(relation.getScore());
+                    item.setDescription(relation.getDescription());
+                    item.setAttributes(relation.getAttributes());
+                    return item;
+                }).toList();
+        if (!relations.isEmpty()) {
+            spec.setRelations(relations);
+            spec.setDependsOn(extractRelationReferences(relations));
+        }
+        definition.setSpec(spec);
+        return definition;
+    }
+
+    private List<EntityIdentity> toEntityIdentities(EntityDefinition.Telemetry telemetry) {
+        if (telemetry == null || CollectionUtils.isEmpty(telemetry.getIdentities())) {
+            return Collections.emptyList();
+        }
+        return telemetry.getIdentities().stream()
+                .filter(identity -> StringUtils.hasText(identity.getKey()) && StringUtils.hasText(identity.getValue()))
+                .map(identity -> EntityIdentity.builder()
+                        .identityKey(identity.getKey().trim())
+                        .identityValue(identity.getValue().trim())
+                        .identityType(defaultText(identity.getType(), SOURCE_MANUAL))
+                        .priority(identity.getPriority())
+                        .primaryIdentity(Boolean.TRUE.equals(identity.getPrimary()))
+                        .build())
+                .toList();
+    }
+
+    private List<EntityMonitorBind> toEntityMonitorBinds(EntityDefinition.Telemetry telemetry) {
+        if (telemetry == null || CollectionUtils.isEmpty(telemetry.getMonitors())) {
+            return Collections.emptyList();
+        }
+        return telemetry.getMonitors().stream()
+                .filter(bind -> bind.getMonitorId() != null)
+                .map(bind -> EntityMonitorBind.builder()
+                        .monitorId(bind.getMonitorId())
+                        .bindType(defaultText(bind.getBindType(), SOURCE_MANUAL))
+                        .bindSource(defaultText(bind.getBindSource(), SOURCE_MANUAL))
+                        .status(defaultText(bind.getStatus(), BIND_ACTIVE))
+                        .score(bind.getScore())
+                        .matchContext(bind.getMatchContext())
+                        .build())
+                .toList();
+    }
+
+    private List<EntityRelation> toEntityRelations(List<EntityDefinition.Relation> relations,
+                                                   Long entityId,
+                                                   String requestWorkspaceId) {
+        if (CollectionUtils.isEmpty(relations)) {
+            return Collections.emptyList();
+        }
+        return relations.stream()
+                .map(relation -> EntityRelation.builder()
+                        .sourceEntityId(entityId)
+                        .targetEntityId(defaultTargetEntityId(relation, requestWorkspaceId))
+                        .targetRef(defaultText(relation.getTargetRef(),
+                                relation.getTargetEntityId() == null
+                                        ? null
+                                        : entityRelationService.buildEntityReference(relation.getTargetEntityId())))
+                        .relationType(defaultText(relation.getRelationType(), "depends_on"))
+                        .relationSource(defaultText(relation.getRelationSource(), SOURCE_MANUAL))
+                        .status(defaultText(relation.getStatus(), RELATION_CONFIRMED))
+                        .score(relation.getScore())
+                        .description(relation.getDescription())
+                        .attributes(relation.getAttributes())
+                        .build())
+                .filter(relation -> relation.getTargetEntityId() != null || StringUtils.hasText(relation.getTargetRef()))
+                .toList();
+    }
+
+    private List<String> normalizeTags(List<String> tags, Map<String, String> labels) {
+        List<String> normalized = defaultList(tags, Collections.emptyList()).stream()
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .toList();
+        if (!CollectionUtils.isEmpty(normalized)) {
+            return normalized;
+        }
+        return toDefinitionTags(labels);
+    }
+
+    private List<String> toDefinitionTags(Map<String, String> labels) {
+        if (CollectionUtils.isEmpty(labels)) {
+            return Collections.emptyList();
+        }
+        return labels.entrySet().stream()
+                .filter(entry -> StringUtils.hasText(entry.getKey()))
+                .map(entry -> StringUtils.hasText(entry.getValue())
+                        ? entry.getKey().trim() + ":" + entry.getValue().trim()
+                        : entry.getKey().trim())
+                .toList();
+    }
+
+    private List<EntityCatalogLink> toEntityLinks(List<EntityDefinition.Link> links, String runbookUrl) {
+        if (CollectionUtils.isEmpty(links) && !StringUtils.hasText(runbookUrl)) {
+            return Collections.emptyList();
+        }
+        List<EntityCatalogLink> result = new ArrayList<>();
+        if (!CollectionUtils.isEmpty(links)) {
+            for (EntityDefinition.Link link : links) {
+                if (link == null || !StringUtils.hasText(link.getUrl())) {
+                    continue;
+                }
+                result.add(new EntityCatalogLink(
+                        defaultText(link.getName(), link.getType()),
+                        defaultText(link.getType(), "link"),
+                        trimToNull(link.getProvider()),
+                        link.getUrl().trim()
+                ));
+            }
+        }
+        if (StringUtils.hasText(runbookUrl)) {
+            boolean hasRunbook = result.stream().anyMatch(link -> "runbook".equalsIgnoreCase(defaultText(link.getType(), link.getName())));
+            if (!hasRunbook) {
+                result.add(0, new EntityCatalogLink("runbook", "runbook", "manual", runbookUrl.trim()));
+            }
+        }
+        return result;
+    }
+
+    private List<EntityCatalogContact> toEntityContacts(List<EntityDefinition.Contact> contacts) {
+        if (CollectionUtils.isEmpty(contacts)) {
+            return Collections.emptyList();
+        }
+        return contacts.stream()
+                .filter(contact -> contact != null && StringUtils.hasText(defaultText(contact.getValue(), contact.getContact())))
+                .map(contact -> {
+                    String value = defaultText(contact.getValue(), contact.getContact()).trim();
+                    EntityCatalogContact item = new EntityCatalogContact(
+                            defaultText(contact.getName(), contact.getType()),
+                            defaultText(contact.getType(), "contact"),
+                            value
+                    );
+                    item.setContact(value);
+                    return item;
+                })
+                .toList();
+    }
+
+    private List<EntityDefinition.Link> toDefinitionLinks(List<EntityCatalogLink> links, String runbookUrl) {
+        List<EntityDefinition.Link> result = new ArrayList<>();
+        if (!CollectionUtils.isEmpty(links)) {
+            for (EntityCatalogLink link : links) {
+                if (link == null || !StringUtils.hasText(link.getUrl())) {
+                    continue;
+                }
+                EntityDefinition.Link item = new EntityDefinition.Link();
+                item.setName(defaultText(link.getName(), link.getType()));
+                item.setType(defaultText(link.getType(), "link"));
+                item.setProvider(trimToNull(link.getProvider()));
+                item.setUrl(link.getUrl().trim());
+                result.add(item);
+            }
+        }
+        if (StringUtils.hasText(runbookUrl)) {
+            boolean hasRunbook = result.stream().anyMatch(link -> "runbook".equalsIgnoreCase(defaultText(link.getType(), link.getName())));
+            if (!hasRunbook) {
+                EntityDefinition.Link runbook = new EntityDefinition.Link();
+                runbook.setName("runbook");
+                runbook.setType("runbook");
+                runbook.setProvider("manual");
+                runbook.setUrl(runbookUrl.trim());
+                result.add(0, runbook);
+            }
+        }
+        return result;
+    }
+
+    private List<EntityDefinition.Contact> toDefinitionContacts(List<EntityCatalogContact> contacts) {
+        if (CollectionUtils.isEmpty(contacts)) {
+            return Collections.emptyList();
+        }
+        return contacts.stream()
+                .filter(contact -> contact != null && StringUtils.hasText(defaultText(contact.getContact(), contact.getValue())))
+                .map(contact -> {
+                    EntityDefinition.Contact item = new EntityDefinition.Contact();
+                    item.setName(defaultText(contact.getName(), contact.getType()));
+                    item.setType(defaultText(contact.getType(), "contact"));
+                    item.setContact(defaultText(contact.getContact(), contact.getValue()).trim());
+                    return item;
+                })
+                .toList();
+    }
+
+    private EntityDefinition.Hertzbeat toDefinitionHertzbeat(JsonNode node) {
+        if (node == null || node.isNull()) {
+            return null;
+        }
+        return JsonUtil.fromJson(node.toString(), EntityDefinition.Hertzbeat.class);
+    }
+
+    private EntityDefinition.ApiInterface toDefinitionApiInterface(JsonNode node) {
+        if (node == null || node.isNull()) {
+            return null;
+        }
+        return JsonUtil.fromJson(node.toString(), EntityDefinition.ApiInterface.class);
+    }
+
+    private Map<String, Object> toObjectNodeMap(JsonNode node) {
+        if (node == null || node.isNull() || !node.isObject()) {
+            return Collections.emptyMap();
+        }
+        return JsonUtil.fromJson(node.toString(), new tools.jackson.core.type.TypeReference<Map<String, Object>>() {
+        });
+    }
+
+    private JsonNode toJsonNode(Object value) {
+        if (value == null) {
+            return null;
+        }
+        return JsonUtil.fromJson(JsonUtil.toJson(value));
+    }
+
+    private List<String> extractRelationReferences(List<EntityDefinition.Relation> relations) {
+        if (CollectionUtils.isEmpty(relations)) {
+            return Collections.emptyList();
+        }
+        return relations.stream()
+                .map(relation -> defaultText(relation.getTargetRef(),
+                        relation.getTargetEntityId() == null
+                                ? null
+                                : entityRelationService.buildEntityReference(relation.getTargetEntityId())))
+                .filter(StringUtils::hasText)
+                .distinct()
+                .toList();
+    }
+
+    private List<EntityOwnerRef> toEntityOwnerRefs(List<EntityDefinition.OwnerRef> owners) {
+        if (CollectionUtils.isEmpty(owners)) {
+            return Collections.emptyList();
+        }
+        return owners.stream()
+                .filter(owner -> owner != null && StringUtils.hasText(owner.getName()))
+                .map(owner -> new EntityOwnerRef(owner.getName().trim(), defaultText(owner.getType(), "team")))
+                .toList();
+    }
+
+    private List<EntityDefinition.OwnerRef> toDefinitionOwnerRefs(List<EntityOwnerRef> owners) {
+        if (CollectionUtils.isEmpty(owners)) {
+            return Collections.emptyList();
+        }
+        return owners.stream()
+                .filter(owner -> owner != null && StringUtils.hasText(owner.getName()))
+                .map(owner -> {
+                    EntityDefinition.OwnerRef item = new EntityDefinition.OwnerRef();
+                    item.setName(owner.getName().trim());
+                    item.setType(defaultText(owner.getType(), "team"));
+                    return item;
+                })
+                .toList();
+    }
+
+    private <T> List<T> defaultList(List<T> primary, List<T> fallback) {
+        if (!CollectionUtils.isEmpty(primary)) {
+            return primary;
+        }
+        return fallback;
+    }
+
+    private String normalizeEntityTypeFromKind(String kind) {
+        if (!StringUtils.hasText(kind)) {
+            return null;
+        }
+        if (LEGACY_ENTITY_DEFINITION_KIND.equalsIgnoreCase(kind.trim())) {
+            return null;
+        }
+        return switch (kind.trim().toLowerCase()) {
+            case KIND_DATASTORE -> TYPE_DATABASE;
+            case KIND_API -> TYPE_API;
+            default -> kind.trim().toLowerCase();
+        };
+    }
+
+    private String toDefinitionKind(String entityType) {
+        if (!StringUtils.hasText(entityType)) {
+            return null;
+        }
+        return switch (entityType.trim().toLowerCase()) {
+            case TYPE_DATABASE -> KIND_DATASTORE;
+            case TYPE_API -> KIND_API;
+            default -> entityType.trim().toLowerCase();
+        };
+    }
+
+    private Long defaultTargetEntityId(EntityDefinition.Relation relation, String requestWorkspaceId) {
+        if (relation == null) {
+            return null;
+        }
+        if (relation.getTargetEntityId() != null) {
+            return entityRelationService.resolveDirectEntityReference(relation.getTargetEntityId(), requestWorkspaceId);
+        }
+        return entityRelationService.resolveEntityReference(relation.getTargetRef(), requestWorkspaceId);
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionMetadataNormalizationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionMetadataNormalizationService.java
new file mode 100644
index 0000000000..2a66977b38
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionMetadataNormalizationService.java
@@ -0,0 +1,360 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Normalizes the metadata-facing portions of entity definition documents.
+ */
+@Service
+public class EntityDefinitionMetadataNormalizationService {
+
+    public EntityDefinition.Metadata extractDefinitionMetadata(Map<String, Object> root, Map<String, Object> specMap) {
+        Map<String, Object> rootMap = root == null ? Collections.emptyMap() : root;
+        Map<String, Object> metadataMap = toObjectMap(rootMap.get("metadata"));
+        Map<String, Object> normalizedSpecMap = specMap == null ? Collections.emptyMap() : specMap;
+        boolean useRootMetadataFallback = !metadataMap.containsKey("labels") && !metadataMap.containsKey("tags")
+                && (rootMap.containsKey("labels") || rootMap.containsKey("tags"));
+
+        EntityDefinition.Metadata metadata = new EntityDefinition.Metadata();
+        metadata.setName(defaultText(
+                asText(metadataMap.get("name")),
+                asText(rootMap.get("dd-service")),
+                asText(rootMap.get("dd_service")),
+                asText(rootMap.get("name"))
+        ));
+        metadata.setNamespace(defaultText(asText(metadataMap.get("namespace")),
+                asText(normalizedSpecMap.get("namespace")), asText(normalizedSpecMap.get("serviceNamespace")),
+                asText(rootMap.get("namespace"))));
+        metadata.setOwner(defaultText(asText(metadataMap.get("owner")),
+                asText(metadataMap.get("team")),
+                asText(normalizedSpecMap.get("owner")), asText(normalizedSpecMap.get("ownedBy")),
+                asText(normalizedSpecMap.get("team")),
+                asText(rootMap.get("team"))));
+        metadata.setAdditionalOwners(extractDefinitionOwnerRefs(defaultText(
+                metadataMap.containsKey("additionalOwners") ? "additionalOwners" : null,
+                normalizedSpecMap.containsKey("additionalOwners") ? "additionalOwners" : null,
+                normalizedSpecMap.containsKey("owners") ? "owners" : null
+        ), metadataMap, normalizedSpecMap));
+        metadata.setInheritFrom(defaultText(asText(metadataMap.get("inheritFrom")),
+                asText(metadataMap.get("inherit_from")), asText(rootMap.get("inheritFrom")),
+                asText(rootMap.get("inherit_from"))));
+        metadata.setDisplayName(defaultText(asText(metadataMap.get("displayName")),
+                asText(metadataMap.get("display_name")), asText(rootMap.get("displayName")),
+                asText(rootMap.get("display_name"))));
+        metadata.setDescription(defaultText(
+                asText(metadataMap.get("description")),
+                asText(normalizedSpecMap.get("description")),
+                asText(rootMap.get("description"))
+        ));
+        metadata.setLabels(extractDefinitionLabels(useRootMetadataFallback ? rootMap : metadataMap));
+        metadata.setTags(extractDefinitionTags(useRootMetadataFallback ? rootMap : metadataMap));
+        metadata.setLinks(extractDefinitionLinks(defaultText(metadataMap.containsKey("links") ? "links" : null,
+                normalizedSpecMap.containsKey("links") ? "links" : null,
+                rootMap.containsKey("links") ? "links" : null), metadataMap, normalizedSpecMap, rootMap, null));
+        metadata.setContacts(extractDefinitionContacts(defaultText(metadataMap.containsKey("contacts") ? "contacts" : null,
+                normalizedSpecMap.containsKey("contacts") ? "contacts" : null,
+                rootMap.containsKey("contacts") ? "contacts" : null), metadataMap, normalizedSpecMap, rootMap));
+        return metadata;
+    }
+
+    public String extractDefinitionRunbook(Map<String, Object> root, Map<String, Object> specMap) {
+        Map<String, Object> rootMap = root == null ? Collections.emptyMap() : root;
+        Map<String, Object> normalizedSpecMap = specMap == null ? Collections.emptyMap() : specMap;
+        Map<String, Object> metadataMap = toObjectMap(rootMap.get("metadata"));
+        return defaultText(
+                asText(normalizedSpecMap.get("runbook")),
+                extractRunbook(normalizedSpecMap.get("links")),
+                extractRunbook(metadataMap.get("links")),
+                extractRunbook(rootMap.get("links")),
+                asText(rootMap.get("runbook"))
+        );
+    }
+
+    private Map<String, String> extractDefinitionLabels(Map<String, Object> metadataMap) {
+        Map<String, String> labels = toStringMap(metadataMap.get("labels"));
+        if (!labels.isEmpty()) {
+            return labels;
+        }
+        Object tags = metadataMap.get("tags");
+        if (!(tags instanceof List<?> tagList)) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> mappedLabels = new LinkedHashMap<>();
+        for (Object tag : tagList) {
+            String item = asText(tag);
+            if (!StringUtils.hasText(item)) {
+                continue;
+            }
+            int index = item.indexOf(':');
+            if (index <= 0 || index >= item.length() - 1) {
+                mappedLabels.put(item.trim(), "");
+            } else {
+                mappedLabels.put(item.substring(0, index).trim(), item.substring(index + 1).trim());
+            }
+        }
+        return mappedLabels;
+    }
+
+    private List<String> extractDefinitionTags(Map<String, Object> metadataMap) {
+        if (metadataMap == null || metadataMap.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Object tags = metadataMap.get("tags");
+        if (tags instanceof List<?> tagList) {
+            return tagList.stream()
+                    .map(this::asText)
+                    .filter(StringUtils::hasText)
+                    .toList();
+        }
+        Map<String, String> labels = toStringMap(metadataMap.get("labels"));
+        return toDefinitionTags(labels);
+    }
+
+    private List<String> toDefinitionTags(Map<String, String> labels) {
+        if (CollectionUtils.isEmpty(labels)) {
+            return Collections.emptyList();
+        }
+        return labels.entrySet().stream()
+                .filter(entry -> StringUtils.hasText(entry.getKey()))
+                .map(entry -> StringUtils.hasText(entry.getValue())
+                        ? entry.getKey().trim() + ":" + entry.getValue().trim()
+                        : entry.getKey().trim())
+                .toList();
+    }
+
+    private String extractRunbook(Object links) {
+        if (links instanceof Map<?, ?> mapLinks) {
+            Object runbook = mapLinks.get("runbook");
+            return asText(runbook);
+        }
+        if (!(links instanceof List<?> items)) {
+            return null;
+        }
+        for (Object item : items) {
+            Map<String, Object> link = toObjectMap(item);
+            String name = asText(link.get("name"));
+            String type = asText(link.get("type"));
+            if ("runbook".equals(name) || "runbook".equals(type)) {
+                return asText(link.get("url"));
+            }
+        }
+        return null;
+    }
+
+    private List<EntityDefinition.Link> extractDefinitionLinks(String key, Map<String, Object> metadataMap,
+                                                               Map<String, Object> specMap, Map<String, Object> rootMap,
+                                                               String runbookUrl) {
+        Object links = null;
+        if (key != null) {
+            if (metadataMap.containsKey(key)) {
+                links = metadataMap.get(key);
+            } else if (specMap.containsKey(key)) {
+                links = specMap.get(key);
+            } else if (rootMap.containsKey(key)) {
+                links = rootMap.get(key);
+            }
+        }
+        List<EntityDefinition.Link> result = new ArrayList<>();
+        if (links instanceof Map<?, ?> linkMap) {
+            for (Map.Entry<?, ?> entry : linkMap.entrySet()) {
+                String linkName = entry.getKey() == null ? null : String.valueOf(entry.getKey());
+                String url = asText(entry.getValue());
+                if (!StringUtils.hasText(linkName) || !StringUtils.hasText(url)) {
+                    continue;
+                }
+                EntityDefinition.Link item = new EntityDefinition.Link();
+                item.setName(linkName.trim());
+                item.setType("runbook".equalsIgnoreCase(linkName) ? "runbook" : "link");
+                item.setUrl(url.trim());
+                result.add(item);
+            }
+        } else if (links instanceof List<?> items) {
+            for (Object itemValue : items) {
+                Map<String, Object> linkMap = toObjectMap(itemValue);
+                String url = defaultText(asText(linkMap.get("url")), asText(linkMap.get("href")));
+                if (!StringUtils.hasText(url)) {
+                    continue;
+                }
+                EntityDefinition.Link item = new EntityDefinition.Link();
+                item.setName(defaultText(asText(linkMap.get("name")), asText(linkMap.get("label"))));
+                item.setType(defaultText(asText(linkMap.get("type")), "link"));
+                item.setProvider(asText(linkMap.get("provider")));
+                item.setUrl(url.trim());
+                result.add(item);
+            }
+        }
+        if (StringUtils.hasText(runbookUrl)) {
+            boolean hasRunbook = result.stream().anyMatch(link -> "runbook".equalsIgnoreCase(defaultText(link.getType(), link.getName())));
+            if (!hasRunbook) {
+                EntityDefinition.Link runbook = new EntityDefinition.Link();
+                runbook.setName("runbook");
+                runbook.setType("runbook");
+                runbook.setUrl(runbookUrl.trim());
+                result.add(0, runbook);
+            }
+        }
+        return result;
+    }
+
+    private List<EntityDefinition.Contact> extractDefinitionContacts(String key, Map<String, Object> metadataMap,
+                                                                     Map<String, Object> specMap, Map<String, Object> rootMap) {
+        Object contacts = null;
+        if (key != null) {
+            if (metadataMap.containsKey(key)) {
+                contacts = metadataMap.get(key);
+            } else if (specMap.containsKey(key)) {
+                contacts = specMap.get(key);
+            } else if (rootMap.containsKey(key)) {
+                contacts = rootMap.get(key);
+            }
+        }
+        if (contacts instanceof Map<?, ?> contactMap) {
+            List<EntityDefinition.Contact> result = new ArrayList<>();
+            for (Map.Entry<?, ?> entry : contactMap.entrySet()) {
+                String contactName = entry.getKey() == null ? null : String.valueOf(entry.getKey());
+                String value = asText(entry.getValue());
+                if (!StringUtils.hasText(contactName) || !StringUtils.hasText(value)) {
+                    continue;
+                }
+                EntityDefinition.Contact item = new EntityDefinition.Contact();
+                item.setName(contactName.trim());
+                item.setType(contactName.trim());
+                item.setValue(value.trim());
+                item.setContact(value.trim());
+                result.add(item);
+            }
+            return result;
+        }
+        if (!(contacts instanceof List<?> items)) {
+            return Collections.emptyList();
+        }
+        List<EntityDefinition.Contact> result = new ArrayList<>();
+        for (Object itemValue : items) {
+            Map<String, Object> contactMap = toObjectMap(itemValue);
+            String value = defaultText(
+                    asText(contactMap.get("value")),
+                    asText(contactMap.get("contact")),
+                    asText(contactMap.get("url"))
+            );
+            if (!StringUtils.hasText(value)) {
+                continue;
+            }
+            EntityDefinition.Contact item = new EntityDefinition.Contact();
+            item.setName(defaultText(asText(contactMap.get("name")), asText(contactMap.get("label"))));
+            item.setType(defaultText(asText(contactMap.get("type")), "contact"));
+            item.setValue(value.trim());
+            item.setContact(value.trim());
+            result.add(item);
+        }
+        return result;
+    }
+
+    private List<EntityDefinition.OwnerRef> extractDefinitionOwnerRefs(String ownerKey, Map<String, Object> metadataMap,
+                                                                       Map<String, Object> specMap) {
+        Object ownerValue = ownerKey == null ? null : (metadataMap.containsKey(ownerKey) ? metadataMap.get(ownerKey) : specMap.get(ownerKey));
+        if (!(ownerValue instanceof List<?> items)) {
+            return Collections.emptyList();
+        }
+        List<EntityDefinition.OwnerRef> result = new ArrayList<>();
+        Set<String> dedupe = new LinkedHashSet<>();
+        for (Object item : items) {
+            if (item instanceof String value) {
+                String name = value.trim();
+                if (dedupe.add(name + "|team")) {
+                    EntityDefinition.OwnerRef owner = new EntityDefinition.OwnerRef();
+                    owner.setName(name);
+                    owner.setType("team");
+                    result.add(owner);
+                }
+                continue;
+            }
+            Map<String, Object> ownerMap = toObjectMap(item);
+            String name = defaultText(asText(ownerMap.get("name")), asText(ownerMap.get("team")), asText(ownerMap.get("value")));
+            if (name != null) {
+                String type = defaultText(asText(ownerMap.get("type")), "team");
+                if (dedupe.add(name.trim() + "|" + type.trim())) {
+                    EntityDefinition.OwnerRef owner = new EntityDefinition.OwnerRef();
+                    owner.setName(name.trim());
+                    owner.setType(type.trim());
+                    result.add(owner);
+                }
+            }
+        }
+        return result;
+    }
+
+    private Map<String, Object> toObjectMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    private Map<String, String> toStringMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            String key = entry.getKey() == null ? null : String.valueOf(entry.getKey());
+            String itemValue = entry.getValue() == null ? null : String.valueOf(entry.getValue());
+            if (StringUtils.hasText(key) && itemValue != null) {
+                result.put(key, itemValue);
+            }
+        }
+        return result;
+    }
+
+    private String asText(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value);
+        return StringUtils.hasText(text) ? text.trim() : null;
+    }
+
+    private String defaultText(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionNormalizationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionNormalizationService.java
new file mode 100644
index 0000000000..451fcfbe85
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionNormalizationService.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Map;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.springframework.stereotype.Service;
+
+/**
+ * Normalizes raw entity definition documents into the canonical HertzBeat definition model.
+ */
+@Service
+public class EntityDefinitionNormalizationService {
+
+    private final EntityDefinitionDocumentFieldNormalizationService entityDefinitionDocumentFieldNormalizationService;
+    private final EntityDefinitionMetadataNormalizationService entityDefinitionMetadataNormalizationService;
+    private final EntityDefinitionSpecNormalizationService entityDefinitionSpecNormalizationService;
+    private final EntityDefinitionExtensionNormalizationService entityDefinitionExtensionNormalizationService;
+    private final EntityDefinitionTypeResolverService entityDefinitionTypeResolverService;
+    private final EntityDefinitionTelemetryNormalizationService entityDefinitionTelemetryNormalizationService;
+    private final EntityDefinitionRelationNormalizationService entityDefinitionRelationNormalizationService;
+    private final EntityDefinitionHertzbeatNormalizationService entityDefinitionHertzbeatNormalizationService;
+
+    public EntityDefinitionNormalizationService(EntityDefinitionDocumentFieldNormalizationService entityDefinitionDocumentFieldNormalizationService,
+                                                EntityDefinitionMetadataNormalizationService entityDefinitionMetadataNormalizationService,
+                                                EntityDefinitionSpecNormalizationService entityDefinitionSpecNormalizationService,
+                                                EntityDefinitionExtensionNormalizationService entityDefinitionExtensionNormalizationService,
+                                                EntityDefinitionTypeResolverService entityDefinitionTypeResolverService,
+                                                EntityDefinitionTelemetryNormalizationService entityDefinitionTelemetryNormalizationService,
+                                                EntityDefinitionRelationNormalizationService entityDefinitionRelationNormalizationService,
+                                                EntityDefinitionHertzbeatNormalizationService entityDefinitionHertzbeatNormalizationService) {
+        this.entityDefinitionDocumentFieldNormalizationService = entityDefinitionDocumentFieldNormalizationService;
+        this.entityDefinitionMetadataNormalizationService = entityDefinitionMetadataNormalizationService;
+        this.entityDefinitionSpecNormalizationService = entityDefinitionSpecNormalizationService;
+        this.entityDefinitionExtensionNormalizationService = entityDefinitionExtensionNormalizationService;
+        this.entityDefinitionTypeResolverService = entityDefinitionTypeResolverService;
+        this.entityDefinitionTelemetryNormalizationService = entityDefinitionTelemetryNormalizationService;
+        this.entityDefinitionRelationNormalizationService = entityDefinitionRelationNormalizationService;
+        this.entityDefinitionHertzbeatNormalizationService = entityDefinitionHertzbeatNormalizationService;
+    }
+
+    public EntityDefinition normalizeDefinition(Map<String, Object> root) {
+        Map<String, Object> specMap = entityDefinitionDocumentFieldNormalizationService.resolveDefinitionSpecMap(root);
+        Map<String, Object> telemetryMap =
+                entityDefinitionDocumentFieldNormalizationService.resolveDefinitionTelemetryMap(specMap);
+
+        EntityDefinition definition = new EntityDefinition();
+        String normalizedKind = entityDefinitionTypeResolverService.resolveDefinitionEntityType(root, specMap);
+        definition.setApiVersion(entityDefinitionDocumentFieldNormalizationService.resolveDefinitionApiVersion(
+                root, entityDefinitionTypeResolverService.defaultApiVersion()));
+        definition.setKind(normalizedKind);
+
+        EntityDefinition.Metadata metadata = entityDefinitionMetadataNormalizationService.extractDefinitionMetadata(root, specMap);
+        definition.setMetadata(metadata);
+
+        EntityDefinition.Spec spec = entityDefinitionSpecNormalizationService.extractDefinitionSpec(
+                root,
+                specMap,
+                metadata,
+                entityDefinitionTypeResolverService.resolveDefinitionSubtype(root, specMap, normalizedKind),
+                entityDefinitionMetadataNormalizationService.extractDefinitionRunbook(root, specMap));
+
+        spec.setTelemetry(entityDefinitionTelemetryNormalizationService.extractDefinitionTelemetry(telemetryMap));
+        entityDefinitionRelationNormalizationService.attachDefinitionRelations(spec, specMap);
+
+        entityDefinitionExtensionNormalizationService.attachDefinitionAddOns(definition, root, specMap);
+        entityDefinitionHertzbeatNormalizationService.attachDefinitionHertzbeat(definition, root, specMap);
+        definition.setSpec(spec);
+        return definition;
+    }
+
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionRelationNormalizationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionRelationNormalizationService.java
new file mode 100644
index 0000000000..5b4a8aefe0
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionRelationNormalizationService.java
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Normalizes relation and dependency evidence declared by entity definitions.
+ */
+@Service
+public class EntityDefinitionRelationNormalizationService {
+
+    private static final String SOURCE_MANUAL = "manual";
+    private static final String RELATION_CONFIRMED = "confirmed";
+
+    private final EntityRelationService entityRelationService;
+
+    public EntityDefinitionRelationNormalizationService(EntityRelationService entityRelationService) {
+        this.entityRelationService = entityRelationService;
+    }
+
+    public void attachDefinitionRelations(EntityDefinition.Spec spec, Map<String, Object> specMap) {
+        if (spec == null) {
+            return;
+        }
+        Map<String, Object> relationSpecMap = specMap == null ? Collections.emptyMap() : specMap;
+        List<EntityDefinition.Relation> relations = mergeDefinitionRelations(
+                extractDefinitionRelations(
+                        defaultText(
+                                relationSpecMap.containsKey("relations") ? "relations" : null,
+                                relationSpecMap.containsKey("dependencies") ? "dependencies" : null),
+                        relationSpecMap),
+                extractDefinitionDependsOn(relationSpecMap.get("dependsOn"))
+        );
+        spec.setRelations(relations);
+        spec.setDependsOn(extractRelationReferences(relations));
+    }
+
+    public List<EntityDefinition.Relation> extractDefinitionRelations(String relationKey, Map<String, Object> specMap) {
+        Object relationValue = relationKey == null ? null : specMap.get(relationKey);
+        if (!(relationValue instanceof List<?> items)) {
+            return Collections.emptyList();
+        }
+        List<EntityDefinition.Relation> result = new ArrayList<>();
+        for (Object item : items) {
+            Map<String, Object> relationMap = toObjectMap(item);
+            Long targetEntityId = asLong(defaultText(relationMap.containsKey("targetEntityId") ? "targetEntityId" : null,
+                    relationMap.containsKey("target") ? "target" : null), relationMap);
+            String targetRef = defaultText(asText(relationMap.get("targetRef")), asText(relationMap.get("ref")));
+            if (targetEntityId == null && !StringUtils.hasText(targetRef)) {
+                continue;
+            }
+            EntityDefinition.Relation relation = new EntityDefinition.Relation();
+            relation.setTargetEntityId(targetEntityId);
+            relation.setTargetRef(targetRef);
+            relation.setRelationType(defaultText(asText(relationMap.get("relationType")), "depends_on"));
+            relation.setRelationSource(defaultText(asText(relationMap.get("relationSource")), SOURCE_MANUAL));
+            relation.setStatus(defaultText(asText(relationMap.get("status")), RELATION_CONFIRMED));
+            relation.setScore(asInteger(relationMap.get("score")));
+            relation.setDescription(asText(relationMap.get("description")));
+            relation.setAttributes(toStringMap(relationMap.get("attributes")));
+            result.add(relation);
+        }
+        return result;
+    }
+
+    public List<EntityDefinition.Relation> extractDefinitionDependsOn(Object dependsOnValue) {
+        if (!(dependsOnValue instanceof List<?> items)) {
+            return Collections.emptyList();
+        }
+        List<EntityDefinition.Relation> result = new ArrayList<>();
+        for (Object item : items) {
+            EntityDefinition.Relation relation = new EntityDefinition.Relation();
+            if (item instanceof String text) {
+                relation.setTargetRef(asText(text));
+            } else {
+                Map<String, Object> dependencyMap = toObjectMap(item);
+                relation.setTargetRef(defaultText(asText(dependencyMap.get("ref")),
+                        asText(dependencyMap.get("entity")), asText(dependencyMap.get("entityRef"))));
+                relation.setTargetEntityId(asLong(defaultText(
+                        dependencyMap.containsKey("targetEntityId") ? "targetEntityId" : null,
+                        dependencyMap.containsKey("id") ? "id" : null
+                ), dependencyMap));
+            }
+            if (relation.getTargetEntityId() == null && !StringUtils.hasText(relation.getTargetRef())) {
+                continue;
+            }
+            relation.setRelationType("depends_on");
+            relation.setRelationSource(SOURCE_MANUAL);
+            relation.setStatus(RELATION_CONFIRMED);
+            relation.setScore(100);
+            result.add(relation);
+        }
+        return result;
+    }
+
+    public List<EntityDefinition.Relation> mergeDefinitionRelations(List<EntityDefinition.Relation> primary,
+                                                                    List<EntityDefinition.Relation> additional) {
+        if (CollectionUtils.isEmpty(primary)) {
+            return CollectionUtils.isEmpty(additional) ? Collections.emptyList() : additional;
+        }
+        if (CollectionUtils.isEmpty(additional)) {
+            return primary;
+        }
+        List<EntityDefinition.Relation> result = new ArrayList<>(primary);
+        Set<String> seen = primary.stream().map(this::relationSignature).collect(Collectors.toCollection(LinkedHashSet::new));
+        for (EntityDefinition.Relation relation : additional) {
+            String signature = relationSignature(relation);
+            if (seen.add(signature)) {
+                result.add(relation);
+            }
+        }
+        return result;
+    }
+
+    public List<String> extractRelationReferences(List<EntityDefinition.Relation> relations) {
+        if (CollectionUtils.isEmpty(relations)) {
+            return Collections.emptyList();
+        }
+        return relations.stream()
+                .map(relation -> defaultText(relation.getTargetRef(), buildEntityReference(relation.getTargetEntityId())))
+                .filter(StringUtils::hasText)
+                .distinct()
+                .toList();
+    }
+
+    private String relationSignature(EntityDefinition.Relation relation) {
+        return String.join("|",
+                defaultText(relation.getRelationType(), "depends_on"),
+                relation.getTargetEntityId() == null ? "" : String.valueOf(relation.getTargetEntityId()),
+                relation.getTargetRef() == null ? "" : relation.getTargetRef());
+    }
+
+    private String buildEntityReference(Long entityId) {
+        if (entityId == null || entityRelationService == null) {
+            return null;
+        }
+        return entityRelationService.buildEntityReference(entityId);
+    }
+
+    private Map<String, Object> toObjectMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    private Map<String, String> toStringMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            String key = entry.getKey() == null ? null : String.valueOf(entry.getKey());
+            String itemValue = entry.getValue() == null ? null : String.valueOf(entry.getValue());
+            if (StringUtils.hasText(key) && itemValue != null) {
+                result.put(key, itemValue);
+            }
+        }
+        return result;
+    }
+
+    private String asText(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value);
+        return StringUtils.hasText(text) ? text.trim() : null;
+    }
+
+    private Integer asInteger(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Number number) {
+            return number.intValue();
+        }
+        try {
+            return Integer.parseInt(String.valueOf(value).trim());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    private Long asLong(String key, Map<String, Object> source) {
+        if (!StringUtils.hasText(key)) {
+            return null;
+        }
+        return asLong(source.get(key));
+    }
+
+    private Long asLong(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Number number) {
+            return number.longValue();
+        }
+        try {
+            return Long.parseLong(String.valueOf(value).trim());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    private String defaultText(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionSpecNormalizationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionSpecNormalizationService.java
new file mode 100644
index 0000000000..6922ec8037
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionSpecNormalizationService.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Normalizes the spec profile portion of entity definition documents.
+ */
+@Service
+public class EntityDefinitionSpecNormalizationService {
+
+    private static final String SOURCE_MANUAL = "manual";
+
+    public EntityDefinition.Spec extractDefinitionSpec(Map<String, Object> root, Map<String, Object> specMap,
+                                                       EntityDefinition.Metadata metadata, String resolvedSubtype,
+                                                       String runbook) {
+        Map<String, Object> rootMap = root == null ? Collections.emptyMap() : root;
+        Map<String, Object> normalizedSpecMap = specMap == null ? Collections.emptyMap() : specMap;
+        EntityDefinition.Metadata safeMetadata = metadata == null ? new EntityDefinition.Metadata() : metadata;
+
+        EntityDefinition.Spec spec = new EntityDefinition.Spec();
+        spec.setType(resolvedSubtype);
+        spec.setSource(defaultText(asText(normalizedSpecMap.get("source")), SOURCE_MANUAL));
+        spec.setOwner(defaultText(asText(normalizedSpecMap.get("owner")),
+                asText(normalizedSpecMap.get("ownedBy")),
+                asText(normalizedSpecMap.get("team")),
+                safeMetadata.getOwner(),
+                asText(rootMap.get("team"))));
+        spec.setOwnedBy(defaultText(asText(normalizedSpecMap.get("ownedBy")),
+                asText(normalizedSpecMap.get("owner")),
+                asText(normalizedSpecMap.get("team")),
+                safeMetadata.getOwner(),
+                asText(rootMap.get("team"))));
+        spec.setNamespace(defaultText(safeMetadata.getNamespace(),
+                asText(normalizedSpecMap.get("namespace")),
+                asText(normalizedSpecMap.get("serviceNamespace")),
+                asText(rootMap.get("namespace"))));
+        spec.setEnvironment(defaultText(asText(normalizedSpecMap.get("environment")),
+                asText(normalizedSpecMap.get("deploymentEnvironment")),
+                asText(rootMap.get("environment")),
+                asText(rootMap.get("env"))));
+        spec.setCriticality(defaultText(asText(normalizedSpecMap.get("criticality")), asText(rootMap.get("criticality"))));
+        spec.setRunbook(runbook);
+        spec.setLifecycle(defaultText(asText(normalizedSpecMap.get("lifecycle")), asText(rootMap.get("lifecycle"))));
+        spec.setTier(defaultText(asText(normalizedSpecMap.get("tier")), asText(rootMap.get("tier"))));
+
+        List<String> components = defaultList(extractDefinitionStringList("components", normalizedSpecMap),
+                extractDefinitionStringList("components", rootMap));
+        List<String> componentOf = defaultList(extractDefinitionStringList("componentOf", normalizedSpecMap),
+                extractDefinitionStringList("componentOf", rootMap));
+        String explicitSpecSystem = defaultText(
+                asText(normalizedSpecMap.get("partOf")),
+                asText(normalizedSpecMap.get("system")),
+                asText(normalizedSpecMap.get("systemName")),
+                asText(normalizedSpecMap.get("system_name"))
+        );
+        String rootSystem = defaultText(
+                asText(rootMap.get("partOf")),
+                asText(rootMap.get("system")),
+                asText(rootMap.get("systemName")),
+                asText(rootMap.get("system_name")),
+                asText(rootMap.get("application"))
+        );
+        boolean deriveSystemFromLegacyComponentOf = !StringUtils.hasText(explicitSpecSystem) && componentOf.size() > 1;
+        String system = defaultText(explicitSpecSystem, deriveSystemFromLegacyComponentOf ? componentOf.getFirst() : null,
+                rootSystem);
+        if (deriveSystemFromLegacyComponentOf) {
+            componentOf = new ArrayList<>(componentOf.subList(1, componentOf.size()));
+        }
+        spec.setSystem(system);
+        spec.setPartOf(system);
+        spec.setComponentOf(componentOf);
+        spec.setComponents(components);
+        spec.setImplementedBy(defaultList(extractDefinitionStringList("implementedBy", normalizedSpecMap),
+                extractDefinitionStringList("implementedBy", rootMap)));
+        spec.setApiInterface(extractDefinitionApiInterface(firstNonNull(
+                normalizedSpecMap.get("interface"), rootMap.get("interface"))));
+        spec.setLanguages(defaultList(extractDefinitionStringList("languages", normalizedSpecMap),
+                extractDefinitionStringList("languages", rootMap)));
+        return spec;
+    }
+
+    private EntityDefinition.ApiInterface extractDefinitionApiInterface(Object value) {
+        Map<String, Object> interfaceMap = toObjectMap(value);
+        if (interfaceMap.isEmpty()) {
+            return null;
+        }
+        EntityDefinition.ApiInterface apiInterface = new EntityDefinition.ApiInterface();
+        Object definition = firstNonNull(interfaceMap.get("definition"), interfaceMap.get("schema"));
+        if (definition != null) {
+            apiInterface.setDefinition(definition);
+        }
+        apiInterface.setFileRef(defaultText(
+                asText(interfaceMap.get("fileRef")),
+                asText(interfaceMap.get("fileref")),
+                asText(interfaceMap.get("file_ref"))
+        ));
+        if (apiInterface.getDefinition() == null && !StringUtils.hasText(apiInterface.getFileRef())) {
+            return null;
+        }
+        return apiInterface;
+    }
+
+    private List<String> extractDefinitionStringList(String key, Map<String, Object> source) {
+        if (!StringUtils.hasText(key) || source == null || source.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Object rawValue = source.get(key);
+        if (rawValue instanceof List<?> items) {
+            return items.stream()
+                    .map(this::asText)
+                    .filter(StringUtils::hasText)
+                    .map(String::trim)
+                    .distinct()
+                    .toList();
+        }
+        String value = asText(rawValue);
+        if (StringUtils.hasText(value)) {
+            return List.of(value.trim());
+        }
+        return Collections.emptyList();
+    }
+
+    private <T> List<T> defaultList(List<T> primary, List<T> fallback) {
+        if (!CollectionUtils.isEmpty(primary)) {
+            return primary;
+        }
+        return CollectionUtils.isEmpty(fallback) ? Collections.emptyList() : fallback;
+    }
+
+    private Map<String, Object> toObjectMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    private String asText(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value);
+        return StringUtils.hasText(text) ? text.trim() : null;
+    }
+
+    private Object firstNonNull(Object... values) {
+        if (values == null) {
+            return null;
+        }
+        for (Object value : values) {
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private String defaultText(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTelemetryNormalizationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTelemetryNormalizationService.java
new file mode 100644
index 0000000000..6653527228
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTelemetryNormalizationService.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Normalizes telemetry identity and monitor-bind evidence declared by entity definitions.
+ */
+@Service
+public class EntityDefinitionTelemetryNormalizationService {
+
+    private static final String SOURCE_MANUAL = "manual";
+    private static final String BIND_ACTIVE = "active";
+
+    public EntityDefinition.Telemetry extractDefinitionTelemetry(Map<String, Object> telemetryMap) {
+        if (telemetryMap == null) {
+            return null;
+        }
+        EntityDefinition.Telemetry telemetry = new EntityDefinition.Telemetry();
+        telemetry.setIdentities(extractDefinitionIdentities(telemetryMap.get("identities")));
+        telemetry.setMonitors(extractDefinitionMonitorBinds(telemetryMap.get("monitors")));
+        if (CollectionUtils.isEmpty(telemetry.getIdentities())
+                && CollectionUtils.isEmpty(telemetry.getMonitors())) {
+            return null;
+        }
+        return telemetry;
+    }
+
+    public List<EntityDefinition.Identity> extractDefinitionIdentities(Object identities) {
+        if (!(identities instanceof List<?> items)) {
+            return Collections.emptyList();
+        }
+        List<EntityDefinition.Identity> result = new ArrayList<>();
+        for (Object item : items) {
+            Map<String, Object> identityMap = toObjectMap(item);
+            String key = asText(identityMap.get("key"));
+            String value = asText(identityMap.get("value"));
+            if (!StringUtils.hasText(key) || !StringUtils.hasText(value)) {
+                continue;
+            }
+            EntityDefinition.Identity identity = new EntityDefinition.Identity();
+            identity.setKey(key.trim());
+            identity.setValue(value.trim());
+            identity.setType(defaultText(asText(identityMap.get("type")),
+                    asText(identityMap.get("source")), SOURCE_MANUAL));
+            identity.setPriority(asInteger(identityMap.get("priority")));
+            identity.setPrimary(asBoolean(identityMap.get("primary")));
+            result.add(identity);
+        }
+        return result;
+    }
+
+    public List<EntityDefinition.MonitorBind> extractDefinitionMonitorBinds(Object monitors) {
+        if (!(monitors instanceof List<?> items)) {
+            return Collections.emptyList();
+        }
+        List<EntityDefinition.MonitorBind> result = new ArrayList<>();
+        for (Object item : items) {
+            Map<String, Object> bindMap = toObjectMap(item);
+            Long monitorId = asLong(defaultText(bindMap.containsKey("monitorId") ? "monitorId" : null,
+                    bindMap.containsKey("id") ? "id" : null), bindMap);
+            if (monitorId == null) {
+                continue;
+            }
+            EntityDefinition.MonitorBind bind = new EntityDefinition.MonitorBind();
+            bind.setMonitorId(monitorId);
+            bind.setBindType(defaultText(asText(bindMap.get("bindType")), SOURCE_MANUAL));
+            bind.setBindSource(defaultText(asText(bindMap.get("bindSource")), SOURCE_MANUAL));
+            bind.setStatus(defaultText(asText(bindMap.get("status")), BIND_ACTIVE));
+            bind.setScore(asInteger(bindMap.get("score")));
+            bind.setMatchContext(toStringListMap(bindMap.get("matchContext")));
+            result.add(bind);
+        }
+        return result;
+    }
+
+    private Map<String, Object> toObjectMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    private Map<String, List<String>> toStringListMap(Object value) {
+        if (!(value instanceof Map<?, ?> rawMap)) {
+            return Collections.emptyMap();
+        }
+        Map<String, List<String>> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            String key = entry.getKey() == null ? null : String.valueOf(entry.getKey());
+            if (!StringUtils.hasText(key) || entry.getValue() == null) {
+                continue;
+            }
+            List<String> values = new ArrayList<>();
+            if (entry.getValue() instanceof List<?> items) {
+                for (Object item : items) {
+                    String text = asText(item);
+                    if (text != null) {
+                        values.add(text);
+                    }
+                }
+            } else {
+                String text = asText(entry.getValue());
+                if (text != null) {
+                    values.add(text);
+                }
+            }
+            if (!values.isEmpty()) {
+                result.put(key, values);
+            }
+        }
+        return result;
+    }
+
+    private String asText(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value);
+        return StringUtils.hasText(text) ? text.trim() : null;
+    }
+
+    private Integer asInteger(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Number number) {
+            return number.intValue();
+        }
+        try {
+            return Integer.parseInt(String.valueOf(value).trim());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    private Boolean asBoolean(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Boolean booleanValue) {
+            return booleanValue;
+        }
+        return Boolean.parseBoolean(String.valueOf(value).trim());
+    }
+
+    private Long asLong(String key, Map<String, Object> source) {
+        if (!StringUtils.hasText(key)) {
+            return null;
+        }
+        return asLong(source.get(key));
+    }
+
+    private Long asLong(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Number number) {
+            return number.longValue();
+        }
+        try {
+            return Long.parseLong(String.valueOf(value).trim());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    private String defaultText(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTypeResolverService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTypeResolverService.java
new file mode 100644
index 0000000000..a86d6859e5
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDefinitionTypeResolverService.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * Resolves entity definition kind/type aliases into the canonical HertzBeat taxonomy.
+ */
+@Service
+public class EntityDefinitionTypeResolverService {
+
+    private static final String ENTITY_DEFINITION_API_VERSION = "hertzbeat/v1";
+    private static final String LEGACY_ENTITY_DEFINITION_KIND = "Entity";
+    private static final String TYPE_SYSTEM = "system";
+    private static final String TYPE_DATABASE = "database";
+    private static final String TYPE_QUEUE = "queue";
+    private static final String TYPE_API = "api";
+    private static final String TYPE_ENDPOINT = "endpoint";
+    private static final String KIND_DATASTORE = "datastore";
+    private static final String KIND_API = "api";
+    private static final Set<String> SUPPORTED_TYPES = Set.of(
+            "service", "host", TYPE_SYSTEM, TYPE_DATABASE, TYPE_QUEUE, "middleware", "device", TYPE_API, TYPE_ENDPOINT, "k8s_workload"
+    );
+
+    public String defaultApiVersion() {
+        return ENTITY_DEFINITION_API_VERSION;
+    }
+
+    public String resolveDefinitionEntityType(Map<String, Object> root, Map<String, Object> specMap) {
+        String normalizedKind = normalizeSupportedEntityType(asText(root.get("kind")));
+        if (StringUtils.hasText(normalizedKind)) {
+            return normalizedKind;
+        }
+        String normalizedEntityType = normalizeSupportedEntityType(defaultText(
+                asText(specMap.get("entityType")),
+                asText(specMap.get("entity_type")),
+                asText(root.get("entityType")),
+                asText(root.get("entity_type"))
+        ));
+        if (StringUtils.hasText(normalizedEntityType)) {
+            return normalizedEntityType;
+        }
+        return defaultText(
+                normalizeSupportedEntityType(asText(specMap.get("type"))),
+                normalizeSupportedEntityType(asText(root.get("type"))),
+                asText(root.get("dd-service")) != null || asText(root.get("dd_service")) != null ? "service" : null,
+                "service"
+        );
+    }
+
+    public String resolveDefinitionSubtype(Map<String, Object> root, Map<String, Object> specMap, String entityType) {
+        String explicitSubtype = defaultText(
+                asText(specMap.get("subtype")),
+                asText(specMap.get("serviceType")),
+                asText(specMap.get("resourceType")),
+                asText(root.get("subtype")),
+                asText(root.get("serviceType")),
+                asText(root.get("resourceType"))
+        );
+        if (StringUtils.hasText(explicitSubtype)) {
+            return explicitSubtype.trim();
+        }
+        String rawType = defaultText(asText(specMap.get("type")), asText(root.get("type")));
+        if (!StringUtils.hasText(rawType)) {
+            return null;
+        }
+        String normalizedSupportedType = normalizeSupportedEntityType(rawType);
+        if (StringUtils.hasText(normalizedSupportedType)
+                && Objects.equals(normalizedSupportedType, entityType)) {
+            return null;
+        }
+        return rawType.trim();
+    }
+
+    private String normalizeEntityTypeFromKind(String kind) {
+        if (!StringUtils.hasText(kind)) {
+            return null;
+        }
+        if (LEGACY_ENTITY_DEFINITION_KIND.equalsIgnoreCase(kind.trim())) {
+            return null;
+        }
+        return switch (kind.trim().toLowerCase()) {
+            case KIND_DATASTORE -> TYPE_DATABASE;
+            case KIND_API -> TYPE_API;
+            default -> kind.trim().toLowerCase();
+        };
+    }
+
+    private String normalizeSupportedEntityType(String value) {
+        String normalized = normalizeEntityTypeFromKind(value);
+        if (!StringUtils.hasText(normalized)) {
+            return null;
+        }
+        return SUPPORTED_TYPES.contains(normalized) ? normalized : null;
+    }
+
+    private String asText(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value);
+        return StringUtils.hasText(text) ? text.trim() : null;
+    }
+
+    private String defaultText(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value.trim();
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDeletionWriteModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDeletionWriteModelService.java
new file mode 100644
index 0000000000..0a81778b6d
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDeletionWriteModelService.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import org.springframework.stereotype.Service;
+
+/**
+ * Owns entity deletion side effects behind the workspace boundary.
+ */
+@Service
+public class EntityDeletionWriteModelService {
+
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+    private final EntityIdentityWriteModelService entityIdentityWriteModelService;
+    private final EntityMonitorBindService entityMonitorBindService;
+    private final EntityRelationService entityRelationService;
+    private final EntityCoreWriteModelService entityCoreWriteModelService;
+
+    public EntityDeletionWriteModelService(EntityWorkspaceAccessService entityWorkspaceAccessService,
+                                           EntityIdentityWriteModelService entityIdentityWriteModelService,
+                                           EntityMonitorBindService entityMonitorBindService,
+                                           EntityRelationService entityRelationService,
+                                           EntityCoreWriteModelService entityCoreWriteModelService) {
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+        this.entityIdentityWriteModelService = entityIdentityWriteModelService;
+        this.entityMonitorBindService = entityMonitorBindService;
+        this.entityRelationService = entityRelationService;
+        this.entityCoreWriteModelService = entityCoreWriteModelService;
+    }
+
+    public boolean deleteEntity(long entityId) {
+        if (entityWorkspaceAccessService.findAccessibleEntityForRequestWorkspace(entityId).isEmpty()) {
+            return false;
+        }
+        entityIdentityWriteModelService.deleteIdentities(entityId);
+        entityMonitorBindService.deleteMonitorBinds(entityId);
+        entityRelationService.deleteRelationsForEntity(entityId);
+        entityCoreWriteModelService.deleteEntityById(entityId);
+        return true;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java
new file mode 100644
index 0000000000..294797e37e
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityAlertSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityEvidenceSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityUnifiedEvidenceSummary;
+import org.apache.hertzbeat.common.observability.dto.entity.MonitorInfo;
+import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
+import org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence;
+import org.apache.hertzbeat.common.observability.dto.evidence.TraceEvidence;
+import org.apache.hertzbeat.common.observability.dto.handoff.EntityResponseHandoffsInfo;
+import org.apache.hertzbeat.common.observability.dto.log.EntityLogQueryHint;
+import org.apache.hertzbeat.common.observability.dto.log.EntityLogSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceQueryHintDto;
+import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceSummaryDto;
+import org.apache.hertzbeat.common.observability.gateway.EntityObservabilityGateway;
+import org.apache.hertzbeat.common.observability.model.ObservedEntityContext;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionActivityInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDetailDto;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Assembles the entity detail observability payload behind a dedicated read-model boundary.
+ */
+@Service
+public class EntityDetailObservabilityReadModelService {
+
+    private final EntityDetailReadModelService entityDetailReadModelService;
+    private final EntityStatusRefreshService entityStatusRefreshService;
+    private final EntityObservabilityGateway entityObservabilityGateway;
+    private final EntityResponseHandoffReadModelService entityResponseHandoffReadModelService;
+    private final EntityNoiseControlReadModelService entityNoiseControlReadModelService;
+    private final EntityActivityReadModelService entityActivityReadModelService;
+
+    public EntityDetailObservabilityReadModelService(EntityDetailReadModelService entityDetailReadModelService,
+                                                     EntityStatusRefreshService entityStatusRefreshService,
+                                                     EntityObservabilityGateway entityObservabilityGateway,
+                                                     EntityResponseHandoffReadModelService entityResponseHandoffReadModelService,
+                                                     EntityNoiseControlReadModelService entityNoiseControlReadModelService,
+                                                     EntityActivityReadModelService entityActivityReadModelService) {
+        this.entityDetailReadModelService = entityDetailReadModelService;
+        this.entityStatusRefreshService = entityStatusRefreshService;
+        this.entityObservabilityGateway = entityObservabilityGateway;
+        this.entityResponseHandoffReadModelService = entityResponseHandoffReadModelService;
+        this.entityNoiseControlReadModelService = entityNoiseControlReadModelService;
+        this.entityActivityReadModelService = entityActivityReadModelService;
+    }
+
+    public EntityDetailDto buildEntityDetail(long entityId) {
+        EntityDto entityDto = entityDetailReadModelService.loadEntityDto(entityId);
+        if (entityDto == null) {
+            return null;
+        }
+        return buildEntityDetail(entityDto);
+    }
+
+    public EntityDetailDto buildEntityDetail(EntityDto entityDto) {
+        return buildEntityDetail(entityDto, null, true);
+    }
+
+    public EntityDetailDto buildEntityDetail(EntityDto entityDto, String requestWorkspaceId) {
+        return buildEntityDetail(entityDto, requestWorkspaceId, false);
+    }
+
+    private EntityDetailDto buildEntityDetail(EntityDto entityDto,
+                                              String requestWorkspaceId,
+                                              boolean resolveRequestWorkspaceAtEvidenceBoundaries) {
+        if (entityDto == null || entityDto.getEntity() == null) {
+            return null;
+        }
+        ObserveEntity entity = entityDto.getEntity();
+        Long entityId = entity.getId();
+        if (entityId == null) {
+            return null;
+        }
+        EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence =
+                resolveRequestWorkspaceAtEvidenceBoundaries
+                        ? entityStatusRefreshService.refreshEntityStatusWithEvidence(entity)
+                        : entityStatusRefreshService.refreshEntityStatusWithEvidence(entity, requestWorkspaceId);
+        List<Monitor> monitors = runtimeEvidence.monitors();
+        List<SingleAlert> activeAlerts = runtimeEvidence.activeAlerts();
+        EntityStatusInfo statusInfo = runtimeEvidence.statusInfo();
+        entityDto.setEntity(entity);
+        ObservedEntityContext entityContext = ObservedEntityContext.from(
+                entityDto.getEntity(), entityDto.getIdentities(),
+                entityDto.getEntityInfo() == null ? null : entityDto.getEntityInfo().getHertzbeat()
+        );
+        List<MonitorInfo> boundMonitors = monitors.stream().map(MonitorInfo::fromEntity).toList();
+        List<EntityLogQueryHint> logQueryHints =
+                entityObservabilityGateway.buildEntityLogQueryHints(entityDto.getIdentities(), monitors);
+        EntityEvidenceSummaryInfo evidenceSummary = entityObservabilityGateway.buildEntityEvidenceSummary(
+                entityDto.getEntity(),
+                statusInfo,
+                CollectionUtils.isEmpty(entityDto.getIdentities()) ? 0 : entityDto.getIdentities().size(),
+                logQueryHints == null ? 0 : logQueryHints.size(),
+                monitors,
+                activeAlerts
+        );
+        EntityAlertSummaryInfo alertSummary = entityObservabilityGateway.buildEntityAlertSummary(activeAlerts);
+        EntityMonitorSummaryInfo monitorSummary = entityObservabilityGateway.buildEntityMonitorSummary(monitors);
+        EntityLogSummaryInfo logSummary = entityObservabilityGateway.buildEntityLogSummary(logQueryHints);
+        EntityObservabilityDetailBundle observabilityDetail = entityObservabilityGateway.resolveEntityDetailBundle(
+                entityContext, statusInfo, evidenceSummary, monitorSummary, logSummary, monitors, logQueryHints);
+        logSummary = observabilityDetail.getLogSummary();
+        logQueryHints = observabilityDetail.getLogQueryHints();
+        EntityTraceSummaryDto traceSummary = observabilityDetail.getTraceSummary();
+        List<EntityTraceQueryHintDto> traceQueryHints = observabilityDetail.getTraceQueryHints();
+        List<MetricEvidence> metricEvidence = observabilityDetail.getMetricEvidence();
+        List<LogEvidence> logEvidence = observabilityDetail.getLogEvidence();
+        List<TraceEvidence> traceEvidence = observabilityDetail.getTraceEvidence();
+        EntityUnifiedEvidenceSummary unifiedEvidenceSummary = observabilityDetail.getUnifiedEvidenceSummary();
+        EntityTriageRecommendation triageRecommendation = observabilityDetail.getTriageRecommendation();
+        long relationCount = CollectionUtils.isEmpty(entityDto.getRelations()) ? 0 : entityDto.getRelations().size();
+        EntityOpsSummaryInfo opsSummary = entityObservabilityGateway.buildEntityOpsSummary(
+                entityDto.getEntity(), relationCount, evidenceSummary);
+        List<EntityNextActionInfo> nextActions =
+                entityObservabilityGateway.buildEntityNextActions(
+                        entityDto.getEntity(), evidenceSummary, logSummary, opsSummary);
+        EntityStatusPageSummaryInfo statusPageSummary =
+                entityObservabilityGateway.buildEntityStatusPageSummary(entity, opsSummary);
+        EntityResponseHandoffsInfo responseHandoffs =
+                entityResponseHandoffReadModelService.buildResponseHandoffs(
+                        entityId, entityContext, activeAlerts, monitors, logSummary, traceSummary,
+                        metricEvidence, logEvidence, traceEvidence, traceQueryHints, opsSummary);
+        EntityDetailDto.EntityNoiseControlSummaryInfo noiseControlSummary =
+                resolveRequestWorkspaceAtEvidenceBoundaries
+                        ? entityNoiseControlReadModelService.buildNoiseControlSummary(entityDto, monitors, activeAlerts)
+                        : entityNoiseControlReadModelService.buildNoiseControlSummary(
+                                entityDto, monitors, activeAlerts, requestWorkspaceId);
+        List<EntityDefinitionActivityInfo> definitionActivities =
+                resolveRequestWorkspaceAtEvidenceBoundaries
+                        ? entityActivityReadModelService.getDefinitionActivities(entityId, 12)
+                        : entityActivityReadModelService.getDefinitionActivities(entityId, 12, requestWorkspaceId);
+        return new EntityDetailDto(entityDto, statusInfo, evidenceSummary, alertSummary, monitorSummary, logSummary,
+                traceSummary, metricEvidence, logEvidence, traceEvidence, unifiedEvidenceSummary,
+                triageRecommendation, opsSummary, nextActions, statusPageSummary, responseHandoffs,
+                noiseControlSummary, boundMonitors, activeAlerts, logQueryHints, traceQueryHints,
+                definitionActivities);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailReadModelService.java
new file mode 100644
index 0000000000..8796bbdc28
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailReadModelService.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Optional;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
+import org.springframework.stereotype.Service;
+
+/**
+ * Loads the legacy entity detail DTO through a workspace-aware read boundary.
+ */
+@Service
+public class EntityDetailReadModelService {
+
+    private final EntityIdentityReadModelService entityIdentityReadModelService;
+    private final EntityMonitorBindService entityMonitorBindService;
+    private final EntityRelationService entityRelationService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityDetailReadModelService(EntityIdentityReadModelService entityIdentityReadModelService,
+                                        EntityMonitorBindService entityMonitorBindService,
+                                        EntityRelationService entityRelationService,
+                                        EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityIdentityReadModelService = entityIdentityReadModelService;
+        this.entityMonitorBindService = entityMonitorBindService;
+        this.entityRelationService = entityRelationService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public EntityDto loadEntityDto(long entityId) {
+        return buildEntityDto(entityId, entityWorkspaceAccessService.findAccessibleEntityForRequestWorkspace(entityId));
+    }
+
+    public EntityDto loadEntityDto(long entityId, String requestWorkspaceId) {
+        return buildEntityDto(entityId, entityWorkspaceAccessService.findAccessibleEntityById(
+                entityId, requestWorkspaceId));
+    }
+
+    private EntityDto buildEntityDto(long entityId, Optional<ObserveEntity> optional) {
+        if (optional.isEmpty()) {
+            return null;
+        }
+        ObserveEntity entity = optional.get();
+        EntityDto dto = new EntityDto();
+        dto.setEntity(entity);
+        dto.setIdentities(entityIdentityReadModelService.findIdentities(entityId));
+        dto.setMonitorBinds(entityMonitorBindService.findMonitorBinds(entityId));
+        dto.setRelations(entityRelationService.findEntityRelations(entityId));
+        return dto;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityEvidenceReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityEvidenceReadModelService.java
new file mode 100644
index 0000000000..8f5d019619
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityEvidenceReadModelService.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.observability.dto.entity.MonitorInfo;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+
+/**
+ * Coordinates entity evidence reads that require entity access and bound monitor context.
+ */
+@Service
+public class EntityEvidenceReadModelService {
+
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+    private final EntityMonitorBindService entityMonitorBindService;
+    private final EntityAlertEvidenceReadModelService entityAlertEvidenceReadModelService;
+    private final EntityMonitorEvidenceReadModelService entityMonitorEvidenceReadModelService;
+
+    public EntityEvidenceReadModelService(EntityWorkspaceAccessService entityWorkspaceAccessService,
+                                          EntityMonitorBindService entityMonitorBindService,
+                                          EntityAlertEvidenceReadModelService entityAlertEvidenceReadModelService,
+                                          EntityMonitorEvidenceReadModelService entityMonitorEvidenceReadModelService) {
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+        this.entityMonitorBindService = entityMonitorBindService;
+        this.entityAlertEvidenceReadModelService = entityAlertEvidenceReadModelService;
+        this.entityMonitorEvidenceReadModelService = entityMonitorEvidenceReadModelService;
+    }
+
+    public Page<SingleAlert> getEntityAlerts(long entityId, String status, String severity,
+                                             int pageIndex, int pageSize) {
+        PageRequest pageRequest = normalizePageRequest(
+                pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "gmtUpdate"));
+        if (!entityWorkspaceAccessService.isEntityAccessibleForRequestWorkspace(entityId)) {
+            return new PageImpl<>(Collections.emptyList(), pageRequest, 0);
+        }
+        List<Monitor> monitors = entityMonitorBindService.findEntityMonitors(entityId);
+        return entityAlertEvidenceReadModelService.buildEntityAlertPage(
+                monitors, status, severity, pageIndex, pageSize);
+    }
+
+    public Page<MonitorInfo> getEntityMonitors(long entityId, Byte status, String app, int pageIndex, int pageSize) {
+        PageRequest pageRequest = normalizePageRequest(pageIndex, pageSize, Sort.unsorted());
+        if (!entityWorkspaceAccessService.isEntityAccessibleForRequestWorkspace(entityId)) {
+            return new PageImpl<>(Collections.emptyList(), pageRequest, 0);
+        }
+        return entityMonitorEvidenceReadModelService.buildEntityMonitorPage(
+                entityMonitorBindService.findEntityMonitors(entityId), status, app, pageIndex, pageSize);
+    }
+
+    private PageRequest normalizePageRequest(int pageIndex, int pageSize, Sort sort) {
+        int safePageIndex = Math.max(pageIndex, 0);
+        int safePageSize = pageSize <= 0 ? 10 : Math.min(pageSize, 100);
+        return PageRequest.of(safePageIndex, safePageSize, sort);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateQueryService.java
new file mode 100644
index 0000000000..86e19e97d1
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateQueryService.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.hertzbeat.common.entity.manager.EntityGovernanceState;
+import org.apache.hertzbeat.manager.dao.EntityGovernanceStateDao;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * Owns raw persisted governance-state lookup and workspace-scoped read routing.
+ */
+@Service
+public class EntityGovernanceStateQueryService {
+
+    private final EntityGovernanceStateDao entityGovernanceStateDao;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityGovernanceStateQueryService(EntityGovernanceStateDao entityGovernanceStateDao,
+                                             EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityGovernanceStateDao = entityGovernanceStateDao;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public List<EntityGovernanceState> findGovernanceStates(
+            String stateScope, String stateKind, Pageable pageable) {
+        return findGovernanceStates(
+                stateScope, stateKind, pageable, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public List<EntityGovernanceState> findGovernanceStates(
+            String stateScope, String stateKind, Pageable pageable, String requestWorkspaceId) {
+        if (StringUtils.hasText(requestWorkspaceId)) {
+            return entityGovernanceStateDao.findAllByStateScopeAndStateKindAndWorkspaceIdOrderByGmtUpdateDescIdDesc(
+                    stateScope, stateKind, requestWorkspaceId, pageable);
+        }
+        return entityGovernanceStateDao.findAllByStateScopeAndStateKindOrderByGmtUpdateDescIdDesc(
+                stateScope, stateKind, pageable);
+    }
+
+    public Optional<EntityGovernanceState> findGovernanceState(
+            String stateScope, String stateKind, String stateKey) {
+        return findGovernanceState(
+                stateScope, stateKind, stateKey, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public Optional<EntityGovernanceState> findGovernanceState(
+            String stateScope, String stateKind, String stateKey, String requestWorkspaceId) {
+        if (StringUtils.hasText(requestWorkspaceId)) {
+            return entityGovernanceStateDao.findByStateScopeAndStateKindAndStateKeyAndWorkspaceId(
+                    stateScope, stateKind, stateKey, requestWorkspaceId);
+        }
+        return entityGovernanceStateDao.findByStateScopeAndStateKindAndStateKey(stateScope, stateKind, stateKey);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateService.java
new file mode 100644
index 0000000000..1cea1a9149
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateService.java
@@ -0,0 +1,661 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.hertzbeat.common.entity.manager.EntityGovernanceState;
+import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionWorkspaceActivityInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionWorkspaceResumeInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionWorkspaceTemplateInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDiscoveryGovernanceActivityInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDiscoveryGovernanceEntityRefInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDiscoveryGovernancePresetInfo;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+
+/**
+ * Owns workspace-scoped shared governance state for entity definition and discovery workflows.
+ */
+@Service
+public class EntityGovernanceStateService {
+
+    private static final String ACTIVITY_SUCCESS = "success";
+    private static final String FORMAT_JSON = "json";
+    private static final String FORMAT_YAML = "yaml";
+    private static final String FORMAT_CURL = "curl";
+    private static final String GOVERNANCE_SCOPE_DISCOVERY = "discovery";
+    private static final String GOVERNANCE_SCOPE_DEFINITION = "definition";
+    private static final String GOVERNANCE_STATE_KIND_PRESET = "preset";
+    private static final String GOVERNANCE_STATE_KIND_ACTIVITY = "activity";
+    private static final String GOVERNANCE_STATE_KIND_TEMPLATE = "template";
+    private static final String GOVERNANCE_STATE_KIND_RESUME = "resume";
+    private static final ObjectMapper PRETTY_JSON_MAPPER = JsonMapper.builder().build();
+
+    private final EntityGovernanceStateQueryService entityGovernanceStateQueryService;
+    private final EntityGovernanceStateWriteModelService entityGovernanceStateWriteModelService;
+
+    public EntityGovernanceStateService(EntityGovernanceStateQueryService entityGovernanceStateQueryService,
+                                        EntityGovernanceStateWriteModelService entityGovernanceStateWriteModelService) {
+        this.entityGovernanceStateQueryService = entityGovernanceStateQueryService;
+        this.entityGovernanceStateWriteModelService = entityGovernanceStateWriteModelService;
+    }
+
+    public List<EntityDefinitionWorkspaceTemplateInfo> getDefinitionWorkspaceTemplates(
+            String templateId, int limit) {
+        return getDefinitionWorkspaceTemplates(templateId, limit, null, true);
+    }
+
+    public List<EntityDefinitionWorkspaceTemplateInfo> getDefinitionWorkspaceTemplates(
+            String templateId, int limit, String requestWorkspaceId) {
+        return getDefinitionWorkspaceTemplates(templateId, limit, requestWorkspaceId, false);
+    }
+
+    private List<EntityDefinitionWorkspaceTemplateInfo> getDefinitionWorkspaceTemplates(
+            String templateId, int limit, String requestWorkspaceId, boolean useCurrentRequestWorkspace) {
+        if (StringUtils.hasText(templateId)) {
+            return readGovernanceState(
+                            GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_TEMPLATE, templateId.trim(),
+                            requestWorkspaceId, useCurrentRequestWorkspace)
+                    .map(this::toDefinitionWorkspaceTemplateInfo)
+                    .map(List::of)
+                    .orElseGet(List::of);
+        }
+        return readGovernanceStates(
+                        GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_TEMPLATE, pageRequest(limit),
+                        requestWorkspaceId, useCurrentRequestWorkspace)
+                .stream()
+                .map(this::toDefinitionWorkspaceTemplateInfo)
+                .toList();
+    }
+
+    public EntityDefinitionWorkspaceTemplateInfo saveDefinitionWorkspaceTemplate(
+            EntityDefinitionWorkspaceTemplateInfo templateInfo) {
+        return saveDefinitionWorkspaceTemplate(templateInfo, null, true);
+    }
+
+    public EntityDefinitionWorkspaceTemplateInfo saveDefinitionWorkspaceTemplate(
+            EntityDefinitionWorkspaceTemplateInfo templateInfo, String requestWorkspaceId) {
+        return saveDefinitionWorkspaceTemplate(templateInfo, requestWorkspaceId, false);
+    }
+
+    private EntityDefinitionWorkspaceTemplateInfo saveDefinitionWorkspaceTemplate(
+            EntityDefinitionWorkspaceTemplateInfo templateInfo, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (templateInfo == null || !StringUtils.hasText(templateInfo.getName())) {
+            throw new IllegalArgumentException("Definition workspace template name can not be blank.");
+        }
+        if (!StringUtils.hasText(templateInfo.getContent())) {
+            throw new IllegalArgumentException("Definition workspace template content can not be blank.");
+        }
+        String format = normalizeDefinitionActivityFormat(templateInfo.getFormat());
+        String templateId = defaultText(
+                trimToNull(templateInfo.getId()), "definition-template-" + SnowFlakeIdGenerator.generateId());
+        EntityGovernanceState state = prepareGovernanceStateForWrite(
+                GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_TEMPLATE, templateId,
+                requestWorkspaceId, useCurrentRequestWorkspace);
+        state.setStateScope(GOVERNANCE_SCOPE_DEFINITION);
+        state.setStateKind(GOVERNANCE_STATE_KIND_TEMPLATE);
+        state.setStateKey(templateId);
+        state.setStateName(limitLength(trimToNull(templateInfo.getName()), 128));
+        state.setStatus(limitLength(trimToNull(templateInfo.getSource()), 32));
+        Map<String, Object> content = new LinkedHashMap<>();
+        putIfPresent(content, "format", format);
+        putIfPresent(content, "content", trimToNull(templateInfo.getContent()));
+        putIfPresent(content, "summary", trimToNull(templateInfo.getSummary()));
+        putIfPresent(content, "source", trimToNull(templateInfo.getSource()));
+        putIfPresent(content, "kind", trimToNull(templateInfo.getKind()));
+        state.setContent(PRETTY_JSON_MAPPER.valueToTree(content));
+        return toDefinitionWorkspaceTemplateInfo(entityGovernanceStateWriteModelService.saveGovernanceState(state));
+    }
+
+    public void deleteDefinitionWorkspaceTemplate(String templateId) {
+        if (!StringUtils.hasText(templateId)) {
+            return;
+        }
+        removeGovernanceState(
+                GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_TEMPLATE, templateId.trim(), null, true);
+    }
+
+    public void deleteDefinitionWorkspaceTemplate(String templateId, String requestWorkspaceId) {
+        if (!StringUtils.hasText(templateId)) {
+            return;
+        }
+        removeGovernanceState(
+                GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_TEMPLATE, templateId.trim(),
+                requestWorkspaceId, false);
+    }
+
+    public List<EntityDefinitionWorkspaceActivityInfo> getDefinitionWorkspaceActivities(
+            String activityId, int limit) {
+        return getDefinitionWorkspaceActivities(activityId, limit, null, true);
+    }
+
+    public List<EntityDefinitionWorkspaceActivityInfo> getDefinitionWorkspaceActivities(
+            String activityId, int limit, String requestWorkspaceId) {
+        return getDefinitionWorkspaceActivities(activityId, limit, requestWorkspaceId, false);
+    }
+
+    private List<EntityDefinitionWorkspaceActivityInfo> getDefinitionWorkspaceActivities(
+            String activityId, int limit, String requestWorkspaceId, boolean useCurrentRequestWorkspace) {
+        if (StringUtils.hasText(activityId)) {
+            return readGovernanceState(
+                            GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_ACTIVITY, activityId.trim(),
+                            requestWorkspaceId, useCurrentRequestWorkspace)
+                    .map(this::toDefinitionWorkspaceActivityInfo)
+                    .map(List::of)
+                    .orElseGet(List::of);
+        }
+        return readGovernanceStates(
+                        GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_ACTIVITY, pageRequest(limit),
+                        requestWorkspaceId, useCurrentRequestWorkspace)
+                .stream()
+                .map(this::toDefinitionWorkspaceActivityInfo)
+                .toList();
+    }
+
+    public EntityDefinitionWorkspaceActivityInfo saveDefinitionWorkspaceActivity(
+            EntityDefinitionWorkspaceActivityInfo activityInfo) {
+        return saveDefinitionWorkspaceActivity(activityInfo, null, true);
+    }
+
+    public EntityDefinitionWorkspaceActivityInfo saveDefinitionWorkspaceActivity(
+            EntityDefinitionWorkspaceActivityInfo activityInfo, String requestWorkspaceId) {
+        return saveDefinitionWorkspaceActivity(activityInfo, requestWorkspaceId, false);
+    }
+
+    private EntityDefinitionWorkspaceActivityInfo saveDefinitionWorkspaceActivity(
+            EntityDefinitionWorkspaceActivityInfo activityInfo, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (activityInfo == null || !StringUtils.hasText(activityInfo.getSummary())) {
+            throw new IllegalArgumentException("Definition workspace activity summary can not be blank.");
+        }
+        String activityId = defaultText(
+                trimToNull(activityInfo.getId()), "definition-workspace-activity-" + SnowFlakeIdGenerator.generateId());
+        EntityGovernanceState state = prepareGovernanceStateForWrite(
+                GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_ACTIVITY, activityId,
+                requestWorkspaceId, useCurrentRequestWorkspace);
+        state.setStateScope(GOVERNANCE_SCOPE_DEFINITION);
+        state.setStateKind(GOVERNANCE_STATE_KIND_ACTIVITY);
+        state.setStateKey(activityId);
+        state.setStateName(limitLength(trimToNull(activityInfo.getSummary()), 128));
+        state.setStatus(limitLength(trimToNull(activityInfo.getStatus()), 32));
+        Map<String, Object> content = new LinkedHashMap<>();
+        putIfPresent(content, "format", normalizeDefinitionActivityFormat(activityInfo.getFormat()));
+        putIfPresent(content, "summary", trimToNull(activityInfo.getSummary()));
+        putIfPresent(content, "detail", trimToNull(activityInfo.getDetail()));
+        putIfPresent(content, "entityId", activityInfo.getEntityId());
+        putIfPresent(content, "entityName", trimToNull(activityInfo.getEntityName()));
+        state.setContent(PRETTY_JSON_MAPPER.valueToTree(content));
+        return toDefinitionWorkspaceActivityInfo(entityGovernanceStateWriteModelService.saveGovernanceState(state));
+    }
+
+    public EntityDefinitionWorkspaceResumeInfo getDefinitionWorkspaceResume(String resumeToken) {
+        return getDefinitionWorkspaceResume(resumeToken, null, true);
+    }
+
+    public EntityDefinitionWorkspaceResumeInfo getDefinitionWorkspaceResume(
+            String resumeToken, String requestWorkspaceId) {
+        return getDefinitionWorkspaceResume(resumeToken, requestWorkspaceId, false);
+    }
+
+    private EntityDefinitionWorkspaceResumeInfo getDefinitionWorkspaceResume(
+            String resumeToken, String requestWorkspaceId, boolean useCurrentRequestWorkspace) {
+        if (!StringUtils.hasText(resumeToken)) {
+            return null;
+        }
+        return readGovernanceState(
+                        GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_RESUME, resumeToken.trim(),
+                        requestWorkspaceId, useCurrentRequestWorkspace)
+                .map(this::toDefinitionWorkspaceResumeInfo)
+                .orElse(null);
+    }
+
+    public EntityDefinitionWorkspaceResumeInfo saveDefinitionWorkspaceResume(
+            EntityDefinitionWorkspaceResumeInfo resumeInfo) {
+        return saveDefinitionWorkspaceResume(resumeInfo, null, true);
+    }
+
+    public EntityDefinitionWorkspaceResumeInfo saveDefinitionWorkspaceResume(
+            EntityDefinitionWorkspaceResumeInfo resumeInfo, String requestWorkspaceId) {
+        return saveDefinitionWorkspaceResume(resumeInfo, requestWorkspaceId, false);
+    }
+
+    private EntityDefinitionWorkspaceResumeInfo saveDefinitionWorkspaceResume(
+            EntityDefinitionWorkspaceResumeInfo resumeInfo, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (resumeInfo == null || !StringUtils.hasText(resumeInfo.getToken())) {
+            throw new IllegalArgumentException("Definition workspace resume token can not be blank.");
+        }
+        if (!StringUtils.hasText(resumeInfo.getContent())) {
+            throw new IllegalArgumentException("Definition workspace resume content can not be blank.");
+        }
+        String resumeToken = resumeInfo.getToken().trim();
+        EntityGovernanceState state = prepareGovernanceStateForWrite(
+                GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_RESUME, resumeToken,
+                requestWorkspaceId, useCurrentRequestWorkspace);
+        state.setStateScope(GOVERNANCE_SCOPE_DEFINITION);
+        state.setStateKind(GOVERNANCE_STATE_KIND_RESUME);
+        state.setStateKey(resumeToken);
+        state.setStateName(limitLength(trimToNull(resumeInfo.getSource()), 128));
+        state.setStatus(limitLength(trimToNull(resumeInfo.getFormat()), 32));
+        Map<String, Object> content = new LinkedHashMap<>();
+        putIfPresent(content, "content", trimToNull(resumeInfo.getContent()));
+        putIfPresent(content, "format", normalizeDefinitionActivityFormat(resumeInfo.getFormat()));
+        putIfPresent(content, "source", trimToNull(resumeInfo.getSource()));
+        putIfPresent(content, "count", resumeInfo.getCount());
+        putIfPresent(content, "ownerDraft", trimToNull(resumeInfo.getOwnerDraft()));
+        putIfPresent(content, "systemDraft", trimToNull(resumeInfo.getSystemDraft()));
+        putIfPresent(content, "runbookDraft", trimToNull(resumeInfo.getRunbookDraft()));
+        if (resumeInfo.getQueryParams() != null && !resumeInfo.getQueryParams().isEmpty()) {
+            content.put("queryParams", resumeInfo.getQueryParams());
+        }
+        state.setContent(PRETTY_JSON_MAPPER.valueToTree(content));
+        return toDefinitionWorkspaceResumeInfo(entityGovernanceStateWriteModelService.saveGovernanceState(state));
+    }
+
+    public void deleteDefinitionWorkspaceResume(String resumeToken) {
+        if (!StringUtils.hasText(resumeToken)) {
+            return;
+        }
+        removeGovernanceState(
+                GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_RESUME, resumeToken.trim(), null, true);
+    }
+
+    public void deleteDefinitionWorkspaceResume(String resumeToken, String requestWorkspaceId) {
+        if (!StringUtils.hasText(resumeToken)) {
+            return;
+        }
+        removeGovernanceState(
+                GOVERNANCE_SCOPE_DEFINITION, GOVERNANCE_STATE_KIND_RESUME, resumeToken.trim(),
+                requestWorkspaceId, false);
+    }
+
+    public List<EntityDiscoveryGovernancePresetInfo> getDiscoveryGovernancePresets(int limit) {
+        return getDiscoveryGovernancePresets(limit, null, true);
+    }
+
+    public List<EntityDiscoveryGovernancePresetInfo> getDiscoveryGovernancePresets(
+            int limit, String requestWorkspaceId) {
+        return getDiscoveryGovernancePresets(limit, requestWorkspaceId, false);
+    }
+
+    private List<EntityDiscoveryGovernancePresetInfo> getDiscoveryGovernancePresets(
+            int limit, String requestWorkspaceId, boolean useCurrentRequestWorkspace) {
+        return readGovernanceStates(
+                        GOVERNANCE_SCOPE_DISCOVERY, GOVERNANCE_STATE_KIND_PRESET, pageRequest(limit),
+                        requestWorkspaceId, useCurrentRequestWorkspace)
+                .stream()
+                .map(this::toDiscoveryGovernancePresetInfo)
+                .toList();
+    }
+
+    public EntityDiscoveryGovernancePresetInfo saveDiscoveryGovernancePreset(
+            EntityDiscoveryGovernancePresetInfo presetInfo) {
+        return saveDiscoveryGovernancePreset(presetInfo, null, true);
+    }
+
+    public EntityDiscoveryGovernancePresetInfo saveDiscoveryGovernancePreset(
+            EntityDiscoveryGovernancePresetInfo presetInfo, String requestWorkspaceId) {
+        return saveDiscoveryGovernancePreset(presetInfo, requestWorkspaceId, false);
+    }
+
+    private EntityDiscoveryGovernancePresetInfo saveDiscoveryGovernancePreset(
+            EntityDiscoveryGovernancePresetInfo presetInfo, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (presetInfo == null || !StringUtils.hasText(presetInfo.getName())) {
+            throw new IllegalArgumentException("Discovery governance preset name can not be blank.");
+        }
+        String presetId = defaultText(trimToNull(presetInfo.getId()), "preset-" + SnowFlakeIdGenerator.generateId());
+        EntityGovernanceState state = prepareGovernanceStateForWrite(
+                GOVERNANCE_SCOPE_DISCOVERY, GOVERNANCE_STATE_KIND_PRESET, presetId,
+                requestWorkspaceId, useCurrentRequestWorkspace);
+        state.setStateScope(GOVERNANCE_SCOPE_DISCOVERY);
+        state.setStateKind(GOVERNANCE_STATE_KIND_PRESET);
+        state.setStateKey(presetId);
+        state.setStateName(limitLength(trimToNull(presetInfo.getName()), 128));
+        state.setStatus(limitLength(trimToNull(presetInfo.getStatus()), 32));
+        Map<String, Object> content = new LinkedHashMap<>();
+        putIfPresent(content, "owner", trimToNull(presetInfo.getOwner()));
+        putIfPresent(content, "system", trimToNull(presetInfo.getSystem()));
+        putIfPresent(content, "source", trimToNull(presetInfo.getSource()));
+        putIfPresent(content, "environment", trimToNull(presetInfo.getEnvironment()));
+        putIfPresent(content, "status", trimToNull(presetInfo.getStatus()));
+        putIfPresent(content, "bulkOwner", trimToNull(presetInfo.getBulkOwner()));
+        putIfPresent(content, "bulkSystem", trimToNull(presetInfo.getBulkSystem()));
+        state.setContent(PRETTY_JSON_MAPPER.valueToTree(content));
+        return toDiscoveryGovernancePresetInfo(entityGovernanceStateWriteModelService.saveGovernanceState(state));
+    }
+
+    public void deleteDiscoveryGovernancePreset(String presetId) {
+        if (!StringUtils.hasText(presetId)) {
+            return;
+        }
+        removeGovernanceState(GOVERNANCE_SCOPE_DISCOVERY, GOVERNANCE_STATE_KIND_PRESET, presetId.trim(), null, true);
+    }
+
+    public void deleteDiscoveryGovernancePreset(String presetId, String requestWorkspaceId) {
+        if (!StringUtils.hasText(presetId)) {
+            return;
+        }
+        removeGovernanceState(
+                GOVERNANCE_SCOPE_DISCOVERY, GOVERNANCE_STATE_KIND_PRESET, presetId.trim(),
+                requestWorkspaceId, false);
+    }
+
+    public List<EntityDiscoveryGovernanceActivityInfo> getDiscoveryGovernanceActivities(
+            String activityId, int limit) {
+        return getDiscoveryGovernanceActivities(activityId, limit, null, true);
+    }
+
+    public List<EntityDiscoveryGovernanceActivityInfo> getDiscoveryGovernanceActivities(
+            String activityId, int limit, String requestWorkspaceId) {
+        return getDiscoveryGovernanceActivities(activityId, limit, requestWorkspaceId, false);
+    }
+
+    private List<EntityDiscoveryGovernanceActivityInfo> getDiscoveryGovernanceActivities(
+            String activityId, int limit, String requestWorkspaceId, boolean useCurrentRequestWorkspace) {
+        if (StringUtils.hasText(activityId)) {
+            return readGovernanceState(
+                            GOVERNANCE_SCOPE_DISCOVERY, GOVERNANCE_STATE_KIND_ACTIVITY, activityId.trim(),
+                            requestWorkspaceId, useCurrentRequestWorkspace)
+                    .map(this::toDiscoveryGovernanceActivityInfo)
+                    .map(List::of)
+                    .orElseGet(List::of);
+        }
+        return readGovernanceStates(
+                        GOVERNANCE_SCOPE_DISCOVERY, GOVERNANCE_STATE_KIND_ACTIVITY, pageRequest(limit),
+                        requestWorkspaceId, useCurrentRequestWorkspace)
+                .stream()
+                .map(this::toDiscoveryGovernanceActivityInfo)
+                .toList();
+    }
+
+    public EntityDiscoveryGovernanceActivityInfo saveDiscoveryGovernanceActivity(
+            EntityDiscoveryGovernanceActivityInfo activityInfo) {
+        return saveDiscoveryGovernanceActivity(activityInfo, null, true);
+    }
+
+    public EntityDiscoveryGovernanceActivityInfo saveDiscoveryGovernanceActivity(
+            EntityDiscoveryGovernanceActivityInfo activityInfo, String requestWorkspaceId) {
+        return saveDiscoveryGovernanceActivity(activityInfo, requestWorkspaceId, false);
+    }
+
+    private EntityDiscoveryGovernanceActivityInfo saveDiscoveryGovernanceActivity(
+            EntityDiscoveryGovernanceActivityInfo activityInfo, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (activityInfo == null || !StringUtils.hasText(activityInfo.getSummary())) {
+            throw new IllegalArgumentException("Discovery governance activity summary can not be blank.");
+        }
+        String activityId = defaultText(trimToNull(activityInfo.getId()), "activity-" + SnowFlakeIdGenerator.generateId());
+        EntityGovernanceState state = prepareGovernanceStateForWrite(
+                GOVERNANCE_SCOPE_DISCOVERY, GOVERNANCE_STATE_KIND_ACTIVITY, activityId,
+                requestWorkspaceId, useCurrentRequestWorkspace);
+        state.setStateScope(GOVERNANCE_SCOPE_DISCOVERY);
+        state.setStateKind(GOVERNANCE_STATE_KIND_ACTIVITY);
+        state.setStateKey(activityId);
+        state.setStateName(limitLength(trimToNull(activityInfo.getSummary()), 128));
+        state.setStatus(limitLength(trimToNull(activityInfo.getStatus()), 32));
+        Map<String, Object> content = new LinkedHashMap<>();
+        putIfPresent(content, "action", trimToNull(activityInfo.getAction()));
+        putIfPresent(content, "summary", trimToNull(activityInfo.getSummary()));
+        putIfPresent(content, "detail", trimToNull(activityInfo.getDetail()));
+        putIfPresent(content, "workspacePath", trimToNull(activityInfo.getWorkspacePath()));
+        putIfPresent(content, "seedDefinitionDraft", trimToNull(activityInfo.getSeedDefinitionDraft()));
+        putIfPresent(content, "seedDefinitionFormat", trimToNull(activityInfo.getSeedDefinitionFormat()));
+        putIfPresent(content, "seedDefinitionSource", trimToNull(activityInfo.getSeedDefinitionSource()));
+        putIfPresent(content, "seedDefinitionCount", activityInfo.getSeedDefinitionCount());
+        putIfPresent(content, "entityRefs",
+                CollectionUtils.isEmpty(activityInfo.getEntityRefs()) ? null : activityInfo.getEntityRefs());
+        state.setContent(PRETTY_JSON_MAPPER.valueToTree(content));
+        return toDiscoveryGovernanceActivityInfo(entityGovernanceStateWriteModelService.saveGovernanceState(state));
+    }
+
+    private List<EntityGovernanceState> readGovernanceStates(
+            String stateScope, String stateKind, PageRequest pageRequest, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (useCurrentRequestWorkspace) {
+            return entityGovernanceStateQueryService.findGovernanceStates(stateScope, stateKind, pageRequest);
+        }
+        return entityGovernanceStateQueryService.findGovernanceStates(
+                stateScope, stateKind, pageRequest, requestWorkspaceId);
+    }
+
+    private Optional<EntityGovernanceState> readGovernanceState(
+            String stateScope, String stateKind, String stateKey, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (useCurrentRequestWorkspace) {
+            return entityGovernanceStateQueryService.findGovernanceState(stateScope, stateKind, stateKey);
+        }
+        return entityGovernanceStateQueryService.findGovernanceState(
+                stateScope, stateKind, stateKey, requestWorkspaceId);
+    }
+
+    private EntityGovernanceState prepareGovernanceStateForWrite(
+            String stateScope, String stateKind, String stateKey, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (useCurrentRequestWorkspace) {
+            return entityGovernanceStateWriteModelService.findGovernanceStateForWrite(stateScope, stateKind, stateKey);
+        }
+        return entityGovernanceStateWriteModelService.findGovernanceStateForWrite(
+                stateScope, stateKind, stateKey, requestWorkspaceId);
+    }
+
+    private void removeGovernanceState(
+            String stateScope, String stateKind, String stateKey, String requestWorkspaceId,
+            boolean useCurrentRequestWorkspace) {
+        if (useCurrentRequestWorkspace) {
+            entityGovernanceStateWriteModelService.deleteGovernanceState(stateScope, stateKind, stateKey);
+            return;
+        }
+        entityGovernanceStateWriteModelService.deleteGovernanceState(
+                stateScope, stateKind, stateKey, requestWorkspaceId);
+    }
+
+    private PageRequest pageRequest(int limit) {
+        int pageSize = limit <= 0 ? 8 : Math.min(limit, 50);
+        return PageRequest.of(0, pageSize);
+    }
+
+    private EntityDefinitionWorkspaceTemplateInfo toDefinitionWorkspaceTemplateInfo(EntityGovernanceState state) {
+        JsonNode content = state.getContent();
+        LocalDateTime updatedAt = state.getGmtUpdate() != null ? state.getGmtUpdate() : state.getGmtCreate();
+        return new EntityDefinitionWorkspaceTemplateInfo(
+                state.getStateKey(),
+                defaultText(trimToNull(state.getStateName()), state.getStateKey()),
+                defaultText(jsonText(content, "format"), FORMAT_YAML),
+                defaultText(jsonText(content, "content"), ""),
+                jsonText(content, "summary"),
+                defaultText(jsonText(content, "source"), state.getStatus()),
+                jsonText(content, "kind"),
+                state.getCreator(),
+                updatedAt
+        );
+    }
+
+    private EntityDefinitionWorkspaceActivityInfo toDefinitionWorkspaceActivityInfo(EntityGovernanceState state) {
+        JsonNode content = state.getContent();
+        LocalDateTime happenedAt = state.getGmtUpdate() != null ? state.getGmtUpdate() : state.getGmtCreate();
+        Long entityId = content != null && content.hasNonNull("entityId") ? content.get("entityId").asLong() : null;
+        return new EntityDefinitionWorkspaceActivityInfo(
+                state.getStateKey(),
+                happenedAt,
+                defaultText(state.getStatus(), ACTIVITY_SUCCESS),
+                defaultText(jsonText(content, "format"), FORMAT_YAML),
+                defaultText(jsonText(content, "summary"), trimToNull(state.getStateName()), state.getStateKey()),
+                jsonText(content, "detail"),
+                entityId,
+                jsonText(content, "entityName"),
+                state.getCreator()
+        );
+    }
+
+    private EntityDefinitionWorkspaceResumeInfo toDefinitionWorkspaceResumeInfo(EntityGovernanceState state) {
+        JsonNode content = state.getContent();
+        LocalDateTime updatedAt = state.getGmtUpdate() != null ? state.getGmtUpdate() : state.getGmtCreate();
+        return new EntityDefinitionWorkspaceResumeInfo(
+                state.getStateKey(),
+                defaultText(jsonText(content, "content"), ""),
+                defaultText(jsonText(content, "format"), FORMAT_YAML),
+                defaultText(jsonText(content, "source"), trimToNull(state.getStateName())),
+                jsonInteger(content, "count"),
+                jsonText(content, "ownerDraft"),
+                jsonText(content, "systemDraft"),
+                jsonText(content, "runbookDraft"),
+                jsonStringMap(content == null ? null : content.get("queryParams")),
+                state.getCreator(),
+                updatedAt
+        );
+    }
+
+    private EntityDiscoveryGovernancePresetInfo toDiscoveryGovernancePresetInfo(EntityGovernanceState state) {
+        JsonNode content = state.getContent();
+        LocalDateTime updatedAt = state.getGmtUpdate() != null ? state.getGmtUpdate() : state.getGmtCreate();
+        return new EntityDiscoveryGovernancePresetInfo(
+                state.getStateKey(),
+                defaultText(trimToNull(state.getStateName()), state.getStateKey()),
+                jsonText(content, "owner"),
+                jsonText(content, "system"),
+                jsonText(content, "source"),
+                jsonText(content, "environment"),
+                defaultText(jsonText(content, "status"), state.getStatus()),
+                jsonText(content, "bulkOwner"),
+                jsonText(content, "bulkSystem"),
+                state.getCreator(),
+                updatedAt
+        );
+    }
+
+    private EntityDiscoveryGovernanceActivityInfo toDiscoveryGovernanceActivityInfo(EntityGovernanceState state) {
+        JsonNode content = state.getContent();
+        LocalDateTime happenedAt = state.getGmtUpdate() != null ? state.getGmtUpdate() : state.getGmtCreate();
+        return new EntityDiscoveryGovernanceActivityInfo(
+                state.getStateKey(),
+                happenedAt,
+                defaultText(state.getStatus(), "info"),
+                defaultText(jsonText(content, "action"), "review"),
+                defaultText(jsonText(content, "summary"), trimToNull(state.getStateName()), state.getStateKey()),
+                jsonText(content, "detail"),
+                jsonEntityRefs(content == null ? null : content.get("entityRefs")),
+                jsonText(content, "workspacePath"),
+                jsonText(content, "seedDefinitionDraft"),
+                jsonText(content, "seedDefinitionFormat"),
+                jsonText(content, "seedDefinitionSource"),
+                jsonInteger(content, "seedDefinitionCount"),
+                state.getCreator()
+        );
+    }
+
+    private List<EntityDiscoveryGovernanceEntityRefInfo> jsonEntityRefs(JsonNode node) {
+        if (node == null || !node.isArray()) {
+            return List.of();
+        }
+        java.util.ArrayList<EntityDiscoveryGovernanceEntityRefInfo> refs = new java.util.ArrayList<>();
+        node.forEach(item -> {
+            Long entityId = item != null && item.hasNonNull("entityId") ? item.get("entityId").asLong() : null;
+            if (entityId != null) {
+                refs.add(new EntityDiscoveryGovernanceEntityRefInfo(entityId, jsonText(item, "entityName")));
+            }
+        });
+        return refs;
+    }
+
+    private Map<String, String> jsonStringMap(JsonNode node) {
+        if (node == null || !node.isObject()) {
+            return Map.of();
+        }
+        Map<String, String> raw = PRETTY_JSON_MAPPER.convertValue(node, new TypeReference<Map<String, String>>() { });
+        if (raw == null || raw.isEmpty()) {
+            return Map.of();
+        }
+        Map<String, String> values = new LinkedHashMap<>();
+        raw.forEach((key, value) -> {
+            String normalizedValue = trimToNull(value);
+            if (StringUtils.hasText(key) && normalizedValue != null) {
+                values.put(key, normalizedValue);
+            }
+        });
+        return values.isEmpty() ? Map.of() : values;
+    }
+
+    private String jsonText(JsonNode node, String fieldName) {
+        if (node == null || !node.has(fieldName) || node.get(fieldName).isNull()) {
+            return null;
+        }
+        return trimToNull(node.get(fieldName).asText());
+    }
+
+    private Integer jsonInteger(JsonNode node, String fieldName) {
+        if (node == null || !node.has(fieldName) || node.get(fieldName).isNull()) {
+            return null;
+        }
+        return node.get(fieldName).asInt();
+    }
+
+    private String normalizeDefinitionActivityFormat(String format) {
+        if (!StringUtils.hasText(format)) {
+            return null;
+        }
+        if (FORMAT_CURL.equalsIgnoreCase(defaultText(format, ""))) {
+            return FORMAT_CURL;
+        }
+        if (FORMAT_JSON.equalsIgnoreCase(defaultText(format, ""))) {
+            return FORMAT_JSON;
+        }
+        return FORMAT_YAML;
+    }
+
+    private void putIfPresent(Map<String, Object> content, String key, Object value) {
+        if (value != null) {
+            content.put(key, value);
+        }
+    }
+
+    private String limitLength(String value, int maxLength) {
+        if (value == null || value.length() <= maxLength) {
+            return value;
+        }
+        return value.substring(0, maxLength);
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateWriteModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateWriteModelService.java
new file mode 100644
index 0000000000..04b5add2b2
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceStateWriteModelService.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Optional;
+import org.apache.hertzbeat.common.entity.manager.EntityGovernanceState;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.dao.EntityGovernanceStateDao;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * Owns raw persisted governance-state upsert, delete, and workspace routing.
+ */
+@Service
+public class EntityGovernanceStateWriteModelService {
+
+    private final EntityGovernanceStateDao entityGovernanceStateDao;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityGovernanceStateWriteModelService(EntityGovernanceStateDao entityGovernanceStateDao,
+                                                  EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityGovernanceStateDao = entityGovernanceStateDao;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public EntityGovernanceState findGovernanceStateForWrite(
+            String stateScope, String stateKind, String stateKey) {
+        return findGovernanceStateForWrite(
+                stateScope, stateKind, stateKey, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public EntityGovernanceState findGovernanceStateForWrite(
+            String stateScope, String stateKind, String stateKey, String requestWorkspaceId) {
+        EntityGovernanceState state = findExistingGovernanceState(
+                stateScope, stateKind, stateKey, requestWorkspaceId).orElseGet(EntityGovernanceState::new);
+        state.setWorkspaceId(resolveGovernanceWorkspaceId(state.getWorkspaceId(), requestWorkspaceId));
+        return state;
+    }
+
+    public EntityGovernanceState saveGovernanceState(EntityGovernanceState state) {
+        return entityGovernanceStateDao.saveAndFlush(state);
+    }
+
+    public void deleteGovernanceState(
+            String stateScope, String stateKind, String stateKey) {
+        deleteGovernanceState(
+                stateScope, stateKind, stateKey, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public void deleteGovernanceState(
+            String stateScope, String stateKind, String stateKey, String requestWorkspaceId) {
+        if (StringUtils.hasText(requestWorkspaceId)) {
+            entityGovernanceStateDao.deleteByStateScopeAndStateKindAndStateKeyAndWorkspaceId(
+                    stateScope, stateKind, stateKey, requestWorkspaceId);
+            return;
+        }
+        entityGovernanceStateDao.deleteByStateScopeAndStateKindAndStateKey(stateScope, stateKind, stateKey);
+    }
+
+    private Optional<EntityGovernanceState> findExistingGovernanceState(
+            String stateScope, String stateKind, String stateKey, String requestWorkspaceId) {
+        if (StringUtils.hasText(requestWorkspaceId)) {
+            return entityGovernanceStateDao.findByStateScopeAndStateKindAndStateKeyAndWorkspaceId(
+                    stateScope, stateKind, stateKey, requestWorkspaceId);
+        }
+        return entityGovernanceStateDao.findByStateScopeAndStateKindAndStateKey(stateScope, stateKind, stateKey);
+    }
+
+    private String resolveGovernanceWorkspaceId(String currentWorkspaceId, String requestWorkspaceId) {
+        if (StringUtils.hasText(requestWorkspaceId)) {
+            return requestWorkspaceId;
+        }
+        if (StringUtils.hasText(currentWorkspaceId)) {
+            return AuthTokenScopes.normalizeWorkspaceId(currentWorkspaceId);
+        }
+        return AuthTokenScopes.DEFAULT_WORKSPACE_ID;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceWorkflowService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceWorkflowService.java
new file mode 100644
index 0000000000..f18e10c87b
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityGovernanceWorkflowService.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionWorkspaceActivityInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionWorkspaceResumeInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionWorkspaceTemplateInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDiscoveryGovernanceActivityInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDiscoveryGovernancePresetInfo;
+import org.springframework.stereotype.Service;
+
+/**
+ * Coordinates shared entity governance workflows.
+ */
+@Service
+public class EntityGovernanceWorkflowService {
+
+    private final EntityGovernanceStateService entityGovernanceStateService;
+
+    public EntityGovernanceWorkflowService(EntityGovernanceStateService entityGovernanceStateService) {
+        this.entityGovernanceStateService = entityGovernanceStateService;
+    }
+
+    public List<EntityDefinitionWorkspaceTemplateInfo> getDefinitionWorkspaceTemplates(String templateId, int limit) {
+        return entityGovernanceStateService.getDefinitionWorkspaceTemplates(templateId, limit);
+    }
+
+    public EntityDefinitionWorkspaceTemplateInfo saveDefinitionWorkspaceTemplate(
+            EntityDefinitionWorkspaceTemplateInfo templateInfo) {
+        return entityGovernanceStateService.saveDefinitionWorkspaceTemplate(templateInfo);
+    }
+
+    public void deleteDefinitionWorkspaceTemplate(String templateId) {
+        entityGovernanceStateService.deleteDefinitionWorkspaceTemplate(templateId);
+    }
+
+    public List<EntityDefinitionWorkspaceActivityInfo> getDefinitionWorkspaceActivities(String activityId, int limit) {
+        return entityGovernanceStateService.getDefinitionWorkspaceActivities(activityId, limit);
+    }
+
+    public EntityDefinitionWorkspaceActivityInfo saveDefinitionWorkspaceActivity(
+            EntityDefinitionWorkspaceActivityInfo activityInfo) {
+        return entityGovernanceStateService.saveDefinitionWorkspaceActivity(activityInfo);
+    }
+
+    public EntityDefinitionWorkspaceResumeInfo getDefinitionWorkspaceResume(String resumeToken) {
+        return entityGovernanceStateService.getDefinitionWorkspaceResume(resumeToken);
+    }
+
+    public EntityDefinitionWorkspaceResumeInfo saveDefinitionWorkspaceResume(
+            EntityDefinitionWorkspaceResumeInfo resumeInfo) {
+        return entityGovernanceStateService.saveDefinitionWorkspaceResume(resumeInfo);
+    }
+
+    public void deleteDefinitionWorkspaceResume(String resumeToken) {
+        entityGovernanceStateService.deleteDefinitionWorkspaceResume(resumeToken);
+    }
+
+    public List<EntityDiscoveryGovernancePresetInfo> getDiscoveryGovernancePresets(int limit) {
+        return entityGovernanceStateService.getDiscoveryGovernancePresets(limit);
+    }
+
+    public EntityDiscoveryGovernancePresetInfo saveDiscoveryGovernancePreset(
+            EntityDiscoveryGovernancePresetInfo presetInfo) {
+        return entityGovernanceStateService.saveDiscoveryGovernancePreset(presetInfo);
+    }
+
+    public void deleteDiscoveryGovernancePreset(String presetId) {
+        entityGovernanceStateService.deleteDiscoveryGovernancePreset(presetId);
+    }
+
+    public List<EntityDiscoveryGovernanceActivityInfo> getDiscoveryGovernanceActivities(String activityId, int limit) {
+        return entityGovernanceStateService.getDiscoveryGovernanceActivities(activityId, limit);
+    }
+
+    public EntityDiscoveryGovernanceActivityInfo saveDiscoveryGovernanceActivity(
+            EntityDiscoveryGovernanceActivityInfo activityInfo) {
+        return entityGovernanceStateService.saveDiscoveryGovernanceActivity(activityInfo);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityQueryService.java
new file mode 100644
index 0000000000..ef5cc05750
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityQueryService.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
+import org.springframework.stereotype.Service;
+
+/**
+ * Query boundary for raw persisted entity identity rows, counts, and match lookups.
+ */
+@Service
+public class EntityIdentityQueryService {
+
+    private final EntityIdentityDao entityIdentityDao;
+
+    public EntityIdentityQueryService(EntityIdentityDao entityIdentityDao) {
+        this.entityIdentityDao = entityIdentityDao;
+    }
+
+    public List<EntityIdentity> findIdentities(long entityId) {
+        return entityIdentityDao.findAllByEntityIdOrderByPriorityDescIdAsc(entityId);
+    }
+
+    public List<EntityIdentity> findMatchingIdentities(Set<String> identityKeys, Set<String> normalizedValues) {
+        return entityIdentityDao.findAllByIdentityKeyInAndNormalizedValueIn(identityKeys, normalizedValues);
+    }
+
+    public long countDistinctEntityIdsByIdentityKeys(Set<String> identityKeys) {
+        return entityIdentityDao.countDistinctEntityIdsByIdentityKeyIn(identityKeys);
+    }
+
+    public long countIdentities(long entityId) {
+        return entityIdentityDao.countByEntityId(entityId);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityReadModelService.java
new file mode 100644
index 0000000000..93f9b6dd21
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityReadModelService.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.springframework.stereotype.Service;
+
+/**
+ * Owns persisted entity identity read evidence.
+ */
+@Service
+public class EntityIdentityReadModelService {
+
+    private final EntityIdentityQueryService entityIdentityQueryService;
+
+    public EntityIdentityReadModelService(EntityIdentityQueryService entityIdentityQueryService) {
+        this.entityIdentityQueryService = entityIdentityQueryService;
+    }
+
+    public List<EntityIdentity> findIdentities(long entityId) {
+        return entityIdentityQueryService.findIdentities(entityId);
+    }
+
+    public List<EntityIdentity> findMatchingIdentities(Set<String> identityKeys, Set<String> normalizedValues) {
+        return entityIdentityQueryService.findMatchingIdentities(identityKeys, normalizedValues);
+    }
+
+    public long countIdentities(long entityId) {
+        return entityIdentityQueryService.countIdentities(entityId);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityResolutionService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityResolutionService.java
new file mode 100644
index 0000000000..e85378fcf1
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityResolutionService.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.model.EntityCanonicalIdentityRegistry;
+import org.apache.hertzbeat.manager.pojo.dto.EntityMonitorBindingCandidate;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * Resolves entity identity matches and monitor binding candidates.
+ */
+@Service
+public class EntityIdentityResolutionService {
+
+    private static final String RECOMMEND_DIRECT = "direct";
+    private static final String RECOMMEND_SUGGESTED = "suggested";
+
+    private static final Set<String> HOST_LIKE_APPS = Set.of(
+            "linux", "windows", "macos", "darwin", "centos", "debian", "ubuntu", "almalinux",
+            "redhat", "rockylinux", "opensuse", "euleros", "coreos", "freebsd"
+    );
+    private static final Set<String> SERVICE_LIKE_APPS = Set.of(
+            "springboot2", "springboot3", "jvm", "jetty", "tomcat", "nginx", "hertzbeat", "api",
+            "api_code", "fullsite", "website", "prometheus", "registry"
+    );
+    private static final Set<String> DATABASE_LIKE_APPS = Set.of(
+            "mysql", "postgresql", "postgres", "oracle", "sqlserver", "mongodb", "opengauss",
+            "tidb", "db2", "dm", "clickhouse", "elasticsearch"
+    );
+    private static final Set<String> MIDDLEWARE_LIKE_APPS = Set.of(
+            "redis", "kafka", "rabbitmq", "rocketmq", "zookeeper", "consul", "nacos",
+            "etcd", "memcached", "emq", "activemq"
+    );
+    private static final Set<String> QUEUE_LIKE_APPS = Set.of(
+            "kafka", "rabbitmq", "rocketmq", "emq", "activemq"
+    );
+    private static final Set<String> ENDPOINT_LIKE_APPS = Set.of(
+            "api", "api_code", "dns", "website", "fullsite", "port", "udp_port", "ping", "ssl_cert",
+            "websocket"
+    );
+    private static final Set<String> SYNTHETIC_IDENTITY_KEYS = Set.of(
+            "monitor.instance", "monitor.name", "monitor.app", "endpoint.url", "k8s.workload.name",
+            "messaging.destination.name"
+    );
+    private static final Map<String, Integer> IDENTITY_SCORES = Map.ofEntries(
+            Map.entry("endpoint.url", 120),
+            Map.entry("messaging.destination.name", 120),
+            Map.entry("monitor.instance", 80),
+            Map.entry("monitor.name", 50),
+            Map.entry("monitor.app", 20),
+            Map.entry("k8s.workload.name", 90)
+    );
+
+    private final EntityIdentityReadModelService entityIdentityReadModelService;
+    private final EntityMonitorBindService entityMonitorBindService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityIdentityResolutionService(EntityIdentityReadModelService entityIdentityReadModelService,
+                                           EntityMonitorBindService entityMonitorBindService,
+                                           EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityIdentityReadModelService = entityIdentityReadModelService;
+        this.entityMonitorBindService = entityMonitorBindService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public List<EntityMonitorBindingCandidate> resolveMonitorBindingCandidates(Monitor monitor) {
+        return resolveMonitorBindingCandidates(monitor, null, true);
+    }
+
+    public List<EntityMonitorBindingCandidate> resolveMonitorBindingCandidates(Monitor monitor,
+                                                                               String requestWorkspaceId) {
+        return resolveMonitorBindingCandidates(monitor, requestWorkspaceId, false);
+    }
+
+    private List<EntityMonitorBindingCandidate> resolveMonitorBindingCandidates(Monitor monitor,
+                                                                                String requestWorkspaceId,
+                                                                                boolean useRequestWorkspace) {
+        if (monitor == null || monitor.getId() == null) {
+            return Collections.emptyList();
+        }
+        Map<String, String> monitorIdentities = extractMonitorIdentityCandidates(monitor);
+        if (monitorIdentities.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<EntityIdentity> matchedIdentities = entityIdentityReadModelService.findMatchingIdentities(
+                monitorIdentities.keySet(), new HashSet<>(monitorIdentities.values())
+        );
+        Set<Long> boundEntityIds = entityMonitorBindService.findMonitorBindsByMonitorId(monitor.getId()).stream()
+                .map(EntityMonitorBind::getEntityId)
+                .collect(Collectors.toSet());
+        Map<Long, BindingCandidateAccumulator> candidates = new LinkedHashMap<>();
+        for (EntityIdentity identity : matchedIdentities) {
+            String candidateValue = monitorIdentities.get(identity.getIdentityKey());
+            if (!Objects.equals(candidateValue, identity.getNormalizedValue())) {
+                continue;
+            }
+            BindingCandidateAccumulator accumulator = candidates.computeIfAbsent(identity.getEntityId(),
+                    key -> new BindingCandidateAccumulator());
+            accumulator.score += matchScore(identity);
+            accumulator.matchedIdentities.computeIfAbsent(identity.getIdentityKey(), key -> new ArrayList<>())
+                    .add(identity.getIdentityValue());
+        }
+        if (candidates.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<ObserveEntity> accessibleEntities = useRequestWorkspace
+                ? entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(candidates.keySet())
+                : entityWorkspaceAccessService.findAccessibleEntitiesByIds(candidates.keySet(), requestWorkspaceId);
+        Map<Long, ObserveEntity> entityMap = accessibleEntities
+                .stream()
+                .collect(Collectors.toMap(ObserveEntity::getId, item -> item));
+        return candidates.entrySet().stream()
+                .map(entry -> toBindingCandidate(entry.getKey(), entry.getValue(), entityMap, boundEntityIds))
+                .filter(Objects::nonNull)
+                .sorted(Comparator.comparing(EntityMonitorBindingCandidate::getScore).reversed())
+                .toList();
+    }
+
+    public int defaultIdentityPriority(String identityKey) {
+        if (EntityCanonicalIdentityRegistry.isCanonicalOtelResourceKey(identityKey)) {
+            return EntityCanonicalIdentityRegistry.defaultPriority(identityKey);
+        }
+        if (identityKey != null && identityKey.startsWith("k8s.")) {
+            return 70;
+        }
+        return IDENTITY_SCORES.getOrDefault(identityKey, 40);
+    }
+
+    public String normalizeIdentityValue(String identityKey, String identityValue) {
+        if (!StringUtils.hasText(identityValue)) {
+            return identityValue;
+        }
+        if ("monitor.app".equals(identityKey)) {
+            return identityValue.trim().toLowerCase(Locale.ROOT);
+        }
+        return identityValue.trim().toLowerCase(Locale.ROOT);
+    }
+
+    private EntityMonitorBindingCandidate toBindingCandidate(Long entityId,
+                                                             BindingCandidateAccumulator accumulator,
+                                                             Map<Long, ObserveEntity> entityMap,
+                                                             Set<Long> boundEntityIds) {
+        ObserveEntity entity = entityMap.get(entityId);
+        if (entity == null) {
+            return null;
+        }
+        int score = accumulator.finalScore();
+        return new EntityMonitorBindingCandidate(
+                entity.getId(),
+                entity.getDisplayName() == null ? entity.getName() : entity.getDisplayName(),
+                entity.getType(),
+                score,
+                score >= 100 ? RECOMMEND_DIRECT : RECOMMEND_SUGGESTED,
+                boundEntityIds.contains(entity.getId()),
+                accumulator.matchedIdentities
+        );
+    }
+
+    private Map<String, String> extractMonitorIdentityCandidates(Monitor monitor) {
+        Map<String, String> identities = new LinkedHashMap<>();
+        putSupportedCandidates(identities, monitor.getLabels());
+        putSupportedCandidates(identities, monitor.getAnnotations());
+        String app = monitor.getApp() == null ? null : monitor.getApp().toLowerCase(Locale.ROOT);
+        if (StringUtils.hasText(app) && HOST_LIKE_APPS.contains(app)) {
+            putIdentityCandidate(identities, "host.name", monitor.getName());
+            putIdentityCandidate(identities, "host.id", monitor.getInstance());
+        }
+        if (StringUtils.hasText(app) && QUEUE_LIKE_APPS.contains(app)) {
+            putIdentityCandidate(identities, "messaging.destination.name", defaultText(monitor.getName(), monitor.getInstance()));
+        }
+        if (StringUtils.hasText(app) && (SERVICE_LIKE_APPS.contains(app) || DATABASE_LIKE_APPS.contains(app)
+                || (MIDDLEWARE_LIKE_APPS.contains(app) && !QUEUE_LIKE_APPS.contains(app)))) {
+            putIdentityCandidate(identities, "service.name", monitor.getName());
+        }
+        if (StringUtils.hasText(app) && ENDPOINT_LIKE_APPS.contains(app)) {
+            putIdentityCandidate(identities, "endpoint.url", monitor.getInstance());
+        }
+        if (containsK8sSignal(monitor.getLabels()) || containsK8sSignal(monitor.getAnnotations())) {
+            putIdentityCandidate(identities, "k8s.workload.name", monitor.getName());
+        }
+        putIdentityCandidate(identities, "monitor.instance", monitor.getInstance());
+        putIdentityCandidate(identities, "monitor.name", monitor.getName());
+        putIdentityCandidate(identities, "monitor.app", monitor.getApp());
+        return identities;
+    }
+
+    private void putSupportedCandidates(Map<String, String> target, Map<String, String> source) {
+        if (source == null || source.isEmpty()) {
+            return;
+        }
+        for (Map.Entry<String, String> entry : source.entrySet()) {
+            if (!StringUtils.hasText(entry.getKey()) || !StringUtils.hasText(entry.getValue())) {
+                continue;
+            }
+            if (isOtelResourceIdentity(entry.getKey()) || SYNTHETIC_IDENTITY_KEYS.contains(entry.getKey())) {
+                putIdentityCandidate(target, entry.getKey(), entry.getValue());
+            }
+        }
+    }
+
+    private void putIdentityCandidate(Map<String, String> target, String key, String value) {
+        if (!StringUtils.hasText(key) || !StringUtils.hasText(value) || target.containsKey(key)) {
+            return;
+        }
+        target.put(key, normalizeIdentityValue(key, value));
+    }
+
+    private int matchScore(EntityIdentity identity) {
+        int base = identity.getPriority() == null ? defaultIdentityPriority(identity.getIdentityKey()) : identity.getPriority();
+        if (identity.isPrimaryIdentity()) {
+            base += 10;
+        }
+        return base;
+    }
+
+    private boolean containsK8sSignal(Map<String, String> values) {
+        if (values == null || values.isEmpty()) {
+            return false;
+        }
+        return values.keySet().stream().anyMatch(key -> key != null && key.startsWith("k8s."));
+    }
+
+    private boolean isOtelResourceIdentity(String identityKey) {
+        if (!StringUtils.hasText(identityKey)) {
+            return false;
+        }
+        return EntityCanonicalIdentityRegistry.isCanonicalOtelResourceKey(identityKey);
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private static final class BindingCandidateAccumulator {
+        private int score;
+        private final Map<String, List<String>> matchedIdentities = new LinkedHashMap<>();
+
+        private int finalScore() {
+            int finalScore = score;
+            if (matchedIdentities.containsKey("service.name") && matchedIdentities.containsKey("service.namespace")) {
+                finalScore += 30;
+            }
+            if (matchedIdentities.containsKey("host.id") && matchedIdentities.containsKey("host.name")) {
+                finalScore += 20;
+            }
+            if (matchedIdentities.keySet().stream().anyMatch(key -> key.startsWith("k8s."))) {
+                finalScore += 20;
+            }
+            return finalScore;
+        }
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityWriteModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityWriteModelService.java
new file mode 100644
index 0000000000..93182b7d75
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIdentityWriteModelService.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Owns normalized identity row writes for entities.
+ */
+@Service
+public class EntityIdentityWriteModelService {
+
+    private static final String SOURCE_MANUAL = "manual";
+    private static final String SOURCE_DERIVED = "derived";
+    private static final String TYPE_QUEUE = "queue";
+    private static final String TYPE_API = "api";
+
+    private final EntityIdentityDao entityIdentityDao;
+    private final EntityIdentityResolutionService entityIdentityResolutionService;
+
+    public EntityIdentityWriteModelService(EntityIdentityDao entityIdentityDao,
+                                           EntityIdentityResolutionService entityIdentityResolutionService) {
+        this.entityIdentityDao = entityIdentityDao;
+        this.entityIdentityResolutionService = entityIdentityResolutionService;
+    }
+
+    public void replaceIdentities(ObserveEntity entity, List<EntityIdentity> identities) {
+        entityIdentityDao.deleteAllByEntityId(entity.getId());
+        entityIdentityDao.flush();
+        List<EntityIdentity> rows = new ArrayList<>();
+        Set<String> dedupeKeys = new LinkedHashSet<>();
+        if (!CollectionUtils.isEmpty(identities)) {
+            for (EntityIdentity identity : identities) {
+                if (!StringUtils.hasText(identity.getIdentityKey()) || !StringUtils.hasText(identity.getIdentityValue())) {
+                    continue;
+                }
+                String identityKey = identity.getIdentityKey().trim();
+                String identityType = defaultText(identity.getIdentityType(), SOURCE_MANUAL);
+                String identityValue = canonicalizeIdentityValue(
+                        entity, identityKey, identity.getIdentityValue().trim(), identityType);
+                if (!StringUtils.hasText(identityValue)) {
+                    continue;
+                }
+                String normalizedValue = entityIdentityResolutionService.normalizeIdentityValue(identityKey, identityValue);
+                String dedupeKey = identityKey + "\u0000" + normalizedValue;
+                if (!dedupeKeys.add(dedupeKey)) {
+                    continue;
+                }
+                rows.add(EntityIdentity.builder()
+                        .entityId(entity.getId())
+                        .identityType(identityType)
+                        .identityKey(identityKey)
+                        .identityValue(identityValue)
+                        .normalizedValue(normalizedValue)
+                        .priority(identity.getPriority() == null
+                                ? entityIdentityResolutionService.defaultIdentityPriority(identityKey)
+                                : identity.getPriority())
+                        .primaryIdentity(identity.isPrimaryIdentity())
+                        .build());
+            }
+        }
+        if (rows.isEmpty()) {
+            rows.addAll(buildDefaultIdentities(entity));
+        }
+        entityIdentityDao.saveAll(rows);
+    }
+
+    public void deleteIdentities(long entityId) {
+        entityIdentityDao.deleteAllByEntityId(entityId);
+    }
+
+    private List<EntityIdentity> buildDefaultIdentities(ObserveEntity entity) {
+        List<EntityIdentity> identities = new ArrayList<>();
+        String name = StringUtils.hasText(entity.getName()) ? entity.getName() : entity.getDisplayName();
+        String primaryKey = switch (entity.getType()) {
+            case "host", "device" -> "host.name";
+            case "endpoint" -> "endpoint.url";
+            case TYPE_QUEUE -> "messaging.destination.name";
+            case TYPE_API -> "service.name";
+            case "k8s_workload" -> "k8s.workload.name";
+            default -> "service.name";
+        };
+        if (StringUtils.hasText(name)) {
+            identities.add(EntityIdentity.builder()
+                    .entityId(entity.getId())
+                    .identityType(SOURCE_DERIVED)
+                    .identityKey(primaryKey)
+                    .identityValue(name)
+                    .normalizedValue(entityIdentityResolutionService.normalizeIdentityValue(primaryKey, name))
+                    .priority(entityIdentityResolutionService.defaultIdentityPriority(primaryKey))
+                    .primaryIdentity(true)
+                    .build());
+        }
+        if (StringUtils.hasText(entity.getNamespace())) {
+            identities.add(EntityIdentity.builder()
+                    .entityId(entity.getId())
+                    .identityType(SOURCE_DERIVED)
+                    .identityKey("service.namespace")
+                    .identityValue(entity.getNamespace())
+                    .normalizedValue(entityIdentityResolutionService.normalizeIdentityValue(
+                            "service.namespace", entity.getNamespace()))
+                    .priority(entityIdentityResolutionService.defaultIdentityPriority("service.namespace"))
+                    .primaryIdentity(false)
+                    .build());
+        }
+        if (StringUtils.hasText(entity.getEnvironment())) {
+            identities.add(EntityIdentity.builder()
+                    .entityId(entity.getId())
+                    .identityType(SOURCE_DERIVED)
+                    .identityKey("deployment.environment.name")
+                    .identityValue(entity.getEnvironment())
+                    .normalizedValue(entityIdentityResolutionService.normalizeIdentityValue(
+                            "deployment.environment.name", entity.getEnvironment()))
+                    .priority(entityIdentityResolutionService.defaultIdentityPriority("deployment.environment.name"))
+                    .primaryIdentity(false)
+                    .build());
+        }
+        return identities;
+    }
+
+    private String canonicalizeIdentityValue(ObserveEntity entity, String identityKey, String identityValue, String identityType) {
+        if (!StringUtils.hasText(identityType) || SOURCE_MANUAL.equalsIgnoreCase(identityType)) {
+            return identityValue;
+        }
+        return switch (identityKey) {
+            case "service.name" -> isServiceLikeEntity(entity) && StringUtils.hasText(entity.getName())
+                    ? entity.getName()
+                    : identityValue;
+            case "messaging.destination.name" -> TYPE_QUEUE.equals(entity.getType()) && StringUtils.hasText(entity.getName())
+                    ? entity.getName()
+                    : identityValue;
+            case "k8s.workload.name" -> StringUtils.hasText(entity.getName())
+                    ? entity.getName()
+                    : identityValue;
+            case "display_name" -> StringUtils.hasText(entity.getDisplayName())
+                    ? entity.getDisplayName()
+                    : identityValue;
+            default -> identityValue;
+        };
+    }
+
+    private boolean isServiceLikeEntity(ObserveEntity entity) {
+        if (entity == null || !StringUtils.hasText(entity.getType())) {
+            return false;
+        }
+        return switch (entity.getType()) {
+            case "service", "database", "middleware" -> true;
+            default -> false;
+        };
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIntegrationHintService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIntegrationHintService.java
new file mode 100644
index 0000000000..56ca09f10b
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityIntegrationHintService.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.manager.pojo.dto.EntityMonitorBindingCandidate;
+import org.springframework.stereotype.Service;
+
+/**
+ * Builds integration hints that connect traditional monitors to entity catalog candidates.
+ */
+@Service
+public class EntityIntegrationHintService {
+
+    private final EntityMonitorQueryService entityMonitorQueryService;
+    private final EntityIdentityResolutionService entityIdentityResolutionService;
+
+    public EntityIntegrationHintService(EntityMonitorQueryService entityMonitorQueryService,
+                                        EntityIdentityResolutionService entityIdentityResolutionService) {
+        this.entityMonitorQueryService = entityMonitorQueryService;
+        this.entityIdentityResolutionService = entityIdentityResolutionService;
+    }
+
+    public List<EntityMonitorBindingCandidate> getMonitorBindingCandidates(long monitorId) {
+        Monitor monitor = entityMonitorQueryService.findMonitor(monitorId).orElse(null);
+        if (monitor == null) {
+            return Collections.emptyList();
+        }
+        return entityIdentityResolutionService.resolveMonitorBindingCandidates(monitor);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityListReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityListReadModelService.java
new file mode 100644
index 0000000000..ef6d77623c
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityListReadModelService.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.common.entity.manager.EntityDefinitionActivity;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
+import org.apache.hertzbeat.manager.pojo.dto.EntitySummaryInfo;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * Reads and enriches entity catalog list pages from persisted catalog and runtime evidence.
+ */
+@Service
+public class EntityListReadModelService {
+
+    private final EntityCatalogQueryService entityCatalogQueryService;
+    private final EntityStatusRefreshService entityStatusRefreshService;
+    private final EntitySummaryReadModelService entitySummaryReadModelService;
+
+    public EntityListReadModelService(EntityCatalogQueryService entityCatalogQueryService,
+                                      EntityStatusRefreshService entityStatusRefreshService,
+                                      EntitySummaryReadModelService entitySummaryReadModelService) {
+        this.entityCatalogQueryService = entityCatalogQueryService;
+        this.entityStatusRefreshService = entityStatusRefreshService;
+        this.entitySummaryReadModelService = entitySummaryReadModelService;
+    }
+
+    public Page<EntitySummaryInfo> getEntities(List<Long> entityIds, String type, String status, String search,
+                                               String owner, String source, String environment, String lifecycle,
+                                               String tier, String system, String sort, String order,
+                                               int pageIndex, int pageSize) {
+        Page<ObserveEntity> entityPage = entityCatalogQueryService.findEntityPage(
+                entityIds, type, status, search, owner, source, environment, lifecycle, tier, system,
+                sort, order, pageIndex, pageSize);
+        return buildEntitySummaryPage(entityPage, null, false);
+    }
+
+    public Page<EntitySummaryInfo> getEntities(List<Long> entityIds, String type, String status, String search,
+                                               String owner, String source, String environment, String lifecycle,
+                                               String tier, String system, String sort, String order,
+                                               int pageIndex, int pageSize, String requestWorkspaceId) {
+        Page<ObserveEntity> entityPage = entityCatalogQueryService.findEntityPage(
+                entityIds, type, status, search, owner, source, environment, lifecycle, tier, system,
+                sort, order, pageIndex, pageSize, requestWorkspaceId);
+        return buildEntitySummaryPage(entityPage, requestWorkspaceId, true);
+    }
+
+    private Page<EntitySummaryInfo> buildEntitySummaryPage(Page<ObserveEntity> entityPage,
+                                                           String requestWorkspaceId,
+                                                           boolean explicitWorkspace) {
+        List<ObserveEntity> scopedEntities = entityPage.getContent();
+        Map<Long, EntityDefinitionActivity> latestActivityMap =
+                entitySummaryReadModelService.loadLatestDefinitionActivities(scopedEntities);
+        List<EntitySummaryInfo> summaries = scopedEntities.stream()
+                .map(entity -> explicitWorkspace
+                        ? buildSummary(entity, latestActivityMap.get(entity.getId()), requestWorkspaceId)
+                        : buildSummary(entity, latestActivityMap.get(entity.getId())))
+                .toList();
+        return new PageImpl<>(summaries, entityPage.getPageable(), entityPage.getTotalElements());
+    }
+
+    private EntitySummaryInfo buildSummary(ObserveEntity entity,
+                                           EntityDefinitionActivity latestActivity) {
+        EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence =
+                entityStatusRefreshService.refreshEntityStatusWithEvidence(entity);
+        List<Monitor> monitors = runtimeEvidence.monitors();
+        EntityStatusInfo statusInfo = runtimeEvidence.statusInfo();
+        return entitySummaryReadModelService.buildEntitySummary(entity, latestActivity, statusInfo, monitors);
+    }
+
+    private EntitySummaryInfo buildSummary(ObserveEntity entity,
+                                           EntityDefinitionActivity latestActivity,
+                                           String requestWorkspaceId) {
+        EntityStatusRefreshService.EntityRuntimeStatusEvidence runtimeEvidence =
+                entityStatusRefreshService.refreshEntityStatusWithEvidence(entity, requestWorkspaceId);
+        List<Monitor> monitors = runtimeEvidence.monitors();
+        EntityStatusInfo statusInfo = runtimeEvidence.statusInfo();
+        return entitySummaryReadModelService.buildEntitySummary(entity, latestActivity, statusInfo, monitors);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindQueryService.java
new file mode 100644
index 0000000000..824e467683
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindQueryService.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
+import org.apache.hertzbeat.manager.dao.EntityMonitorBindDao;
+import org.springframework.stereotype.Service;
+
+/**
+ * Owns persisted monitor-bind evidence lookup for entity read-model boundaries.
+ */
+@Service
+public class EntityMonitorBindQueryService {
+
+    private final EntityMonitorBindDao entityMonitorBindDao;
+
+    public EntityMonitorBindQueryService(EntityMonitorBindDao entityMonitorBindDao) {
+        this.entityMonitorBindDao = entityMonitorBindDao;
+    }
+
+    public List<EntityMonitorBind> findMonitorBinds(Long entityId) {
+        return entityMonitorBindDao.findAllByEntityIdOrderByIdAsc(entityId);
+    }
+
+    public List<EntityMonitorBind> findMonitorBindsByMonitorId(Long monitorId) {
+        if (monitorId == null) {
+            return Collections.emptyList();
+        }
+        return entityMonitorBindDao.findAllByMonitorId(monitorId);
+    }
+
+    public long countMonitorBinds(Long entityId) {
+        return entityMonitorBindDao.countByEntityId(entityId);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindService.java
new file mode 100644
index 0000000000..673c613880
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindService.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Maintains entity monitor bindings and hides manager DAO lookup details.
+ */
+@Service
+public class EntityMonitorBindService {
+
+    private static final String SOURCE_MANUAL = "manual";
+    private static final String BIND_ACTIVE = "active";
+
+    private final EntityMonitorBindQueryService entityMonitorBindQueryService;
+    private final EntityMonitorQueryService entityMonitorQueryService;
+    private final EntityMonitorBindWriteModelService entityMonitorBindWriteModelService;
+
+    public EntityMonitorBindService(EntityMonitorBindQueryService entityMonitorBindQueryService,
+                                    EntityMonitorQueryService entityMonitorQueryService,
+                                    EntityMonitorBindWriteModelService entityMonitorBindWriteModelService) {
+        this.entityMonitorBindQueryService = entityMonitorBindQueryService;
+        this.entityMonitorQueryService = entityMonitorQueryService;
+        this.entityMonitorBindWriteModelService = entityMonitorBindWriteModelService;
+    }
+
+    public void deleteMonitorBinds(Long entityId) {
+        entityMonitorBindWriteModelService.deleteMonitorBinds(entityId);
+    }
+
+    public void deleteMonitorBindsByMonitorIds(Set<Long> monitorIds) {
+        entityMonitorBindWriteModelService.deleteMonitorBindsByMonitorIds(monitorIds);
+    }
+
+    public List<EntityMonitorBind> findMonitorBinds(Long entityId) {
+        return entityMonitorBindQueryService.findMonitorBinds(entityId);
+    }
+
+    public List<EntityMonitorBind> findMonitorBindsByMonitorId(Long monitorId) {
+        return entityMonitorBindQueryService.findMonitorBindsByMonitorId(monitorId);
+    }
+
+    public long countMonitorBinds(Long entityId) {
+        return entityMonitorBindQueryService.countMonitorBinds(entityId);
+    }
+
+    public void replaceMonitorBinds(Long entityId, List<EntityMonitorBind> monitorBinds) {
+        List<EntityMonitorBind> rows = new ArrayList<>();
+        if (!CollectionUtils.isEmpty(monitorBinds)) {
+            for (EntityMonitorBind bind : monitorBinds) {
+                if (bind == null || !entityMonitorQueryService.monitorExists(bind.getMonitorId())) {
+                    continue;
+                }
+                rows.add(EntityMonitorBind.builder()
+                        .entityId(entityId)
+                        .monitorId(bind.getMonitorId())
+                        .bindType(defaultText(bind.getBindType(), SOURCE_MANUAL))
+                        .bindSource(defaultText(bind.getBindSource(), SOURCE_MANUAL))
+                        .status(defaultText(bind.getStatus(), BIND_ACTIVE))
+                        .score(bind.getScore() == null ? 100 : bind.getScore())
+                        .matchContext(bind.getMatchContext())
+                        .build());
+            }
+        }
+        entityMonitorBindWriteModelService.replaceMonitorBinds(entityId, rows);
+    }
+
+    public List<Monitor> findEntityMonitors(Long entityId) {
+        List<EntityMonitorBind> binds = entityMonitorBindQueryService.findMonitorBinds(entityId);
+        if (CollectionUtils.isEmpty(binds)) {
+            return Collections.emptyList();
+        }
+        Set<Long> monitorIds = binds.stream()
+                .map(EntityMonitorBind::getMonitorId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+        if (monitorIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<Long, Monitor> monitorMap = entityMonitorQueryService.findMonitorsByIds(monitorIds).stream()
+                .collect(Collectors.toMap(Monitor::getId, item -> item, (left, right) -> left));
+        List<Monitor> monitors = new ArrayList<>();
+        for (Long monitorId : monitorIds) {
+            Monitor monitor = monitorMap.get(monitorId);
+            if (monitor != null) {
+                monitors.add(monitor);
+            }
+        }
+        return monitors;
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindWriteModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindWriteModelService.java
new file mode 100644
index 0000000000..ed122307cd
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorBindWriteModelService.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.hertzbeat.common.entity.manager.EntityMonitorBind;
+import org.apache.hertzbeat.manager.dao.EntityMonitorBindDao;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Persists entity monitor-bind rows for the monitor-bind domain boundary.
+ */
+@Service
+public class EntityMonitorBindWriteModelService {
+
+    private final EntityMonitorBindDao entityMonitorBindDao;
+
+    public EntityMonitorBindWriteModelService(EntityMonitorBindDao entityMonitorBindDao) {
+        this.entityMonitorBindDao = entityMonitorBindDao;
+    }
+
+    public void deleteMonitorBinds(Long entityId) {
+        entityMonitorBindDao.deleteAllByEntityId(entityId);
+        entityMonitorBindDao.flush();
+    }
+
+    public void deleteMonitorBindsByMonitorIds(Set<Long> monitorIds) {
+        if (CollectionUtils.isEmpty(monitorIds)) {
+            return;
+        }
+        entityMonitorBindDao.deleteAllByMonitorIdIn(monitorIds);
+    }
+
+    public void replaceMonitorBinds(Long entityId, List<EntityMonitorBind> rows) {
+        entityMonitorBindDao.deleteAllByEntityId(entityId);
+        entityMonitorBindDao.flush();
+        if (!CollectionUtils.isEmpty(rows)) {
+            entityMonitorBindDao.saveAll(rows);
+        }
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorEvidenceReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorEvidenceReadModelService.java
new file mode 100644
index 0000000000..87e06569f6
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorEvidenceReadModelService.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.observability.dto.entity.MonitorInfo;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Shapes bound monitor evidence for entity read APIs.
+ */
+@Service
+public class EntityMonitorEvidenceReadModelService {
+
+    public Page<MonitorInfo> buildEntityMonitorPage(List<Monitor> monitors, Byte status, String app,
+                                                    int pageIndex, int pageSize) {
+        PageRequest pageRequest = normalizePageRequest(pageIndex, pageSize);
+        if (CollectionUtils.isEmpty(monitors)) {
+            return new PageImpl<>(Collections.emptyList(), pageRequest, 0);
+        }
+        List<MonitorInfo> filteredMonitors = monitors.stream()
+                .filter(Objects::nonNull)
+                .filter(monitor -> status == null || monitor.getStatus() == status)
+                .filter(monitor -> !StringUtils.hasText(app) || app.trim().equalsIgnoreCase(monitor.getApp()))
+                .sorted(Comparator
+                        .comparingInt((Monitor monitor) -> monitorStatusPriority(monitor.getStatus())).reversed()
+                        .thenComparing(Monitor::getGmtUpdate, Comparator.nullsLast(Comparator.reverseOrder()))
+                        .thenComparing(Monitor::getId, Comparator.nullsLast(Comparator.reverseOrder())))
+                .map(MonitorInfo::fromEntity)
+                .toList();
+        return slicePage(filteredMonitors, pageRequest);
+    }
+
+    private int monitorStatusPriority(Byte status) {
+        if (Objects.equals(status, CommonConstants.MONITOR_DOWN_CODE)) {
+            return 3;
+        }
+        if (Objects.equals(status, CommonConstants.MONITOR_UP_CODE)) {
+            return 2;
+        }
+        if (Objects.equals(status, CommonConstants.MONITOR_PAUSED_CODE)) {
+            return 1;
+        }
+        return 0;
+    }
+
+    private PageRequest normalizePageRequest(int pageIndex, int pageSize) {
+        int safePageIndex = Math.max(pageIndex, 0);
+        int safePageSize = pageSize <= 0 ? 10 : Math.min(pageSize, 100);
+        return PageRequest.of(safePageIndex, safePageSize, Sort.unsorted());
+    }
+
+    private <T> Page<T> slicePage(List<T> items, PageRequest pageRequest) {
+        if (CollectionUtils.isEmpty(items)) {
+            return new PageImpl<>(Collections.emptyList(), pageRequest, 0);
+        }
+        int start = Math.min((int) pageRequest.getOffset(), items.size());
+        int end = Math.min(start + pageRequest.getPageSize(), items.size());
+        return new PageImpl<>(items.subList(start, end), pageRequest, items.size());
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorQueryService.java
new file mode 100644
index 0000000000..fe8cd185c1
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMonitorQueryService.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.manager.dao.MonitorDao;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Anti-corruption query boundary for old monitor records used by entity workflows.
+ */
+@Service
+public class EntityMonitorQueryService {
+
+    private final MonitorDao monitorDao;
+
+    public EntityMonitorQueryService(MonitorDao monitorDao) {
+        this.monitorDao = monitorDao;
+    }
+
+    public Optional<Monitor> findMonitor(long monitorId) {
+        return monitorDao.findById(monitorId);
+    }
+
+    public boolean monitorExists(Long monitorId) {
+        return monitorId != null && monitorDao.existsById(monitorId);
+    }
+
+    public List<Monitor> findMonitorsByIds(Collection<Long> monitorIds) {
+        if (CollectionUtils.isEmpty(monitorIds)) {
+            return Collections.emptyList();
+        }
+        Set<Long> acceptedMonitorIds = monitorIds.stream()
+                .filter(Objects::nonNull)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+        if (acceptedMonitorIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return monitorDao.findMonitorsByIdIn(acceptedMonitorIds);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMutationWorkflowService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMutationWorkflowService.java
new file mode 100644
index 0000000000..c0015577f4
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityMutationWorkflowService.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDefinitionRequest;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Coordinates entity write workflows while dedicated components own each write stage.
+ */
+@Service
+public class EntityMutationWorkflowService {
+
+    private static final String SOURCE_MANUAL = "manual";
+    private static final String ACTIVITY_TYPE_DEFINITION_IMPORT = "definition_import";
+    private static final String ACTIVITY_TYPE_DEFINITION_UPDATE = "definition_update";
+
+    private final EntityCoreWriteModelService entityCoreWriteModelService;
+    private final EntityIdentityWriteModelService entityIdentityWriteModelService;
+    private final EntityMonitorBindService entityMonitorBindService;
+    private final EntityRelationService entityRelationService;
+    private final EntityStatusRefreshService entityStatusRefreshService;
+    private final EntityActivityWriteModelService entityActivityWriteModelService;
+    private final EntityDefinitionDraftService entityDefinitionDraftService;
+    private final EntityValidationService entityValidationService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityMutationWorkflowService(EntityCoreWriteModelService entityCoreWriteModelService,
+                                         EntityIdentityWriteModelService entityIdentityWriteModelService,
+                                         EntityMonitorBindService entityMonitorBindService,
+                                         EntityRelationService entityRelationService,
+                                         EntityStatusRefreshService entityStatusRefreshService,
+                                         EntityActivityWriteModelService entityActivityWriteModelService,
+                                         EntityDefinitionDraftService entityDefinitionDraftService,
+                                         EntityValidationService entityValidationService,
+                                         EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityCoreWriteModelService = entityCoreWriteModelService;
+        this.entityIdentityWriteModelService = entityIdentityWriteModelService;
+        this.entityMonitorBindService = entityMonitorBindService;
+        this.entityRelationService = entityRelationService;
+        this.entityStatusRefreshService = entityStatusRefreshService;
+        this.entityActivityWriteModelService = entityActivityWriteModelService;
+        this.entityDefinitionDraftService = entityDefinitionDraftService;
+        this.entityValidationService = entityValidationService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public long addEntity(EntityDto entityDto) {
+        return addEntity(entityDto, true);
+    }
+
+    public void modifyEntity(EntityDto entityDto) {
+        modifyEntity(entityDto, true);
+    }
+
+    public long addEntityByDefinition(EntityDefinitionRequest definitionRequest) {
+        EntityDto entityDto = entityDefinitionDraftService.parseEntityDefinition(definitionRequest, null);
+        entityValidationService.validate(entityDto, false);
+        long entityId = addEntity(entityDto, false);
+        entityActivityWriteModelService.recordDefinitionActivity(
+                entityId,
+                ACTIVITY_TYPE_DEFINITION_IMPORT,
+                definitionRequest == null ? null : definitionRequest.getFormat(),
+                entityDto.getEntity());
+        return entityId;
+    }
+
+    public List<Long> addEntitiesByDefinitionBundle(EntityDefinitionRequest definitionRequest) {
+        List<EntityDto> entityDtos = entityDefinitionDraftService.parseEntityDefinitionBundle(definitionRequest);
+        if (CollectionUtils.isEmpty(entityDtos)) {
+            return Collections.emptyList();
+        }
+        if (entityDtos.size() == 1) {
+            EntityDto entityDto = entityDtos.getFirst();
+            entityValidationService.validate(entityDto, false);
+            return List.of(addEntity(entityDto));
+        }
+        for (EntityDto entityDto : entityDtos) {
+            entityValidationService.validate(entityDto, false);
+        }
+        List<ObserveEntity> entities = entityCoreWriteModelService.createEntities(
+                entityDtos.stream().map(EntityDto::getEntity).toList(), SOURCE_MANUAL);
+        for (int index = 0; index < entities.size(); index++) {
+            ObserveEntity entity = entities.get(index);
+            EntityDto entityDto = entityDtos.get(index);
+            entityIdentityWriteModelService.replaceIdentities(entity, entityDto.getIdentities());
+            entityMonitorBindService.replaceMonitorBinds(entity.getId(), entityDto.getMonitorBinds());
+        }
+        for (int index = 0; index < entities.size(); index++) {
+            ObserveEntity entity = entities.get(index);
+            EntityDto entityDto = entityDtos.get(index);
+            entityRelationService.replaceRelations(entity.getId(), entityDto.getRelations());
+            entityStatusRefreshService.refreshEntityStatus(entity);
+            entityActivityWriteModelService.recordDefinitionActivity(
+                    entity.getId(),
+                    ACTIVITY_TYPE_DEFINITION_IMPORT,
+                    definitionRequest == null ? null : definitionRequest.getFormat(),
+                    entity);
+        }
+        return entities.stream().map(ObserveEntity::getId).toList();
+    }
+
+    public void modifyEntityByDefinition(long entityId, EntityDefinitionRequest definitionRequest) {
+        entityWorkspaceAccessService.requireAccessibleEntityForBoundWorkspace(entityId);
+        try {
+            EntityDto entityDto = entityDefinitionDraftService.parseEntityDefinition(definitionRequest, entityId);
+            entityValidationService.validate(entityDto, true);
+            modifyEntity(entityDto, false);
+            entityActivityWriteModelService.recordDefinitionActivity(
+                    entityId,
+                    ACTIVITY_TYPE_DEFINITION_UPDATE,
+                    definitionRequest == null ? null : definitionRequest.getFormat(),
+                    entityDto.getEntity());
+        } catch (RuntimeException ex) {
+            entityActivityWriteModelService.recordDefinitionActivityFailure(
+                    entityId,
+                    ACTIVITY_TYPE_DEFINITION_UPDATE,
+                    definitionRequest == null ? null : definitionRequest.getFormat(),
+                    ex);
+            throw ex;
+        }
+    }
+
+    private long addEntity(EntityDto entityDto, boolean recordLifecycleActivity) {
+        ObserveEntity input = entityDto.getEntity();
+        ObserveEntity entity = entityCoreWriteModelService.createEntity(input, SOURCE_MANUAL);
+        entityIdentityWriteModelService.replaceIdentities(entity, entityDto.getIdentities());
+        entityMonitorBindService.replaceMonitorBinds(entity.getId(), entityDto.getMonitorBinds());
+        entityRelationService.replaceRelations(entity.getId(), entityDto.getRelations());
+        entityStatusRefreshService.refreshEntityStatus(entity);
+        if (recordLifecycleActivity) {
+            entityActivityWriteModelService.recordEntityLifecycleActivity(
+                    entity.getId(),
+                    entityActivityWriteModelService.resolveCreateLifecycleActivityType(entityDto),
+                    entity);
+        }
+        return entity.getId();
+    }
+
+    private void modifyEntity(EntityDto entityDto, boolean recordLifecycleActivity) {
+        ObserveEntity update = entityDto.getEntity();
+        ObserveEntity entity = entityWorkspaceAccessService.requireAccessibleEntityForMutation(update.getId());
+        String lifecycleActivityType = recordLifecycleActivity
+                ? entityActivityWriteModelService.resolveModifyLifecycleActivityType(
+                        entity, update, entityDto.getMonitorBinds())
+                : null;
+        entityCoreWriteModelService.saveEntityCore(entity, update, entity.getSource());
+        entityIdentityWriteModelService.replaceIdentities(entity, entityDto.getIdentities());
+        entityMonitorBindService.replaceMonitorBinds(entity.getId(), entityDto.getMonitorBinds());
+        entityRelationService.replaceRelations(entity.getId(), entityDto.getRelations());
+        entityStatusRefreshService.refreshEntityStatus(entity);
+        if (recordLifecycleActivity && lifecycleActivityType != null) {
+            entityActivityWriteModelService.recordEntityLifecycleActivity(
+                    entity.getId(), lifecycleActivityType, entity);
+        }
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlReadModelService.java
new file mode 100644
index 0000000000..58c0a1e929
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlReadModelService.java
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.alerter.AlertInhibit;
+import org.apache.hertzbeat.common.entity.alerter.AlertSilence;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDetailDto;
+import org.apache.hertzbeat.manager.pojo.dto.EntityDto;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Builds entity noise-control evidence from real alert, monitor, and rule labels.
+ */
+@Service
+public class EntityNoiseControlReadModelService {
+
+    private static final int NOISE_CONTROL_PREVIEW_LIMIT = 3;
+
+    private static final List<String> ALERT_WORKSPACE_LABEL_KEYS = List.of(
+            "hertzbeat.workspace_id",
+            AuthTokenScopes.CLAIM_WORKSPACE_ID,
+            "workspace.id"
+    );
+
+    private final EntityNoiseControlRuleQueryService entityNoiseControlRuleQueryService;
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+
+    public EntityNoiseControlReadModelService(EntityNoiseControlRuleQueryService entityNoiseControlRuleQueryService,
+                                              EntityWorkspaceAccessService entityWorkspaceAccessService) {
+        this.entityNoiseControlRuleQueryService = entityNoiseControlRuleQueryService;
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+    }
+
+    public EntityDetailDto.EntityNoiseControlSummaryInfo buildNoiseControlSummary(EntityDto entityDto,
+                                                                                  List<Monitor> monitors,
+                                                                                  List<SingleAlert> activeAlerts) {
+        return buildNoiseControlSummary(
+                entityDto, monitors, activeAlerts, entityWorkspaceAccessService.currentRequestWorkspaceId());
+    }
+
+    public EntityDetailDto.EntityNoiseControlSummaryInfo buildNoiseControlSummary(EntityDto entityDto,
+                                                                                  List<Monitor> monitors,
+                                                                                  List<SingleAlert> activeAlerts,
+                                                                                  String requestWorkspaceId) {
+        List<Map<String, String>> candidateLabels = buildNoiseControlCandidateLabels(
+                entityDto, monitors, activeAlerts, requestWorkspaceId);
+        List<EntityDetailDto.EntityNoiseControlRuleInfo> matchedSilences =
+                entityNoiseControlRuleQueryService.findEnabledSilences(requestWorkspaceId).stream()
+                .map(rule -> matchSilenceRule(rule, candidateLabels))
+                .filter(Objects::nonNull)
+                .sorted(Comparator.comparing(EntityDetailDto.EntityNoiseControlRuleInfo::getUpdatedAt,
+                        Comparator.nullsLast(Comparator.reverseOrder())))
+                .toList();
+        List<EntityDetailDto.EntityNoiseControlRuleInfo> matchedInhibits =
+                entityNoiseControlRuleQueryService.findEnabledInhibits(requestWorkspaceId).stream()
+                .map(rule -> matchInhibitRule(rule, candidateLabels))
+                .filter(Objects::nonNull)
+                .sorted(Comparator.comparing(EntityDetailDto.EntityNoiseControlRuleInfo::getUpdatedAt,
+                        Comparator.nullsLast(Comparator.reverseOrder())))
+                .toList();
+        boolean possibleAlertSuppression = CollectionUtils.isEmpty(activeAlerts)
+                && (!matchedSilences.isEmpty() || !matchedInhibits.isEmpty());
+        return new EntityDetailDto.EntityNoiseControlSummaryInfo(
+                matchedSilences.size(),
+                matchedInhibits.size(),
+                matchedSilences.stream().limit(NOISE_CONTROL_PREVIEW_LIMIT).toList(),
+                matchedInhibits.stream().limit(NOISE_CONTROL_PREVIEW_LIMIT).toList(),
+                possibleAlertSuppression
+        );
+    }
+
+    private List<Map<String, String>> buildNoiseControlCandidateLabels(EntityDto entityDto,
+                                                                       List<Monitor> monitors,
+                                                                       List<SingleAlert> activeAlerts,
+                                                                       String requestWorkspaceId) {
+        List<Map<String, String>> candidates = new ArrayList<>();
+        if (!CollectionUtils.isEmpty(activeAlerts)) {
+            activeAlerts.stream()
+                    .map(SingleAlert::getLabels)
+                    .map(this::normalizeNoiseControlLabels)
+                    .filter(labels -> !labels.isEmpty())
+                    .forEach(candidates::add);
+        }
+        if (!candidates.isEmpty()) {
+            return candidates;
+        }
+        Map<String, String> entityLabels = new LinkedHashMap<>();
+        if (entityDto != null && entityDto.getEntity() != null) {
+            String entityName = trimToNull(entityDto.getEntity().getName());
+            if (entityName != null) {
+                entityLabels.put("service.name", entityName);
+            }
+            String environment = trimToNull(entityDto.getEntity().getEnvironment());
+            if (environment != null) {
+                entityLabels.put("env", environment);
+            }
+            addNoiseControlWorkspaceLabels(entityLabels, defaultText(entityDto.getEntity().getWorkspaceId(), requestWorkspaceId));
+        }
+        if (!CollectionUtils.isEmpty(entityDto == null ? null : entityDto.getIdentities())) {
+            entityDto.getIdentities().forEach(identity -> {
+                String key = trimToNull(identity.getIdentityKey());
+                String value = trimToNull(identity.getIdentityValue());
+                if (key != null && value != null) {
+                    entityLabels.put(key, value);
+                }
+            });
+        }
+        Map<String, String> normalizedEntityLabels = normalizeNoiseControlLabels(entityLabels);
+        if (!normalizedEntityLabels.isEmpty()) {
+            candidates.add(normalizedEntityLabels);
+        }
+        if (!CollectionUtils.isEmpty(monitors)) {
+            monitors.stream()
+                    .map(monitor -> buildMonitorNoiseControlLabels(monitor, requestWorkspaceId))
+                    .filter(labels -> !labels.isEmpty())
+                    .forEach(candidates::add);
+        }
+        return candidates;
+    }
+
+    private Map<String, String> buildMonitorNoiseControlLabels(Monitor monitor, String requestWorkspaceId) {
+        Map<String, String> labels = new LinkedHashMap<>();
+        if (monitor == null) {
+            return labels;
+        }
+        if (!CollectionUtils.isEmpty(monitor.getLabels())) {
+            labels.putAll(monitor.getLabels());
+        }
+        String instance = trimToNull(monitor.getInstance());
+        if (instance != null) {
+            labels.put(CommonConstants.LABEL_INSTANCE, instance);
+            labels.put("service.name", instance);
+        }
+        String name = trimToNull(monitor.getName());
+        if (name != null) {
+            labels.put(CommonConstants.LABEL_INSTANCE_NAME, name);
+        }
+        String app = trimToNull(monitor.getApp());
+        if (app != null) {
+            labels.put("job", app);
+        }
+        addNoiseControlWorkspaceLabels(labels, requestWorkspaceId);
+        return normalizeNoiseControlLabels(labels);
+    }
+
+    private Map<String, String> normalizeNoiseControlLabels(Map<String, String> labels) {
+        if (CollectionUtils.isEmpty(labels)) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> normalizedLabels = new LinkedHashMap<>();
+        labels.forEach((key, value) -> {
+            String normalizedKey = trimToNull(key);
+            String normalizedValue = trimToNull(value);
+            if (normalizedKey != null && normalizedValue != null) {
+                normalizedLabels.putIfAbsent(normalizedKey, normalizedValue);
+            }
+        });
+        return normalizedLabels;
+    }
+
+    private EntityDetailDto.EntityNoiseControlRuleInfo matchSilenceRule(AlertSilence silence,
+                                                                        List<Map<String, String>> candidateLabels) {
+        if (silence == null) {
+            return null;
+        }
+        if (Boolean.TRUE.equals(silence.isMatchAll())) {
+            return new EntityDetailDto.EntityNoiseControlRuleInfo(
+                    silence.getId(),
+                    silence.getName(),
+                    "silence",
+                    true,
+                    Collections.emptyList(),
+                    toEpochMillis(silence.getGmtUpdate() == null ? silence.getGmtCreate() : silence.getGmtUpdate())
+            );
+        }
+        Map<String, String> labels = normalizeNoiseControlLabels(silence.getLabels());
+        if (labels.isEmpty()) {
+            return null;
+        }
+        return candidateLabels.stream()
+                .filter(candidate -> labels.entrySet().stream()
+                        .allMatch(entry -> Objects.equals(candidate.get(entry.getKey()), entry.getValue())))
+                .findFirst()
+                .map(candidate -> new EntityDetailDto.EntityNoiseControlRuleInfo(
+                        silence.getId(),
+                        silence.getName(),
+                        "silence",
+                        false,
+                        new ArrayList<>(labels.keySet()),
+                        toEpochMillis(silence.getGmtUpdate() == null ? silence.getGmtCreate() : silence.getGmtUpdate())
+                ))
+                .orElse(null);
+    }
+
+    private EntityDetailDto.EntityNoiseControlRuleInfo matchInhibitRule(AlertInhibit inhibit,
+                                                                        List<Map<String, String>> candidateLabels) {
+        if (inhibit == null) {
+            return null;
+        }
+        Map<String, String> labels = normalizeNoiseControlLabels(inhibit.getTargetLabels());
+        if (labels.isEmpty()) {
+            return null;
+        }
+        return candidateLabels.stream()
+                .filter(candidate -> labels.entrySet().stream()
+                        .allMatch(entry -> Objects.equals(candidate.get(entry.getKey()), entry.getValue())))
+                .findFirst()
+                .map(candidate -> new EntityDetailDto.EntityNoiseControlRuleInfo(
+                        inhibit.getId(),
+                        inhibit.getName(),
+                        "inhibit",
+                        false,
+                        new ArrayList<>(labels.keySet()),
+                        toEpochMillis(inhibit.getGmtUpdate() == null ? inhibit.getGmtCreate() : inhibit.getGmtUpdate())
+                ))
+                .orElse(null);
+    }
+
+    private void addNoiseControlWorkspaceLabels(Map<String, String> labels, String workspaceId) {
+        String normalizedWorkspaceId = trimToNull(workspaceId);
+        if (labels == null || normalizedWorkspaceId == null) {
+            return;
+        }
+        for (String key : ALERT_WORKSPACE_LABEL_KEYS) {
+            labels.putIfAbsent(key, AuthTokenScopes.normalizeWorkspaceId(normalizedWorkspaceId));
+        }
+    }
+
+    private String defaultText(String value, String fallback) {
+        String normalized = trimToNull(value);
+        return normalized == null ? trimToNull(fallback) : normalized;
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+
+    private Long toEpochMillis(LocalDateTime value) {
+        if (value == null) {
+            return null;
+        }
+        return value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQueryService.java
new file mode 100644
index 0000000000..eb1059d710
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityNoiseControlRuleQueryService.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hertzbeat.alert.dao.AlertInhibitDao;
+import org.apache.hertzbeat.alert.dao.AlertSilenceDao;
+import org.apache.hertzbeat.common.entity.alerter.AlertInhibit;
+import org.apache.hertzbeat.common.entity.alerter.AlertSilence;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Query boundary for alert silence and inhibit rules used as entity noise-control evidence.
+ */
+@Service
+public class EntityNoiseControlRuleQueryService {
+
+    private static final Set<String> ALERT_WORKSPACE_LABEL_KEYS = Set.of(
+            "hertzbeat.workspace_id",
+            AuthTokenScopes.CLAIM_WORKSPACE_ID,
+            "workspace.id"
+    );
+
+    private final AlertSilenceDao alertSilenceDao;
+    private final AlertInhibitDao alertInhibitDao;
+
+    public EntityNoiseControlRuleQueryService(AlertSilenceDao alertSilenceDao, AlertInhibitDao alertInhibitDao) {
+        this.alertSilenceDao = alertSilenceDao;
+        this.alertInhibitDao = alertInhibitDao;
+    }
+
+    public List<AlertSilence> findEnabledSilences(String requestWorkspaceId) {
+        return alertSilenceDao.findAll().stream()
+                .filter(rule -> Boolean.TRUE.equals(rule.isEnable()))
+                .filter(rule -> matchesSilenceRuleRequestWorkspace(rule, requestWorkspaceId))
+                .toList();
+    }
+
+    public List<AlertInhibit> findEnabledInhibits(String requestWorkspaceId) {
+        return alertInhibitDao.findAll().stream()
+                .filter(rule -> Boolean.TRUE.equals(rule.getEnable()))
+                .filter(rule -> matchesInhibitRuleRequestWorkspace(rule, requestWorkspaceId))
+                .toList();
+    }
+
+    private boolean matchesSilenceRuleRequestWorkspace(AlertSilence silence, String requestWorkspaceId) {
+        return matchesNoiseControlRuleRequestWorkspace(silence == null ? null : silence.getLabels(), requestWorkspaceId);
+    }
+
+    private boolean matchesInhibitRuleRequestWorkspace(AlertInhibit inhibit, String requestWorkspaceId) {
+        if (inhibit == null) {
+            return false;
+        }
+        Map<String, String> labels = new LinkedHashMap<>();
+        if (!CollectionUtils.isEmpty(inhibit.getSourceLabels())) {
+            labels.putAll(inhibit.getSourceLabels());
+        }
+        if (!CollectionUtils.isEmpty(inhibit.getTargetLabels())) {
+            labels.putAll(inhibit.getTargetLabels());
+        }
+        return matchesNoiseControlRuleRequestWorkspace(labels, requestWorkspaceId);
+    }
+
+    private boolean matchesNoiseControlRuleRequestWorkspace(Map<String, String> labels, String requestWorkspaceId) {
+        String normalizedRequestWorkspaceId = trimToNull(requestWorkspaceId);
+        if (normalizedRequestWorkspaceId == null) {
+            return true;
+        }
+        normalizedRequestWorkspaceId = AuthTokenScopes.normalizeWorkspaceId(normalizedRequestWorkspaceId);
+        Map<String, String> normalizedLabels = normalizeNoiseControlLabels(labels);
+        boolean hasWorkspaceLabel = false;
+        for (String key : ALERT_WORKSPACE_LABEL_KEYS) {
+            String workspaceId = trimToNull(normalizedLabels.get(key));
+            if (workspaceId == null) {
+                continue;
+            }
+            hasWorkspaceLabel = true;
+            if (!normalizedRequestWorkspaceId.equals(AuthTokenScopes.normalizeWorkspaceId(workspaceId))) {
+                return false;
+            }
+        }
+        return hasWorkspaceLabel || AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(normalizedRequestWorkspaceId);
+    }
+
+    private Map<String, String> normalizeNoiseControlLabels(Map<String, String> labels) {
+        if (CollectionUtils.isEmpty(labels)) {
+            return Map.of();
+        }
+        Map<String, String> normalizedLabels = new LinkedHashMap<>();
+        labels.forEach((key, value) -> {
+            String normalizedKey = trimToNull(key);
+            String normalizedValue = trimToNull(value);
+            if (normalizedKey != null && normalizedValue != null) {
+                normalizedLabels.putIfAbsent(normalizedKey, normalizedValue);
+            }
+        });
+        return normalizedLabels;
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationQueryService.java
new file mode 100644
index 0000000000..1ff5f42eac
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationQueryService.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.apache.hertzbeat.manager.dao.EntityRelationDao;
+import org.springframework.stereotype.Service;
+
+/**
+ * Owns persisted relation evidence lookup for entity read-model boundaries.
+ */
+@Service
+public class EntityRelationQueryService {
+
+    private final EntityRelationDao entityRelationDao;
+
+    public EntityRelationQueryService(EntityRelationDao entityRelationDao) {
+        this.entityRelationDao = entityRelationDao;
+    }
+
+    public List<EntityRelation> findEntityRelations(Long entityId) {
+        return entityRelationDao.findBySourceEntityIdOrTargetEntityId(entityId, entityId);
+    }
+
+    public long countEntityRelations(Long entityId) {
+        return entityRelationDao.countBySourceEntityIdOrTargetEntityId(entityId, entityId);
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationService.java
new file mode 100644
index 0000000000..1df37194df
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationService.java
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Maintains entity relations and resolves catalog references behind the workspace boundary.
+ */
+@Service
+public class EntityRelationService {
+
+    private static final String SOURCE_MANUAL = "manual";
+    private static final String RELATION_CONFIRMED = "confirmed";
+    private static final String TYPE_DATABASE = "database";
+    private static final String TYPE_API = "api";
+    private static final String KIND_DATASTORE = "datastore";
+    private static final String KIND_API = "api";
+    private static final String LEGACY_ENTITY_DEFINITION_KIND = "Entity";
+    private static final String DEFAULT_NAMESPACE = "default";
+
+    private final EntityWorkspaceAccessService entityWorkspaceAccessService;
+    private final EntityRelationQueryService entityRelationQueryService;
+    private final EntityRelationWriteModelService entityRelationWriteModelService;
+
+    public EntityRelationService(EntityWorkspaceAccessService entityWorkspaceAccessService,
+                                 EntityRelationQueryService entityRelationQueryService,
+                                 EntityRelationWriteModelService entityRelationWriteModelService) {
+        this.entityWorkspaceAccessService = entityWorkspaceAccessService;
+        this.entityRelationQueryService = entityRelationQueryService;
+        this.entityRelationWriteModelService = entityRelationWriteModelService;
+    }
+
+    public void replaceRelations(Long entityId, List<EntityRelation> relations) {
+        replaceRelationsForRequestWorkspace(entityId, relations);
+    }
+
+    public void replaceRelationsForRequestWorkspace(Long entityId, List<EntityRelation> relations) {
+        replaceRelationsInternal(entityId, relations, null, true);
+    }
+
+    public void deleteRelationsForEntity(Long entityId) {
+        entityRelationWriteModelService.deleteIncomingAndOutgoingRelations(entityId);
+    }
+
+    public List<EntityRelation> findEntityRelations(Long entityId) {
+        return entityRelationQueryService.findEntityRelations(entityId);
+    }
+
+    public long countEntityRelations(Long entityId) {
+        return entityRelationQueryService.countEntityRelations(entityId);
+    }
+
+    public void replaceRelations(Long entityId, List<EntityRelation> relations, String requestWorkspaceId) {
+        replaceRelationsInternal(entityId, relations, requestWorkspaceId, false);
+    }
+
+    private void replaceRelationsInternal(Long entityId,
+                                          List<EntityRelation> relations,
+                                          String requestWorkspaceId,
+                                          boolean requestWorkspaceScoped) {
+        if (CollectionUtils.isEmpty(relations)) {
+            entityRelationWriteModelService.replaceSourceRelations(entityId, List.of());
+            return;
+        }
+        List<EntityRelation> rows = new ArrayList<>();
+        Set<String> dedupeKeys = new LinkedHashSet<>();
+        for (EntityRelation relation : relations) {
+            Long sourceEntityId = relation.getSourceEntityId() == null ? entityId : relation.getSourceEntityId();
+            String targetRef = trimToNull(relation.getTargetRef());
+            Long targetEntityId = relation.getTargetEntityId();
+            if (targetEntityId == null && StringUtils.hasText(targetRef)) {
+                targetEntityId = requestWorkspaceScoped
+                        ? resolveEntityReferenceForRequestWorkspace(targetRef)
+                        : resolveEntityReference(targetRef, requestWorkspaceId);
+            }
+            if (!Objects.equals(sourceEntityId, entityId)) {
+                continue;
+            }
+            if (targetEntityId == null && !StringUtils.hasText(targetRef)) {
+                continue;
+            }
+            if (targetEntityId != null && Objects.equals(sourceEntityId, targetEntityId)) {
+                continue;
+            }
+            String normalizedTargetRef = defaultText(targetRef, buildEntityReference(targetEntityId));
+            String dedupeKey = String.join("|",
+                    String.valueOf(sourceEntityId),
+                    defaultText(relation.getRelationType(), "depends_on"),
+                    targetEntityId == null ? "" : String.valueOf(targetEntityId),
+                    defaultText(normalizedTargetRef, ""));
+            if (!dedupeKeys.add(dedupeKey)) {
+                continue;
+            }
+            rows.add(EntityRelation.builder()
+                    .sourceEntityId(sourceEntityId)
+                    .targetEntityId(targetEntityId)
+                    .targetRef(normalizedTargetRef)
+                    .relationType(defaultText(relation.getRelationType(), "depends_on"))
+                    .relationSource(defaultText(relation.getRelationSource(), SOURCE_MANUAL))
+                    .status(defaultText(relation.getStatus(), RELATION_CONFIRMED))
+                    .score(relation.getScore() == null ? 100 : relation.getScore())
+                    .description(relation.getDescription())
+                    .attributes(relation.getAttributes())
+                    .build());
+        }
+        entityRelationWriteModelService.replaceSourceRelations(entityId, rows);
+    }
+
+    public Long resolveEntityReferenceForRequestWorkspace(String targetRef) {
+        if (!StringUtils.hasText(targetRef)) {
+            return null;
+        }
+        Long directId = parseLong(targetRef);
+        if (directId != null) {
+            return resolveDirectEntityReferenceForRequestWorkspace(directId);
+        }
+        String normalized = targetRef.trim();
+        int typeSeparator = normalized.indexOf(':');
+        if (typeSeparator < 0 || typeSeparator == normalized.length() - 1) {
+            return null;
+        }
+        String type = normalizeEntityTypeFromKind(normalized.substring(0, typeSeparator));
+        String remainder = normalized.substring(typeSeparator + 1).trim();
+        if (!StringUtils.hasText(type) || !StringUtils.hasText(remainder)) {
+            return null;
+        }
+        int namespaceSeparator = remainder.indexOf('/');
+        if (namespaceSeparator > 0 && namespaceSeparator < remainder.length() - 1) {
+            String namespace = remainder.substring(0, namespaceSeparator).trim();
+            String name = remainder.substring(namespaceSeparator + 1).trim();
+            return entityWorkspaceAccessService
+                    .findAccessibleEntityByReferenceForRequestWorkspace(type, namespace, name)
+                    .map(ObserveEntity::getId)
+                    .orElseGet(() -> entityWorkspaceAccessService
+                            .findAccessibleEntityByReferenceForRequestWorkspace(type, name)
+                            .map(ObserveEntity::getId)
+                            .orElse(null));
+        }
+        return entityWorkspaceAccessService.findAccessibleEntityByReferenceForRequestWorkspace(type, remainder)
+                .map(ObserveEntity::getId)
+                .orElse(null);
+    }
+
+    public Long resolveEntityReference(String targetRef, String requestWorkspaceId) {
+        if (!StringUtils.hasText(targetRef)) {
+            return null;
+        }
+        Long directId = parseLong(targetRef);
+        if (directId != null) {
+            return resolveDirectEntityReference(directId, requestWorkspaceId);
+        }
+        String normalized = targetRef.trim();
+        int typeSeparator = normalized.indexOf(':');
+        if (typeSeparator < 0 || typeSeparator == normalized.length() - 1) {
+            return null;
+        }
+        String type = normalizeEntityTypeFromKind(normalized.substring(0, typeSeparator));
+        String remainder = normalized.substring(typeSeparator + 1).trim();
+        if (!StringUtils.hasText(type) || !StringUtils.hasText(remainder)) {
+            return null;
+        }
+        int namespaceSeparator = remainder.indexOf('/');
+        if (namespaceSeparator > 0 && namespaceSeparator < remainder.length() - 1) {
+            String namespace = remainder.substring(0, namespaceSeparator).trim();
+            String name = remainder.substring(namespaceSeparator + 1).trim();
+            return entityWorkspaceAccessService.findAccessibleEntityByReference(requestWorkspaceId, type, namespace, name)
+                    .map(ObserveEntity::getId)
+                    .orElseGet(() -> entityWorkspaceAccessService
+                            .findAccessibleEntityByReference(requestWorkspaceId, type, name)
+                            .map(ObserveEntity::getId)
+                            .orElse(null));
+        }
+        return entityWorkspaceAccessService.findAccessibleEntityByReference(requestWorkspaceId, type, remainder)
+                .map(ObserveEntity::getId)
+                .orElse(null);
+    }
+
+    public Long resolveDirectEntityReferenceForRequestWorkspace(Long entityId) {
+        if (entityId == null) {
+            return null;
+        }
+        return entityWorkspaceAccessService.isEntityAccessibleForRequestWorkspace(entityId) ? entityId : null;
+    }
+
+    public Long resolveDirectEntityReference(Long entityId, String requestWorkspaceId) {
+        if (entityId == null) {
+            return null;
+        }
+        if (!StringUtils.hasText(requestWorkspaceId)) {
+            return entityId;
+        }
+        return entityWorkspaceAccessService.findAccessibleEntityById(entityId, requestWorkspaceId)
+                .map(ObserveEntity::getId)
+                .orElse(null);
+    }
+
+    public String buildEntityReference(Long entityId) {
+        if (entityId == null) {
+            return null;
+        }
+        return entityWorkspaceAccessService.findEntityById(entityId)
+                .map(this::buildEntityReference)
+                .orElse(null);
+    }
+
+    public String buildEntityReference(ObserveEntity entity) {
+        if (entity == null || !StringUtils.hasText(entity.getType()) || !StringUtils.hasText(entity.getName())) {
+            return null;
+        }
+        String namespace = defaultText(entity.getNamespace(), DEFAULT_NAMESPACE);
+        return toDefinitionKind(entity.getType()) + ":" + namespace + "/" + entity.getName();
+    }
+
+    private String normalizeEntityTypeFromKind(String kind) {
+        if (!StringUtils.hasText(kind)) {
+            return null;
+        }
+        if (LEGACY_ENTITY_DEFINITION_KIND.equalsIgnoreCase(kind.trim())) {
+            return null;
+        }
+        return switch (kind.trim().toLowerCase()) {
+            case KIND_DATASTORE -> TYPE_DATABASE;
+            case KIND_API -> TYPE_API;
+            default -> kind.trim().toLowerCase();
+        };
+    }
+
+    private String toDefinitionKind(String entityType) {
+        if (!StringUtils.hasText(entityType)) {
+            return null;
+        }
+        return switch (entityType.trim().toLowerCase()) {
+            case TYPE_DATABASE -> KIND_DATASTORE;
+            case TYPE_API -> KIND_API;
+            default -> entityType.trim().toLowerCase();
+        };
+    }
+
+    private Long parseLong(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        try {
+            return Long.parseLong(value.trim());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    private String defaultText(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationWriteModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationWriteModelService.java
new file mode 100644
index 0000000000..84c37270a7
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRelationWriteModelService.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.apache.hertzbeat.manager.dao.EntityRelationDao;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Persists entity relation rows for the relation domain boundary.
+ */
+@Service
+public class EntityRelationWriteModelService {
+
+    private final EntityRelationDao entityRelationDao;
+
+    public EntityRelationWriteModelService(EntityRelationDao entityRelationDao) {
+        this.entityRelationDao = entityRelationDao;
+    }
+
+    public void deleteIncomingAndOutgoingRelations(Long entityId) {
+        entityRelationDao.deleteAllBySourceEntityIdOrTargetEntityId(entityId, entityId);
+    }
+
+    public void replaceSourceRelations(Long entityId, List<EntityRelation> rows) {
+        entityRelationDao.deleteAllBySourceEntityId(entityId);
+        entityRelationDao.flush();
+        if (!CollectionUtils.isEmpty(rows)) {
+            entityRelationDao.saveAll(rows);
+        }
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java
new file mode 100644
index 0000000000..924e62d8f5
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.List;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
+import org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence;
+import org.apache.hertzbeat.common.observability.dto.evidence.TraceEvidence;
+import org.apache.hertzbeat.common.observability.dto.handoff.EntityResponseHandoffsInfo;
+import org.apache.hertzbeat.common.observability.dto.handoff.EntityResponseHandoffsRequest;
+import org.apache.hertzbeat.common.observability.dto.log.EntityLogSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceQueryHintDto;
+import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceSummaryDto;
+import org.apache.hertzbeat.common.observability.gateway.EntityObservabilityGateway;
+import org.apache.hertzbeat.common.observability.model.ObservedEntityContext;
+import org.springframework.stereotype.Service;
+
+/**
+ * Builds response handoff read models for entity detail without owning signal-specific routing rules.
+ */
+@Service
+public class EntityResponseHandoffReadModelService {
+
+    private final EntityObservabilityGateway entityObservabilityGateway;
+
+    public EntityResponseHandoffReadModelService(EntityObservabilityGateway entityObservabilityGateway) {
+        this.entityObservabilityGateway = entityObservabilityGateway;
+    }
+
+    public EntityResponseHandoffsInfo buildResponseHandoffs(long entityId,
+                                                            ObservedEntityContext entityContext,
+                                                            List<SingleAlert> activeAlerts,
+                                                            List<Monitor> monitors,
+                                                            EntityLogSummaryInfo logSummary,
+                                                            EntityTraceSummaryDto traceSummary,
+                                                            List<MetricEvidence> metricEvidence,
+                                                            List<LogEvidence> logEvidence,
+                                                            List<TraceEvidence> traceEvidence,
+                                                            List<EntityTraceQueryHintDto> traceQueryHints,
+                                                            EntityOpsSummaryInfo opsSummary) {
+        return entityObservabilityGateway.buildEntityResponseHandoffs(new EntityResponseHandoffsRequest(
+                "/entities/" + entityId,
+                entityObservabilityGateway.buildEntityReturnLabel(entityContext),
+                entityContext == null || entityContext.getEntity() == null ? null : entityContext.getEntity().getOwner(),
+                entityContext == null || entityContext.getEntity() == null ? null : entityContext.getEntity().getSystem(),
+                entityContext == null || entityContext.getEntity() == null
+                        ? null : entityContext.getEntity().getEnvironment(),
+                entityContext == null || entityContext.getEntity() == null ? null : entityContext.getEntity().getSource(),
+                entityContext,
+                activeAlerts,
+                monitors,
+                logSummary,
+                traceSummary,
+                metricEvidence,
+                logEvidence,
+                traceEvidence,
+                traceQueryHints,
+                opsSummary != null && opsSummary.isOwnerReady(),
+                opsSummary != null && opsSummary.isRunbookReady(),
+                opsSummary != null && opsSummary.isRelationReady(),
+                opsSummary != null && opsSummary.isTelemetryReady()
+        ));
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRuntimeHealthService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRuntimeHealthService.java
new file mode 100644
index 0000000000..11ac976209
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRuntimeHealthService.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Evaluates and persists runtime health for observed entities from real monitor and alert evidence.
+ */
+@Service
+public class EntityRuntimeHealthService {
+
+    private static final String STATUS_UNKNOWN = "unknown";
+    private static final String STATUS_HEALTHY = "healthy";
+    private static final String STATUS_DEGRADED = "degraded";
+    private static final String STATUS_CRITICAL = "critical";
+    private static final String STATUS_PAUSED = "paused";
+
+    private final EntityRuntimeHealthWriteModelService entityRuntimeHealthWriteModelService;
+
+    public EntityRuntimeHealthService(EntityRuntimeHealthWriteModelService entityRuntimeHealthWriteModelService) {
+        this.entityRuntimeHealthWriteModelService = entityRuntimeHealthWriteModelService;
+    }
+
+    public EntityStatusInfo refreshEntityStatus(ObserveEntity entity, List<Monitor> monitors,
+                                                List<SingleAlert> activeAlerts) {
+        List<Monitor> safeMonitors = CollectionUtils.isEmpty(monitors) ? Collections.emptyList() : monitors;
+        int monitorTotal = safeMonitors.size();
+        int monitorUpCount = (int) safeMonitors.stream()
+                .filter(monitor -> monitor.getStatus() == CommonConstants.MONITOR_UP_CODE)
+                .count();
+        int monitorDownCount = (int) safeMonitors.stream()
+                .filter(monitor -> monitor.getStatus() == CommonConstants.MONITOR_DOWN_CODE)
+                .count();
+        int monitorPausedCount = (int) safeMonitors.stream()
+                .filter(monitor -> monitor.getStatus() == CommonConstants.MONITOR_PAUSED_CODE)
+                .count();
+        int activeAlertCount = CollectionUtils.isEmpty(activeAlerts) ? 0 : activeAlerts.size();
+        String status;
+        String reason;
+        if (activeAlertCount > 0) {
+            status = STATUS_CRITICAL;
+            reason = activeAlertCount + " firing alerts";
+        } else if (monitorDownCount > 0) {
+            status = STATUS_DEGRADED;
+            reason = monitorDownCount + " monitors down";
+        } else if (monitorUpCount > 0) {
+            status = STATUS_HEALTHY;
+            reason = monitorUpCount + " monitors healthy";
+        } else if (monitorTotal > 0 && monitorPausedCount == monitorTotal) {
+            status = STATUS_PAUSED;
+            reason = "all bound monitors paused";
+        } else {
+            status = STATUS_UNKNOWN;
+            reason = "no live evidence bound yet";
+        }
+        if (entity != null && !Objects.equals(entity.getStatus(), status)) {
+            entityRuntimeHealthWriteModelService.persistStatus(entity, status);
+        }
+        return new EntityStatusInfo(
+                status,
+                reason,
+                monitorTotal,
+                monitorUpCount,
+                monitorDownCount,
+                monitorPausedCount,
+                activeAlertCount,
+                LocalDateTime.now()
+        );
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRuntimeHealthWriteModelService.java
similarity index 56%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
copy to hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRuntimeHealthWriteModelService.java
index 87fc1ba19a..a82d9d5875 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityRuntimeHealthWriteModelService.java
@@ -15,19 +15,24 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.dao;
+package org.apache.hertzbeat.manager.service.entity;
... 26597 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 05/50: feat(web-next): restore monitor list parity

Posted by zh...@apache.org.
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 d21cadcbe3f2465457c1077914370eb63182ee4d
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 00:27:52 2026 +0800

    feat(web-next): restore monitor list parity
---
 web-next/app/monitors/[...rest]/page.test.ts       |   27 +-
 web-next/app/monitors/[...rest]/page.tsx           |    8 +-
 web-next/app/monitors/monitor-manage-page.tsx      | 2059 +++++++++++++
 web-next/app/monitors/not-found.tsx                |    3 +-
 web-next/app/monitors/page.test.tsx                | 3094 +++++++++++++++++++-
 web-next/app/monitors/page.tsx                     |  762 +----
 web-next/lib/monitor-manage/app-picker.ts          |   68 +
 web-next/lib/monitor-manage/controller.test.ts     |   45 +
 web-next/lib/monitor-manage/controller.ts          |   18 +
 .../lib/monitor-manage/display-mapping.test.ts     |   21 +-
 web-next/lib/monitor-manage/display-mapping.ts     |   27 +-
 web-next/lib/monitor-manage/navigation.test.ts     |   14 +
 web-next/lib/monitor-manage/navigation.ts          |   11 +-
 web-next/lib/monitor-manage/query-state.test.ts    |   77 +-
 web-next/lib/monitor-manage/query-state.ts         |   35 +-
 web-next/lib/monitor-manage/view-model.test.ts     |   32 +
 web-next/lib/monitor-manage/view-model.ts          |    6 +-
 17 files changed, 5383 insertions(+), 924 deletions(-)

diff --git a/web-next/app/monitors/[...rest]/page.test.ts b/web-next/app/monitors/[...rest]/page.test.ts
index c72e1469cb..78c36c832b 100644
--- a/web-next/app/monitors/[...rest]/page.test.ts
+++ b/web-next/app/monitors/[...rest]/page.test.ts
@@ -14,7 +14,32 @@ describe('monitors catch-all route', () => {
 
     const { default: MonitorUnknownRoutePage } = await import('./page');
 
-    expect(() => MonitorUnknownRoutePage()).toThrow('redirect:/monitors');
+    await expect(MonitorUnknownRoutePage()).rejects.toThrow('redirect:/monitors');
     expect(redirect).toHaveBeenCalledWith('/monitors');
   });
+
+  it('preserves monitor list query context when redirecting unknown nested paths', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: MonitorUnknownRoutePage } = await import('./page');
+
+    await expect(
+      MonitorUnknownRoutePage({
+        searchParams: Promise.resolve({
+          app: 'website',
+          labels: 'team=platform',
+          entityId: '42',
+          returnTo: '/entities/42?returnLabel=Checkout',
+          start: '1700000000000'
+        })
+      })
+    ).rejects.toThrow(
+      'redirect:/monitors?app=website&labels=team%3Dplatform&entityId=42&returnTo=%2Fentities%2F42&start=1700000000000'
+    );
+    expect(redirect).toHaveBeenLastCalledWith(
+      '/monitors?app=website&labels=team%3Dplatform&entityId=42&returnTo=%2Fentities%2F42&start=1700000000000'
+    );
+  });
 });
diff --git a/web-next/app/monitors/[...rest]/page.tsx b/web-next/app/monitors/[...rest]/page.tsx
index 994b91a36d..83bdd67495 100644
--- a/web-next/app/monitors/[...rest]/page.tsx
+++ b/web-next/app/monitors/[...rest]/page.tsx
@@ -1,5 +1,9 @@
 import { redirect } from 'next/navigation';
+import { buildMonitorListCompatRouteUrl, type SearchParamsRecord } from '../../../lib/monitor-manage/navigation';
 
-export default function MonitorUnknownRoutePage() {
-  redirect('/monitors');
+export default async function MonitorUnknownRoutePage(props: {
+  searchParams?: Promise<SearchParamsRecord>;
+} = {}) {
+  const resolvedSearchParams = await props?.searchParams;
+  redirect(buildMonitorListCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/monitors/monitor-manage-page.tsx b/web-next/app/monitors/monitor-manage-page.tsx
new file mode 100644
index 0000000000..4e49086d10
--- /dev/null
+++ b/web-next/app/monitors/monitor-manage-page.tsx
@@ -0,0 +1,2059 @@
+'use client';
+
+import React from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useQueryClient } from '@tanstack/react-query';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { BarChart3, Copy as CopyIcon, FileCode2, FileText, GitBranch as GitBranchIcon, MoreHorizontal, Network, PauseCircle, Pencil, PlayCircle, RefreshCw, Trash2 } from 'lucide-react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { useI18n } from '@/components/providers/i18n-provider';
+import {
+  HzButton,
+  HzButtonLink,
+  HzBatchToolbar,
+  HzCheckbox,
+  HzConfirmDialog,
+  HzDataCellText,
+  HzDataTable,
+  HzEmptyState,
+  HzExplorerFrame,
+  HzExportTypeDialog,
+  HzFileInput,
+  HzIconButton,
+  HzLabelTag,
+  HzInlineFeedback,
+  HzMonitorFilterBar,
+  HzPaginationBar,
+  HzStatusBadge,
+  HzTableRowActionButton,
+  HzTypePickerDialog,
+  type HzExportTypeDialogScope,
+  type HzDataColumn,
+  type HzTemplateCategory
+} from '@hertzbeat/ui';
+import { getCurrentLocale, type ApiClientError } from '@/lib/api-client';
+import { formatTime } from '@/lib/format';
+import { buildMonitorEditCacheKey, loadMonitorEditorDraftFromFacade } from '@/lib/monitor-editor/controller';
+import {
+  importMonitorsFromFacade,
+  loadMonitorListFromFacade,
+  resolveDownloadFilename
+} from '@/lib/monitor-manage/controller';
+import { api } from '@/lib/monitor-api-facade';
+import { queryKeys } from '@/lib/query-keys';
+import {
+  buildMonitorAppPickerGroups,
+  type MonitorAppHierarchyItem,
+  type MonitorAppPickerGroup
+} from '@/lib/monitor-manage/app-picker';
+import { monitorStatusTone, statusBadgeVariant, statusLabel } from '@/lib/monitor-manage/display-mapping';
+import { renderAngularLabelColor } from '@/lib/label-manage/view-model';
+import {
+  buildMonitorDetailHref,
+  buildMonitorEditHref,
+  buildMonitorEntityReturnHref,
+  buildMonitorNewHref,
+  resolveMonitorCheckboxSelection
+} from '@/lib/monitor-manage/navigation';
+import {
+  applyMonitorWorkspaceDefaults,
+  buildMonitorRouteUrl,
+  buildMonitorUrl,
+  hasMonitorEntityContext,
+  type MonitorQueryState
+} from '@/lib/monitor-manage/query-state';
+import {
+  buildMonitorWorkbenchNarrative,
+  shouldFallbackMonitorEntityWorkbench
+} from '@/lib/monitor-manage/view-model';
+import { buildCodeNavigationUrl } from '@/lib/code-navigation';
+import type { Monitor, PageResult } from '@/lib/types';
+import { consumeWorkbenchLoad } from '@/lib/workbench-load-cache';
+
+type MonitorPageData = {
+  list: PageResult<Monitor>;
+  query: MonitorQueryState;
+  entityWorkbenchFallback: boolean;
+};
+
+type MonitorActionFeedback = {
+  tone: Exclude<HzStatusTone, 'neutral'>;
+  title: string;
+  description?: string;
+};
+
+type MonitorRowResponseConfirm = {
+  action: 'enable' | 'pause';
+  monitor: Monitor;
+};
+
+type MonitorBatchResponseConfirm = {
+  action: 'enable' | 'pause';
+  ids: number[];
+};
+
+type MonitorAppPickerMode = 'new' | 'filter';
+
+const MONITOR_MANAGE_SETTLED_CACHE_TTL_MS = 10_000;
+const MONITOR_MANAGE_AUTO_REFRESH_MS = 120_000;
+const MONITOR_MANAGE_DISAPPEARED_GRACE_MS = 5_000;
+const EMPTY_MONITOR_QUERY: MonitorQueryState = {
+  search: '',
+  app: '',
+  labels: '',
+  status: '',
+  pageIndex: '',
+  pageSize: '',
+  entityId: '',
+  entityName: '',
+  returnTo: ''
+};
+
+function buildOptimisticCopyName(sourceName: string | null | undefined, existingNames: Set<string>) {
+  const baseName = sourceName?.trim() || 'monitor';
+  let candidate = `${baseName}_copy`;
+  let index = 2;
+  while (existingNames.has(candidate)) {
+    candidate = `${baseName}_copy_${index}`;
+    index += 1;
+  }
+  return candidate;
+}
+
+function isMonitorUnavailableRefreshError(error: unknown) {
+  const apiError = error as ApiClientError | undefined;
+  return apiError?.status === 404 || apiError?.code === 3;
+}
+
+function appendDefinedMonitorParam(params: URLSearchParams, key: string, value?: string | number | null) {
+  if (value == null) {
+    return;
+  }
+  const text = String(value).trim();
+  if (text !== '') {
+    params.set(key, text);
+  }
+}
+
+function buildMonitorRowTimeWindow(item: Monitor) {
+  const rawEnd = (item.gmtUpdate || item.gmtCreate || null) as number | string | null;
+  const parsedEnd =
+    typeof rawEnd === 'number'
+      ? rawEnd
+      : typeof rawEnd === 'string'
+        ? Date.parse(rawEnd)
+        : Number.NaN;
+  const end = Number.isFinite(parsedEnd) ? parsedEnd : null;
+  if (!end) {
+    return {};
+  }
+  return {
+    start: Math.max(0, end - 15 * 60 * 1000),
+    end
+  };
+}
+
+function buildMonitorRowSignalHref(kind: 'logs' | 'traces', item: Monitor, query: MonitorQueryState) {
+  const params = new URLSearchParams();
+  const search = item.name || item.instance || query.search || query.entityName;
+  const serviceName = query.entityName || item.name || undefined;
+  const timeWindow = buildMonitorRowTimeWindow(item);
+
+  appendDefinedMonitorParam(params, 'entityId', query.entityId);
+  appendDefinedMonitorParam(params, 'entityName', query.entityName);
+  appendDefinedMonitorParam(params, 'serviceName', serviceName);
+  appendDefinedMonitorParam(params, 'start', timeWindow.start);
+  appendDefinedMonitorParam(params, 'end', timeWindow.end);
+  appendDefinedMonitorParam(params, 'search', kind === 'logs' ? search : undefined);
+  appendDefinedMonitorParam(params, 'returnTo', buildMonitorRouteUrl(query));
+
+  const queryString = params.toString();
+  return `${kind === 'logs' ? '/log/manage' : '/trace/manage'}${queryString ? `?${queryString}` : ''}`;
+}
+
+function buildMonitorRowCodeHref(item: Monitor) {
+  const annotations = item.annotations || {};
+  return buildCodeNavigationUrl({
+    repositoryUrl: annotations.codeRepo || annotations.repositoryUrl,
+    provider: annotations.codeProvider,
+    defaultPath: annotations.codePath,
+    searchQuery: annotations.codeSearch || item.name || item.instance,
+    label: annotations.codeLabel || item.name || item.instance
+  });
+}
+
+export default function MonitorsPage({
+  initialQuery,
+  explicitStatus = ''
+}: {
+  initialQuery?: MonitorQueryState;
+  explicitStatus?: string;
+} = {}) {
+  const { t } = useI18n();
+  const router = useRouter();
+  const queryClient = useQueryClient();
+  const initialMonitorQuery = initialQuery ?? EMPTY_MONITOR_QUERY;
+  const [draft, setDraft] = useState<MonitorQueryState>(() => initialMonitorQuery);
+  const [query, setQuery] = useState<MonitorQueryState>(() => initialMonitorQuery);
+  const [selectedId, setSelectedId] = useState<number | null>(null);
+  const [checkedIds, setCheckedIds] = useState<number[]>([]);
+  const checkedIdsRef = useRef<number[]>([]);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [deleteTargetIds, setDeleteTargetIds] = useState<number[] | null>(null);
+  const [rowResponseConfirm, setRowResponseConfirm] = useState<MonitorRowResponseConfirm | null>(null);
+  const [batchResponseConfirm, setBatchResponseConfirm] = useState<MonitorBatchResponseConfirm | null>(null);
+  const [exportDialogScope, setExportDialogScope] = useState<HzExportTypeDialogScope | null>(null);
+  const [appPickerOpen, setAppPickerOpen] = useState(false);
+  const [appPickerLoading, setAppPickerLoading] = useState(false);
+  const [appPickerGroups, setAppPickerGroups] = useState<MonitorAppPickerGroup[]>([]);
+  const [appPickerSearch, setAppPickerSearch] = useState('');
+  const [appPickerError, setAppPickerError] = useState<string | null>(null);
+  const [appPickerMode, setAppPickerMode] = useState<MonitorAppPickerMode>('new');
+  const [reloadKey, setReloadKey] = useState(0);
+  const [actionFeedback, setActionFeedback] = useState<MonitorActionFeedback | null>(null);
+  const [pendingActionId, setPendingActionId] = useState<string | null>(null);
+  const [optimisticCopies, setOptimisticCopies] = useState<Monitor[]>([]);
+  const [copyingIds, setCopyingIds] = useState<number[]>([]);
+  const [openRowActionMenuId, setOpenRowActionMenuId] = useState<number | null>(null);
+  const importInputRef = useRef<HTMLInputElement | null>(null);
+  const warmedMonitorEditIdsRef = useRef<Set<number>>(new Set());
+  const previousMonitorsRef = useRef<Monitor[]>([]);
+  const disappearedMonitorTimersRef = useRef<Map<number, ReturnType<typeof window.setTimeout>>>(new Map());
+  const setCheckedIdsImmediate = useCallback((nextOrUpdater: number[] | ((current: number[]) => number[])) => {
+    const next = typeof nextOrUpdater === 'function' ? nextOrUpdater(checkedIdsRef.current) : nextOrUpdater;
+    checkedIdsRef.current = next;
+    setCheckedIds(next);
+  }, []);
+  const monitorListUrl = useMemo(() => buildMonitorUrl(query), [query]);
+  const monitorManageCacheKey = useMemo(
+    () => ['monitor-manage', monitorListUrl, explicitStatus || 'implicit-status', reloadKey].join('|'),
+    [explicitStatus, monitorListUrl, reloadKey]
+  );
+  const invalidateMonitorQueries = useCallback(
+    () => queryClient.invalidateQueries({ queryKey: queryKeys.monitors.all }),
+    [queryClient]
+  );
+  const refreshMonitorWorkbench = useCallback(() => {
+    void invalidateMonitorQueries();
+    setReloadKey(prev => prev + 1);
+  }, [invalidateMonitorQueries]);
+  const readMonitorListWithQuery = useCallback(
+    (nextQuery: MonitorQueryState) =>
+      queryClient.fetchQuery({
+        queryKey: queryKeys.monitors.list({
+          ...nextQuery,
+          explicitStatus: explicitStatus || 'implicit-status',
+          reloadKey
+        }),
+        queryFn: () => loadMonitorListFromFacade(api.monitors.page, nextQuery),
+        staleTime: 5000
+      }),
+    [explicitStatus, queryClient, reloadKey]
+  );
+
+  useEffect(() => {
+    setDraft(initialMonitorQuery);
+    setQuery(initialMonitorQuery);
+  }, [initialMonitorQuery]);
+
+  useEffect(() => {
+    setOptimisticCopies([]);
+    setCopyingIds([]);
+  }, [monitorListUrl]);
+
+  useEffect(() => {
+    checkedIdsRef.current = checkedIds;
+  }, [checkedIds]);
+
+  useEffect(
+    () => () => {
+      disappearedMonitorTimersRef.current.forEach(timerId => window.clearTimeout(timerId));
+      disappearedMonitorTimersRef.current.clear();
+    },
+    []
+  );
+
+  useEffect(() => {
+    const intervalId = window.setInterval(() => {
+      setCheckedIdsImmediate([]);
+      setSelectedId(null);
+      refreshMonitorWorkbench();
+    }, MONITOR_MANAGE_AUTO_REFRESH_MS);
+    return () => window.clearInterval(intervalId);
+  }, [refreshMonitorWorkbench, setCheckedIdsImmediate]);
+
+  const clearDisappearedMonitorTimer = useCallback((monitorId: number) => {
+    const timerId = disappearedMonitorTimersRef.current.get(monitorId);
+    if (!timerId) {
+      return;
+    }
+    window.clearTimeout(timerId);
+    disappearedMonitorTimersRef.current.delete(monitorId);
+  }, []);
+
+  const isMonitorDisabled = useCallback((monitor: Monitor) => monitor._displayStatus === 'DISAPPEARED', []);
+
+  const reconcileMonitorStates = useCallback(
+    (newMonitors: Monitor[]): Monitor[] => {
+      if (previousMonitorsRef.current.length === 0) {
+        const activeMonitors = newMonitors.map(monitor => ({ ...monitor, _displayStatus: 'ACTIVE' as const }));
+        previousMonitorsRef.current = activeMonitors;
+        return activeMonitors;
+      }
+
+      const newMonitorMap = new Map(newMonitors.map(monitor => [monitor.id, monitor]));
+      const previousMonitorMap = new Map(previousMonitorsRef.current.map(monitor => [monitor.id, monitor]));
+      const reconciledMonitors: Monitor[] = [];
+
+      newMonitors.forEach(newMonitor => {
+        if (previousMonitorMap.has(newMonitor.id)) {
+          clearDisappearedMonitorTimer(newMonitor.id);
+        }
+        reconciledMonitors.push({ ...newMonitor, _displayStatus: 'ACTIVE' as const });
+      });
+
+      previousMonitorsRef.current.forEach(previousMonitor => {
+        if (newMonitorMap.has(previousMonitor.id)) {
+          return;
+        }
+
+        if (previousMonitor._displayStatus === 'DISAPPEARED') {
+          reconciledMonitors.push(previousMonitor);
+          return;
+        }
+
+        const disappearedMonitor = {
+          ...previousMonitor,
+          _displayStatus: 'DISAPPEARED' as const,
+          _disappearTime: Date.now()
+        };
+        const timerId = window.setTimeout(() => {
+          previousMonitorsRef.current = previousMonitorsRef.current.filter(monitor => monitor.id !== disappearedMonitor.id);
+          disappearedMonitorTimersRef.current.delete(disappearedMonitor.id);
+          setCheckedIdsImmediate(current => current.filter(id => id !== disappearedMonitor.id));
+          refreshMonitorWorkbench();
+        }, MONITOR_MANAGE_DISAPPEARED_GRACE_MS);
+        disappearedMonitorTimersRef.current.set(disappearedMonitor.id, timerId);
+        reconciledMonitors.push(disappearedMonitor);
+      });
+
+      previousMonitorsRef.current = reconciledMonitors;
+      return reconciledMonitors;
+    },
+    [clearDisappearedMonitorTimer, refreshMonitorWorkbench, setCheckedIdsImmediate]
+  );
+
+  const load = useCallback(async (): Promise<MonitorPageData> => {
+    void reloadKey;
+    const list = await readMonitorListWithQuery(query);
+    const entityContextActive = hasMonitorEntityContext(query);
+    const statusWasImplicit = explicitStatus.trim() === '';
+
+    if (
+      shouldFallbackMonitorEntityWorkbench({
+        entityContextActive,
+        statusWasImplicit,
+        status: query.status,
+        totalElements: list.totalElements || 0,
+        fellBackToAll: false
+      })
+    ) {
+      const fallbackQuery = {
+        ...query,
+        status: ''
+      };
+      const fallbackList = await readMonitorListWithQuery(fallbackQuery);
+      return {
+        list: {
+          ...fallbackList,
+          content: reconcileMonitorStates(fallbackList.content)
+        },
+        query: fallbackQuery,
+        entityWorkbenchFallback: true
+      };
+    }
+
+    return {
+      list: {
+        ...list,
+        content: reconcileMonitorStates(list.content)
+      },
+      query,
+      entityWorkbenchFallback: false
+    };
+  }, [explicitStatus, query, readMonitorListWithQuery, reconcileMonitorStates, reloadKey]);
+
+  return (
+    <div
+      data-monitor-response-confirm-closable-contract="angular-nz-closable-false"
+      data-monitor-response-confirm-ok-contract="angular-nz-ok-danger-primary"
+      data-monitor-response-confirm-modal-owner="hertzbeat-ui-confirm-dialog"
+      data-monitor-copy-lifecycle-contract="angular-copy-success-refresh-unavailable-refresh"
+      data-monitor-copy-lifecycle-owner="route-copy-contract"
+      data-monitor-delete-confirm-closable-contract="angular-nz-closable-false"
+      data-monitor-delete-confirm-ok-contract="angular-nz-ok-danger-primary"
+      data-monitor-delete-confirm-modal-owner="hertzbeat-ui-confirm-dialog"
+      data-monitor-export-type-dialog-contract="angular-nz-modal-600-no-footer"
+      data-monitor-export-type-dialog-flow="angular-trigger-type-modal-before-download"
+      data-monitor-export-type-dialog-owner="hertzbeat-ui-export-type-dialog"
+      data-monitor-batch-response-filter-contract="angular-status-filtered-selection"
+      data-monitor-batch-response-filter-owner="route-batch-response-contract"
+      data-monitor-batch-more-menu-contract="angular-toolbar-ellipsis-menu"
+      data-monitor-batch-more-menu-owner="route-batch-toolbar-contract"
+    >
+      <ClientWorkbench
+        load={load}
+        loadingCopy={t('monitors.loading')}
+        cacheKey={monitorManageCacheKey}
+        cacheSettledTtlMs={MONITOR_MANAGE_SETTLED_CACHE_TTL_MS}
+      >
+        {data => {
+        const backendMonitorIds = new Set(data.list.content.map(item => item.id));
+        const backendMonitorNames = new Set(data.list.content.map(item => item.name));
+        const visibleOptimisticCopies = optimisticCopies.filter(
+          item => !backendMonitorIds.has(item.id) && !backendMonitorNames.has(item.name)
+        );
+        const tableRows = [...visibleOptimisticCopies, ...data.list.content];
+        const selected =
+          tableRows.find(item => item.id === selectedId && !isMonitorDisabled(item)) ??
+          tableRows.find(item => !isMonitorDisabled(item)) ??
+          tableRows[0] ??
+          null;
+        const resolvedPageIndex = Math.max(0, Number.parseInt(data.query.pageIndex || String(data.list.pageIndex ?? 0), 10) || 0);
+        const resolvedPageSize = Math.max(1, Number.parseInt(data.query.pageSize || String(data.list.pageSize ?? 8), 10) || 8);
+        const displayTotal = (data.list.totalElements || 0) + visibleOptimisticCopies.length;
+        const totalPages = Math.max(1, Math.ceil(displayTotal / resolvedPageSize));
+        const handlePageJumpChange = (value: string) => {
+          const requestedPage = Number.parseInt(value, 10);
+          if (!Number.isFinite(requestedPage)) {
+            return;
+          }
+          const nextPageIndex = Math.min(totalPages - 1, Math.max(0, requestedPage - 1));
+          navigateWithQuery({
+            ...data.query,
+            pageIndex: String(nextPageIndex),
+            pageSize: String(resolvedPageSize)
+          });
+        };
+        const entityContextActive = hasMonitorEntityContext(query);
+        const selectableTableRows = tableRows.filter(item => !isMonitorDisabled(item));
+        const downCount = selectableTableRows.filter(item => item.status === 2).length;
+        const resetQuery = () => {
+          const empty = applyMonitorWorkspaceDefaults({
+            search: '',
+            app: '',
+            labels: '',
+            status: '',
+            pageIndex: '',
+            pageSize: '',
+            entityId: query.entityId,
+            entityName: query.entityName,
+            returnTo: query.returnTo
+          });
+          setDraft(empty);
+          setQuery(empty);
+          setCheckedIdsImmediate([]);
+          setSelectedId(null);
+          router.replace(buildMonitorRouteUrl(empty));
+        };
+
+        const applyQuery = () => {
+          const nextQuery = {
+            ...draft,
+            pageIndex: '0',
+            pageSize: draft.pageSize || query.pageSize || '8'
+          };
+          setQuery(nextQuery);
+          setCheckedIdsImmediate([]);
+          setSelectedId(null);
+          router.replace(buildMonitorRouteUrl(nextQuery));
+        };
+
+        const navigateWithQuery = (nextQuery: MonitorQueryState) => {
+          setDraft(nextQuery);
+          setQuery(nextQuery);
+          setCheckedIdsImmediate([]);
+          setSelectedId(null);
+          router.replace(buildMonitorRouteUrl(nextQuery));
+        };
+
+        const handleManualRefresh = () => {
+          setCheckedIdsImmediate([]);
+          setSelectedId(null);
+          refreshMonitorWorkbench();
+        };
+
+        const clearSearchFilter = () => {
+          navigateWithQuery({
+            ...draft,
+            search: '',
+            pageIndex: '0',
+            pageSize: draft.pageSize || query.pageSize || '8'
+          });
+        };
+
+        const clearLabelFilter = () => {
+          navigateWithQuery({
+            ...draft,
+            labels: '',
+            pageIndex: '0',
+            pageSize: draft.pageSize || query.pageSize || '8'
+          });
+        };
+
+        const handleTypeFilterChange = (app: string) => {
+          navigateWithQuery({
+            ...draft,
+            app,
+            pageIndex: '0',
+            pageSize: draft.pageSize || query.pageSize || '8'
+          });
+        };
+
+        const checkedCount = checkedIds.length;
+        const deleteDialogIds = deleteTargetIds ?? checkedIds;
+        const deleteDialogCount = deleteDialogIds.length;
+        const currentPageIds = selectableTableRows.map(item => item.id);
+        const monitorBatchActionProps = (action: string) =>
+          ({
+            'data-monitor-manage-batch-action-owner': 'hertzbeat-ui-button',
+            'data-monitor-manage-batch-action': action
+          }) as React.ButtonHTMLAttributes<HTMLButtonElement>;
+        const emptyValue = t('common.none');
+        const resolveMonitorTypeLabel = (app?: string | null) => {
+          const appValue = app?.trim();
+          if (!appValue) return emptyValue;
+          const appKey = appValue.toLowerCase();
+          const translated = t(`monitor.app.${appKey}`);
+          return translated === `monitor.app.${appKey}` ? appValue : translated;
+        };
+        const navigationContext = {
+          app: data.query.app || selected?.app || 'website',
+          labels: data.query.labels,
+          pageIndex: String(resolvedPageIndex),
+          pageSize: String(resolvedPageSize),
+          entityId: data.query.entityId,
+          entityName: data.query.entityName,
+          returnTo: buildMonitorRouteUrl(data.query)
+        };
+        const newMonitorBaseContext = {
+          ...navigationContext,
+          app: data.query.app || ''
+        };
+        const appPickerCategories: HzTemplateCategory[] = appPickerGroups.map(group => ({
+          id: group.key,
+          label: group.label,
+          items: group.rows.map(row => ({
+            id: row.app,
+            label: row.label
+          }))
+        }));
+        const statusFilterOptions = [
+          { value: '', label: t('monitors.status.all'), count: displayTotal },
+          { value: '1', label: t('monitors.status.up'), count: selectableTableRows.filter(item => item.status === 1).length },
+          { value: '2', label: t('monitors.status.down'), count: downCount },
+          { value: '0', label: t('monitors.status.paused'), count: selectableTableRows.filter(item => item.status === 0).length }
+        ];
+        const monitorTypeValues = Array.from(
+          new Set(
+            [draft.app, data.query.app, ...tableRows.map(item => item.app)]
+              .map(app => app?.trim())
+              .filter((app): app is string => Boolean(app))
+          )
+        ).sort((left, right) => resolveMonitorTypeLabel(left).localeCompare(resolveMonitorTypeLabel(right)));
+        const monitorTypeFilterOptions = [
+          { value: '', label: t('monitors.type.all') },
+          ...monitorTypeValues.map(value => ({
+            value,
+            label: resolveMonitorTypeLabel(value)
+          }))
+        ];
+
+        const openAppPicker = async (mode: MonitorAppPickerMode) => {
+          setAppPickerMode(mode);
+          setAppPickerOpen(true);
+          setAppPickerError(null);
+          if (appPickerGroups.length > 0) return;
+          setAppPickerLoading(true);
+          try {
+            const locale = getCurrentLocale();
+            const hierarchy = await api.monitors.appHierarchy<MonitorAppHierarchyItem[]>(locale);
+            setAppPickerGroups(buildMonitorAppPickerGroups(hierarchy, t));
+          } catch (error) {
+            setAppPickerError(error instanceof Error ? error.message : t('common.load-failed'));
+          } finally {
+            setAppPickerLoading(false);
+          }
+        };
+
+        const selectNewMonitorApp = (app: string) => {
+          setAppPickerOpen(false);
+          setAppPickerSearch('');
+          if (appPickerMode === 'filter') {
+            handleTypeFilterChange(app);
+            return;
+          }
+          router.push(buildMonitorNewHref({ ...newMonitorBaseContext, app }));
+        };
+
+        const handleStatusFilterChange = (status: string) => {
+          navigateWithQuery({
+            ...draft,
+            status,
+            pageIndex: '0',
+            pageSize: draft.pageSize || query.pageSize || '8'
+          });
+        };
+
+        const downloadExport = async (response: Response, fallbackName: string) => {
+          if (!response.ok) {
+            throw new Error(t('common.notify.export-fail-status', { status: response.status }));
+          }
+          const contentType = response.headers.get('Content-Type') || '';
+          if (contentType.includes('application/json')) {
+            throw new Error(t('common.notify.export-fail'));
+          }
+          const blob = await response.blob();
+          const url = window.URL.createObjectURL(blob);
+          const anchor = document.createElement('a');
+          anchor.href = url;
+          anchor.download = resolveDownloadFilename(response.headers.get('Content-Disposition'), fallbackName);
+          anchor.click();
+          window.URL.revokeObjectURL(url);
+        };
+
+        const uploadImport = async (file: File) => {
+          await importMonitorsFromFacade(api.monitors.import, file);
+        };
+
+        const warmMonitorEdit = (item: Monitor, href: string) => {
+          router.prefetch(href);
+          if (warmedMonitorEditIdsRef.current.has(item.id)) {
+            return;
+          }
+          warmedMonitorEditIdsRef.current.add(item.id);
+          void consumeWorkbenchLoad(
+            buildMonitorEditCacheKey(item.id),
+            () =>
+              loadMonitorEditorDraftFromFacade(
+                {
+                  readMonitorDetail: api.monitors.editorDetail,
+                  readCollectors: api.monitors.editorCollectors,
+                  readParamDefines: api.monitors.editorParamDefines
+                },
+                'edit',
+                { monitorId: String(item.id) }
+              ),
+            { settledTtlMs: MONITOR_MANAGE_SETTLED_CACHE_TTL_MS }
+          ).catch(() => {
+            warmedMonitorEditIdsRef.current.delete(item.id);
+          });
+        };
+
+        const showSelectionWarning = (message: string) => {
+          setActionFeedback({ tone: 'warning', title: message });
+        };
+
+        const requireSelection = (message: string) => {
+          const selectedIds = checkedIdsRef.current;
+          if (selectedIds.length > 0) {
+            return selectedIds;
+          }
+          showSelectionWarning(message);
+          return null;
+        };
+        const requireSelectionByStatus = (
+          predicate: (monitor: Monitor) => boolean,
+          message: string
+        ) => {
+          const selectedIds = checkedIdsRef.current;
+          const selectedMonitorIds = selectedIds.filter(id => {
+            const monitor = tableRows.find(item => item.id === id);
+            return monitor ? predicate(monitor) : false;
+          });
+          if (selectedMonitorIds.length > 0) {
+            return selectedMonitorIds;
+          }
+          showSelectionWarning(message);
+          return null;
+        };
+
+        const runBatchAction = async (
+          actionId: string,
+          pendingTitle: string,
+          task: () => Promise<string>,
+          fallbackErrorTitle: string,
+          options?: {
+            refreshOnUnavailable?: boolean;
+          }
+        ): Promise<boolean> => {
+          setPendingActionId(actionId);
+          setActionFeedback({ tone: 'info', title: pendingTitle });
+          try {
+            const successTitle = await task();
+            setActionFeedback({ tone: 'success', title: successTitle });
+            return true;
+          } catch (error) {
+            if (options?.refreshOnUnavailable && isMonitorUnavailableRefreshError(error)) {
+              setCheckedIdsImmediate([]);
+              setSelectedId(null);
+              refreshMonitorWorkbench();
+              setActionFeedback({ tone: 'warning', title: t('monitor.item.unavailable.refresh') });
+              return false;
+            }
+            setActionFeedback({
+              tone: 'critical',
+              title: fallbackErrorTitle,
+              description: error instanceof Error ? error.message : undefined
+            });
+            return false;
+          } finally {
+            setPendingActionId(current => (current === actionId ? null : current));
+          }
+        };
+
+        const handleCopyMonitor = async (item: Monitor) => {
+          const optimisticId = -Date.now();
+          const optimisticName = buildOptimisticCopyName(item.name, new Set([...backendMonitorNames, ...optimisticCopies.map(copy => copy.name)]));
+          const optimisticCopy: Monitor = {
+            ...item,
+            id: optimisticId,
+            name: optimisticName,
+            gmtCreate: Date.now(),
+            gmtUpdate: Date.now()
+          };
+          setCopyingIds(prev => Array.from(new Set([...prev, item.id])));
+          setOptimisticCopies(prev => [optimisticCopy, ...prev]);
+          try {
+            await api.monitors.copy(item.id);
+            setActionFeedback({ tone: 'success', title: t('monitor.copy.success') });
+            refreshMonitorWorkbench();
+          } catch (error) {
+            setOptimisticCopies(prev => prev.filter(copy => copy.id !== optimisticId));
+            if (isMonitorUnavailableRefreshError(error)) {
+              setCheckedIdsImmediate([]);
+              setSelectedId(null);
+              refreshMonitorWorkbench();
+              setActionFeedback({ tone: 'warning', title: t('monitor.item.unavailable.refresh') });
+              return;
+            }
+            setActionFeedback({ tone: 'critical', title: error instanceof Error ? error.message : t('monitor.copy.failed') });
+          } finally {
+            setCopyingIds(prev => prev.filter(id => id !== item.id));
+          }
+        };
+
+        const handleCopyInstance = async (value: string) => {
+          const copyValue = value.trim();
+          if (!copyValue) {
+            return;
+          }
+          try {
+            await navigator.clipboard?.writeText(copyValue);
+            setActionFeedback({ tone: 'success', title: t('common.notify.copy-success') });
+          } catch (error) {
+            setActionFeedback({
+              tone: 'critical',
+              title: t('common.notify.copy-fail'),
+              description: error instanceof Error ? error.message : undefined
+            });
+          }
+        };
+
+        const refreshAfterDelete = (deletedCount: number) => {
+          setCheckedIdsImmediate([]);
+          setSelectedId(null);
+          setDeleteDialogOpen(false);
+          setDeleteTargetIds(null);
+          const nextTotal = Math.max(0, displayTotal - deletedCount);
+          const nextPageIndex = Math.min(resolvedPageIndex, Math.max(0, Math.ceil(nextTotal / resolvedPageSize) - 1));
+          if (nextPageIndex !== resolvedPageIndex) {
+            const nextQuery = {
+              ...data.query,
+              pageIndex: String(nextPageIndex),
+              pageSize: String(resolvedPageSize)
+            };
+            setDraft(nextQuery);
+            setQuery(nextQuery);
+            router.replace(buildMonitorRouteUrl(nextQuery));
+          }
+          refreshMonitorWorkbench();
+        };
+
+        const handleConfirmDelete = async () => {
+          const targetIds = deleteTargetIds ?? checkedIdsRef.current;
+          if (targetIds.length === 0) {
+            showSelectionWarning(t('common.notify.no-select-delete'));
+            setDeleteDialogOpen(false);
+            setDeleteTargetIds(null);
+            return;
+          }
+          await runBatchAction('delete', t('common.notify.delete-pending'), async () => {
+            const grafanaDeleteTask = Promise.allSettled(targetIds.map(id => api.monitors.deleteGrafanaDashboard(id)));
+            await api.monitors.delete(targetIds);
+            await grafanaDeleteTask;
+            refreshAfterDelete(targetIds.length);
+            return t('common.delete-success');
+          }, t('common.notify.delete-fail'));
+          setDeleteDialogOpen(false);
+          setDeleteTargetIds(null);
+        };
+
+        const openDeleteDialog = (ids: number[] | null) => {
+          setDeleteTargetIds(ids);
+          setDeleteDialogOpen(true);
+        };
+
+        const handleSelectVisible = () => {
+          setCheckedIdsImmediate(currentPageIds);
+        };
+
+        const handleSelectAllResults = async () => {
+          if (displayTotal <= currentPageIds.length) {
+            setCheckedIdsImmediate(currentPageIds);
+            return;
+          }
+          setPendingActionId('select-all-results');
+          try {
+            const selectAllQuery = {
+              ...data.query,
+              pageIndex: '0',
+              pageSize: String(displayTotal)
+            };
+            const allResults = await readMonitorListWithQuery(selectAllQuery);
+            const allIds = Array.from(new Set([...visibleOptimisticCopies.map(item => item.id), ...allResults.content.map(item => item.id)]));
+            setCheckedIdsImmediate(allIds);
+          } catch (error) {
+            setActionFeedback({ tone: 'critical', title: error instanceof Error ? error.message : t('common.load-failed') });
+          } finally {
+            setPendingActionId(current => (current === 'select-all-results' ? null : current));
+          }
+        };
+
+        const handleClearSelection = () => {
+          setCheckedIdsImmediate([]);
+        };
+
+        const handleExportSelected = async (type: 'JSON' | 'EXCEL') => {
+          const selectedIds = requireSelection(t('common.notify.no-select-export'));
+          if (!selectedIds) {
+            return false;
+          }
+          const actionId = type === 'JSON' ? 'export-json' : 'export-excel';
+          return runBatchAction(actionId, t('common.notify.export-pending'), async () => {
+            await downloadExport(
+              await api.monitors.exportResponse(selectedIds, type),
+              type === 'JSON' ? 'monitors.json' : 'monitors.xlsx'
+            );
+            return t('common.notify.export-success');
+          }, t('common.notify.export-fail'));
+        };
+
+        const handleExportAll = async (type: 'JSON' | 'EXCEL') => {
+          const actionId = type === 'JSON' ? 'export-all-json' : 'export-all-excel';
+          return runBatchAction(actionId, t('common.notify.export-pending'), async () => {
+            await downloadExport(
+              await api.monitors.exportAllResponse(type),
+              type === 'JSON' ? 'monitors-all.json' : 'monitors-all.xlsx'
+            );
+            return t('common.notify.export-success');
+          }, t('common.notify.export-fail'));
+        };
+
+        const openExportDialog = (scope: HzExportTypeDialogScope) => {
+          if (scope === 'selected' && !requireSelection(t('common.notify.no-select-export'))) {
+            return;
+          }
+          setExportDialogScope(scope);
+        };
+
+        const handleExportDialogSelect = async (type: 'JSON' | 'EXCEL') => {
+          const currentScope = exportDialogScope;
+          if (currentScope === 'selected') {
+            const success = await handleExportSelected(type);
+            if (success) {
+              setExportDialogScope(null);
+            }
+            return;
+          }
+          if (currentScope === 'all') {
+            const success = await handleExportAll(type);
+            if (success) {
+              setExportDialogScope(null);
+            }
+          }
+        };
+
+        const handleImportClick = () => {
+          importInputRef.current?.click();
+        };
+
+        const handleImportChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
+          const file = event.target.files?.[0];
+          event.target.value = '';
+          if (!file) return;
+          await runBatchAction(
+            'import',
+            t('common.notify.import-submitted', { taskName: file.name }),
+            async () => {
+              await uploadImport(file);
+              setCheckedIdsImmediate([]);
+              setSelectedId(null);
+              refreshMonitorWorkbench();
+              return t('common.notify.import-success-detail', { taskName: file.name });
+            },
+            t('common.notify.import-fail-detail', {
+              taskName: file.name,
+              errMsg: t('common.notify.import-fail')
+            })
+          );
+        };
+
+        const openBatchResponseConfirm = (action: MonitorBatchResponseConfirm['action']) => {
+          const selectedIds =
+            action === 'enable'
+              ? requireSelectionByStatus(monitor => monitor.status === 0, t('common.notify.no-select-enable'))
+              : requireSelectionByStatus(monitor => monitor.status !== 0, t('common.notify.no-select-cancel'));
+          if (!selectedIds) return;
+          setBatchResponseConfirm({ action, ids: selectedIds });
+        };
+
+        const handleEnableSelected = async (selectedIds: number[]) =>
+          await runBatchAction('enable', t('common.notify.enable-pending'), async () => {
+            await api.monitors.enable(selectedIds);
+            setCheckedIdsImmediate([]);
+            refreshMonitorWorkbench();
+            return t('common.notify.enable-success');
+          }, t('common.notify.enable-fail'), { refreshOnUnavailable: true });
+
+        const handlePauseSelected = async (selectedIds: number[]) =>
+          await runBatchAction('pause', t('common.notify.cancel-pending'), async () => {
+            await api.monitors.pause(selectedIds);
+            setCheckedIdsImmediate([]);
+            refreshMonitorWorkbench();
+            return t('common.notify.cancel-success');
+          }, t('common.notify.cancel-fail'), { refreshOnUnavailable: true });
+
+        const batchResponseConfirmBusy =
+          batchResponseConfirm?.action === 'enable'
+            ? pendingActionId === 'enable'
+            : batchResponseConfirm?.action === 'pause'
+              ? pendingActionId === 'pause'
+              : false;
+        const handleConfirmBatchResponse = async () => {
+          if (!batchResponseConfirm) return;
+          const succeeded =
+            batchResponseConfirm.action === 'enable'
+              ? await handleEnableSelected(batchResponseConfirm.ids)
+              : await handlePauseSelected(batchResponseConfirm.ids);
+          if (succeeded) {
+            setBatchResponseConfirm(null);
+          }
+        };
+
+        const openRowResponseConfirm = (item: Monitor, action: MonitorRowResponseConfirm['action']) => {
+          setRowResponseConfirm({ action, monitor: item });
+        };
+
+        const handleEnableRow = async (item: Monitor) =>
+          runBatchAction(`row-enable-${item.id}`, t('common.notify.enable-pending'), async () => {
+            await api.monitors.enable([item.id]);
+            refreshMonitorWorkbench();
+            return t('common.notify.enable-success');
+          }, t('common.notify.enable-fail'), { refreshOnUnavailable: true });
+
+        const handlePauseRow = async (item: Monitor) =>
+          runBatchAction(`row-pause-${item.id}`, t('common.notify.cancel-pending'), async () => {
+            await api.monitors.pause([item.id]);
+            refreshMonitorWorkbench();
+            return t('common.notify.cancel-success');
+          }, t('common.notify.cancel-fail'), { refreshOnUnavailable: true });
+
+        const rowResponseConfirmActionId = rowResponseConfirm
+          ? `row-${rowResponseConfirm.action === 'enable' ? 'enable' : 'pause'}-${rowResponseConfirm.monitor.id}`
+          : null;
+        const rowResponseConfirmBusy = rowResponseConfirmActionId ? pendingActionId === rowResponseConfirmActionId : false;
+        const handleConfirmRowResponse = async () => {
+          if (!rowResponseConfirm) return;
+          const succeeded =
+            rowResponseConfirm.action === 'enable'
+              ? await handleEnableRow(rowResponseConfirm.monitor)
+              : await handlePauseRow(rowResponseConfirm.monitor);
+          if (succeeded) {
+            setRowResponseConfirm(null);
+          }
+        };
+
+          return (
+          <>
+            <HzExplorerFrame
+              data-monitor-manage-auto-refresh-owner="react-interval"
+              data-monitor-manage-auto-refresh-interval-ms={String(MONITOR_MANAGE_AUTO_REFRESH_MS)}
+              data-monitor-manage-auto-refresh-tick={String(reloadKey)}
+              data-monitor-response-confirm-closable-contract="angular-nz-closable-false"
+              data-monitor-response-confirm-ok-contract="angular-nz-ok-danger-primary"
+              data-monitor-response-confirm-modal-owner="hertzbeat-ui-confirm-dialog"
+              data-monitor-delete-confirm-closable-contract="angular-nz-closable-false"
+              data-monitor-delete-confirm-ok-contract="angular-nz-ok-danger-primary"
+              data-monitor-delete-confirm-modal-owner="hertzbeat-ui-confirm-dialog"
+              data-monitor-export-type-dialog-contract="angular-nz-modal-600-no-footer"
+              data-monitor-export-type-dialog-flow="angular-trigger-type-modal-before-download"
+              data-monitor-export-type-dialog-owner="hertzbeat-ui-export-type-dialog"
+              title={t('monitors.title')}
+              description={
+                entityContextActive
+                  ? buildMonitorWorkbenchNarrative(
+                      {
+                        entityContextActive,
+                        total: displayTotal,
+                        downCount,
+                        status: data.query.status,
+                        fellBackToAll: data.entityWorkbenchFallback
+                      },
+                      t
+                    )
+                  : undefined
+              }
+              mainId="monitor-manage-main"
+              mainLabel={t('monitors.title')}
+              filterRailLabel={t('monitors.section.list.title')}
+              actions={
+                <>
+                  {entityContextActive ? (
+                    <Link href={buildMonitorEntityReturnHref(query)}>
+                      <HzButton
+                        size="sm"
+                        intent="primary"
+                        data-monitor-manage-primary-action-owner="hertzbeat-ui-button"
+                        data-monitor-manage-primary-action="entity-return"
+                      >
+                        {query.entityName || t('entity.response.context.return')}
+                      </HzButton>
+                    </Link>
+                  ) : null}
+                  {!entityContextActive ? (
+                    <>
+                      <HzFileInput
+                        ref={importInputRef}
+                        data-monitor-manage-import-input-owner="hertzbeat-ui-file-input"
+                        data-monitors-import-file-input="true"
+                        onChange={event => void handleImportChange(event)}
+                      />
+                      <HzIconButton
+                        label={t('common.refresh')}
+                        intent="ghost"
+                        onClick={handleManualRefresh}
+                        data-monitor-manage-manual-refresh-owner="hertzbeat-ui-icon-button"
+                        data-monitor-manage-manual-refresh-action="sync"
+                        data-monitor-manage-manual-refresh-tick={String(reloadKey)}
+                      >
+                        <RefreshCw size={13} />
+                      </HzIconButton>
+                      {data.query.app ? (
+                        <Link href={buildMonitorNewHref({ ...newMonitorBaseContext, app: data.query.app })}>
+                          <HzButton
+                            size="sm"
+                            data-monitor-manage-primary-action-owner="hertzbeat-ui-button"
+                            data-monitor-manage-primary-action="new-monitor-link"
+                          >
+                            {t('monitor.new-monitor')}
+                          </HzButton>
+                        </Link>
+                      ) : (
+                        <HzButton
+                          size="sm"
+                          data-monitor-manage-primary-action-owner="hertzbeat-ui-button"
+                          data-monitor-manage-primary-action="new-monitor-picker"
+                          data-monitors-new-app-picker-trigger="true"
+                          onClick={() => void openAppPicker('new')}
+                        >
+                          {t('monitor.new-monitor')}
+                        </HzButton>
+                      )}
+                    </>
+                  ) : null}
+                </>
+              }
+              queryBar={
+                <HzMonitorFilterBar
+                  data-monitor-manage-filter-owner="hertzbeat-ui-monitor-filter-bar"
+                  searchLabel={t('common.search')}
+                  searchPlaceholder={t('monitors.search.placeholder')}
+                  searchValue={draft.search}
+                  onSearchChange={value => setDraft(prev => ({ ...prev, search: value }))}
+                  searchClearLabel={t('common.clear')}
+                  onSearchClear={clearSearchFilter}
+                  searchClearButtonProps={
+                    {
+                      'data-monitors-filter-clear-action': 'search',
+                      'data-monitor-filter-clear-action-owner': 'hertzbeat-ui-icon-button'
+                    } as React.ComponentProps<typeof HzMonitorFilterBar>['searchClearButtonProps']
+                  }
+                  searchInputProps={
+                    {
+                      'data-monitors-search-input': 'true',
+                      'data-monitor-manage-filter-input-owner': 'hertzbeat-ui-input',
+                      'data-monitors-filter-enter-submit': 'search',
+                      'data-monitor-filter-enter-submit-owner': 'hertzbeat-ui-input'
+                    } as React.InputHTMLAttributes<HTMLInputElement>
+                  }
+                  labelFilterLabel={t('monitor.search.label')}
+                  labelFilterPlaceholder={t('monitor.search.label')}
+                  labelFilterValue={draft.labels}
+                  onLabelFilterChange={value => setDraft(prev => ({ ...prev, labels: value }))}
+                  labelFilterClearLabel={t('common.clear')}
+                  onLabelFilterClear={clearLabelFilter}
+                  labelFilterClearButtonProps={
+                    {
+                      'data-monitors-filter-clear-action': 'labels',
+                      'data-monitor-label-filter-clear-action-owner': 'hertzbeat-ui-icon-button'
+                    } as React.ComponentProps<typeof HzMonitorFilterBar>['labelFilterClearButtonProps']
+                  }
+                  labelFilterInputProps={
+                    {
+                      'data-monitors-label-filter-input': 'true',
+                      'data-monitor-manage-label-filter-input-owner': 'hertzbeat-ui-input',
+                      'data-monitors-filter-enter-submit': 'labels',
+                      'data-monitor-label-filter-enter-submit-owner': 'hertzbeat-ui-input'
+                    } as React.InputHTMLAttributes<HTMLInputElement>
+                  }
+                  typeLabel={t('monitor.app')}
+                  typeValue={draft.app}
+                  typeOptions={monitorTypeFilterOptions}
+                  onTypeChange={handleTypeFilterChange}
+                  typePickerLabel={t('monitor.search.app')}
+                  onTypePickerOpen={() => void openAppPicker('filter')}
+                  typePickerButtonProps={
+                    {
+                      'data-monitor-manage-filter-action-owner': 'hertzbeat-ui-button',
+                      'data-monitor-manage-filter-action': 'app-picker',
+                      'data-monitors-filter-app-picker-trigger': 'true'
+                    } as React.ButtonHTMLAttributes<HTMLButtonElement>
+                  }
+                  typeSelectProps={{
+                    'data-monitor-type-filter': 'true',
+                    'data-monitor-manage-filter-select-owner': 'hertzbeat-ui-select',
+                    'data-monitor-manage-filter-select': 'type',
+                    'data-monitors-app-filter-autosubmit': 'true',
+                    'data-monitor-app-filter-autosubmit-owner': 'hertzbeat-ui-select'
+                  }}
+                  statusLabel={t('common.status')}
+                  statusValue={draft.status}
+                  statusOptions={statusFilterOptions.map(option => ({
+                    value: option.value,
+                    label: option.label
+                  }))}
+                  onStatusChange={handleStatusFilterChange}
+                  statusSelectProps={{
+                    'data-monitors-status-filter': 'true',
+                    'data-monitor-manage-filter-select-owner': 'hertzbeat-ui-select',
+                    'data-monitor-manage-filter-select': 'status',
+                    'data-monitor-status-filter-autosubmit-owner': 'hertzbeat-ui-select',
+                    'data-monitors-status-filter-autosubmit': 'true'
+                  }}
+                  applyLabel={t('common.apply-filters')}
+                  clearLabel={t('common.clear')}
+                  onApply={applyQuery}
+                  onClear={resetQuery}
+                  applyButtonProps={{
+                    'data-monitor-manage-filter-action-owner': 'hertzbeat-ui-button',
+                    'data-monitor-manage-filter-action': 'apply'
+                  }}
+                  clearButtonProps={{
+                    'data-monitor-manage-filter-action-owner': 'hertzbeat-ui-button',
+                    'data-monitor-manage-filter-action': 'clear'
+                  }}
+                />
+              }
+            >
+              <div
+                className="min-w-0"
+                data-monitor-manage-shell-owner="hertzbeat-ui-explorer-frame"
+              >
+                <section className="min-w-0">
+                  {tableRows.length === 0 ? (
+                    <div className="p-3">
+                      <HzEmptyState
+                        title={t('monitors.empty-results.title')}
+                        description={t('monitors.empty-results.copy')}
+                        data-monitor-manage-empty-owner="hertzbeat-ui-empty-state"
+                      />
+                    </div>
+                  ) : (
+                    <div className="grid min-w-0 gap-0">
+                      <HzBatchToolbar
+                        data-monitor-manage-batch-owner="hertzbeat-ui-batch-toolbar"
+                        data-monitor-batch-more-menu-contract="angular-toolbar-ellipsis-menu"
+                        data-monitor-batch-more-menu-owner="hertzbeat-ui-batch-toolbar"
+                        selectionCount={checkedCount}
+                        selectionLabel={`${t('common.selected')} / ${displayTotal}`}
+                        variant="embedded"
+                        overflowLabel={t('monitors.controls.more-actions')}
+                        overflowButtonProps={{
+                          'data-monitor-batch-more-menu-trigger': 'angular-ellipsis-menu',
+                          'data-monitor-batch-more-menu-trigger-owner': 'hertzbeat-ui-icon-button'
+                        } as React.ComponentProps<typeof HzBatchToolbar>['overflowButtonProps']}
+                        overflowPanelProps={{
+                          'data-monitor-batch-more-menu-panel': 'angular-nz-dropdown-menu',
+                          'data-monitor-batch-more-menu-clearance': 'floating-overlay-no-table-crop',
+                          'data-monitor-batch-more-menu-panel-owner': 'hertzbeat-ui-batch-toolbar'
+                        } as React.ComponentProps<typeof HzBatchToolbar>['overflowPanelProps']}
+                        actions={[
+                          {
+                            id: 'select-all-results',
+                            label: t('common.select-all-results'),
+                            busy: pendingActionId === 'select-all-results',
+                            busyLabel: t('common.loading'),
+                            disabled: displayTotal === 0 || Boolean(pendingActionId && pendingActionId !== 'select-all-results'),
+                            onSelect: () => void handleSelectAllResults(),
+                            buttonProps: monitorBatchActionProps('select-all-results')
+                          },
+                          {
+                            id: 'select-page',
+                            label: t('common.select-page'),
+                            disabled: currentPageIds.length === 0 || pendingActionId !== null,
+                            onSelect: handleSelectVisible,
+                            buttonProps: monitorBatchActionProps('select-page')
+                          },
+                          {
+                            id: 'clear-selection',
+                            label: t('common.clear-selection'),
+                            disabled: checkedCount === 0 || pendingActionId !== null,
+                            onSelect: handleClearSelection,
+                            buttonProps: monitorBatchActionProps('clear-selection')
+                          },
+                          {
+                            id: 'enable',
+                            label: t('monitors.controls.enable-selected'),
+                            presentation: 'menu',
+                            busy: pendingActionId === 'enable',
+                            busyLabel: t('common.notify.enable-pending'),
+                            disabled: Boolean(pendingActionId && pendingActionId !== 'enable'),
+                            onSelect: () => openBatchResponseConfirm('enable'),
+                            buttonProps: {
+                              ...monitorBatchActionProps('enable'),
+                              'data-monitor-batch-response-filter': 'angular-status-filtered-selection',
+                              'data-monitor-batch-response-eligible-status': 'paused',
+                              'data-monitor-batch-more-menu-action': 'enable'
+                            } as React.ButtonHTMLAttributes<HTMLButtonElement>
+                          },
+                          {
+                            id: 'pause',
+                            label: t('monitors.controls.pause-selected'),
+                            presentation: 'menu',
+                            busy: pendingActionId === 'pause',
+                            busyLabel: t('common.notify.cancel-pending'),
+                            disabled: Boolean(pendingActionId && pendingActionId !== 'pause'),
+                            onSelect: () => openBatchResponseConfirm('pause'),
+                            buttonProps: {
+                              ...monitorBatchActionProps('pause'),
+                              'data-monitors-pause-selected-action': 'true',
+                              'data-monitor-batch-response-filter': 'angular-status-filtered-selection',
+                              'data-monitor-batch-response-eligible-status': 'active',
+                              'data-monitor-batch-more-menu-action': 'pause'
+                            } as React.ButtonHTMLAttributes<HTMLButtonElement>
+                          },
+                          ...(!entityContextActive
+                            ? [
+                                {
+                                  id: 'import',
+                                  label: t('common.import'),
+                                  presentation: 'menu' as const,
+                                  busy: pendingActionId === 'import',
+                                  busyLabel: t('common.notify.import-pending'),
+                                  disabled: Boolean(pendingActionId && pendingActionId !== 'import'),
+                                  onSelect: handleImportClick,
+                                  buttonProps: {
+                                    ...monitorBatchActionProps('import'),
+                                    'data-monitor-batch-more-menu-action': 'import'
+                                  } as React.ButtonHTMLAttributes<HTMLButtonElement>
+                                }
+                              ]
+                            : []),
+                          ...(!entityContextActive
+                            ? [
+                                {
+                                  id: 'export-selected',
+                                  label: t('monitor.export'),
+                                  presentation: 'menu' as const,
+                                  busy: pendingActionId === 'export-json' || pendingActionId === 'export-excel',
+                                  busyLabel: t('common.notify.export-pending'),
+                                  disabled: Boolean(
+                                    pendingActionId && pendingActionId !== 'export-json' && pendingActionId !== 'export-excel'
+                                  ),
+                                  onSelect: () => openExportDialog('selected'),
+                                  buttonProps: {
+                                    ...monitorBatchActionProps('export-selected'),
+                                    'data-monitors-export-type-trigger': 'selected',
+                                    'data-monitor-export-type-trigger-owner': 'hertzbeat-ui-batch-toolbar',
+                                    'data-monitor-batch-more-menu-action': 'export-selected'
+                                  } as React.ButtonHTMLAttributes<HTMLButtonElement>
+                                },
+                                {
+                                  id: 'export-all',
+                                  label: t('monitor.export-all'),
+                                  presentation: 'menu' as const,
+                                  busy: pendingActionId === 'export-all-json' || pendingActionId === 'export-all-excel',
+                                  busyLabel: t('common.notify.export-pending'),
+                                  disabled: Boolean(
+                                    pendingActionId && pendingActionId !== 'export-all-json' && pendingActionId !== 'export-all-excel'
+                                  ),
+                                  onSelect: () => openExportDialog('all'),
+                                  buttonProps: {
+                                    ...monitorBatchActionProps('export-all'),
+                                    'data-monitors-export-type-trigger': 'all',
+                                    'data-monitor-export-type-trigger-owner': 'hertzbeat-ui-batch-toolbar',
+                                    'data-monitor-batch-more-menu-action': 'export-all'
+                                  } as React.ButtonHTMLAttributes<HTMLButtonElement>
+                                },
+                              ]
+                            : []),
+                          {
+                            id: 'delete',
+                            label: `${t('common.button.delete')} ${checkedCount > 0 ? `(${checkedCount})` : ''}`,
+                            presentation: 'menu',
+                            busy: pendingActionId === 'delete',
+                            busyLabel: t('common.notify.delete-pending'),
+                            tone: 'critical',
+                            disabled: Boolean(pendingActionId && pendingActionId !== 'delete'),
+                            onSelect: () => {
+                              if (!requireSelection(t('common.notify.no-select-delete'))) {
+                                return;
+                              }
+                              openDeleteDialog(null);
+                            },
+                            buttonProps: {
+                              ...monitorBatchActionProps('delete'),
+                              'data-monitors-delete-confirm-trigger': 'cold-modal',
+                              'data-monitor-batch-more-menu-action': 'delete'
+                            } as React.ButtonHTMLAttributes<HTMLButtonElement>
+                          }
+                        ]}
+                      />
+                      {actionFeedback ? (
+                        <div data-monitor-action-feedback={actionFeedback.tone}>
+                          <HzInlineFeedback
+                            tone={actionFeedback.tone}
+                            title={actionFeedback.title}
+                            description={actionFeedback.description}
+                            meta={pendingActionId ? t('common.loading') : undefined}
+                            data-monitor-action-feedback-owner="hertzbeat-ui-inline-feedback"
+                          />
+                        </div>
+                      ) : null}
+                      <div data-monitor-manage-list-owner="hertzbeat-ui-data-table">
+                        <HzDataTable<Monitor>
+                          columns={
+                            [
+                              {
+                                key: 'select',
+                                header: t('common.select'),
+                                width: '96px',
+                                render: item => (
+                                  <div onClick={event => event.stopPropagation()}>
+                                    <HzCheckbox
+                                      data-monitor-manage-selection-owner="hertzbeat-ui-checkbox"
+                                      data-monitors-row-select={String(item.id)}
+                                      data-monitor-manage-selection-disabled={isMonitorDisabled(item) ? 'disappeared' : undefined}
+                                      aria-label={t('common.select')}
+                                      disabled={isMonitorDisabled(item)}
+                                      checked={checkedIds.includes(item.id)}
+                                      onChange={event => {
+                                        if (isMonitorDisabled(item)) {
+                                          return;
+                                        }
+                                        const nextChecked = event.target.checked;
+                                        setCheckedIdsImmediate(prev => {
+                                          const next = resolveMonitorCheckboxSelection(prev, selectedId, item.id, nextChecked);
+                                          setSelectedId(next.selectedId);
+                                          return next.checkedIds;
+                                        });
+                                      }}
+                                    />
+                                  </div>
+                                )
+                              },
+                              {
+                                key: 'monitor',
+                                header: t('monitor.list'),
+                                render: item => {
+                                  const isDynamicScrape = Boolean(item.scrape && item.scrape !== 'static');
+                                  const rowCopy = isDynamicScrape ? t(`monitor.scrape.type.${item.scrape}`) : item.instance || emptyValue;
+                                  const canCopyInstance = Boolean(item.instance && !isDynamicScrape);
+                                  const entityResponseActionId = item.status === 0 ? `row-enable-${item.id}` : `row-pause-${item.id}`;
+                                  const entityResponseBusy = pendingActionId === entityResponseActionId;
+                                  const entityResponseDisabled =
+                                    isMonitorDisabled(item) ||
+                                    entityResponseBusy ||
+                                    item.id < 0 ||
+                                    Boolean(pendingActionId && pendingActionId !== entityResponseActionId);
+                                  const entityTriageReason = entityContextActive
+                                    ? item.status === 2
+                                      ? t('entity.monitor.workbench.reason.down')
+                                      : data.entityWorkbenchFallback
+                                        ? t('entity.monitor.workbench.reason.fallback')
+                                        : item.status === 0
+                                          ? t('entity.monitor.workbench.reason.paused')
+                                          : t('entity.monitor.workbench.reason.default')
+                                    : null;
+                                  const relatedLogsHref = buildMonitorRowSignalHref('logs', item, data.query);
+                                  const relatedTracesHref = buildMonitorRowSignalHref('traces', item, data.query);
+                                  const codeHref = buildMonitorRowCodeHref(item);
+                                  const labelEntries = Object.entries(item.labels || {}).slice(0, 3);
+                                  return (
+                                    <div className="min-w-0">
+                                      <HzDataCellText
+                                        variant="title"
+                                        display="block"
+                                        data-monitor-row-name-owner="hertzbeat-ui-data-cell-text"
+                                        data-monitor-row-optimistic-copy={item.id < 0 ? 'true' : undefined}
+                                      >
+                                        {item.name}
+                                      </HzDataCellText>
+                                      <div
+                                        className="mt-0.5 flex min-w-0 items-center gap-1"
+                                        data-monitor-row-instance-copy-layout="angular-host-copy"
+                                      >
+                                        <HzDataCellText
+                                          variant="copy"
+                                          display="block"
+                                          spacing="stack-tight"
+                                          data-monitor-row-copy-owner="hertzbeat-ui-data-cell-text"
+                                          data-monitor-row-copy={rowCopy}
+                                          data-monitor-row-scrape-display={isDynamicScrape ? 'angular-service-discovery' : undefined}
+                                          data-monitor-row-scrape-display-owner={isDynamicScrape ? 'hertzbeat-ui-data-cell-text' : undefined}
+                                          data-monitor-row-scrape-value={isDynamicScrape ? item.scrape : undefined}
+                                        >
+                                          {isDynamicScrape ? (
+                                            <>
+                                              <GitBranchIcon size={12} aria-hidden="true" data-monitor-row-scrape-icon="partition" />
+                                              {rowCopy}
+                                            </>
+                                          ) : rowCopy}
+                                        </HzDataCellText>
+                                        {canCopyInstance ? (
+                                          <HzIconButton
+                                            label={t('common.button.copy.tip')}
+                                            intent="ghost"
+                                            onClick={event => {
+                                              event.stopPropagation();
+                                              void handleCopyInstance(item.instance || '');
+                                            }}
+                                            data-monitor-row-instance-copy="angular-host-copy"
+                                            data-monitor-row-instance-copy-owner="hertzbeat-ui-icon-button"
+                                            data-monitor-row-instance-copy-target={item.instance}
+                                          >
+                                            <CopyIcon size={13} />
+                                          </HzIconButton>
+                                        ) : null}
+                                      </div>
+                                      {labelEntries.length > 0 ? (
+                                        <div
+                                          className="mt-1 flex min-w-0 flex-wrap gap-1"
+                                          data-monitor-row-labels-owner="hertzbeat-ui-label-tag"
+                                          data-monitor-row-label-color="angular-render-label-color"
+                                        >
+                                          {labelEntries.map(([key, value]) => {
+                                            const colorToken = renderAngularLabelColor(key);
+                                            return (
+                                              <HzLabelTag
+                                                key={`${key}:${String(value)}`}
+                                                colorToken={colorToken}
+                                                data-monitor-row-label-color-token={colorToken}
+                                              >
+                                                {key}:{String(value)}
+                                              </HzLabelTag>
+                                            );
+                                          })}
+                                        </div>
+                                      ) : null}
+                                      {entityTriageReason ? (
+                                        <HzDataCellText
+                                          variant="meta"
+                                          display="block"
+                                          spacing="stack"
+                                          casing="plain"
+                                          data-monitor-row-entity-triage="angular-entity-reason"
+                                          data-monitor-row-entity-triage-owner="hertzbeat-ui-data-cell-text"
+                                          data-monitor-row-entity-triage-status={
+                                            item.status === 2
+                                              ? 'down'
+                                              : data.entityWorkbenchFallback
+                                                ? 'fallback'
+                                                : item.status === 0
+                                                  ? 'paused'
+                                                  : 'default'
+                                          }
+                                        >
+                                          {entityTriageReason}
+                                        </HzDataCellText>
+                                      ) : null}
+                                      {entityContextActive ? (
+                                        <HzButton
+                                          size="xs"
+                                          intent="secondary"
+                                          aria-busy={entityResponseBusy ? 'true' : undefined}
+                                          disabled={entityResponseDisabled}
+                                          onClick={event => {
+                                            event.stopPropagation();
+                                            openRowResponseConfirm(item, item.status === 0 ? 'enable' : 'pause');
+                                          }}
+                                          data-monitor-row-entity-response-action="angular-inline-host-action"
+                                          data-monitor-row-entity-response-owner="hertzbeat-ui-button"
+                                          data-monitor-row-response-action={item.status === 0 ? 'enable' : 'pause'}
+                                          data-monitor-row-action-disabled={isMonitorDisabled(item) ? 'disappeared' : undefined}
+                                        >
+                                          {item.status === 0 ? <PlayCircle size={13} /> : <PauseCircle size={13} />}
+                                          {item.status === 0 ? t('monitor.enable') : t('monitor.cancel')}
+                                        </HzButton>
+                                      ) : null}
+                                      {entityContextActive ? (
+                                        <div
+                                          className="mt-2 flex min-w-0 flex-wrap gap-1.5"
+                                          data-monitor-row-entity-support-actions="angular-platform-support-action-bar"
+                                          data-monitor-row-entity-support-actions-owner="hertzbeat-ui-button-link"
+                                        >
+                                          <HzButtonLink
+                                            component={Link}
+                                            href={relatedLogsHref}
+                                            size="xs"
+                                            intent="secondary"
+                                            data-monitor-row-entity-support-action="related-logs"
+                                            data-monitor-row-entity-support-target={relatedLogsHref}
+                                          >
+                                            <FileText size={13} />
+                                            {t('entity.response.open-related-logs')}
+                                          </HzButtonLink>
+                                          <HzButtonLink
+                                            component={Link}
+                                            href={relatedTracesHref}
+                                            size="xs"
+                                            intent="secondary"
+                                            data-monitor-row-entity-support-action="related-traces"
+                                            data-monitor-row-entity-support-target={relatedTracesHref}
+                                          >
+                                            <Network size={13} />
+                                            {t('entity.response.open-related-traces')}
+                                          </HzButtonLink>
+                                          {codeHref ? (
+                                            <HzButtonLink
+                                              href={codeHref}
+                                              target="_blank"
+                                              rel="noopener noreferrer"
+                                              size="xs"
+                                              intent="secondary"
+                                              data-monitor-row-entity-support-action="code"
+                                              data-monitor-row-entity-support-target={codeHref}
+                                            >
+                                              <FileCode2 size={13} />
+                                              {t('entity.response.open-code')}
+                                            </HzButtonLink>
+                                          ) : (
+                                            <HzButton
+                                              size="xs"
+                                              intent="secondary"
+                                              disabled
+                                              data-monitor-row-entity-support-action="code"
+                                              data-monitor-row-entity-support-disabled="missing-code-navigation"
+                                            >
+                                              <FileCode2 size={13} />
+                                              {t('entity.response.open-code')}
+                                            </HzButton>
+                                          )}
+                                        </div>
+                                      ) : null}
+                                    </div>
+                                  );
+                                }
+                              },
+                              {
+                                key: 'type',
+                                header: t('monitor.app'),
+                                width: '156px',
+                                render: item => {
+                                  const appKey = item.app || '';
+                                  const appLabel = resolveMonitorTypeLabel(item.app);
+                                  const appFilterHref = appKey ? `/monitors?app=${encodeURIComponent(appKey)}` : null;
+                                  const cell = (
+                                    <HzDataCellText
+                                      variant="type"
+                                      data-monitor-row-type-owner="hertzbeat-ui-data-cell-text"
+                                      data-monitor-row-type={item.app || emptyValue}
+                                    >
+                                      {appLabel}
+                                    </HzDataCellText>
+                                  );
+
+                                  return appFilterHref ? (
+                                    <Link
+                                      href={appFilterHref}
+                                      className="inline-flex max-w-full"
+                                      data-monitor-row-app-filter-link="angular-app-tag-link"
+                                      data-monitor-row-app-filter-target={appFilterHref}
+                                    >
+                                      {cell}
+                                    </Link>
+                                  ) : cell;
+                                }
+                              },
+                              {
+                                key: 'status',
+                                header: t('common.status'),
+                                width: '132px',
+                                render: item => (
+                                  <HzStatusBadge
+                                    tone={monitorStatusTone(item.status)}
+                                    data-monitor-manage-status-tone={statusBadgeVariant(item.status)}
+                                  >
+                                    {statusLabel(item.status, t)}
+                                  </HzStatusBadge>
+                                )
+                              },
+                              {
+                                key: 'updated',
+                                header: t('common.updated'),
+                                width: '220px',
+                                render: item => {
+                                  const updatedTime = formatTime(item.gmtUpdate || item.gmtCreate || null);
+                                  return (
+                                    <HzDataCellText
+                                      variant="copy"
+                                      data-monitor-row-updated-owner="hertzbeat-ui-data-cell-text"
+                                      data-monitor-row-meta={updatedTime}
+                                    >
+                                      {updatedTime}
+                                    </HzDataCellText>
+                                  );
+                                }
+                              },
+                              {
+                                key: 'actions',
+                                header: t('common.operations'),
+                                width: '136px',
+                                render: item => {
+                                  const rowNavigationContext = {
+                                    ...navigationContext,
+                                    app: data.query.app || item.app || navigationContext.app
+                                  };
+                                  const detailHref = buildMonitorDetailHref(item.id, rowNavigationContext);
+                                  const editHref = buildMonitorEditHref(item.id, rowNavigationContext);
+                                  const copying = copyingIds.includes(item.id);
+                                  const rowResponseActionId = item.status === 0 ? `row-enable-${item.id}` : `row-pause-${item.id}`;
+                                  const rowResponseBusy = pendingActionId === rowResponseActionId;
+                                  const rowDisabled = isMonitorDisabled(item);
+                                  const rowActionMenuOpen = openRowActionMenuId === item.id;
+                                  return (
+                                    <div
+                                      className="relative flex justify-end"
+                                      data-monitor-row-actions="angular-ellipsis-dropdown"
+                                      data-monitor-row-actions-owner="hertzbeat-ui-table-row-action-button"
+                                      data-monitor-row-action-menu="angular-ellipsis-dropdown"
+                                      data-monitor-row-action-menu-open={rowActionMenuOpen ? 'true' : 'false'}
+                                      data-monitor-row-action-menu-layer="overlay-visible-above-table"
+                                      data-monitor-row-action-menu-clearance="floating-overlay-no-table-crop"
+                                      data-monitor-row-actions-entity-context={entityContextActive ? 'detail-edit-copy-only' : undefined}
+                                      onClick={event => event.stopPropagation()}
+                                    >
+                                      <HzIconButton
+                                        label={t('common.edit')}
+                                        intent="ghost"
+                                        disabled={rowDisabled}
+                                        aria-expanded={rowActionMenuOpen}
+                                        onClick={() => setOpenRowActionMenuId(rowActionMenuOpen ? null : item.id)}
+                                        data-monitor-row-action-menu-trigger="angular-ellipsis-dropdown"
+                                        data-monitor-row-action-menu-trigger-owner="hertzbeat-ui-icon-button"
+                                        data-monitor-row-action-menu-open={rowActionMenuOpen ? 'true' : 'false'}
+                                        data-monitor-row-action-disabled={rowDisabled ? 'disappeared' : undefined}
+                                      >
+                                        <MoreHorizontal size={14} />
+                                      </HzIconButton>
+                                      <div
+                                        hidden={!rowActionMenuOpen}
+                                        className="absolute right-0 top-7 z-30 grid min-w-[176px] gap-1 border border-[var(--hz-ui-line-strong)] bg-[var(--hz-ui-surface-raised)] p-1 shadow-[0_18px_42px_rgba(0,0,0,0.42)]"
+                                        data-monitor-row-action-menu-panel="angular-nz-dropdown-menu"
+                                        data-monitor-row-action-menu-panel-open={rowActionMenuOpen ? 'true' : 'false'}
+                                        data-monitor-row-action-menu-layer-panel="overlay-visible-above-table"
+                                        data-monitor-row-action-menu-clearance-panel="floating-overlay-no-table-crop"
+                                      >
+                                        {rowDisabled ? (
+                                          <HzTableRowActionButton
+                                            disabled
+                                            width="root-span"
+                                            data-monitors-open-detail-action="true"
+                                            data-monitor-row-action-owner="hertzbeat-ui-table-row-action-button"
+                                            data-monitor-row-action="detail"
+                                            data-monitor-row-action-disabled="disappeared"
+                                          >
+                                            <BarChart3 size={13} />
+                                            {t('monitor.detail')}
+                                          </HzTableRowActionButton>
+                                        ) : (
+                                          <HzButtonLink
+                                            component={Link}
+                                            href={detailHref}
+                                            size="xs"
+                                            intent="ghost"
+                                            layout="full"
+                                            onClick={() => setOpenRowActionMenuId(null)}
+                                            data-monitors-open-detail-action="true"
+                                            data-monitor-row-action-owner="hertzbeat-ui-button-link"
+                                            data-monitor-row-action="detail"
+                                          >
+                                            <BarChart3 size={13} />
+                                            {t('monitor.detail')}
+                                          </HzButtonLink>
+                                        )}
+                                        {rowDisabled ? (
+                                          <HzTableRowActionButton
+                                            disabled
+                                            width="root-span"
+                                            data-monitors-edit-action="true"
+                                            data-monitor-row-action-owner="hertzbeat-ui-table-row-action-button"
+                                            data-monitor-row-action="edit"
+                                            data-monitor-row-action-disabled="disappeared"
+                                          >
+                                            <Pencil size={13} />
+                                            {t('monitor.edit-monitor')}
+                                          </HzTableRowActionButton>
+                                        ) : (
+                                          <HzButtonLink
+                                            component={Link}
+                                            href={editHref}
+                                            size="xs"
+                                            intent="ghost"
+                                            layout="full"
+                                            onClick={() => setOpenRowActionMenuId(null)}
+                                            onFocus={() => warmMonitorEdit(item, editHref)}
+                                            onMouseEnter={() => warmMonitorEdit(item, editHref)}
+                                            onPointerDown={() => warmMonitorEdit(item, editHref)}
+                                            data-monitors-edit-action="true"
+                                            data-monitor-row-action-owner="hertzbeat-ui-button-link"
+                                            data-monitor-row-action="edit"
+                                          >
+                                            <Pencil size={13} />
+                                            {t('monitor.edit-monitor')}
+                                          </HzButtonLink>
+                                        )}
+                                        <HzTableRowActionButton
+                                          width="root-span"
+                                          aria-busy={copying ? 'true' : undefined}
+                                          disabled={rowDisabled || copying || item.id < 0}
+                                          onClick={() => {
+                                            setOpenRowActionMenuId(null);
+                                            void handleCopyMonitor(item);
+                                          }}
+                                          data-monitors-copy-action="true"
+                                          data-monitor-row-copy-lifecycle="angular-copy-success-refresh-unavailable-refresh"
+                                          data-monitor-row-copy-lifecycle-owner="hertzbeat-ui-table-row-action-button"
+                                          data-monitor-row-action-owner="hertzbeat-ui-table-row-action-button"
+                                          data-monitor-row-action="copy"
+                                          data-monitor-row-action-disabled={rowDisabled ? 'disappeared' : undefined}
+                                        >
+                                          <CopyIcon size={13} />
+                                          {t('monitor.copy.action')}
+                                        </HzTableRowActionButton>
+                                        {!entityContextActive && item.status === 0 ? (
+                                          <HzTableRowActionButton
+                                            width="root-span"
+                                            aria-busy={rowResponseBusy ? 'true' : undefined}
+                                            disabled={rowDisabled || rowResponseBusy || item.id < 0 || Boolean(pendingActionId && pendingActionId !== rowResponseActionId)}
+                                            onClick={() => {
+                                              setOpenRowActionMenuId(null);
+                                              openRowResponseConfirm(item, 'enable');
+                                            }}
+                                            data-monitor-row-response-action-owner="hertzbeat-ui-table-row-action-button"
+                                            data-monitor-row-response-action="enable"
+                                            data-monitor-row-action-disabled={rowDisabled ? 'disappeared' : undefined}
+                                          >
+                                            <PlayCircle size={13} />
+                                            {t('monitor.enable')}
+                                          </HzTableRowActionButton>
+                                        ) : !entityContextActive ? (
+                                          <HzTableRowActionButton
+                                            width="root-span"
+                                            aria-busy={rowResponseBusy ? 'true' : undefined}
+                                            disabled={rowDisabled || rowResponseBusy || item.id < 0 || Boolean(pendingActionId && pendingActionId !== rowResponseActionId)}
+                                            onClick={() => {
+                                              setOpenRowActionMenuId(null);
+                                              openRowResponseConfirm(item, 'pause');
+                                            }}
+                                            data-monitor-row-response-action-owner="hertzbeat-ui-table-row-action-button"
+                                            data-monitor-row-response-action="pause"
+                                            data-monitor-row-action-disabled={rowDisabled ? 'disappeared' : undefined}
+                                          >
+                                            <PauseCircle size={13} />
+                                            {t('monitor.cancel')}
+                                          </HzTableRowActionButton>
+                                        ) : null}
+                                        {!entityContextActive ? (
+                                          <HzTableRowActionButton
+                                            width="root-span"
+                                            intent="danger"
+                                            disabled={rowDisabled || item.id < 0 || Boolean(pendingActionId)}
+                                            onClick={() => {
+                                              setOpenRowActionMenuId(null);
+                                              openDeleteDialog([item.id]);
+                                            }}
+                                            data-monitor-row-delete-action-owner="hertzbeat-ui-table-row-action-button"
+                                            data-monitor-row-delete-action="single"
+                                            data-monitor-row-action-disabled={rowDisabled ? 'disappeared' : undefined}
+                                          >
+                                            <Trash2 size={13} />
+                                            {t('monitor.delete-monitor')}
+                                          </HzTableRowActionButton>
+                                        ) : null}
+                                      </div>
+                                    </div>
+                                  );
+                                }
+                              }
+                            ] satisfies HzDataColumn<Monitor>[]
+                          }
+                          rows={tableRows}
+                          getRowKey={item => item.id}
+                          selectedRowKey={selected?.id}
+                          getRowProps={item =>
+                            isMonitorDisabled(item)
+                              ? {
+                                  'aria-disabled': 'true',
+                                  'data-monitor-row-disabled': 'disappeared',
+                                  className: 'opacity-55'
+                                }
+                              : undefined
+                          }
+                          onRowClick={item => {
+                            if (isMonitorDisabled(item)) {
+                              return;
+                            }
+                            setSelectedId(item.id);
+                            router.push(buildMonitorDetailHref(item.id, navigationContext));
+                          }}
+                          emptyLabel={t('monitors.empty-results.title')}
+                          variant="embedded"
+                          data-monitor-manage-list-variant="embedded"
+                        />
+                      </div>
+                      <HzPaginationBar
+                        data-monitor-manage-pagination-owner="hertzbeat-ui-pagination-bar"
+                        summary={`${t('common.page')} ${resolvedPageIndex + 1} / ${totalPages} · ${t('monitor.center.console.result.total')} ${displayTotal}`}
+                        pageSizeLabel={t('common.page-size')}
+                        pageSizeValue={String(resolvedPageSize)}
+                        pageSizeOptions={[
+                          { value: '8', label: '8' },
+                          { value: '20', label: '20' },
+                          { value: '50', label: '50' }
+                        ]}
+                        pageSizeSelectProps={{
+                          'data-monitor-manage-pagination-select-owner': 'hertzbeat-ui-select'
+                        }}
+                        onPageSizeChange={value =>
+                          navigateWithQuery({
+                            ...data.query,
+                            pageIndex: '0',
+                            pageSize: value
+                          })
+                        }
+                        previousLabel={t('common.previous-page')}
+                        nextLabel={t('common.next-page')}
+                        pageJumpLabel={t('common.page')}
+                        pageJumpValue={String(resolvedPageIndex + 1)}
+                        pageJumpMax={totalPages}
+                        onPageJumpChange={handlePageJumpChange}
+                        pageJumpInputProps={{
+                          'data-monitor-manage-pagination-page-jump-owner': 'hertzbeat-ui-input',
+                          'data-monitor-manage-pagination-action': 'page-jump'
+                        }}
+                        previousDisabled={resolvedPageIndex <= 0}
+                        nextDisabled={resolvedPageIndex >= totalPages - 1}
+                        previousButtonProps={{
+                          'data-monitor-manage-pagination-action-owner': 'hertzbeat-ui-button',
+                          'data-monitor-manage-pagination-action': 'previous'
+                        }}
+                        nextButtonProps={{
+                          'data-monitor-manage-pagination-action-owner': 'hertzbeat-ui-button',
+                          'data-monitor-manage-pagination-action': 'next'
+                        }}
+                        onPrevious={() =>
+                          navigateWithQuery({
+                            ...data.query,
+                            pageIndex: String(Math.max(0, resolvedPageIndex - 1)),
+                            pageSize: String(resolvedPageSize)
+                          })
+                        }
+                        onNext={() =>
+                          navigateWithQuery({
+                            ...data.query,
+                            pageIndex: String(Math.min(totalPages - 1, resolvedPageIndex + 1)),
+                            pageSize: String(resolvedPageSize)
+                          })
+                        }
+                      />
+                    </div>
+                  )}
+                </section>
+              </div>
+            </HzExplorerFrame>
+            <div data-monitors-new-app-picker={appPickerOpen ? 'open' : 'closed'}>
+              <HzTypePickerDialog
+                open={appPickerOpen}
+                onClose={() => setAppPickerOpen(false)}
+                title={appPickerMode === 'filter' ? t('monitor.search.app') : t('monitor.new-monitor')}
+                description={appPickerLoading ? t('common.loading') : appPickerError || undefined}
+                categories={appPickerCategories}
+                selectedId={data.query.app || undefined}
+                search={appPickerSearch}
+                onSearchChange={setAppPickerSearch}
+                onSelect={selectNewMonitorApp}
+                searchInputProps={
+                  {
+                    'data-monitor-app-picker-search-owner': 'hertzbeat-ui-input',
+                    'data-monitor-app-picker-search-action': 'filter',
+                    'data-monitors-app-picker-search-input': 'true'
+                  } as React.ComponentProps<typeof HzTypePickerDialog>['searchInputProps']
+                }
+                labels={{
+                  close: t('monitors.app-picker.close'),
+                  catalogTitle: t('monitors.app-picker.catalog-title'),
+                  templatePicker: {
+                    itemCount: total => `${total} ${t('monitors.app-picker.item-count')}`,
+                    searchPlaceholder: t('monitors.app-picker.search-placeholder'),
+                    empty: t('monitors.app-picker.empty')
+                  }
+                }}
+              />
+            </div>
+            <HzExportTypeDialog
+              open={exportDialogScope !== null}
+              title={t('monitor.export.switch-type')}
+              description={
+                exportDialogScope === 'selected'
+                  ? t('monitor.export.use-type', { type: `${checkedCount}` })
+                  : t('monitor.export-all.use-type', { type: `${displayTotal}` })
+              }
+              scope={exportDialogScope ?? 'selected'}
+              selectedCount={checkedCount}
+              closeLabel={t('common.button.cancel')}
+              onClose={() => setExportDialogScope(null)}
+              onSelect={type => void handleExportDialogSelect(type)}
+              jsonBusy={pendingActionId === (exportDialogScope === 'all' ? 'export-all-json' : 'export-json')}
+              excelBusy={pendingActionId === (exportDialogScope === 'all' ? 'export-all-excel' : 'export-excel')}
+              jsonDescription={
+                exportDialogScope === 'all'
+                  ? t('monitor.export-all.use-type', { type: 'JSON' })
+                  : t('monitor.export.use-type', { type: 'JSON' })
+              }
+              excelDescription={
+                exportDialogScope === 'all'
+                  ? t('monitor.export-all.use-type', { type: 'EXCEL' })
+                  : t('monitor.export.use-type', { type: 'EXCEL' })
+              }
+              data-monitor-export-type-dialog-owner="hertzbeat-ui-export-type-dialog"
+              data-monitor-export-type-dialog-contract="angular-nz-modal-600-no-footer"
+              data-monitor-export-type-dialog-flow="angular-trigger-type-modal-before-download"
+              data-monitors-export-type-dialog={exportDialogScope ?? 'closed'}
+              jsonButtonProps={
+                {
+                  'data-monitor-export-type-option-owner': 'hertzbeat-ui-export-type-dialog',
+                  'data-monitors-export-type-option': 'json'
+                } as React.ComponentProps<typeof HzExportTypeDialog>['jsonButtonProps']
+              }
+              excelButtonProps={
+                {
+                  'data-monitor-export-type-option-owner': 'hertzbeat-ui-export-type-dialog',
+                  'data-monitors-export-type-option': 'excel'
+                } as React.ComponentProps<typeof HzExportTypeDialog>['excelButtonProps']
+              }
+            />
+            <HzConfirmDialog
+              open={Boolean(batchResponseConfirm)}
+              onClose={() => {
+                if (batchResponseConfirmBusy) return;
+                setBatchResponseConfirm(null);
+              }}
+              kicker={t('menu.monitor.center')}
+              title={batchResponseConfirm?.action === 'enable' ? t('common.confirm.enable-batch') : t('common.confirm.cancel-batch')}
+              tone="critical"
+              cancelLabel={t('common.button.cancel')}
+              confirmLabel={t('common.button.ok')}
+              onConfirm={() => void handleConfirmBatchResponse()}
+              confirmDisabled={!batchResponseConfirm || batchResponseConfirmBusy}
+              bodyRhythm="stack"
+              data-monitor-batch-response-confirm="angular-modal-confirm"
+              data-monitor-batch-response-confirm-owner="hertzbeat-ui-confirm-dialog"
+              data-monitor-batch-response-confirm-action={batchResponseConfirm?.action ?? 'closed'}
+              data-monitor-batch-response-confirm-count={batchResponseConfirm?.ids.length ?? 0}
+              data-monitor-batch-response-confirm-closable="angular-nz-closable-false"
+              data-monitor-batch-response-confirm-ok="angular-nz-ok-danger-primary"
+            >
+              <div data-monitor-batch-response-confirm-body="angular-batch-confirm">
+                <HzInlineFeedback
+                  tone="critical"
+                  title={t('monitors.selected-count', { count: batchResponseConfirm?.ids.length ?? 0 })}
+                  data-monitor-batch-response-confirm-selected-owner="hertzbeat-ui-inline-feedback"
+                />
+              </div>
+            </HzConfirmDialog>
+            <HzConfirmDialog
+              open={Boolean(rowResponseConfirm)}
+              onClose={() => {
+                if (rowResponseConfirmBusy) return;
+                setRowResponseConfirm(null);
+              }}
+              kicker={t('menu.monitor.center')}
+              title={rowResponseConfirm?.action === 'enable' ? t('common.confirm.enable') : t('common.confirm.cancel')}
+              tone="critical"
+              cancelLabel={t('common.button.cancel')}
+              confirmLabel={t('common.button.ok')}
+              onConfirm={() => void handleConfirmRowResponse()}
+              confirmDisabled={!rowResponseConfirm || rowResponseConfirmBusy}
+              bodyRhythm="stack"
+              data-monitor-row-response-confirm="angular-modal-confirm"
+              data-monitor-row-response-confirm-owner="hertzbeat-ui-confirm-dialog"
+              data-monitor-row-response-confirm-action={rowResponseConfirm?.action ?? 'closed'}
+              data-monitor-row-response-confirm-target={rowResponseConfirm?.monitor.id ?? 'none'}
+              data-monitor-row-response-confirm-closable="angular-nz-closable-false"
+              data-monitor-row-response-confirm-ok="angular-nz-ok-danger-primary"
+            >
+              <div data-monitor-row-response-confirm-body="angular-single-row-confirm">
+                <HzInlineFeedback
+                  tone="critical"
+                  title={rowResponseConfirm?.monitor.name || rowResponseConfirm?.monitor.instance || t('monitors.title')}
+                  data-monitor-row-response-confirm-selected-owner="hertzbeat-ui-inline-feedback"
+                />
+              </div>
+            </HzConfirmDialog>
+            <HzConfirmDialog
+              open={deleteDialogOpen}
+              onClose={() => {
+                setDeleteDialogOpen(false);
+                setDeleteTargetIds(null);
+              }}
+              kicker={t('menu.monitor.center')}
+              title={t('monitors.delete.title')}
+              tone="critical"
+              cancelLabel={t('common.button.cancel')}
+              confirmLabel={t('monitors.delete.confirm')}
+              onConfirm={() => void handleConfirmDelete()}
+              confirmDisabled={deleteDialogCount === 0}
+              bodyRhythm="stack"
+              data-monitor-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+              data-monitor-delete-confirm-closable="angular-nz-closable-false"
+              data-monitor-delete-confirm-ok="angular-nz-ok-danger-primary"
+            >
+              <div data-monitors-delete-confirm="cold-modal">
+                <p>{t('monitors.delete.copy')}</p>
+                <HzInlineFeedback
+                  tone="critical"
+                  title={t('monitors.delete.selected', { count: deleteDialogCount })}
+                  data-monitor-delete-confirm-selected-owner="hertzbeat-ui-inline-feedback"
+                />
+              </div>
+            </HzConfirmDialog>
+          </>
+        );
+        }}
+      </ClientWorkbench>
+    </div>
+  );
+}
diff --git a/web-next/app/monitors/not-found.tsx b/web-next/app/monitors/not-found.tsx
index 24e09c26f2..105ceeeb86 100644
--- a/web-next/app/monitors/not-found.tsx
+++ b/web-next/app/monitors/not-found.tsx
@@ -1,5 +1,6 @@
 import { redirect } from 'next/navigation';
+import { buildMonitorListCompatRouteUrl } from '../../lib/monitor-manage/navigation';
 
 export default function MonitorsNotFound() {
-  redirect('/monitors');
+  redirect(buildMonitorListCompatRouteUrl());
 }
diff --git a/web-next/app/monitors/page.test.tsx b/web-next/app/monitors/page.test.tsx
index f57f19f320..4432712d50 100644
--- a/web-next/app/monitors/page.test.tsx
+++ b/web-next/app/monitors/page.test.tsx
@@ -1,15 +1,52 @@
+// @vitest-environment jsdom
+
 import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
-import React from 'react';
+import React, { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 const apiMessageGet = vi.fn();
+const apiMessagePost = vi.fn();
+const apiMessageDelete = vi.fn();
+const apiDownload = vi.fn();
+
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+const EMPTY_MONITOR_TEST_QUERY = {
+  search: '',
+  app: '',
+  labels: '',
+  status: '',
+  pageIndex: '',
+  pageSize: '',
+  entityId: '',
+  entityName: '',
+  returnTo: ''
+};
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
+  initialQuery: {
+    search: '',
+    app: '',
+    labels: '',
+    status: '',
+    pageIndex: '',
+    pageSize: '',
+    entityId: '',
+    entityName: '',
+    returnTo: ''
+  },
+  explicitStatus: '',
   replace: vi.fn(),
+  push: vi.fn(),
+  prefetch: vi.fn(),
+  queryClient: {
+    fetchQuery: vi.fn(async ({ queryFn }: { queryFn: () => Promise<unknown> }) => queryFn()),
+    invalidateQueries: vi.fn()
+  },
   lastLoad: null as null | (() => Promise<unknown>),
   renderData: null as any
 }));
@@ -25,12 +62,13 @@ vi.mock('next/link', () => ({
 vi.mock('next/navigation', () => ({
   useRouter: () => ({
     replace: mockState.replace,
-    push: vi.fn()
-  }),
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
+    push: mockState.push,
+    prefetch: mockState.prefetch
+  })
+}));
+
+vi.mock('@tanstack/react-query', () => ({
+  useQueryClient: () => mockState.queryClient
 }));
 
 vi.mock('@/components/providers/i18n-provider', () => ({
@@ -42,10 +80,12 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
     const data =
@@ -68,21 +108,11 @@ vi.mock('@/components/workbench/client-workbench', () => ({
           pageSize: 8,
           totalElements: 1
         },
-        query: {
-          search: mockState.searchParams.get('search') || '',
-          app: mockState.searchParams.get('app') || '',
-          labels: mockState.searchParams.get('labels') || '',
-          status: mockState.searchParams.get('status') || '',
-          pageIndex: mockState.searchParams.get('pageIndex') || '',
-          pageSize: mockState.searchParams.get('pageSize') || '',
-          entityId: mockState.searchParams.get('entityId') || '',
-          entityName: mockState.searchParams.get('entityName') || '',
-          returnTo: mockState.searchParams.get('returnTo') || ''
-        },
+        query: mockState.initialQuery,
         entityWorkbenchFallback: false
       };
     return (
-      <div data-client-workbench="true">
+      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
         {children(data)}
       </div>
     );
@@ -149,6 +179,8 @@ vi.mock('@/components/observability', () => ({
       {rows.map((row: any) => (
         <div key={row.key}>
           <div>{row.title}</div>
+          <div data-monitor-row-copy={row.copy}>{row.copy}</div>
+          <div data-monitor-row-meta={row.meta}>{row.meta}</div>
           {row.extra}
         </div>
       ))}
@@ -220,7 +252,15 @@ vi.mock('@/components/workbench/toolbar', () => ({
 
 vi.mock('@/components/workbench/workbench-page', () => ({
   MetricGrid: ({ items }: any) => <div data-metric-grid="true">{items.map((item: any) => item.label).join('|')}</div>,
-  RowList: ({ rows }: any) => <div data-row-list="true">{rows.map((row: any) => row.title).join('|')}</div>,
+  RowList: ({ rows }: any) => (
+    <div data-row-list="true">
+      {rows.map((row: any) => (
+        <div key={row.title} data-row-list-title={row.title} data-row-list-copy={row.copy} data-row-list-meta={row.meta}>
+          {`${row.title}:${row.copy}:${row.meta}`}
+        </div>
+      ))}
+    </div>
+  ),
   WorkbenchPage: ({ title, subtitle, actions, main, side }: any) => (
     <div data-workbench-page="true">
       <h1>{title}</h1>
@@ -233,14 +273,43 @@ vi.mock('@/components/workbench/workbench-page', () => ({
 }));
 
 vi.mock('@/lib/monitor-manage/controller', () => ({
+  buildCopyMonitorUrl: vi.fn((monitorId: number | string) => `/monitor/copy/${monitorId}`),
+  buildDeleteGrafanaDashboardUrl: vi.fn((monitorId: number | string) => `/grafana/dashboard?monitorId=${monitorId}`),
+  buildDeleteMonitorsUrl: vi.fn((ids: Array<number | string> = []) => {
+    const params = new URLSearchParams();
+    ids.forEach(id => params.append('ids', String(id)));
+    return `/monitors?${params.toString()}`;
+  }),
+  buildEnableMonitorsUrl: vi.fn((ids: Array<number | string> = []) => {
+    const params = new URLSearchParams();
+    ids.forEach(id => params.append('ids', String(id)));
+    return `/monitors/manage?${params.toString()}`;
+  }),
   buildExportAllMonitorsUrl: vi.fn(() => '/monitors/export/all?type=JSON'),
-  buildExportMonitorsUrl: vi.fn(() => '/monitors/export?type=JSON'),
+  buildExportMonitorsUrl: vi.fn((ids: Array<number | string> = [], type: 'JSON' | 'EXCEL' = 'JSON') => {
+    const params = new URLSearchParams();
+    ids.forEach(id => params.append('ids', String(id)));
+    params.append('type', type);
+    return `/monitors/export?${params.toString()}`;
+  }),
   buildImportMonitorsUrl: vi.fn(() => '/monitors/import'),
+  buildPauseMonitorsUrl: vi.fn((ids: Array<number | string> = []) => {
+    const params = new URLSearchParams();
+    ids.forEach(id => params.append('ids', String(id)));
+    params.append('type', 'JSON');
+    return `/monitors/manage?${params.toString()}`;
+  }),
   copyMonitor: vi.fn(),
   deleteGrafanaDashboard: vi.fn(),
   deleteMonitors: vi.fn(),
   enableMonitor: vi.fn(),
   enableMonitors: vi.fn(),
+  importMonitorsFromFacade: vi.fn((writeImportMonitors: any, file: File) => {
+    const formData = new FormData();
+    formData.append('file', file);
+    return writeImportMonitors(formData);
+  }),
+  loadMonitorListFromFacade: vi.fn((readMonitorList: any, query: any) => readMonitorList(query)),
   pauseMonitor: vi.fn(),
   pauseMonitors: vi.fn(),
   resolveDownloadFilename: vi.fn(() => 'monitors.json')
@@ -253,6 +322,8 @@ vi.mock('@/lib/monitor-manage/display-mapping', () => ({
       copy: value,
       meta: 'label'
     })),
+  monitorStatusTone: (status?: number) => (status === 2 ? 'critical' : status === 1 ? 'success' : 'neutral'),
+  statusBadgeVariant: (status?: number) => (status === 2 ? 'danger' : status === 1 ? 'success' : 'default'),
   statusLabel: (status: number) => (status === 2 ? 'Down' : status === 1 ? 'Up' : 'Paused')
 }));
 
@@ -268,7 +339,11 @@ vi.mock('@/lib/monitor-manage/navigation', () => ({
         ? `/entities/${context.entityId}`
         : '/entities',
   buildMonitorNewHref: (context?: any) =>
-    context?.entityId ? `/monitors/new?entityId=${context.entityId}` : '/monitors/new',
+    context?.app
+      ? `/monitors/new?app=${context.app}`
+      : context?.entityId
+        ? `/monitors/new?entityId=${context.entityId}`
+        : '/monitors/new',
   resolveMonitorCheckboxSelection: (checkedIds: number[], _selectedId: number | null, monitorId: number, checked: boolean) => ({
     checkedIds: checked ? Array.from(new Set([...checkedIds, monitorId])) : checkedIds.filter(id => id !== monitorId),
     selectedId: monitorId
@@ -322,9 +397,10 @@ vi.mock('@/lib/monitor-manage/view-model', () => ({
 }));
 
 vi.mock('@/lib/api-client', () => ({
-  apiMessageDelete: vi.fn(),
+  apiDownload,
+  apiMessageDelete,
   apiMessageGet,
-  apiMessagePost: vi.fn(),
+  apiMessagePost,
   getAuthorizationToken: vi.fn(() => ''),
   getCurrentLocale: vi.fn(() => 'en-US')
 }));
@@ -334,181 +410,2879 @@ vi.mock('@/lib/format', () => ({
 }));
 
 beforeEach(() => {
-  mockState.searchParams = new URLSearchParams();
+  mockState.initialQuery = { ...EMPTY_MONITOR_TEST_QUERY };
+  mockState.explicitStatus = '';
   mockState.replace.mockReset();
+  mockState.push.mockReset();
+  mockState.prefetch.mockReset();
+  mockState.queryClient.fetchQuery.mockReset();
+  mockState.queryClient.fetchQuery.mockImplementation(async ({ queryFn }: { queryFn: () => Promise<unknown> }) => queryFn());
+  mockState.queryClient.invalidateQueries.mockReset();
   mockState.lastLoad = null;
   mockState.renderData = null;
   apiMessageGet.mockReset();
+  apiMessagePost.mockReset();
+  apiMessagePost.mockResolvedValue(undefined);
+  apiMessageDelete.mockReset();
+  apiMessageDelete.mockResolvedValue(undefined);
+  apiDownload.mockReset();
+  apiDownload.mockImplementation((path: string) =>
+    fetch(path.startsWith('/api') ? path : `/api${path}`, {
+      headers: { 'Accept-Language': 'en-US' },
+      credentials: 'same-origin',
+      cache: 'no-store'
+    })
+  );
 });
 
+function buildMonitorTestQuery(overrides: Partial<typeof EMPTY_MONITOR_TEST_QUERY> = {}) {
+  const query = { ...EMPTY_MONITOR_TEST_QUERY, ...overrides };
+  if (!query.status && (query.entityId || query.entityName || query.returnTo)) {
+    query.status = '2';
+  }
+  return query;
+}
+
+async function renderMonitorsPage(
+  overrides: Partial<typeof EMPTY_MONITOR_TEST_QUERY> = {},
+  explicitStatus = ''
+) {
+  mockState.initialQuery = buildMonitorTestQuery(overrides);
+  mockState.explicitStatus = explicitStatus;
+  const { default: MonitorsPage } = await import('./monitor-manage-page');
+  const initialQuery = mockState.initialQuery;
+  return renderToStaticMarkup(<MonitorsPage initialQuery={initialQuery} explicitStatus={explicitStatus} />);
+}
+
 describe('monitors page', () => {
-  it('defaults entity workbench routes to down monitors and swaps create/import for a return action', async () => {
-    mockState.searchParams = new URLSearchParams(
-      'app=website&entityId=42&entityName=Checkout%20Service&returnTo=%2Fentities%2F42%3FreturnLabel%3DReturn%2520to%2520entity&returnLabel=Return%20to%20entity'
-    );
-    apiMessageGet.mockResolvedValueOnce({
-      content: [],
-      pageIndex: 0,
-      pageSize: 8,
-      totalElements: 0
+  let interactionContainer: HTMLDivElement | null = null;
+  let interactionRoot: Root | null = null;
+
+  afterEach(async () => {
+    await act(async () => {
+      interactionRoot?.unmount();
+      await Promise.resolve();
     });
+    interactionRoot = null;
+    interactionContainer?.remove();
+    interactionContainer = null;
+    vi.useRealTimers();
+    vi.unstubAllGlobals();
+  });
 
-    const { default: MonitorsPage } = await import('./page');
-    const html = renderToStaticMarkup(<MonitorsPage />);
-    await mockState.lastLoad?.();
+  it('opens the old Angular monitor type picker instead of defaulting new monitor to the selected row app', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: { team: 'platform' },
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 3,
+        pageSize: 20,
+        totalElements: 61
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '3', pageSize: '20' },
+      entityWorkbenchFallback: false
+    };
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
 
-    expect(apiMessageGet).toHaveBeenCalledWith(
-      '/monitors?pageIndex=0&pageSize=8&app=website&status=2&entityId=42&entityName=Checkout+Service&returnTo=%2Fentities%2F42'
-    );
-    expect(html).toContain('Checkout Service');
-    expect(html).not.toContain('Return to entity');
-    expect(html).toContain('href="/entities/42"');
-    expect(html).not.toContain('New monitor');
-    expect(html).not.toContain('Import');
-  });
+    await act(async () => {
+      interactionRoot?.render(
+        <MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '3', pageSize: '20' }} />
+      );
+      await Promise.resolve();
+    });
 
-  it('keeps the entity return action on an internal entity route when returnTo is unsafe', async () => {
-    mockState.searchParams = new URLSearchParams(
-      'app=website&entityId=42&entityName=Checkout%20Service&returnTo=%2F%2Fevil.example%2Fsteal-session&returnLabel=Return%20to%20entity'
-    );
-    apiMessageGet.mockResolvedValueOnce({
-      content: [],
-      pageIndex: 0,
-      pageSize: 8,
-      totalElements: 0
+    const rowCheckbox = interactionContainer.querySelector('input[data-monitors-row-select="501"]') as HTMLInputElement | null;
+    const refreshButton = interactionContainer.querySelector(
+      'button[data-monitor-manage-manual-refresh-action="sync"]'
+    ) as HTMLButtonElement | null;
+    expect(rowCheckbox).not.toBeNull();
+    expect(refreshButton).not.toBeNull();
+    expect(refreshButton?.getAttribute('data-monitor-manage-manual-refresh-owner')).toBe('hertzbeat-ui-icon-button');
+    expect(refreshButton?.getAttribute('data-monitor-manage-manual-refresh-tick')).toBe('0');
+
+    await act(async () => {
+      rowCheckbox?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
     });
+    expect(rowCheckbox?.checked).toBe(true);
 
-    const { default: MonitorsPage } = await import('./page');
-    const html = renderToStaticMarkup(<MonitorsPage />);
+    await act(async () => {
+      refreshButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
 
-    expect(html).toContain('Checkout Service');
-    expect(html).not.toContain('Return to entity');
-    expect(html).toContain('href="/entities/42"');
-    expect(html).not.toContain('href="//evil.example/steal-session"');
-  });
+    const refreshedButton = interactionContainer.querySelector(
+      'button[data-monitor-manage-manual-refresh-action="sync"]'
+    ) as HTMLButtonElement | null;
+    expect(refreshedButton?.getAttribute('data-monitor-manage-manual-refresh-tick')).toBe('1');
+    expect(rowCheckbox?.checked).toBe(false);
+    expect(mockState.replace).not.toHaveBeenCalled();
+  }, 15000);
 
-  it('falls back entity workbench routes from down-only mode to all monitors when the abnormal pass is empty', async () => {
-    mockState.searchParams = new URLSearchParams(
-      'app=website&entityId=42&entityName=Checkout%20Service&returnTo=%2Fentities%2F42%3FreturnLabel%3DReturn%2520to%2520entity&returnLabel=Return%20to%20entity'
-    );
-    apiMessageGet
-      .mockResolvedValueOnce({
-        content: [],
+  it('supports the old Angular simple pagination page index change workflow', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: { team: 'platform' },
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 1,
+        pageSize: 20,
+        totalElements: 61
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '1', pageSize: '20' },
+      entityWorkbenchFallback: false
+    };
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '1', pageSize: '20' }} />
+      );
+      await Promise.resolve();
+    });
+
+    const pageJumpInput = interactionContainer.querySelector(
+      'input[data-monitor-manage-pagination-action="page-jump"]'
+    ) as HTMLInputElement | null;
+    expect(pageJumpInput).not.toBeNull();
+    expect(pageJumpInput?.getAttribute('data-hz-pagination-action')).toBe('page-jump');
+    expect(pageJumpInput?.getAttribute('data-monitor-manage-pagination-page-jump-owner')).toBe('hertzbeat-ui-input');
+    expect(pageJumpInput?.value).toBe('2');
+
+    const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
+    await act(async () => {
+      valueSetter?.call(pageJumpInput, '4');
+      pageJumpInput?.dispatchEvent(new Event('input', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.replace).toHaveBeenCalledWith('/monitors?search=prod&app=mysql&pageIndex=3&pageSize=20');
+  }, 15000);
+
+  it('wires compact row actions to detail/edit hrefs and copy mutation', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: { team: 'platform' },
+            gmtUpdate: 1713200000000
+          }
+        ],
         pageIndex: 0,
         pageSize: 8,
-        totalElements: 0
-      })
-      .mockResolvedValueOnce({
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const controller = await import('@/lib/monitor-manage/controller');
+    apiMessageDelete.mockClear();
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const detailLink = interactionContainer.querySelector(
+      'a[data-monitors-open-detail-action="true"]'
+    ) as HTMLAnchorElement | null;
+    const editLink = interactionContainer.querySelector(
+      'a[data-monitors-edit-action="true"]'
+    ) as HTMLAnchorElement | null;
+    const appFilterLink = interactionContainer.querySelector(
+      'a[data-monitor-row-app-filter-link="angular-app-tag-link"]'
+    ) as HTMLAnchorElement | null;
+    const labelColorGroup = interactionContainer.querySelector(
+      '[data-monitor-row-label-color="angular-render-label-color"]'
+    ) as HTMLElement | null;
+    const labelTag = interactionContainer.querySelector(
+      '[data-monitor-row-label-color-token]'
+    ) as HTMLElement | null;
+    const copyButton = interactionContainer.querySelector(
+      'button[data-monitors-copy-action="true"]'
+    ) as HTMLButtonElement | null;
+    const pauseButton = interactionContainer.querySelector(
+      'button[data-monitor-row-response-action="pause"]'
+    ) as HTMLButtonElement | null;
+    const deleteButton = interactionContainer.querySelector(
+      'button[data-monitor-row-delete-action="single"]'
+    ) as HTMLButtonElement | null;
+    expect(detailLink).not.toBeNull();
+    expect(detailLink?.getAttribute('href')).toBe('/monitors/501');
+    expect(editLink).not.toBeNull();
+    expect(editLink?.getAttribute('href')).toBe('/monitors/501/edit');
+    expect(appFilterLink).not.toBeNull();
+    expect(appFilterLink?.getAttribute('href')).toBe('/monitors?app=mysql');
+    expect(appFilterLink?.getAttribute('data-monitor-row-app-filter-target')).toBe('/monitors?app=mysql');
+    expect(labelColorGroup).not.toBeNull();
+    expect(labelColorGroup?.getAttribute('data-monitor-row-labels-owner')).toBe('hertzbeat-ui-label-tag');
+    expect(labelTag?.getAttribute('data-hz-ui')).toBe('label-tag');
+    expect(labelTag?.textContent).toBe('team:platform');
+    expect(copyButton).not.toBeNull();
+    expect(copyButton?.getAttribute('data-monitor-row-copy-lifecycle')).toBe('angular-copy-success-refresh-unavailable-refresh');
+    expect(copyButton?.getAttribute('data-monitor-row-copy-lifecycle-owner')).toBe('hertzbeat-ui-table-row-action-button');
+    expect(pauseButton).not.toBeNull();
+    expect(deleteButton).not.toBeNull();
+
+    await act(async () => {
+      copyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(apiMessagePost).toHaveBeenCalledWith('/monitor/copy/501', null);
+
+    await act(async () => {
+      pauseButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(apiMessageDelete).not.toHaveBeenCalledWith('/monitors/manage?ids=501&type=JSON');
+    expect(interactionContainer.querySelector('[data-monitor-row-response-confirm="angular-modal-confirm"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Please confirm whether to cancel monitor!');
+
+    const rowConfirmButton = interactionContainer.querySelector(
+      '[data-monitor-row-response-confirm="angular-modal-confirm"] button[data-hz-confirm-action="confirm"]'
+    ) as HTMLButtonElement | null;
+
+    await act(async () => {
+      rowConfirmButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(apiMessageDelete).toHaveBeenCalledWith('/monitors/manage?ids=501&type=JSON');
+    expect(interactionContainer.textContent).toContain('Paused successfully');
+
+    await act(async () => {
+      deleteButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.querySelector('[data-monitor-delete-confirm-owner="hertzbeat-ui-confirm-dialog"]')).not.toBeNull();
+    expect(
+      interactionContainer
+        .querySelector('[data-monitor-delete-confirm-owner="hertzbeat-ui-confirm-dialog"]')
+        ?.getAttribute('data-monitor-delete-confirm-closable')
+    ).toBe('angular-nz-closable-false');
+    expect(
+      interactionContainer
+        .querySelector('[data-monitor-delete-confirm-owner="hertzbeat-ui-confirm-dialog"]')
+        ?.getAttribute('data-monitor-delete-confirm-ok')
+    ).toBe('angular-nz-ok-danger-primary');
+    expect(interactionContainer.textContent).toContain('1 monitors selected');
+
+    const confirmDeleteButton = interactionContainer.querySelector(
+      'button[data-hz-confirm-action="confirm"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      confirmDeleteButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(apiMessageDelete).toHaveBeenCalledWith('/monitors?ids=501');
+    expect(apiMessageDelete).toHaveBeenCalledWith('/grafana/dashboard?monitorId=501');
+  }, 15000);
+
+  it('moves back from an emptied last page after Angular-style monitor delete success', async () => {
+    mockState.initialQuery = buildMonitorTestQuery({ pageIndex: '1', pageSize: '8' });
+    mockState.renderData = {
+      list: {
         content: [
           {
-            id: 99,
-            name: 'checkout-healthy',
-            app: 'http',
-            instance: '10.0.0.99',
+            id: 909,
+            name: 'last-page-monitor',
+            app: 'mysql',
+            instance: 'db.internal:3306',
             status: 1,
             scrape: 'static',
             labels: {},
             gmtUpdate: 1713200000000
           }
         ],
-        pageIndex: 0,
+        pageIndex: 1,
         pageSize: 8,
-        totalElements: 1
-      });
+        totalElements: 9
+      },
+      query: mockState.initialQuery,
+      entityWorkbenchFallback: false
+    };
+    const controller = await import('@/lib/monitor-manage/controller');
+    apiMessageDelete.mockClear();
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
 
-    const { default: MonitorsPage } = await import('./page');
-    renderToStaticMarkup(<MonitorsPage />);
-    const loaded = await mockState.lastLoad?.();
-    mockState.renderData = loaded;
-    const html = renderToStaticMarkup(<MonitorsPage />);
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...mockState.initialQuery }} />);
+      await Promise.resolve();
+    });
 
-    expect(apiMessageGet).toHaveBeenNthCalledWith(
-      1,
-      '/monitors?pageIndex=0&pageSize=8&app=website&status=2&entityId=42&entityName=Checkout+Service&returnTo=%2Fentities%2F42'
-    );
-    expect(apiMessageGet).toHaveBeenNthCalledWith(
-      2,
-      '/monitors?pageIndex=0&pageSize=8&app=website&entityId=42&entityName=Checkout+Service&returnTo=%2Fentities%2F42'
-    );
-    expect(loaded).toMatchObject({
-      entityWorkbenchFallback: true,
-      query: {
-        status: ''
+    const deleteButton = interactionContainer.querySelector(
+      'button[data-monitor-row-delete-action="single"]'
+    ) as HTMLButtonElement | null;
+    expect(deleteButton).not.toBeNull();
+
+    await act(async () => {
+      deleteButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const confirmDeleteButton = interactionContainer.querySelector(
+      'button[data-hz-confirm-action="confirm"]'
+    ) as HTMLButtonElement | null;
+    expect(confirmDeleteButton).not.toBeNull();
+
+    await act(async () => {
+      confirmDeleteButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(apiMessageDelete).toHaveBeenCalledWith('/monitors?ids=909');
+    expect(apiMessageDelete).toHaveBeenCalledWith('/grafana/dashboard?monitorId=909');
+    expect(mockState.replace).toHaveBeenCalledWith('/monitors?pageIndex=0&pageSize=8');
+    expect(interactionContainer.querySelector('[data-monitor-manage-auto-refresh-tick="1"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('0 Selected / 9');
+  }, 15000);
+
+  it('closes the delete confirmation but keeps selection and page when Angular-style monitor delete fails', async () => {
+    mockState.initialQuery = buildMonitorTestQuery({ pageIndex: '1', pageSize: '8' });
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 909,
+            name: 'last-page-monitor',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 1,
+        pageSize: 8,
+        totalElements: 9
       },
+      query: mockState.initialQuery,
+      entityWorkbenchFallback: false
+    };
+    const controller = await import('@/lib/monitor-manage/controller');
+    apiMessageDelete.mockClear();
+    apiMessageDelete
+      .mockResolvedValueOnce(undefined)
+      .mockRejectedValueOnce(new Error('backend refused delete'));
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...mockState.initialQuery }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+    expect(interactionContainer.textContent).toContain('1 Selected / 9');
+
+    const deleteBatchButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="delete"]'
+    ) as HTMLButtonElement | null;
+    expect(deleteBatchButton).not.toBeNull();
+
+    await act(async () => {
+      deleteBatchButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+    expect(interactionContainer.querySelector('[data-monitor-delete-confirm-owner="hertzbeat-ui-confirm-dialog"]')).not.toBeNull();
+
+    const confirmDeleteButton = interactionContainer.querySelector(
+      'button[data-hz-confirm-action="confirm"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      confirmDeleteButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(apiMessageDelete).toHaveBeenCalledWith('/monitors?ids=909');
+    expect(apiMessageDelete).toHaveBeenCalledWith('/grafana/dashboard?monitorId=909');
+    expect(interactionContainer.querySelector('[data-monitor-delete-confirm-owner="hertzbeat-ui-confirm-dialog"]')).toBeNull();
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="critical"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Delete Failed!');
+    expect(interactionContainer.textContent).toContain('backend refused delete');
+    expect(interactionContainer.textContent).toContain('1 Selected / 9');
+    expect(interactionContainer.querySelector('[data-monitor-manage-auto-refresh-tick="0"]')).not.toBeNull();
+    expect(mockState.replace).not.toHaveBeenCalled();
+  }, 15000);
+
+  it('prewarms the edit route and monitor editor draft before the operator clicks edit', async () => {
+    mockState.renderData = {
       list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
         totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    apiMessageGet.mockImplementation(async (url: string) => {
+      if (url === '/monitor/501') {
+        return {
+          monitor: {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            scrape: 'static',
+            status: 1
+          },
+          params: [],
+          collector: null
+        };
+      }
+      if (url === '/collector') {
+        return { content: [] };
       }
+      if (url === '/apps/mysql/params') {
+        return [];
+      }
+      return { content: [], pageIndex: 0, pageSize: 8, totalElements: 0 };
     });
-    expect(html).toContain('Entity monitor fallback state');
-  });
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
 
-  it('renders stable list/detail selectors for monitor release-path browser smoke', async () => {
-    mockState.searchParams = new URLSearchParams('search=checkout-http&app=website&status=2&pageIndex=0&pageSize=8');
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
 
-    const { default: MonitorsPage } = await import('./page');
-    const html = renderToStaticMarkup(<MonitorsPage />);
+    const editLink = interactionContainer.querySelector(
+      'a[data-monitors-edit-action="true"]'
+    ) as HTMLAnchorElement | null;
+    expect(editLink).not.toBeNull();
 
-    expect(html).toContain('data-monitors-search-input="true"');
-    expect(html).toContain('data-monitors-app-input="true"');
-    expect(html).toContain('data-monitors-status-filter="true"');
-    expect(html).toContain('data-monitors-open-detail-action="true"');
-    expect(html).toContain('data-monitors-copy-action="true"');
-    expect(html).toContain('data-monitors-pause-selected-action="true"');
-    expect(html).toContain('data-monitors-row-select="42"');
-    expect(html).toContain('data-cold-checkbox-owner="cold-checkbox"');
-    expect(html).toContain('data-cold-checkbox-control="native-hidden"');
-  });
+    await act(async () => {
+      editLink?.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
 
-  it('routes the monitor workbench through observability owners', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
-    const t = createTranslatorMock();
-    const { default: MonitorsPage } = await import('./page');
-    const html = renderToStaticMarkup(<MonitorsPage />);
-
-    expect(html).toContain('data-summary-metric-grid="true"');
-    expect(html).toContain(`data-stage-section="${t('monitors.section.list.title')}"`);
-    expect(html).toContain(`data-drawer-section="${t('monitors.rail.selected')}"`);
-    expect(html).toContain(`data-drawer-section="${t('monitors.rail.labels')}"`);
-    expect(html).toContain(`data-drawer-section="${t('monitors.rail.controls')}"`);
-    expect(html).toContain('data-drawer-code-preview="true"');
-    expect(source).toContain("from '@/components/observability'");
-    expect(source).toContain('SummaryMetricGrid');
-    expect(source).toContain('StageSection');
-    expect(source).toContain('DrawerSection');
-    expect(source).toContain('DrawerCodePreview');
-    expect(source).toContain('ObservabilityStatusState');
-    expect(source).not.toContain("from '@/components/workbench/primitives'");
-    expect(source).not.toContain("from '@/components/workbench/toolbar'");
-    expect(source).not.toContain("from '@/components/workbench/selectable-evidence-list'");
-  });
+    expect(mockState.prefetch).toHaveBeenCalledWith('/monitors/501/edit');
+    expect(apiMessageGet).toHaveBeenCalledWith('/monitor/501');
+    expect(apiMessageGet).toHaveBeenCalledWith('/collector');
+    expect(apiMessageGet).toHaveBeenCalledWith('/apps/mysql/params');
+  }, 15000);
 
-  it('uses the shared cold checkbox for monitor row selection instead of default checkbox chrome', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
+  it('shows an optimistic copied row immediately while the backend copy request is still pending', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const controller = await import('@/lib/monitor-manage/controller');
+    let resolveCopy: ((value: unknown) => void) | null = null;
+    apiMessagePost.mockImplementationOnce(
+      () => new Promise(resolve => {
+        resolveCopy = resolve;
+      })
+    );
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
 
-    expect(source).toContain("import { Checkbox } from '@/components/ui/checkbox';");
-    expect(source).toContain('<Checkbox');
-    expect(source).toContain('data-monitors-row-select={String(item.id)}');
-    expect(source).not.toContain('type="checkbox"');
-  });
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
 
-  it('canonicalizes display-only return labels out of the live monitor URL', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
+    const copyButton = interactionContainer.querySelector(
+      'button[data-monitors-copy-action="true"]'
+    ) as HTMLButtonElement | null;
+    expect(copyButton).not.toBeNull();
 
-    expect(source).toContain("searchParams.has('returnLabel')");
-    expect(source).toContain("searchParams.get('returnTo')?.includes('returnLabel')");
-    expect(source).toContain('router.replace(canonicalRoute)');
-  });
+    await act(async () => {
+      copyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
 
-  it('routes selected monitor deletion through a cold modal instead of native confirm', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
+    expect(interactionContainer.textContent).toContain('mysql-production_copy');
+    expect(interactionContainer.querySelector('[data-monitor-row-optimistic-copy="true"]')).not.toBeNull();
+    expect(copyButton?.getAttribute('aria-busy')).toBe('true');
 
-    expect(source).not.toContain('window.confirm');
-    expect(source).not.toContain('confirm(');
-    expect(source).toContain('OverlayDialog');
-    expect(source).toContain('data-monitors-delete-confirm-trigger="cold-modal"');
-    expect(source).toContain('data-monitors-delete-confirm="cold-modal"');
-    expect(source).toContain('确认删除监控');
-    expect(source).toContain('确认删除');
-    expect(source).toContain('取消');
+    await act(async () => {
+      resolveCopy?.({ id: 777 });
+      await Promise.resolve();
+    });
+  }, 15000);
+
+  it('copies the static monitor host from the row-level Angular host affordance', async () => {
+    const writeText = vi.fn().mockResolvedValue(undefined);
+    Object.defineProperty(window.navigator, 'clipboard', {
+      configurable: true,
+      value: { writeText }
+    });
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const hostCopyButton = interactionContainer.querySelector(
+      'button[data-monitor-row-instance-copy="angular-host-copy"]'
+    ) as HTMLButtonElement | null;
+    expect(hostCopyButton).not.toBeNull();
+    expect(hostCopyButton?.getAttribute('data-monitor-row-instance-copy-owner')).toBe('hertzbeat-ui-icon-button');
+    expect(hostCopyButton?.getAttribute('data-monitor-row-instance-copy-target')).toBe('db.internal:3306');
+
+    await act(async () => {
+      hostCopyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(writeText).toHaveBeenCalledWith('db.internal:3306');
+    expect(interactionContainer.textContent).toContain('Copy Success!');
+  }, 15000);
+
+  it('renders dynamic service-discovery scrape rows with Angular scrape type text instead of copying instance', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 502,
+            name: 'discovered-http',
+            app: 'website',
+            instance: 'http-sd-target.internal:9100',
+            status: 1,
+            scrape: 'http_sd',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+
+    const html = await renderMonitorsPage();
+
+    expect(html).toContain('data-monitor-row-copy="Http Service Discovery"');
+    expect(html).toContain('data-monitor-row-scrape-display="angular-service-discovery"');
+    expect(html).toContain('data-monitor-row-scrape-display-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-monitor-row-scrape-value="http_sd"');
+    expect(html).toContain('data-monitor-row-scrape-icon="partition"');
+    expect(html).not.toContain('data-monitor-row-copy="http-sd-target.internal:9100"');
+    expect(html).not.toContain('data-monitor-row-instance-copy="angular-host-copy"');
+  });
+
+  it('refreshes the monitor list when Angular-style copy receives unavailable item errors', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const controller = await import('@/lib/monitor-manage/controller');
+    apiMessagePost.mockRejectedValueOnce(Object.assign(new Error('gone'), { code: 3 }));
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+    expect(interactionContainer.textContent).toContain('1 Selected / 1');
+
+    const copyButton = interactionContainer.querySelector(
+      'button[data-monitors-copy-action="true"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      copyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(apiMessagePost).toHaveBeenCalledWith('/monitor/copy/501', null);
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="warning"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('This monitor item is no longer available, the list will refresh automatically');
+    expect(interactionContainer.textContent).toContain('0 Selected / 1');
+    expect(interactionContainer.textContent).not.toContain('mysql-production_copy');
+    expect(interactionContainer.querySelector('[data-monitor-manage-auto-refresh-tick="1"]')).not.toBeNull();
+  }, 15000);
+
+  it('uses one batch action feedback path for missing selections instead of inert batch buttons', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const missingSelectionActions = [
+      ['enable', 'Select monitors before enabling'],
+      ['pause', 'Select monitors before pausing'],
+      ['export-selected', 'No monitors selected for export'],
+      ['delete', 'Select monitors before deleting']
+    ] as const;
+
+    for (const [actionId, message] of missingSelectionActions) {
+      const actionButton = interactionContainer.querySelector(
+        `button[data-hz-batch-action="${actionId}"]`
+      ) as HTMLButtonElement | null;
+      expect(actionButton).not.toBeNull();
+      expect(actionButton?.disabled).toBe(false);
+
+      await act(async () => {
+        actionButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+        await Promise.resolve();
+      });
+
+      expect(interactionContainer.querySelector('[data-monitor-action-feedback="warning"]')).not.toBeNull();
+      expect(
+        interactionContainer.querySelector(
+          '[data-monitor-action-feedback-owner="hertzbeat-ui-inline-feedback"]'
+        )
+      ).not.toBeNull();
+      expect(interactionContainer.textContent).toContain(message);
+      expect(interactionContainer.querySelector('[data-overlay-dialog="true"]')).toBeNull();
+    }
+  }, 15000);
+
+  it('routes selected batch enable and pause through the Angular modal confirmation contract', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const controller = await import('@/lib/monitor-manage/controller');
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const rowCheckbox = interactionContainer.querySelector('input[data-monitors-row-select="501"]') as HTMLInputElement | null;
+    const pauseButton = interactionContainer.querySelector('button[data-hz-batch-action="pause"]') as HTMLButtonElement | null;
+    expect(rowCheckbox).not.toBeNull();
+    expect(pauseButton).not.toBeNull();
+
+    await act(async () => {
+      rowCheckbox?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+    expect(rowCheckbox?.checked).toBe(true);
+
+    await act(async () => {
+      pauseButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(apiMessageDelete).not.toHaveBeenCalledWith('/monitors/manage?ids=501&type=JSON');
+    const batchDialog = interactionContainer.querySelector('[data-monitor-batch-response-confirm="angular-modal-confirm"]');
+    expect(batchDialog).not.toBeNull();
+    expect(batchDialog?.getAttribute('data-monitor-batch-response-confirm-closable')).toBe('angular-nz-closable-false');
+    expect(batchDialog?.getAttribute('data-monitor-batch-response-confirm-ok')).toBe('angular-nz-ok-danger-primary');
+    expect(batchDialog?.getAttribute('data-hz-confirm-closable')).toBe('false');
+    expect(batchDialog?.getAttribute('data-hz-confirm-ok-danger')).toBe('true');
+    expect(batchDialog?.getAttribute('data-hz-confirm-ok-type')).toBe('primary');
+    expect(interactionContainer.textContent).toContain('Please confirm whether to cancel monitor in batches!');
+    expect(interactionContainer.textContent).toContain('1 monitors selected');
+
+    const batchConfirmButton = interactionContainer.querySelector(
+      '[data-monitor-batch-response-confirm="angular-modal-confirm"] button[data-hz-confirm-action="confirm"]'
+    ) as HTMLButtonElement | null;
+
+    await act(async () => {
+      batchConfirmButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(apiMessageDelete).toHaveBeenCalledWith('/monitors/manage?ids=501&type=JSON');
+    expect(interactionContainer.textContent).toContain('Paused successfully');
+  }, 15000);
+
+  it('filters selected batch enable and pause ids by the old Angular monitor status predicates', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-active',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          },
+          {
+            id: 502,
+            name: 'mysql-paused',
+            app: 'mysql',
+            instance: 'db.internal:3307',
+            status: 0,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000001
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 2
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const controller = await import('@/lib/monitor-manage/controller');
+    apiMessageDelete.mockClear();
+    apiMessageGet.mockClear();
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.textContent).toContain('2 Selected / 2');
+    expect(interactionContainer.innerHTML).toContain('data-monitor-batch-response-filter="angular-status-filtered-selection"');
+    expect(interactionContainer.innerHTML).toContain('data-monitor-batch-response-eligible-status="paused"');
+    expect(interactionContainer.innerHTML).toContain('data-monitor-batch-response-eligible-status="active"');
+
+    const pauseButton = interactionContainer.querySelector('button[data-hz-batch-action="pause"]') as HTMLButtonElement | null;
+    await act(async () => {
+      pauseButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.textContent).toContain('1 monitors selected');
+    const pauseConfirmButton = interactionContainer.querySelector(
+      '[data-monitor-batch-response-confirm="angular-modal-confirm"] button[data-hz-confirm-action="confirm"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      pauseConfirmButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+    expect(apiMessageDelete).toHaveBeenCalledWith('/monitors/manage?ids=501&type=JSON');
+
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const enableButton = interactionContainer.querySelector('button[data-hz-batch-action="enable"]') as HTMLButtonElement | null;
+    await act(async () => {
+      enableButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.textContent).toContain('1 monitors selected');
+    const enableConfirmButton = interactionContainer.querySelector(
+      '[data-monitor-batch-response-confirm="angular-modal-confirm"] button[data-hz-confirm-action="confirm"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      enableConfirmButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+    expect(apiMessageGet).toHaveBeenCalledWith('/monitors/manage?ids=502');
+  }, 15000);
+
+  it('refreshes the monitor list when Angular-style enable receives unavailable item errors', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 0,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const controller = await import('@/lib/monitor-manage/controller');
+    apiMessageGet.mockClear();
+    apiMessageGet.mockRejectedValueOnce(Object.assign(new Error('gone'), { status: 404 }));
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.textContent).toContain('1 Selected / 1');
+
+    const enableButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="enable"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      enableButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(apiMessageGet).not.toHaveBeenCalledWith('/monitors/manage?ids=501');
+    expect(interactionContainer.querySelector('[data-monitor-batch-response-confirm="angular-modal-confirm"]')).not.toBeNull();
+
+    const batchConfirmButton = interactionContainer.querySelector(
+      '[data-monitor-batch-response-confirm="angular-modal-confirm"] button[data-hz-confirm-action="confirm"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      batchConfirmButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(apiMessageGet).toHaveBeenCalledWith('/monitors/manage?ids=501');
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="warning"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('This monitor item is no longer available, the list will refresh automatically');
+    expect(interactionContainer.textContent).toContain('0 Selected / 1');
+    expect(
+      interactionContainer.querySelector('[data-monitor-manage-auto-refresh-tick="1"]')
+    ).not.toBeNull();
+  }, 15000);
+
+  it('marks batch actions busy immediately while export is preparing', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const fetchMock = vi.fn(() => new Promise(() => {}));
+    vi.stubGlobal('fetch', fetchMock);
+    const controller = await import('@/lib/monitor-manage/controller');
+    vi.mocked(controller.buildExportAllMonitorsUrl).mockClear();
+    vi.mocked(controller.buildExportMonitorsUrl).mockClear();
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    const exportTypeButton = interactionContainer.querySelector(
+      'button[data-monitors-export-type-trigger="selected"]'
+    ) as HTMLButtonElement | null;
+    expect(exportTypeButton?.disabled).toBe(false);
+
+    await act(async () => {
+      exportTypeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const exportButton = interactionContainer.querySelector(
+      'button[data-monitors-export-type-option="json"]'
+    ) as HTMLButtonElement | null;
+    expect(exportButton).not.toBeNull();
+
+    await act(async () => {
+      exportButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(controller.buildExportMonitorsUrl).toHaveBeenCalledWith([501], 'JSON');
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/export?ids=501&type=JSON',
+      expect.objectContaining({ cache: 'no-store', credentials: 'same-origin' })
+    );
+    expect(exportButton?.getAttribute('aria-busy')).toBe('true');
+    expect(exportButton?.getAttribute('data-hz-export-type-busy')).toBe('true');
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="info"]')).not.toBeNull();
+    expect(
+      interactionContainer.querySelector('[data-monitor-action-feedback-owner="hertzbeat-ui-inline-feedback"]')
+    ).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Preparing export');
+  }, 15000);
+
+  it('opens the shared Angular export type chooser before exporting selected monitors', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const fetchMock = vi.fn(() => new Promise(() => {}));
+    vi.stubGlobal('fetch', fetchMock);
+    const controller = await import('@/lib/monitor-manage/controller');
+    vi.mocked(controller.buildExportAllMonitorsUrl).mockClear();
+    vi.mocked(controller.buildExportMonitorsUrl).mockClear();
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    const exportTypeButton = interactionContainer.querySelector(
+      'button[data-monitors-export-type-trigger="selected"]'
+    ) as HTMLButtonElement | null;
+    expect(exportTypeButton).not.toBeNull();
+    expect(exportTypeButton?.getAttribute('data-monitor-export-type-trigger-owner')).toBe('hertzbeat-ui-batch-toolbar');
+
+    await act(async () => {
+      exportTypeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const dialog = interactionContainer.querySelector('[data-hz-ui="export-type-dialog"]') as HTMLElement | null;
+    expect(dialog).not.toBeNull();
+    expect(dialog?.getAttribute('data-hz-export-scope')).toBe('selected');
+    expect(dialog?.getAttribute('data-hz-export-selected-count')).toBe('1');
+    expect(dialog?.getAttribute('data-monitor-export-type-dialog-owner')).toBe('hertzbeat-ui-export-type-dialog');
+    expect(dialog?.getAttribute('data-monitors-export-type-dialog')).toBe('selected');
+
+    const jsonOption = interactionContainer.querySelector(
+      'button[data-monitors-export-type-option="json"]'
+    ) as HTMLButtonElement | null;
+    const excelOption = interactionContainer.querySelector(
+      'button[data-monitors-export-type-option="excel"]'
+    ) as HTMLButtonElement | null;
+    expect(jsonOption).not.toBeNull();
+    expect(jsonOption?.getAttribute('data-hz-export-type-option')).toBe('JSON');
+    expect(jsonOption?.getAttribute('data-monitor-export-type-option-owner')).toBe('hertzbeat-ui-export-type-dialog');
+    expect(excelOption).not.toBeNull();
+    expect(excelOption?.getAttribute('data-hz-export-type-option')).toBe('EXCEL');
+
+    await act(async () => {
+      jsonOption?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(controller.buildExportMonitorsUrl).toHaveBeenCalledWith([501], 'JSON');
+    expect(controller.buildExportAllMonitorsUrl).not.toHaveBeenCalled();
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/export?ids=501&type=JSON',
+      expect.objectContaining({ cache: 'no-store', credentials: 'same-origin' })
+    );
+    expect(jsonOption?.getAttribute('aria-busy')).toBe('true');
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="info"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Preparing export');
+  }, 15000);
+
+  it('keeps the shared export type chooser open when the selected export returns an Angular JSON error body', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const fetchMock = vi.fn(() =>
+      Promise.resolve(
+        new Response(JSON.stringify({ code: 1, msg: 'export denied' }), {
+          status: 200,
+          headers: { 'Content-Type': 'application/json' }
+        })
+      )
+    );
+    vi.stubGlobal('fetch', fetchMock);
+    const controller = await import('@/lib/monitor-manage/controller');
+    vi.mocked(controller.buildExportAllMonitorsUrl).mockClear();
+    vi.mocked(controller.buildExportMonitorsUrl).mockClear();
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    const exportTypeButton = interactionContainer.querySelector(
+      'button[data-monitors-export-type-trigger="selected"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      exportTypeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const jsonOption = interactionContainer.querySelector(
+      'button[data-monitors-export-type-option="json"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      jsonOption?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(controller.buildExportMonitorsUrl).toHaveBeenCalledWith([501], 'JSON');
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/export?ids=501&type=JSON',
+      expect.objectContaining({ cache: 'no-store', credentials: 'same-origin' })
+    );
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="critical"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Export failed');
+    const dialog = interactionContainer.querySelector('[data-hz-ui="export-type-dialog"]') as HTMLElement | null;
+    expect(dialog).not.toBeNull();
+    expect(dialog?.getAttribute('data-monitors-export-type-dialog')).toBe('selected');
+    expect(dialog?.getAttribute('data-hz-export-scope')).toBe('selected');
+    expect(jsonOption?.getAttribute('aria-busy')).not.toBe('true');
+  }, 15000);
+
+  it('keeps Angular export-all actions available without requiring a selected monitor', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const fetchMock = vi.fn(() => new Promise(() => {}));
+    vi.stubGlobal('fetch', fetchMock);
+    const controller = await import('@/lib/monitor-manage/controller');
+    vi.mocked(controller.buildExportAllMonitorsUrl).mockClear();
+    vi.mocked(controller.buildExportMonitorsUrl).mockClear();
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.textContent).toContain('0 Selected / 1');
+    const exportAllButton = interactionContainer.querySelector(
+      'button[data-monitors-export-type-trigger="all"]'
+    ) as HTMLButtonElement | null;
+    expect(exportAllButton).not.toBeNull();
+    expect(exportAllButton?.disabled).toBe(false);
+
+    await act(async () => {
+      exportAllButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const jsonOption = interactionContainer.querySelector(
+      'button[data-monitors-export-type-option="json"]'
+    ) as HTMLButtonElement | null;
+    expect(jsonOption).not.toBeNull();
+
+    await act(async () => {
+      jsonOption?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(controller.buildExportAllMonitorsUrl).toHaveBeenCalledWith('JSON');
+    expect(controller.buildExportMonitorsUrl).not.toHaveBeenCalled();
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/export/all?type=JSON',
+      expect.objectContaining({ cache: 'no-store', credentials: 'same-origin' })
+    );
+    expect(jsonOption?.getAttribute('aria-busy')).toBe('true');
+    expect(jsonOption?.getAttribute('data-hz-export-type-busy')).toBe('true');
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="info"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Preparing export');
+  }, 15000);
+
+  it('opens the shared Angular export type chooser for all monitors without a selection', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    const fetchMock = vi.fn(() => new Promise(() => {}));
+    vi.stubGlobal('fetch', fetchMock);
+    const controller = await import('@/lib/monitor-manage/controller');
+    vi.mocked(controller.buildExportAllMonitorsUrl).mockClear();
+    vi.mocked(controller.buildExportMonitorsUrl).mockClear();
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.textContent).toContain('0 Selected / 1');
+    const exportTypeButton = interactionContainer.querySelector(
+      'button[data-monitors-export-type-trigger="all"]'
+    ) as HTMLButtonElement | null;
+    expect(exportTypeButton).not.toBeNull();
+    expect(exportTypeButton?.getAttribute('data-monitor-export-type-trigger-owner')).toBe('hertzbeat-ui-batch-toolbar');
+
+    await act(async () => {
+      exportTypeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const dialog = interactionContainer.querySelector('[data-hz-ui="export-type-dialog"]') as HTMLElement | null;
+    expect(dialog).not.toBeNull();
+    expect(dialog?.getAttribute('data-hz-export-scope')).toBe('all');
+    expect(dialog?.getAttribute('data-hz-export-selected-count')).toBe('0');
+    expect(dialog?.getAttribute('data-monitor-export-type-dialog-owner')).toBe('hertzbeat-ui-export-type-dialog');
+    expect(dialog?.getAttribute('data-monitors-export-type-dialog')).toBe('all');
+
+    const jsonOption = interactionContainer.querySelector(
+      'button[data-monitors-export-type-option="json"]'
+    ) as HTMLButtonElement | null;
+    expect(jsonOption).not.toBeNull();
+
+    await act(async () => {
+      jsonOption?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(controller.buildExportAllMonitorsUrl).toHaveBeenCalledWith('JSON');
+    expect(controller.buildExportMonitorsUrl).not.toHaveBeenCalled();
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/export/all?type=JSON',
+      expect.objectContaining({ cache: 'no-store', credentials: 'same-origin' })
+    );
+    expect(jsonOption?.getAttribute('aria-busy')).toBe('true');
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="info"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Preparing export');
+  }, 15000);
+
+  it('keeps Angular monitor import feedback tied to the uploaded file name', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    let resolveImport: (value?: unknown) => void = () => {};
+    apiMessagePost.mockImplementationOnce(
+      () =>
+        new Promise(resolve => {
+          resolveImport = resolve;
+        })
+    );
+    const controller = await import('@/lib/monitor-manage/controller');
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+    expect(interactionContainer.textContent).toContain('1 Selected / 1');
+
+    const importButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="import"]'
+    ) as HTMLButtonElement | null;
+    const importInput = interactionContainer.querySelector(
+      'input[data-monitors-import-file-input="true"]'
+    ) as HTMLInputElement | null;
+    expect(importButton).not.toBeNull();
+    expect(importInput).not.toBeNull();
+
+    await act(async () => {
+      importButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const importFile = new File(['name: mysql-production'], 'monitors-prod.yml', { type: 'application/x-yaml' });
+    Object.defineProperty(importInput, 'files', { value: [importFile], configurable: true });
+    await act(async () => {
+      importInput?.dispatchEvent(new Event('change', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(controller.buildImportMonitorsUrl).toHaveBeenCalled();
+    expect(apiMessagePost).toHaveBeenCalledWith('/monitors/import', expect.any(FormData));
+    expect(importButton?.getAttribute('aria-busy')).toBe('true');
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="info"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Import [monitors-prod.yml] submitted');
+
+    await act(async () => {
+      resolveImport(undefined);
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="success"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Import [monitors-prod.yml] Success!');
+    expect(interactionContainer.textContent).toContain('0 Selected / 1');
+  }, 15000);
+
+  it('keeps the current selection and page when Angular-style monitor import returns a non-zero code', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+    let rejectImport: (error: Error) => void = () => {};
+    apiMessagePost.mockImplementationOnce(
+      () =>
+        new Promise((_resolve, reject) => {
+          rejectImport = reject;
+        })
+    );
+    const controller = await import('@/lib/monitor-manage/controller');
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+    expect(interactionContainer.textContent).toContain('1 Selected / 1');
+
+    const importInput = interactionContainer.querySelector(
+      'input[data-monitors-import-file-input="true"]'
+    ) as HTMLInputElement | null;
+    expect(importInput).not.toBeNull();
+
+    const importFile = new File(['invalid: true'], 'bad-monitors.yml', { type: 'application/x-yaml' });
+    Object.defineProperty(importInput, 'files', { value: [importFile], configurable: true });
+    await act(async () => {
+      importInput?.dispatchEvent(new Event('change', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(controller.buildImportMonitorsUrl).toHaveBeenCalled();
+    expect(apiMessagePost).toHaveBeenCalledWith('/monitors/import', expect.any(FormData));
+    expect(interactionContainer.textContent).toContain('Import [bad-monitors.yml] submitted');
+
+    await act(async () => {
+      rejectImport(new Error('invalid yaml'));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(interactionContainer.querySelector('[data-monitor-action-feedback="critical"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('Import [bad-monitors.yml] Failed: Import failed');
+    expect(interactionContainer.textContent).toContain('invalid yaml');
+    expect(interactionContainer.textContent).toContain('1 Selected / 1');
+    expect(interactionContainer.querySelector('[data-monitor-manage-auto-refresh-tick="0"]')).not.toBeNull();
+    expect(mockState.replace).not.toHaveBeenCalled();
+  }, 15000);
+
+  it('applies the Angular status filter immediately when the operator changes the select value', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 2,
+        pageSize: 20,
+        totalElements: 41
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '2', pageSize: '20' },
+      entityWorkbenchFallback: false
+    };
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '2', pageSize: '20' }} />
+      );
+      await Promise.resolve();
+    });
+
+    const statusSelect = interactionContainer.querySelector(
+      '[data-monitors-status-filter="true"]'
+    ) as HTMLElement | null;
+    expect(statusSelect).not.toBeNull();
+    expect(statusSelect?.getAttribute('data-monitors-status-filter-autosubmit')).toBe('true');
+    expect(statusSelect?.getAttribute('data-monitor-status-filter-autosubmit-owner')).toBe('hertzbeat-ui-select');
+    const statusTrigger = statusSelect?.querySelector('button[data-hz-ui="select-trigger"]') as HTMLButtonElement | null;
+    expect(statusTrigger).not.toBeNull();
+
+    await act(async () => {
+      statusTrigger?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const downOption = Array.from(interactionContainer.querySelectorAll('button[role="option"]')).find(button =>
+      button.textContent?.includes('Down')
+    ) as HTMLButtonElement | undefined;
+    expect(downOption).toBeDefined();
+
+    await act(async () => {
+      downOption?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.replace).toHaveBeenCalledWith('/monitors?search=prod&app=mysql&status=2&pageIndex=0&pageSize=20');
+  }, 15000);
+
+  it('keeps the old Angular label filter as an operator-visible query field', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: { team: 'platform' },
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 3,
+        pageSize: 20,
+        totalElements: 61
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '3', pageSize: '20' },
+      entityWorkbenchFallback: false
+    };
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '3', pageSize: '20' }} />
+      );
+      await Promise.resolve();
+    });
+
+    const labelInput = interactionContainer.querySelector(
+      'input[data-monitors-label-filter-input="true"]'
+    ) as HTMLInputElement | null;
+    const applyButton = interactionContainer.querySelector(
+      'button[data-monitor-manage-filter-action="apply"]'
+    ) as HTMLButtonElement | null;
+    expect(labelInput).not.toBeNull();
+    expect(labelInput?.getAttribute('data-monitor-manage-label-filter-input-owner')).toBe('hertzbeat-ui-input');
+    expect(labelInput?.getAttribute('data-hz-monitor-filter-field')).toBe('labels');
+    expect(applyButton).not.toBeNull();
+
+    const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
+    await act(async () => {
+      valueSetter?.call(labelInput, 'team=platform');
+      labelInput?.dispatchEvent(new Event('input', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      applyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.replace).toHaveBeenCalledWith('/monitors?search=prod&app=mysql&labels=team%3Dplatform&pageIndex=0&pageSize=20');
+  }, 15000);
+
+  it('submits search and label filters with the old Angular Enter-key workflow', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: { team: 'platform' },
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 3,
+        pageSize: 20,
+        totalElements: 61
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '3', pageSize: '20' },
+      entityWorkbenchFallback: false
+    };
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY, search: 'prod', app: 'mysql', pageIndex: '3', pageSize: '20' }} />
+      );
+      await Promise.resolve();
+    });
+
+    const searchInput = interactionContainer.querySelector(
+      'input[data-monitors-search-input="true"]'
+    ) as HTMLInputElement | null;
+    const labelInput = interactionContainer.querySelector(
+      'input[data-monitors-label-filter-input="true"]'
+    ) as HTMLInputElement | null;
+    expect(searchInput).not.toBeNull();
+    expect(labelInput).not.toBeNull();
+    expect(searchInput?.getAttribute('data-hz-monitor-filter-enter-submit')).toBe('search');
+    expect(labelInput?.getAttribute('data-hz-monitor-filter-enter-submit')).toBe('labels');
+    expect(searchInput?.getAttribute('data-monitors-filter-enter-submit')).toBe('search');
+    expect(labelInput?.getAttribute('data-monitors-filter-enter-submit')).toBe('labels');
+
+    const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
+    await act(async () => {
+      valueSetter?.call(searchInput, 'checkout');
+      searchInput?.dispatchEvent(new Event('input', { bubbles: true }));
+      await Promise.resolve();
+      searchInput?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.replace).toHaveBeenCalledWith('/monitors?search=checkout&app=mysql&pageIndex=0&pageSize=20');
+    mockState.replace.mockClear();
+
+    await act(async () => {
+      valueSetter?.call(labelInput, 'team=platform');
+      labelInput?.dispatchEvent(new Event('input', { bubbles: true }));
+      await Promise.resolve();
+      labelInput?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.replace).toHaveBeenCalledWith(
+      '/monitors?search=checkout&app=mysql&labels=team%3Dplatform&pageIndex=0&pageSize=20'
+    );
+  }, 15000);
+
+  it('submits search and label filters when the operator uses the old Angular clear trigger', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: { team: 'platform' },
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 3,
+        pageSize: 20,
+        totalElements: 61
+      },
+      query: {
+        ...EMPTY_MONITOR_TEST_QUERY,
+        search: 'prod',
+        app: 'mysql',
+        labels: 'team=platform',
+        pageIndex: '3',
+        pageSize: '20'
+      },
+      entityWorkbenchFallback: false
+    };
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <MonitorsPage
+          initialQuery={{
+            ...EMPTY_MONITOR_TEST_QUERY,
+            search: 'prod',
+            app: 'mysql',
+            labels: 'team=platform',
+            pageIndex: '3',
+            pageSize: '20'
+          }}
+        />
+      );
+      await Promise.resolve();
+    });
+
+    const searchClearButton = interactionContainer.querySelector(
+      'button[data-monitors-filter-clear-action="search"]'
+    ) as HTMLButtonElement | null;
+    const labelClearButton = interactionContainer.querySelector(
+      'button[data-monitors-filter-clear-action="labels"]'
+    ) as HTMLButtonElement | null;
+    expect(searchClearButton).not.toBeNull();
+    expect(labelClearButton).not.toBeNull();
+    expect(searchClearButton?.getAttribute('data-hz-monitor-filter-clear-action')).toBe('search');
+    expect(labelClearButton?.getAttribute('data-hz-monitor-filter-clear-action')).toBe('labels');
+    expect(searchClearButton?.getAttribute('data-monitor-filter-clear-action-owner')).toBe('hertzbeat-ui-icon-button');
+    expect(labelClearButton?.getAttribute('data-monitor-label-filter-clear-action-owner')).toBe('hertzbeat-ui-icon-button');
+
+    await act(async () => {
+      searchClearButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.replace).toHaveBeenCalledWith('/monitors?app=mysql&labels=team%3Dplatform&pageIndex=0&pageSize=20');
+    mockState.replace.mockClear();
+
+    const refreshedLabelClearButton = interactionContainer.querySelector(
+      'button[data-monitors-filter-clear-action="labels"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      refreshedLabelClearButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(mockState.replace).toHaveBeenCalledWith('/monitors?app=mysql&pageIndex=0&pageSize=20');
+  }, 15000);
+
+  it('selects all monitor results across pages before running batch actions', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 1,
+        totalElements: 2
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY, pageSize: '1' },
+      entityWorkbenchFallback: false
+    };
+    apiMessageGet.mockResolvedValueOnce({
+      content: [
+        {
+          id: 501,
+          name: 'mysql-production',
+          app: 'mysql',
+          instance: 'db.internal:3306',
+          status: 1,
+          scrape: 'static',
+          labels: {},
+          gmtUpdate: 1713200000000
+        },
+        {
+          id: 502,
+          name: 'redis-production',
+          app: 'redis',
+          instance: 'redis.internal:6379',
+          status: 1,
+          scrape: 'static',
+          labels: {},
+          gmtUpdate: 1713200000000
+        }
+      ],
+      pageIndex: 0,
+      pageSize: 2,
+      totalElements: 2
+    });
+    const fetchMock = vi.fn(() => new Promise(() => {}));
+    vi.stubGlobal('fetch', fetchMock);
+    const controller = await import('@/lib/monitor-manage/controller');
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY, pageSize: '1' }} />);
+      await Promise.resolve();
+    });
+
+    const selectAllButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-all-results"]'
+    ) as HTMLButtonElement | null;
+    expect(selectAllButton).not.toBeNull();
+    expect(selectAllButton?.textContent).toContain('Select all');
+
+    await act(async () => {
+      selectAllButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(apiMessageGet).toHaveBeenCalledWith('/monitors?pageIndex=0&pageSize=2');
+    expect(interactionContainer.textContent).toContain('2 Selected / 2');
+
+    const exportTypeButton = interactionContainer.querySelector(
+      'button[data-monitors-export-type-trigger="selected"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      exportTypeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const exportButton = interactionContainer.querySelector(
+      'button[data-monitors-export-type-option="json"]'
+    ) as HTMLButtonElement | null;
+    await act(async () => {
+      exportButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(controller.buildExportMonitorsUrl).toHaveBeenCalledWith([501, 502], 'JSON');
+  }, 15000);
+
+  it('defaults entity workbench routes to down monitors and swaps create/import for a return action', async () => {
+    apiMessageGet
+      .mockResolvedValueOnce({
+        content: [],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 0
+      })
+      .mockResolvedValueOnce({
+        content: [],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 0
+      });
+
+    const html = await renderMonitorsPage({
+      app: 'website',
+      entityId: '42',
+      entityName: 'Checkout Service',
+      returnTo: '/entities/42'
+    });
+    await mockState.lastLoad?.();
+
+    expect(apiMessageGet).toHaveBeenCalledWith(
+      '/monitors?pageIndex=0&pageSize=8&app=website&status=2&entityId=42&entityName=Checkout+Service&returnTo=%2Fentities%2F42'
+    );
+    expect(html).toContain('Checkout Service');
+    expect(html).not.toContain('Return to entity');
+    expect(html).toContain('href="/entities/42"');
+    expect(html).not.toContain('New monitor');
+    expect(html).not.toContain('Import');
+  }, 15000);
+
+  it('keeps the entity return action on an internal entity route when returnTo is unsafe', async () => {
+    apiMessageGet.mockResolvedValueOnce({
+      content: [],
+      pageIndex: 0,
+      pageSize: 8,
+      totalElements: 0
+    });
+
+    const html = await renderMonitorsPage({
+      app: 'website',
+      entityId: '42',
+      entityName: 'Checkout Service',
+      returnTo: '//evil.example/steal-session'
+    });
+
+    expect(html).toContain('Checkout Service');
+    expect(html).not.toContain('Return to entity');
+    expect(html).toContain('href="/entities/42"');
+    expect(html).not.toContain('href="//evil.example/steal-session"');
+  }, 15000);
+
+  it('falls back entity workbench routes from down-only mode to all monitors when the abnormal pass is empty', async () => {
+    apiMessageGet
+      .mockResolvedValueOnce({
+        content: [],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 0
+      })
+      .mockResolvedValueOnce({
+        content: [
+          {
+            id: 99,
+            name: 'checkout-healthy',
+            app: 'http',
+            instance: '10.0.0.99',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      });
+
+    await renderMonitorsPage({
+      app: 'website',
+      entityId: '42',
+      entityName: 'Checkout Service',
+      returnTo: '/entities/42'
+    });
+    const loaded = await mockState.lastLoad?.();
+    mockState.renderData = loaded;
+    const html = await renderMonitorsPage({
+      app: 'website',
+      entityId: '42',
+      entityName: 'Checkout Service',
+      returnTo: '/entities/42'
+    });
+
+    expect(apiMessageGet).toHaveBeenNthCalledWith(
+      1,
+      '/monitors?pageIndex=0&pageSize=8&app=website&status=2&entityId=42&entityName=Checkout+Service&returnTo=%2Fentities%2F42'
+    );
+    expect(apiMessageGet).toHaveBeenNthCalledWith(
+      2,
+      '/monitors?pageIndex=0&pageSize=8&app=website&entityId=42&entityName=Checkout+Service&returnTo=%2Fentities%2F42'
+    );
+    expect(loaded).toMatchObject({
+      entityWorkbenchFallback: true,
+      query: {
+        status: ''
+      },
+      list: {
+        totalElements: 1
+      }
+    });
+    expect(html).toContain('Entity monitor fallback state');
+  }, 15000);
+
+  it('renders stable list/detail selectors for monitor release-path browser smoke', async () => {
+    const html = await renderMonitorsPage(
+      { search: 'checkout-http', app: 'website', status: '2', pageIndex: '0', pageSize: '8' },
+      '2'
+    );
+
+    expect(html).toContain('data-loading-copy="Loading monitor center"');
+    expect(html).toContain('data-hz-ui="explorer-frame"');
+    expect(html).toContain('data-monitor-manage-shell-owner="hertzbeat-ui-explorer-frame"');
+    expect(html).toContain('data-monitor-manage-auto-refresh-owner="react-interval"');
+    expect(html).toContain('data-monitor-manage-auto-refresh-interval-ms="120000"');
+    expect(html).toContain('data-monitor-manage-auto-refresh-tick="0"');
+    expect(html).toContain('data-monitor-manage-manual-refresh-owner="hertzbeat-ui-icon-button"');
+    expect(html).toContain('data-monitor-manage-manual-refresh-action="sync"');
+    expect(html).toContain('data-monitor-manage-manual-refresh-tick="0"');
+    expect(html).not.toContain('data-workbench-page="true"');
+    expect(html).toContain('data-monitors-search-input="true"');
+    expect(html).toContain('data-monitor-manage-filter-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-monitor-filter-enter-submit="search"');
+    expect(html).toContain('data-monitors-filter-enter-submit="search"');
+    expect(html).toContain('data-monitor-filter-enter-submit-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-monitor-filter-clearable-field="search"');
+    expect(html).toContain('data-monitors-label-filter-input="true"');
+    expect(html).toContain('data-monitor-manage-label-filter-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-monitor-filter-enter-submit="labels"');
+    expect(html).toContain('data-monitors-filter-enter-submit="labels"');
+    expect(html).toContain('data-monitors-export-type-trigger="selected"');
+    expect(html).toContain('data-monitors-export-type-trigger="all"');
+    expect(html).toContain('data-monitor-export-type-trigger-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-monitor-label-filter-enter-submit-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-monitor-filter-clearable-field="labels"');
+    expect(html).not.toContain('data-monitors-app-input="true"');
+    expect(html).toContain('data-monitor-type-filter="true"');
+    expect(html).toContain('data-monitor-manage-filter-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-monitor-manage-filter-select="type"');
+    expect(html).toContain('data-monitors-app-filter-autosubmit="true"');
+    expect(html).toContain('data-monitor-app-filter-autosubmit-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-monitor-filter-field="type-picker"');
+    expect(html).toContain('data-monitors-filter-app-picker-trigger="true"');
+    expect(html).toContain('data-monitor-manage-filter-action="app-picker"');
+    expect(html).toContain('data-monitors-status-filter="true"');
+    expect(html).toContain('data-monitor-manage-filter-select="status"');
+    expect(html).toContain('data-monitors-status-filter-autosubmit="true"');
+    expect(html).toContain('data-monitor-status-filter-autosubmit-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-monitor-manage-filter-action-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-monitor-manage-filter-action="apply"');
+    expect(html).toContain('data-monitor-manage-filter-action="clear"');
+    expect(html).toContain('data-monitors-open-detail-action="true"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).toContain('data-monitor-manage-list-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-hz-ui="pagination-bar"');
+    expect(html).toContain('data-monitor-manage-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-hz-pagination-page-size="select-menu"');
+    expect(html).toContain('data-monitor-manage-pagination-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-pagination-page-jump="number-input"');
+    expect(html).toContain('data-hz-pagination-action="page-jump"');
+    expect(html).toContain('data-monitor-manage-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-monitor-manage-pagination-action="page-jump"');
+    expect(html).toContain('data-monitor-manage-pagination-action-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-monitor-manage-pagination-action="previous"');
+    expect(html).toContain('data-monitor-manage-pagination-action="next"');
+    expect(html).toContain('Previous page');
+    expect(html).toContain('Next page');
+    expect(html).not.toContain('Previous</button>');
+    expect(html).not.toContain('Next</button>');
+    expect(html).toContain('data-hz-row-clickable="true"');
+    expect(html).toContain('href="/monitors/42/edit"');
+    expect(html).toContain('data-monitors-copy-action="true"');
+    expect(html).toContain('data-monitors-pause-selected-action="true"');
+    expect(html).toContain('data-monitors-row-select="42"');
+    expect(html).toContain('data-monitor-row-type="http"');
+    expect(html).toContain('data-monitor-row-name-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-monitor-row-copy-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-monitor-row-type-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-monitor-row-updated-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).not.toContain('Collection mode static');
+    expect(html).not.toContain('scrape static');
+    expect(html).toContain('data-hz-batch-action="select-all-results"');
+    expect(html).toContain('data-monitor-batch-more-menu-contract="angular-toolbar-ellipsis-menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-hz-batch-overflow-mode="angular-ellipsis-menu"');
+    expect(html).toContain('data-hz-batch-overflow-trigger="angular-ellipsis-menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-trigger="angular-ellipsis-menu"');
+    expect(html).toContain('data-hz-batch-overflow-panel="angular-nz-dropdown-menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-panel="angular-nz-dropdown-menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-clearance="floating-overlay-no-table-crop"');
+    expect(html).toContain('data-hz-batch-action-presentation="inline"');
+    expect(html).toContain('data-hz-batch-action-presentation="menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-action="enable"');
+    expect(html).toContain('data-monitor-batch-more-menu-action="pause"');
+    expect(html).toContain('data-monitor-batch-more-menu-action="delete"');
+    expect(html).toContain('Select all');
+    expect(html).toContain('Select page');
+    expect(html).toContain('data-monitor-manage-selection-owner="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-hz-ui="checkbox"');
+    expect(html).toContain('data-hz-checkbox-control="native-hidden"');
+    expect(html).not.toContain('data-cold-checkbox-owner');
+  }, 15000);
+
+  it('keeps monitor row updated cells as timestamp-only table values', async () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('data-monitor-row-meta={updatedTime}');
+    expect(source).not.toContain("t('monitors.updated-at'");
+
+    const html = await renderMonitorsPage();
+
+    expect(html).toContain('data-monitor-row-meta="2026-04-19 09:00:00"');
+    expect(html).not.toContain('Updated 2026-04-19 09:00:00');
+    expect(html).not.toContain('更新时间 2026-04-19 09:00:00');
+  });
+
+  it('keeps monitor row status badges on the shared badge density without page-local sizing overrides', async () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('HzStatusBadge');
+    expect(source).toContain('data-monitor-manage-status-tone={statusBadgeVariant(item.status)}');
+    expect(source).not.toContain('className="min-h-5 px-1.5 leading-4"');
+
+    const html = await renderMonitorsPage();
+
+    expect(html).toContain('data-hz-ui="status-badge"');
+    expect(html).toContain('data-monitor-manage-status-tone="danger"');
+    expect(html).toContain('min-h-6');
+    expect(html).not.toContain('min-h-5');
+  });
+
+  it('renders Angular entity workbench triage reasons on monitor rows', async () => {
+    const entityQuery = buildMonitorTestQuery({
+      entityId: '42',
+      entityName: 'checkout-api',
+      returnTo: '/entities/42?tab=monitors'
+    });
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'checkout-http',
+            app: 'website',
+            instance: '127.0.0.1:80',
+            status: 2,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: entityQuery,
+      entityWorkbenchFallback: false
+    };
+
+    const html = await renderMonitorsPage({
+      entityId: '42',
+      entityName: 'checkout-api',
+      returnTo: '/entities/42?tab=monitors'
+    });
+
+    expect(html).toContain('data-monitor-row-entity-triage="angular-entity-reason"');
+    expect(html).toContain('data-monitor-row-entity-triage-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-monitor-row-entity-triage-status="down"');
+    expect(html).toContain('data-monitor-row-entity-response-action="angular-inline-host-action"');
+    expect(html).toContain('data-monitor-row-entity-response-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-monitor-row-response-action="pause"');
+    expect(html).toContain('data-monitor-row-entity-support-actions="angular-platform-support-action-bar"');
+    expect(html).toContain('data-monitor-row-entity-support-actions-owner="hertzbeat-ui-button-link"');
+    expect(html).toContain('data-monitor-row-entity-support-action="related-logs"');
+    expect(html).toContain('data-monitor-row-entity-support-action="related-traces"');
+    expect(html).toContain('data-monitor-row-entity-support-action="code"');
+    expect(html).toContain('data-monitor-row-entity-support-disabled="missing-code-navigation"');
+    expect(html).toContain('Open related logs');
+    expect(html).toContain('Open related traces');
+    expect(html).toContain('data-monitor-row-actions-entity-context="detail-edit-copy-only"');
+    expect(html).toContain('This monitor is unhealthy. Check it first.');
+    expect(html).not.toContain('data-monitor-row-response-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).not.toContain('data-monitor-row-delete-action="single"');
+  });
+
+  it('renders missing monitor row identity with the localized empty fallback', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 77,
+            name: 'unassigned-monitor',
+            app: '',
+            instance: '',
+            status: 0,
+            scrape: '',
+            labels: {},
+            gmtUpdate: 1713200000000
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 1
+      },
+      query: {
+        search: '',
+        app: '',
+        labels: '',
+        status: '',
+        pageIndex: '',
+        pageSize: '',
+        entityId: '',
+        entityName: '',
+        returnTo: ''
+      },
+      entityWorkbenchFallback: false
+    };
+
+    const html = await renderMonitorsPage();
+
+    expect(html).toContain('data-monitor-row-copy="None"');
+    expect(html).toContain('data-monitor-row-type="None"');
+    expect(html).toContain('data-monitors-row-select="77"');
+    expect(html).not.toContain('data-monitor-row-instance-copy="angular-host-copy"');
+    expect(html).not.toContain('unassigned-monitor</div><div data-monitor-row-copy="-');
+  });
+
+  it('renders empty monitor results through the shared HzEmptyState', async () => {
+    mockState.renderData = {
+      list: {
+        content: [],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 0
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+
+    const html = await renderMonitorsPage();
+
+    expect(html).toContain('data-hz-ui="empty-state"');
+    expect(html).toContain('data-monitor-manage-empty-owner="hertzbeat-ui-empty-state"');
+    expect(html).toContain('No Matching Monitors');
+    expect(html).not.toContain('data-observability-status-state="true"');
+  }, 15000);
+
+  it('keeps labels as row evidence instead of a right-side detail rail on the monitor list', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('labelEntries');
+    expect(source).toContain('Object.entries(item.labels || {})');
+    expect(source).toContain('HzLabelTag');
+    expect(source).toContain('renderAngularLabelColor(key)');
+    expect(source).toContain('data-monitor-row-label-color="angular-render-label-color"');
+    expect(source).toContain('data-monitor-row-labels-owner="hertzbeat-ui-label-tag"');
+    expect(source).not.toContain('data-monitor-manage-detail-rail');
+    expect(source).not.toContain("t('monitors.rail.labels')");
+    expect(source).not.toContain('RowList');
+  });
+
+  it('loads monitor list pages through the monitor domain facade instead of raw getters', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain("import { api } from '@/lib/monitor-api-facade';");
+    expect(source).toContain('queryClient.fetchQuery');
+    expect(source).toContain('queryKeys.monitors.list');
+    expect(source).toContain('queryFn: () => loadMonitorListFromFacade(api.monitors.page, nextQuery)');
+    expect(source).toContain('readMonitorListWithQuery(query)');
+    expect(source).toContain('readMonitorListWithQuery(fallbackQuery)');
+    expect(source).toContain('readMonitorListWithQuery(selectAllQuery)');
+    expect(source).not.toContain('apiMessageGet<PageResult<Monitor>>(monitorListUrl)');
+    expect(source).not.toContain('apiMessageGet<PageResult<Monitor>>(buildMonitorUrl(fallbackQuery))');
+    expect(source).not.toContain('apiMessageGet<PageResult<Monitor>>(buildMonitorUrl(selectAllQuery))');
+  });
+
+  it('invalidates monitor React Query data after monitor mutations', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('useQueryClient');
+    expect(source).toContain('queryClient.invalidateQueries({ queryKey: queryKeys.monitors.all })');
+    expect(source).toContain('refreshMonitorWorkbench');
+    expect(source).toContain('api.monitors.copy(item.id)');
+    expect(source).toContain('api.monitors.delete(targetIds)');
+    expect(source).toContain('api.monitors.enable(selectedIds)');
+    expect(source).toContain('api.monitors.pause(selectedIds)');
+    expect(source).toContain('api.monitors.enable([item.id])');
+    expect(source).toContain('api.monitors.pause([item.id])');
+    expect(source).not.toContain('copyMonitor(apiMessagePost as any');
+    expect(source).not.toContain('deleteMonitors(apiMessageDelete as any');
+    expect(source).not.toContain('enableMonitors(apiMessageGet as any');
+    expect(source).not.toContain('pauseMonitors(apiMessageDelete as any');
+  });
+
+  it('prewarms monitor editor drafts through the monitor domain facade instead of a raw getter', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('loadMonitorEditorDraftFromFacade(');
+    expect(source).toContain('readMonitorDetail: api.monitors.editorDetail');
+    expect(source).toContain('readCollectors: api.monitors.editorCollectors');
+    expect(source).toContain('readParamDefines: api.monitors.editorParamDefines');
+    expect(source).not.toContain('loadMonitorEditorDraft(apiMessageGet');
+  });
+
+  it('routes the monitor workbench through the shared HertzBeat UI shell owners', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('HzExplorerFrame');
+    expect(source).toContain('HzMonitorFilterBar');
+    expect(source).toContain('HzBatchToolbar');
+    expect(source).toContain('HzPaginationBar');
+    expect(source).toContain('data-monitor-manage-shell-owner="hertzbeat-ui-explorer-frame"');
+    expect(source).toContain('data-monitor-manage-filter-owner="hertzbeat-ui-monitor-filter-bar"');
+    expect(source).toContain('data-monitor-manage-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(source).toContain('handlePageJumpChange');
+    expect(source).toContain('pageJumpLabel={t(\'common.page\')}');
+    expect(source).toContain('data-monitor-manage-pagination-page-jump-owner');
+    expect(source).toContain("'data-monitor-manage-pagination-action': 'page-jump'");
+    expect(source).toContain('MONITOR_MANAGE_AUTO_REFRESH_MS = 120_000');
+    expect(source).toContain('window.setInterval');
+    expect(source).toContain('setReloadKey(prev => prev + 1)');
+    expect(source).toContain('data-monitor-manage-auto-refresh-owner="react-interval"');
+    expect(source).toContain('data-monitor-manage-auto-refresh-interval-ms');
+    expect(source).toContain('data-monitor-manage-auto-refresh-tick');
+    expect(source).toContain('handleManualRefresh');
+    expect(source).toContain('data-monitor-manage-manual-refresh-owner="hertzbeat-ui-icon-button"');
+    expect(source).toContain('data-monitor-manage-manual-refresh-action="sync"');
+    expect(source).toContain('data-monitor-manage-manual-refresh-tick');
+    expect(source).toContain('<HzMonitorFilterBar');
+    expect(source).not.toContain('data-monitor-manage-filter-owner="hertzbeat-ui-light-toolbar"');
+    expect(source).not.toContain('<HzToolbar');
+    expect(source).toContain('<HzBatchToolbar');
+    expect(source).toContain('<HzPaginationBar');
+    expect(source).toContain("previousLabel={t('common.previous-page')}");
+    expect(source).toContain("nextLabel={t('common.next-page')}");
+    expect(source).not.toContain("previousLabel={t('common.previous')}");
+    expect(source).not.toContain("nextLabel={t('common.next')}");
+    expect(source).not.toContain('flex flex-wrap items-center justify-between gap-3 border-t border-[var(--hz-ui-line-soft)] px-3 py-2.5');
+    expect(source).toContain('data-monitor-manage-primary-action-owner="hertzbeat-ui-button"');
+    expect(source).toContain('data-monitor-manage-primary-action="new-monitor-link"');
+    expect(source).toContain('data-monitor-manage-primary-action="new-monitor-picker"');
+    expect(source).toContain('data-monitor-manage-primary-action="entity-return"');
+    expect(source).not.toContain('HzField');
+    expect(source).not.toContain('data-monitors-app-input');
+    expect(source).toContain("'data-monitor-type-filter': 'true'");
+    expect(source).toContain('handleTypeFilterChange');
+    expect(source).toContain("'data-monitors-app-filter-autosubmit': 'true'");
+    expect(source).toContain("'data-monitor-app-filter-autosubmit-owner': 'hertzbeat-ui-select'");
+    expect(source).toContain("'data-monitors-filter-app-picker-trigger': 'true'");
+    expect(source).toContain("openAppPicker('filter')");
+    expect(source).toContain('searchInputProps');
+    expect(source).toContain("'data-monitor-app-picker-search-owner': 'hertzbeat-ui-input'");
+    expect(source).toContain("'data-monitor-app-picker-search-action': 'filter'");
+    expect(source).toContain("'data-monitors-app-picker-search-input': 'true'");
+    expect(source).toContain('handleStatusFilterChange');
+    expect(source).toContain("'data-monitors-status-filter-autosubmit': 'true'");
+    expect(source).toContain("appPickerMode === 'filter'");
+    expect(source).toContain("key: 'type'");
+    expect(source).toContain('data-monitor-row-type');
+    expect(source).toContain('HzDataCellText');
+    expect(source).toContain('data-monitor-row-name-owner="hertzbeat-ui-data-cell-text"');
+    expect(source).toContain('data-monitor-row-copy-owner="hertzbeat-ui-data-cell-text"');
+    expect(source).toContain('data-monitor-row-instance-copy="angular-host-copy"');
+    expect(source).toContain('data-monitor-row-instance-copy-owner="hertzbeat-ui-icon-button"');
+    expect(source).toContain('data-monitor-row-instance-copy-target={item.instance}');
+    expect(source).toContain('data-monitor-copy-lifecycle-contract="angular-copy-success-refresh-unavailable-refresh"');
+    expect(source).toContain('data-monitor-copy-lifecycle-owner="route-copy-contract"');
+    expect(source).toContain('data-monitor-row-copy-lifecycle="angular-copy-success-refresh-unavailable-refresh"');
+    expect(source).toContain('data-monitor-row-copy-lifecycle-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain("navigator.clipboard?.writeText(copyValue)");
+    expect(source).toContain("setActionFeedback({ tone: 'success', title: t('common.notify.copy-success') })");
+    expect(source).toContain("t(`monitor.scrape.type.${item.scrape}`)");
+    expect(source).toContain("data-monitor-row-scrape-display={isDynamicScrape ? 'angular-service-discovery' : undefined}");
+    expect(source).toContain("data-monitor-row-scrape-display-owner={isDynamicScrape ? 'hertzbeat-ui-data-cell-text' : undefined}");
+    expect(source).toContain('data-monitor-row-scrape-value={isDynamicScrape ? item.scrape : undefined}');
+    expect(source).toContain('data-monitor-row-scrape-icon="partition"');
+    expect(source).toContain('entityTriageReason');
+    expect(source).toContain("t('entity.monitor.workbench.reason.down')");
+    expect(source).toContain("t('entity.monitor.workbench.reason.fallback')");
+    expect(source).toContain("t('entity.monitor.workbench.reason.paused')");
+    expect(source).toContain("t('entity.monitor.workbench.reason.default')");
+    expect(source).toContain('data-monitor-row-entity-triage="angular-entity-reason"');
+    expect(source).toContain('data-monitor-row-entity-triage-owner="hertzbeat-ui-data-cell-text"');
+    expect(source).toContain('data-monitor-row-entity-response-action="angular-inline-host-action"');
+    expect(source).toContain('data-monitor-row-entity-response-owner="hertzbeat-ui-button"');
+    expect(source).toContain('buildMonitorRowSignalHref');
+    expect(source).toContain("buildMonitorRowSignalHref('logs', item, data.query)");
+    expect(source).toContain("buildMonitorRowSignalHref('traces', item, data.query)");
+    expect(source).toContain('buildMonitorRowCodeHref(item)');
+    expect(source).toContain('data-monitor-row-entity-support-actions="angular-platform-support-action-bar"');
+    expect(source).toContain('data-monitor-row-entity-support-actions-owner="hertzbeat-ui-button-link"');
+    expect(source).toContain('data-monitor-row-entity-support-action="related-logs"');
+    expect(source).toContain('data-monitor-row-entity-support-action="related-traces"');
+    expect(source).toContain('data-monitor-row-entity-support-disabled="missing-code-navigation"');
+    expect(source).toContain("data-monitor-row-actions-entity-context={entityContextActive ? 'detail-edit-copy-only' : undefined}");
+    expect(source).toContain('!entityContextActive && item.status === 0');
+    expect(source).toContain('data-monitor-row-labels-owner="hertzbeat-ui-label-tag"');
+    expect(source).toContain("labelFilterLabel={t('monitor.search.label')}");
+    expect(source).toContain('labelFilterValue={draft.labels}');
+    expect(source).toContain('data-monitors-label-filter-input');
+    expect(source).toContain("'data-monitors-filter-enter-submit': 'search'");
+    expect(source).toContain("'data-monitors-filter-enter-submit': 'labels'");
+    expect(source).toContain("'data-monitor-filter-enter-submit-owner': 'hertzbeat-ui-input'");
+    expect(source).toContain("'data-monitor-label-filter-enter-submit-owner': 'hertzbeat-ui-input'");
+    expect(source).toContain('clearSearchFilter');
+    expect(source).toContain('clearLabelFilter');
+    expect(source).toContain("'data-monitors-filter-clear-action': 'search'");
+    expect(source).toContain("'data-monitors-filter-clear-action': 'labels'");
+    expect(source).toContain("'data-monitor-filter-clear-action-owner': 'hertzbeat-ui-icon-button'");
+    expect(source).toContain("'data-monitor-label-filter-clear-action-owner': 'hertzbeat-ui-icon-button'");
+    expect(source).toContain('data-monitor-row-type-owner="hertzbeat-ui-data-cell-text"');
+    expect(source).toContain('data-monitor-row-updated-owner="hertzbeat-ui-data-cell-text"');
+    expect(source).toContain('display="block"');
+    expect(source).toContain('spacing="stack-tight"');
+    expect(source).toContain('HzLabelTag');
+    expect(source).toContain('renderAngularLabelColor(key)');
+    expect(source).not.toContain('className="block"');
+    expect(source).not.toContain('className="mt-0.5 block"');
+    expect(source).not.toContain('className="mt-1 block"');
+    expect(source).not.toContain('truncate text-[13px] font-semibold text-[#f3f6fb]');
+    expect(source).not.toContain('mt-0.5 truncate font-mono text-[11px] text-[#8f99ab]');
+    expect(source).not.toContain('mt-1 truncate text-[10px] uppercase tracking-[0.12em] text-[#727b8c]');
+    expect(source).not.toContain('font-mono text-[11px] uppercase tracking-[0.14em] text-[#8f99ab]');
+    expect(source).not.toContain('text-[11px] text-[#8f99ab]');
+    expect(source).not.toContain('scrapeLabel');
+    expect(source).not.toContain('采集方式');
+    expect(source).toContain("id: 'import'");
+    expect(source).toContain('onSelect: handleImportClick');
+    expect(source).toContain('importMonitorsFromFacade(api.monitors.import, file)');
+    expect(source).toContain("t('common.notify.import-submitted', { taskName: file.name })");
+    expect(source).toContain("t('common.notify.import-success-detail', { taskName: file.name })");
+    expect(source).toContain("t('common.notify.import-fail-detail'");
+    expect(source).not.toContain('fetch(`/api${buildImportMonitorsUrl()}`');
+    expect(source).toContain('HzExportTypeDialog');
+    expect(source).toContain('openExportDialog');
+    expect(source).toContain('handleExportDialogSelect');
+    expect(source).toContain("id: 'export-selected'");
+    expect(source).toContain("id: 'export-all'");
+    expect(source).toContain("'data-monitors-export-type-trigger': 'selected'");
+    expect(source).toContain("'data-monitors-export-type-trigger': 'all'");
+    expect(source).toContain("'data-monitor-export-type-trigger-owner': 'hertzbeat-ui-batch-toolbar'");
+    expect(source).toContain('data-monitor-export-type-dialog-owner="hertzbeat-ui-export-type-dialog"');
+    expect(source).toContain('data-monitor-export-type-dialog-contract="angular-nz-modal-600-no-footer"');
+    expect(source).toContain('data-monitor-export-type-dialog-flow="angular-trigger-type-modal-before-download"');
+    expect(source).toContain('data-monitors-export-type-dialog={exportDialogScope ?? \'closed\'}');
+    expect(source).toContain("'data-monitor-export-type-option-owner': 'hertzbeat-ui-export-type-dialog'");
+    expect(source).toContain("'data-monitors-export-type-option': 'json'");
+    expect(source).toContain("'data-monitors-export-type-option': 'excel'");
+    expect(source).toContain('api.monitors.exportResponse(selectedIds, type)');
+    expect(source).toContain('api.monitors.exportAllResponse(type)');
+    expect(source).not.toContain('const response = await fetch');
+    expect(source).not.toContain("id: 'export-all-json'");
+    expect(source).not.toContain("id: 'export-all-excel'");
+    expect(source).not.toContain("id: 'export-json'");
+    expect(source).not.toContain("id: 'export-excel'");
+    expect(source).not.toContain("'data-monitors-export-all-action': 'json'");
+    expect(source).not.toContain("'data-monitors-export-all-action': 'excel'");
+    expect(source).not.toContain('<HzButton size="sm" onClick={handleImportClick}>');
+    expect(source).toContain("id: 'enable'");
+    expect(source).toContain('data-monitor-batch-more-menu-contract="angular-toolbar-ellipsis-menu"');
+    expect(source).toContain('data-monitor-batch-more-menu-owner="hertzbeat-ui-batch-toolbar"');
+    expect(source).toContain("overflowLabel={t('monitors.controls.more-actions')}");
+    expect(source).toContain("'data-monitor-batch-more-menu-trigger': 'angular-ellipsis-menu'");
+    expect(source).toContain("'data-monitor-batch-more-menu-panel': 'angular-nz-dropdown-menu'");
+    expect(source).toContain("'data-monitor-batch-more-menu-clearance': 'floating-overlay-no-table-crop'");
+    expect(source).toContain("presentation: 'menu'");
+    expect(source).toContain("'data-monitor-batch-more-menu-action': 'enable'");
+    expect(source).toContain("'data-monitor-batch-more-menu-action': 'pause'");
+    expect(source).toContain("'data-monitor-batch-more-menu-action': 'import'");
+    expect(source).toContain("'data-monitor-batch-more-menu-action': 'export-selected'");
+    expect(source).toContain("'data-monitor-batch-more-menu-action': 'export-all'");
+    expect(source).toContain("'data-monitor-batch-more-menu-action': 'delete'");
+    expect(source).not.toContain("id: 'enable',\n                            label: t('monitors.controls.enable-selected'),\n                            busy: pendingActionId === 'enable',\n                            busyLabel: t('common.notify.enable-pending'),\n                            tone: 'info'");
+    expect(source).toContain("key: 'actions'");
+    expect(source).toContain('data-monitor-row-actions="angular-ellipsis-dropdown"');
+    expect(source).toContain('data-monitor-row-actions-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('data-monitor-row-action-menu="angular-ellipsis-dropdown"');
+    expect(source).toContain('data-monitor-row-action-menu-layer="overlay-visible-above-table"');
+    expect(source).toContain('data-monitor-row-action-menu-clearance="floating-overlay-no-table-crop"');
+    expect(source).toContain('data-monitor-row-action-menu-trigger="angular-ellipsis-dropdown"');
+    expect(source).toContain('data-monitor-row-action-menu-panel="angular-nz-dropdown-menu"');
+    expect(source).toContain('HzButtonLink');
+    expect(source).toContain('HzTableRowActionButton');
+    expect(source).toContain('component={Link}');
+    expect(source).toContain('HzIconButton');
+    expect(source).toContain('data-monitor-row-action-owner="hertzbeat-ui-button-link"');
+    expect(source).toContain('data-monitor-row-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('data-monitor-row-action="detail"');
+    expect(source).toContain('data-monitor-row-action="edit"');
+    expect(source).toContain('data-monitor-row-action="copy"');
+    expect(source).toContain('data-monitor-row-response-action="pause"');
+    expect(source).toContain('data-monitor-row-response-action="enable"');
+    expect(source).toContain('data-monitor-row-response-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('data-monitor-row-delete-action="single"');
+    expect(source).toContain('data-monitor-row-delete-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('{!entityContextActive ? (');
+    expect(source).toContain("openRowResponseConfirm(item, 'pause')");
+    expect(source).toContain("openRowResponseConfirm(item, 'enable')");
+    expect(source).toContain('handleConfirmRowResponse');
+    expect(source).toContain('openDeleteDialog([item.id])');
+    expect(source).toContain('api.monitors.deleteGrafanaDashboard(id)');
+    expect(source).toContain('PauseCircle');
+    expect(source).toContain('PlayCircle');
+    expect(source).toContain('Trash2');
+    expect(source).not.toContain('data-monitor-row-actions="compact-icons"');
+    expect(source).not.toContain('className="flex flex-nowrap items-center gap-1.5"');
+    expect(source).toContain('Pencil');
+    expect(source).toContain('BarChart3');
+    expect(source).toContain('CopyIcon');
+    expect(source).toContain('type MonitorBatchResponseConfirm');
+    expect(source).toContain('const [batchResponseConfirm, setBatchResponseConfirm]');
+    expect(source).toContain("onSelect: () => openBatchResponseConfirm('enable')");
+    expect(source).toContain("onSelect: () => openBatchResponseConfirm('pause')");
+    expect(source).toContain('data-monitor-batch-response-confirm="angular-modal-confirm"');
+    expect(source).toContain('data-monitor-batch-response-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain("title={batchResponseConfirm?.action === 'enable' ? t('common.confirm.enable-batch') : t('common.confirm.cancel-batch')}");
+    expect(source).not.toContain('router.push(buildMonitorEditHref');
+    expect(source).not.toContain("eyebrow={t('monitors.kicker')}");
+    expect(source).not.toContain("查看传统采集资源");
+    expect(source).toContain('data-monitors-open-detail-action="true"');
+    expect(source).not.toContain('HzFilterWorkbench');
+    expect(source).not.toContain('HzQueryBar');
+    expect(source).not.toContain('HzResultControls');
+    expect(source).not.toContain('HzMetricStrip');
+    expect(source).not.toContain('HzWorkbenchSurface');
+    expect(source).not.toContain('buildSelectedMonitorRows');
+    expect(source).not.toContain('buildLabelRows');
+    expect(source).not.toContain('metricStrip=');
+    expect(source).not.toContain('data-monitor-manage-detail-rail');
+    expect(source).not.toContain('xl:grid-cols-[minmax(0,1fr)_');
+    expect(source).not.toContain("t('monitors.rail.");
+    expect(source).not.toContain('DrawerCodePreview');
+    expect(source).not.toContain('JSON.stringify(selected');
+    expect(source).not.toContain('grid gap-3 p-3');
+    expect(source).not.toContain('<WorkbenchPage');
+    expect(source).not.toContain('SummaryMetricGrid');
+    expect(source).not.toContain('StageSection');
+    expect(source).not.toContain('DrawerSection');
+    expect(source).toContain('HzEmptyState');
+    expect(source).toContain('data-monitor-manage-empty-owner="hertzbeat-ui-empty-state"');
+    expect(source).not.toContain('ObservabilityStatusState');
+    expect(source).not.toContain("from '@/components/observability'");
+    expect(source).not.toContain("from '@/components/workbench/primitives'");
+    expect(source).not.toContain("from '@/components/workbench/toolbar'");
+    expect(source).not.toContain("from '@/components/workbench/selectable-evidence-list'");
+  });
+
+  it('keeps monitor batch toolbar buttons on the shared flat/solid button baseline', async () => {
+    const html = await renderMonitorsPage();
+    const batchToolbar = html.match(/<div[^>]*data-hz-ui="batch-toolbar"[\s\S]*?<\/div><\/div>/)?.[0] ?? '';
+    const batchButtons = html.match(/<button[^>]*data-hz-batch-action="[^"]+"[^>]*>/g) ?? [];
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('data-monitor-manage-batch-owner="hertzbeat-ui-batch-toolbar"');
+    expect(source).toContain('variant="embedded"');
+    expect(source).not.toContain('className="border-x-0 border-t-0"');
+    expect(html).toContain('data-monitor-manage-batch-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-hz-batch-toolbar-surface="transparent-lined"');
+    expect(html).toContain('data-hz-batch-toolbar-variant="embedded"');
+    expect(source).not.toContain('flex items-center justify-between border-b border-[var(--hz-ui-line-soft)] px-3 py-2 text-[11px] uppercase tracking-[0.14em] text-[#727b8c]');
+    expect(source).not.toContain("t('monitors.section.list.head')");
+    expect(html).not.toContain('Current results');
+    expect(batchToolbar).not.toContain('bg-[var(--hz-ui-active-soft)]');
+    expect(batchButtons.length).toBeGreaterThan(0);
+    for (const button of batchButtons) {
+      expect(button).toContain('data-hz-ui="button"');
+      expect(button).toContain('data-hz-control-height="28"');
+      expect(button).toMatch(/data-hz-control-edge="(?:flat|solid)"/);
+      expect(button).toMatch(/data-hz-button-tier="(?:flat-neutral|solid-danger)"/);
+      expect(button).toContain('data-hz-batch-action-owner="hertzbeat-ui-button"');
+      expect(button).toContain('data-monitor-manage-batch-action-owner="hertzbeat-ui-button"');
+      expect(button).toContain('data-monitor-manage-batch-action=');
+    }
+    expect(html).toContain('data-monitor-manage-batch-action="select-all-results"');
+    expect(html).toContain('data-monitor-manage-batch-action="select-page"');
+    expect(html).toContain('data-monitor-manage-batch-action="export-selected"');
+    expect(html).toContain('data-monitor-manage-batch-action="export-all"');
+    expect(html).toContain('data-monitor-manage-batch-action="delete"');
+  });
+
+  it('routes the main monitor results through the shared HzDataTable instead of an evidence-list card surrogate', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('HzDataTable,');
+    expect(source).toContain('data-monitor-manage-list-owner="hertzbeat-ui-data-table"');
+    expect(source).toContain('data-monitor-manage-list-variant="embedded"');
+    expect(source).toContain('<HzDataTable');
+    expect(source).toContain('variant="embedded"');
+    expect(source).not.toContain('className="border-t-0"');
+    expect(source).not.toContain('<SelectableEvidenceList');
+  });
+
+  it('uses the shared HertzBeat UI checkbox for monitor row selection instead of page-local checkbox chrome', async () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('HzCheckbox');
+    expect(source).toContain('<HzCheckbox');
+    expect(source).toContain('data-monitor-manage-selection-owner="hertzbeat-ui-checkbox"');
+    expect(source).toContain('data-monitors-row-select={String(item.id)}');
+    expect(source).toContain('data-monitor-manage-selection-disabled={isMonitorDisabled(item) ? \'disappeared\' : undefined}');
+    expect(source).toContain("aria-label={t('common.select')}");
+    expect(source).toContain('disabled={isMonitorDisabled(item)}');
+    expect(source).not.toContain('containerClassName="text-[11px] text-[var(--ops-text-secondary)]"');
+    expect(source).not.toMatch(/\slabel=\{t\('common\.select'\)\}/);
+    expect(source).not.toContain("from '@/components/ui/checkbox'");
+    expect(source).not.toContain('<Checkbox');
+    expect(source).not.toContain('type="checkbox"');
+
+    const html = await renderMonitorsPage();
+    expect(html).toContain('data-monitor-manage-selection-owner="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-hz-ui="checkbox"');
+    expect(html).toContain('data-hz-control-height="32"');
+    expect(html).toContain('data-hz-control-edge="lined"');
+    expect(html).toContain('aria-label="Select"');
+    expect(html).not.toContain('data-hz-checkbox-label="true">Select</span>');
+    expect(html).not.toContain('data-cold-checkbox-owner');
+  });
+
+  it('excludes disappeared monitors from Angular-style page selection and disables their row actions', async () => {
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 501,
+            name: 'mysql-production',
+            app: 'mysql',
+            instance: 'db.internal:3306',
+            status: 1,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000
+          },
+          {
+            id: 502,
+            name: 'redis-disappeared',
+            app: 'redis',
+            instance: 'cache.internal:6379',
+            status: 2,
+            scrape: 'static',
+            labels: {},
+            gmtUpdate: 1713200000000,
+            _displayStatus: 'DISAPPEARED'
+          }
+        ],
+        pageIndex: 0,
+        pageSize: 8,
+        totalElements: 2
+      },
+      query: { ...EMPTY_MONITOR_TEST_QUERY },
+      entityWorkbenchFallback: false
+    };
+
+    const { default: MonitorsPage } = await import('./monitor-manage-page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<MonitorsPage initialQuery={{ ...EMPTY_MONITOR_TEST_QUERY }} />);
+      await Promise.resolve();
+    });
+
+    const activeCheckbox = interactionContainer.querySelector(
+      'input[data-monitors-row-select="501"]'
+    ) as HTMLInputElement | null;
+    const disappearedCheckbox = interactionContainer.querySelector(
+      'input[data-monitors-row-select="502"]'
+    ) as HTMLInputElement | null;
+    const selectPageButton = interactionContainer.querySelector(
+      'button[data-hz-batch-action="select-page"]'
+    ) as HTMLButtonElement | null;
+
+    expect(activeCheckbox?.disabled).toBe(false);
+    expect(disappearedCheckbox?.disabled).toBe(true);
+    expect(disappearedCheckbox?.getAttribute('data-monitor-manage-selection-disabled')).toBe('disappeared');
+    expect(interactionContainer.querySelector('tr[data-monitor-row-disabled="disappeared"]')).not.toBeNull();
+    expect(
+      interactionContainer.querySelectorAll('button[data-monitor-row-action-disabled="disappeared"]')
+        .length
+    ).toBeGreaterThanOrEqual(4);
+
+    await act(async () => {
+      selectPageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    const batchToolbar = interactionContainer.querySelector('[data-monitor-manage-batch-owner="hertzbeat-ui-batch-toolbar"]');
+    expect(batchToolbar?.getAttribute('data-hz-batch-selection-count')).toBe('1');
+    expect(activeCheckbox?.checked).toBe(true);
+    expect(disappearedCheckbox?.checked).toBe(false);
+  }, 15000);
+
+  it('uses the shared HertzBeat UI file input for monitor import instead of page-local file input chrome', async () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('HzFileInput');
+    expect(source).toContain('<HzFileInput');
+    expect(source).toContain('data-monitor-manage-import-input-owner="hertzbeat-ui-file-input"');
+    expect(source).toContain('data-monitors-import-file-input="true"');
+    expect(source).not.toContain("from '@/components/ui/file-input'");
+    expect(source).not.toContain('<FileInput');
+    expect(source).not.toContain('data-cold-file-input-owner');
+
+    const html = await renderMonitorsPage();
+    expect(html).toContain('data-monitor-manage-import-input-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-hz-ui="file-input"');
+    expect(html).toContain('data-hz-file-input-control="native-hidden-file"');
+    expect(html).not.toContain('data-cold-file-input-owner');
+  });
+
+  it('canonicalizes display-only return labels out of the live monitor URL', () => {
+    const routeSource = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
+    const queryStateSource = readFileSync(resolve(process.cwd(), 'lib/monitor-manage/query-state.ts'), 'utf8');
+
+    expect(routeSource).toContain('redirect(routeState.canonicalRoute)');
+    expect(queryStateSource).toContain('shouldRedirect: hasDisplayLabelParam(searchParams)');
+    expect(queryStateSource).toContain("firstParam(searchParams.returnTo)?.includes('returnLabel')");
+  });
+
+  it('keeps monitor manage remounts on a short settled cache window while monitor mutations invalidate it', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('MONITOR_MANAGE_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['monitor-manage', monitorListUrl, explicitStatus || 'implicit-status', reloadKey].join('|')");
+    expect(source).toContain('[explicitStatus, monitorListUrl, reloadKey]');
+    expect(source).toContain('setReloadKey(prev => prev + 1)');
+    expect(source).toContain('cacheSettledTtlMs={MONITOR_MANAGE_SETTLED_CACHE_TTL_MS}');
+  });
+
+  it('routes selected monitor deletion through a cold modal instead of native confirm', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).not.toContain('window.confirm');
+    expect(source).not.toContain('confirm(');
+    expect(source).not.toContain('OverlayDialog');
+    expect(source).not.toContain("from '@/components/workbench/overlay-dialog'");
+    expect(source).toContain('HzConfirmDialog');
+    expect(source).toContain('data-monitor-delete-confirm-closable-contract="angular-nz-closable-false"');
+    expect(source).toContain('data-monitor-delete-confirm-ok-contract="angular-nz-ok-danger-primary"');
+    expect(source).toContain('data-monitor-delete-confirm-modal-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-monitor-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-monitor-delete-confirm-closable="angular-nz-closable-false"');
+    expect(source).toContain('data-monitor-delete-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(source).toContain('bodyRhythm="stack"');
+    expect(source).toContain('HzInlineFeedback');
+    expect(source).toContain('data-monitor-delete-confirm-selected-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-monitors-delete-confirm-trigger');
+    expect(source).toContain('data-monitors-delete-confirm="cold-modal"');
+    expect(source).toContain("t('monitors.delete.title')");
+    expect(source).toContain("t('monitors.delete.copy')");
+    expect(source).toContain("t('monitors.delete.confirm')");
+    expect(source).toContain("t('monitors.delete.selected', { count: deleteDialogCount })");
+    expect(source).toContain("t('common.button.cancel')");
+    expect(source).not.toContain('className="space-y-3"');
+    expect(source).not.toContain('border border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface-soft)] px-3 py-2 font-semibold text-[#f3f6fb]');
+  });
+
+  it('routes single-row enable and pause through the Angular modal confirmation contract', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+
+    expect(source).toContain('type MonitorRowResponseConfirm');
+    expect(source).toContain('const [rowResponseConfirm, setRowResponseConfirm]');
+    expect(source).toContain('openRowResponseConfirm(item, item.status === 0 ?');
+    expect(source).toContain("openRowResponseConfirm(item, 'enable')");
+    expect(source).toContain("openRowResponseConfirm(item, 'pause')");
+    expect(source).toContain('data-monitor-row-response-confirm="angular-modal-confirm"');
+    expect(source).toContain('data-monitor-row-response-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain("data-monitor-row-response-confirm-action={rowResponseConfirm?.action ?? 'closed'}");
+    expect(source).toContain('data-monitor-response-confirm-closable-contract="angular-nz-closable-false"');
+    expect(source).toContain('data-monitor-response-confirm-ok-contract="angular-nz-ok-danger-primary"');
+    expect(source).toContain('data-monitor-response-confirm-modal-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-monitor-row-response-confirm-closable="angular-nz-closable-false"');
+    expect(source).toContain('data-monitor-row-response-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(source).toContain('data-monitor-batch-response-confirm-closable="angular-nz-closable-false"');
+    expect(source).toContain('data-monitor-batch-response-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(source).toContain("title={rowResponseConfirm?.action === 'enable' ? t('common.confirm.enable') : t('common.confirm.cancel')}");
+    expect(source).toContain("confirmLabel={t('common.button.ok')}");
+    expect(source).toContain("cancelLabel={t('common.button.cancel')}");
+    expect(source).toContain('data-monitor-row-response-confirm-selected-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).not.toContain('onClick={() => void handleEnableRow(item)}');
+    expect(source).not.toContain('onClick={() => void handlePauseRow(item)}');
   });
 });
diff --git a/web-next/app/monitors/page.tsx b/web-next/app/monitors/page.tsx
index 15c752df0a..c13c6e881c 100644
--- a/web-next/app/monitors/page.tsx
+++ b/web-next/app/monitors/page.tsx
@@ -1,754 +1,20 @@
-'use client';
-
 import React from 'react';
-import Link from 'next/link';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { useCallback, useEffect, useRef, useState } from 'react';
-import {
-  DrawerCodePreview,
-  DrawerSection,
-  ObservabilityStatusState,
-  SelectableEvidenceList,
-  StageSection,
-  SummaryMetricGrid,
-  ToolbarField,
-  ToolbarRow
-} from '@/components/observability';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { Badge } from '@/components/ui/badge';
-import { Button } from '@/components/ui/button';
-import { Checkbox } from '@/components/ui/checkbox';
-import { FileInput } from '@/components/ui/file-input';
-import { Input } from '@/components/ui/input';
-import { Select } from '@/components/ui/select';
-import { OverlayDialog } from '@/components/workbench/overlay-dialog';
-import { RowList, WorkbenchPage } from '@/components/workbench/workbench-page';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, getAuthorizationToken, getCurrentLocale } from '@/lib/api-client';
-import { formatTime } from '@/lib/format';
-import {
-  buildExportAllMonitorsUrl,
-  buildExportMonitorsUrl,
-  buildImportMonitorsUrl,
-  copyMonitor,
-  deleteGrafanaDashboard,
-  deleteMonitors,
-  enableMonitor,
-  enableMonitors,
-  pauseMonitor,
-  pauseMonitors,
-  resolveDownloadFilename
-} from '@/lib/monitor-manage/controller';
-import { buildLabelRows, statusLabel } from '@/lib/monitor-manage/display-mapping';
-import {
-  buildMonitorDetailHref,
-  buildMonitorEditHref,
-  buildMonitorEntityReturnHref,
-  buildMonitorNewHref,
-  resolveMonitorCheckboxSelection
-} from '@/lib/monitor-manage/navigation';
-import {
-  applyMonitorWorkspaceDefaults,
-  buildMonitorRouteUrl,
-  buildMonitorUrl,
-  hasMonitorEntityContext,
-  queryStateFromParams,
-  type MonitorQueryState
-} from '@/lib/monitor-manage/query-state';
-import {
-  buildMonitorMetrics,
-  buildMonitorWorkbenchNarrative,
-  buildSelectedMonitorRows,
-  shouldFallbackMonitorEntityWorkbench
-} from '@/lib/monitor-manage/view-model';
-import type { Monitor, PageResult } from '@/lib/types';
-
-type MonitorPageData = {
-  list: PageResult<Monitor>;
-  query: MonitorQueryState;
-  entityWorkbenchFallback: boolean;
-};
-
-
-export default function MonitorsPage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const router = useRouter();
-  const [draft, setDraft] = useState<MonitorQueryState>(() =>
-    applyMonitorWorkspaceDefaults(queryStateFromParams(searchParams))
-  );
-  const [query, setQuery] = useState<MonitorQueryState>(() =>
-    applyMonitorWorkspaceDefaults(queryStateFromParams(searchParams))
-  );
-  const [selectedId, setSelectedId] = useState<number | null>(null);
-  const [checkedIds, setCheckedIds] = useState<number[]>([]);
-  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
-  const [reloadKey, setReloadKey] = useState(0);
-  const [actionMessage, setActionMessage] = useState<string | null>(null);
-  const [actionError, setActionError] = useState<string | null>(null);
-  const importInputRef = useRef<HTMLInputElement | null>(null);
-  const explicitStatus = searchParams.get('status') || '';
-
-  useEffect(() => {
-    const nextQuery = applyMonitorWorkspaceDefaults(queryStateFromParams(searchParams));
-    setDraft(nextQuery);
-    setQuery(nextQuery);
-    const hasDisplayLabelParam = searchParams.has('returnLabel') || Boolean(searchParams.get('returnTo')?.includes('returnLabel'));
-    if (hasDisplayLabelParam) {
-      const canonicalRoute = buildMonitorRouteUrl(nextQuery);
-      const currentQuery = searchParams.toString();
-      const currentRoute = currentQuery ? `/monitors?${currentQuery}` : '/monitors';
-      if (canonicalRoute !== currentRoute) {
-        router.replace(canonicalRoute);
-      }
-    }
-  }, [router, searchParams]);
-
-  const load = useCallback(async (): Promise<MonitorPageData> => {
-    void reloadKey;
-    const list = await apiMessageGet<PageResult<Monitor>>(buildMonitorUrl(query));
-    const entityContextActive = hasMonitorEntityContext(query);
-    const statusWasImplicit = explicitStatus.trim() === '';
-
-    if (
-      shouldFallbackMonitorEntityWorkbench({
-        entityContextActive,
-        statusWasImplicit,
-        status: query.status,
-        totalElements: list.totalElements || 0,
-        fellBackToAll: false
-      })
-    ) {
-      const fallbackQuery = {
-        ...query,
-        status: ''
-      };
-      const fallbackList = await apiMessageGet<PageResult<Monitor>>(buildMonitorUrl(fallbackQuery));
-      return {
-        list: fallbackList,
-        query: fallbackQuery,
-        entityWorkbenchFallback: true
-      };
-    }
-
-    return {
-      list,
-      query,
-      entityWorkbenchFallback: false
-    };
-  }, [explicitStatus, query, reloadKey]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('monitors.loading')}>
-      {data => {
-        const selected = data.list.content.find(item => item.id === selectedId) ?? data.list.content[0] ?? null;
-        const labelRows = buildLabelRows(selected?.labels, t);
-        const resolvedPageIndex = Math.max(0, Number.parseInt(data.query.pageIndex || String(data.list.pageIndex ?? 0), 10) || 0);
-        const resolvedPageSize = Math.max(1, Number.parseInt(data.query.pageSize || String(data.list.pageSize ?? 8), 10) || 8);
-        const totalPages = Math.max(1, Math.ceil((data.list.totalElements || 0) / resolvedPageSize));
-        const entityContextActive = hasMonitorEntityContext(query);
-        const downCount = data.list.content.filter(item => item.status === 2).length;
-        const badgeVariantForStatus = (status?: number) => {
-          if (status === 1) return 'success' as const;
-          if (status === 2) return 'danger' as const;
-          return 'default' as const;
-        };
-
-        const resetQuery = () => {
-          const empty = applyMonitorWorkspaceDefaults({
-            search: '',
-            app: '',
-            labels: '',
-            status: '',
-            pageIndex: '',
-            pageSize: '',
-            entityId: query.entityId,
-            entityName: query.entityName,
-            returnTo: query.returnTo
-          });
-          setDraft(empty);
-          setQuery(empty);
-          setCheckedIds([]);
-          setSelectedId(null);
-          router.replace(buildMonitorRouteUrl(empty));
-        };
-
-        const applyQuery = () => {
-          const nextQuery = {
-            ...draft,
-            pageIndex: '0',
-            pageSize: draft.pageSize || query.pageSize || '8'
-          };
-          setQuery(nextQuery);
-          setCheckedIds([]);
-          setSelectedId(null);
-          router.replace(buildMonitorRouteUrl(nextQuery));
-        };
-
-        const navigateWithQuery = (nextQuery: MonitorQueryState) => {
-          setDraft(nextQuery);
-          setQuery(nextQuery);
-          setCheckedIds([]);
-          setSelectedId(null);
-          router.replace(buildMonitorRouteUrl(nextQuery));
-        };
-
-        const checkedCount = checkedIds.length;
-        const currentPageIds = data.list.content.map(item => item.id);
-        const allVisibleSelected = currentPageIds.length > 0 && currentPageIds.every(id => checkedIds.includes(id));
-        const selectedSummary = selected ? `${selected.name} · ${statusLabel(selected.status, t)}` : t('common.none');
-        const navigationContext = {
-          app: data.query.app || selected?.app || 'website',
-          labels: data.query.labels,
-          pageIndex: String(resolvedPageIndex),
-          pageSize: String(resolvedPageSize),
-          entityId: data.query.entityId,
-          entityName: data.query.entityName,
-          returnTo: buildMonitorRouteUrl(data.query)
-        };
-
-        const downloadExport = async (path: string, fallbackName: string) => {
-          const token = getAuthorizationToken();
-          const locale = getCurrentLocale();
-          const response = await fetch(path.startsWith('/api') ? path : `/api${path}`, {
-            headers: {
-              ...(token ? { Authorization: `Bearer ${token}` } : {}),
-              ...(locale ? { 'Accept-Language': locale } : {})
-            },
-            cache: 'no-store'
-          });
-          if (!response.ok) {
-            throw new Error(`Export failed: ${response.status}`);
-          }
-          const contentType = response.headers.get('Content-Type') || '';
-          if (contentType.includes('application/json')) {
-            throw new Error(t('common.notify.export-fail'));
-          }
-          const blob = await response.blob();
-          const url = window.URL.createObjectURL(blob);
-          const anchor = document.createElement('a');
-          anchor.href = url;
-          anchor.download = resolveDownloadFilename(response.headers.get('Content-Disposition'), fallbackName);
-          anchor.click();
-          window.URL.revokeObjectURL(url);
-        };
-
-        const uploadImport = async (file: File) => {
-          const token = getAuthorizationToken();
-          const locale = getCurrentLocale();
-          const formData = new FormData();
-          formData.append('file', file);
-          const response = await fetch(`/api${buildImportMonitorsUrl()}`, {
-            method: 'POST',
-            headers: {
-              ...(token ? { Authorization: `Bearer ${token}` } : {}),
-              ...(locale ? { 'Accept-Language': locale } : {})
-            },
-            body: formData
-          });
-          if (!response.ok) {
-            throw new Error(`Import failed: ${response.status}`);
-          }
-          const payload = (await response.json()) as { code: number; msg?: string };
-          if (payload.code !== 0) {
-            throw new Error(payload.msg || t('common.notify.import-fail'));
-          }
-        };
-
-        const handleCopyMonitor = async () => {
-          if (!selected?.id) return;
-          try {
-            await copyMonitor(apiMessagePost as any, selected.id);
-            setActionMessage(t('monitor.copy.success'));
-            setActionError(null);
-            setReloadKey(prev => prev + 1);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('monitor.copy.failed'));
-          }
-        };
-
-        const handleEnableMonitor = async () => {
-          if (!selected?.id) return;
-          try {
-            await enableMonitor(apiMessageGet as any, selected.id);
-            setActionMessage(t('common.notify.enable-success'));
-            setActionError(null);
-            setReloadKey(prev => prev + 1);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.notify.enable-fail'));
-          }
-        };
-
-        const handlePauseMonitor = async () => {
-          if (!selected?.id) return;
-          try {
-            await pauseMonitor(apiMessageDelete as any, selected.id);
-            setActionMessage(t('common.notify.cancel-success'));
-            setActionError(null);
-            setReloadKey(prev => prev + 1);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.notify.cancel-fail'));
-          }
-        };
-
-        const handleDeleteGrafanaDashboard = async () => {
-          if (!selected?.id) return;
-          try {
-            await deleteGrafanaDashboard(apiMessageDelete as any, selected.id);
-            setActionMessage(t('common.delete-success'));
-            setActionError(null);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.delete-failed'));
-          }
-        };
-
-        const handleDeleteSelected = async () => {
-          if (checkedIds.length === 0) {
-            setDeleteDialogOpen(false);
-            return;
-          }
-          try {
-            await deleteMonitors(apiMessageDelete as any, checkedIds);
-            setActionMessage(t('common.delete-success'));
-            setActionError(null);
-            setCheckedIds([]);
-            setDeleteDialogOpen(false);
-            setReloadKey(prev => prev + 1);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.delete-failed'));
-          }
-        };
-
-        const handleSelectVisible = () => {
-          setCheckedIds(currentPageIds);
-        };
-
-        const handleClearSelection = () => {
-          setCheckedIds([]);
-        };
-
-        const handleExportSelected = async (type: 'JSON' | 'EXCEL') => {
-          if (checkedIds.length === 0) {
-            setActionError(t('common.notify.no-select-export'));
-            return;
-          }
-          try {
-            await downloadExport(
-              buildExportMonitorsUrl(checkedIds, type),
-              type === 'JSON' ? 'monitors.json' : 'monitors.xlsx'
-            );
-            setActionMessage(t('common.notify.export-success'));
-            setActionError(null);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.notify.export-fail'));
-          }
-        };
-
-        const handleExportAll = async (type: 'JSON' | 'EXCEL') => {
-          try {
-            await downloadExport(
-              buildExportAllMonitorsUrl(type),
-              type === 'JSON' ? 'all-monitors.json' : 'all-monitors.xlsx'
-            );
-            setActionMessage(t('common.notify.export-success'));
-            setActionError(null);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.notify.export-fail'));
-          }
-        };
-
-        const handleImportClick = () => {
-          importInputRef.current?.click();
-        };
-
-        const handleImportChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
-          const file = event.target.files?.[0];
-          event.target.value = '';
-          if (!file) return;
-          try {
-            await uploadImport(file);
-            setActionMessage(t('common.notify.import-submitted'));
-            setActionError(null);
-            setReloadKey(prev => prev + 1);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.notify.import-fail'));
-          }
-        };
-
-        const handleEnableSelected = async () => {
-          if (checkedIds.length === 0) return;
-          try {
-            await enableMonitors(apiMessageGet as any, checkedIds);
-            setActionMessage(t('common.notify.enable-success'));
-            setActionError(null);
-            setCheckedIds([]);
-            setReloadKey(prev => prev + 1);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.notify.enable-fail'));
-          }
-        };
-
-        const handlePauseSelected = async () => {
-          if (checkedIds.length === 0) return;
-          try {
-            await pauseMonitors(apiMessageDelete as any, checkedIds);
-            setActionMessage(t('common.notify.cancel-success'));
-            setActionError(null);
-            setCheckedIds([]);
-            setReloadKey(prev => prev + 1);
-          } catch (error) {
-            setActionError(error instanceof Error ? error.message : t('common.notify.cancel-fail'));
-          }
-        };
-
-        return (
-          <>
-            <WorkbenchPage
-            kicker={t('monitors.kicker')}
-            title={t('monitors.title')}
-            subtitle={buildMonitorWorkbenchNarrative(
-              {
-                entityContextActive,
-                total: data.list.totalElements || 0,
-                downCount,
-                status: data.query.status,
-                fellBackToAll: data.entityWorkbenchFallback
-              },
-              t
-            )}
-            tone="operator"
-            facts={[
-              { label: t('common.workspace'), value: t('monitors.workspace') },
-              { label: t('monitors.fact.total'), value: String(data.list.totalElements ?? 0) },
-              { label: t('monitors.fact.page-count'), value: String(data.list.content?.length ?? 0) },
-              { label: t('monitors.fact.filter-status'), value: data.query.status ? statusLabel(Number(data.query.status), t) : t('common.all') }
-            ]}
-            actions={
-              <>
-                <Button size="sm" variant="subtle" onClick={resetQuery}>
-                  {t('common.clear')}
-                </Button>
-                <Button size="sm" variant="subtle" onClick={allVisibleSelected ? handleClearSelection : handleSelectVisible}>
-                  {allVisibleSelected ? t('common.clear-selection') : t('common.select-all')}
-                </Button>
-                {entityContextActive ? (
-                  <Link href={buildMonitorEntityReturnHref(query)}>
-                    <Button size="sm" variant="default">
-                      {query.entityName || t('entity.response.context.return')}
-                    </Button>
-                  </Link>
-                ) : null}
-                {!entityContextActive ? (
-                  <>
-                    <FileInput
-                      ref={importInputRef}
-                      data-monitors-import-file-input="true"
-                      onChange={event => void handleImportChange(event)}
-                    />
-                    <Link href={buildMonitorNewHref(navigationContext)}>
-                      <Button size="sm" variant="subtle">{t('monitor.new-monitor')}</Button>
-                    </Link>
-                    <Button size="sm" variant="subtle" onClick={handleImportClick}>
-                      {t('common.import')}
-                    </Button>
-                  </>
-                ) : null}
-                <Button size="sm" variant="primary" onClick={applyQuery}>
-                  {t('common.apply-filters')}
-                </Button>
-                {selected ? (
-                  <Link href={buildMonitorDetailHref(selected.id, navigationContext)}>
-                    <Button size="sm" variant="default" data-monitors-open-detail-action="true">
-                      {t('monitors.open-detail')}
-                    </Button>
-                  </Link>
-                ) : null}
-                {selected ? (
-                  <Link href={buildMonitorEditHref(selected.id, navigationContext)}>
-                    <Button size="sm" variant="default">{t('monitor.edit-monitor')}</Button>
-                  </Link>
-                ) : null}
-              </>
-            }
-            main={
-              <div className="space-y-3">
-                <ToolbarRow className="grid md:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)_160px]">
-                  <ToolbarField label={t('common.search')}>
-                    <Input
-                      data-monitors-search-input="true"
-                      placeholder={t('monitors.search.placeholder')}
-                      value={draft.search}
-                      onChange={e => setDraft(prev => ({ ...prev, search: e.target.value }))}
-                    />
-                  </ToolbarField>
-                  <ToolbarField label={t('common.app')}>
-                    <Input
-                      data-monitors-app-input="true"
-                      placeholder={t('common.app.placeholder')}
-                      value={draft.app}
-                      onChange={e => setDraft(prev => ({ ...prev, app: e.target.value }))}
-                    />
-                  </ToolbarField>
-                  <ToolbarField label={t('common.labels')}>
-                    <Input placeholder={t('monitors.labels.placeholder')} value={draft.labels} onChange={e => setDraft(prev => ({ ...prev, labels: e.target.value }))} />
-                  </ToolbarField>
-                  <ToolbarField label={t('common.status')}>
-                    <Select
-                      data-monitors-status-filter="true"
-                      value={draft.status}
-                      onChange={e => setDraft(prev => ({ ...prev, status: e.target.value }))}
-                    >
-                      <option value="">{t('monitors.status.all')}</option>
-                      <option value="1">{t('monitors.status.up')}</option>
-                      <option value="2">{t('monitors.status.down')}</option>
-                      <option value="0">{t('monitors.status.paused')}</option>
-                    </Select>
-                  </ToolbarField>
-                </ToolbarRow>
-
-                <SummaryMetricGrid items={buildMonitorMetrics(data.list.content, t)} compact tone="operator" />
+import { redirect } from 'next/navigation';
 
-                <StageSection
-                  title={t('monitors.section.list.title')}
-                  description={t('monitors.section.list.copy')}
-                  compact
-                  chrome="plain"
-                >
-                  {data.list.content.length === 0 ? (
-                    <ObservabilityStatusState title={t('monitors.empty-results.title')} copy={t('monitors.empty-results.copy')} />
-                  ) : (
-                    <div className="grid gap-0">
-                      <div className="flex items-center justify-between border-b border-[var(--ops-border-color)] px-3 py-2 text-[11px] uppercase tracking-[0.14em] text-[var(--ops-text-tertiary)]">
-                        <div>{t('monitors.section.list.head')}</div>
-                        <div>{`${t('common.selected')} ${checkedCount} / ${data.list.content.length}`}</div>
-                      </div>
-                      <div className="flex flex-wrap items-center gap-2 border-b border-[var(--ops-border-color)] px-3 py-2.5">
-                        <Button size="sm" variant="subtle" onClick={() => void handleEnableSelected()} disabled={checkedCount === 0}>
-                          {t('monitors.controls.enable-selected')}
-                        </Button>
-                        <Button
-                          size="sm"
-                          variant="subtle"
-                          onClick={() => void handlePauseSelected()}
-                          disabled={checkedCount === 0}
-                          data-monitors-pause-selected-action="true"
-                        >
-                          {t('monitors.controls.pause-selected')}
-                        </Button>
-                        <Button size="sm" variant="subtle" onClick={() => void handleExportSelected('JSON')} disabled={checkedCount === 0}>
-                          {t('common.export-selected-json')}
-                        </Button>
-                        <Button size="sm" variant="subtle" onClick={() => void handleExportSelected('EXCEL')} disabled={checkedCount === 0}>
-                          {t('common.export-selected-excel')}
-                        </Button>
-                        <Button
-                          size="sm"
-                          variant="subtle"
-                          onClick={() => setDeleteDialogOpen(true)}
-                          disabled={checkedCount === 0}
-                          data-monitors-delete-confirm-trigger="cold-modal"
-                        >
-                          {t('common.button.delete')} {checkedCount > 0 ? `(${checkedCount})` : ''}
-                        </Button>
-                      </div>
-                      <SelectableEvidenceList
-                        rows={data.list.content.map(item => ({
-                          key: String(item.id),
-                          title: item.name,
-                          copy: item.instance || `${item.app} · ${item.scrape || '-'}`,
-                          meta: t('monitors.updated-at', { time: formatTime(item.gmtUpdate || item.gmtCreate || null) }),
-                          extra: (
-                            <div className="flex flex-wrap items-center justify-between gap-2 pt-0.5">
-                              <div className="flex flex-wrap items-center gap-2">
-                                <Badge variant={badgeVariantForStatus(item.status)}>{statusLabel(item.status, t)}</Badge>
-                                <span className="text-[10px] uppercase tracking-[0.14em] text-[var(--ops-text-tertiary)]">
-                                  {`${item.app} · scrape ${item.scrape || '-'}`}
-                                </span>
-                              </div>
-                              <div onClick={event => event.stopPropagation()}>
-                                <Checkbox
-                                  data-monitors-row-select={String(item.id)}
-                                  checked={checkedIds.includes(item.id)}
-                                  onChange={event => {
-                                    const nextChecked = event.target.checked;
-                                    setCheckedIds(prev => {
-                                      const next = resolveMonitorCheckboxSelection(prev, selectedId, item.id, nextChecked);
-                                      setSelectedId(next.selectedId);
-                                      return next.checkedIds;
-                                    });
-                                  }}
-                                  containerClassName="min-h-0 text-[11px] text-[var(--ops-text-secondary)]"
-                                  label={t('common.select')}
-                                />
-                              </div>
-                            </div>
-                          )
-                        }))}
-                        selectedKey={selectedId != null ? String(selectedId) : null}
-                        onSelect={key => {
-                          const monitorId = Number(key);
-                          setSelectedId(monitorId);
-                          router.push(buildMonitorDetailHref(monitorId, navigationContext));
-                        }}
-                      />
-                      <div className="flex flex-wrap items-center justify-between gap-3 border-t border-[var(--ops-border-color)] px-3 py-2.5">
-                        <div className="text-xs text-[var(--ops-text-secondary)]">
-                          {`${t('common.page')} ${resolvedPageIndex + 1} / ${totalPages} · ${t('monitor.center.console.result.total')} ${data.list.totalElements || 0}`}
-                        </div>
-                        <div className="flex flex-wrap items-center gap-2">
-                          <span className="text-xs text-[var(--ops-text-secondary)]">{t('common.page-size')}</span>
-                          <Select
-                            value={String(resolvedPageSize)}
-                            onChange={event =>
-                              navigateWithQuery({
-                                ...data.query,
-                                pageIndex: '0',
-                                pageSize: event.target.value
-                              })
-                            }
-                          >
-                            <option value="8">8</option>
-                            <option value="20">20</option>
-                            <option value="50">50</option>
-                          </Select>
-                          <Button
-                            size="sm"
-                            variant="subtle"
-                            disabled={resolvedPageIndex <= 0}
-                            onClick={() =>
-                              navigateWithQuery({
-                                ...data.query,
-                                pageIndex: String(Math.max(0, resolvedPageIndex - 1)),
-                                pageSize: String(resolvedPageSize)
-                              })
-                            }
-                          >
-                            {t('common.previous')}
-                          </Button>
-                          <Button
-                            size="sm"
-                            variant="subtle"
-                            disabled={resolvedPageIndex >= totalPages - 1}
-                            onClick={() =>
-                              navigateWithQuery({
-                                ...data.query,
-                                pageIndex: String(Math.min(totalPages - 1, resolvedPageIndex + 1)),
-                                pageSize: String(resolvedPageSize)
-                              })
-                            }
-                          >
-                            {t('common.next')}
-                          </Button>
-                        </div>
-                      </div>
-                    </div>
-                  )}
-                </StageSection>
-              </div>
-            }
-            side={
-              <div className="space-y-3">
-                <DrawerSection title={t('monitors.rail.selected')} chrome="plain">
-                  <RowList
-                    rows={buildSelectedMonitorRows(selected, t, formatTime, statusLabel)}
-                  />
-                  {selected ? (
-                    <DrawerCodePreview className="mt-2">
-                      {JSON.stringify(selected, null, 2)}
-                    </DrawerCodePreview>
-                  ) : null}
-                </DrawerSection>
+import MonitorManagePage from './monitor-manage-page';
+import { readMonitorManageRouteState, type MonitorManageSearchParams } from '../../lib/monitor-manage/query-state';
 
-                <DrawerSection title={t('monitors.rail.labels')} chrome="plain">
-                  <RowList rows={labelRows.length > 0 ? labelRows : [{ title: t('monitors.empty-labels.title'), copy: t('monitors.empty-labels.copy'), meta: '-' }]} />
-                </DrawerSection>
+export default async function MonitorsRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<MonitorManageSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readMonitorManageRouteState(resolvedSearchParams);
 
-                <DrawerSection title={t('monitors.rail.controls')} chrome="plain">
-                  <div className="grid gap-2">
-                    <section className="grid gap-2 border-b border-[var(--ops-border-color)] pb-2">
-                      <div className="text-[10px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">
-                        {t('monitors.controls.selected.title')}
-                      </div>
-                      <div className="text-[12px] leading-5 text-[var(--ops-text-secondary)]">{selectedSummary}</div>
-                      <div className="flex flex-wrap gap-2">
-                        {selected ? (
-                          <Button size="sm" variant="default" onClick={() => void handleCopyMonitor()} data-monitors-copy-action="true">
-                            {t('monitor.copy.action')}
-                          </Button>
-                        ) : null}
-                        {selected?.status === 0 ? (
-                          <Button size="sm" variant="default" onClick={() => void handleEnableMonitor()}>
-                            {t('common.enable')}
-                          </Button>
-                        ) : null}
-                        {selected && selected.status !== 0 ? (
-                          <Button size="sm" variant="subtle" onClick={() => void handlePauseMonitor()}>
-                            {t('common.pause')}
-                          </Button>
-                        ) : null}
-                        {selected ? (
-                          <Button size="sm" variant="subtle" onClick={() => void handleDeleteGrafanaDashboard()}>
-                            {t('monitor.detail.delete-grafana')}
-                          </Button>
-                        ) : null}
-                      </div>
-                    </section>
+  if (routeState.shouldRedirect) {
+    redirect(routeState.canonicalRoute);
+  }
 
-                    <section className="grid gap-2 pt-1">
-                      <div className="text-[10px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">
-                        {t('monitors.controls.batch.title')}
-                      </div>
-                      <div className="text-[12px] leading-5 text-[var(--ops-text-secondary)]">
-                        {t('monitors.controls.batch.copy')}
-                        {' · '}
-                        {`${t('common.selected')} ${checkedCount}`}
-                      </div>
-                      <div className="flex flex-wrap gap-2">
-                        <Button size="sm" variant="subtle" onClick={() => void handleExportAll('JSON')}>
-                          {t('common.export-all-json')}
-                        </Button>
-                        <Button size="sm" variant="subtle" onClick={() => void handleExportAll('EXCEL')}>
-                          {t('common.export-all-excel')}
-                        </Button>
-                      </div>
-                    </section>
-                  </div>
-                  {actionMessage ? <div className="mt-2 text-sm text-emerald-300">{actionMessage}</div> : null}
-                  {actionError ? <div className="mt-2"><ObservabilityStatusState title={t('common.failed')} copy={actionError} tone="danger" /></div> : null}
-                </DrawerSection>
-              </div>
-            }
-            />
-            <OverlayDialog
-              open={deleteDialogOpen}
-              onClose={() => setDeleteDialogOpen(false)}
-              kicker={t('menu.monitor.center')}
-              title="确认删除监控"
-              maxWidthClassName="max-w-xl"
-              footer={
-                <div className="flex justify-end gap-2">
-                  <Button size="sm" variant="subtle" onClick={() => setDeleteDialogOpen(false)}>
-                    取消
-                  </Button>
-                  <Button size="sm" variant="primary" onClick={() => void handleDeleteSelected()} disabled={checkedCount === 0}>
-                    确认删除
-                  </Button>
-                </div>
-              }
-            >
-              <div
-                data-monitors-delete-confirm="cold-modal"
-                className="space-y-3 text-[12px] leading-6 text-[var(--ops-text-secondary)]"
-              >
-                <p>删除后选中的监控对象会从监控目录移除,相关实体绑定和告警排查入口需要重新归并。</p>
-                <div className="rounded-[4px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-3 py-2 font-semibold text-[var(--ops-text-primary)]">
-                  已选择 {checkedCount} 个监控对象
-                </div>
-              </div>
-            </OverlayDialog>
-          </>
-        );
-      }}
-    </ClientWorkbench>
-  );
+  return <MonitorManagePage initialQuery={routeState.query} explicitStatus={routeState.explicitStatus} />;
 }
diff --git a/web-next/lib/monitor-manage/app-picker.ts b/web-next/lib/monitor-manage/app-picker.ts
new file mode 100644
index 0000000000..01ea8ac590
--- /dev/null
+++ b/web-next/lib/monitor-manage/app-picker.ts
@@ -0,0 +1,68 @@
+type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
+
+export type MonitorAppHierarchyItem = {
+  category?: string | null;
+  value?: string | null;
+  label?: string | null;
+  hide?: boolean | null;
+};
+
+export type MonitorAppPickerItem = {
+  key: string;
+  app: string;
+  label: string;
+  category: string;
+  hidden: boolean;
+};
+
+export type MonitorAppPickerGroup = {
+  key: string;
+  label: string;
+  rows: MonitorAppPickerItem[];
+};
+
+function formatMonitorCategoryLabel(category: string, t: Translator) {
+  const key = `menu.monitor.${category}`;
+  const translated = t(key);
+  return translated && translated !== key ? translated : category.toUpperCase();
+}
+
+export function buildMonitorAppPickerGroups(items: MonitorAppHierarchyItem[], t: Translator) {
+  const groups = new Map<string, MonitorAppPickerGroup>();
+
+  for (const item of items) {
+    const app = item.value?.trim();
+    const category = item.category?.trim();
+    if (!app || !category || category === '__system__') continue;
+
+    if (!groups.has(category)) {
+      groups.set(category, {
+        key: category,
+        label: formatMonitorCategoryLabel(category, t),
+        rows: []
+      });
+    }
+
+    groups.get(category)!.rows.push({
+      key: `${category}:${app}`,
+      app,
+      label: item.label?.trim() || app,
+      category,
+      hidden: item.hide === true
+    });
+  }
+
+  return Array.from(groups.values());
+}
+
+export function filterMonitorAppPickerGroups(groups: MonitorAppPickerGroup[], search: string) {
+  const normalizedSearch = search.trim().toLowerCase();
+  if (!normalizedSearch) return groups;
+
+  return groups
+    .map(group => ({
+      ...group,
+      rows: group.rows.filter(row => row.label.toLowerCase().includes(normalizedSearch))
+    }))
+    .filter(group => group.rows.length > 0);
+}
diff --git a/web-next/lib/monitor-manage/controller.test.ts b/web-next/lib/monitor-manage/controller.test.ts
index 10526717d6..071a9c11a6 100644
--- a/web-next/lib/monitor-manage/controller.test.ts
+++ b/web-next/lib/monitor-manage/controller.test.ts
@@ -11,16 +11,43 @@ import {
   buildPauseMonitorUrl,
   buildPauseMonitorsUrl,
   copyMonitor,
+  copyMonitorFromFacade,
   deleteGrafanaDashboard,
   deleteMonitors,
   enableMonitor,
   enableMonitors,
+  importMonitorsFromFacade,
+  loadMonitorListFromFacade,
   pauseMonitor,
   pauseMonitors,
   resolveDownloadFilename
 } from './controller';
 
 describe('monitor manage controller', () => {
+  it('loads the monitor list through the domain facade reader', async () => {
+    const readMonitorList = vi.fn().mockResolvedValue({ content: [{ id: 42 }], totalElements: 1, pageIndex: 0, pageSize: 8 });
+    const query = {
+      search: 'mysql',
+      app: 'mysql',
+      labels: '',
+      status: '2',
+      pageIndex: '0',
+      pageSize: '8',
+      entityId: '',
+      entityName: '',
+      returnTo: ''
+    };
+
+    await expect(loadMonitorListFromFacade(readMonitorList as any, query)).resolves.toEqual({
+      content: [{ id: 42 }],
+      totalElements: 1,
+      pageIndex: 0,
+      pageSize: 8
+    });
+
+    expect(readMonitorList).toHaveBeenCalledWith(query);
+  });
+
   it('builds copy url and posts null payload', async () => {
     const apiPost = vi.fn().mockResolvedValue({ id: 108 });
 
@@ -29,6 +56,14 @@ describe('monitor manage controller', () => {
     expect(apiPost).toHaveBeenCalledWith('/monitor/copy/42', null);
   });
 
+  it('copies monitors through the domain facade writer', async () => {
+    const writeCopyMonitor = vi.fn().mockResolvedValue({ id: 108 });
+
+    await expect(copyMonitorFromFacade(writeCopyMonitor, 42)).resolves.toEqual({ id: 108 });
+
+    expect(writeCopyMonitor).toHaveBeenCalledWith(42);
+  });
+
   it('builds enable and pause urls', async () => {
     const apiGet = vi.fn().mockResolvedValue(undefined);
     const apiDelete = vi.fn().mockResolvedValue(undefined);
@@ -78,4 +113,14 @@ describe('monitor manage controller', () => {
     expect(resolveDownloadFilename('attachment; filename=monitors.json', 'fallback.json')).toBe('monitors.json');
     expect(resolveDownloadFilename(undefined, 'fallback.json')).toBe('fallback.json');
   });
+
+  it('builds Angular-style import form data through the facade writer', async () => {
+    const writeImportMonitors = vi.fn().mockResolvedValue(undefined);
+    const file = new Blob(['name: mysql'], { type: 'application/x-yaml' }) as File;
+
+    await importMonitorsFromFacade(writeImportMonitors, file);
+
+    expect(writeImportMonitors).toHaveBeenCalledWith(expect.any(FormData));
+    expect(await ((writeImportMonitors.mock.calls[0]?.[0] as FormData).get('file') as Blob).text()).toBe('name: mysql');
+  });
 });
diff --git a/web-next/lib/monitor-manage/controller.ts b/web-next/lib/monitor-manage/controller.ts
index b293dd86ee..daaf94e658 100644
--- a/web-next/lib/monitor-manage/controller.ts
+++ b/web-next/lib/monitor-manage/controller.ts
@@ -1,6 +1,14 @@
+import type { Monitor, PageResult } from '@/lib/types';
+import type { MonitorQueryState } from './query-state';
+
 type ApiPost = <T>(url: string, payload: unknown) => Promise<T>;
 type ApiGet = <T>(url: string) => Promise<T>;
 type ApiDelete = <T>(url: string) => Promise<T>;
+type MonitorListReader = <T = PageResult<Monitor>>(query: MonitorQueryState) => Promise<T>;
+
+export async function loadMonitorListFromFacade(readMonitorList: MonitorListReader, query: MonitorQueryState) {
+  return readMonitorList<PageResult<Monitor>>(query);
+}
 
 export function buildCopyMonitorUrl(monitorId: number | string) {
   return `/monitor/copy/${monitorId}`;
@@ -10,6 +18,10 @@ export async function copyMonitor(apiPost: ApiPost, monitorId: number | string)
   return apiPost<unknown>(buildCopyMonitorUrl(monitorId), null);
 }
 
+export async function copyMonitorFromFacade(writeCopyMonitor: (monitorId: number | string) => Promise<unknown>, monitorId: number | string) {
+  return writeCopyMonitor(monitorId);
+}
+
 export function buildEnableMonitorUrl(monitorId: number | string) {
   return `/monitors/manage?ids=${monitorId}`;
 }
@@ -69,6 +81,12 @@ export function buildImportMonitorsUrl() {
   return '/monitors/import';
 }
 
+export async function importMonitorsFromFacade(writeImportMonitors: (body: FormData) => Promise<unknown>, file: File) {
+  const formData = new FormData();
+  formData.append('file', file);
+  return writeImportMonitors(formData);
+}
+
 export function buildExportMonitorsUrl(ids: Array<number | string>, type: 'JSON' | 'EXCEL') {
   const params = new URLSearchParams();
   ids.forEach(id => params.append('ids', String(id)));
diff --git a/web-next/lib/monitor-manage/display-mapping.test.ts b/web-next/lib/monitor-manage/display-mapping.test.ts
index ae2356a95f..f9d0bcf10f 100644
--- a/web-next/lib/monitor-manage/display-mapping.test.ts
+++ b/web-next/lib/monitor-manage/display-mapping.test.ts
@@ -1,14 +1,27 @@
 import { describe, expect, it, vi } from 'vitest';
-import { buildLabelRows, statusLabel } from './display-mapping';
+import { buildLabelRows, statusBadgeVariant, statusLabel } from './display-mapping';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('monitor display mapping', () => {
   it('maps status values to display labels', () => {
-    expect(statusLabel(1, t)).toBe('运行中');
-    expect(statusLabel(2, t)).toBe('异常');
-    expect(statusLabel(0, t)).toBe('已暂停');
+    expect(statusLabel(1, t)).toBe('正常');
+    expect(statusLabel(2, t)).toBe('宕机');
+    expect(statusLabel(0, t)).toBe('暂停');
+  });
+
+  it('uses runtime English fallbacks when no translator is supplied', () => {
+    expect(statusLabel(1)).toBe('Up');
+    expect(statusLabel(2)).toBe('Down');
+    expect(statusLabel(0)).toBe('Paused');
+  });
+
+  it('maps status values to badge variants', () => {
+    expect(statusBadgeVariant(1)).toBe('success');
+    expect(statusBadgeVariant(2)).toBe('danger');
+    expect(statusBadgeVariant(0)).toBe('default');
+    expect(statusBadgeVariant(null)).toBe('default');
   });
 
   it('maps labels into row objects', () => {
diff --git a/web-next/lib/monitor-manage/display-mapping.ts b/web-next/lib/monitor-manage/display-mapping.ts
index dd23813310..4193bbeeb8 100644
--- a/web-next/lib/monitor-manage/display-mapping.ts
+++ b/web-next/lib/monitor-manage/display-mapping.ts
@@ -1,9 +1,30 @@
+import { SUPPLEMENTAL_MESSAGES } from '../i18n-runtime-messages';
+
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
 
+export type MonitorStatusBadgeVariant = 'default' | 'success' | 'danger';
+export type MonitorStatusTone = 'neutral' | 'success' | 'critical';
+
+const MONITOR_STATUS_UP_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['monitors.status.up'] ?? 'monitors.status.up';
+const MONITOR_STATUS_DOWN_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['monitors.status.down'] ?? 'monitors.status.down';
+const MONITOR_STATUS_PAUSED_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['monitors.status.paused'] ?? 'monitors.status.paused';
+
 export function statusLabel(status: number, t?: Translator): string {
-  if (status === 1) return t ? t('monitors.status.up') : 'UP';
-  if (status === 2) return t ? t('monitors.status.down') : 'DOWN';
-  return t ? t('monitors.status.paused') : 'PAUSED';
+  if (status === 1) return t ? t('monitors.status.up') : MONITOR_STATUS_UP_FALLBACK_LABEL;
+  if (status === 2) return t ? t('monitors.status.down') : MONITOR_STATUS_DOWN_FALLBACK_LABEL;
+  return t ? t('monitors.status.paused') : MONITOR_STATUS_PAUSED_FALLBACK_LABEL;
+}
+
+export function statusBadgeVariant(status?: number | null): MonitorStatusBadgeVariant {
+  if (status === 1) return 'success';
+  if (status === 2) return 'danger';
+  return 'default';
+}
+
+export function monitorStatusTone(status?: number | null): MonitorStatusTone {
+  if (status === 1) return 'success';
+  if (status === 2) return 'critical';
+  return 'neutral';
 }
 
 export function buildLabelRows(labels: Record<string, string> | undefined, t: Translator) {
diff --git a/web-next/lib/monitor-manage/navigation.test.ts b/web-next/lib/monitor-manage/navigation.test.ts
index 327b1bd402..f15a8a5aad 100644
--- a/web-next/lib/monitor-manage/navigation.test.ts
+++ b/web-next/lib/monitor-manage/navigation.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
 import {
   buildMonitorDetailHref,
   buildMonitorEditHref,
+  buildMonitorListCompatRouteUrl,
   buildMonitorListReturnHref,
   buildMonitorEntityReturnHref,
   buildMonitorNewHref,
@@ -85,6 +86,19 @@ describe('monitor manage navigation', () => {
     expect(buildMonitorListReturnHref()).toBe('/monitors');
   });
 
+  it('builds monitor list compatibility redirects with normalized query context', () => {
+    expect(buildMonitorListCompatRouteUrl()).toBe('/monitors');
+    expect(
+      buildMonitorListCompatRouteUrl({
+        app: 'website',
+        labels: 'team=platform',
+        entityId: '42',
+        returnTo: '/entities/42?returnLabel=Checkout Service',
+        returnLabel: 'Monitor list'
+      })
+    ).toBe('/monitors?app=website&labels=team%3Dplatform&entityId=42&returnTo=%2Fentities%2F42');
+  });
+
   it('keeps selected monitor context when checkbox selection changes', () => {
     expect(resolveMonitorCheckboxSelection([], null, 7, true)).toEqual({
       checkedIds: [7],
diff --git a/web-next/lib/monitor-manage/navigation.ts b/web-next/lib/monitor-manage/navigation.ts
index 9b617dd2e4..f3c00bbc9e 100644
--- a/web-next/lib/monitor-manage/navigation.ts
+++ b/web-next/lib/monitor-manage/navigation.ts
@@ -1,5 +1,10 @@
+import { buildCompatRedirectTarget, type SearchParamsRecord } from '../compat/search-params';
 import { stripReturnLabelFromHref } from '../signal-route-context';
 
+export type { SearchParamsRecord } from '../compat/search-params';
+
+export const MONITOR_LIST_ROUTE = '/monitors';
+
 export type MonitorNavigationContext = {
   app?: string | null;
   labels?: string | null;
@@ -54,12 +59,16 @@ export function buildMonitorNewHref(context?: MonitorNavigationContext) {
   return appendMonitorContext('/monitors/new', context);
 }
 
+export function buildMonitorListCompatRouteUrl(searchParams?: SearchParamsRecord) {
+  return buildCompatRedirectTarget(MONITOR_LIST_ROUTE, searchParams);
+}
+
 export function buildMonitorListReturnHref(context?: MonitorNavigationContext) {
   if (isSafeInternalHref(context?.returnTo)) {
     return stripReturnLabelFromHref(context?.returnTo)!;
   }
 
-  return appendMonitorContext('/monitors', {
+  return appendMonitorContext(MONITOR_LIST_ROUTE, {
     app: context?.app,
     labels: context?.labels,
     pageIndex: context?.pageIndex,
diff --git a/web-next/lib/monitor-manage/query-state.test.ts b/web-next/lib/monitor-manage/query-state.test.ts
index a75b506c74..f98ba7183d 100644
--- a/web-next/lib/monitor-manage/query-state.test.ts
+++ b/web-next/lib/monitor-manage/query-state.test.ts
@@ -3,7 +3,8 @@ import {
   applyMonitorWorkspaceDefaults,
   buildMonitorRouteUrl,
   buildMonitorUrl,
-  queryStateFromParams
+  queryStateFromParams,
+  readMonitorManageRouteState
 } from './query-state';
 
 describe('monitor query state codec', () => {
@@ -24,6 +25,24 @@ describe('monitor query state codec', () => {
     });
   });
 
+  it('honors the legacy Angular content query before search and canonicalizes it', () => {
+    const params = new URLSearchParams('content=mysql-primary&search=ignored&app=mysql');
+
+    expect(queryStateFromParams(params)).toMatchObject({
+      search: 'mysql-primary',
+      app: 'mysql'
+    });
+
+    expect(readMonitorManageRouteState({ content: 'mysql-primary', search: 'ignored', app: 'mysql' })).toMatchObject({
+      query: {
+        search: 'mysql-primary',
+        app: 'mysql'
+      },
+      canonicalRoute: '/monitors?search=mysql-primary&app=mysql',
+      shouldRedirect: true
+    });
+  });
+
   it('builds list url from query state', () => {
     expect(
       buildMonitorUrl({
@@ -115,4 +134,60 @@ describe('monitor query state codec', () => {
       }).status
     ).toBe('');
   });
+
+  it('normalizes multi-value URL search params into the first monitor manage query value', () => {
+    expect(
+      readMonitorManageRouteState({
+        search: ['mysql', 'ignored'],
+        app: ['website', 'ignored'],
+        labels: ['team=platform', 'ignored'],
+        status: ['2', '1'],
+        pageIndex: ['3', '0'],
+        pageSize: ['20', '8'],
+        entityId: ['42', '43'],
+        entityName: ['Checkout', 'Other'],
+        returnTo: ['/entities/42?returnLabel=Checkout', '/entities/43'],
+        returnLabel: 'Checkout'
+      })
+    ).toMatchObject({
+      query: {
+        search: 'mysql',
+        app: 'website',
+        labels: 'team=platform',
+        status: '2',
+        pageIndex: '3',
+        pageSize: '20',
+        entityId: '42',
+        entityName: 'Checkout',
+        returnTo: '/entities/42'
+      },
+      explicitStatus: '2',
+      canonicalRoute:
+        '/monitors?search=mysql&app=website&labels=team%3Dplatform&status=2&pageIndex=3&pageSize=20&entityId=42&entityName=Checkout&returnTo=%2Fentities%2F42',
+      shouldRedirect: true
+    });
+  });
+
+  it('canonicalizes display-only return labels out of monitor manage route state', () => {
+    expect(
+      readMonitorManageRouteState({
+        app: 'website',
+        entityId: '42',
+        entityName: 'Checkout Service',
+        returnTo: '/entities/42?returnLabel=Checkout',
+        returnLabel: 'Checkout'
+      })
+    ).toMatchObject({
+      query: {
+        app: 'website',
+        status: '2',
+        entityId: '42',
+        entityName: 'Checkout Service',
+        returnTo: '/entities/42'
+      },
+      explicitStatus: '',
+      canonicalRoute: '/monitors?app=website&status=2&entityId=42&entityName=Checkout+Service&returnTo=%2Fentities%2F42',
+      shouldRedirect: true
+    });
+  });
 });
diff --git a/web-next/lib/monitor-manage/query-state.ts b/web-next/lib/monitor-manage/query-state.ts
index a21eb5ada4..3b44fd6dd6 100644
--- a/web-next/lib/monitor-manage/query-state.ts
+++ b/web-next/lib/monitor-manage/query-state.ts
@@ -1,5 +1,8 @@
+import { createCompatSearchParamReader, type SearchParamsRecord } from '../compat/search-params';
 import { stripReturnLabelFromHref } from '../signal-route-context';
 
+export type MonitorManageSearchParams = SearchParamsRecord;
+
 export type MonitorQueryState = {
   search: string;
   app: string;
@@ -16,13 +19,20 @@ export type SearchParamReader = {
   get(name: string): string | null;
 };
 
+export type MonitorManageRouteState = {
+  query: MonitorQueryState;
+  explicitStatus: string;
+  canonicalRoute: string;
+  shouldRedirect: boolean;
+};
+
 export function hasMonitorEntityContext(query: Pick<MonitorQueryState, 'entityId' | 'entityName' | 'returnTo'>) {
   return Boolean(query.entityId.trim() || query.entityName.trim() || query.returnTo.trim());
 }
 
 export function queryStateFromParams(searchParams: SearchParamReader): MonitorQueryState {
   return {
-    search: searchParams.get('search') || '',
+    search: searchParams.get('content') || searchParams.get('search') || '',
     app: searchParams.get('app') || '',
     labels: searchParams.get('labels') || '',
     status: searchParams.get('status') || '',
@@ -34,6 +44,29 @@ export function queryStateFromParams(searchParams: SearchParamReader): MonitorQu
   };
 }
 
+function firstParam(value: string | string[] | undefined) {
+  return Array.isArray(value) ? value[0] : value;
+}
+
+function hasDisplayLabelParam(searchParams: MonitorManageSearchParams = {}) {
+  return Boolean(firstParam(searchParams.returnLabel) || firstParam(searchParams.returnTo)?.includes('returnLabel'));
+}
+
+function hasLegacyContentParam(searchParams: MonitorManageSearchParams = {}) {
+  return Boolean(firstParam(searchParams.content));
+}
+
+export function readMonitorManageRouteState(searchParams: MonitorManageSearchParams = {}): MonitorManageRouteState {
+  const query = applyMonitorWorkspaceDefaults(queryStateFromParams(createCompatSearchParamReader(searchParams)));
+
+  return {
+    query,
+    explicitStatus: firstParam(searchParams.status) ?? '',
+    canonicalRoute: buildMonitorRouteUrl(query),
+    shouldRedirect: hasDisplayLabelParam(searchParams) || hasLegacyContentParam(searchParams)
+  };
+}
+
 export function applyMonitorWorkspaceDefaults(query: MonitorQueryState): MonitorQueryState {
   if (!query.status.trim() && hasMonitorEntityContext(query)) {
     return {
diff --git a/web-next/lib/monitor-manage/view-model.test.ts b/web-next/lib/monitor-manage/view-model.test.ts
index e628e86add..12fe5a74fc 100644
--- a/web-next/lib/monitor-manage/view-model.test.ts
+++ b/web-next/lib/monitor-manage/view-model.test.ts
@@ -49,6 +49,38 @@ describe('monitor view model', () => {
     ]);
   });
 
+  it('uses localized empty fallback for missing selected monitor rail meta', () => {
+    expect(
+      buildSelectedMonitorRows(
+        null,
+        t,
+        () => '2026-04-10 18:00:00',
+        () => '异常'
+      )
+    ).toEqual([{ title: '还未选择监控', copy: '从列表选择监控实例后,这里会显示操作和上下文。', meta: '无' }]);
+  });
+
+  it('uses localized fallback for missing selected monitor identity facts', () => {
+    expect(
+      buildSelectedMonitorRows(
+        {
+          name: 'orphan-monitor',
+          app: '',
+          instance: '',
+          status: 1,
+          labels: {},
+          gmtCreate: '2026-04-10T10:00:00Z'
+        } as any,
+        t,
+        () => '2026-04-10 18:00:00',
+        () => '正常'
+      )
+    ).toEqual([
+      { title: 'orphan-monitor', copy: '无 · 无', meta: '正常' },
+      { title: '标签', copy: '0', meta: '更新时间 2026-04-10 18:00:00' }
+    ]);
+  });
+
   it('builds entity workbench copy for fallback and empty states', () => {
     expect(
       buildMonitorWorkbenchNarrative(
diff --git a/web-next/lib/monitor-manage/view-model.ts b/web-next/lib/monitor-manage/view-model.ts
index 6b4f6b9653..cc40705102 100644
--- a/web-next/lib/monitor-manage/view-model.ts
+++ b/web-next/lib/monitor-manage/view-model.ts
@@ -36,12 +36,14 @@ export function buildSelectedMonitorRows(
   formatTime: (value?: number | string | null) => string,
   statusLabel: (status: number, t?: (key: string, params?: Record<string, string | number | null | undefined>) => string) => string
 ) {
+  const emptyValue = t('common.none');
+
   if (!selected) {
-    return [{ title: t('monitors.empty-selected.title'), copy: t('monitors.empty-selected.copy'), meta: '-' }];
+    return [{ title: t('monitors.empty-selected.title'), copy: t('monitors.empty-selected.copy'), meta: emptyValue }];
   }
 
   return [
-    { title: selected.name, copy: `${selected.app} · ${selected.instance}`, meta: statusLabel(selected.status, t) },
+    { title: selected.name, copy: `${selected.app || emptyValue} · ${selected.instance || emptyValue}`, meta: statusLabel(selected.status, t) },
     {
       title: t('common.labels'),
       copy: String(Object.keys(selected.labels || {}).length),


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 09/50: feat(web-next): restore overview dashboard parity

Posted by zh...@apache.org.
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 e403551b18751a377b5159696e60c2164274241e
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 00:45:37 2026 +0800

    feat(web-next): restore overview dashboard parity
---
 .../app/overview/{page.tsx => overview-page.tsx}   | 139 ++++++-
 web-next/app/overview/page.test.tsx                | 173 ++++++++-
 web-next/app/overview/page.tsx                     | 416 +--------------------
 .../components/overview/overview-console.test.tsx  |  26 +-
 web-next/components/overview/overview-console.tsx  |  35 +-
 .../overview/overview-detail-dialog.test.tsx       |  26 ++
 .../components/overview/overview-detail-dialog.tsx |  33 +-
 web-next/lib/overview/navigation.test.ts           |  24 +-
 web-next/lib/overview/navigation.ts                |   9 +
 web-next/lib/overview/view-model.test.ts           |  62 +++
 web-next/lib/overview/view-model.ts                |  27 +-
 11 files changed, 496 insertions(+), 474 deletions(-)

diff --git a/web-next/app/overview/page.tsx b/web-next/app/overview/overview-page.tsx
similarity index 79%
copy from web-next/app/overview/page.tsx
copy to web-next/app/overview/overview-page.tsx
index c70cc0b294..deb0761bd4 100644
--- a/web-next/app/overview/page.tsx
+++ b/web-next/app/overview/overview-page.tsx
@@ -2,6 +2,7 @@
 
 import React from 'react';
 import Link from 'next/link';
+import { useQueryClient } from '@tanstack/react-query';
 import { StageSection, SupportPanel } from '@/components/observability';
 import { ClientWorkbench } from '@/components/workbench/client-workbench';
 import { useI18n } from '@/components/providers/i18n-provider';
@@ -19,35 +20,131 @@ import {
   type OverviewImpactedItem,
   type OverviewSummaryItem
 } from '@/components/overview/overview-console';
-import { apiMessageGet } from '@/lib/api-client';
+import { api } from '@/lib/api-facade';
 import { buildOverviewConsoleViewModel } from '@/lib/overview/view-model';
+import { queryKeys } from '@/lib/query-keys';
 import type { DashboardSummary, PageResult, SingleAlert } from '@/lib/types';
 import { OverviewDetailDialog } from '../../components/overview/overview-detail-dialog';
 import { OverviewProblemFocusDialog } from '../../components/overview/overview-problem-focus-dialog';
 import { ThreeSignalDeskShell } from '../../components/pages/three-signal-desk-shell';
 import { buildOverviewSignalDeskHref } from '../../lib/overview/navigation';
 
-type OverviewData = {
+export type OverviewData = {
   summary: DashboardSummary;
   alerts: PageResult<SingleAlert>;
+  summaryFailed?: boolean;
+  alertsFailed?: boolean;
 };
 
-function resolveProblemFocusBadgeVariant(severity: string) {
-  if (severity === 'critical' || severity === 'error') {
+export type OverviewRenderDataState = {
+  renderData: OverviewData;
+  nextReadyData: OverviewData | null;
+  retainedReadyData: boolean;
+};
+
+const OVERVIEW_SUMMARY_URL = '/summary';
+const OVERVIEW_ALERT_LIST_URL = '/alerts?pageIndex=0&pageSize=6&sort=gmtUpdate&order=desc';
+const OVERVIEW_ALERT_LIST_QUERY = { pageIndex: 0, pageSize: 6, sort: 'gmtUpdate', order: 'desc' } as const;
+const OVERVIEW_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_OVERVIEW_SUMMARY: DashboardSummary = { apps: [] };
+const EMPTY_OVERVIEW_ALERTS: PageResult<SingleAlert> = {
+  content: [],
+  totalElements: 0,
+  pageIndex: OVERVIEW_ALERT_LIST_QUERY.pageIndex,
+  pageSize: OVERVIEW_ALERT_LIST_QUERY.pageSize
+};
+
+type OverviewRequestState<T> = {
+  data: T;
+  failed: boolean;
+};
+
+async function loadOverviewRequest<T>(read: () => Promise<T>, fallback: T): Promise<OverviewRequestState<T>> {
+  try {
+    return { data: await read(), failed: false };
+  } catch {
+    return { data: fallback, failed: true };
+  }
+}
+
+async function loadOverviewConsoleData(): Promise<OverviewData> {
+  const [summary, alerts] = await Promise.all([
+    loadOverviewRequest(() => api.overview.summary(), EMPTY_OVERVIEW_SUMMARY),
+    loadOverviewRequest(() => api.overview.alerts(OVERVIEW_ALERT_LIST_QUERY), EMPTY_OVERVIEW_ALERTS)
+  ]);
+
+  return {
+    summary: summary.data,
+    alerts: alerts.data,
+    summaryFailed: summary.failed,
+    alertsFailed: alerts.failed
+  };
+}
+
+export function hasOverviewReadyContext(data: OverviewData): boolean {
+  return (data.summary.apps || []).length > 0 || (data.alerts.content || []).length > 0;
+}
+
+export function resolveOverviewRenderData(data: OverviewData, previousReadyData: OverviewData | null): OverviewRenderDataState {
+  if (hasOverviewReadyContext(data)) {
+    return {
+      renderData: data,
+      nextReadyData: data,
+      retainedReadyData: false
+    };
+  }
+
+  if (previousReadyData) {
+    return {
+      renderData: previousReadyData,
+      nextReadyData: previousReadyData,
+      retainedReadyData: true
+    };
+  }
+
+  return {
+    renderData: data,
+    nextReadyData: null,
+    retainedReadyData: false
+  };
+}
+
+function problemFocusBadgeVariant(severityTone: 'default' | 'success' | 'warning' | 'danger') {
+  if (severityTone === 'danger') {
     return 'danger' as const;
   }
-  if (severity === 'healthy') {
+  if (severityTone === 'success') {
     return 'success' as const;
   }
-  return 'accent' as const;
+  if (severityTone === 'warning') {
+    return 'accent' as const;
+  }
+  return 'default' as const;
 }
 
 export default function OverviewPage() {
   const { t } = useI18n();
+  const queryClient = useQueryClient();
   const [refreshNonce, setRefreshNonce] = React.useState(0);
   const [problemFocusDialogOpen, setProblemFocusDialogOpen] = React.useState(false);
   const [selectedSummaryCard, setSelectedSummaryCard] = React.useState<OverviewSummaryItem | null>(null);
   const [selectedImpactedEntity, setSelectedImpactedEntity] = React.useState<OverviewImpactedItem | null>(null);
+  const lastReadyOverviewDataRef = React.useRef<OverviewData | null>(null);
+  const overviewCacheKey = React.useMemo(
+    () => ['overview', OVERVIEW_SUMMARY_URL, OVERVIEW_ALERT_LIST_URL, refreshNonce].join(':'),
+    [refreshNonce]
+  );
+  const load = React.useCallback(async (): Promise<OverviewData> => {
+    return queryClient.fetchQuery({
+      queryKey: queryKeys.overview.console({
+        summary: OVERVIEW_SUMMARY_URL,
+        alerts: OVERVIEW_ALERT_LIST_URL,
+        refreshNonce
+      }),
+      queryFn: loadOverviewConsoleData,
+      staleTime: 5000
+    });
+  }, [queryClient, refreshNonce]);
 
   function openProblemFocusDialog() {
     setSelectedSummaryCard(null);
@@ -70,18 +167,17 @@ export default function OverviewPage() {
   return (
     <ClientWorkbench
       key={refreshNonce}
-      load={async (): Promise<OverviewData> => {
-        const [summary, alerts] = await Promise.all([
-          apiMessageGet<DashboardSummary>('/summary'),
-          apiMessageGet<PageResult<SingleAlert>>('/alerts?pageIndex=0&pageSize=6&sort=gmtUpdate&order=desc')
-        ]);
-        return { summary, alerts };
-      }}
+      load={load}
       loadingCopy={t('overview.loading')}
+      cacheKey={overviewCacheKey}
+      cacheSettledTtlMs={OVERVIEW_SETTLED_CACHE_TTL_MS}
     >
       {data => {
-        const apps = data.summary.apps || [];
-        const alerts = data.alerts.content || [];
+        const { renderData, nextReadyData, retainedReadyData } = resolveOverviewRenderData(data, lastReadyOverviewDataRef.current);
+        lastReadyOverviewDataRef.current = nextReadyData;
+        const apps = renderData.summary.apps || [];
+        const alerts = renderData.alerts.content || [];
+        const overviewReadPartiallyFailed = Boolean(data.summaryFailed || data.alertsFailed);
         const viewModel = buildOverviewConsoleViewModel(apps, alerts, t);
         const topAlert = alerts[0];
         const setupRoute = buildOverviewSignalDeskHref('/ingestion/otlp?signal=logs', topAlert);
@@ -188,7 +284,11 @@ export default function OverviewPage() {
                 </>
               }
               main={
-                <div className="grid gap-3">
+                <div
+                  className="grid gap-3"
+                  data-overview-request-fallback={overviewReadPartiallyFailed ? 'angular-partial-request-fallback' : undefined}
+                  data-overview-stale-ready-retained={retainedReadyData ? 'angular-has-ready-overview-retains-last-data' : undefined}
+                >
                 <OverviewStatusGrid
                   title={t('dashboard.home.status.title')}
                   description={t('dashboard.home.status.copy')}
@@ -229,7 +329,10 @@ export default function OverviewPage() {
                           chrome="plain"
                           compact
                           actions={
-                            <Badge variant={resolveProblemFocusBadgeVariant(viewModel.problemFocus.severity)}>
+                            <Badge
+                              variant={problemFocusBadgeVariant(viewModel.problemFocus.severityTone)}
+                              data-overview-problem-focus-severity-tone={viewModel.problemFocus.severityTone}
+                            >
                               {viewModel.problemFocus.severityLabel}
                             </Badge>
                           }
@@ -396,7 +499,7 @@ export default function OverviewPage() {
                 title={selectedImpactedEntity ? `${selectedImpactedEntity.name} ${selectedImpactedEntity.type}` : ''}
                 subtitle={t('dashboard.affected.drawer-subtitle')}
                 description={selectedImpactedEntity?.lastIssue || ''}
-                status={selectedImpactedEntity?.severity}
+                statusTone={selectedImpactedEntity?.severityTone}
                 statusLabel={selectedImpactedEntity?.severityLabel}
                 sections={
                   selectedImpactedEntity
diff --git a/web-next/app/overview/page.test.tsx b/web-next/app/overview/page.test.tsx
index 3454fecb1a..7861ccf4bb 100644
--- a/web-next/app/overview/page.test.tsx
+++ b/web-next/app/overview/page.test.tsx
@@ -1,3 +1,5 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -53,6 +55,7 @@ function createOverviewViewModel() {
       title: 'checkout latency spike',
       severity: 'critical',
       severityLabel: 'Critical',
+      severityTone: 'danger',
       entity: 'checkout',
       owner: 'Platform',
       summary: 'Latency high'
@@ -61,7 +64,7 @@ function createOverviewViewModel() {
       { label: 'Alert Trend', value: '1', insight: 'Check alert pressure.', tone: 'danger' }
     ],
     impactedEntities: [
-      { name: 'checkout', type: 'service', severity: 'critical', severityLabel: 'Critical', owner: 'Platform', status: 'impacted', statusLabel: 'Impacted', lastIssue: 'Latency high' }
+      { name: 'checkout', type: 'service', severity: 'critical', severityLabel: 'Critical', severityTone: 'danger', owner: 'Platform', status: 'impacted', statusLabel: 'Impacted', lastIssue: 'Latency high' }
     ],
     activityItems: [
       { title: 'checkout latency spike', detail: 'Platform · checkout', timestamp: '2026-04-16 22:00:00', tone: 'danger', tag: 'Firing' }
@@ -103,7 +106,10 @@ function createOverviewViewModel() {
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
   clientData: createClientData(),
-  viewModel: createOverviewViewModel()
+  viewModel: createOverviewViewModel(),
+  queryClient: {
+    fetchQuery: vi.fn(async ({ queryFn }: { queryFn: () => Promise<unknown> }) => queryFn())
+  }
 }));
 
 const apiMessageGet = vi.fn();
@@ -119,16 +125,28 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 }));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
-  ClientWorkbench: ({ children, load }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown> }) => {
+  ClientWorkbench: ({
+    children,
+    load,
+    loadingCopy
+  }: {
+    children: (data: any) => React.ReactNode;
+    load: () => Promise<unknown>;
+    loadingCopy?: string;
+  }) => {
     mockState.lastLoad = load;
     return (
-      <div data-client-workbench="true">
+      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
         {children(mockState.clientData)}
       </div>
     );
   }
 }));
 
+vi.mock('@tanstack/react-query', () => ({
+  useQueryClient: () => mockState.queryClient
+}));
+
 vi.mock('../../components/workbench/primitives', () => ({
   WorkbenchPanel: ({ as: Component = 'div', children, ...props }: any) => <Component {...props}>{children}</Component>
 }));
@@ -272,10 +290,27 @@ beforeEach(() => {
   mockState.lastLoad = null;
   mockState.clientData = createClientData();
   mockState.viewModel = createOverviewViewModel();
+  mockState.queryClient.fetchQuery.mockReset();
+  mockState.queryClient.fetchQuery.mockImplementation(async ({ queryFn }: { queryFn: () => Promise<unknown> }) => queryFn());
   apiMessageGet.mockReset();
 });
 
 describe('overview page', () => {
+  it('keeps overview remounts on a short settled cache window without bypassing refresh keys', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/overview/overview-page.tsx'), 'utf8');
+
+    expect(source).toContain('OVERVIEW_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('cacheSettledTtlMs={OVERVIEW_SETTLED_CACHE_TTL_MS}');
+    expect(source).toContain('key={refreshNonce}');
+    expect(source).toContain('cacheKey={overviewCacheKey}');
+    expect(source).toContain('queryKeys.overview.console');
+    expect(source).toContain('api.overview.summary()');
+    expect(source).toContain('api.overview.alerts(OVERVIEW_ALERT_LIST_QUERY)');
+    expect(source).toContain('resolveOverviewRenderData(data, lastReadyOverviewDataRef.current)');
+    expect(source).toContain('data-overview-stale-ready-retained');
+    expect(source).not.toContain("import { apiMessageGet } from '@/lib/api-client'");
+  });
+
   it('renders the HertzBeat overview shell and keeps the expected data loader contract', async () => {
     apiMessageGet
       .mockResolvedValueOnce({ apps: [] })
@@ -287,6 +322,7 @@ describe('overview page', () => {
 
     expect(html).toContain('data-workspace-shell="true"');
     expect(html).toContain('data-workspace-shell-rail-width="wide"');
+    expect(html).toContain('data-loading-copy="Loading overview console"');
     expect(html).toContain('checkout latency spike');
     expect(html).toContain('Refresh');
     expect(html).toContain('Alerts');
@@ -317,6 +353,131 @@ describe('overview page', () => {
       ['/summary'],
       ['/alerts?pageIndex=0&pageSize=6&sort=gmtUpdate&order=desc']
     ]);
+    expect(mockState.queryClient.fetchQuery).toHaveBeenCalledWith(
+      expect.objectContaining({
+        queryKey: ['overview', 'console', {
+          summary: '/summary',
+          alerts: '/alerts?pageIndex=0&pageSize=6&sort=gmtUpdate&order=desc',
+          refreshNonce: 0
+        }],
+        staleTime: 5000
+      })
+    );
+  }, 60000);
+
+  it('keeps Angular forkJoin-style summary fallback when alerts still load', async () => {
+    apiMessageGet
+      .mockRejectedValueOnce(new Error('summary unavailable'))
+      .mockResolvedValueOnce({
+        content: [
+          {
+            id: 7,
+            fingerprint: 'alert-7',
+            content: 'collector timeout',
+            status: 'firing',
+            labels: { severity: 'warning', service: 'collector' }
+          }
+        ],
+        totalElements: 1,
+        pageIndex: 0,
+        pageSize: 6
+      });
+
+    const { default: OverviewPage } = await import('./page');
+    renderToStaticMarkup(<OverviewPage />);
+
+    await expect(mockState.lastLoad?.()).resolves.toMatchObject({
+      summary: { apps: [] },
+      alerts: { content: [{ fingerprint: 'alert-7' }] },
+      summaryFailed: true,
+      alertsFailed: false
+    });
+    expect(apiMessageGet.mock.calls).toEqual([
+      ['/summary'],
+      ['/alerts?pageIndex=0&pageSize=6&sort=gmtUpdate&order=desc']
+    ]);
+  }, 60000);
+
+  it('keeps Angular forkJoin-style alert fallback when summary still loads', async () => {
+    apiMessageGet
+      .mockResolvedValueOnce({
+        apps: [
+          { app: 'api', category: 'service', size: 2, availableSize: 2, unAvailableSize: 0, unManageSize: 0 }
+        ]
+      })
+      .mockRejectedValueOnce(new Error('alerts unavailable'));
+
+    const { default: OverviewPage } = await import('./page');
+    renderToStaticMarkup(<OverviewPage />);
+
+    await expect(mockState.lastLoad?.()).resolves.toMatchObject({
+      summary: { apps: [{ app: 'api' }] },
+      alerts: { content: [], totalElements: 0, pageIndex: 0, pageSize: 6 },
+      summaryFailed: false,
+      alertsFailed: true
+    });
+  }, 60000);
+
+  it('marks partial overview read fallback without changing the route shell', async () => {
+    mockState.clientData = {
+      ...createClientData(),
+      summaryFailed: true
+    };
+
+    const { default: OverviewPage } = await import('./page');
+    const html = renderToStaticMarkup(<OverviewPage />);
+
+    expect(html).toContain('data-overview-request-fallback="angular-partial-request-fallback"');
+    expect(html).toContain('data-workspace-shell="true"');
+  }, 60000);
+
+  it('retains the previous ready overview data when a later refresh returns empty fallback data', async () => {
+    const { hasOverviewReadyContext, resolveOverviewRenderData } = await import('./overview-page');
+    const readyData = {
+      summary: {
+        apps: [
+          { app: 'checkout', category: 'service', size: 4, availableSize: 3, unAvailableSize: 1, unManageSize: 0 }
+        ]
+      },
+      alerts: {
+        content: [
+          {
+            id: 7,
+            fingerprint: 'alert-7',
+            content: 'collector timeout',
+            status: 'firing',
+            labels: { severity: 'warning', service: 'collector' }
+          }
+        ],
+        totalElements: 1,
+        pageIndex: 0,
+        pageSize: 6
+      }
+    };
+    const emptyFallbackData = {
+      summary: { apps: [] },
+      alerts: { content: [], totalElements: 0, pageIndex: 0, pageSize: 6 },
+      summaryFailed: true,
+      alertsFailed: true
+    };
+
+    expect(hasOverviewReadyContext(readyData)).toBe(true);
+    expect(hasOverviewReadyContext(emptyFallbackData)).toBe(false);
+    expect(resolveOverviewRenderData(readyData, null)).toEqual({
+      renderData: readyData,
+      nextReadyData: readyData,
+      retainedReadyData: false
+    });
+    expect(resolveOverviewRenderData(emptyFallbackData, readyData)).toEqual({
+      renderData: readyData,
+      nextReadyData: readyData,
+      retainedReadyData: true
+    });
+    expect(resolveOverviewRenderData(emptyFallbackData, null)).toEqual({
+      renderData: emptyFallbackData,
+      nextReadyData: null,
+      retainedReadyData: false
+    });
   }, 60000);
 
   it('keeps the populated overview rail focused on next-step guidance instead of rendering the duplicated workbench recap panel', async () => {
@@ -410,6 +571,7 @@ describe('overview page', () => {
           type: 'service',
           severity: 'healthy',
           severityLabel: 'Healthy',
+          severityTone: 'success',
           owner: 'Platform',
           status: 'healthy',
           statusLabel: 'Stable',
@@ -433,6 +595,7 @@ describe('overview page', () => {
         title: 'No issue needs attention right now',
         severity: 'healthy',
         severityLabel: 'Healthy',
+        severityTone: 'success',
         entity: 'Overall environment',
         owner: 'Platform on call',
         summary: 'There is no issue requiring immediate escalation right now. Start with the trend and affected items.'
@@ -468,6 +631,7 @@ describe('overview page', () => {
         title: 'No issue needs attention right now',
         severity: 'healthy',
         severityLabel: 'Healthy',
+        severityTone: 'success',
         entity: 'Overall environment',
         owner: 'Platform on call',
         summary: 'There is no issue requiring immediate escalation right now. Start with the trend and affected items.'
@@ -483,6 +647,7 @@ describe('overview page', () => {
           type: 'service',
           severity: 'warning',
           severityLabel: 'Warning',
+          severityTone: 'warning',
           owner: 'Platform',
           status: 'impacted',
           statusLabel: 'Impacted',
diff --git a/web-next/app/overview/page.tsx b/web-next/app/overview/page.tsx
index c70cc0b294..5fe10f88d6 100644
--- a/web-next/app/overview/page.tsx
+++ b/web-next/app/overview/page.tsx
@@ -1,417 +1,7 @@
-'use client';
-
 import React from 'react';
-import Link from 'next/link';
-import { StageSection, SupportPanel } from '@/components/observability';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { Badge } from '@/components/ui/badge';
-import { Button, buttonVariants } from '@/components/ui/button';
-import {
-  OverviewActivityTimeline,
-  OverviewChecklist,
-  OverviewGuidancePanel,
-  OverviewImpactedList,
-  OverviewQuickEntryGrid,
-  OverviewSectionAction,
-  OverviewStatusGrid,
-  OverviewSummaryGrid,
-  type OverviewImpactedItem,
-  type OverviewSummaryItem
-} from '@/components/overview/overview-console';
-import { apiMessageGet } from '@/lib/api-client';
-import { buildOverviewConsoleViewModel } from '@/lib/overview/view-model';
-import type { DashboardSummary, PageResult, SingleAlert } from '@/lib/types';
-import { OverviewDetailDialog } from '../../components/overview/overview-detail-dialog';
-import { OverviewProblemFocusDialog } from '../../components/overview/overview-problem-focus-dialog';
-import { ThreeSignalDeskShell } from '../../components/pages/three-signal-desk-shell';
-import { buildOverviewSignalDeskHref } from '../../lib/overview/navigation';
-
-type OverviewData = {
-  summary: DashboardSummary;
-  alerts: PageResult<SingleAlert>;
-};
-
-function resolveProblemFocusBadgeVariant(severity: string) {
-  if (severity === 'critical' || severity === 'error') {
-    return 'danger' as const;
-  }
-  if (severity === 'healthy') {
-    return 'success' as const;
-  }
-  return 'accent' as const;
-}
-
-export default function OverviewPage() {
-  const { t } = useI18n();
-  const [refreshNonce, setRefreshNonce] = React.useState(0);
-  const [problemFocusDialogOpen, setProblemFocusDialogOpen] = React.useState(false);
-  const [selectedSummaryCard, setSelectedSummaryCard] = React.useState<OverviewSummaryItem | null>(null);
-  const [selectedImpactedEntity, setSelectedImpactedEntity] = React.useState<OverviewImpactedItem | null>(null);
-
-  function openProblemFocusDialog() {
-    setSelectedSummaryCard(null);
-    setSelectedImpactedEntity(null);
-    setProblemFocusDialogOpen(true);
-  }
-
-  function openSummaryCardDialog(card: OverviewSummaryItem) {
-    setProblemFocusDialogOpen(false);
-    setSelectedImpactedEntity(null);
-    setSelectedSummaryCard(card);
-  }
-
-  function openImpactedEntityDialog(entity: OverviewImpactedItem) {
-    setProblemFocusDialogOpen(false);
-    setSelectedSummaryCard(null);
-    setSelectedImpactedEntity(entity);
-  }
-
-  return (
-    <ClientWorkbench
-      key={refreshNonce}
-      load={async (): Promise<OverviewData> => {
-        const [summary, alerts] = await Promise.all([
-          apiMessageGet<DashboardSummary>('/summary'),
-          apiMessageGet<PageResult<SingleAlert>>('/alerts?pageIndex=0&pageSize=6&sort=gmtUpdate&order=desc')
-        ]);
-        return { summary, alerts };
-      }}
-      loadingCopy={t('overview.loading')}
-    >
-      {data => {
-        const apps = data.summary.apps || [];
-        const alerts = data.alerts.content || [];
-        const viewModel = buildOverviewConsoleViewModel(apps, alerts, t);
-        const topAlert = alerts[0];
-        const setupRoute = buildOverviewSignalDeskHref('/ingestion/otlp?signal=logs', topAlert);
-        const statusActionHref = '/entities';
-        const statusActionLabel = t('dashboard.home.status.action.entities');
-        const quickEntryItems = viewModel.quickEntryItems.map(item => ({
-          ...item,
-          route: buildOverviewSignalDeskHref(item.route, topAlert)
-        }));
-        const guidanceNextLinks = viewModel.guidanceNextLinks.map(item => ({
-          ...item,
-          route: buildOverviewSignalDeskHref(item.route, topAlert)
-        }));
-        const guidanceLinkLabels = new Set(guidanceNextLinks.map(item => item.label));
-        const condensedQuickEntryItems = quickEntryItems.filter(item => !guidanceLinkLabels.has(item.label));
-        const actionableImpactedEntities = viewModel.impactedEntities.filter(
-          item => item.status !== 'healthy' || item.severity !== 'healthy'
-        );
-        const healthyOverviewPlaceholder = !viewModel.showSetupGuide && viewModel.problemFocus.severity === 'healthy';
-        const statusItems = healthyOverviewPlaceholder
-          ? [
-              {
-                key: 'workspace' as const,
-                label: t('dashboard.home.status.workspace'),
-                value: t('dashboard.home.status.ready'),
-                ready: true
-              },
-              {
-                key: 'ingestion' as const,
-                label: t('dashboard.home.status.ingestion'),
-                value: t('dashboard.home.status.pending'),
-                ready: false
-              },
-              {
-                key: 'entities' as const,
-                label: t('dashboard.home.status.entities'),
-                value: t('dashboard.home.status.pending'),
-                ready: false
-              },
-              {
-                key: 'alerts' as const,
-                label: t('dashboard.home.status.alerts'),
-                value: t('dashboard.home.status.pending'),
-                ready: false
-              }
-            ]
-          : viewModel.workspaceStatusItems;
-        const railChecklistItems = healthyOverviewPlaceholder
-          ? viewModel.checklistItems.map(item => ({
-              ...item,
-              ready: false
-            }))
-          : viewModel.checklistItems;
-        const setupLikeGuidance = viewModel.showSetupGuide || healthyOverviewPlaceholder;
-        const railGuidanceHeadline = setupLikeGuidance
-          ? t('dashboard.guidance.setup.headline')
-          : viewModel.guidanceHeadline;
-        const railGuidanceDescription = setupLikeGuidance
-          ? t('dashboard.guidance.setup.description')
-          : viewModel.guidanceDescription;
-        const railGuidanceReasons = setupLikeGuidance
-          ? [
-              { label: t('dashboard.setup.status.logs'), value: t('dashboard.setup.status.pending') },
-              { label: t('dashboard.setup.status.traces'), value: t('dashboard.setup.status.pending') },
-              { label: t('dashboard.setup.status.metrics'), value: t('dashboard.setup.status.pending') }
-            ]
-          : viewModel.guidanceReasons;
-        const railGuidanceNextLinks = setupLikeGuidance
-          ? []
-          : guidanceNextLinks.map(item => ({
-              label: item.label,
-              description: item.description,
-              href: item.route
-            }));
-
-        return (
-          <>
-            <ThreeSignalDeskShell
-              kicker={t('dashboard.darkops.kicker')}
-              title={t('dashboard.darkops.title')}
-              subtitle={t('dashboard.darkops.subtitle')}
-              showRail
-              showFooter
-              railWidth="wide"
-              actions={
-                <>
-                  <Button
-                    size="sm"
-                    variant="default"
-                    onClick={() => {
-                      setProblemFocusDialogOpen(false);
-                      setSelectedSummaryCard(null);
-                      setSelectedImpactedEntity(null);
-                      setRefreshNonce(current => current + 1);
-                    }}
-                  >
-                    {t('dashboard.darkops.action.refresh')}
-                  </Button>
-                  {!viewModel.showSetupGuide && !healthyOverviewPlaceholder ? (
-                    <Link href="/alerts" className={buttonVariants({ variant: 'primary', size: 'sm' })}>
-                      {t('dashboard.darkops.action.review-alerts')}
-                    </Link>
-                  ) : null}
-                </>
-              }
-              main={
-                <div className="grid gap-3">
-                <OverviewStatusGrid
-                  title={t('dashboard.home.status.title')}
-                  description={t('dashboard.home.status.copy')}
-                  items={statusItems}
-                  density={healthyOverviewPlaceholder ? 'compact' : 'default'}
-                  action={
-                    !viewModel.showSetupGuide && !healthyOverviewPlaceholder ? (
-                      <OverviewSectionAction href={statusActionHref} label={statusActionLabel} />
-                    ) : undefined
-                  }
-                />
-
-                {viewModel.showSetupGuide ? (
-                  <SupportPanel title={t('dashboard.empty.title')} subtitle={t('dashboard.empty.copy')} chrome="plain" expanded>
-                    <div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--ops-text-tertiary)]">
-                      {t('dashboard.empty.title')}
-                    </div>
-                  </SupportPanel>
-                ) : (
-                  <>
-                    {healthyOverviewPlaceholder ? (
-                      <SupportPanel
-                        title={t('dashboard.empty.title')}
-                        subtitle={t('dashboard.empty.copy')}
-                        chrome="default"
-                        tone="default"
-                        expanded
-                      >
-                        <div className="min-h-[48px]" />
-                      </SupportPanel>
-                    ) : null}
-
-                    {!healthyOverviewPlaceholder ? (
-                      <>
-                        <StageSection
-                          title={viewModel.problemFocus.title}
-                          description={viewModel.problemFocus.summary}
-                          chrome="plain"
-                          compact
-                          actions={
-                            <Badge variant={resolveProblemFocusBadgeVariant(viewModel.problemFocus.severity)}>
-                              {viewModel.problemFocus.severityLabel}
-                            </Badge>
-                          }
-                        >
-                          <div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--ops-text-tertiary)]">
-                            {t('dashboard.problem-focus.kicker')}
-                          </div>
-
-                          <div className="mt-3 flex flex-wrap gap-4">
-                            <div className="grid gap-1 border-r border-[var(--ops-border-color)] pr-4">
-                              <div className="text-[11px] font-semibold uppercase tracking-[0.06em] text-[var(--ops-text-tertiary)]">
-                                {t('dashboard.problem-focus.entity')}
-                              </div>
-                              <div className="text-[13px] font-semibold text-[var(--ops-text-primary)]">
-                                {viewModel.problemFocus.entity}
-                              </div>
-                            </div>
-                            <div className="grid gap-1">
-                              <div className="text-[11px] font-semibold uppercase tracking-[0.06em] text-[var(--ops-text-tertiary)]">
-                                {t('dashboard.problem-focus.owner')}
-                              </div>
-                              <div className="text-[13px] font-semibold text-[var(--ops-text-primary)]">
-                                {viewModel.problemFocus.owner}
-                              </div>
-                            </div>
-                          </div>
-
-                          <div className="mt-3 flex flex-wrap gap-2">
-                            <OverviewSectionAction
-                              onClick={openProblemFocusDialog}
-                              label={t('dashboard.problem-focus.open-context')}
-                              variant="primary"
-                            />
-                            <OverviewSectionAction
-                              href="/alerts"
-                              label={t('dashboard.problem-focus.review-alerts')}
-                            />
-                          </div>
-                        </StageSection>
-
-                        <OverviewSummaryGrid
-                          items={viewModel.summaryCards}
-                          onSelect={openSummaryCardDialog}
-                        />
-                      </>
-                    ) : null}
-
-                    {!healthyOverviewPlaceholder ? (
-                      <OverviewQuickEntryGrid
-                        kicker={t('dashboard.quick-entry.kicker')}
-                        title={t('dashboard.quick-entry.title')}
-                        items={condensedQuickEntryItems}
-                      />
-                    ) : null}
 
-                    {!healthyOverviewPlaceholder && actionableImpactedEntities.length > 0 ? (
-                      <OverviewImpactedList
-                        kicker={t('dashboard.affected.kicker')}
-                        title={t('dashboard.affected.title')}
-                        action={
-                          <OverviewSectionAction
-                            href="/entities"
-                            label={t('dashboard.affected.browse-all')}
-                          />
-                        }
-                        items={actionableImpactedEntities}
-                        onOpenItem={openImpactedEntityDialog}
-                      />
-                    ) : null}
-                  </>
-                )}
-                </div>
-              }
-              rail={
-                <div className="grid gap-3">
-                <OverviewGuidancePanel
-                  headline={railGuidanceHeadline}
-                  description={railGuidanceDescription}
-                  density={healthyOverviewPlaceholder ? 'compact' : 'default'}
-                  compactReasons={healthyOverviewPlaceholder}
-                  reasonDensity={healthyOverviewPlaceholder ? 'compact' : 'default'}
-                  primaryAction={
-                    setupLikeGuidance ? (
-                      <OverviewSectionAction
-                        href={setupRoute}
-                        label={t('dashboard.guidance.setup.action')}
-                        variant="primary"
-                      />
-                    ) : (
-                      <OverviewSectionAction
-                        onClick={openProblemFocusDialog}
-                        label={t('dashboard.problem-focus.open-context')}
-                        variant="primary"
-                      />
-                    )
-                  }
-                  secondaryAction={
-                    !setupLikeGuidance ? (
-                      <OverviewSectionAction
-                        href="/alerts"
-                        label={t('dashboard.problem-focus.review-alerts')}
-                      />
-                    ) : undefined
-                  }
-                  reasons={railGuidanceReasons}
-                  nextLinks={railGuidanceNextLinks}
-                  startLabel={t('workspace.guidance.start')}
-                  reasonsLabel={t('workspace.guidance.reasons')}
-                  nextLabel={t('workspace.guidance.next')}
-                />
+import OverviewPage from './overview-page';
 
-                <OverviewChecklist
-                  title={t('dashboard.setup.checklist.title')}
-                  items={railChecklistItems}
-                  density={healthyOverviewPlaceholder ? 'compact' : 'default'}
-                />
-                </div>
-              }
-              footer={
-                <OverviewActivityTimeline
-                  title={t('dashboard.activity.title')}
-                  items={viewModel.activityItems}
-                  emptyText={t('dashboard.activity.empty')}
-                />
-              }
-            />
-            {!viewModel.showSetupGuide ? (
-              <OverviewProblemFocusDialog
-                open={problemFocusDialogOpen}
-                onClose={() => setProblemFocusDialogOpen(false)}
-                kicker={t('dashboard.problem-focus.kicker')}
-                title={viewModel.problemFocus.title}
-                summary={viewModel.problemFocus.summary}
-                subtitle={`${viewModel.problemFocus.entity} · ${viewModel.problemFocus.severityLabel}`}
-                ownerLabel={t('dashboard.problem-focus.owner')}
-                owner={viewModel.problemFocus.owner}
-                entityLabel={t('dashboard.problem-focus.entity')}
-                entity={viewModel.problemFocus.entity}
-                closeLabel={t('common.button.cancel')}
-              />
-            ) : null}
-            {!viewModel.showSetupGuide ? (
-              <OverviewDetailDialog
-                open={selectedSummaryCard !== null}
-                onClose={() => setSelectedSummaryCard(null)}
-                title={selectedSummaryCard?.label || ''}
-                subtitle={t('dashboard.summary.drawer-subtitle')}
-                description={selectedSummaryCard?.hint || ''}
-                sections={
-                  selectedSummaryCard
-                    ? [
-                        { label: t('dashboard.summary.value'), value: selectedSummaryCard.value },
-                        { label: t('dashboard.summary.delta'), value: selectedSummaryCard.delta }
-                      ]
-                    : []
-                }
-                closeLabel={t('common.button.cancel')}
-              />
-            ) : null}
-            {!viewModel.showSetupGuide ? (
-              <OverviewDetailDialog
-                open={selectedImpactedEntity !== null}
-                onClose={() => setSelectedImpactedEntity(null)}
-                title={selectedImpactedEntity ? `${selectedImpactedEntity.name} ${selectedImpactedEntity.type}` : ''}
-                subtitle={t('dashboard.affected.drawer-subtitle')}
-                description={selectedImpactedEntity?.lastIssue || ''}
-                status={selectedImpactedEntity?.severity}
-                statusLabel={selectedImpactedEntity?.severityLabel}
-                sections={
-                  selectedImpactedEntity
-                    ? [
-                        { label: t('dashboard.problem-focus.owner'), value: selectedImpactedEntity.owner },
-                        { label: t('dashboard.affected.status-label'), value: selectedImpactedEntity.statusLabel }
-                      ]
-                    : []
-                }
-                closeLabel={t('common.button.cancel')}
-              />
-            ) : null}
-          </>
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function OverviewRoutePage() {
+  return <OverviewPage />;
 }
diff --git a/web-next/components/overview/overview-console.test.tsx b/web-next/components/overview/overview-console.test.tsx
index 87d1634b0c..7d2e430616 100644
--- a/web-next/components/overview/overview-console.test.tsx
+++ b/web-next/components/overview/overview-console.test.tsx
@@ -60,6 +60,18 @@ describe('overview console primitives', () => {
       />
     );
 
+    const customGuidanceHtml = renderToStaticMarkup(
+      <OverviewGuidancePanel
+        headline="Next: preserve caller-owned labels"
+        description="Custom labels are still passed through by callers that own this workflow language."
+        reasons={[{ label: 'Source', value: 'Caller' }]}
+        nextLinks={[{ label: 'Review', href: '/overview' }]}
+        startLabel="Custom start"
+        reasonsLabel="Custom reasons"
+        nextLabel="Custom next"
+      />
+    );
+
     expect(summaryHtml).toContain('data-overview-summary-grid="true"');
     expect(summaryHtml).toContain('data-overview-summary-item="true"');
     expect(summaryHtml).toContain('data-overview-summary-item-chrome="flat"');
@@ -67,8 +79,11 @@ describe('overview console primitives', () => {
     expect(summaryHtml).toContain('Critical pressure is still active');
     expect(summaryHtml).not.toContain('rounded-[10px] border border-[var(--ops-border-color)]');
     expect(guidanceHtml).toContain('data-overview-guidance="true"');
+    expect(guidanceHtml).toContain('>Next<');
     expect(guidanceHtml).toContain('Next: work the most important issue first');
+    expect(guidanceHtml).toContain('>Reasons<');
     expect(guidanceHtml).toContain('Entities in scope');
+    expect(guidanceHtml).toContain('>After that<');
     expect(guidanceHtml).toContain('href="/log/manage"');
     expect(compactGuidanceHtml).toContain('data-overview-guidance-reasons-layout="pill-row"');
     expect(compactGuidanceHtml).toContain('data-overview-guidance-reasons-density="compact"');
@@ -96,6 +111,11 @@ describe('overview console primitives', () => {
     expect(compactGuidanceHtml).toContain('text-[10px] leading-[1.25]');
     expect(compactGuidanceHtml).toContain('Logs');
     expect(compactGuidanceHtml).toContain('Pending');
+    expect(customGuidanceHtml).toContain('Custom start');
+    expect(customGuidanceHtml).toContain('Custom reasons');
+    expect(customGuidanceHtml).toContain('Custom next');
+    expect(customGuidanceHtml).not.toContain('>Reasons<');
+    expect(customGuidanceHtml).not.toContain('>After that<');
   });
 
   it('switches summary cards and impacted rows to button-backed actions when overview needs drawer-first posture', () => {
@@ -125,6 +145,7 @@ describe('overview console primitives', () => {
             type: 'service',
             severity: 'critical',
             severityLabel: 'Critical',
+            severityTone: 'danger',
             owner: 'Platform',
             statusLabel: 'Impacted',
             lastIssue: 'Latency high'
@@ -140,6 +161,7 @@ describe('overview console primitives', () => {
     expect(impactedHtml).toContain('<button');
     expect(impactedHtml).toContain('type="button"');
     expect(impactedHtml).toContain('checkout');
+    expect(impactedHtml).toContain('data-overview-impacted-severity-tone="danger"');
     expect(impactedHtml).not.toContain('href="/entities?app=checkout"');
   });
 
@@ -168,7 +190,7 @@ describe('overview console primitives', () => {
       <OverviewChecklist
         title="Next Steps"
         items={[
-          { key: 'logs', label: 'Review logs', ready: false },
+          { key: 'logs', label: 'Review logs', ready: true },
           { key: 'traces', label: 'Review traces', ready: false },
           { key: 'metrics', label: 'Review metrics', ready: false },
           { key: 'alerts', label: 'Create an alert', ready: false }
@@ -184,6 +206,8 @@ describe('overview console primitives', () => {
     expect(checklistHtml).toContain('gap-0 py-1');
     expect(checklistHtml).toContain('text-[10px] leading-[1.25]');
     expect(checklistHtml).toContain('text-[9px] leading-[1.2]');
+    expect(checklistHtml).toContain('>Ready<');
+    expect(checklistHtml).toContain('>Pending<');
     expect(checklistHtml).not.toContain('gap-1.5');
     expect(checklistHtml).not.toContain('py-1.5');
   });
diff --git a/web-next/components/overview/overview-console.tsx b/web-next/components/overview/overview-console.tsx
index 3654276c0b..e055bf912b 100644
--- a/web-next/components/overview/overview-console.tsx
+++ b/web-next/components/overview/overview-console.tsx
@@ -2,6 +2,7 @@
 
 import Link from 'next/link';
 import * as React from 'react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { WorkbenchPanel } from '../../components/workbench/primitives';
 import { Badge } from '../ui/badge';
 import { buttonVariants } from '../ui/button';
@@ -36,6 +37,7 @@ export type OverviewImpactedItem = {
   type: string;
   severity: string;
   severityLabel: string;
+  severityTone: Tone;
   owner: string;
   statusLabel: string;
   lastIssue: string;
@@ -111,6 +113,12 @@ function timelineDotClass(tone: Tone) {
   }
 }
 
+const DEFAULT_OVERVIEW_GUIDANCE_START_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['overview.guidance.default.start'] ?? 'overview.guidance.default.start';
+const DEFAULT_OVERVIEW_GUIDANCE_REASONS_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['overview.guidance.default.reasons'] ?? 'overview.guidance.default.reasons';
+const DEFAULT_OVERVIEW_GUIDANCE_NEXT_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['overview.guidance.default.next'] ?? 'overview.guidance.default.next';
+const OVERVIEW_CHECKLIST_READY_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['overview.checklist.status.ready'] ?? 'overview.checklist.status.ready';
+const OVERVIEW_CHECKLIST_PENDING_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['overview.checklist.status.pending'] ?? 'overview.checklist.status.pending';
+
 export function OverviewStatusGrid({
   title,
   description,
@@ -271,7 +279,10 @@ export function OverviewImpactedList({
                 <span className="text-[12px] text-[var(--ops-text-secondary)]">{item.type} · {item.owner}</span>
               </span>
               <span className="grid justify-items-end gap-1">
-                <span className={cn('text-[12px] font-semibold uppercase', toneTextClass(resolveSeverityTone(item.severity)))}>
+                <span
+                  className={cn('text-[12px] font-semibold uppercase', toneTextClass(item.severityTone))}
+                  data-overview-impacted-severity-tone={item.severityTone}
+                >
                   {item.severityLabel}
                 </span>
                 <span className="text-[12px] text-[var(--ops-text-secondary)]">{item.statusLabel}</span>
@@ -322,9 +333,9 @@ export function OverviewGuidancePanel({
   density = 'default',
   compactReasons = false,
   reasonDensity = 'default',
-  startLabel = 'Next',
-  reasonsLabel = 'Reasons',
-  nextLabel = 'After that'
+  startLabel = DEFAULT_OVERVIEW_GUIDANCE_START_LABEL,
+  reasonsLabel = DEFAULT_OVERVIEW_GUIDANCE_REASONS_LABEL,
+  nextLabel = DEFAULT_OVERVIEW_GUIDANCE_NEXT_LABEL
 }: {
   headline: string;
   description: string;
@@ -531,7 +542,7 @@ export function OverviewChecklist({
                 density === 'compact' ? 'text-[9px] leading-[1.2]' : 'text-[11px] leading-[1.45]'
               )}
             >
-              {item.ready ? 'Ready' : 'Pending'}
+              {item.ready ? OVERVIEW_CHECKLIST_READY_LABEL : OVERVIEW_CHECKLIST_PENDING_LABEL}
             </div>
           </div>
         ))}
@@ -666,17 +677,3 @@ export function OverviewSectionAction({
     </Link>
   );
 }
-
-function resolveSeverityTone(severity: string): Tone {
-  switch (severity.toLowerCase()) {
-    case 'critical':
-    case 'error':
-      return 'danger';
-    case 'warning':
-      return 'warning';
-    case 'healthy':
-      return 'success';
-    default:
-      return 'default';
-  }
-}
diff --git a/web-next/components/overview/overview-detail-dialog.test.tsx b/web-next/components/overview/overview-detail-dialog.test.tsx
new file mode 100644
index 0000000000..3347d4bbcf
--- /dev/null
+++ b/web-next/components/overview/overview-detail-dialog.test.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it } from 'vitest';
+import { OverviewDetailDialog } from './overview-detail-dialog';
+
+describe('OverviewDetailDialog', () => {
+  it('renders the status badge tone supplied by the caller', () => {
+    const html = renderToStaticMarkup(
+      <OverviewDetailDialog
+        open
+        onClose={() => undefined}
+        title="checkout-service service"
+        subtitle="Affected item"
+        description="Latency threshold breached"
+        statusTone="danger"
+        statusLabel="Critical"
+        sections={[{ label: 'Owner', value: 'Platform' }]}
+        closeLabel="Close"
+      />
+    );
+
+    expect(html).toContain('data-overview-detail-dialog="true"');
+    expect(html).toContain('data-overview-detail-status-tone="danger"');
+    expect(html).toContain('Critical');
+  });
+});
diff --git a/web-next/components/overview/overview-detail-dialog.tsx b/web-next/components/overview/overview-detail-dialog.tsx
index c2c2ae5447..bb9049dc19 100644
--- a/web-next/components/overview/overview-detail-dialog.tsx
+++ b/web-next/components/overview/overview-detail-dialog.tsx
@@ -10,18 +10,19 @@ type OverviewDetailSection = {
   value: string;
 };
 
-function resolveBadgeVariant(severity?: string) {
-  switch (`${severity || ''}`.toLowerCase()) {
-    case 'critical':
-    case 'error':
-      return 'danger' as const;
-    case 'healthy':
-      return 'success' as const;
-    case 'warning':
-      return 'accent' as const;
-    default:
-      return 'default' as const;
+type OverviewDetailBadgeTone = 'default' | 'success' | 'warning' | 'danger';
+
+function overviewDetailBadgeVariant(tone: OverviewDetailBadgeTone) {
+  if (tone === 'danger') {
+    return 'danger' as const;
+  }
+  if (tone === 'success') {
+    return 'success' as const;
+  }
+  if (tone === 'warning') {
+    return 'accent' as const;
   }
+  return 'default' as const;
 }
 
 export function OverviewDetailDialog({
@@ -31,7 +32,7 @@ export function OverviewDetailDialog({
   title,
   subtitle,
   description,
-  status,
+  statusTone = 'default',
   statusLabel,
   sections,
   closeLabel
@@ -42,7 +43,7 @@ export function OverviewDetailDialog({
   title: string;
   subtitle: string;
   description: string;
-  status?: string;
+  statusTone?: OverviewDetailBadgeTone;
   statusLabel?: string;
   sections: OverviewDetailSection[];
   closeLabel: string;
@@ -68,7 +69,11 @@ export function OverviewDetailDialog({
             <div className="text-[12px] font-semibold uppercase tracking-[0.08em] text-[var(--ops-text-tertiary)]">{subtitle}</div>
             <div className="text-[13px] leading-[1.6] text-[var(--ops-text-secondary)]">{description}</div>
           </div>
-          {statusLabel ? <Badge variant={resolveBadgeVariant(status)}>{statusLabel}</Badge> : null}
+          {statusLabel ? (
+            <Badge variant={overviewDetailBadgeVariant(statusTone)} data-overview-detail-status-tone={statusTone}>
+              {statusLabel}
+            </Badge>
+          ) : null}
         </div>
         <div className="grid gap-3 sm:grid-cols-2">
           {sections.map(section => (
diff --git a/web-next/lib/overview/navigation.test.ts b/web-next/lib/overview/navigation.test.ts
index 355d041148..69fe58a81c 100644
--- a/web-next/lib/overview/navigation.test.ts
+++ b/web-next/lib/overview/navigation.test.ts
@@ -1,7 +1,7 @@
 import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { describe, expect, it } from 'vitest';
-import { buildOverviewSignalDeskHref } from './navigation';
+import { buildOverviewCompatRouteUrl, buildOverviewSignalDeskHref } from './navigation';
 
 const topAlert = {
   labels: {
@@ -14,9 +14,29 @@ const topAlert = {
 } as const;
 
 describe('overview navigation', () => {
+  it('builds overview compatibility redirects with normalized machine query context', () => {
+    const target = buildOverviewCompatRouteUrl({
+      source: 'root',
+      serviceName: 'checkout',
+      returnTo: '/entities?returnLabel=Catalog',
+      returnLabel: 'Overview',
+      start: '1700000000000',
+      environment: ['prod', 'ignored']
+    });
+    const url = new URL(target, 'http://127.0.0.1');
+
+    expect(url.pathname).toBe('/overview');
+    expect(url.searchParams.get('source')).toBe('root');
+    expect(url.searchParams.get('serviceName')).toBe('checkout');
+    expect(url.searchParams.get('environment')).toBe('prod');
+    expect(url.searchParams.get('returnTo')).toBe('/entities');
+    expect(url.searchParams.get('returnLabel')).toBeNull();
+    expect(url.searchParams.get('start')).toBe('1700000000000');
+  });
+
   it('keeps overview and monitor-editor navigation machine-only without display-label fields', () => {
     const overviewNavigationSource = readFileSync(resolve(process.cwd(), 'lib/overview/navigation.ts'), 'utf8');
-    const overviewPageSource = readFileSync(resolve(process.cwd(), 'app/overview/page.tsx'), 'utf8');
+    const overviewPageSource = readFileSync(resolve(process.cwd(), 'app/overview/overview-page.tsx'), 'utf8');
     const monitorEditorTestSource = readFileSync(resolve(process.cwd(), 'lib/monitor-editor/navigation.test.ts'), 'utf8');
 
     expect(overviewNavigationSource).not.toContain('returnLabel: string');
diff --git a/web-next/lib/overview/navigation.ts b/web-next/lib/overview/navigation.ts
index 2fbe210233..d9c970f464 100644
--- a/web-next/lib/overview/navigation.ts
+++ b/web-next/lib/overview/navigation.ts
@@ -1,6 +1,11 @@
+import { buildCompatRedirectTarget, type SearchParamsRecord } from '../compat/search-params';
 import type { SingleAlert } from '../types';
 import { stripReturnLabelFromHref } from '../signal-route-context';
 
+export type { SearchParamsRecord };
+
+export const OVERVIEW_ROUTE = '/overview';
+
 const OVERVIEW_SIGNAL_DESK_PATHS = new Set([
   '/log/manage',
   '/trace/manage',
@@ -28,6 +33,10 @@ function setIfMissing(params: URLSearchParams, key: string, value: string | unde
   params.set(key, value);
 }
 
+export function buildOverviewCompatRouteUrl(searchParams?: SearchParamsRecord) {
+  return buildCompatRedirectTarget(OVERVIEW_ROUTE, searchParams);
+}
+
 export function buildOverviewSignalDeskHref(
   href: string,
   alert: Pick<SingleAlert, 'labels'> | null | undefined
diff --git a/web-next/lib/overview/view-model.test.ts b/web-next/lib/overview/view-model.test.ts
index cd0dd09b6d..f960fc87ad 100644
--- a/web-next/lib/overview/view-model.test.ts
+++ b/web-next/lib/overview/view-model.test.ts
@@ -7,6 +7,7 @@ import {
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 const t = createTranslatorMock({ locale: 'zh-CN' });
+const enT = createTranslatorMock({ locale: 'en-US' });
 
 describe('overview view model', () => {
   it('builds entity and alert metrics from app summary data', () => {
@@ -51,6 +52,61 @@ describe('overview view model', () => {
     expect(lanes.find(item => item.href === '/log/manage')?.stat).toBe('无链路 ID 时优先');
   });
 
+  it('localizes overview investigation lane titles, copy, stats, and actions', () => {
+    const lanes = buildInvestigationLanes(
+      {
+        totalEntities: 7,
+        appCount: 2,
+        topAlertHasTraceId: false
+      },
+      t
+    );
+
+    expect(lanes.find(item => item.href === '/entities')).toMatchObject({
+      title: '实体目录',
+      eyebrow: '实体优先',
+      copy: '先从监控实体和服务归属定位影响范围,再进入信号排查。',
+      stat: '已纳管 7 个实体',
+      action: '打开实体目录'
+    });
+    expect(lanes.find(item => item.href === '/log/manage')).toMatchObject({
+      title: '日志',
+      eyebrow: '运行证据',
+      copy: '用相同服务、实体和时间上下文查看日志事件。',
+      stat: '日志可用',
+      action: '打开日志'
+    });
+    expect(lanes.find(item => item.href === '/trace/manage')).toMatchObject({
+      title: '链路',
+      eyebrow: '请求路径',
+      copy: '存在链路上下文时跟进跨度,再回到实体证据。',
+      stat: '分布式链路就绪',
+      action: '打开链路'
+    });
+    expect(lanes.find(item => item.href === '/ingestion/otlp/metrics')).toMatchObject({
+      title: 'OTLP 指标',
+      eyebrow: '三信号接入',
+      copy: '在私有部署的 HertzBeat 工作区查看进入的指标序列。',
+      stat: '已接入 2 个监控应用',
+      action: '打开指标'
+    });
+  });
+
+  it('keeps English log lane recommendation copy in English', () => {
+    const lanes = buildInvestigationLanes(
+      {
+        totalEntities: 7,
+        appCount: 2,
+        topAlertHasTraceId: true
+      },
+      enT
+    );
+    const logStat = lanes.find(item => item.href === '/log/manage')?.stat;
+
+    expect(logStat).toBe('Prioritize when trace ID is missing');
+    expect(logStat).not.toMatch(/[\u4e00-\u9fff]/);
+  });
+
   it('builds the HertzBeat overview console structure from app and alert data', () => {
     const viewModel = buildOverviewConsoleViewModel(
       [
@@ -87,8 +143,14 @@ describe('overview view model', () => {
     expect(viewModel.problemFocus).toMatchObject({
       title: 'checkout latency spike',
       severity: 'critical',
+      severityTone: 'danger',
       owner: 'Platform'
     });
+    expect(viewModel.impactedEntities[0]).toMatchObject({
+      name: 'checkout',
+      severity: 'critical',
+      severityTone: 'danger'
+    });
     expect(viewModel.quickEntryItems.map(item => item.route)).toEqual([
       '/entities',
       '/log/manage',
diff --git a/web-next/lib/overview/view-model.ts b/web-next/lib/overview/view-model.ts
index 7305e48e76..9a959c1129 100644
--- a/web-next/lib/overview/view-model.ts
+++ b/web-next/lib/overview/view-model.ts
@@ -2,6 +2,8 @@ import type { AlertSummary, AppCount, SingleAlert } from '@/lib/types';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
 
+type OverviewTone = 'default' | 'success' | 'warning' | 'danger';
+
 type OverviewMetrics = {
   totalEntities: number;
   healthyEntities: number;
@@ -27,13 +29,14 @@ type OverviewSummaryCard = {
   value: string;
   hint: string;
   delta: string;
-  tone: 'default' | 'success' | 'warning' | 'danger';
+  tone: OverviewTone;
 };
 
 type OverviewProblemFocus = {
   title: string;
   severity: string;
   severityLabel: string;
+  severityTone: OverviewTone;
   entity: string;
   owner: string;
   summary: string;
@@ -43,7 +46,7 @@ type OverviewTrendCard = {
   label: string;
   value: string;
   insight: string;
-  tone: 'default' | 'success' | 'warning' | 'danger';
+  tone: OverviewTone;
 };
 
 type OverviewImpactedEntity = {
@@ -51,6 +54,7 @@ type OverviewImpactedEntity = {
   type: string;
   severity: string;
   severityLabel: string;
+  severityTone: OverviewTone;
   owner: string;
   status: string;
   statusLabel: string;
@@ -61,7 +65,7 @@ type OverviewActivityItem = {
   title: string;
   detail: string;
   timestamp: string;
-  tone: 'default' | 'success' | 'warning' | 'danger';
+  tone: OverviewTone;
   tag: string;
 };
 
@@ -332,6 +336,7 @@ function buildProblemFocus(alerts: SingleAlert[], t: Translator): OverviewProble
       title: t('dashboard.problem-focus.empty.title'),
       severity: 'healthy',
       severityLabel: resolveSeverityLabel('healthy', t),
+      severityTone: overviewSeverityTone('healthy'),
       entity: t('dashboard.problem-focus.empty.entity'),
       owner: t('dashboard.problem-focus.empty.owner'),
       summary: t('dashboard.problem-focus.empty.summary')
@@ -343,6 +348,7 @@ function buildProblemFocus(alerts: SingleAlert[], t: Translator): OverviewProble
     title: focus.content || focus.annotations?.summary || t('dashboard.problem-focus.default-title'),
     severity,
     severityLabel: resolveSeverityLabel(severity, t),
+    severityTone: overviewSeverityTone(severity),
     entity: focus.labels?.service || focus.labels?.job || focus.labels?.instance || t('dashboard.problem-focus.default-entity'),
     owner: getAlertOwnerLabel(focus, t),
     summary: focus.annotations?.summary || focus.content || t('dashboard.problem-focus.default-summary')
@@ -401,6 +407,7 @@ function buildImpactedEntities(appCounts: AppCount[], alerts: SingleAlert[], t:
         type: item.category || 'service',
         severity,
         severityLabel: resolveSeverityLabel(severity, t),
+        severityTone: overviewSeverityTone(severity),
         owner: getAlertOwnerLabel(linkedAlert, t),
         status: degraded > 0 ? 'impacted' : 'healthy',
         statusLabel: degraded > 0
@@ -451,6 +458,20 @@ function getAlertSeverity(alert?: SingleAlert) {
   return `${alert?.labels?.severity || alert?.annotations?.severity || 'warning'}`.toLowerCase();
 }
 
+function overviewSeverityTone(severity: string): OverviewTone {
+  switch (severity.toLowerCase()) {
+    case 'critical':
+    case 'error':
+      return 'danger';
+    case 'warning':
+      return 'warning';
+    case 'healthy':
+      return 'success';
+    default:
+      return 'default';
+  }
+}
+
 function getAlertOwnerLabel(alert: SingleAlert | undefined, t: Translator) {
   return alert?.labels?.owner || alert?.annotations?.owner || t('dashboard.owner.unassigned');
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 15/50: feat(web-next): restore entity workbench parity

Posted by zh...@apache.org.
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 5e3edd3f41c81bd8b02a640b80377cc6fdb3a99c
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 01:26:51 2026 +0800

    feat(web-next): restore entity workbench parity
---
 web-next/app/entities/[...rest]/page.test.ts       |   27 +-
 web-next/app/entities/[...rest]/page.tsx           |    8 +-
 .../definition/entity-definition-page.tsx          |   60 +
 .../entities/[entityId]/definition/page.test.tsx   |   69 +-
 .../app/entities/[entityId]/definition/page.tsx    |   34 +-
 .../entities/[entityId]/edit/entity-edit-page.tsx  |   45 +
 .../app/entities/[entityId]/edit/page.test.tsx     |   79 +-
 web-next/app/entities/[entityId]/edit/page.tsx     |   28 +-
 .../{page.tsx => entity-detail-page.tsx}           |   38 +-
 web-next/app/entities/[entityId]/page.test.tsx     |   55 +-
 web-next/app/entities/[entityId]/page.tsx          |   78 +-
 .../entities/discovery/entity-discovery-page.tsx   |   59 +
 web-next/app/entities/discovery/page.test.tsx      |   84 +-
 web-next/app/entities/discovery/page.tsx           |   20 +-
 .../entities/{page.tsx => entity-list-page.tsx}    |   47 +-
 .../app/entities/import/entity-import-page.tsx     |   41 +
 web-next/app/entities/import/page.test.tsx         |   58 +-
 web-next/app/entities/import/page.tsx              |   20 +-
 web-next/app/entities/new/entity-new-page.tsx      |   63 +
 web-next/app/entities/new/page.test.tsx            |  114 +-
 web-next/app/entities/new/page.tsx                 |   40 +-
 web-next/app/entities/not-found.tsx                |    3 +-
 web-next/app/entities/page.test.tsx                |   88 +-
 web-next/app/entities/page.tsx                     |   85 +-
 web-next/app/topology/page.test.tsx                | 1998 +++++++++++++++++++-
 web-next/app/topology/page.tsx                     |  408 +---
 web-next/app/topology/topology-page.tsx            | 1935 +++++++++++++++++++
 .../entity-definition-workspace-surface.test.tsx   |  202 +-
 .../pages/entity-definition-workspace-surface.tsx  |  204 +-
 .../pages/entity-detail-surface.test.tsx           |  160 +-
 .../components/pages/entity-detail-surface.tsx     |  215 ++-
 .../pages/entity-discovery-surface.test.tsx        |   92 +-
 .../components/pages/entity-discovery-surface.tsx  |  205 +-
 .../pages/entity-editor-surface.test.tsx           |  168 +-
 .../components/pages/entity-editor-surface.tsx     |  303 +--
 .../components/pages/entity-list-surface.test.tsx  |   16 +-
 web-next/components/pages/entity-list-surface.tsx  |   87 +-
 web-next/lib/api-facade.test.ts                    |  993 ++++++++++
 web-next/lib/api-facade.ts                         |  302 +++
 web-next/lib/entity-definition/controller.test.ts  |   65 +-
 web-next/lib/entity-definition/controller.ts       |   60 +-
 web-next/lib/entity-definition/view-model.test.ts  |   20 +-
 web-next/lib/entity-definition/view-model.ts       |   10 +-
 web-next/lib/entity-detail/controller.test.ts      |   32 +-
 web-next/lib/entity-detail/controller.ts           |   45 +-
 web-next/lib/entity-detail/query-state.test.ts     |   49 +
 web-next/lib/entity-detail/query-state.ts          |    8 +
 web-next/lib/entity-detail/view-model.test.ts      |  242 ++-
 web-next/lib/entity-detail/view-model.ts           |  598 ++++--
 web-next/lib/entity-discovery/controller.test.ts   |   51 +-
 web-next/lib/entity-discovery/controller.ts        |   51 +-
 web-next/lib/entity-discovery/search-state.test.ts |   40 +-
 web-next/lib/entity-discovery/search-state.ts      |   61 +
 web-next/lib/entity-discovery/view-model.test.ts   |  202 +-
 web-next/lib/entity-discovery/view-model.ts        |  305 +--
 web-next/lib/entity-editor/controller.test.ts      |  155 ++
 web-next/lib/entity-editor/controller.ts           |  124 +-
 web-next/lib/entity-editor/query-state.test.ts     |   44 +
 web-next/lib/entity-editor/query-state.ts          |   34 +
 web-next/lib/entity-editor/view-model.test.ts      |   48 +-
 web-next/lib/entity-editor/view-model.ts           |   68 +-
 web-next/lib/entity-health-affordance.test.ts      |   31 +-
 web-next/lib/entity-health-affordance.ts           |   55 +-
 web-next/lib/entity-import/controller.test.ts      |   33 +-
 web-next/lib/entity-import/controller.ts           |   37 +-
 web-next/lib/entity-import/view-model.test.ts      |  211 ++-
 web-next/lib/entity-import/view-model.ts           |  208 +-
 web-next/lib/entity-manage/controller.test.ts      |   24 +-
 web-next/lib/entity-manage/controller.ts           |   13 +-
 web-next/lib/entity-manage/display-mapping.test.ts |   64 +
 web-next/lib/entity-manage/display-mapping.ts      |   42 +-
 web-next/lib/entity-manage/query-state.test.ts     |   36 +-
 web-next/lib/entity-manage/query-state.ts          |   22 +-
 web-next/lib/entity-manage/view-model.test.ts      |  110 +-
 web-next/lib/entity-manage/view-model.ts           |   23 +-
 web-next/lib/entity-workbench-navigation.test.ts   |   42 +
 web-next/lib/entity-workbench-navigation.ts        |   43 +
 web-next/lib/topology-surface/controller.test.ts   |  135 ++
 web-next/lib/topology-surface/controller.ts        |   84 +
 web-next/lib/topology-surface/query-state.test.ts  |   54 +
 web-next/lib/topology-surface/query-state.ts       |   24 +
 web-next/lib/topology-surface/query.ts             |   13 +
 web-next/lib/topology-surface/view-model.test.ts   |  555 +++++-
 web-next/lib/topology-surface/view-model.ts        |  781 +++++++-
 84 files changed, 11454 insertions(+), 2029 deletions(-)

diff --git a/web-next/app/entities/[...rest]/page.test.ts b/web-next/app/entities/[...rest]/page.test.ts
index 834cb1402b..85a9e26a0b 100644
--- a/web-next/app/entities/[...rest]/page.test.ts
+++ b/web-next/app/entities/[...rest]/page.test.ts
@@ -14,7 +14,32 @@ describe('entities catch-all route', () => {
 
     const { default: EntityUnknownRoutePage } = await import('./page');
 
-    expect(() => EntityUnknownRoutePage()).toThrow('redirect:/entities');
+    await expect(EntityUnknownRoutePage()).rejects.toThrow('redirect:/entities');
     expect(redirect).toHaveBeenCalledWith('/entities');
   });
+
+  it('preserves entity catalog query context when redirecting unknown nested paths', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: EntityUnknownRoutePage } = await import('./page');
+
+    await expect(
+      EntityUnknownRoutePage({
+        searchParams: Promise.resolve({
+          search: 'checkout',
+          type: 'service',
+          status: 'review',
+          source: 'otlp',
+          returnTo: '/trace/manage?returnLabel=Trace'
+        })
+      })
+    ).rejects.toThrow(
+      'redirect:/entities?search=checkout&type=service&status=review&source=otlp&returnTo=%2Ftrace%2Fmanage'
+    );
+    expect(redirect).toHaveBeenLastCalledWith(
+      '/entities?search=checkout&type=service&status=review&source=otlp&returnTo=%2Ftrace%2Fmanage'
+    );
+  });
 });
diff --git a/web-next/app/entities/[...rest]/page.tsx b/web-next/app/entities/[...rest]/page.tsx
index e6edcda51b..cf9ca1297f 100644
--- a/web-next/app/entities/[...rest]/page.tsx
+++ b/web-next/app/entities/[...rest]/page.tsx
@@ -1,5 +1,9 @@
 import { redirect } from 'next/navigation';
+import { buildEntityListCompatRouteUrl, type SearchParamsRecord } from '../../../lib/entity-manage/query-state';
 
-export default function EntityUnknownRoutePage() {
-  redirect('/entities');
+export default async function EntityUnknownRoutePage(props: {
+  searchParams?: Promise<SearchParamsRecord>;
+} = {}) {
+  const resolvedSearchParams = await props?.searchParams;
+  redirect(buildEntityListCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/entities/[entityId]/definition/entity-definition-page.tsx b/web-next/app/entities/[entityId]/definition/entity-definition-page.tsx
new file mode 100644
index 0000000000..3cf38029cb
--- /dev/null
+++ b/web-next/app/entities/[entityId]/definition/entity-definition-page.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import React, { useCallback } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { EntityDefinitionWorkspaceSurface } from '@/components/pages/entity-definition-workspace-surface';
+import { api } from '@/lib/api-facade';
+import {
+  buildEntityDefinitionActivitiesUrl,
+  buildEntityDefinitionTemplatesUrl,
+  buildEntityDefinitionUrl,
+  loadEntityDefinitionPageDataFromFacade
+} from '@/lib/entity-definition/controller';
+
+type DefinitionPageData = Awaited<ReturnType<typeof loadEntityDefinitionPageDataFromFacade>>;
+
+const ENTITY_DEFINITION_SETTLED_CACHE_TTL_MS = 10_000;
+
+export default function EntityDefinitionPage({ entityId }: { entityId: string }) {
+  const { t } = useI18n();
+  const entityDefinitionUrl = React.useMemo(() => buildEntityDefinitionUrl(entityId, 'yaml'), [entityId]);
+  const entityDefinitionActivitiesUrl = React.useMemo(() => buildEntityDefinitionActivitiesUrl(entityId), [entityId]);
+  const entityDefinitionTemplatesUrl = React.useMemo(() => buildEntityDefinitionTemplatesUrl(), []);
+  const entityDefinitionCacheKey = React.useMemo(
+    () => ['entity-definition', entityDefinitionUrl, entityDefinitionActivitiesUrl, entityDefinitionTemplatesUrl].join(':'),
+    [entityDefinitionActivitiesUrl, entityDefinitionTemplatesUrl, entityDefinitionUrl]
+  );
+  const load = useCallback(async (): Promise<DefinitionPageData> => {
+    return loadEntityDefinitionPageDataFromFacade(
+      {
+        definition: api.entities.definition,
+        activities: api.entities.definitionActivities,
+        templates: api.entities.definitionTemplates
+      },
+      entityId,
+      'yaml'
+    );
+  }, [entityId]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('entities.definition.loading')}
+      cacheKey={entityDefinitionCacheKey}
+      cacheSettledTtlMs={ENTITY_DEFINITION_SETTLED_CACHE_TTL_MS}
+    >
+      {data => (
+        <EntityDefinitionWorkspaceSurface
+          mode="definition"
+          entityId={data.entityId}
+          initialContent={data.definition}
+          initialFormat="yaml"
+          initialMessage={data.loadMessage}
+          templates={data.templates}
+          activities={data.activities}
+        />
+      )}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/entities/[entityId]/definition/page.test.tsx b/web-next/app/entities/[entityId]/definition/page.test.tsx
index 5268e35aa1..58872c688f 100644
--- a/web-next/app/entities/[entityId]/definition/page.test.tsx
+++ b/web-next/app/entities/[entityId]/definition/page.test.tsx
@@ -1,7 +1,8 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
-import EntityDefinitionPage from './page';
+import EntityDefinitionPage from './entity-definition-page';
+import { createTranslatorMock } from '../../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
@@ -13,22 +14,41 @@ const mockState = vi.hoisted(() => ({
   }
 }));
 
-const loadEntityDefinitionPageData = vi.hoisted(() => vi.fn(async () => mockState.renderData));
-const apiMessageGet = vi.hoisted(() => vi.fn());
+const readEntityDefinition = vi.hoisted(() => vi.fn(async () => mockState.renderData.definition));
+const readEntityDefinitionActivities = vi.hoisted(() => vi.fn(async () => mockState.renderData.activities));
+const readEntityDefinitionTemplates = vi.hoisted(() => vi.fn(async () => mockState.renderData.templates));
+const loadEntityDefinitionPageDataFromFacade = vi.hoisted(() => vi.fn(async () => mockState.renderData));
+
+vi.mock('@/components/providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t: createTranslatorMock({
+      locale: 'zh-CN'
+    })
+  })
+}));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
     load,
-    loadingCopy
+    loadingCopy,
+    cacheKey,
+    cacheSettledTtlMs
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
     loadingCopy?: string;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
   }) => {
     mockState.lastLoad = load;
     return (
-      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
+      <div
+        data-client-workbench="true"
+        data-loading-copy={loadingCopy}
+        data-cache-key={cacheKey}
+        data-cache-settled-ttl={cacheSettledTtlMs}
+      >
         {children(mockState.renderData)}
       </div>
     );
@@ -44,31 +64,54 @@ vi.mock('@/components/pages/entity-definition-workspace-surface', () => ({
   )
 }));
 
-vi.mock('@/lib/api-client', () => ({
-  apiMessageGet
+vi.mock('@/lib/api-facade', () => ({
+  api: {
+    entities: {
+      definition: readEntityDefinition,
+      definitionActivities: readEntityDefinitionActivities,
+      definitionTemplates: readEntityDefinitionTemplates
+    }
+  }
 }));
 
 vi.mock('@/lib/entity-definition/controller', () => ({
-  loadEntityDefinitionPageData
+  buildEntityDefinitionActivitiesUrl: (entityId: string) => `/entities/definition-activities?entityId=${entityId}&limit=8`,
+  buildEntityDefinitionTemplatesUrl: () => '/entities/definition/templates?limit=8',
+  buildEntityDefinitionUrl: (entityId: string, format: string) => `/entities/${entityId}/definition?format=${format}`,
+  loadEntityDefinitionPageDataFromFacade
 }));
 
 describe('EntityDefinitionPage', () => {
   beforeEach(() => {
     mockState.lastLoad = null;
-    loadEntityDefinitionPageData.mockClear().mockResolvedValue(mockState.renderData);
-    apiMessageGet.mockClear();
+    readEntityDefinition.mockClear().mockResolvedValue(mockState.renderData.definition);
+    readEntityDefinitionActivities.mockClear().mockResolvedValue(mockState.renderData.activities);
+    readEntityDefinitionTemplates.mockClear().mockResolvedValue(mockState.renderData.templates);
+    loadEntityDefinitionPageDataFromFacade.mockClear().mockResolvedValue(mockState.renderData);
   });
 
   it('loads definition workspace data and renders the shared definition workspace surface', async () => {
-    const html = renderToStaticMarkup(<EntityDefinitionPage params={Promise.resolve({ entityId: '42' })} />);
+    const html = renderToStaticMarkup(<EntityDefinitionPage entityId="42" />);
 
     expect(html).toContain('data-entity-definition-workspace="definition"');
     expect(html).toContain('data-loading-copy="编辑实体定义"');
+    expect(html).toContain(
+      'data-cache-key="entity-definition:/entities/42/definition?format=yaml:/entities/definition-activities?entityId=42&amp;limit=8:/entities/definition/templates?limit=8"'
+    );
+    expect(html).toContain('data-cache-settled-ttl="10000"');
     expect(html).toContain('kind: service / 1 templates / 1 activities');
 
     await mockState.lastLoad?.();
 
-    expect(loadEntityDefinitionPageData).toHaveBeenCalledWith(apiMessageGet, '42', 'yaml');
+    expect(loadEntityDefinitionPageDataFromFacade).toHaveBeenCalledWith(
+      {
+        definition: readEntityDefinition,
+        activities: readEntityDefinitionActivities,
+        templates: readEntityDefinitionTemplates
+      },
+      '42',
+      'yaml'
+    );
   });
 
   it('forwards recoverable load messages into the definition surface for empty-error parity', () => {
@@ -80,7 +123,7 @@ describe('EntityDefinitionPage', () => {
       activities: []
     } as any;
 
-    const html = renderToStaticMarkup(<EntityDefinitionPage params={Promise.resolve({ entityId: '1' })} />);
+    const html = renderToStaticMarkup(<EntityDefinitionPage entityId="1" />);
 
     expect(html).toContain('data-entity-definition-workspace="definition"');
     expect(html).toContain('Entity not exist.');
diff --git a/web-next/app/entities/[entityId]/definition/page.tsx b/web-next/app/entities/[entityId]/definition/page.tsx
index 0dba244c45..0947decd90 100644
--- a/web-next/app/entities/[entityId]/definition/page.tsx
+++ b/web-next/app/entities/[entityId]/definition/page.tsx
@@ -1,32 +1,8 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { EntityDefinitionWorkspaceSurface } from '@/components/pages/entity-definition-workspace-surface';
-import { apiMessageGet } from '@/lib/api-client';
-import { loadEntityDefinitionPageData } from '@/lib/entity-definition/controller';
+import EntityDefinitionPage from './entity-definition-page';
 
-type DefinitionPageData = Awaited<ReturnType<typeof loadEntityDefinitionPageData>>;
-
-export default function EntityDefinitionPage({ params }: { params: Promise<{ entityId: string }> }) {
-  const load = useCallback(async (): Promise<DefinitionPageData> => {
-    const resolved = await params;
-    return loadEntityDefinitionPageData(apiMessageGet, resolved.entityId, 'yaml');
-  }, [params]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy="编辑实体定义">
-      {data => (
-        <EntityDefinitionWorkspaceSurface
-          mode="definition"
-          entityId={data.entityId}
-          initialContent={data.definition}
-          initialFormat="yaml"
-          initialMessage={data.loadMessage}
-          templates={data.templates}
-          activities={data.activities}
-        />
-      )}
-    </ClientWorkbench>
-  );
+export default async function EntityDefinitionRoutePage({ params }: { params: Promise<{ entityId: string }> }) {
+  const { entityId } = await params;
+  return <EntityDefinitionPage entityId={entityId} />;
 }
diff --git a/web-next/app/entities/[entityId]/edit/entity-edit-page.tsx b/web-next/app/entities/[entityId]/edit/entity-edit-page.tsx
new file mode 100644
index 0000000000..ae95a77867
--- /dev/null
+++ b/web-next/app/entities/[entityId]/edit/entity-edit-page.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import React from 'react';
+import { useCallback } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { EntityEditorSurface } from '@/components/pages/entity-editor-surface';
+import { api } from '@/lib/api-facade';
+import type { EntityCatalogSuggestions, EntityDto } from '@/lib/types';
+import {
+  buildEntityEditorCatalogSuggestionsUrl,
+  buildEntityEditorEntityUrl,
+  loadEntityEditorCatalogSuggestionsFromFacade,
+  loadEntityEditorEntityFromFacade
+} from '../../../../lib/entity-editor/controller';
+
+const ENTITY_EDIT_SETTLED_CACHE_TTL_MS = 10_000;
+
+export default function EntityEditPage({ entityId }: { entityId: string }) {
+  const { t } = useI18n();
+  const entityEditorEntityUrl = React.useMemo(() => buildEntityEditorEntityUrl(entityId), [entityId]);
+  const entityEditorCatalogUrl = React.useMemo(() => buildEntityEditorCatalogSuggestionsUrl(), []);
+  const entityEditCacheKey = React.useMemo(
+    () => ['entity-edit', entityEditorEntityUrl, entityEditorCatalogUrl].join(':'),
+    [entityEditorEntityUrl, entityEditorCatalogUrl]
+  );
+  const load = useCallback(async (): Promise<{ entityId: string; dto: EntityDto; catalogSuggestions: EntityCatalogSuggestions }> => {
+    const [dto, catalogSuggestions] = await Promise.all([
+      loadEntityEditorEntityFromFacade(api.entities.editorEntity, entityId),
+      loadEntityEditorCatalogSuggestionsFromFacade(api.entities.catalogSuggestions)
+    ]);
+    return { entityId, dto, catalogSuggestions };
+  }, [entityId]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('entities.edit.loading')}
+      cacheKey={entityEditCacheKey}
+      cacheSettledTtlMs={ENTITY_EDIT_SETTLED_CACHE_TTL_MS}
+    >
+      {data => <EntityEditorSurface initial={data.dto} mode="edit" entityId={data.entityId} catalogSuggestions={data.catalogSuggestions} />}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/entities/[entityId]/edit/page.test.tsx b/web-next/app/entities/[entityId]/edit/page.test.tsx
index ab556c7204..49be4eb406 100644
--- a/web-next/app/entities/[entityId]/edit/page.test.tsx
+++ b/web-next/app/entities/[entityId]/edit/page.test.tsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
-import EntityEditPage from './page';
+import EntityEditPage from './entity-edit-page';
 import { createTranslatorMock } from '../../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
@@ -20,17 +20,8 @@ const mockState = vi.hoisted(() => ({
   }
 }));
 
-const apiMessageGet = vi.hoisted(() =>
-  vi.fn((path: string) => {
-    if (path === '/entities/42') {
-      return Promise.resolve(mockState.renderData.dto);
-    }
-    if (path === '/entities/catalog-suggestions?limit=120') {
-      return Promise.resolve(mockState.renderData.catalogSuggestions);
-    }
-    return Promise.reject(new Error(`unexpected path: ${path}`));
-  })
-);
+const readEntityEditorEntity = vi.hoisted(() => vi.fn(async () => mockState.renderData.dto));
+const readEntityCatalogSuggestions = vi.hoisted(() => vi.fn(async () => mockState.renderData.catalogSuggestions));
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
@@ -41,13 +32,28 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    cacheKey,
+    cacheSettledTtlMs,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return (
+      <div
+        data-client-workbench="true"
+        data-cache-key={cacheKey}
+        data-cache-settled-ttl={cacheSettledTtlMs}
+        data-loading-copy={loadingCopy}
+      >
+        {children(mockState.renderData)}
+      </div>
+    );
   }
 }));
 
@@ -59,40 +65,41 @@ vi.mock('@/components/pages/entity-editor-surface', () => ({
   )
 }));
 
-vi.mock('@/lib/api-client', () => ({
-  apiMessageGet
+vi.mock('@/lib/api-facade', () => ({
+  api: {
+    entities: {
+      editorEntity: readEntityEditorEntity,
+      catalogSuggestions: readEntityCatalogSuggestions
+    }
+  }
 }));
 
 describe('EntityEditPage', () => {
   beforeEach(() => {
     mockState.lastLoad = null;
-    apiMessageGet.mockClear();
+    readEntityEditorEntity.mockClear().mockResolvedValue(mockState.renderData.dto);
+    readEntityCatalogSuggestions.mockClear().mockResolvedValue(mockState.renderData.catalogSuggestions);
   });
 
   it('loads entity detail plus catalog suggestions and renders the shared editor surface in edit mode', async () => {
-    const html = renderToStaticMarkup(<EntityEditPage params={Promise.resolve({ entityId: '42' })} />);
+    const html = renderToStaticMarkup(<EntityEditPage entityId="42" />);
 
     expect(html).toContain('data-entity-editor-surface="edit"');
     expect(html).toContain('data-entity-id="42"');
+    expect(html).toContain('data-cache-key="entity-edit:/entities/42:/entities/catalog-suggestions?limit=120"');
+    expect(html).toContain('data-cache-settled-ttl="10000"');
+    expect(html).toContain('data-loading-copy="Loading entity editor"');
     expect(html).toContain('checkout-api');
 
     await mockState.lastLoad?.();
 
-    expect(apiMessageGet).toHaveBeenCalledWith('/entities/42');
-    expect(apiMessageGet).toHaveBeenCalledWith('/entities/catalog-suggestions?limit=120');
+    expect(readEntityEditorEntity).toHaveBeenCalledWith('42');
+    expect(readEntityCatalogSuggestions).toHaveBeenCalledWith(120);
   });
 
   it('falls back to an empty catalog suggestion payload when the shared catalog endpoint is missing', async () => {
-    apiMessageGet.mockImplementation((path: string) => {
-      if (path === '/entities/42') {
-        return Promise.resolve(mockState.renderData.dto);
-      }
-      if (path === '/entities/catalog-suggestions?limit=120') {
-        return Promise.reject(new Error('GET /entities/catalog-suggestions?limit=120 failed with 404'));
-      }
-      return Promise.reject(new Error(`unexpected path: ${path}`));
-    });
-    renderToStaticMarkup(<EntityEditPage params={Promise.resolve({ entityId: '42' })} />);
+    readEntityCatalogSuggestions.mockRejectedValueOnce(new Error('GET /entities/catalog-suggestions?limit=120 failed with 404'));
+    renderToStaticMarkup(<EntityEditPage entityId="42" />);
 
     await expect(mockState.lastLoad?.()).resolves.toEqual({
       entityId: '42',
@@ -113,16 +120,8 @@ describe('EntityEditPage', () => {
   });
 
   it('falls back to a shared draft entity when the entity detail endpoint is missing', async () => {
-    apiMessageGet.mockImplementation((path: string) => {
-      if (path === '/entities/42') {
-        return Promise.reject(new Error('GET /entities/42 failed with 404'));
-      }
-      if (path === '/entities/catalog-suggestions?limit=120') {
-        return Promise.resolve(mockState.renderData.catalogSuggestions);
-      }
-      return Promise.reject(new Error(`unexpected path: ${path}`));
-    });
-    renderToStaticMarkup(<EntityEditPage params={Promise.resolve({ entityId: '42' })} />);
+    readEntityEditorEntity.mockRejectedValueOnce(new Error('GET /entities/42 failed with 404'));
+    renderToStaticMarkup(<EntityEditPage entityId="42" />);
 
     await expect(mockState.lastLoad?.()).resolves.toEqual({
       entityId: '42',
diff --git a/web-next/app/entities/[entityId]/edit/page.tsx b/web-next/app/entities/[entityId]/edit/page.tsx
index db41051d72..096d9bd8e2 100644
--- a/web-next/app/entities/[entityId]/edit/page.tsx
+++ b/web-next/app/entities/[entityId]/edit/page.tsx
@@ -1,28 +1,8 @@
-'use client';
-
 import React from 'react';
-import { useCallback } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { EntityEditorSurface } from '@/components/pages/entity-editor-surface';
-import { apiMessageGet } from '@/lib/api-client';
-import type { EntityCatalogSuggestions, EntityDto } from '@/lib/types';
-import { loadEntityEditorCatalogSuggestions, loadEntityEditorEntity } from '../../../../lib/entity-editor/controller';
 
-export default function EntityEditPage({ params }: { params: Promise<{ entityId: string }> }) {
-  const { t } = useI18n();
-  const load = useCallback(async (): Promise<{ entityId: string; dto: EntityDto; catalogSuggestions: EntityCatalogSuggestions }> => {
-    const resolved = await params;
-    const [dto, catalogSuggestions] = await Promise.all([
-      loadEntityEditorEntity(apiMessageGet, resolved.entityId),
-      loadEntityEditorCatalogSuggestions(apiMessageGet)
-    ]);
-    return { entityId: resolved.entityId, dto, catalogSuggestions };
-  }, [params]);
+import EntityEditPage from './entity-edit-page';
 
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('entities.edit.loading')}>
-      {data => <EntityEditorSurface initial={data.dto} mode="edit" entityId={data.entityId} catalogSuggestions={data.catalogSuggestions} />}
-    </ClientWorkbench>
-  );
+export default async function EntityEditRoutePage({ params }: { params: Promise<{ entityId: string }> }) {
+  const { entityId } = await params;
+  return <EntityEditPage entityId={entityId} />;
 }
diff --git a/web-next/app/entities/[entityId]/page.tsx b/web-next/app/entities/[entityId]/entity-detail-page.tsx
similarity index 61%
copy from web-next/app/entities/[entityId]/page.tsx
copy to web-next/app/entities/[entityId]/entity-detail-page.tsx
index 7fa0e4ef6e..a1cdc60311 100644
--- a/web-next/app/entities/[entityId]/page.tsx
+++ b/web-next/app/entities/[entityId]/entity-detail-page.tsx
@@ -2,28 +2,39 @@
 
 import React from 'react';
 import { useCallback, useState, useTransition } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
+import { useRouter } from 'next/navigation';
 import { EntityDetailSurface } from '@/components/pages/entity-detail-surface';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { loadEntityDetail } from '@/lib/entity-detail/controller';
-import { apiMessageDelete, apiMessageGet } from '@/lib/api-client';
-import { readSignalRouteContext } from '@/lib/signal-route-context';
+import { apiMessageDelete } from '@/lib/api-client';
+import { api } from '@/lib/api-facade';
+import { buildEntityDetailUrl, loadEntityDetailFromFacade } from '@/lib/entity-detail/controller';
+import type { SignalRouteContext } from '@/lib/signal-route-context';
 import type { EntityDetailDto } from '@/lib/types';
 
-export default function EntityDetailPage({ params }: { params: Promise<{ entityId: string }> }) {
+const ENTITY_DETAIL_SETTLED_CACHE_TTL_MS = 10_000;
+
+export default function EntityDetailPage({
+  entityId,
+  routeContext
+}: {
+  entityId: string;
+  routeContext?: SignalRouteContext;
+}) {
   const router = useRouter();
-  const searchParams = useSearchParams();
   const { t } = useI18n();
-  const routeContext = readSignalRouteContext(searchParams);
   const [reloadNonce, setReloadNonce] = useState(0);
   const [actionError, setActionError] = useState<string | null>(null);
   const [isPending, startTransition] = useTransition();
+  const entityDetailUrl = React.useMemo(() => buildEntityDetailUrl(entityId), [entityId]);
+  const entityDetailCacheKey = React.useMemo(
+    () => ['entity-detail', entityDetailUrl, reloadNonce].join(':'),
+    [entityDetailUrl, reloadNonce]
+  );
   const load = useCallback(async (): Promise<EntityDetailDto> => {
     void reloadNonce;
-    const resolved = await params;
-    return loadEntityDetail(apiMessageGet, resolved.entityId);
-  }, [params, reloadNonce]);
+    return loadEntityDetailFromFacade(api.entities.detail, entityId, t);
+  }, [entityId, reloadNonce, t]);
 
   const handleRefresh = useCallback(() => {
     setActionError(null);
@@ -53,7 +64,12 @@ export default function EntityDetailPage({ params }: { params: Promise<{ entityI
   );
 
   return (
-    <ClientWorkbench load={load} loadingCopy={t('entities.detail.loading')}>
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('entities.detail.loading')}
+      cacheKey={entityDetailCacheKey}
+      cacheSettledTtlMs={ENTITY_DETAIL_SETTLED_CACHE_TTL_MS}
+    >
       {detail => (
         <EntityDetailSurface
           detail={detail}
diff --git a/web-next/app/entities/[entityId]/page.test.tsx b/web-next/app/entities/[entityId]/page.test.tsx
index 36fb9eeea2..28869f52a3 100644
--- a/web-next/app/entities/[entityId]/page.test.tsx
+++ b/web-next/app/entities/[entityId]/page.test.tsx
@@ -3,7 +3,7 @@ import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
-import EntityDetailPage from './page';
+import EntityDetailPage from './entity-detail-page';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
@@ -44,7 +44,6 @@ const mockState = vi.hoisted(() => ({
       }
     ]
   },
-  searchParams: new URLSearchParams(),
   refresh: vi.fn(),
   push: vi.fn()
 }));
@@ -63,8 +62,7 @@ vi.mock('next/navigation', () => ({
   useRouter: () => ({
     refresh: mockState.refresh,
     push: mockState.push
-  }),
-  useSearchParams: () => mockState.searchParams
+  })
 }));
 
 vi.mock('@/components/providers/i18n-provider', () => ({
@@ -126,26 +124,36 @@ vi.mock('@/components/ui/button', () => ({
 }));
 
 vi.mock('@/lib/api-client', () => ({
-  apiMessageDelete: vi.fn(),
-  apiMessageGet: vi.fn()
+  apiMessageDelete: vi.fn()
 }));
 
-vi.mock('@/lib/entity-detail/controller', () => ({
-  loadEntityDetail
+vi.mock('@/lib/api-facade', () => ({
+  api: {
+    entities: {
+      detail: vi.fn()
+    }
+  }
 }));
 
+vi.mock('@/lib/entity-detail/controller', async importOriginal => {
+  const actual = await importOriginal<typeof import('@/lib/entity-detail/controller')>();
+  return {
+    ...actual,
+    loadEntityDetailFromFacade: loadEntityDetail
+  };
+});
+
 describe('EntityDetailPage', () => {
   beforeEach(() => {
     mockState.lastLoad = null;
-    mockState.searchParams = new URLSearchParams();
     mockState.refresh.mockReset();
     mockState.push.mockReset();
     loadEntityDetail.mockClear().mockResolvedValue(mockState.renderData);
   });
 
   it('renders the cold full-width entity detail workbench without old Workbench side panels', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/page.tsx'), 'utf8');
-    const html = renderToStaticMarkup(<EntityDetailPage params={Promise.resolve({ entityId: '42' })} />);
+    const source = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/entity-detail-page.tsx'), 'utf8');
+    const html = renderToStaticMarkup(<EntityDetailPage entityId="42" />);
 
     expect(html).toContain('data-entity-detail-surface="otlp-cold-entity-detail"');
     expect(html).toContain('data-entity-detail-style-baseline="hertzbeat-cold-matte"');
@@ -186,11 +194,11 @@ describe('EntityDetailPage', () => {
 
     await mockState.lastLoad?.();
 
-    expect(loadEntityDetail).toHaveBeenCalledWith(expect.any(Function), '42');
+    expect(loadEntityDetail).toHaveBeenCalledWith(expect.any(Function), '42', expect.any(Function));
   });
 
   it('passes inherited time and monitor context into the entity detail surface', () => {
-    mockState.searchParams = new URLSearchParams({
+    const routeContext = {
       timeRange: 'last-45m',
       start: '1713200000000',
       end: '1713202700000',
@@ -202,11 +210,28 @@ describe('EntityDetailPage', () => {
       monitorName: 'checkout-http',
       monitorApp: 'website',
       monitorInstance: 'example.com:443'
-    });
+    };
 
-    const html = renderToStaticMarkup(<EntityDetailPage params={Promise.resolve({ entityId: '42' })} />);
+    const html = renderToStaticMarkup(<EntityDetailPage entityId="42" routeContext={routeContext} />);
 
     expect(html).toContain('data-entity-detail-route-monitor-id="632051474676992"');
     expect(html).toContain('data-entity-detail-route-time-range="last-45m"');
   });
+
+  it('keeps entity detail remounts on a short settled cache window with refresh invalidation', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/entity-detail-page.tsx'), 'utf8');
+
+    expect(source).toContain('ENTITY_DETAIL_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('const [reloadNonce, setReloadNonce] = useState(0)');
+    expect(source).toContain("['entity-detail', entityDetailUrl, reloadNonce].join(':')");
+    expect(source).toContain('[entityDetailUrl, reloadNonce]');
+    expect(source).toContain('void reloadNonce');
+    expect(source).toContain("import { api } from '@/lib/api-facade';");
+    expect(source).toContain('return loadEntityDetailFromFacade(api.entities.detail, entityId, t);');
+    expect(source).not.toContain('return loadEntityDetail(apiMessageGet, entityId, t);');
+    expect(source).toContain('setReloadNonce(current => current + 1)');
+    expect(source).toContain('router.refresh();');
+    expect(source).toContain('cacheKey={entityDetailCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={ENTITY_DETAIL_SETTLED_CACHE_TTL_MS}');
+  });
 });
diff --git a/web-next/app/entities/[entityId]/page.tsx b/web-next/app/entities/[entityId]/page.tsx
index 7fa0e4ef6e..4aaadbf0f4 100644
--- a/web-next/app/entities/[entityId]/page.tsx
+++ b/web-next/app/entities/[entityId]/page.tsx
@@ -1,69 +1,17 @@
-'use client';
-
 import React from 'react';
-import { useCallback, useState, useTransition } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { EntityDetailSurface } from '@/components/pages/entity-detail-surface';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { loadEntityDetail } from '@/lib/entity-detail/controller';
-import { apiMessageDelete, apiMessageGet } from '@/lib/api-client';
-import { readSignalRouteContext } from '@/lib/signal-route-context';
-import type { EntityDetailDto } from '@/lib/types';
-
-export default function EntityDetailPage({ params }: { params: Promise<{ entityId: string }> }) {
-  const router = useRouter();
-  const searchParams = useSearchParams();
-  const { t } = useI18n();
-  const routeContext = readSignalRouteContext(searchParams);
-  const [reloadNonce, setReloadNonce] = useState(0);
-  const [actionError, setActionError] = useState<string | null>(null);
-  const [isPending, startTransition] = useTransition();
-  const load = useCallback(async (): Promise<EntityDetailDto> => {
-    void reloadNonce;
-    const resolved = await params;
-    return loadEntityDetail(apiMessageGet, resolved.entityId);
-  }, [params, reloadNonce]);
-
-  const handleRefresh = useCallback(() => {
-    setActionError(null);
-    setReloadNonce(current => current + 1);
-    router.refresh();
-  }, [router]);
-
-  const handleDelete = useCallback(
-    async (entityId: string | number | null | undefined) => {
-      if (entityId == null) {
-        return;
-      }
-
-      setActionError(null);
 
-      try {
-        await apiMessageDelete<void>(`/entities/${entityId}`);
-        startTransition(() => {
-          router.push('/entities');
-          router.refresh();
-        });
-      } catch (error) {
-        setActionError(error instanceof Error ? error.message : t('entities.detail.delete.failed'));
-      }
-    },
-    [router, t]
-  );
+import EntityDetailPage from './entity-detail-page';
+import { readEntityDetailRouteContext, type EntityDetailSearchParams } from '../../../lib/entity-detail/query-state';
 
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('entities.detail.loading')}>
-      {detail => (
-        <EntityDetailSurface
-          detail={detail}
-          routeContext={routeContext}
-          actionError={actionError}
-          isPending={isPending}
-          onDelete={handleDelete}
-          onRefresh={handleRefresh}
-        />
-      )}
-    </ClientWorkbench>
-  );
+export default async function EntityDetailRoutePage({
+  params,
+  searchParams
+}: {
+  params: Promise<{ entityId: string }>;
+  searchParams?: Promise<EntityDetailSearchParams>;
+}) {
+  const { entityId } = await params;
+  const resolvedSearchParams = await searchParams;
+  const routeContext = readEntityDetailRouteContext(resolvedSearchParams);
+  return <EntityDetailPage entityId={entityId} routeContext={routeContext} />;
 }
diff --git a/web-next/app/entities/discovery/entity-discovery-page.tsx b/web-next/app/entities/discovery/entity-discovery-page.tsx
new file mode 100644
index 0000000000..c2a8d25fcf
--- /dev/null
+++ b/web-next/app/entities/discovery/entity-discovery-page.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+import React, { useCallback } from 'react';
+import { useSearchParams } from 'next/navigation';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { EntityDiscoverySurface } from '@/components/pages/entity-discovery-surface';
+import { api } from '@/lib/api-facade';
+import {
+  buildDiscoveryCatalogSuggestionsUrl,
+  buildDiscoveryGovernanceActivitiesUrl,
+  buildDiscoveryGovernancePresetsUrl,
+  loadDiscoveryDataFromFacade
+} from '@/lib/entity-discovery/controller';
+import { resolveDiscoveryCandidateContext } from '@/lib/entity-discovery/search-state';
+
+const ENTITY_DISCOVERY_SETTLED_CACHE_TTL_MS = 10_000;
+
+type DiscoveryData = Awaited<ReturnType<typeof loadDiscoveryDataFromFacade>>;
+
+export default function EntityDiscoveryPage() {
+  const { t } = useI18n();
+  const searchParams = useSearchParams();
+  const entityDiscoveryPresetsUrl = React.useMemo(() => buildDiscoveryGovernancePresetsUrl(), []);
+  const entityDiscoveryActivitiesUrl = React.useMemo(() => buildDiscoveryGovernanceActivitiesUrl(), []);
+  const entityDiscoveryCatalogUrl = React.useMemo(() => buildDiscoveryCatalogSuggestionsUrl(), []);
+  const candidateContext = React.useMemo(() => resolveDiscoveryCandidateContext(searchParams), [searchParams]);
+  const entityDiscoveryCacheKey = React.useMemo(
+    () => ['entity-discovery', entityDiscoveryPresetsUrl, entityDiscoveryActivitiesUrl, entityDiscoveryCatalogUrl].join(':'),
+    [entityDiscoveryActivitiesUrl, entityDiscoveryCatalogUrl, entityDiscoveryPresetsUrl]
+  );
+  const load = useCallback(
+    async (): Promise<DiscoveryData> =>
+      loadDiscoveryDataFromFacade({
+        presets: api.entities.discoveryGovernancePresets,
+        activities: api.entities.discoveryGovernanceActivities,
+        catalogSuggestions: api.entities.catalogSuggestions
+      }),
+    []
+  );
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('entities.discovery.loading')}
+      cacheKey={entityDiscoveryCacheKey}
+      cacheSettledTtlMs={ENTITY_DISCOVERY_SETTLED_CACHE_TTL_MS}
+    >
+      {data => (
+        <EntityDiscoverySurface
+          presets={data.presets}
+          activities={data.activities}
+          catalog={data.catalog}
+          candidateContext={candidateContext}
+        />
+      )}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/entities/discovery/page.test.tsx b/web-next/app/entities/discovery/page.test.tsx
index 6b2919b069..89650b5d7c 100644
--- a/web-next/app/entities/discovery/page.test.tsx
+++ b/web-next/app/entities/discovery/page.test.tsx
@@ -1,7 +1,8 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
-import EntityDiscoveryPage from './page';
+import EntityDiscoveryPage from './entity-discovery-page';
+import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
@@ -12,22 +13,48 @@ const mockState = vi.hoisted(() => ({
   }
 }));
 
-const loadDiscoveryData = vi.hoisted(() => vi.fn(async () => mockState.renderData));
-const apiMessageGet = vi.hoisted(() => vi.fn());
+const readDiscoveryPresets = vi.hoisted(() => vi.fn(async () => mockState.renderData.presets));
+const readDiscoveryActivities = vi.hoisted(() => vi.fn(async () => mockState.renderData.activities));
+const readCatalogSuggestions = vi.hoisted(() => vi.fn(async () => mockState.renderData.catalog));
+const loadDiscoveryDataFromFacade = vi.hoisted(() => vi.fn(async () => mockState.renderData));
+
+vi.mock('@/components/providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t: createTranslatorMock({
+      locale: 'zh-CN'
+    })
+  })
+}));
+
+vi.mock('next/navigation', () => ({
+  useSearchParams: () =>
+    new URLSearchParams(
+      'identityKey=service.name&identityValue=billing&serviceName=billing-api&serviceNamespace=commerce&environment=prod'
+    )
+}));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
     load,
-    loadingCopy
+    loadingCopy,
+    cacheKey,
+    cacheSettledTtlMs
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
     loadingCopy?: string;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
   }) => {
     mockState.lastLoad = load;
     return (
-      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
+      <div
+        data-client-workbench="true"
+        data-loading-copy={loadingCopy}
+        data-cache-key={cacheKey}
+        data-cache-settled-ttl={cacheSettledTtlMs}
+      >
         {children(mockState.renderData)}
       </div>
     );
@@ -35,26 +62,46 @@ vi.mock('@/components/workbench/client-workbench', () => ({
 }));
 
 vi.mock('@/components/pages/entity-discovery-surface', () => ({
-  EntityDiscoverySurface: ({ presets, activities, catalog }: any) => (
+  EntityDiscoverySurface: ({ presets, activities, catalog, candidateContext }: any) => (
     <div data-entity-discovery-surface="otlp-cold-discovery-console">
+      {candidateContext ? (
+        <span
+          data-page-candidate-source={candidateContext.source}
+          data-page-candidate-identity={`${candidateContext.identityKey}:${candidateContext.identityValue}`}
+          data-page-candidate-service={candidateContext.serviceName}
+          data-page-candidate-namespace={candidateContext.serviceNamespace}
+          data-page-candidate-environment={candidateContext.environment}
+        />
+      ) : null}
       {presets.length} presets / {activities.length} activities / {catalog.owners.length} owners
     </div>
   )
 }));
 
-vi.mock('@/lib/api-client', () => ({
-  apiMessageGet
+vi.mock('@/lib/api-facade', () => ({
+  api: {
+    entities: {
+      discoveryGovernancePresets: readDiscoveryPresets,
+      discoveryGovernanceActivities: readDiscoveryActivities,
+      catalogSuggestions: readCatalogSuggestions
+    }
+  }
 }));
 
 vi.mock('@/lib/entity-discovery/controller', () => ({
-  loadDiscoveryData
+  buildDiscoveryCatalogSuggestionsUrl: () => '/entities/catalog-suggestions?limit=120',
+  buildDiscoveryGovernanceActivitiesUrl: () => '/entities/discovery/governance-activities?limit=8',
+  buildDiscoveryGovernancePresetsUrl: () => '/entities/discovery/governance-presets?limit=8',
+  loadDiscoveryDataFromFacade
 }));
 
 describe('EntityDiscoveryPage', () => {
   beforeEach(() => {
     mockState.lastLoad = null;
-    loadDiscoveryData.mockClear().mockResolvedValue(mockState.renderData);
-    apiMessageGet.mockClear();
+    readDiscoveryPresets.mockClear().mockResolvedValue(mockState.renderData.presets);
+    readDiscoveryActivities.mockClear().mockResolvedValue(mockState.renderData.activities);
+    readCatalogSuggestions.mockClear().mockResolvedValue(mockState.renderData.catalog);
+    loadDiscoveryDataFromFacade.mockClear().mockResolvedValue(mockState.renderData);
   });
 
   it('loads discovery workspace data and renders the shared discovery surface', async () => {
@@ -62,10 +109,23 @@ describe('EntityDiscoveryPage', () => {
 
     expect(html).toContain('data-entity-discovery-surface="otlp-cold-discovery-console"');
     expect(html).toContain('data-loading-copy="遥测发现"');
+    expect(html).toContain(
+      'data-cache-key="entity-discovery:/entities/discovery/governance-presets?limit=8:/entities/discovery/governance-activities?limit=8:/entities/catalog-suggestions?limit=120"'
+    );
+    expect(html).toContain('data-cache-settled-ttl="10000"');
     expect(html).toContain('1 presets / 1 activities / 1 owners');
+    expect(html).toContain('data-page-candidate-source="otlp-candidate"');
+    expect(html).toContain('data-page-candidate-identity="service.name:billing"');
+    expect(html).toContain('data-page-candidate-service="billing-api"');
+    expect(html).toContain('data-page-candidate-namespace="commerce"');
+    expect(html).toContain('data-page-candidate-environment="prod"');
 
     await mockState.lastLoad?.();
 
-    expect(loadDiscoveryData).toHaveBeenCalledWith(apiMessageGet);
+    expect(loadDiscoveryDataFromFacade).toHaveBeenCalledWith({
+      presets: readDiscoveryPresets,
+      activities: readDiscoveryActivities,
+      catalogSuggestions: readCatalogSuggestions
+    });
   });
 });
diff --git a/web-next/app/entities/discovery/page.tsx b/web-next/app/entities/discovery/page.tsx
index 0eeccb780a..6e8b21225a 100644
--- a/web-next/app/entities/discovery/page.tsx
+++ b/web-next/app/entities/discovery/page.tsx
@@ -1,19 +1,7 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { EntityDiscoverySurface } from '@/components/pages/entity-discovery-surface';
-import { apiMessageGet } from '@/lib/api-client';
-import { loadDiscoveryData } from '@/lib/entity-discovery/controller';
+import EntityDiscoveryPage from './entity-discovery-page';
 
-type DiscoveryData = Awaited<ReturnType<typeof loadDiscoveryData>>;
-
-export default function EntityDiscoveryPage() {
-  const load = useCallback(async (): Promise<DiscoveryData> => loadDiscoveryData(apiMessageGet), []);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy="遥测发现">
-      {data => <EntityDiscoverySurface presets={data.presets} activities={data.activities} catalog={data.catalog} />}
-    </ClientWorkbench>
-  );
+export default function EntityDiscoveryRoutePage() {
+  return <EntityDiscoveryPage />;
 }
diff --git a/web-next/app/entities/page.tsx b/web-next/app/entities/entity-list-page.tsx
similarity index 54%
copy from web-next/app/entities/page.tsx
copy to web-next/app/entities/entity-list-page.tsx
index baaaa2bc2f..154250ebe1 100644
--- a/web-next/app/entities/page.tsx
+++ b/web-next/app/entities/entity-list-page.tsx
@@ -1,37 +1,54 @@
 'use client';
 
-import React, { useCallback, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
+import React, { useCallback, useMemo, useState } from 'react';
 import { ClientWorkbench } from '@/components/workbench/client-workbench';
 import { EntityListSurface } from '../../components/pages/entity-list-surface';
 import { useI18n } from '@/components/providers/i18n-provider';
-import { apiMessageGet } from '@/lib/api-client';
-import { loadEntityList } from '@/lib/entity-manage/controller';
+import { api } from '@/lib/api-facade';
+import { loadEntityListFromFacade } from '@/lib/entity-manage/controller';
 import { entityEnvironmentLabel, entityStatusLabel, entityTypeLabel } from '@/lib/entity-manage/display-mapping';
 import { formatTime } from '@/lib/format';
-import { queryStateFromParams, type EntityQueryState } from '@/lib/entity-manage/query-state';
-import { buildEntityTableRows } from '@/lib/entity-manage/view-model';
+import { buildEntityUrl, type EntityQueryState } from '@/lib/entity-manage/query-state';
+import { buildEntityTableRows, isEntityHealthyStatus } from '@/lib/entity-manage/view-model';
 
-export default function EntitiesPage() {
+const ENTITY_LIST_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_ENTITY_QUERY: EntityQueryState = { search: '', type: '', status: '' };
+
+export default function EntityListPage({ initialQuery }: { initialQuery?: EntityQueryState } = {}) {
   const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const [draft, setDraft] = useState<EntityQueryState>(() => queryStateFromParams(searchParams));
-  const [query, setQuery] = useState<EntityQueryState>(() => queryStateFromParams(searchParams));
+  const initialEntityQuery = initialQuery ?? EMPTY_ENTITY_QUERY;
+  const [draft, setDraft] = useState<EntityQueryState>(() => ({ ...initialEntityQuery }));
+  const [query, setQuery] = useState<EntityQueryState>(() => ({ ...initialEntityQuery }));
+  const [refreshNonce, setRefreshNonce] = useState(0);
+  const entityListUrl = useMemo(() => buildEntityUrl(query), [query]);
+  const entityListCacheKey = useMemo(
+    () => ['entity-list', entityListUrl, refreshNonce].join(':'),
+    [entityListUrl, refreshNonce]
+  );
 
   const load = useCallback(async () => {
-    const list = await loadEntityList(apiMessageGet, query);
+    const list = await loadEntityListFromFacade(api.entities.list, query);
     return { list };
   }, [query]);
 
   const applyQuery = () => setQuery({ ...draft });
+  const refreshQuery = () => {
+    setQuery({ ...draft });
+    setRefreshNonce(current => current + 1);
+  };
   const resetQuery = () => {
-    const empty = { search: '', type: '', status: '' };
+    const empty = { ...EMPTY_ENTITY_QUERY };
     setDraft(empty);
     setQuery(empty);
   };
 
   return (
-    <ClientWorkbench load={load} loadingCopy={t('entities.list.loading')}>
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('entities.list.loading')}
+      cacheKey={entityListCacheKey}
+      cacheSettledTtlMs={ENTITY_LIST_SETTLED_CACHE_TTL_MS}
+    >
       {data => {
         const rows = buildEntityTableRows(
           data.list.content,
@@ -46,7 +63,7 @@ export default function EntitiesPage() {
         const pageSize = data.list.pageSize || Math.max(rows.length, 1);
         const rangeFrom = total > 0 ? pageIndex * pageSize + 1 : 0;
         const rangeTo = total > 0 ? Math.min(pageIndex * pageSize + rows.length, total) : 0;
-        const abnormalCount = rows.filter(row => row.status !== 'healthy' && row.status !== '健康').length;
+        const abnormalCount = data.list.content.filter(item => !isEntityHealthyStatus(item.entity?.status)).length;
         const alertingCount = rows.filter(row => Number(row.activeAlertCount) > 0).length;
         const linkedCount = rows.filter(row => Number(row.relationCount) > 0).length;
 
@@ -63,7 +80,7 @@ export default function EntitiesPage() {
             linkedCount={linkedCount}
             onDraftChange={patch => setDraft(prev => ({ ...prev, ...patch }))}
             onSearch={applyQuery}
-            onRefresh={applyQuery}
+            onRefresh={refreshQuery}
             onReset={resetQuery}
           />
         );
diff --git a/web-next/app/entities/import/entity-import-page.tsx b/web-next/app/entities/import/entity-import-page.tsx
new file mode 100644
index 0000000000..64c83c88b0
--- /dev/null
+++ b/web-next/app/entities/import/entity-import-page.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import React, { useCallback } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { EntityImportSurface } from '@/components/pages/entity-import-surface';
+import { api } from '@/lib/api-facade';
+import { buildImportActivitiesUrl, buildImportTemplatesUrl, loadImportDataFromFacade } from '@/lib/entity-import/controller';
+
+const ENTITY_IMPORT_SETTLED_CACHE_TTL_MS = 10_000;
+
+type ImportData = Awaited<ReturnType<typeof loadImportDataFromFacade>>;
+
+export default function EntityImportPage() {
+  const { t } = useI18n();
+  const entityImportTemplatesUrl = React.useMemo(() => buildImportTemplatesUrl(), []);
+  const entityImportActivitiesUrl = React.useMemo(() => buildImportActivitiesUrl(), []);
+  const entityImportCacheKey = React.useMemo(
+    () => ['entity-import', entityImportTemplatesUrl, entityImportActivitiesUrl].join(':'),
+    [entityImportActivitiesUrl, entityImportTemplatesUrl]
+  );
+  const load = useCallback(
+    async (): Promise<ImportData> =>
+      loadImportDataFromFacade({
+        templates: api.entities.importTemplates,
+        activities: api.entities.importActivities
+      }),
+    []
+  );
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('entities.import.loading')}
+      cacheKey={entityImportCacheKey}
+      cacheSettledTtlMs={ENTITY_IMPORT_SETTLED_CACHE_TTL_MS}
+    >
+      {data => <EntityImportSurface templates={data.templates} activities={data.activities} />}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/entities/import/page.test.tsx b/web-next/app/entities/import/page.test.tsx
index c99c6ed31c..ac3164fee7 100644
--- a/web-next/app/entities/import/page.test.tsx
+++ b/web-next/app/entities/import/page.test.tsx
@@ -1,7 +1,8 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
-import EntityImportPage from './page';
+import EntityImportPage from './entity-import-page';
+import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
@@ -11,20 +12,43 @@ const mockState = vi.hoisted(() => ({
   }
 }));
 
-const loadImportData = vi.hoisted(() => vi.fn(async () => mockState.renderData));
+const readImportTemplates = vi.hoisted(() => vi.fn(async () => mockState.renderData.templates));
+const readImportActivities = vi.hoisted(() => vi.fn(async () => mockState.renderData.activities));
+const loadImportDataFromFacade = vi.hoisted(() => vi.fn(async () => mockState.renderData));
+
+vi.mock('@/components/providers/i18n-provider', () => ({
+  useI18n: () => ({
+    t: createTranslatorMock({
+      locale: 'zh-CN'
+    })
+  })
+}));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
     load,
-    loadingCopy
+    loadingCopy,
+    cacheKey,
+    cacheSettledTtlMs
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
     loadingCopy: string;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true" data-loading-copy={loadingCopy}>{children(mockState.renderData)}</div>;
+    return (
+      <div
+        data-client-workbench="true"
+        data-loading-copy={loadingCopy}
+        data-cache-key={cacheKey}
+        data-cache-settled-ttl={cacheSettledTtlMs}
+      >
+        {children(mockState.renderData)}
+      </div>
+    );
   }
 }));
 
@@ -36,18 +60,27 @@ vi.mock('@/components/pages/entity-import-surface', () => ({
   )
 }));
 
-vi.mock('@/lib/api-client', () => ({
-  apiMessageGet: vi.fn()
+vi.mock('@/lib/api-facade', () => ({
+  api: {
+    entities: {
+      importTemplates: readImportTemplates,
+      importActivities: readImportActivities
+    }
+  }
 }));
 
 vi.mock('@/lib/entity-import/controller', () => ({
-  loadImportData
+  buildImportActivitiesUrl: () => '/entities/definition-activities?limit=8',
+  buildImportTemplatesUrl: () => '/entities/definition/templates?limit=8',
+  loadImportDataFromFacade
 }));
 
 describe('EntityImportPage', () => {
   beforeEach(() => {
     mockState.lastLoad = null;
-    loadImportData.mockClear().mockResolvedValue(mockState.renderData);
+    readImportTemplates.mockClear().mockResolvedValue(mockState.renderData.templates);
+    readImportActivities.mockClear().mockResolvedValue(mockState.renderData.activities);
+    loadImportDataFromFacade.mockClear().mockResolvedValue(mockState.renderData);
   });
 
   it('loads shared import data and renders the shared import surface', async () => {
@@ -55,10 +88,17 @@ describe('EntityImportPage', () => {
 
     expect(html).toContain('data-entity-import-surface="true"');
     expect(html).toContain('data-loading-copy="导入实体定义"');
+    expect(html).toContain(
+      'data-cache-key="entity-import:/entities/definition/templates?limit=8:/entities/definition-activities?limit=8"'
+    );
+    expect(html).toContain('data-cache-settled-ttl="10000"');
     expect(html).toContain('1 templates / 1 activities');
 
     await mockState.lastLoad?.();
 
-    expect(loadImportData).toHaveBeenCalledTimes(1);
+    expect(loadImportDataFromFacade).toHaveBeenCalledWith({
+      templates: readImportTemplates,
+      activities: readImportActivities
+    });
   });
 });
diff --git a/web-next/app/entities/import/page.tsx b/web-next/app/entities/import/page.tsx
index cd63aff2fe..aec47ded66 100644
--- a/web-next/app/entities/import/page.tsx
+++ b/web-next/app/entities/import/page.tsx
@@ -1,19 +1,7 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { EntityImportSurface } from '@/components/pages/entity-import-surface';
-import { apiMessageGet } from '@/lib/api-client';
-import { loadImportData } from '@/lib/entity-import/controller';
+import EntityImportPage from './entity-import-page';
 
-type ImportData = Awaited<ReturnType<typeof loadImportData>>;
-
-export default function EntityImportPage() {
-  const load = useCallback(async (): Promise<ImportData> => loadImportData(apiMessageGet), []);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy="导入实体定义">
-      {data => <EntityImportSurface templates={data.templates} activities={data.activities} />}
-    </ClientWorkbench>
-  );
+export default function EntityImportRoutePage() {
+  return <EntityImportPage />;
 }
diff --git a/web-next/app/entities/new/entity-new-page.tsx b/web-next/app/entities/new/entity-new-page.tsx
new file mode 100644
index 0000000000..264199bc0a
--- /dev/null
+++ b/web-next/app/entities/new/entity-new-page.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import React, { useCallback } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { EntityEditorSurface } from '@/components/pages/entity-editor-surface';
+import { api } from '@/lib/api-facade';
+import {
+  buildEntityEditorCatalogSuggestionsUrl,
+  buildEntityEditorNewDraftFromFacade,
+  buildEntityEditorSeedMonitorUrl,
+  loadEntityEditorCatalogSuggestionsFromFacade,
+  type EntityEditorNewDraftSeed
+} from '@/lib/entity-editor/controller';
+
+const ENTITY_NEW_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_ENTITY_NEW_SEED: EntityEditorNewDraftSeed = { source: null, monitorId: null };
+
+export default function EntityNewPage({ initialSeed }: { initialSeed?: EntityEditorNewDraftSeed } = {}) {
+  const { t } = useI18n();
+  const entityNewSeed = initialSeed ?? EMPTY_ENTITY_NEW_SEED;
+  const { source, monitorId, identityKey, identityValue, serviceName, serviceNamespace, environment } = entityNewSeed;
+  const entityNewCatalogUrl = React.useMemo(() => buildEntityEditorCatalogSuggestionsUrl(), []);
+  const entityNewSeedUrl = React.useMemo(() => {
+    if (source === 'telemetry' && monitorId) {
+      return buildEntityEditorSeedMonitorUrl(monitorId);
+    }
+    if (source === 'otlp-candidate' && identityKey && identityValue) {
+      return [
+        'otlp-candidate',
+        identityKey,
+        identityValue,
+        serviceName ?? 'none',
+        serviceNamespace ?? 'none',
+        environment ?? 'none'
+      ].join(':');
+    }
+    return ['manual', source ?? 'none', monitorId ?? 'none'].join(':');
+  }, [environment, identityKey, identityValue, monitorId, serviceName, serviceNamespace, source]);
+  const entityNewCacheKey = React.useMemo(
+    () => ['entity-new', entityNewSeedUrl, entityNewCatalogUrl].join(':'),
+    [entityNewCatalogUrl, entityNewSeedUrl]
+  );
+
+  const load = useCallback(async () => {
+    const [initial, catalogSuggestions] = await Promise.all([
+      buildEntityEditorNewDraftFromFacade(api.monitors.detail, entityNewSeed),
+      loadEntityEditorCatalogSuggestionsFromFacade(api.entities.catalogSuggestions)
+    ]);
+    return { initial, catalogSuggestions };
+  }, [entityNewSeed]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('entities.new.loading')}
+      cacheKey={entityNewCacheKey}
+      cacheSettledTtlMs={ENTITY_NEW_SETTLED_CACHE_TTL_MS}
+    >
+      {data => <EntityEditorSurface initial={data.initial} mode="new" catalogSuggestions={data.catalogSuggestions} />}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/entities/new/page.test.tsx b/web-next/app/entities/new/page.test.tsx
index 2bb2c52e69..82312cbf71 100644
--- a/web-next/app/entities/new/page.test.tsx
+++ b/web-next/app/entities/new/page.test.tsx
@@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   renderData: {
     initial: {
@@ -19,16 +18,12 @@ const mockState = vi.hoisted(() => ({
   }
 }));
 
-const apiMessageGet = vi.hoisted(() => vi.fn(async () => mockState.renderData.catalogSuggestions));
-const loadEntityEditorNewDraft = vi.hoisted(() => vi.fn(async () => mockState.renderData.initial));
-const loadEntityEditorCatalogSuggestions = vi.hoisted(() => vi.fn(async () => mockState.renderData.catalogSuggestions));
-
-vi.mock('next/navigation', () => ({
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
-}));
+const readSeedMonitor = vi.hoisted(() => vi.fn(async () => ({ id: 42, app: 'website', name: 'checkout', instance: 'example.com' })));
+const readEntityCatalogSuggestions = vi.hoisted(() => vi.fn(async () => mockState.renderData.catalogSuggestions));
+const buildEntityEditorNewDraftFromFacade = vi.hoisted(() => vi.fn(async () => mockState.renderData.initial));
+const loadEntityEditorCatalogSuggestionsFromFacade = vi.hoisted(() =>
+  vi.fn(async (readCatalogSuggestions: () => Promise<unknown>) => readCatalogSuggestions())
+);
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
@@ -39,13 +34,28 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    cacheKey,
+    cacheSettledTtlMs,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    cacheKey?: string;
+    cacheSettledTtlMs?: number;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return (
+      <div
+        data-client-workbench="true"
+        data-cache-key={cacheKey}
+        data-cache-settled-ttl={cacheSettledTtlMs}
+        data-loading-copy={loadingCopy}
+      >
+        {children(mockState.renderData)}
+      </div>
+    );
   }
 }));
 
@@ -57,42 +67,55 @@ vi.mock('@/components/pages/entity-editor-surface', () => ({
   )
 }));
 
-vi.mock('@/lib/api-client', () => ({
-  apiMessageGet
+vi.mock('@/lib/api-facade', () => ({
+  api: {
+    monitors: {
+      detail: readSeedMonitor
+    },
+    entities: {
+      catalogSuggestions: readEntityCatalogSuggestions
+    }
+  }
 }));
 
 vi.mock('@/lib/entity-editor/controller', () => ({
-  buildEntityEditorNewDraft: loadEntityEditorNewDraft,
-  loadEntityEditorCatalogSuggestions
+  buildEntityEditorCatalogSuggestionsUrl: () => '/entities/catalog-suggestions?limit=120',
+  buildEntityEditorNewDraftFromFacade,
+  buildEntityEditorSeedMonitorUrl: (monitorId: string) => `/monitor/${monitorId}`,
+  loadEntityEditorCatalogSuggestionsFromFacade
 }));
 
 describe('EntityNewPage', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams();
     mockState.lastLoad = null;
-    apiMessageGet.mockClear().mockResolvedValue(mockState.renderData.catalogSuggestions);
-    loadEntityEditorNewDraft.mockClear().mockResolvedValue(mockState.renderData.initial);
-    loadEntityEditorCatalogSuggestions.mockClear().mockResolvedValue(mockState.renderData.catalogSuggestions);
+    readSeedMonitor.mockClear().mockResolvedValue({ id: 42, app: 'website', name: 'checkout', instance: 'example.com' });
+    readEntityCatalogSuggestions.mockClear().mockResolvedValue(mockState.renderData.catalogSuggestions);
+    buildEntityEditorNewDraftFromFacade.mockClear().mockResolvedValue(mockState.renderData.initial);
+    loadEntityEditorCatalogSuggestionsFromFacade.mockClear().mockImplementation(async readCatalogSuggestions => readCatalogSuggestions());
   });
 
   it('loads catalog suggestions and renders the shared editor surface in create mode', async () => {
-    const { default: EntityNewPage } = await import('./page');
+    const { default: EntityNewPage } = await import('./entity-new-page');
     const html = renderToStaticMarkup(<EntityNewPage />);
 
     expect(html).toContain('data-entity-editor-surface="new"');
+    expect(html).toContain('data-cache-key="entity-new:manual:none:none:/entities/catalog-suggestions?limit=120"');
+    expect(html).toContain('data-cache-settled-ttl="10000"');
+    expect(html).toContain('data-loading-copy="Loading entity draft"');
     expect(html).toContain('checkout-api');
 
     await mockState.lastLoad?.();
 
-    expect(loadEntityEditorNewDraft).toHaveBeenCalledWith(expect.any(Function), {
+    expect(buildEntityEditorNewDraftFromFacade).toHaveBeenCalledWith(readSeedMonitor, {
       source: null,
       monitorId: null
     });
-    expect(loadEntityEditorCatalogSuggestions).toHaveBeenCalledWith(expect.any(Function));
+    expect(loadEntityEditorCatalogSuggestionsFromFacade).toHaveBeenCalledWith(readEntityCatalogSuggestions);
+    expect(readEntityCatalogSuggestions).toHaveBeenCalled();
   });
 
   it('falls back to an empty catalog suggestion payload when the shared catalog endpoint is missing', async () => {
-    loadEntityEditorCatalogSuggestions.mockResolvedValueOnce({
+    loadEntityEditorCatalogSuggestionsFromFacade.mockResolvedValueOnce({
       owners: [],
       namespaces: [],
       environments: [],
@@ -104,7 +127,7 @@ describe('EntityNewPage', () => {
       languages: [],
       linkProviders: []
     });
-    const { default: EntityNewPage } = await import('./page');
+    const { default: EntityNewPage } = await import('./entity-new-page');
     renderToStaticMarkup(<EntityNewPage />);
 
     await expect(mockState.lastLoad?.()).resolves.toEqual({
@@ -125,16 +148,47 @@ describe('EntityNewPage', () => {
   });
 
   it('passes telemetry handoff query state into the shared new-draft loader', async () => {
-    mockState.searchParams = new URLSearchParams('source=telemetry&monitorId=42');
+    const { default: EntityNewPage } = await import('./entity-new-page');
+    const initialSeed = { source: 'telemetry', monitorId: '42' };
+    const html = renderToStaticMarkup(<EntityNewPage initialSeed={initialSeed} />);
 
-    const { default: EntityNewPage } = await import('./page');
-    renderToStaticMarkup(<EntityNewPage />);
+    expect(html).toContain('data-cache-key="entity-new:/monitor/42:/entities/catalog-suggestions?limit=120"');
 
     await mockState.lastLoad?.();
 
-    expect(loadEntityEditorNewDraft).toHaveBeenCalledWith(expect.any(Function), {
+    expect(buildEntityEditorNewDraftFromFacade).toHaveBeenCalledWith(readSeedMonitor, {
       source: 'telemetry',
       monitorId: '42'
     });
   });
+
+  it('passes OTLP candidate query state into the shared new-draft loader and cache key', async () => {
+    const { default: EntityNewPage } = await import('./entity-new-page');
+    const initialSeed = {
+      source: 'otlp-candidate',
+      monitorId: null,
+      identityKey: 'service.name',
+      identityValue: 'billing',
+      serviceName: 'billing-api',
+      serviceNamespace: 'commerce',
+      environment: 'prod'
+    };
+    const html = renderToStaticMarkup(<EntityNewPage initialSeed={initialSeed as any} />);
+
+    expect(html).toContain(
+      'data-cache-key="entity-new:otlp-candidate:service.name:billing:billing-api:commerce:prod:/entities/catalog-suggestions?limit=120"'
+    );
+
+    await mockState.lastLoad?.();
+
+    expect(buildEntityEditorNewDraftFromFacade).toHaveBeenCalledWith(readSeedMonitor, {
+      source: 'otlp-candidate',
+      monitorId: null,
+      identityKey: 'service.name',
+      identityValue: 'billing',
+      serviceName: 'billing-api',
+      serviceNamespace: 'commerce',
+      environment: 'prod'
+    });
+  });
 });
diff --git a/web-next/app/entities/new/page.tsx b/web-next/app/entities/new/page.tsx
index bf871fa1e5..7a2a350ce5 100644
--- a/web-next/app/entities/new/page.tsx
+++ b/web-next/app/entities/new/page.tsx
@@ -1,32 +1,14 @@
-'use client';
-
 import React from 'react';
-import { useCallback } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { EntityEditorSurface } from '@/components/pages/entity-editor-surface';
-import { apiMessageGet } from '@/lib/api-client';
-import { buildEntityEditorNewDraft, loadEntityEditorCatalogSuggestions } from '@/lib/entity-editor/controller';
-import type { EntityCatalogSuggestions } from '@/lib/types';
-
-export default function EntityNewPage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const source = searchParams.get('source');
-  const monitorId = searchParams.get('monitorId');
-
-  const load = useCallback(async () => {
-    const [initial, catalogSuggestions] = await Promise.all([
-      buildEntityEditorNewDraft(apiMessageGet, { source, monitorId }),
-      loadEntityEditorCatalogSuggestions(apiMessageGet)
-    ]);
-    return { initial, catalogSuggestions };
-  }, [monitorId, source]);
 
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('entities.new.loading')}>
-      {data => <EntityEditorSurface initial={data.initial} mode="new" catalogSuggestions={data.catalogSuggestions} />}
-    </ClientWorkbench>
-  );
+import EntityNewPage from './entity-new-page';
+import { readEntityNewDraftSeed, type EntityNewSearchParams } from '../../../lib/entity-editor/query-state';
+
+export default async function EntityNewRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<EntityNewSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const initialSeed = readEntityNewDraftSeed(resolvedSearchParams);
+  return <EntityNewPage initialSeed={initialSeed} />;
 }
diff --git a/web-next/app/entities/not-found.tsx b/web-next/app/entities/not-found.tsx
index d7f9774435..50075b99a7 100644
--- a/web-next/app/entities/not-found.tsx
+++ b/web-next/app/entities/not-found.tsx
@@ -1,5 +1,6 @@
 import { redirect } from 'next/navigation';
+import { buildEntityListCompatRouteUrl } from '../../lib/entity-manage/query-state';
 
 export default function EntitiesNotFound() {
-  redirect('/entities');
+  redirect(buildEntityListCompatRouteUrl());
 }
diff --git a/web-next/app/entities/page.test.tsx b/web-next/app/entities/page.test.tsx
index c806d4dd0a..89c9edd3ba 100644
--- a/web-next/app/entities/page.test.tsx
+++ b/web-next/app/entities/page.test.tsx
@@ -3,11 +3,10 @@ import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
-import EntitiesPage from './page';
+import EntityListPage from './entity-list-page';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   renderData: {
     list: {
@@ -27,31 +26,50 @@ vi.mock('next/link', () => ({
   )
 }));
 
-vi.mock('next/navigation', () => ({
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
-}));
-
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
       locale: 'zh-CN',
       overrides: {
         'entities.list.title': '对象目录',
+        'entities.list.kicker': '对象优先调查',
+        'entities.list.subtitle': '围绕服务、资源与实体定位问题并进入调查,先按对象筛出风险,再决定进入日志、链路、指标还是告警工作台。',
         'entities.list.environment.select': '选择环境',
         'entities.list.environment.all': '全部环境',
         'entities.list.resource-search.placeholder': '搜索并过滤资源属性',
         'entities.list.action.new': '新建对象',
+        'entities.list.action.create': '创建实体',
+        'entities.list.action.discovery': '从遥测发现',
+        'entities.list.action.import': '导入定义',
+        'entities.list.metric.total': '实体总数',
+        'entities.list.metric.abnormal': '活跃异常对象',
+        'entities.list.metric.alerting': '高风险对象',
+        'entities.list.metric.linked': '有关联对象',
+        'entities.list.search.placeholder': '搜索实体名称、命名空间、负责人',
         'entities.list.column.object': '对象',
+        'entities.list.column.owner': '负责人',
+        'entities.list.column.progress': '进展',
+        'entities.list.column.evidence': '证据关联',
         'entities.list.column.type': '类型',
         'entities.list.column.status': '状态',
+        'entities.list.column.next-action': '下一步动作',
         'entities.list.column.alerts': '告警',
         'entities.list.column.monitors': '监控',
         'entities.list.column.relations': '关系',
         'entities.list.column.updated': '最近证据',
+        'entities.list.table.total': '{{total}} 个实体',
+        'entities.list.table.range': '显示 {{from}}-{{to}} / {{total}}',
         'entities.list.pagination.items': '{{from}}-{{to}} / {{total}} 项',
+        'entities.list.row.owner.unset': '未设置',
+        'entities.list.row.progress.incomplete': '{{percent}} · 待完善',
+        'entities.list.row.progress.missing': '当前缺少负责人、处置手册',
+        'entities.list.row.evidence.monitors': '{{count}} 监控',
+        'entities.list.row.evidence.alerts': '{{count}} 告警',
+        'entities.list.row.evidence.identities': '{{count}} 身份标识',
+        'entities.list.row.evidence.updated': '最近更新 {{time}}',
+        'entities.list.row.action.owner': '设置负责人',
+        'entities.list.row.action.logs': '日志线索',
+        'entities.list.row.action.traces': '链路证据',
         'common.clear': '清除',
         'common.apply-filters': '应用筛选'
       }
@@ -62,13 +80,15 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return <div data-client-workbench="true" data-loading-copy={loadingCopy}>{children(mockState.renderData)}</div>;
   }
 }));
 
@@ -129,13 +149,24 @@ vi.mock('@/components/ui/input', () => ({
 }));
 
 vi.mock('@/lib/api-client', () => ({
-  apiMessageGet: vi.fn()
 }));
 
-vi.mock('@/lib/entity-manage/controller', () => ({
-  loadEntityList
+vi.mock('@/lib/api-facade', () => ({
+  api: {
+    entities: {
+      list: vi.fn()
+    }
+  }
 }));
 
+vi.mock('@/lib/entity-manage/controller', async importOriginal => {
+  const actual = await importOriginal<typeof import('@/lib/entity-manage/controller')>();
+  return {
+    ...actual,
+    loadEntityListFromFacade: loadEntityList
+  };
+});
+
 vi.mock('@/lib/entity-manage/display-mapping', () => ({
   entityEnvironmentLabel: (value: string) => value,
   entityStatusLabel: (value: string) => value,
@@ -144,11 +175,6 @@ vi.mock('@/lib/entity-manage/display-mapping', () => ({
 
 vi.mock('@/lib/entity-manage/query-state', () => ({
   buildEntityUrl: vi.fn(),
-  queryStateFromParams: (params: { get(name: string): string | null }) => ({
-    search: params.get('search') || '',
-    type: params.get('type') || '',
-    status: params.get('status') || ''
-  }),
   queryStateToQueryString: (query: Record<string, string>) =>
     new URLSearchParams(
       Object.entries(query).filter(([, value]) => value)
@@ -156,6 +182,8 @@ vi.mock('@/lib/entity-manage/query-state', () => ({
 }));
 
 vi.mock('@/lib/entity-manage/view-model', () => ({
+  isEntityHealthyStatus: (status: string | null | undefined) =>
+    ['healthy', 'up', 'normal'].includes(String(status || '').toLowerCase().replace(/[\s-]+/g, '_')),
   buildEntityTableRows: vi.fn(() => [
     {
       key: '1',
@@ -176,17 +204,28 @@ vi.mock('@/lib/format', () => ({
   formatTime: vi.fn(() => 'now')
 }));
 
-describe('EntitiesPage', () => {
+describe('EntityListPage', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams('search=checkout&type=service&status=healthy');
     mockState.lastLoad = null;
     loadEntityList.mockClear().mockResolvedValue(mockState.renderData.list);
   });
 
+  it('keeps entity catalog remounts on a short settled cache window while refresh invalidates that cache', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/entities/entity-list-page.tsx'), 'utf8');
+
+    expect(source).toContain('ENTITY_LIST_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['entity-list', entityListUrl, refreshNonce].join(':')");
+    expect(source).toContain('setRefreshNonce(current => current + 1)');
+    expect(source).toContain('onRefresh={refreshQuery}');
+    expect(source).toContain('cacheSettledTtlMs={ENTITY_LIST_SETTLED_CACHE_TTL_MS}');
+  });
+
   it('loads the entity list through the shared controller contract', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/entities/page.tsx'), 'utf8');
-    const html = renderToStaticMarkup(<EntitiesPage />);
+    const source = readFileSync(resolve(process.cwd(), 'app/entities/entity-list-page.tsx'), 'utf8');
+    const initialQuery = { search: 'checkout', type: 'service', status: 'healthy' };
+    const html = renderToStaticMarkup(<EntityListPage initialQuery={initialQuery} />);
 
+    expect(html).toContain('data-loading-copy="正在加载对象目录"');
     expect(html).toContain('data-entity-list-surface="otlp-cold-entity-console"');
     expect(html).toContain('data-entity-list-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-entity-list-header="cold-compact-header"');
@@ -248,6 +287,9 @@ describe('EntitiesPage', () => {
     expect(source).not.toContain('signoz-services-table');
     expect(source).not.toContain('signoz-services-rail');
     expect(source).not.toContain('angular-sidebar-flush');
+    expect(source).toContain("import { api } from '@/lib/api-facade';");
+    expect(source).toContain('const list = await loadEntityListFromFacade(api.entities.list, query);');
+    expect(source).not.toContain('const list = await loadEntityList(apiMessageGet, query);');
 
     await mockState.lastLoad?.();
 
diff --git a/web-next/app/entities/page.tsx b/web-next/app/entities/page.tsx
index baaaa2bc2f..95f5005b09 100644
--- a/web-next/app/entities/page.tsx
+++ b/web-next/app/entities/page.tsx
@@ -1,73 +1,14 @@
-'use client';
-
-import React, { useCallback, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { EntityListSurface } from '../../components/pages/entity-list-surface';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { apiMessageGet } from '@/lib/api-client';
-import { loadEntityList } from '@/lib/entity-manage/controller';
-import { entityEnvironmentLabel, entityStatusLabel, entityTypeLabel } from '@/lib/entity-manage/display-mapping';
-import { formatTime } from '@/lib/format';
-import { queryStateFromParams, type EntityQueryState } from '@/lib/entity-manage/query-state';
-import { buildEntityTableRows } from '@/lib/entity-manage/view-model';
-
-export default function EntitiesPage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const [draft, setDraft] = useState<EntityQueryState>(() => queryStateFromParams(searchParams));
-  const [query, setQuery] = useState<EntityQueryState>(() => queryStateFromParams(searchParams));
-
-  const load = useCallback(async () => {
-    const list = await loadEntityList(apiMessageGet, query);
-    return { list };
-  }, [query]);
-
-  const applyQuery = () => setQuery({ ...draft });
-  const resetQuery = () => {
-    const empty = { search: '', type: '', status: '' };
-    setDraft(empty);
-    setQuery(empty);
-  };
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('entities.list.loading')}>
-      {data => {
-        const rows = buildEntityTableRows(
-          data.list.content,
-          t,
-          value => entityTypeLabel(value, t),
-          value => entityEnvironmentLabel(value, t),
-          value => entityStatusLabel(value, t),
-          formatTime
-        );
-        const total = data.list.totalElements || rows.length;
-        const pageIndex = data.list.pageIndex || 0;
-        const pageSize = data.list.pageSize || Math.max(rows.length, 1);
-        const rangeFrom = total > 0 ? pageIndex * pageSize + 1 : 0;
-        const rangeTo = total > 0 ? Math.min(pageIndex * pageSize + rows.length, total) : 0;
-        const abnormalCount = rows.filter(row => row.status !== 'healthy' && row.status !== '健康').length;
-        const alertingCount = rows.filter(row => Number(row.activeAlertCount) > 0).length;
-        const linkedCount = rows.filter(row => Number(row.relationCount) > 0).length;
-
-        return (
-          <EntityListSurface
-            t={t}
-            rows={rows}
-            draft={draft}
-            total={total}
-            rangeFrom={rangeFrom}
-            rangeTo={rangeTo}
-            abnormalCount={abnormalCount}
-            alertingCount={alertingCount}
-            linkedCount={linkedCount}
-            onDraftChange={patch => setDraft(prev => ({ ...prev, ...patch }))}
-            onSearch={applyQuery}
-            onRefresh={applyQuery}
-            onReset={resetQuery}
-          />
-        );
-      }}
-    </ClientWorkbench>
-  );
+import React from 'react';
+
+import EntityListPage from './entity-list-page';
+import { readEntityListQueryState, type EntityListSearchParams } from '../../lib/entity-manage/query-state';
+
+export default async function EntitiesRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<EntityListSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const initialQuery = readEntityListQueryState(resolvedSearchParams);
+  return <EntityListPage initialQuery={initialQuery} />;
 }
diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 688e0acc48..3b9495c28f 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -9,12 +9,6 @@ const i18nState = vi.hoisted(() => ({
   locale: 'zh-CN' as 'zh-CN' | 'en-US'
 }));
 
-let mockSearchParams = new URLSearchParams();
-
-vi.mock('next/navigation', () => ({
-  useSearchParams: () => mockSearchParams
-}));
-
 vi.mock('../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     locale: i18nState.locale,
@@ -25,28 +19,344 @@ vi.mock('../../components/providers/i18n-provider', () => ({
   })
 }));
 
+vi.mock('next/navigation', () => ({
+  useRouter: () => ({
+    push: vi.fn()
+  })
+}));
+
 describe('topology page', () => {
   beforeEach(() => {
-    mockSearchParams = new URLSearchParams();
     i18nState.locale = 'zh-CN';
   });
 
+  function buildApiTopologyFixture() {
+    return {
+      apiBacked: true,
+      focusEntityId: 'service:commerce/checkout',
+      depth: 2,
+      sourceKinds: ['entity-relation', 'monitor-bind', 'otlp-trace-call'],
+      nodes: [
+        {
+          id: 'app-commerce',
+          entityId: 'application:commerce',
+          entityName: 'commerce',
+          entityType: 'application',
+          namespace: 'commerce',
+          environment: 'prod',
+          health: 'warning',
+          evidenceBadges: ['entity-relation', 'manual']
+        },
+        {
+          id: 'svc-frontend',
+          entityId: 'service:commerce/frontend',
+          entityName: 'frontend',
+          entityType: 'service',
+          namespace: 'commerce',
+          environment: 'prod',
+          health: 'healthy',
+          evidenceBadges: ['entity-relation', 'otlp-trace-call']
+        },
+        {
+          id: 'svc-checkout',
+          entityId: 'service:commerce/checkout',
+          entityName: 'checkout-api',
+          entityType: 'service',
+          namespace: 'commerce',
+          environment: 'prod',
+          health: 'warning',
+          evidenceBadges: ['entity-relation', 'otlp-trace-call']
+        },
+        {
+          id: 'monitor-checkout',
+          entityId: 'monitor:commerce/checkout-http',
+          entityName: 'HTTP template',
+          entityType: 'monitor',
+          namespace: 'commerce',
+          environment: 'prod',
+          health: 'healthy',
+          evidenceBadges: ['entity-relation', 'monitor-bind']
+        },
+        {
+          id: 'k8s-checkout-workload',
+          entityId: 'k8s:commerce/deployment/checkout',
+          entityName: 'checkout deploy',
+          entityType: 'k8s-workload',
+          namespace: 'commerce',
+          environment: 'prod',
+          health: 'healthy',
+          evidenceBadges: ['entity-relation', 'k8s-workload']
+        },
+        {
+          id: 'res-orders-db',
+          entityId: 'database:commerce/orders',
+          entityName: 'orders-db',
+          entityType: 'database',
+          namespace: 'commerce',
+          environment: 'prod',
+          health: 'critical',
+          evidenceBadges: ['entity-relation', 'database-middleware-connection']
+        },
+        {
+          id: 'res-redis',
+          entityId: 'middleware:commerce/redis-cart',
+          entityName: 'redis',
+          entityType: 'middleware',
+          namespace: 'commerce',
+          environment: 'prod',
+          health: 'warning',
+          evidenceBadges: ['entity-relation', 'template-dependency']
+        }
+      ],
+      edges: [
+        {
+          sourceNodeId: 'app-commerce',
+          targetNodeId: 'svc-frontend',
+          relationType: 'application ownership',
+          relationSource: 'cmdb-manual-label',
+          status: 'confirmed',
+          score: 92,
+          evidenceBadges: ['entity-relation', 'manual']
+        },
+        {
+          sourceNodeId: 'app-commerce',
+          targetNodeId: 'svc-checkout',
+          relationType: 'application ownership',
+          relationSource: 'cmdb-manual-label',
+          status: 'warning',
+          score: 78,
+          evidenceBadges: ['entity-relation', 'manual']
+        },
+        {
+          sourceNodeId: 'svc-frontend',
+          targetNodeId: 'svc-checkout',
+          relationType: 'HTTP call',
+          relationSource: 'otlp-trace-call',
+          status: 'active',
+          score: 96,
+          evidenceBadges: ['entity-relation', 'otlp-trace-call'],
+          redMetrics: {
+            requestRatePerSecond: 7.25,
+            errorRate: 0.021,
+            latencyP95Ms: 123
+          }
+        },
+        {
+          sourceNodeId: 'monitor-checkout',
+          targetNodeId: 'svc-checkout',
+          relationType: 'monitor ownership',
+          relationSource: 'monitor-bind',
+          status: 'confirmed',
+          score: 92,
+          evidenceBadges: ['entity-relation', 'monitor-bind']
+        },
+        {
+          sourceNodeId: 'k8s-checkout-workload',
+          targetNodeId: 'svc-checkout',
+          relationType: 'k8s ownership',
+          relationSource: 'k8s-workload',
+          status: 'confirmed',
+          score: 90,
+          evidenceBadges: ['entity-relation', 'k8s-workload']
+        },
+        {
+          sourceNodeId: 'svc-checkout',
+          targetNodeId: 'res-orders-db',
+          relationType: 'database connection',
+          relationSource: 'database-middleware-connection',
+          status: 'critical',
+          score: 48,
+          evidenceBadges: ['entity-relation', 'database-middleware-connection']
+        },
+        {
+          sourceNodeId: 'svc-checkout',
+          targetNodeId: 'res-redis',
+          relationType: 'template dependency',
+          relationSource: 'template-dependency',
+          status: 'warning',
+          score: 76,
+          evidenceBadges: ['entity-relation', 'template-dependency']
+        }
+      ]
+    };
+  }
+
   it('keeps topology on the HertzBeat entity relationship surface instead of copied service-map chrome', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/topology/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
 
+    expect(source).toContain('loadTopologyGraph');
+    expect(source).toContain('resolveTopologyApiTimeoutMs');
+    expect(source).toContain('const TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;');
+    expect(source).toContain('const DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;');
+    expect(source).toContain('return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;');
+    expect(source).toContain('return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;');
+    expect(source).not.toContain('return 8000');
+    expect(source).toContain('{ timeoutMs: resolveTopologyApiTimeoutMs(routeContext) }');
+    expect(source).toContain('buildTopologyServiceMapFromApiGraph');
+    expect(source).toContain('HzTopologyWorkbenchFrame');
+    expect(source).toContain('HzTopologyWorkbenchHeader');
+    expect(source).toContain('HzTopologyWorkbenchGrid');
+    expect(source).toContain('HzTopologyWorkbenchSlot');
+    expect(source).toContain('HzTopologyCanvas');
+    expect(source).toContain('HzTopologyCanvasAnnotation');
+    expect(source).toContain('HzTopologyG6Canvas');
+    expect(source).toContain("from '@hertzbeat/ui/topology-g6'");
+    expect(source).toContain("selectedFocusLabel={t('topology.view.focus-selected')}");
+    expect(source).toContain('HzTopologyFocusTrail');
+    expect(source).toContain('HzTopologyScopeBar');
+    expect(source).toContain('HzTopologyToolbar');
+    expect(source).toContain('HzTopologyCompanionRail');
+    expect(source).toContain('HzTopologyHoverTooltip');
+    expect(source).toContain('HzTopologyLegend');
+    expect(source).toContain('HzTopologyDetailDrawer');
+    expect(source).toContain('HzTopologyEvidenceList');
+    expect(source).toContain('HzTopologyFilterStrip');
+    expect(source).toContain('HzTopologyActionLink');
+    expect(source).not.toContain("from '../../components/ui/input'");
+    expect(source).not.toContain("from '../../components/ui/select'");
+    expect(source).not.toContain('className="relative min-h-[680px] overflow-hidden bg-[#08090c]"');
+    expect(source).not.toContain('nodeToneClass');
+    expect(source).not.toContain('nodeFocusClass');
+    expect(source).not.toContain('const edgeColor');
+    expect(source).not.toContain('edgeColor[');
+    expect(source).toContain('data-topology-data-source=');
     expect(source).toContain('data-topology-route="hertzbeat-entity-topology"');
+    expect(source).toContain('data-topology-workbench-frame-owner="hertzbeat-ui-workbench-frame"');
+    expect(source).toContain('data-topology-workbench-frame-boundary-owner="hertzbeat-ui-workbench-frame-boundary"');
+    expect(source).toContain('data-topology-workbench-header-owner="hertzbeat-ui-workbench-header"');
+    expect(source).toContain('density="operational-compact"');
+    expect(source).toContain('copyVisibility="assistive"');
+    expect(source).not.toContain('data-topology-header-density="concise-operational"');
+    expect(source).toContain('data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
+    expect(source).toContain('layout="canvas-only"');
+    expect(source).toContain('data-topology-canvas-slot-owner="hertzbeat-ui-workbench-slot"');
+    expect(source).toContain('data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"');
+    expect(source).toContain('data-topology-companion-rail-visibility="hidden-from-layout"');
+    expect(source).toContain('kind="canvas"');
+    expect(source).toContain('kind="companion"');
+    expect(source).toContain('surface="content"');
+    expect(source).toContain('data-topology-scope-bar-owner="hertzbeat-ui-scope-bar"');
+    expect(source).toContain('data-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"');
+    expect(source).toContain('data-topology-focus-trail-owner="hertzbeat-ui-focus-trail"');
+    expect(source).toContain('data-topology-focus-trail-boundary-owner="hertzbeat-ui-focus-trail-boundary"');
+    expect(source).toContain('focusMode={topologyFocusTrailMode}');
+    expect(source).toContain('focusDepth={String(topologyFocusTrailDepth)}');
+    expect(source).toContain('focusEntityId={topologyFocusTrailEntityId}');
+    expect(source).toContain('topologyFocusExitHref');
+    expect(source).toContain("t('topology.focus-trail.label')");
+    expect(source).toContain("t('topology.focus-trail.exit')");
+    expect(source).toContain("t('topology.focus-trail.hidden-count'");
+    expect(source).toContain('boundary="none"');
+    expect(source).toContain('data-topology-refresh-action="refresh"');
     expect(source).toContain('data-topology-controls="hertzbeat-topology-controls"');
-    expect(source).toContain('data-topology-source-strip="relationship-source-contract"');
-    expect(source).toContain('data-topology-source-control="relationship-source"');
+    expect(source).toContain('data-topology-toolbar-boundary-owner="hertzbeat-ui-toolbar-boundary"');
+    expect(source).toContain('data-topology-toolbar-state-owner="hertzbeat-ui-toolbar-state"');
+    expect(source).toContain('density="graph-first"');
+    expect(source).toContain('boundary="none"');
+    expect(source).toContain("'data-topology-focus-state':");
+    expect(source).toContain("'data-topology-depth-state':");
+    expect(source).toContain("'data-topology-group-state': map.filterContext.groupBy");
+    expect(source).toContain('sourceKindLabel={t');
+    expect(source).toContain('sourceKindValue={map.filterContext.sourceKind ??');
+    expect(source).toContain('sourceKindOptions={[');
+    expect(source).not.toContain('sourceSlot={');
+    expect(source).not.toContain('data-topology-source-strip="relationship-source-contract"');
+    expect(source).not.toContain('data-topology-source-strip-owner="hertzbeat-ui-filter-strip"');
+    expect(source).not.toContain("'data-topology-source-control': 'relationship-source'");
+    expect(source).toContain('data-topology-view-mode-strip-owner="hertzbeat-ui-filter-strip"');
+    expect(source).toContain('data-topology-companion-rail-owner="hertzbeat-ui-companion-rail"');
+    expect(source).toContain('data-topology-companion-rail-boundary-owner="hertzbeat-ui-companion-rail-boundary"');
+    expect(source).toContain('data-topology-companion-rail-scope="legend-metrics-timeline-detail"');
+    expect(source).toContain('boundary="side"');
     expect(source).toContain('data-topology-canvas="hertzbeat-topology-canvas"');
-    expect(source).toContain('data-topology-node="service-node"');
-    expect(source).toContain('data-topology-node-focus=');
-    expect(source).toContain('data-topology-edge="service-edge"');
-    expect(source).toContain('data-topology-edge-focus=');
-    expect(source).toContain('data-topology-edge-drilldown=');
+    expect(source).toContain('data-topology-canvas-annotation-owner="hertzbeat-ui-canvas-annotation"');
+    expect(source).toContain('data-topology-g6-canvas-owner="hertzbeat-ui-g6-canvas"');
+    expect(source).toContain('data-topology-renderer="antv-g6"');
+    expect(source).not.toContain('data-topology-graph-layer-owner="hertzbeat-ui-graph-layer"');
+    expect(source).not.toContain('<svg className="absolute inset-0 h-full w-full"');
+    expect(source).toContain('data-topology-canvas-interaction-owner="hertzbeat-ui-canvas-interaction"');
+    expect(source).toContain('data-topology-canvas-interaction-scope-owner="hertzbeat-ui-canvas-interaction-scope"');
+    expect(source).toContain('interactionScope="hover-group"');
+    expect(source).toContain('data-topology-canvas-boundary-owner="hertzbeat-ui-canvas-boundary"');
+    expect(source).toContain('boundary="none"');
+    expect(source).not.toContain('className="group"');
+    expect(source).toContain('hoverMode="neighbor-highlight"');
+    expect(source).toContain('drawerMode={topologyCanvasDrawerMode}');
+    expect(source).toContain('focusDepth={topologyCanvasFocusDepth}');
+    expect(source).toContain('const map = React.useMemo(');
+    expect(source).toContain('buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t)');
+    expect(source).toContain('const topologyG6Graph = React.useMemo<HzTopologyG6GraphInput>(() => ({');
+    expect(source).toContain('focus: node.focus');
+    expect(source).not.toContain('topologyCanvasFocusNodeId');
+    expect(source).toContain('topologyG6Graph');
+    expect(source).toContain('graph={topologyG6Graph}');
+    expect(source).toContain('legendSlot={');
+    expect(source).toContain('data-topology-g6-legend-owner="hertzbeat-ui-g6-legend-dock"');
+    expect(source).toContain('density="canvas-dock"');
+    expect(source).toContain('selectedNodeId={topologyTraceCallMissingEdges ? undefined : primaryNode?.id}');
+    expect(source).toContain('selectedEdgeId={topologyCanvasSelectedEdgeId}');
+    expect(source).toContain('hoveredNodeId={topologyG6HoveredNodeId}');
+    expect(source).toContain('hoveredEdgeId={topologyG6HoveredEdgeId}');
+    expect(source).toContain('onNodeHover={handleTopologyG6NodeHover}');
+    expect(source).toContain('onEdgeHover={handleTopologyG6EdgeHover}');
+    expect(source).toContain('onHoverClear={handleTopologyG6HoverClear}');
+    expect(source).toContain('overlayMode="non-occluding"');
+    expect(source).toContain('const topologyLiveHoverEdge = topologyHoveredDetailEdge;');
+    expect(source).toContain('topologyHoveredDetailEdge ?? topologyDetailEdge');
+    expect(source).toContain('searchQuery={topologyEffectiveSearchQuery}');
+    expect(source).toContain('topologyNodeIds');
+    expect(source).not.toContain('const topologyNodeHrefById');
+    expect(source).toContain('topologyNodeFocusHrefById');
+    expect(source).toContain('handleTopologyG6NodeSelect');
+    expect(source).toContain('handleTopologyG6NodeFocus');
+    expect(source).toContain('handleTopologyG6EdgeSelect');
+    expect(source).toContain('data-topology-selection-behavior="in-page-drawer"');
+    expect(source).toContain('onNodeSelect={handleTopologyG6NodeSelect}');
+    expect(source).toContain('onNodeFocus={handleTopologyG6NodeFocus}');
+    expect(source).toContain('onEdgeSelect={handleTopologyG6EdgeSelect}');
+    expect(source).toContain('data-topology-hover-tooltip-owner="hertzbeat-ui-hover-tooltip"');
+    expect(source).toContain('data-topology-hover-edge={topologyLiveHoverEdge.id}');
+    expect(source).toContain('trigger="live-edge-hover"');
+    expect(source).toContain('topologyG6HoverAnchor');
+    expect(source).toContain('setTopologyG6HoverAnchor(anchor)');
+    expect(source).toContain("placement={topologyG6HoverAnchor ? 'canvas-anchor' : 'canvas-right-under-toolbar'}");
+    expect(source).toContain('anchor={topologyG6HoverAnchor}');
+    expect(source).toContain("size={topologyG6HoverAnchor ? 'compact' : 'standard'}");
+    expect(source).toContain('data-topology-legend-owner="hertzbeat-ui-legend"');
+    expect(source).toContain('data-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"');
+    expect(source).toContain('data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"');
+    expect(source).toContain('data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"');
+    expect(source).toContain('data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"');
+    expect(source).toContain('density="graph-first"');
+    expect(source).toContain('clearTopologyG6Hover');
+    expect(source).toContain('clearTopologyG6Hover();');
+    expect(source).toContain("rowAction: t('topology.metric-table.row-action')");
+    expect(source).toContain('data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"');
+    expect(source).toContain("'data-topology-detail-boundary-owner': 'hertzbeat-ui-detail-boundary'");
     expect(source).toContain('data-topology-edge-evidence-panel=');
+    expect(source).toContain('data-topology-current-entity-panel-owner="hertzbeat-ui-detail-drawer"');
+    expect(source).toContain("'data-topology-current-entity-boundary-owner': 'hertzbeat-ui-detail-boundary'");
     expect(source).toContain('data-topology-incoming-context=');
     expect(source).toContain('data-topology-alert-impact-link="alert-center"');
+    expect(source).toContain('data-topology-alert-impact-link-owner="hertzbeat-ui-action-link"');
+    expect(source).toContain('data-topology-fault-context-owner="hertzbeat-ui-evidence-list"');
+    expect(source).toContain('data-topology-fault-context-boundary-owner="hertzbeat-ui-evidence-list-boundary"');
+    expect(source).toContain('data-topology-impact-timeline-owner="hertzbeat-ui-evidence-list"');
+    expect(source).toContain('data-topology-impact-timeline-boundary-owner="hertzbeat-ui-evidence-list-boundary"');
+    expect(source).not.toContain('className="mt-3 block rounded-[4px] border border-[#31405c] bg-[#182238] px-3 py-2 text-[12px] font-semibold text-[#d8e4ff]"');
+    expect(source).not.toContain('className="flex flex-wrap items-center justify-end gap-2 text-[12px] text-[#d6d9e2]"');
+    expect(source).not.toContain('className="border-b border-[#252832] bg-[#0b0c0f] px-4 py-3"');
+    expect(source).not.toContain('className="min-h-[calc(100vh-56px)] bg-[#08090c] text-[#f1f3f7]"');
+    expect(source).not.toContain('className="border-b border-[#252832] bg-[#0b0c0f] px-4 py-4"');
+    expect(source).not.toContain('className="grid min-h-[760px] bg-[#08090c] xl:grid-cols-[minmax(0,1fr)_320px]"');
+    expect(source).not.toContain('className="mt-5 rounded-[4px] border border-[#252832] bg-[#101217] p-3"');
+    expect(source).not.toContain('className="absolute right-4 top-4 z-10 w-[300px]"');
+    expect(source).not.toContain('className="text-[12px] font-semibold tracking-[0.12em] text-[#7e8494]"');
+    expect(source).toContain('boundary="framed"');
+    expect(source).toContain('boundary="toolbar-context"');
+    expect(source).not.toContain('className="mt-5"');
+    expect(source).not.toContain('className="mt-3"');
+    expect(source).toContain('HzTopologySectionLabel');
     expect(source).toContain('useI18n');
     expect(source).toContain("t('topology.search.placeholder')");
     expect(source).not.toContain('搜索实体、服务、资源或标签');
@@ -58,20 +368,683 @@ describe('topology page', () => {
     expect(source).not.toContain('Select Environment/s');
   });
 
+  it('keeps G6 single-click node and edge selection inside the topology page drawer', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
+
+    expect(source).toContain("import { useRouter } from 'next/navigation';");
+    expect(source).toContain('const topologyRouter = useRouter();');
+    expect(source).toContain("type TopologyLocalSelectionSource = 'none' | 'node-click' | 'edge-click' | 'table-row-click';");
+    expect(source).toContain('const [topologyLocalSelection, setTopologyLocalSelection]');
+    expect(source).toContain('const topologyNodeIds = React.useMemo');
+    expect(source).toContain('if (!topologyNodeIds.has(nodeId)) return;');
+    expect(source).toContain("setTopologyLocalSelection({ nodeId, edgeId: undefined, source: 'node-click' })");
+    expect(source).toContain("setTopologyLocalSelection({ nodeId: undefined, edgeId, source: 'edge-click' })");
+    expect(source).toContain("setTopologyLocalSelection({ nodeId: undefined, edgeId: row.id, source: 'table-row-click' })");
+    expect(source).toContain('const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map);');
+    expect(source).toContain('const topologyCanvasSelectedEdgeId = topologyLocalSelection.nodeId');
+    expect(source).toContain("? undefined");
+    expect(source).toContain(': topologyLocalSelectedEdge?.id ?? map.selectedEdgeId ?? topologyDetailEdge?.id;');
+    expect(source).toContain('const topologyMetricSelectedRowId = topologyLocalSelection.nodeId');
+    expect(source).toContain('selectedRowId={topologyMetricSelectedRowId}');
+    expect(source).toContain('data-topology-selection-behavior="in-page-drawer"');
+    expect(source).toContain('data-topology-selection-source={topologyLocalSelection.source}');
+    expect(source).toContain("data-topology-selection-node-id={topologyLocalSelection.nodeId ?? 'none'}");
+    expect(source).toContain("data-topology-selection-edge-id={topologyLocalSelection.edgeId ?? 'none'}");
+    expect(source).toContain('data-topology-g6-mount-lifecycle-browser-regression="selection-preserve-viewport"');
+    expect(source).toContain('data-topology-g6-mount-lifecycle-browser-regression-owner="hertzbeat-ui-g6-mount-lifecycle"');
+    expect(source).toContain('data-topology-g6-mount-lifecycle-selection-policy="node-edge-table-drawer-only"');
+    expect(source).toContain('data-topology-g6-mount-lifecycle-url-policy="preserve-current-url-on-selection"');
+    expect(source).toContain('data-topology-g6-mount-lifecycle-redraw-policy="shared-setData-draw-preserve-viewport"');
+    expect(source).toContain('data-topology-focus-navigation="explicit-soft-route"');
+    expect(source).toContain('data-topology-focus-navigation-owner="hertzbeat-ui-g6-focus-entry"');
+    expect(source).toContain('data-topology-focus-navigation-trigger="double-click-node"');
+    expect(source).toContain('data-topology-focus-route-preservation="previous-graph-until-api-resolves"');
+    expect(source).toContain('data-topology-focus-route-preservation-owner="hertzbeat-ui-g6-focus-entry"');
+    expect(source).toContain('data-topology-focus-route-preservation-source="loaded-api-graph-state"');
+    expect(source).toContain('buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t)');
+    expect(source).toContain('navigateTopologyFocus(topologyNodeFocusHrefById.get(nodeId), topologyRouter.push);');
+    expect(source).toContain('const handleTopologyFocusExit = React.useCallback');
+    expect(source).toContain('event.preventDefault();');
+    expect(source).toContain('navigateTopologyFocus(topologyFocusExitHref, topologyRouter.push);');
+    expect(source).toContain('onClick: handleTopologyFocusExit');
+    expect(source).not.toContain('setLoadedApiGraph(undefined);');
+    expect(source).not.toContain('window.location.href = href;');
+    expect(source).not.toContain('const topologyNodeHrefById');
+    expect(source).not.toContain('[topologyNodeHrefById]');
+    expect(source).not.toContain('navigateTopologySelection(topologyNodeHrefById.get(nodeId));');
+    expect(source).not.toContain('navigateTopologySelection(topologyEdgeHrefById.get(edgeId));');
+    expect(source).not.toContain('navigateTopologySelection(topologyEdgeHrefById.get(row.id));');
+  });
+
+  it('routes toolbar scope controls through soft navigation without clearing the G6 graph', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
+
+    expect(source).toContain("type TopologyLayoutMode = 'layered-service' | 'force' | 'grid-table';");
+    expect(source).toContain("const [topologyLayout] = React.useState<TopologyLayoutMode>('layered-service');");
+    expect(source).toContain('const topologyG6Layout = topologyLayout ===');
+    expect(source).not.toContain('const handleTopologyLayoutChange = React.useCallback');
+    expect(source).not.toContain('setTopologyLayout(value as TopologyLayoutMode);');
+    expect(source).toContain('const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(undefined);');
+    expect(source).toContain('const topologyEffectiveSearchQuery = topologySearchQuery ?? map.filterContext.search;');
+    expect(source).toContain('const handleTopologySearchChange = React.useCallback');
+    expect(source).toContain('setTopologySearchQuery(event.currentTarget.value);');
+    expect(source).toContain('data-topology-layout-navigation="in-page-g6-layout"');
+    expect(source).toContain('data-topology-layout-navigation-owner="hertzbeat-ui-toolbar-layout-control"');
+    expect(source).toContain('data-topology-layout-navigation-url-policy="preserve-current-url"');
+    expect(source).toContain('data-topology-search-navigation="in-page-g6-search"');
+    expect(source).toContain('data-topology-search-navigation-owner="hertzbeat-ui-toolbar-search-control"');
+    expect(source).toContain('data-topology-search-navigation-url-policy="preserve-current-url"');
+    expect(source).toContain('function buildTopologyScopeHref');
+    expect(source).toContain('const handleTopologyEnvironmentChange = React.useCallback');
+    expect(source).toContain("buildTopologyScopeHref(topologyG6ResetParams, { environment: value })");
+    expect(source).toContain('const handleTopologyDepthChange = React.useCallback');
+    expect(source).toContain("buildTopologyScopeHref(topologyG6ResetParams, { depth: value })");
+    expect(source).toContain('const handleTopologySourceKindChange = React.useCallback');
+    expect(source).toContain("buildTopologyScopeHref(topologyG6ResetParams, { sourceKind: value === 'all' ? undefined : value })");
+    expect(source).toContain('const handleTopologyGroupByChange = React.useCallback');
+    expect(source).toContain("buildTopologyScopeHref(topologyG6ResetParams, { groupBy: value })");
+    expect(source).toMatch(
+      /const topologyG6ResetParams = React\.useMemo\(\(\) => \{[\s\S]*if \(map\.filterContext\.groupBy !== 'none'\) params\.set\('groupBy', map\.filterContext\.groupBy\);/
+    );
+    expect(source).toContain('const handleTopologyResetScope = React.useCallback');
+    expect(source).toContain('event.preventDefault();');
+    expect(source).toContain("navigateTopologyFocus('/topology', topologyRouter.push);");
+    expect(source).toContain('data-topology-scope-navigation="explicit-soft-route"');
+    expect(source).toContain('data-topology-scope-navigation-owner="hertzbeat-ui-toolbar-scope-controls"');
+    expect(source).toContain('data-topology-scope-navigation-preservation="previous-graph-until-api-resolves"');
+    expect(source).toContain('onEnvironmentChange={handleTopologyEnvironmentChange}');
+    expect(source).toContain('onSourceKindChange={handleTopologySourceKindChange}');
+    expect(source).toContain('onDepthChange={handleTopologyDepthChange}');
+    expect(source).not.toContain('onLayoutChange={handleTopologyLayoutChange}');
+    expect(source).toContain('onGroupByChange={handleTopologyGroupByChange}');
+    expect(source).toContain('onSearchChange={handleTopologySearchChange}');
+    expect(source).toContain('onReset={handleTopologyResetScope}');
+    expect(source).not.toContain('layoutValue={topologyLayout}');
+    expect(source).toContain('searchValue={topologyEffectiveSearchQuery}');
+    expect(source).toContain("layout={topologyLayout}");
+    expect(source).toContain("layout={topologyG6Layout}");
+    expect(source).toContain('searchQuery={topologyEffectiveSearchQuery}');
+    expect(source).not.toContain('setLoadedApiGraph(undefined);');
+  });
+
+  it('exposes initial in-page selection source markers for real topology browser checks', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage apiGraph={buildApiTopologyFixture()} />);
+
+    expect(html).toContain('data-topology-selection-behavior="in-page-drawer"');
+    expect(html).toContain('data-topology-selection-source="none"');
+    expect(html).toContain('data-topology-selection-node-id="none"');
+    expect(html).toContain('data-topology-selection-edge-id="none"');
+    expect(html).toContain('data-topology-g6-mount-lifecycle-browser-regression="selection-preserve-viewport"');
+    expect(html).toContain('data-topology-g6-mount-lifecycle-browser-regression-owner="hertzbeat-ui-g6-mount-lifecycle"');
+    expect(html).toContain('data-topology-g6-mount-lifecycle-selection-policy="node-edge-table-drawer-only"');
+    expect(html).toContain('data-topology-g6-mount-lifecycle-url-policy="preserve-current-url-on-selection"');
+    expect(html).toContain('data-topology-g6-mount-lifecycle-redraw-policy="shared-setData-draw-preserve-viewport"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-policy="structure-layout-height-only"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-redraw-policy="setData-draw-preserve-viewport"');
+    expect(html).toContain('data-topology-focus-navigation="explicit-soft-route"');
+    expect(html).toContain('data-topology-focus-navigation-owner="hertzbeat-ui-g6-focus-entry"');
+    expect(html).toContain('data-topology-focus-navigation-trigger="double-click-node"');
+    expect(html).toContain('data-topology-focus-route-preservation="previous-graph-until-api-resolves"');
+    expect(html).toContain('data-topology-focus-route-preservation-owner="hertzbeat-ui-g6-focus-entry"');
+    expect(html).toContain('data-topology-focus-route-preservation-source="loaded-api-graph-state"');
+    expect(html).toContain('data-topology-scope-navigation="explicit-soft-route"');
+    expect(html).toContain('data-topology-scope-navigation-owner="hertzbeat-ui-toolbar-scope-controls"');
+    expect(html).toContain('data-topology-scope-navigation-preservation="previous-graph-until-api-resolves"');
+    expect(html).toContain('data-topology-layout-navigation="in-page-g6-layout"');
+    expect(html).toContain('data-topology-layout-navigation-owner="hertzbeat-ui-toolbar-layout-control"');
+    expect(html).toContain('data-topology-layout-navigation-url-policy="preserve-current-url"');
+    expect(html).toContain('data-topology-search-navigation="in-page-g6-search"');
+    expect(html).toContain('data-topology-search-navigation-owner="hertzbeat-ui-toolbar-search-control"');
+    expect(html).toContain('data-topology-search-navigation-url-policy="preserve-current-url"');
+  }, 120000);
+
   it('renders HertzBeat topology sources, graph nodes, and operations closure actions', async () => {
-    const { default: TopologyPage } = await import('./page');
-    const html = renderToStaticMarkup(<TopologyPage />);
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage apiGraph={buildApiTopologyFixture()} />);
 
     expect(html).toContain('data-topology-route="hertzbeat-entity-topology"');
+    expect(html).toContain('data-topology-data-source="api"');
+    expect(html).toContain('data-topology-workbench-frame-owner="hertzbeat-ui-workbench-frame"');
+    expect(html).toContain('data-topology-workbench-frame-boundary-owner="hertzbeat-ui-workbench-frame-boundary"');
+    expect(html).toContain('data-hz-ui="topology-workbench-frame"');
+    expect(html).toContain('data-hz-topology-primitive="workbench-frame"');
+    expect(html).toContain('data-hz-topology-workbench-boundary="route"');
+    expect(html).toContain('data-hz-topology-workbench-frame-boundary-owner="hertzbeat-ui-workbench-frame-boundary"');
+    expect(html).toContain('data-topology-workbench-header-owner="hertzbeat-ui-workbench-header"');
+    expect(html).toContain('data-hz-ui="topology-workbench-header"');
+    expect(html).toContain('data-hz-topology-primitive="workbench-header"');
+    expect(html).toContain('data-hz-topology-workbench-header-owner="hertzbeat-ui-workbench-header"');
+    expect(html).toContain('data-hz-topology-workbench-eyebrow-owner="hertzbeat-ui-workbench-eyebrow"');
+    expect(html).toContain('data-hz-topology-workbench-title-owner="hertzbeat-ui-workbench-title"');
+    expect(html).toContain('data-hz-topology-workbench-header-density="operational-compact"');
+    expect(html).toContain('data-hz-topology-workbench-header-density-owner="hertzbeat-ui-workbench-header-density"');
+    expect(html).toContain('data-hz-topology-workbench-header-boundary="none"');
+    expect(html).toContain('data-hz-topology-workbench-header-boundary-owner="hertzbeat-ui-workbench-header-boundary"');
+    expect(html).toContain('data-hz-topology-workbench-header-alignment="shared-control-grid"');
+    expect(html).toContain('data-hz-topology-workbench-header-inset="16px"');
+    expect(html).toContain('data-hz-topology-workbench-header-control-height="28px"');
+    expect(html).toContain('data-hz-topology-workbench-copy-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-workbench-scope-slot-owner="hertzbeat-ui-workbench-scope-slot"');
+    expect(html).not.toContain('data-hz-topology-workbench-source-slot-owner="hertzbeat-ui-workbench-source-slot"');
+    expect(html).toContain('data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
+    expect(html).toContain('data-hz-ui="topology-workbench-grid"');
+    expect(html).toContain('data-hz-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
+    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-only"');
+    expect(html).toContain('data-topology-scope-bar-owner="hertzbeat-ui-scope-bar"');
+    expect(html).toContain('data-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"');
+    expect(html).toContain('data-hz-ui="topology-scope-bar"');
+    expect(html).toContain('data-hz-topology-primitive="scope-bar"');
+    expect(html).toContain('data-hz-topology-scope-bar-boundary="none"');
+    expect(html).toContain('data-hz-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"');
+    expect(html).toContain('data-hz-topology-scope-item="environment"');
+    expect(html).toContain('data-hz-topology-scope-item-owner="hertzbeat-ui-scope-item"');
+    expect(html).toContain('data-hz-topology-scope-item-value-owner="hertzbeat-ui-scope-item-value"');
+    expect(html).toContain('data-hz-topology-scope-item="time-range"');
+    expect(html).toContain('data-hz-topology-scope-action="refresh"');
+    expect(html).toContain('data-hz-topology-scope-action-owner="hertzbeat-ui-scope-action"');
+    expect(html).toContain('data-topology-focus-trail="focused-context"');
+    expect(html).toContain('data-topology-focus-trail-owner="hertzbeat-ui-focus-trail"');
+    expect(html).toContain('data-topology-focus-trail-boundary-owner="hertzbeat-ui-focus-trail-boundary"');
+    expect(html).toContain('data-hz-ui="topology-focus-trail"');
+    expect(html).toContain('data-hz-topology-primitive="focus-trail"');
+    expect(html).toContain('data-hz-topology-focus-trail-mode="focused"');
+    expect(html).toContain('data-hz-topology-focus-trail-mode-owner="hertzbeat-ui-focus-trail-mode"');
+    expect(html).toContain('data-hz-topology-focus-trail-depth="2"');
+    expect(html).toContain('data-hz-topology-focus-trail-depth-owner="hertzbeat-ui-focus-trail-depth"');
+    expect(html).toContain('data-hz-topology-focus-trail-entity="svc-checkout"');
+    expect(html).toContain('data-hz-topology-focus-trail-entity-owner="hertzbeat-ui-focus-trail-entity"');
+    expect(html).toContain('data-hz-topology-focus-trail-density="graph-dock"');
+    expect(html).toContain('data-hz-topology-focus-trail-layout="single-line-nowrap"');
+    expect(html).toContain('data-hz-topology-focus-trail-height-contract="one-compact-row"');
+    expect(html).toContain('data-hz-topology-focus-trail-occlusion="none"');
+    expect(html).toContain('data-hz-topology-focus-trail-position-contract="document-flow"');
+    expect(html).toContain('data-hz-topology-focus-trail-priority="canvas"');
+    expect(html).toContain('data-hz-topology-focus-trail-alignment="shared-control-grid"');
+    expect(html).toContain('data-hz-topology-focus-trail-inset="16px"');
+    expect(html).toContain('data-hz-topology-focus-trail-control-height="28px"');
+    expect(html).toContain('data-hz-topology-focus-trail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-focus-trail-visual-weight-owner="hertzbeat-ui-focus-trail-visual-weight"');
+    expect(html).toContain('data-hz-topology-focus-trail-chrome="frameless"');
+    expect(html).toContain('data-hz-topology-focus-trail-frame="none"');
+    expect(html).toContain('data-topology-focus-trail-compactness="graph-dock"');
+    expect(html).toContain('data-hz-topology-focus-trail-boundary="none"');
+    expect(html).toContain('data-hz-topology-focus-trail-boundary-owner="hertzbeat-ui-focus-trail-boundary"');
+    expect(html).toContain('data-hz-topology-focus-trail-label-owner="hertzbeat-ui-focus-trail-label"');
+    expect(html).toContain('data-hz-topology-focus-crumbs-owner="hertzbeat-ui-focus-trail-crumbs"');
+    expect(html).toContain('data-hz-topology-focus-crumb="all"');
+    expect(html).toContain('data-hz-topology-focus-crumb-owner="hertzbeat-ui-focus-trail-crumb"');
+    expect(html).toContain('data-hz-topology-focus-crumb="active-entity"');
+    expect(html).toContain('data-hz-topology-focus-crumb-active="true"');
+    expect(html).toContain('data-hz-topology-focus-crumb-label-owner="hertzbeat-ui-focus-trail-crumb-label"');
+    expect(html).toContain('data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value"');
+    expect(html).toContain('data-hz-topology-focus-filters-owner="hertzbeat-ui-focus-trail-filters"');
+    expect(html).toContain('data-hz-topology-focus-filter="environment"');
+    expect(html).toContain('data-hz-topology-focus-filter="time-range"');
+    expect(html).toContain('data-hz-topology-focus-filter="source"');
+    expect(html).toContain('data-hz-topology-focus-filter="view"');
+    expect(html).toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).toContain('data-topology-focus-trail-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).toContain('data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit"');
+    expect(html).toContain('data-topology-focus-trail-exit-owner="hertzbeat-ui-focus-trail-exit"');
+    expect(html).toMatch(
+      /data-hz-topology-focus-exit-href="\/topology\?[^"]*environment=prod[^"]*timeRange=last-1h[^"]*depth=2/
+    );
+    expect(html).toContain('data-hz-topology-focus-exit-href-owner="hertzbeat-ui-focus-trail-exit-href"');
+    expect(html).toContain('data-topology-group-panel="large-graph-grouping"');
+    expect(html).toContain('data-topology-group-panel-owner="hertzbeat-ui-group-panel"');
+    expect(html).toContain('data-topology-group-panel-boundary-owner="hertzbeat-ui-group-panel-boundary"');
+    expect(html).toContain('data-hz-ui="topology-group-panel"');
+    expect(html).toContain('data-hz-topology-primitive="group-panel"');
+    expect(html).toContain('data-hz-topology-group-panel-boundary="framed"');
+    expect(html).toContain('data-hz-topology-group-panel-item="entity-type"');
+    expect(html).toContain('data-hz-topology-group-panel-item="environment"');
+    expect(html).toContain('data-hz-topology-group-panel-item="source-kind"');
+    expect(html).toContain('data-hz-topology-group-panel-item-active="true"');
+    expect(html).toContain('data-hz-topology-group-panel-item-collapsed-count="');
+    expect(html).toContain('data-hz-topology-group-panel-action="clear-group"');
+    expect(html).toContain('data-topology-path-summary="selected-edge-context"');
+    expect(html).toContain('data-topology-path-summary-owner="hertzbeat-ui-path-summary"');
+    expect(html).toContain('data-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"');
+    expect(html).toContain('data-hz-ui="topology-path-summary"');
+    expect(html).toContain('data-hz-topology-primitive="path-summary"');
+    expect(html).toContain('data-hz-topology-path-summary-boundary="section"');
+    expect(html).toContain('data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"');
+    expect(html).toContain('data-hz-topology-path-selected-edge="svc-frontend--svc-checkout"');
+    expect(html).toContain('data-hz-topology-path-hovered-edge="svc-frontend--svc-checkout"');
+    expect(html).toContain('data-hz-topology-path-source-id="svc-frontend"');
+    expect(html).toContain('data-hz-topology-path-target-id="svc-checkout"');
+    expect(html).toContain('data-hz-topology-path-relation-type="trace-call"');
+    expect(html).toContain('data-hz-topology-path-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-path-endpoint="source"');
+    expect(html).toContain('data-hz-topology-path-endpoint="target"');
+    expect(html).toContain('data-hz-topology-path-relation-owner="hertzbeat-ui-path-summary-relation"');
+    expect(html).toContain('data-hz-topology-path-metric="request-rate"');
+    expect(html).toContain('data-hz-topology-path-action="focus-path"');
+    expect(html).toContain('data-hz-topology-path-action="open-trace"');
     expect(html).toContain('data-topology-controls="hertzbeat-topology-controls"');
-    expect(html).toContain('data-topology-source-strip="relationship-source-contract"');
-    expect(html).toContain('data-topology-source-control="relationship-source"');
+    expect(html).toContain('data-topology-controls-owner="hertzbeat-ui-toolbar"');
+    expect(html).toContain('data-topology-toolbar-boundary-owner="hertzbeat-ui-toolbar-boundary"');
+    expect(html).toContain('data-hz-ui="topology-toolbar"');
+    expect(html).toContain('data-hz-topology-primitive="toolbar"');
+    expect(html).toContain('data-hz-topology-toolbar-density="graph-first"');
+    expect(html).toContain('data-hz-topology-toolbar-density-owner="hertzbeat-ui-toolbar-density"');
+    expect(html).toContain('data-hz-topology-toolbar-first-viewport-priority="canvas"');
+    expect(html).toContain('data-hz-topology-toolbar-first-viewport-owner="hertzbeat-ui-toolbar-first-viewport"');
+    expect(html).toContain('data-hz-topology-toolbar-row-contract="single-row-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-row-contract-owner="hertzbeat-ui-toolbar-row-contract"');
+    expect(html).toContain('data-hz-topology-toolbar-alignment="flush-control-grid"');
+    expect(html).toContain('data-hz-topology-toolbar-inset="0px"');
+    expect(html).toContain('data-hz-topology-toolbar-control-height="28px"');
+    expect(html).not.toContain('overflow-x-auto px-4 py-1');
+    expect(html).toContain('overflow-x-auto px-0 py-1');
+    expect(html).toContain('data-hz-topology-toolbar-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-toolbar-visual-weight-owner="hertzbeat-ui-toolbar-visual-weight"');
+    expect(html).toContain('data-hz-topology-toolbar-secondary-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-toolbar-secondary-visibility-owner="hertzbeat-ui-toolbar-secondary-visibility"');
+    expect(html).toContain('data-hz-topology-toolbar-boundary="none"');
+    expect(html).toContain('data-hz-topology-toolbar-boundary-owner="hertzbeat-ui-toolbar-boundary"');
+    expect(html).toContain('data-hz-topology-toolbar-action-policy="scope-controls-only"');
+    expect(html).toContain('data-hz-topology-toolbar-canvas-action-policy="in-canvas-g6-toolbar"');
+    expect(html).toContain('data-hz-topology-toolbar-chrome="frameless"');
+    expect(html).toContain('data-hz-topology-toolbar-frame="none"');
+    expect(html).toContain('data-hz-topology-control="environment"');
+    expect(html).toContain('data-hz-topology-control-owner="hertzbeat-ui-toolbar-control"');
+    expect(html).toContain('data-hz-topology-control="search"');
+    expect(html).toContain('data-hz-topology-control="source-kind"');
+    expect(html).toContain('data-hz-topology-control-source-kind-owner="hertzbeat-ui-toolbar-source-kind-control"');
+    expect(html).toContain('data-hz-topology-control-source-kind-value="');
+    expect(html).not.toContain('data-hz-topology-control="fit-view"');
+    expect(html).not.toContain('data-hz-topology-control="locate-entity"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip="source-depth-group-reset"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-owner="hertzbeat-ui-toolbar-control-strip"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
+    expect(html).toContain('data-hz-topology-control="depth"');
+    expect(html).toContain('data-hz-topology-control-depth-owner="hertzbeat-ui-toolbar-depth-control"');
+    expect(html).toContain('data-hz-topology-control-depth-value="2"');
+    expect(html).not.toContain('data-hz-topology-control="layout"');
+    expect(html).toContain('data-hz-topology-control="group-by"');
+    expect(html).toContain('data-hz-topology-control-group-owner="hertzbeat-ui-toolbar-group-control"');
+    expect(html).toContain('data-hz-topology-control-group-value="none"');
+    expect(html).toContain('data-hz-topology-control="reset-scope"');
+    expect(html).toContain('data-hz-topology-control-reset-owner="hertzbeat-ui-toolbar-reset-control"');
+    expect(html).toContain('data-hz-topology-toolbar-summary-owner="hertzbeat-ui-toolbar-summary"');
+    expect(html).toContain('data-hz-topology-toolbar-summary-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-toolbar-summary-label-owner="hertzbeat-ui-toolbar-summary-label"');
+    expect(html).toContain('data-hz-topology-summary-item-owner="hertzbeat-ui-toolbar-summary-item"');
+    expect(html).toContain('data-topology-toolbar-state-owner="hertzbeat-ui-toolbar-state"');
+    expect(html).toContain('data-hz-topology-toolbar-state="focus-depth-group"');
+    expect(html).toContain('data-hz-topology-toolbar-state-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-toolbar-state-owner="hertzbeat-ui-toolbar-state"');
+    expect(html).toContain('data-hz-topology-state-label-owner="hertzbeat-ui-toolbar-state-label"');
+    expect(html).toContain('data-hz-topology-state-item="focus"');
+    expect(html).toContain('data-hz-topology-state-item-owner="hertzbeat-ui-toolbar-state-item"');
+    expect(html).toContain('data-hz-topology-state-indicator-owner="hertzbeat-ui-toolbar-state-indicator"');
+    expect(html).toContain('data-hz-topology-state-item-label-owner="hertzbeat-ui-toolbar-state-item-label"');
+    expect(html).toContain('data-hz-topology-state-item-value-owner="hertzbeat-ui-toolbar-state-item-value"');
+    expect(html).toContain('data-hz-topology-state-item="depth"');
+    expect(html).not.toContain('data-hz-topology-state-item="layout"');
+    expect(html).toContain('data-hz-topology-state-item="group"');
+    expect(html).toContain('data-topology-focus-state="svc-checkout"');
+    expect(html).toContain('data-topology-depth-state="2"');
+    expect(html).toContain('data-topology-group-state="none"');
+    expect(html).not.toContain('data-topology-source-strip="relationship-source-contract"');
+    expect(html).not.toContain('data-topology-source-control="relationship-source"');
+    expect(html).toContain('data-topology-view-mode-strip-owner="hertzbeat-ui-filter-strip"');
+    expect(html).toContain('data-hz-topology-filter-strip-variant="view-list"');
+    expect(html).toContain('data-hz-topology-filter-strip-copy-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-filter-strip-copy-visibility-owner="hertzbeat-ui-filter-strip-copy-visibility"');
+    expect(html).toContain('data-topology-view-mode-strip-boundary-owner="hertzbeat-ui-filter-strip-boundary"');
+    expect(html).toContain('data-hz-topology-filter-item="service-call"');
+    expect(html).toContain('data-hz-topology-filter-item-label-owner="hertzbeat-ui-filter-strip-label"');
+    expect(html).toContain('data-hz-topology-filter-item-copy-owner="hertzbeat-ui-filter-strip-copy"');
+    expect(html).toContain('sr-only');
+    expect(html).toContain('data-topology-companion-rail-owner="hertzbeat-ui-companion-rail"');
+    expect(html).toContain('data-topology-companion-rail-boundary-owner="hertzbeat-ui-companion-rail-boundary"');
+    expect(html).toContain('data-topology-companion-rail-scope="legend-metrics-timeline-detail"');
+    expect(html).toContain('data-topology-companion-rail-visibility="hidden-from-layout"');
+    expect(html).toContain('data-topology-companion-rail-priority="graph-first"');
+    expect(html).toContain('data-hz-ui="topology-companion-rail"');
+    expect(html).toContain('data-hz-topology-primitive="companion-rail"');
+    expect(html).toContain('data-hz-topology-companion-priority="graph-first"');
+    expect(html).toContain('data-hz-topology-companion-priority-owner="hertzbeat-ui-companion-rail-priority"');
+    expect(html).toContain('data-hz-topology-companion-scroll="contained"');
+    expect(html).toContain('data-hz-topology-companion-scroll-owner="hertzbeat-ui-companion-rail-scroll"');
+    expect(html).toContain('data-hz-topology-companion-viewport-contract="graph-height"');
+    expect(html).toContain('data-hz-topology-companion-sticky-context="jump-list"');
+    expect(html).toContain('data-hz-topology-companion-sticky-context-owner="hertzbeat-ui-companion-rail-sticky-context"');
+    expect(html).toContain('data-hz-topology-companion-sticky-target="topology-companion-jump-list"');
+    expect(html).toContain('data-hz-topology-companion-sticky-target-owner="hertzbeat-ui-companion-rail-sticky-target"');
+    expect(html).toContain('data-topology-companion-jump-list-owner="hertzbeat-ui-companion-jump-list"');
+    expect(html).toContain('data-hz-ui="topology-companion-jump-list"');
+    expect(html).toContain('data-hz-topology-primitive="companion-jump-list"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-owner="hertzbeat-ui-companion-jump-list"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-density="graph-first"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-interaction="anchor-jump"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-sticky="top"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-scroll-scope="contained-rail"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-scroll-scope-owner="hertzbeat-ui-companion-jump-list-scroll-scope"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-active-mode="contained-rail-scroll"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-active-mode-owner="hertzbeat-ui-companion-jump-list-active-mode"');
+    expect(html).toContain('data-hz-topology-companion-jump-item="view-mode"');
+    expect(html).toContain('data-hz-topology-companion-jump-href="#topology-companion-view-mode"');
+    expect(html).toContain('data-hz-topology-companion-jump-scroll-owner="hertzbeat-ui-companion-jump-scroll"');
+    expect(html).toContain('data-hz-topology-companion-jump-active-source="manual"');
+    expect(html).toContain('data-hz-topology-companion-jump-item="edge-red"');
+    expect(html).toContain('data-hz-topology-companion-jump-href="#topology-companion-edge-red"');
+    expect(html).toContain('data-topology-companion-section="legend"');
+    expect(html).toContain('data-topology-companion-section="edge-red"');
+    expect(html).toContain('data-topology-companion-section="current-node"');
+    expect(html).toContain('data-hz-ui="topology-companion-section"');
+    expect(html).toContain('data-hz-topology-primitive="companion-section"');
+    expect(html).toContain('data-hz-topology-companion-section-owner="hertzbeat-ui-companion-section"');
+    expect(html).toContain('data-hz-topology-companion-section-anchor-owner="hertzbeat-ui-companion-section-anchor"');
+    expect(html).toContain('data-topology-companion-section-collapsible="edge-red"');
+    expect(html).toContain('data-hz-topology-companion-section-collapsible="true"');
+    expect(html).toContain('data-hz-topology-companion-section-toggle-owner="hertzbeat-ui-companion-section-toggle"');
+    expect(html).toContain('data-hz-topology-companion-section-body-owner="hertzbeat-ui-companion-section-body"');
+    expect(html).toContain('data-hz-topology-companion-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-companion-visual-weight-owner="hertzbeat-ui-companion-rail-visual-weight"');
+    expect(html).toContain('data-hz-topology-companion-spacing="shared-stack"');
+    expect(html).toContain('data-hz-topology-companion-spacing-owner="hertzbeat-ui-companion-rail-spacing"');
+    expect(html).toContain('data-hz-topology-companion-boundary="side"');
+    expect(html).toContain('data-hz-topology-companion-boundary-owner="hertzbeat-ui-companion-rail-boundary"');
+    expect(html).toContain('data-hz-topology-companion-content-owner="hertzbeat-ui-companion-rail-content"');
+    expect(html).toContain('data-topology-view-mode-label-owner="hertzbeat-ui-section-label"');
+    expect(html).toContain('data-hz-ui="topology-section-label"');
+    expect(html).toContain('data-hz-topology-primitive="section-label"');
+    expect(html).toContain('data-hz-topology-section-label-density="compact"');
+    expect(html).toContain('data-hz-topology-section-label-owner="hertzbeat-ui-section-label"');
+    expect(html).toContain('data-hz-topology-section-label-text-owner="hertzbeat-ui-section-label-text"');
     expect(html).toContain('data-topology-canvas="hertzbeat-topology-canvas"');
+    expect(html).toContain('data-topology-canvas-owner="hertzbeat-ui-canvas"');
+    expect(html).toContain('data-topology-canvas-slot-owner="hertzbeat-ui-workbench-slot"');
+    expect(html).toContain('data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"');
+    expect(html).toContain('data-hz-ui="topology-workbench-slot"');
+    expect(html).toContain('data-hz-topology-primitive="workbench-slot"');
+    expect(html).toContain('data-hz-topology-workbench-slot-owner="hertzbeat-ui-workbench-slot"');
+    expect(html).toContain('data-hz-topology-workbench-slot-kind="canvas"');
+    expect(html).toContain('data-hz-topology-workbench-slot-kind="companion"');
+    expect(html).toContain('data-hz-topology-workbench-slot-surface="content"');
+    expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness="none"');
+    expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness-owner="hertzbeat-ui-workbench-grid-canvas-stickiness"');
+    expect(html).toContain('data-hz-ui="topology-canvas"');
+    expect(html).toContain('data-hz-topology-primitive="canvas"');
+    expect(html).toContain('data-hz-topology-canvas-layout="layered-service"');
+    expect(html).toContain('data-hz-topology-canvas-layout-owner="hertzbeat-ui-canvas-layout"');
+    expect(html).toContain('data-hz-topology-canvas-interaction-mode="inspect"');
+    expect(html).toContain('data-topology-canvas-interaction-owner="hertzbeat-ui-canvas-interaction"');
+    expect(html).toContain('data-hz-topology-canvas-interaction-owner="hertzbeat-ui-canvas-interaction"');
+    expect(html).toContain('data-topology-canvas-interaction-scope-owner="hertzbeat-ui-canvas-interaction-scope"');
+    expect(html).toContain('data-hz-topology-canvas-interaction-scope-owner="hertzbeat-ui-canvas-interaction-scope"');
+    expect(html).toContain('data-hz-topology-canvas-interaction-scope="hover-group"');
+    expect(html).toContain('data-topology-canvas-boundary-owner="hertzbeat-ui-canvas-boundary"');
+    expect(html).toContain('data-hz-topology-canvas-boundary="none"');
+    expect(html).toContain('data-hz-topology-canvas-boundary-owner="hertzbeat-ui-canvas-boundary"');
+    expect(html).toContain('data-hz-topology-canvas-min-height-owner="hertzbeat-ui-canvas-min-height"');
+    expect(html).toContain('data-hz-topology-canvas-hover-mode="neighbor-highlight"');
+    expect(html).toContain('data-hz-topology-canvas-drawer-mode="node-edge"');
+    expect(html).toContain('data-hz-topology-canvas-focus-depth="2-hop"');
+    expect(html).toContain('data-topology-canvas-annotation-owner="hertzbeat-ui-canvas-annotation"');
+    expect(html).toContain('data-hz-ui="topology-canvas-annotation"');
+    expect(html).toContain('data-hz-topology-primitive="canvas-annotation"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-placement="top-left"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-owner="hertzbeat-ui-canvas-annotation"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-occlusion="none"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-hit-test="pass-through"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-title-owner="hertzbeat-ui-canvas-annotation-title"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-copy-owner="hertzbeat-ui-canvas-annotation-copy"');
+    expect(html).toContain('data-topology-renderer="antv-g6"');
+    expect(html).toContain('data-topology-g6-canvas-owner="hertzbeat-ui-g6-canvas"');
+    expect(html).toContain('data-hz-ui="topology-g6-canvas"');
+    expect(html).toContain('data-hz-topology-primitive="g6-canvas"');
+    expect(html).toContain('data-hz-topology-g6-engine="antv-g6"');
+    expect(html).toContain('data-hz-topology-g6-interaction-owner="hertzbeat-ui-g6-interaction"');
+    expect(html).toContain('data-hz-topology-g6-auto-fit="initial-only"');
+    expect(html).toContain('data-hz-topology-g6-auto-fit-owner="hertzbeat-ui-g6-auto-fit"');
+    expect(html).toContain('data-hz-topology-g6-viewport-telemetry="live"');
+    expect(html).toContain('data-hz-topology-g6-viewport-telemetry-owner="hertzbeat-ui-g6-viewport-telemetry"');
+    expect(html).toContain('data-hz-topology-g6-viewport-telemetry-timing="after-action-settled"');
+    expect(html).toContain('data-hz-topology-g6-viewport-telemetry-timing-owner="hertzbeat-ui-g6-viewport-telemetry-timing"');
+    expect(html).toContain('data-hz-topology-g6-viewport-alias-owner="hertzbeat-ui-g6-viewport-telemetry-alias"');
+    expect(html).toContain('data-hz-topology-g6-viewport-zoom="unknown"');
+    expect(html).toContain('data-hz-topology-g6-viewport-position="unknown"');
+    expect(html).toContain('data-hz-topology-g6-command-bridge="custom-event"');
+    expect(html).toContain('data-hz-topology-g6-command-bridge-owner="hertzbeat-ui-g6-command-bridge"');
+    expect(html).toContain('data-hz-topology-g6-command-event="hz-topology-g6-viewport-command"');
+    expect(html).toContain('data-hz-topology-g6-command-request="idle"');
+    expect(html).toContain('data-hz-topology-g6-command-request-owner="hertzbeat-ui-g6-command-request"');
+    expect(html).toContain('data-hz-topology-g6-wheel-mode="manual-clamped-g6-zoom"');
+    expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
+    expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
+    expect(html).toContain('data-hz-topology-g6-wheel-origin="pointer-clamped"');
+    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-4.8"');
+    expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts="plus-minus-zero-fit"');
+    expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts-owner="hertzbeat-ui-g6-keyboard"');
+    expect(html).toContain('data-hz-topology-g6-keyboard-actions="zoom-in zoom-out reset-view fit-view"');
+    expect(html).toContain(
+      'data-topology-g6-render-window-priority-contract="search-only-no-selection-reorder"'
+    );
+    expect(html).toContain(
+      'data-topology-g6-render-window-priority-owner="hertzbeat-ui-g6-render-window-priority"'
+    );
+    expect(html).toContain(
+      'data-hz-topology-g6-render-window-priority-behavior="search-only-no-selection-reorder"'
+    );
+    expect(html).toContain('data-hz-topology-g6-minimap="viewport-overview"');
+    expect(html).toContain('data-hz-topology-g6-minimap-owner="hertzbeat-ui-g6-minimap"');
+    expect(html).toContain('data-hz-topology-g6-minimap-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-g6-minimap-occlusion="none"');
+    expect(html).toContain('data-hz-topology-g6-minimap-node-count="7"');
+    expect(html).toContain('data-hz-topology-g6-minimap-edge-count="7"');
+    expect(html).toContain('data-hz-topology-g6-minimap-viewport-source="initial"');
+    expect(html).toContain('data-hz-topology-g6-node-selection="drawer-sync"');
+    expect(html).toContain('data-hz-topology-g6-edge-selection="drawer-sync"');
+    expect(html).toContain('data-hz-topology-g6-hover-owner="hertzbeat-ui-g6-hover"');
+    expect(html).toContain('data-hz-topology-g6-hover-source="none"');
+    expect(html).toContain('data-hz-topology-g6-hovered-node="none"');
+    expect(html).toContain('data-hz-topology-g6-hovered-edge="none"');
+    expect(html).toContain('data-hz-topology-g6-active-focus-source="search-node"');
+    expect(html).toContain('data-hz-topology-g6-search-owner="hertzbeat-ui-g6-search"');
+    expect(html).toContain('data-hz-topology-g6-search-query="checkout-api"');
+    expect(html).toContain('data-hz-topology-g6-search-match-count="1"');
+    expect(html).toContain('data-hz-topology-g6-search-first-match="svc-checkout"');
+    expect(html).toContain('data-hz-topology-g6-selected-focus-owner="hertzbeat-ui-g6-selected-focus"');
+    expect(html).toContain('data-hz-topology-g6-selected-focus-target="svc-checkout"');
+    expect(html).toContain('data-hz-topology-g6-selected-focus-status="ready"');
+    expect(html).toContain('data-hz-topology-g6-action="focus-selected-node"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-focus-owner="hertzbeat-ui-g6-neighbor-focus"');
+    expect(html).toContain('data-hz-topology-g6-focused-node="svc-checkout"');
+    expect(html).toContain('data-hz-topology-g6-upstream-node-count="4"');
+    expect(html).toContain('data-hz-topology-g6-downstream-node-count="2"');
+    expect(html).toContain('data-hz-topology-g6-node-neighbor-owner="hertzbeat-ui-g6-node-neighbor"');
+    expect(html).toContain('data-hz-topology-g6-edge-neighbor-owner="hertzbeat-ui-g6-edge-neighbor"');
+    expect(html).toContain('data-hz-topology-g6-stage="antv-g6-stage"');
+    expect(html).toContain('data-hz-topology-g6-action="fit-view"');
+    expect(html).toContain('data-hz-topology-g6-action="reset-view"');
+    expect(html).toContain('data-hz-topology-g6-filter-owner="hertzbeat-ui-g6-filter"');
+    expect(html).toContain('data-hz-topology-g6-filter-environment="prod"');
+    expect(html).toContain('data-hz-topology-g6-filter-source-kind="all"');
+    expect(html).toContain('data-hz-topology-g6-filter-search-query="checkout-api"');
+    expect(html).toContain('data-hz-topology-g6-filter-group-by="none"');
+    expect(html).toContain('data-hz-topology-g6-filter-visible-node-count="7"');
+    expect(html).toContain('data-hz-topology-g6-filter-visible-edge-count="7"');
+    expect(html).toContain('data-hz-topology-g6-filter-source-match="true"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-owner="hertzbeat-ui-g6-filter-control"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-count="4"');
+    expect(html).toContain('data-hz-topology-g6-filter-control="source-kind"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-kind="source-kind"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-value="all"');
+    expect(html).toContain('data-hz-topology-g6-filter-control="group-by"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-kind="group-by"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-value="none"');
+    expect(html).toContain('data-hz-topology-g6-filter-control="search"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-kind="search"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-value="checkout-api"');
+    expect(html).toContain('data-hz-topology-g6-filter-control="reset"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-kind="reset"');
+    expect(html).toContain('data-hz-topology-g6-overlay-mode="non-occluding"');
+    expect(html).toContain('data-hz-topology-g6-summary-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-surface-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-visibility="visible"');
+    expect(html.indexOf('data-topology-canvas="hertzbeat-topology-canvas"')).toBeLessThan(
+      html.indexOf('data-topology-group-panel="large-graph-grouping"')
+    );
+    expect(html.indexOf('data-topology-canvas="hertzbeat-topology-canvas"')).toBeLessThan(
+      html.indexOf('data-topology-path-summary="selected-edge-context"')
+    );
     expect(html).toContain('data-topology-node="service-node"');
+    expect(html).toContain('data-topology-node-owner="hertzbeat-ui-node"');
+    expect(html).toContain('data-hz-ui="topology-node"');
+    expect(html).toContain('data-hz-topology-primitive="node"');
+    expect(html).toContain('data-hz-topology-node-owner="hertzbeat-ui-node"');
+    expect(html).toContain('data-hz-topology-node-focus="active"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-role="focus"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-role="upstream"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-role="downstream"');
+    expect(html).toContain('data-hz-topology-node-label-owner="hertzbeat-ui-node-label"');
+    expect(html).toContain('data-hz-topology-node-health-owner="hertzbeat-ui-node-health"');
+    expect(html).toContain('data-hz-topology-node-health-label-owner="hertzbeat-ui-node-health-label"');
+    expect(html).toContain('data-hz-topology-node-red-owner="hertzbeat-ui-node-red"');
+    expect(html).toContain('data-hz-topology-node-red-metric-owner="hertzbeat-ui-node-red-metric"');
+    expect(html).toContain('data-hz-topology-node-badge-list-owner="hertzbeat-ui-node-badge-list"');
+    expect(html).toContain('data-hz-topology-node-badge-owner="hertzbeat-ui-node-badge"');
     expect(html).toContain('data-topology-node-health-affordance="lightweight-service-health"');
     expect(html).toContain('data-topology-node-health-copy="lightweight-service-health"');
     expect(html).toContain('data-topology-edge="service-edge"');
-    expect(html).toContain('HertzBeat 企业运维拓扑');
+    expect(html).toContain('data-topology-edge-owner="hertzbeat-ui-edge"');
+    expect(html).toContain('data-hz-ui="topology-edge"');
+    expect(html).toContain('data-hz-topology-primitive="edge"');
+    expect(html).toContain('data-hz-topology-edge-owner="hertzbeat-ui-edge"');
+    expect(html).toContain('data-hz-topology-edge-variant="line"');
+    expect(html).toContain('data-hz-topology-edge-line-owner="hertzbeat-ui-edge-line"');
+    expect(html).toContain('data-hz-topology-edge-path-owner="hertzbeat-ui-edge-path"');
+    expect(html).toContain('data-hz-topology-edge-arrow-owner="hertzbeat-ui-edge-arrow"');
+    expect(html).toContain('data-hz-topology-edge-variant="drilldown"');
+    expect(html).toContain('data-hz-topology-edge-drilldown-owner="hertzbeat-ui-edge-drilldown"');
+    expect(html).toContain('data-hz-topology-edge-hit-target-owner="hertzbeat-ui-edge-hit-target"');
+    expect(html).toContain('data-hz-topology-edge-red-owner="hertzbeat-ui-edge-red"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-role="incoming"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-role="outgoing"');
+    expect(html).toContain('data-hz-topology-edge-badge-owner="hertzbeat-ui-edge-badge"');
+    expect(html).not.toContain('data-topology-hover-tooltip-owner="hertzbeat-ui-hover-tooltip"');
+    expect(html).toContain('data-topology-legend-owner="hertzbeat-ui-legend"');
+    expect(html).toContain('data-topology-g6-legend-owner="hertzbeat-ui-g6-legend-dock"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-owner="hertzbeat-ui-g6-legend-dock"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-placement="canvas-bottom-left"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-occlusion="inside-canvas-low-interruption"');
+    expect(html).toContain('data-hz-topology-legend-density="canvas-dock"');
+    expect(html).toContain('data-hz-topology-legend-layout="inline-g6-dock"');
+    expect(html).toContain('data-hz-topology-legend-occlusion="low"');
+    expect(html).toContain('data-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"');
+    expect(html).toContain('data-hz-ui="topology-legend"');
+    expect(html).toContain('data-hz-topology-primitive="legend"');
+    expect(html).toContain('data-hz-topology-legend-boundary="framed"');
+    expect(html).toContain('data-hz-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"');
+    expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
+    expect(html).toContain('data-hz-topology-legend-title-owner="hertzbeat-ui-legend-title"');
+    expect(html).toContain('data-hz-topology-legend-summary-owner="hertzbeat-ui-legend-summary"');
+    expect(html).toContain('data-hz-topology-legend-section="status"');
+    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
+    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
+    expect(html).toContain('data-hz-topology-legend-section="interaction"');
+    expect(html).not.toContain('data-hz-topology-legend-section="source-kind"');
+    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
+    expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
+    expect(html).toContain('data-hz-topology-legend-item="warning-node"');
+    expect(html).toContain('data-hz-topology-legend-item="critical-node"');
+    expect(html).toContain('data-hz-topology-legend-item="selected-node"');
+    expect(html).toContain('data-hz-topology-legend-item="directional-edge"');
+    expect(html).toContain('data-hz-topology-legend-item="dimmed-edge"');
+    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
+    expect(html).toContain('data-hz-topology-legend-color="#f59e0b"');
+    expect(html).toContain('data-hz-topology-legend-color="#ef4444"');
+    expect(html).toContain('data-hz-topology-legend-swatch-shape="line"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
+    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
+    expect(html).toContain('data-hz-topology-legend-swatch-owner="hertzbeat-ui-legend-swatch"');
+    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
+    expect(html).toContain('data-hz-topology-legend-item-value-owner="hertzbeat-ui-legend-item-value"');
+    expect(html).not.toContain('data-hz-topology-legend-pattern="dashed"');
+    expect(html).toContain('data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"');
+    expect(html).toContain('data-topology-detail-drawer-surface-owner="hertzbeat-ui-detail-surface"');
+    expect(html).toContain('data-hz-ui="topology-detail-drawer"');
+    expect(html).toContain('data-hz-topology-primitive="detail-drawer"');
+    expect(html).toContain('data-hz-topology-detail-kind="edge"');
+    expect(html).toContain('data-hz-topology-detail-surface="framed"');
+    expect(html).toContain('data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"');
+    expect(html).toContain('data-hz-topology-detail-identity-owner="hertzbeat-ui-detail-identity"');
+    expect(html).toContain('data-hz-topology-detail-subject-id="svc-frontend--svc-checkout"');
+    expect(html).toContain('data-hz-topology-detail-source-id="svc-frontend"');
+    expect(html).toContain('data-hz-topology-detail-target-id="svc-checkout"');
+    expect(html).toContain('data-hz-topology-detail-relation-type="trace-call"');
+    expect(html).toContain('data-hz-topology-detail-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-detail-header-owner="hertzbeat-ui-detail-header"');
+    expect(html).toContain('data-hz-topology-detail-eyebrow-owner="hertzbeat-ui-detail-eyebrow"');
+    expect(html).toContain('data-hz-topology-detail-title-owner="hertzbeat-ui-detail-title"');
+    expect(html).toContain('data-hz-topology-detail-subtitle-owner="hertzbeat-ui-detail-subtitle"');
+    expect(html).toContain('data-hz-topology-detail-boundary="context"');
+    expect(html).toContain('data-hz-topology-detail-boundary-owner="hertzbeat-ui-detail-boundary"');
+    expect(html).toContain('data-topology-detail-boundary-owner="hertzbeat-ui-detail-boundary"');
+    expect(html).toContain('data-hz-topology-detail-boundary-copy-owner="hertzbeat-ui-detail-boundary-copy"');
+    expect(html).toContain('data-hz-topology-detail-fact-group-owner="hertzbeat-ui-detail-fact-group"');
+    expect(html).toContain('data-hz-topology-detail-fact-owner="hertzbeat-ui-detail-fact"');
+    expect(html).toContain('data-hz-topology-detail-fact-label-owner="hertzbeat-ui-detail-fact-label"');
+    expect(html).toContain('data-hz-topology-detail-fact-value-owner="hertzbeat-ui-detail-fact-value"');
+    expect(html).toContain('data-hz-topology-detail-fact-meta-owner="hertzbeat-ui-detail-fact-meta"');
+    expect(html).toContain('data-hz-topology-detail-fact="source-entity"');
+    expect(html).toContain('data-hz-topology-detail-fact="request-rate"');
+    expect(html).toContain('data-hz-topology-detail-action-group-owner="hertzbeat-ui-detail-action-group"');
+    expect(html).toContain('data-hz-topology-detail-action-link-owner="hertzbeat-ui-detail-action-link"');
+    expect(html).toContain('data-hz-topology-detail-action-label-owner="hertzbeat-ui-detail-action-label"');
+    expect(html).toContain('data-hz-topology-detail-action="alert-impact"');
+    expect(html).toContain('data-hz-topology-detail-action-copy-owner="hertzbeat-ui-detail-action-copy"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-group-owner="hertzbeat-ui-detail-signal-action-group"');
+    expect(html).toContain('data-hz-topology-detail-signal-label-owner="hertzbeat-ui-detail-signal-label"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-link-owner="hertzbeat-ui-detail-signal-action-link"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-label-owner="hertzbeat-ui-detail-signal-action-label"');
+    expect(html).toContain('data-hz-topology-detail-signal-action="traces"');
+    expect(html).toContain('data-topology-current-entity-panel-owner="hertzbeat-ui-detail-drawer"');
+    expect(html).toContain('data-topology-current-entity-panel-surface-owner="hertzbeat-ui-detail-surface"');
+    expect(html).toContain('data-topology-current-entity-panel="svc-checkout"');
+    expect(html).toContain('data-hz-topology-detail-kind="node"');
+    expect(html).toContain('data-hz-topology-detail-surface="framed"');
+    expect(html).toContain('data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"');
+    expect(html).toContain('data-hz-topology-detail-subject-id="svc-checkout"');
+    expect(html).toContain('data-hz-topology-detail-entity-type="service"');
+    expect(html).toContain('data-hz-topology-detail-header-owner="hertzbeat-ui-detail-header"');
+    expect(html).toContain('data-hz-topology-detail-eyebrow-owner="hertzbeat-ui-detail-eyebrow"');
+    expect(html).toContain('data-hz-topology-detail-title-owner="hertzbeat-ui-detail-title"');
+    expect(html).toContain('data-hz-topology-detail-subtitle-owner="hertzbeat-ui-detail-subtitle"');
+    expect(html).toContain('data-hz-topology-detail-boundary-owner="hertzbeat-ui-detail-boundary"');
+    expect(html).toContain('data-topology-current-entity-boundary-owner="hertzbeat-ui-detail-boundary"');
+    expect(html).toContain('data-hz-topology-detail-boundary-copy-owner="hertzbeat-ui-detail-boundary-copy"');
+    expect(html).toContain('data-hz-topology-detail-fact-group-owner="hertzbeat-ui-detail-fact-group"');
+    expect(html).toContain('data-hz-topology-detail-fact-owner="hertzbeat-ui-detail-fact"');
+    expect(html).toContain('data-hz-topology-detail-fact-label-owner="hertzbeat-ui-detail-fact-label"');
+    expect(html).toContain('data-hz-topology-detail-fact-value-owner="hertzbeat-ui-detail-fact-value"');
+    expect(html).toContain('data-hz-topology-detail-fact-meta-owner="hertzbeat-ui-detail-fact-meta"');
+    expect(html).toContain('data-hz-topology-detail-fact="health"');
+    expect(html).toContain('data-hz-topology-detail-action-group-owner="hertzbeat-ui-detail-action-group"');
+    expect(html).toContain('data-hz-topology-detail-action-link-owner="hertzbeat-ui-detail-action-link"');
+    expect(html).toContain('data-hz-topology-detail-action-label-owner="hertzbeat-ui-detail-action-label"');
+    expect(html).toContain('data-hz-topology-detail-action="entity"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-group-owner="hertzbeat-ui-detail-signal-action-group"');
+    expect(html).toContain('data-hz-topology-detail-signal-label="signals"');
+    expect(html).toContain('data-hz-topology-detail-signal-label-owner="hertzbeat-ui-detail-signal-label"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-link-owner="hertzbeat-ui-detail-signal-action-link"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-label-owner="hertzbeat-ui-detail-signal-action-label"');
+    expect(html).toContain('data-hz-topology-detail-signal-action="metrics"');
+    expect(html).toContain('运维拓扑');
+    expect(html).not.toContain('HertzBeat 企业运维拓扑');
     expect(html).toContain('应用拓扑');
     expect(html).toContain('服务调用');
     expect(html).toContain('资源依赖');
@@ -87,14 +1060,27 @@ describe('topology page', () => {
     expect(html).toContain('刷新拓扑');
     expect(html).toContain('适配视图');
     expect(html).toContain('data-topology-alert-impact-link="alert-center"');
+    expect(html).toContain('data-topology-alert-impact-link-owner="hertzbeat-ui-action-link"');
+    expect(html).toContain('data-topology-alert-impact-link-spacing-owner="hertzbeat-ui-action-link-spacing"');
+    expect(html).toContain('data-hz-ui="topology-action-link"');
+    expect(html).toContain('data-hz-topology-primitive="action-link"');
+    expect(html).toContain('data-hz-topology-action-link="alert-impact"');
+    expect(html).toContain('data-hz-topology-action-link-emphasis="primary"');
+    expect(html).toContain('data-hz-topology-action-link-spacing="none"');
+    expect(html).toContain('data-hz-topology-action-link-spacing-owner="hertzbeat-ui-action-link-spacing"');
+    expect(html).toContain('data-hz-topology-action-link-label-owner="hertzbeat-ui-action-link-label"');
+    expect(html).toContain('data-hz-topology-action-link-copy-owner="hertzbeat-ui-action-link-copy"');
     expect(html).toContain('查看实体详情');
     expect(html).toContain('打开三信号');
+    expect(html).toContain('data-topology-node-select-href="/topology?');
     expect(html).toContain('data-topology-node-entity-href="/entities/service%3Acommerce%2Fcheckout?');
     expect(html).toContain('data-topology-context-link="entity"');
     expect(html).toContain('data-topology-context-link="metrics"');
     expect(html).toContain('data-topology-context-link="logs"');
     expect(html).toContain('data-topology-context-link="traces"');
-    expect(html).toMatch(/data-topology-node-id="svc-checkout"[\s\S]*data-topology-node-health-copy="lightweight-service-health"[\s\S]*健康评分 62/);
+    expect(html).toContain('data-topology-node-id="svc-checkout"');
+    expect(html).toContain('checkout-api');
+    expect(html).toContain('健康评分 82');
     expect(html).toContain('/ingestion/otlp/metrics?');
     expect(html).toContain('/log/manage?');
     expect(html).toContain('/trace/manage?');
@@ -107,18 +1093,751 @@ describe('topology page', () => {
     expect(html).not.toContain('Select Environment/s');
     expect(html).not.toContain('entry surface');
     expect(html).not.toContain('Monitor center');
+  }, 90000);
+
+  it('keeps the first viewport concise so topology controls and the graph lead the experience', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage apiGraph={buildApiTopologyFixture()} />);
+    const headerStart = html.indexOf('data-topology-workbench-header-owner="hertzbeat-ui-workbench-header"');
+    const headerEnd = html.indexOf('data-topology-focus-trail="focused-context"', headerStart);
+    const headerHtml = html.slice(headerStart, headerEnd);
+
+    expect(headerHtml).toContain('data-hz-topology-workbench-header-density="operational-compact"');
+    expect(headerHtml).toContain('data-hz-topology-workbench-copy-visibility="assistive"');
+    expect(headerHtml).not.toContain('data-hz-topology-workbench-source-slot-owner="hertzbeat-ui-workbench-source-slot"');
+    expect(headerHtml).not.toContain('data-topology-source-strip-density="label-only"');
+    expect(headerHtml).not.toContain('data-topology-source-strip-compactness="single-line-wrap"');
+    expect(headerHtml).not.toContain('data-hz-topology-filter-strip-variant="source-rail"');
+    expect(headerHtml).toContain('运维拓扑');
+    expect(headerHtml).not.toContain('OTLP 调用关系');
+    expect(headerHtml).not.toContain('CMDB / 手工标签');
+    expect(headerHtml).not.toContain('data-hz-topology-workbench-copy-owner="hertzbeat-ui-workbench-copy"');
+    expect(headerHtml).not.toContain('以实体中心为锚点');
+    expect(headerHtml).not.toContain('从 trace span 归并服务调用边');
+    expect(headerHtml).not.toContain('从 HertzBeat 监控对象绑定实体');
+    expect(headerHtml).not.toContain('从 CMDB 导入和手工标签补齐归属');
   });
 
+  it('renders API-backed topology graph data when the topology read API returns nodes and edges', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{ entityId: '501', environment: 'prod', timeRange: 'last-30m' }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 501,
+          depth: 2,
+          sourceKinds: ['otlp-trace-call'],
+          nodes: [
+            {
+              id: '501',
+              entityId: 501,
+              entityName: 'api-checkout',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              focus: true,
+              evidenceBadges: ['entity-relation', 'otlp']
+            },
+            {
+              id: '502',
+              entityId: 502,
+              entityName: 'api-orders',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              focus: false,
+              evidenceBadges: ['entity-relation', 'otlp']
+            }
+          ],
+          edges: [
+            {
+              id: '700',
+              relationId: 700,
+              sourceEntityId: 501,
+              targetEntityId: 502,
+              relationType: 'trace-call',
+              relationSource: 'otlp-trace-call',
+              sampleTraceId: 'trace-701',
+              sampleSpanId: 'span-701',
+              firstSeen: '2026-05-20T03:01:00Z',
+              lastSeen: '2026-05-20T03:08:00Z',
+              status: 'active',
+              score: 96,
+              evidenceBadges: ['entity-relation', 'otlp-trace-call']
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-data-source="api"');
+    expect(html).toContain('data-topology-node-id="entity-501"');
+    expect(html).toContain('data-topology-node-id="entity-502"');
+    expect(html).toContain('api-checkout');
+    expect(html).toContain('api-orders');
+    expect(html).toContain('data-topology-edge-id="relation-700"');
+    expect(html).not.toContain('data-topology-node-id="svc-checkout"');
+    expect(html).not.toContain('checkout-api');
+  }, 30000);
+
+  it('maps focused 1-hop routes into shared FocusTrail handoff metadata and scoped exit hrefs', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          entityId: 'service:commerce/checkout',
+          entityName: 'checkout-api',
+          serviceName: 'checkout-api',
+          serviceNamespace: 'commerce',
+          environment: 'prod',
+          timeRange: 'last-30m',
+          sourceKind: 'cmdb-manual-label',
+          groupBy: 'source-kind',
+          viewMode: 'application',
+          edgeId: 'svc-frontend--svc-checkout',
+          depth: '1'
+        }}
+        apiGraph={buildApiTopologyFixture()}
+      />
+    );
+
+    expect(html).toContain('data-hz-topology-focus-trail-mode="focused"');
+    expect(html).toContain('data-hz-topology-focus-trail-depth="1"');
+    expect(html).toContain('data-hz-topology-focus-trail-entity="svc-checkout"');
+    expect(html).toContain('data-hz-topology-focus-crumb-active="true"');
+    expect(html).toContain('data-hz-topology-focus-filter="source"');
+    expect(html).toContain('data-topology-focus-filter-source="cmdb-manual-label"');
+    expect(html).toMatch(
+      /<a(?=[^>]*data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit")(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*environment=prod)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*timeRange=last-30m)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*sourceKind=cmdb-manual-label)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*groupBy=source-kind)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*depth=2)(?=[^>]*data-hz-topol [...]
+    );
+    expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*entityId=/);
+    expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*serviceName=/);
+  }, 30000);
+
+  it('forwards groupBy route scope into the shared G6 grouping metadata and controls', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          entityId: '501',
+          environment: 'prod',
+          timeRange: 'last-30m',
+          sourceKind: 'otlp-trace-call',
+          groupBy: 'source-kind'
+        }}
+        apiGraph={buildApiTopologyFixture()}
+      />
+    );
+
+    expect(html).toContain('data-hz-topology-g6-filter-group-by="source-kind"');
+    expect(html).toMatch(/data-hz-topology-g6-filter-group-count="[1-9]\d*"/);
+    expect(html).toContain('data-hz-topology-g6-group-owner="hertzbeat-ui-g6-group"');
+    expect(html).toContain('data-hz-topology-g6-group-by="source-kind"');
+    expect(html).toContain('data-hz-topology-g6-group-active="true"');
+    expect(html).toMatch(/data-hz-topology-g6-group-item-count="[1-9]\d*"/);
+    expect(html).toContain('data-hz-topology-g6-group-surface-owner="hertzbeat-ui-g6-group-surface"');
+    expect(html).toContain('data-hz-topology-g6-group-surface-visibility="assistive"');
+    expect(html).not.toContain('absolute left-3 top-12 flex');
+    expect(html).toContain('data-hz-topology-g6-group-item="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-g6-group-item-action-owner="hertzbeat-ui-g6-group-item-action"');
+    expect(html).toMatch(
+      /<a(?=[^>]*data-hz-topology-g6-group-item="otlp-trace-call")(?=[^>]*href="\/topology\?[^"]*sourceKind=otlp-trace-call)(?=[^>]*href="\/topology\?[^"]*groupBy=source-kind)[^>]*>/
+    );
+    expect(html).toContain('data-hz-topology-g6-filter-control="group-by"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-kind="group-by"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-value="source-kind"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-active-state="true"');
+    expect(html).toMatch(
+      /<a(?=[^>]*data-hz-topology-g6-filter-control="reset")(?=[^>]*href="\/topology\?[^"]*sourceKind=otlp-trace-call)[^>]*>/
+    );
+    expect(html).not.toMatch(
+      /<a(?=[^>]*data-hz-topology-g6-filter-control="reset")(?=[^>]*href="\/topology\?[^"]*groupBy=source-kind)[^>]*>/
+    );
+    expect(html).toContain('data-hz-topology-control="source-kind"');
+    expect(html).toContain('data-hz-topology-control-source-kind-owner="hertzbeat-ui-toolbar-source-kind-control"');
+    expect(html).toContain('data-hz-topology-control-source-kind-value="otlp-trace-call"');
+    expect(html).not.toContain('data-topology-source-control="relationship-source"');
+    expect(html).toMatch(
+      /<a(?=[^>]*data-topology-view-mode="resource-dependency")(?=[^>]*href="\/topology\?[^"]*viewMode=resource-dependency)(?=[^>]*href="\/topology\?[^"]*groupBy=source-kind)[^>]*>/
+    );
+    expect(html).toMatch(
+      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-hz-topology-g6-metadata-role="selection-button")(?=[^>]*data-topology-node-select-href="\/topology\?[^"]*groupBy=source-kind)[^>]*>/
+    );
+    expect(html).toContain('data-hz-topology-g6-focus-entry="double-click-node"');
+    expect(html).toContain('data-hz-topology-g6-focus-depth-target="1-hop"');
+    expect(html).toMatch(
+      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-topology-node-select-href="\/topology\?[^"]*depth=2)(?=[^>]*data-topology-node-focus-href="\/topology\?[^"]*depth=1)[^>]*>/
+    );
+    expect(html).toMatch(
+      /data-topology-current-entity-panel="svc-checkout"[^>]*data-topology-current-entity-group-by="source-kind"[^>]*data-topology-current-entity-source-kind="otlp-trace-call"[^>]*data-topology-current-entity-view-mode="application"/
+    );
+  }, 30000);
+
+  it('exposes focused API graph depth, relationship type, health, and evidence badges for browser verification', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{ entityId: '501', environment: 'prod', timeRange: 'last-30m' }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 501,
+          depth: 2,
+          sourceKinds: ['otlp-trace-call', 'database-middleware-connection'],
+          nodes: [
+            {
+              id: '501',
+              entityId: 501,
+              entityName: 'api-checkout',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              focus: true,
+              evidenceBadges: ['entity-relation', 'otlp-trace-call', 'health:warning']
+            },
+            {
+              id: '502',
+              entityId: 502,
+              entityName: 'api-orders',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation', 'otlp-trace-call', 'one-hop']
+            },
+            {
+              id: '503',
+              entityId: 503,
+              entityName: 'api-postgres',
+              entityType: 'database',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'critical',
+              evidenceBadges: ['entity-relation', 'database-middleware-connection', 'two-hop']
+            }
+          ],
+          edges: [
+            {
+              id: '701',
+              relationId: 701,
+              sourceEntityId: 501,
+              targetEntityId: 502,
+              relationType: 'trace-call',
+              relationSource: 'otlp-trace-call',
+              sampleTraceId: 'trace-701',
+              sampleSpanId: 'span-701',
+              firstSeen: '2026-05-20T03:01:00Z',
+              lastSeen: '2026-05-20T03:08:00Z',
+              status: 'active',
+              score: 96,
+              evidenceBadges: ['entity-relation', 'otlp-trace-call', 'one-hop']
+            },
+            {
+              id: '702',
+              relationId: 702,
+              sourceEntityId: 502,
+              targetEntityId: 503,
+              relationType: 'database connection',
+              relationSource: 'database-middleware-connection',
+              status: 'critical',
+              score: 42,
+              evidenceBadges: ['entity-relation', 'database-middleware-connection', 'two-hop']
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-api-depth="2"');
+    expect(html).toMatch(/data-topology-node-id="entity-501"[^>]+data-topology-node-health="warning"[^>]+data-topology-node-evidence-badges="entity-relation otlp-trace-call health:warning"/);
+    expect(html).toMatch(/data-topology-node-id="entity-503"[^>]+data-topology-node-health="critical"[^>]+data-topology-node-evidence-badges="entity-relation database-middleware-connection two-hop"/);
+    expect(html).toMatch(/data-topology-edge-id="relation-701"[^>]+data-topology-edge-relationship-type="trace-call"[^>]+data-topology-edge-evidence-badges="entity-relation otlp-trace-call one-hop"/);
+    expect(html).toMatch(/data-topology-edge-id="relation-702"[^>]+data-topology-edge-relationship-type="database-connection"[^>]+data-topology-edge-evidence-badges="entity-relation database-middleware-connection two-hop"/);
+  }, 30000);
+
+  it('exposes trace-derived service graph RED metrics on nodes, edges, and selected evidence', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          entityId: '501',
+          edgeId: 'relation-701',
+          environment: 'prod',
+          timeRange: 'last-30m',
+          viewMode: 'service-call'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 501,
+          depth: 2,
+          sourceKinds: ['otlp-trace-call'],
+          nodes: [
+            {
+              id: '501',
+              entityId: 501,
+              entityName: 'api-checkout',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              focus: true,
+              evidenceBadges: ['entity-relation', 'otlp-trace-call'],
+              redMetrics: {
+                requestRatePerSecond: 12.34,
+                errorRate: 0.042,
+                latencyP95Ms: 180
+              }
+            },
+            {
+              id: '502',
+              entityId: 502,
+              entityName: 'api-orders',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation', 'otlp-trace-call'],
+              redMetrics: {
+                requestRatePerSecond: 9.5,
+                errorRate: 0.01,
+                latencyP95Ms: 95
+              }
+            }
+          ],
+          edges: [
+            {
+              id: '701',
+              relationId: 701,
+              sourceEntityId: 501,
+              targetEntityId: 502,
+              relationType: 'trace-call',
+              relationSource: 'otlp-trace-call',
+              sampleTraceId: 'trace-701',
+              sampleSpanId: 'span-701',
+              firstSeen: '2026-05-20T03:01:00Z',
+              lastSeen: '2026-05-20T03:08:00Z',
+              status: 'active',
+              score: 96,
+              evidenceBadges: ['entity-relation', 'otlp-trace-call', 'service-graph'],
+              redMetrics: {
+                requestRatePerSecond: 7.25,
+                errorRate: 0.021,
+                latencyP95Ms: 123
+              }
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-node-id="entity-501"');
+    expect(html).toContain('data-topology-node-request-rate="12.34"');
+    expect(html).toContain('data-topology-node-error-rate="0.042"');
+    expect(html).toContain('data-topology-node-latency-p95-ms="180"');
+    expect(html).toContain('data-topology-edge-id="relation-701"');
+    expect(html).toContain('data-topology-trace-call-proof-owner="hertzbeat-ui-workbench-frame"');
+    expect(html).toContain('data-topology-trace-call-edge-count="1"');
+    expect(html).toContain('data-topology-trace-call-red-edge-count="1"');
+    expect(html).toContain('data-topology-trace-call-window-edge-count="1"');
+    expect(html).toContain('data-topology-trace-call-sample-edge-count="1"');
+    expect(html).toContain('data-topology-trace-call-red-state="ready"');
+    expect(html).toContain('data-topology-edge-request-rate="7.25"');
+    expect(html).toContain('data-topology-edge-error-rate="0.021"');
+    expect(html).toContain('data-topology-edge-latency-p95-ms="123"');
+    expect(html).toContain('data-topology-current-entity-red-metric="request-rate"');
+    expect(html).toContain('data-topology-current-entity-panel-owner="hertzbeat-ui-detail-drawer"');
+    expect(html).toContain('data-hz-topology-detail-fact="request-rate"');
+    expect(html).toContain('data-hz-topology-detail-action="entity"');
+    expect(html).toContain('12.34/s');
+    expect(html).toContain('4.2%');
+    expect(html).toContain('180 ms');
+    expect(html).toContain('data-topology-edge-evidence-panel="relation-701"');
+    expect(html).toContain('data-topology-edge-evidence-window-owner="hertzbeat-ui-detail-drawer"');
+    expect(html).toContain('data-topology-edge-evidence-first-seen="2026-05-20T03:01:00Z"');
+    expect(html).toContain('data-topology-edge-evidence-last-seen="2026-05-20T03:08:00Z"');
+    expect(html).toContain('data-topology-edge-evidence-sample-trace-id="trace-701"');
+    expect(html).toContain('data-topology-edge-evidence-sample-span-id="span-701"');
+    expect(html).toContain('data-hz-topology-detail-fact="sample-trace"');
+    expect(html).toContain('trace-701');
+    expect(html).toContain('span-701');
+    expect(html).toContain('data-hz-topology-detail-fact="first-seen"');
+    expect(html).toContain('2026-05-20T03:01:00Z');
+    expect(html).toContain('2026-05-20T03:08:00Z');
+    expect(html).toContain('data-topology-edge-red-metric="request-rate"');
+    expect(html).toContain('7.25/s');
+    expect(html).toContain('2.1%');
+    expect(html).toContain('123 ms');
+    expect(html).toContain('data-topology-metric-table-owner="hertzbeat-ui"');
+    expect(html).toContain('data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"');
+    expect(html).toContain('data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"');
+    expect(html).toContain('data-hz-ui="topology-metric-table"');
+    expect(html).toContain('data-hz-topology-metric-table-boundary="framed"');
+    expect(html).toContain('data-hz-topology-metric-table-density="graph-first"');
+    expect(html).toContain('data-hz-topology-metric-table-density-owner="hertzbeat-ui-metric-table-density"');
+    expect(html).toContain('data-hz-topology-metric-table-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-metric-table-row-density="compressed-red"');
+    expect(html).toContain('data-hz-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"');
+    expect(html).toContain('data-hz-topology-metric-table-header-owner="hertzbeat-ui-metric-table-header"');
+    expect(html).toContain('data-hz-topology-metric-rows="1"');
+    expect(html).toContain('data-hz-topology-metric-table-interaction="row-select-detail"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-mode="direct"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="inactive"');
+    expect(html).toContain('data-hz-topology-metric-table-priority-node-ids="entity-501 entity-502"');
+    expect(html).toContain('data-hz-topology-metric-table-visible-row-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-partial-row-count="0"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-count="0"');
+    expect(html).toContain('data-hz-topology-metric-table-unknown-row-count="0"');
+    expect(html).toContain('data-topology-metric-table-filter-behavior="in-page-no-route-change"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-filter="all"');
+    expect(html).toContain('data-hz-topology-metric-table-filtered-row-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-owner="hertzbeat-ui-metric-table-filter-control"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
+    expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="visible"');
+    expect(html).toContain('data-hz-topology-edge-row-source-node-id="entity-501"');
+    expect(html).toContain('data-hz-topology-edge-row-target-node-id="entity-502"');
+    expect(html).toContain('data-hz-topology-edge-row-source-visible="true"');
+    expect(html).toContain('data-hz-topology-edge-row-target-visible="true"');
+    expect(html).toContain('data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"');
+    expect(html).toContain('data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"');
+    expect(html).toContain('data-hz-topology-edge-row="relation-701"');
+    expect(html).toContain('data-hz-topology-edge-selected="true"');
+    expect(html).toContain('data-hz-topology-metric-table-route-owner="hertzbeat-ui-metric-table-route"');
+    expect(html).toContain('data-hz-topology-metric-table-badge-owner="hertzbeat-ui-metric-table-badge"');
+    expect(html).toContain('data-hz-topology-metric-table-cell-owner="hertzbeat-ui-metric-table-cell"');
+    expect(html).toContain('data-hz-topology-metric-table-value-owner="hertzbeat-ui-metric-table-value"');
+    expect(html).toContain('data-hz-topology-metric-table-label-owner="hertzbeat-ui-metric-table-label"');
+    expect(html).toContain('data-hz-topology-edge-action="relation-701"');
+    expect(html).toContain('data-hz-topology-metric-table-action-owner="hertzbeat-ui-metric-table-action"');
+    expect(html).toContain('data-hz-topology-request-rate="7.25"');
+    expect(html).toContain('data-hz-topology-error-rate="0.021"');
+    expect(html).toContain('data-hz-topology-latency-p95-ms="123"');
+    expect(html).toContain('边指标排行');
+  }, 60000);
+
+  it('renders API-backed impact timeline evidence when topology returns change events', async () => {
+    i18nState.locale = 'en-US';
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{ entityId: '501', environment: 'prod', timeRange: 'last-1h' }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 501,
+          depth: 1,
+          sourceKinds: ['entity-relation'],
+          nodes: [
+            {
+              id: '501',
+              entityId: 501,
+              entityName: 'api-checkout',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              focus: true,
+              evidenceBadges: ['entity-relation']
+            },
+            {
+              id: '502',
+              entityId: 502,
+              entityName: 'api-orders',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation']
+            }
+          ],
+          edges: [
+            {
+              id: '101',
+              relationId: 101,
+              sourceEntityId: 501,
+              targetEntityId: 502,
+              relationType: 'depends_on',
+              relationSource: 'manual',
+              status: 'confirmed',
+              score: 92,
+              evidenceBadges: ['entity-relation', 'manual']
+            }
+          ],
+          impactTimeline: [
+            {
+              id: 'activity:901',
+              entityId: 501,
+              sourceKind: 'cmdb-manual-label',
+              eventType: 'entity-definition',
+              title: 'Definition updated',
+              detail: 'owner changed',
+              actor: 'alice',
+              occurredAt: '2026-05-19T11:12:00'
+            },
+            {
+              id: 'relation:101',
+              edgeId: '101',
+              sourceKind: 'cmdb-manual-label',
+              eventType: 'relation-updated',
+              title: 'depends_on updated',
+              detail: 'manual',
+              actor: 'system',
+              occurredAt: '2026-05-19T11:10:00'
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-impact-timeline="api-evidence"');
+    expect(html).toContain('data-topology-impact-timeline-owner="hertzbeat-ui-evidence-list"');
+    expect(html).toContain('data-topology-impact-timeline-boundary-owner="hertzbeat-ui-evidence-list-boundary"');
+    expect(html).toContain('data-hz-topology-evidence-list-kind="impact-timeline"');
+    expect(html).toContain('data-hz-topology-evidence-list-boundary="companion-timeline"');
+    expect(html).toContain('data-hz-topology-evidence-list-boundary-owner="hertzbeat-ui-evidence-list-boundary"');
+    expect(html).toContain('data-hz-topology-evidence-header-owner="hertzbeat-ui-evidence-list-header"');
+    expect(html).toContain('data-hz-topology-evidence-title-owner="hertzbeat-ui-evidence-list-title"');
+    expect(html).toContain('data-hz-topology-evidence-copy-owner="hertzbeat-ui-evidence-list-copy"');
+    expect(html).toContain('data-hz-topology-evidence-count-owner="hertzbeat-ui-evidence-list-count"');
+    expect(html).toContain('data-topology-impact-timeline-event="activity:901"');
+    expect(html).toContain('data-hz-topology-evidence-item-owner="hertzbeat-ui-evidence-list-item"');
+    expect(html).toContain('data-hz-topology-evidence-item-label-owner="hertzbeat-ui-evidence-list-item-label"');
+    expect(html).toContain('data-hz-topology-evidence-item-value-owner="hertzbeat-ui-evidence-list-item-value"');
+    expect(html).toContain('data-hz-topology-evidence-item-meta-owner="hertzbeat-ui-evidence-list-item-meta"');
+    expect(html).toContain('data-topology-impact-timeline-event="relation:101"');
+    expect(html).toContain('Impact timeline');
+    expect(html).toContain('Definition updated');
+    expect(html).toContain('owner changed');
+    expect(html).toContain('alice');
+    expect(html).toContain('depends_on updated');
+    expect(html).not.toContain('topology.timeline.');
+  }, 30000);
+
+  it('renders API-backed empty topology without the old static seed graph', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          entityId: 'service:commerce/checkout',
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'otlp-trace-call',
+          relationType: 'trace-call'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: null,
+          depth: 2,
+          sourceKinds: ['entity-relation', 'otlp-trace-call'],
+          nodes: [],
+          edges: []
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-data-source="api"');
+    expect(html).toContain('data-topology-empty-state="trace-call-missing"');
+    expect(html).toContain('data-topology-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(html).toContain('data-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
+    expect(html).toContain('data-hz-ui="topology-empty-state"');
+    expect(html).toContain('data-hz-topology-primitive="empty-state"');
+    expect(html).toContain('data-hz-topology-empty-boundary="canvas"');
+    expect(html).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
+    expect(html).toContain('data-hz-topology-empty-kind="filtered-empty"');
+    expect(html).toContain('data-hz-topology-empty-source="Greptime trace graph"');
+    expect(html).toContain('data-hz-topology-empty-time-scope="last-1h"');
+    expect(html).toContain('data-hz-topology-empty-scope-owner="hertzbeat-ui-empty-scope"');
+    expect(html).toContain('data-hz-topology-empty-environment="prod"');
+    expect(html).toContain('data-hz-topology-empty-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-empty-relation-type="trace-call"');
+    expect(html).toContain('data-hz-topology-empty-focus-entity-id="service:commerce/checkout"');
+    expect(html).toContain('data-hz-topology-empty-depth="2"');
+    expect(html).toContain('data-hz-topology-empty-result-count="0"');
+    expect(html).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace"');
+    expect(html).toContain('暂无 trace 调用拓扑证据');
+    expect(html).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
+    expect(html).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
+    expect(html).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
+    expect(html).toContain('data-hz-topology-empty-source-owner="hertzbeat-ui-empty-source"');
+    expect(html).toContain('data-hz-topology-empty-time-scope-owner="hertzbeat-ui-empty-time-scope"');
+    expect(html).not.toContain('data-topology-node-id="svc-checkout"');
+    expect(html).not.toContain('checkout-api');
+    expect(html).not.toContain('redis');
+  }, 30000);
+
+  it('shows explicit missing trace-call evidence instead of a node-only trace graph', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'otlp-trace-call',
+          relationType: 'trace-call'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: null,
+          depth: 2,
+          sourceKinds: ['otlp-trace-call'],
+          nodes: [
+            {
+              id: '501',
+              entityId: 501,
+              entityName: 'Payment API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['entity-relation', 'manual']
+            },
+            {
+              id: '502',
+              entityId: 502,
+              entityName: 'Orders DB',
+              entityType: 'database',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation', 'manual']
+            }
+          ],
+          edges: []
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-data-source="api"');
+    expect(html).toContain('data-topology-trace-call-state="missing-edges"');
+    expect(html).toContain('data-topology-empty-state="trace-call-missing"');
+    expect(html).toContain('data-hz-topology-empty-kind="filtered-empty"');
+    expect(html).toContain('data-hz-topology-empty-source="Greptime trace graph"');
+    expect(html).toContain('data-hz-topology-empty-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-empty-relation-type="trace-call"');
+    expect(html).toContain('data-hz-topology-empty-result-count="0"');
+    expect(html).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace"');
+    expect(html).toContain('暂无 trace 调用拓扑证据');
+    expect(html).toContain('data-hz-topology-g6-node-count="0"');
+    expect(html).toContain('data-hz-topology-g6-edge-count="0"');
+  }, 15000);
+
+  it('exposes the resolved topology API scope for Browser release-gate checks', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          groupBy: 'source-kind'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: null,
+          depth: 2,
+          sourceKinds: ['otlp-trace-call'],
+          nodes: [],
+          edges: []
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-api-scope-owner="hertzbeat-ui-workbench-frame"');
+    expect(html).toContain('data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;sourceKind=otlp-trace-call&amp;relationType=trace-call"');
+    expect(html).toContain('data-topology-api-scope-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-topology-api-scope-relation-type="trace-call"');
+    expect(html).toContain('data-topology-api-scope-view-mode="service-call"');
+    expect(html).toContain('data-topology-api-timeout-ms="60000"');
+    expect(html).toContain('data-hz-topology-empty-relation-type="trace-call"');
+  }, 15000);
+
+  it('keeps the default topology route API-owned while the client API is pending or unavailable', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const pendingHtml = renderToStaticMarkup(<TopologyPage routeContext={{ environment: 'prod', timeRange: 'last-1h' }} />);
+    const pendingTraceCallHtml = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'otlp-trace-call',
+          relationType: 'trace-call'
+        }}
+      />
+    );
+    const unavailableHtml = renderToStaticMarkup(
+      <TopologyPage routeContext={{ environment: 'prod', timeRange: 'last-1h' }} apiGraph={null} />
+    );
+    const traceCallUnavailableHtml = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'otlp-trace-call',
+          relationType: 'trace-call'
+        }}
+        apiGraph={null}
+      />
+    );
+
+    expect(pendingHtml).toContain('data-topology-data-source="api"');
+    expect(pendingHtml).toContain('data-topology-loading-state="api-pending"');
+    expect(pendingHtml).toContain('data-topology-loading-state-owner="hertzbeat-ui-loading-state"');
+    expect(pendingHtml).toContain('data-hz-ui="topology-loading-state"');
+    expect(pendingHtml).toContain('data-hz-topology-primitive="loading-state"');
+    expect(pendingHtml).toContain('data-hz-topology-loading-boundary="canvas"');
+    expect(pendingHtml).toContain('data-hz-topology-loading-source="API"');
+    expect(pendingHtml).toContain('data-hz-topology-loading-time-scope="last-1h"');
+    expect(pendingHtml).toContain('data-hz-topology-loading-evidence-sources="api greptime trace relation"');
+    expect(pendingHtml).toContain('正在加载拓扑证据');
+    expect(pendingHtml).not.toContain('data-topology-empty-state="api-empty"');
+    expect(pendingTraceCallHtml).toContain('data-topology-trace-call-state="pending"');
+    expect(pendingTraceCallHtml).toContain('data-topology-trace-call-red-state="pending"');
+
+    [pendingHtml, unavailableHtml].forEach(html => {
+      expect(html).toContain('data-topology-data-source="api"');
+      expect(html).not.toContain('data-topology-node-id="svc-checkout"');
+      expect(html).not.toContain('checkout-api');
+      expect(html).not.toContain('orders-db');
+      expect(html).not.toContain('redis');
+    });
+    expect(unavailableHtml).toContain('data-topology-empty-state="degraded"');
+    expect(unavailableHtml).toContain('data-topology-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(unavailableHtml).toContain('data-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
+    expect(unavailableHtml).toContain('data-hz-ui="topology-empty-state"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-boundary="canvas"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-kind="degraded"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-source="拓扑 API 不可用"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-evidence-sources="api unavailable"');
+    expect(unavailableHtml).toContain('拓扑 API 不可用');
+    expect(traceCallUnavailableHtml).toContain('data-topology-trace-call-state="degraded"');
+    expect(traceCallUnavailableHtml).toContain('data-topology-empty-state="trace-call-degraded"');
+    expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-source="Greptime trace graph"');
+    expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-source-kind="otlp-trace-call"');
+    expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-relation-type="trace-call"');
+    expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace unavailable"');
+  }, 30000);
+
   it('uses incoming entity context to seed filters, active topology node, and current signal links', async () => {
-    mockSearchParams = new URLSearchParams({
+    const routeContext = {
       entityId: 'service:commerce/checkout',
       serviceName: 'checkout-api',
       environment: 'prod',
       timeRange: 'last-30m'
-    });
+    };
 
-    const { default: TopologyPage } = await import('./page');
-    const html = renderToStaticMarkup(<TopologyPage />);
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildApiTopologyFixture()} />);
 
     expect(html).toContain('data-topology-incoming-context="entity-filter"');
     expect(html).toContain('data-topology-active-node-id="svc-checkout"');
@@ -131,72 +1850,230 @@ describe('topology page', () => {
     expect(html).toContain('last-30m');
     expect(html).toContain('timeRange=last-30m');
     expect(html).toContain('当前筛选');
-  });
+  }, 15000);
 
   it('uses view mode and source query params to narrow topology and expose alert-impact closure', async () => {
-    mockSearchParams = new URLSearchParams({
+    const routeContext = {
       viewMode: 'alert-impact',
       sourceKind: 'alert-impact',
       environment: 'prod',
       timeRange: 'last-1h'
-    });
+    };
 
-    const { default: TopologyPage } = await import('./page');
-    const html = renderToStaticMarkup(<TopologyPage />);
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildApiTopologyFixture()} />);
 
     expect(html).toContain('data-topology-active-view-mode="alert-impact"');
     expect(html).toContain('data-topology-active-source-kind="alert-impact"');
     expect(html).toContain('data-topology-view-mode-active="alert-impact"');
-    expect(html).toContain('data-topology-source-active="alert-impact"');
+    expect(html).toContain('data-hz-topology-control-source-kind-value="alert-impact"');
+    expect(html).not.toContain('data-topology-source-active="alert-impact"');
     expect(html).toContain('data-topology-edge-focus="active-path"');
     expect(html).toContain('data-topology-edge-focus="context-muted"');
-    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-focus="related"/);
+    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-focus="active"/);
     expect(html).toContain('data-topology-alert-impact-link="alert-center"');
+    expect(html).toContain('data-topology-alert-impact-link-owner="hertzbeat-ui-action-link"');
+    expect(html).toContain('data-hz-topology-action-link-label-owner="hertzbeat-ui-action-link-label"');
+    expect(html).toContain('data-hz-topology-action-link-copy-owner="hertzbeat-ui-action-link-copy"');
     expect(html).toContain('/alert/center?');
     expect(html).toContain('source=topology');
     expect(html).toContain('viewMode=alert-impact');
     expect(html).toContain('sourceKind=alert-impact');
+    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-select-href="\/topology\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
     expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-entity-href="\/entities\/service%3Acommerce%2Fcheckout\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
     expect(html).toMatch(/data-topology-context-link="entity"[^>]+href="\/entities\/service%3Acommerce%2Fcheckout\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
     expect(html).toMatch(/data-topology-context-link="metrics"[^>]+href="\/ingestion\/otlp\/metrics\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
     expect(html).toMatch(/data-topology-context-link="logs"[^>]+href="\/log\/manage\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
     expect(html).toMatch(/data-topology-context-link="traces"[^>]+href="\/trace\/manage\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-  });
+  }, 15000);
 
   it('renders selected edge evidence and drilldown links with retained topology context', async () => {
-    mockSearchParams = new URLSearchParams({
+    const routeContext = {
       viewMode: 'resource-dependency',
       sourceKind: 'database-middleware-connection',
       edgeId: 'svc-checkout--res-orders-db',
       environment: 'prod',
       timeRange: 'last-1h'
-    });
+    };
 
-    const { default: TopologyPage } = await import('./page');
-    const html = renderToStaticMarkup(<TopologyPage />);
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildApiTopologyFixture()} />);
 
     expect(html).toContain('data-topology-active-edge-id="svc-checkout--res-orders-db"');
     expect(html).toContain('data-topology-edge-selected="true"');
     expect(html).toMatch(/data-topology-edge-id="svc-checkout--res-orders-db"[^>]+data-topology-edge-focus="active-path"/);
     expect(html).toContain('data-topology-edge-evidence-panel="svc-checkout--res-orders-db"');
     expect(html).toContain('关系证据');
-    expect(html).toContain('订单库连接');
+    expect(html).toContain('data-hz-topology-detail-density="graph-first"');
+    expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
+    expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-fact-density="compressed"');
+    expect(html).toContain('database connection');
     expect(html).toContain('数据库 / 中间件连接');
     expect(html).toContain('采集证据');
     expect(html).toContain('checkout-api');
     expect(html).toContain('orders-db');
     expect(html).toContain('data-topology-edge-evidence-boundary="roadmap-boundary"');
-    expect(html).toContain('当前边仅基于已采集的关系证据;依赖自动发现、变更时间线和根因分析仍是 roadmap 能力。');
+    expect(html).toContain('当前边仅基于已采集的关系证据;依赖自动发现和根因分析仍是 roadmap 能力。');
     expect(html).toContain('data-topology-edge-link-copy="alert-impact"');
     expect(html).toContain('带当前边、实体和三信号上下文进入告警影响面。');
     expect(html).toMatch(/data-topology-edge-link="metrics"[^>]+href="\/ingestion\/otlp\/metrics\?[^"]*viewMode=resource-dependency[^"]*sourceKind=database-middleware-connection[^"]*edgeId=svc-checkout--res-orders-db/);
     expect(html).toMatch(/data-topology-edge-link="logs"[^>]+href="\/log\/manage\?[^"]*viewMode=resource-dependency[^"]*sourceKind=database-middleware-connection[^"]*edgeId=svc-checkout--res-orders-db/);
     expect(html).toMatch(/data-topology-edge-link="traces"[^>]+href="\/trace\/manage\?[^"]*viewMode=resource-dependency[^"]*sourceKind=database-middleware-connection[^"]*edgeId=svc-checkout--res-orders-db/);
     expect(html).toMatch(/data-topology-edge-link="alert-impact"[^>]+href="\/alert\/center\?[^"]*viewMode=resource-dependency[^"]*sourceKind=database-middleware-connection[^"]*edgeId=svc-checkout--res-orders-db/);
-  });
+  }, 15000);
+
+  it('renders current node drawer upstream and downstream dependency evidence', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          entityId: 'service:commerce/checkout',
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'otlp-trace-call',
+          groupBy: 'source-kind'
+        }}
+        apiGraph={buildApiTopologyFixture()}
+      />
+    );
+
+    expect(html).toContain('data-topology-current-entity-panel="svc-checkout"');
+    expect(html).toContain('data-topology-current-entity-neighbor-owner="hertzbeat-ui-detail-neighbor-evidence"');
+    expect(html).toContain('data-topology-current-entity-upstream-count="4"');
+    expect(html).toContain('data-topology-current-entity-downstream-count="2"');
+    expect(html).toContain('data-topology-current-entity-upstream-node-ids="app-commerce svc-frontend monitor-checkout k8s-checkout-workload"');
+    expect(html).toContain('data-topology-current-entity-downstream-node-ids="res-orders-db res-redis"');
+    expect(html).toContain('data-hz-topology-detail-fact="upstream-dependencies"');
+    expect(html).toContain('data-hz-topology-detail-fact="downstream-dependencies"');
+    expect(html).toContain('data-topology-current-entity-neighbor-evidence="upstream"');
+    expect(html).toContain('data-topology-current-entity-neighbor-evidence="downstream"');
+    expect(html).toContain('commerce, frontend, HTTP template, checkout deploy');
+    expect(html).toContain('orders-db, redis');
+  }, 15000);
+
+  it('keeps current node signal handoffs scoped to the selected G6 node context', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          entityId: 'service:commerce/checkout',
+          environment: 'prod',
+          timeRange: 'last-1h',
+          viewMode: 'service-call',
+          sourceKind: 'otlp-trace-call',
+          groupBy: 'source-kind',
+          depth: '2'
+        }}
+        apiGraph={buildApiTopologyFixture()}
+      />
+    );
+
+    expect(html).toContain('data-topology-current-entity-panel="svc-checkout"');
+    expect(html).toMatch(/data-topology-context-link="entity"[^>]+data-topology-current-entity-handoff-owner="hertzbeat-ui-current-node-handoff"/);
+    expect(html).toMatch(/data-topology-context-link="metrics"[^>]+data-topology-current-entity-handoff-owner="hertzbeat-ui-current-node-handoff"/);
+    expect(html).toMatch(/data-topology-context-link="logs"[^>]+data-topology-current-entity-handoff-owner="hertzbeat-ui-current-node-handoff"/);
+    expect(html).toMatch(/data-topology-context-link="traces"[^>]+data-topology-current-entity-handoff-owner="hertzbeat-ui-current-node-handoff"/);
+    expect(html).toMatch(/data-topology-context-link="metrics"[^>]+data-topology-current-entity-handoff-target="metrics"[^>]+data-topology-current-entity-handoff-node-id="svc-checkout"[^>]+data-topology-current-entity-handoff-entity-id="service:commerce\/checkout"/);
+    expect(html).toMatch(/data-topology-context-link="traces"[^>]+data-topology-current-entity-handoff-source-kind="otlp-trace-call"[^>]+data-topology-current-entity-handoff-group-by="source-kind"[^>]+data-topology-current-entity-handoff-view-mode="service-call"/);
+    expect(html).toMatch(/data-topology-context-link="logs"[^>]+data-topology-current-entity-handoff-environment="prod"[^>]+data-topology-current-entity-handoff-time-range="last-1h"[^>]+data-topology-current-entity-handoff-depth="2"/);
+    expect(html).toContain('href="/ingestion/otlp/metrics?');
+    expect(html).toContain('href="/log/manage?');
+    expect(html).toContain('href="/trace/manage?');
+  }, 15000);
+
+  it('keeps trace-call G6 edge drilldowns scoped to the default RED edge before an edgeId is in the URL', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-30m',
+          viewMode: 'service-call',
+          sourceKind: 'otlp-trace-call'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 501,
+          depth: 2,
+          sourceKinds: ['otlp-trace-call'],
+          nodes: [
+            {
+              id: '501',
+              entityId: 501,
+              entityName: 'payment-api',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              focus: true,
+              evidenceBadges: ['entity-relation', 'otlp-trace-call']
+            },
+            {
+              id: '502',
+              entityId: 502,
+              entityName: 'orders-db',
+              entityType: 'database',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'critical',
+              evidenceBadges: ['entity-relation', 'otlp-trace-call']
+            }
+          ],
+          edges: [
+            {
+              id: 'trace-call:501:502:trace-701',
+              sourceEntityId: 501,
+              targetEntityId: 502,
+              relationType: 'trace-call',
+              relationSource: 'otlp-trace-call',
+              sampleTraceId: 'trace-701',
+              sampleSpanId: 'span-701',
+              status: 'critical',
+              score: 44,
+              evidenceBadges: ['entity-relation', 'otlp-trace-call', 'service-graph'],
+              redMetrics: {
+                requestRatePerSecond: 7.25,
+                errorRate: 0.21,
+                latencyP95Ms: 123
+              }
+            }
+          ]
+        }}
+      />
+    );
+
+    const edgeId = 'relation-trace-call:501:502:trace-701';
+    const encodedEdgeId = 'relation-trace-call%3A501%3A502%3Atrace-701';
+    expect(html).toContain(`data-topology-edge-evidence-panel="${edgeId}"`);
+    expect(html).toContain(`data-topology-edge-id="${edgeId}"`);
+    expect(html).toMatch(
+      new RegExp(`href="\\/topology\\?[^"]*viewMode=service-call[^"]*sourceKind=otlp-trace-call[^"]*edgeId=${encodedEdgeId}`)
+    );
+    expect(html).toMatch(
+      new RegExp(`data-topology-edge-link="metrics"[^>]+href="\\/ingestion\\/otlp\\/metrics\\?[^"]*viewMode=service-call[^"]*sourceKind=otlp-trace-call[^"]*edgeId=${encodedEdgeId}`)
+    );
+    expect(html).toMatch(
+      new RegExp(`data-topology-edge-link="logs"[^>]+href="\\/log\\/manage\\?[^"]*viewMode=service-call[^"]*sourceKind=otlp-trace-call[^"]*edgeId=${encodedEdgeId}`)
+    );
+    expect(html).toMatch(
+      new RegExp(`data-topology-edge-link="traces"[^>]+href="\\/trace\\/manage\\?[^"]*viewMode=service-call[^"]*sourceKind=otlp-trace-call[^"]*edgeId=${encodedEdgeId}`)
+    );
+    expect(html).toMatch(
+      /data-topology-edge-link="logs"[^>]+href="\/log\/manage\?[^"]*traceId=trace-701[^"]*spanId=span-701/
+    );
+    expect(html).toMatch(
+      /data-topology-edge-link="traces"[^>]+href="\/trace\/manage\?[^"]*traceId=trace-701[^"]*spanId=span-701/
+    );
+    expect(html).toMatch(
+      /data-topology-edge-link="traces"[^>]+href="\/trace\/manage\?[^"]*returnTo=[^"]*traceId%3Dtrace-701[^"]*spanId%3Dspan-701/
+    );
+    expect(html).toContain('trace-701');
+    expect(html).toContain('span-701');
+  }, 15000);
 
   it('renders incoming fault evidence context without fake future analysis panels', async () => {
-    mockSearchParams = new URLSearchParams({
+    const routeContext = {
       entityId: 'service:commerce/checkout',
       entityName: 'checkout-api',
       serviceName: 'checkout-api',
@@ -211,13 +2088,31 @@ describe('topology page', () => {
       viewMode: 'resource-dependency',
       sourceKind: 'database-middleware-connection',
       edgeId: 'svc-checkout--res-orders-db'
-    });
+    };
 
-    const { default: TopologyPage } = await import('./page');
-    const html = renderToStaticMarkup(<TopologyPage />);
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildApiTopologyFixture()} />);
 
     expect(html).toContain('data-topology-fault-context="incoming-evidence"');
+    expect(html.indexOf('data-topology-canvas="hertzbeat-topology-canvas"')).toBeLessThan(
+      html.indexOf('data-topology-fault-context="incoming-evidence"')
+    );
+    expect(html).toContain('data-topology-fault-context-owner="hertzbeat-ui-evidence-list"');
+    expect(html).toContain('data-topology-fault-context-boundary-owner="hertzbeat-ui-evidence-list-boundary"');
+    expect(html).toContain('data-hz-ui="topology-evidence-list"');
+    expect(html).toContain('data-hz-topology-primitive="evidence-list"');
+    expect(html).toContain('data-hz-topology-evidence-list-kind="fault-context"');
+    expect(html).toContain('data-hz-topology-evidence-list-boundary="toolbar-context"');
+    expect(html).toContain('data-hz-topology-evidence-list-boundary-owner="hertzbeat-ui-evidence-list-boundary"');
+    expect(html).toContain('data-hz-topology-evidence-header-owner="hertzbeat-ui-evidence-list-header"');
+    expect(html).toContain('data-hz-topology-evidence-title-owner="hertzbeat-ui-evidence-list-title"');
+    expect(html).toContain('data-hz-topology-evidence-copy-owner="hertzbeat-ui-evidence-list-copy"');
+    expect(html).toContain('data-hz-topology-evidence-count-owner="hertzbeat-ui-evidence-list-count"');
     expect(html).toContain('data-topology-fault-context-row="当前实体"');
+    expect(html).toContain('data-hz-topology-evidence-item-owner="hertzbeat-ui-evidence-list-item"');
+    expect(html).toContain('data-hz-topology-evidence-item-label-owner="hertzbeat-ui-evidence-list-item-label"');
+    expect(html).toContain('data-hz-topology-evidence-item-value-owner="hertzbeat-ui-evidence-list-item-value"');
+    expect(html).toContain('data-hz-topology-evidence-item-meta-owner="hertzbeat-ui-evidence-list-item-meta"');
     expect(html).toContain('entityId service:commerce/checkout');
     expect(html).toContain('data-topology-fault-context-row="当前服务"');
     expect(html).toContain('checkout-api');
@@ -228,6 +2123,7 @@ describe('topology page', () => {
     expect(html).toContain('采集器 edge-collector-a · 模板 java-service');
     expect(html).toContain('data-topology-active-node-id="svc-checkout"');
     expect(html).toContain('data-topology-active-edge-id="svc-checkout--res-orders-db"');
+    expect(html).toContain('trace-123');
     expect(html).not.toContain('data-topology-root-cause-panel');
     expect(html).not.toContain('data-topology-change-timeline');
     expect(html).not.toContain('data-topology-dependency-discovery');
@@ -235,7 +2131,7 @@ describe('topology page', () => {
 
   it('renders localized topology chrome in en-US without closeout Chinese or raw keys', async () => {
     i18nState.locale = 'en-US';
-    mockSearchParams = new URLSearchParams({
+    const routeContext = {
       entityId: 'service:commerce/checkout',
       entityName: 'checkout-api',
       serviceName: 'checkout-api',
@@ -250,13 +2146,15 @@ describe('topology page', () => {
       viewMode: 'resource-dependency',
       sourceKind: 'database-middleware-connection',
       edgeId: 'svc-checkout--res-orders-db'
-    });
+    };
 
-    const { default: TopologyPage } = await import('./page');
-    const html = renderToStaticMarkup(<TopologyPage />);
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildApiTopologyFixture()} />);
 
-    expect(html).toContain('Topology and impact');
-    expect(html).toContain('HertzBeat operations topology');
+    expect(html).toContain('Topology');
+    expect(html).toContain('Operations topology');
+    expect(html).not.toContain('Topology and impact');
+    expect(html).not.toContain('HertzBeat operations topology');
     expect(html).toContain('Refresh topology');
     expect(html).toContain('Search entities, services, resources, or labels');
     expect(html).toContain('Fault context');
diff --git a/web-next/app/topology/page.tsx b/web-next/app/topology/page.tsx
index cd9b2d459a..c30aab4f96 100644
--- a/web-next/app/topology/page.tsx
+++ b/web-next/app/topology/page.tsx
@@ -1,399 +1,13 @@
-'use client';
-
 import React from 'react';
-import { useSearchParams } from 'next/navigation';
-import { Input } from '../../components/ui/input';
-import { useI18n } from '../../components/providers/i18n-provider';
-import { Select } from '../../components/ui/select';
-import { buildTopologyServiceMap, type TopologyRouteContext, type TopologyServiceEdge, type TopologyServiceNode } from '../../lib/topology-surface/view-model';
-import { readSignalRouteContext } from '../../lib/signal-route-context';
-
-const edgeColor: Record<TopologyServiceEdge['tone'], string> = {
-  green: '#2fa84f',
-  blue: '#2f8ed8',
-  orange: '#f59e0b',
-  purple: '#8b5cf6',
-  red: '#ef4444'
-};
-
-const nodeToneClass: Record<TopologyServiceNode['tone'], string> = {
-  success: 'border-[#365a45] bg-[#122017] text-[#d9f7df]',
-  warning: 'border-[#786032] bg-[#221b0d] text-[#f6e4b0]',
-  danger: 'border-[#80464f] bg-[#241115] text-[#ffd6dc]'
-};
-
-const nodeFocusClass: Record<TopologyServiceNode['focus'], string> = {
-  normal: '',
-  active: 'z-20 ring-2 ring-[#4e74f8] ring-offset-2 ring-offset-[#08090c]',
-  related: 'z-10 shadow-[0_18px_54px_rgba(78,116,248,0.22)]',
-  dimmed: 'opacity-45'
-};
-
-function findNode(nodes: TopologyServiceNode[], id: string) {
-  return nodes.find(node => node.id === id);
-}
-
-function formatTimeRange(timeRange: string, t: (key: string) => string) {
-  if (timeRange === 'last-1h') return t('topology.time.last-1h');
-  return timeRange;
-}
-
-function readTopologyRouteContext(searchParams: ReturnType<typeof useSearchParams>): TopologyRouteContext {
-  return {
-    ...readSignalRouteContext(searchParams),
-    viewMode: searchParams.get('viewMode') ?? undefined,
-    sourceKind: searchParams.get('sourceKind') ?? undefined,
-    edgeId: searchParams.get('edgeId') ?? undefined
-  };
-}
-
-export default function TopologyPage() {
-  const searchParams = useSearchParams();
-  const { t } = useI18n();
-  const map = buildTopologyServiceMap(readTopologyRouteContext(searchParams), t);
-  const primaryNode = map.nodes.find(node => node.id === map.activeNodeId) ?? map.nodes.find(node => node.id === 'svc-checkout') ?? map.nodes[0];
-
-  return (
-    <main
-      data-topology-route="hertzbeat-entity-topology"
-      data-topology-incoming-context={map.filterContext.hasIncomingContext ? 'entity-filter' : 'none'}
-      data-topology-active-node-id={map.activeNodeId ?? 'none'}
-      data-topology-active-edge-id={map.selectedEdgeId ?? 'none'}
-      data-topology-active-view-mode={map.filterContext.viewMode}
-      data-topology-active-source-kind={map.filterContext.sourceKind ?? 'all'}
-      className="min-h-[calc(100vh-56px)] bg-[#08090c] text-[#f1f3f7]"
-    >
-      <header className="border-b border-[#252832] bg-[#0b0c0f] px-4 py-4">
-        <div className="flex flex-wrap items-start justify-between gap-4">
-          <div className="min-w-[260px]">
-            <div className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">{t('topology.kicker')}</div>
-            <h1 className="mt-1 text-[26px] font-semibold leading-tight text-[#f5f7fb]">{map.productIdentity}</h1>
-            <p className="mt-2 max-w-[760px] text-[13px] leading-6 text-[#a9b0bb]">
-              {t('topology.copy')}
-            </p>
-          </div>
-          <div className="flex flex-wrap items-center justify-end gap-2 text-[12px] text-[#d6d9e2]">
-            <span
-              data-topology-filter-environment={map.filterContext.environment}
-              className="rounded-[3px] border border-[#252832] bg-[#151821] px-2 py-1 text-[#9ca3b4]"
-            >
-              {map.filterContext.environment}
-            </span>
-            <span
-              data-topology-filter-time-range={map.filterContext.timeRange}
-              className="rounded-[3px] border border-[#252832] bg-[#151821] px-2 py-1 text-[#9ca3b4]"
-            >
-              {formatTimeRange(map.filterContext.timeRange, t)}
-            </span>
-            <button type="button" className="h-8 rounded-[3px] border border-[#303542] bg-[#151821] px-3 text-[12px] font-semibold text-[#dfe3ec]">
-              {t('topology.refresh')}
-            </button>
-          </div>
-        </div>
-        <div data-topology-source-strip="relationship-source-contract" className="mt-4 grid gap-2 md:grid-cols-2 xl:grid-cols-4">
-          {map.sources.map(source => (
-            <a
-              key={source.kind}
-              href={source.href}
-              data-topology-source-control="relationship-source"
-              data-topology-source-kind={source.kind}
-              data-topology-source-active={source.active ? source.kind : 'false'}
-              className={`rounded-[4px] border px-3 py-2 transition-colors ${source.active ? 'border-[#4e74f8] bg-[#182238]' : 'border-[#252832] bg-[#101217] hover:border-[#364052] hover:bg-[#131821]'}`}
-            >
-              <div className="text-[12px] font-semibold text-[#e3e8f0]">{source.label}</div>
-              <div className="mt-1 text-[11px] leading-5 text-[#8f99ab]">{source.copy}</div>
-            </a>
-          ))}
-        </div>
-      </header>
-
-      <section data-topology-controls="hertzbeat-topology-controls" className="grid gap-2 border-b border-[#252832] bg-[#0b0c0f] px-4 py-3 lg:grid-cols-[180px_minmax(0,1fr)_104px_104px]">
-        <Select
-          aria-label={t('topology.environment.aria')}
-          containerClassName="w-full"
-          className="h-8 min-w-0 text-[#9da6b8]"
-          defaultValue={map.filterContext.environment}
-        >
-          <option value="all">{t('topology.environment.all')}</option>
-          <option value="dev">dev</option>
-          <option value="prod">prod</option>
-        </Select>
-        <Input
-          aria-label={t('topology.search.aria')}
-          className="h-8 rounded-[2px] border border-[#303542] bg-[#111318] px-3 text-[12px] text-[#e7eaf1] placeholder:text-[#626b7c]"
-          placeholder={t('topology.search.placeholder')}
-          defaultValue={map.filterContext.search}
-        />
-        <button type="button" className="h-8 rounded-[4px] border border-[#303542] bg-[#151821] text-[12px] font-semibold text-[#dfe3ec]">
-          {t('topology.view.fit')}
-        </button>
-        <button type="button" className="h-8 rounded-[4px] border border-[#31405c] bg-[#182238] text-[12px] font-semibold text-[#d8e4ff]">
-          {t('topology.locate-entity')}
-        </button>
-        <div data-topology-filter-summary="incoming-context" className="lg:col-span-4 flex flex-wrap items-center gap-2 text-[12px] text-[#8f99ab]">
-          <span className="font-semibold text-[#d6d9e2]">{t('topology.current-filter')}</span>
-          <span>{map.filterContext.search || t('topology.all-entities')}</span>
-          <span>{map.filterContext.environment}</span>
-          <span>{map.filterContext.timeRange}</span>
-          <span>{map.viewModes.find(mode => mode.active)?.label}</span>
-          {map.filterContext.sourceKind ? <span>{map.sources.find(source => source.kind === map.filterContext.sourceKind)?.label}</span> : null}
-        </div>
-      </section>
-
-      {map.faultContextRows.length > 0 ? (
-        <section data-topology-fault-context="incoming-evidence" className="border-b border-[#252832] bg-[#0b0c0f] px-4 py-3">
-          <div className="flex flex-wrap items-center gap-2">
-            <div className="text-[12px] font-semibold text-[#e3e8f0]">{t('topology.fault-context.title')}</div>
-            <div className="text-[12px] text-[#8f99ab]">{t('topology.fault-context.copy')}</div>
-          </div>
-          <div className="mt-3 grid gap-2 md:grid-cols-2 xl:grid-cols-3">
-            {map.faultContextRows.map(row => (
-              <div
-                key={row.label}
-                data-topology-fault-context-row={row.label}
-                className="min-w-0 border-l border-[#31405c] bg-[#101217] px-3 py-2"
-              >
-                <div className="text-[11px] font-semibold text-[#7e8494]">{row.label}</div>
-                <div className="mt-1 truncate text-[12px] font-semibold text-[#e3e8f0]" title={row.value}>{row.value}</div>
-                <div className="mt-1 truncate text-[11px] text-[#8f99ab]" title={row.meta}>{row.meta}</div>
-              </div>
-            ))}
-          </div>
-        </section>
-      ) : null}
-
-      <section className="grid min-h-[760px] bg-[#08090c] xl:grid-cols-[minmax(0,1fr)_320px]">
-        <div data-topology-canvas="hertzbeat-topology-canvas" className="relative min-h-[680px] overflow-hidden bg-[#08090c]">
-        <svg className="absolute inset-0 h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
-          {map.edges.map(edge => {
-            const from = findNode(map.nodes, edge.from);
-            const to = findNode(map.nodes, edge.to);
-            if (!from || !to) return null;
-            return (
-              <line
-                key={`${edge.from}-${edge.to}`}
-                data-topology-edge="service-edge"
-                x1={from.x}
-                y1={from.y}
-                x2={to.x}
-                y2={to.y}
-                stroke={edgeColor[edge.tone]}
-                strokeWidth="0.25"
-                strokeLinecap="round"
-                opacity={edge.focus === 'context-muted' ? '0.32' : '0.92'}
-                data-topology-edge-id={edge.id}
-                data-topology-edge-source={edge.source}
-                data-topology-edge-focus={edge.focus}
-                data-topology-edge-selected={edge.selected ? 'true' : 'false'}
-              />
-            );
-          })}
-        </svg>
-
-        {map.edges.map(edge => {
-          const from = findNode(map.nodes, edge.from);
-          const to = findNode(map.nodes, edge.to);
-          if (!from || !to) return null;
-          return (
-            <a
-              key={`${edge.from}-${edge.to}-dot`}
-              href={edge.drilldownHref}
-              aria-label={t('topology.edge.evidence.aria', { label: edge.label })}
-              data-topology-edge-drilldown={edge.id}
-              data-topology-edge-selected={edge.selected ? 'true' : 'false'}
-              className={`absolute h-2 w-2 rounded-[2px] border ${edge.selected ? 'border-[#c8d5ff] shadow-[0_0_0_4px_rgba(78,116,248,0.22)]' : 'border-transparent'}`}
-              style={{
-                left: `${(from.x + to.x) / 2}%`,
-                top: `${(from.y + to.y) / 2}%`,
-                backgroundColor: edgeColor[edge.tone],
-                transform: 'translate(-50%, -50%)'
-              }}
-            />
-          );
-        })}
-
-        {map.nodes.map(node => (
-          <a
-            key={node.id}
-            href={node.links.entityHref}
-            data-topology-node="service-node"
-            data-topology-node-id={node.id}
-            data-topology-node-focus={node.focus}
-            data-topology-node-active={node.focus === 'active' ? 'true' : 'false'}
-            data-topology-node-entity-type={node.entityType}
-            data-topology-node-source={node.source}
-            data-topology-node-health-affordance="lightweight-service-health"
-            data-topology-node-health-score={node.healthAffordance.score}
-            data-topology-node-entity-href={node.links.entityHref}
-            className={`absolute flex flex-col items-center justify-center gap-0.5 overflow-hidden rounded-[4px] border px-2 py-1 text-center text-[12px] font-semibold shadow-[0_16px_48px_rgba(0,0,0,0.35)] ${nodeToneClass[node.tone]} ${nodeFocusClass[node.focus]}`}
-            style={{
-              left: `${node.x}%`,
-              top: `${node.y}%`,
-              width: node.size,
-              height: node.size,
-              transform: 'translate(-50%, -50%)'
-            }}
-            aria-label={node.label}
-          >
-            <span className="w-full truncate leading-tight">{node.label}</span>
-            <span
-              data-topology-node-health-copy="lightweight-service-health"
-              className="w-full truncate text-[10px] font-medium leading-3 text-[#a9b0bb]"
-              title={`${node.healthAffordance.label} · ${node.healthAffordance.copy}`}
-            >
-              {node.healthAffordance.label}
-            </span>
-          </a>
-        ))}
-        </div>
-        <aside className="border-t border-[#252832] bg-[#0b0c0f] p-4 xl:border-l xl:border-t-0">
-          <div className="text-[12px] font-semibold tracking-[0.12em] text-[#7e8494]">{t('topology.aside.view')}</div>
-          <div className="mt-3 grid gap-2">
-            {map.viewModes.map(mode => (
-              <a
-                key={mode.key}
-                aria-current={mode.active ? 'page' : undefined}
-                href={mode.href}
-                data-topology-view-mode={mode.key}
-                data-topology-view-mode-active={mode.active ? mode.key : 'false'}
-                className={`rounded-[4px] border px-3 py-2 text-left ${mode.active ? 'border-[#4e74f8] bg-[#182238]' : 'border-[#252832] bg-[#101217]'}`}
-              >
-                <span className="text-[13px] font-semibold text-[#eef2f7]">{mode.label}</span>
-                <span className="mt-1 block text-[11px] leading-5 text-[#8f99ab]">{mode.copy}</span>
-              </a>
-            ))}
-          </div>
-          <a
-            data-topology-alert-impact-link="alert-center"
-            className="mt-3 block rounded-[4px] border border-[#31405c] bg-[#182238] px-3 py-2 text-[12px] font-semibold text-[#d8e4ff]"
-            href={map.alertImpactHref}
-          >
-            {t('topology.alert-impact.open')}
-          </a>
-
-          {map.selectedEdge ? (
-            <div data-topology-edge-evidence-panel={map.selectedEdge.id} className="mt-5 rounded-[4px] border border-[#252832] bg-[#101217] p-3">
-              <div className="text-[12px] font-semibold tracking-[0.12em] text-[#7e8494]">{t('topology.edge.evidence.title')}</div>
-              <div className="mt-2 text-[15px] font-semibold text-[#f5f7fb]">{map.selectedEdge.evidence.title}</div>
-              <div className="mt-1 text-[12px] text-[#8f99ab]">{map.selectedEdge.evidence.sourceLabel} · {map.selectedEdge.evidence.collectedBy}</div>
-              <div
-                data-topology-edge-evidence-boundary="roadmap-boundary"
-                className="mt-3 border-l border-[#4b5566] bg-[#0b0c0f] px-3 py-2 text-[12px] leading-5 text-[#a9b0bb]"
-              >
-                {map.selectedEdge.evidence.boundary}
-              </div>
-              <div className="mt-3 grid gap-2">
-                {map.selectedEdge.evidence.rows.map(row => (
-                  <div key={`${row.label}-${row.value}`} className="rounded-[3px] border border-[#252832] bg-[#0b0c0f] px-2 py-2">
-                    <div className="text-[11px] font-semibold text-[#7e8494]">{row.label}</div>
-                    <div className="mt-1 text-[12px] font-semibold text-[#e3e8f0]">{row.value}</div>
-                    <div className="mt-1 text-[11px] text-[#8f99ab]">{row.meta}</div>
-                  </div>
-                ))}
-              </div>
-              <div className="mt-3 flex flex-wrap gap-2">
-                <a
-                  data-topology-edge-link="from-entity"
-                  className="rounded-[3px] border border-[#303542] bg-[#151821] px-3 py-1.5 text-[12px] font-semibold text-[#dfe3ec]"
-                  href={map.selectedEdge.links.fromEntityHref}
-                >
-                  {t('topology.edge.link.from-entity')}
-                </a>
-                <a
-                  data-topology-edge-link="to-entity"
-                  className="rounded-[3px] border border-[#303542] bg-[#151821] px-3 py-1.5 text-[12px] font-semibold text-[#dfe3ec]"
-                  href={map.selectedEdge.links.toEntityHref}
-                >
-                  {t('topology.edge.link.to-entity')}
-                </a>
-                <a
-                  data-topology-edge-link="alert-impact"
-                  className="rounded-[3px] border border-[#31405c] bg-[#182238] px-3 py-1.5 text-[12px] font-semibold text-[#d8e4ff]"
-                  href={map.selectedEdge.links.alertImpactHref}
-                >
-                  {t('topology.edge.link.alert-impact')}
-                </a>
-                <span data-topology-edge-link-copy="alert-impact" className="basis-full text-[11px] leading-5 text-[#8f99ab]">
-                  {map.selectedEdge.evidence.alertImpactCopy}
-                </span>
-              </div>
-              <div className="mt-3 grid gap-2">
-                <a
-                  data-topology-edge-link="metrics"
-                  className="rounded-[3px] border border-[#31405c] bg-[#182238] px-3 py-1.5 text-[12px] font-semibold text-[#d8e4ff]"
-                  href={map.selectedEdge.links.metricsHref}
-                >
-                  {t('topology.edge.link.metrics')}
-                </a>
-                <a
-                  data-topology-edge-link="logs"
-                  className="rounded-[3px] border border-[#303542] bg-[#151821] px-3 py-1.5 text-[12px] font-semibold text-[#dfe3ec]"
-                  href={map.selectedEdge.links.logsHref}
-                >
-                  {t('topology.edge.link.logs')}
-                </a>
-                <a
-                  data-topology-edge-link="traces"
-                  className="rounded-[3px] border border-[#303542] bg-[#151821] px-3 py-1.5 text-[12px] font-semibold text-[#dfe3ec]"
-                  href={map.selectedEdge.links.tracesHref}
-                >
-                  {t('topology.edge.link.traces')}
-                </a>
-              </div>
-            </div>
-          ) : null}
-
-          {primaryNode ? (
-            <div className="mt-5 rounded-[4px] border border-[#252832] bg-[#101217] p-3">
-              <div className="text-[12px] font-semibold tracking-[0.12em] text-[#7e8494]">{t('topology.current-entity')}</div>
-              <div className="mt-2 text-[16px] font-semibold text-[#f5f7fb]">{primaryNode.label}</div>
-              <div className="mt-2 grid gap-1 text-[12px] text-[#a9b0bb]">
-                <span>{primaryNode.entityId}</span>
-                <span>{primaryNode.namespace} / {primaryNode.environment}</span>
-                <span>{t('topology.current-entity.signals', { signals: primaryNode.signals.join(', ') })}</span>
-                <span data-topology-current-entity-health="lightweight-service-health">
-                  {primaryNode.healthAffordance.label} · {primaryNode.healthAffordance.copy}
-                </span>
-              </div>
-              <div className="mt-3 flex flex-wrap gap-2">
-                <a
-                  data-topology-context-link="entity"
-                  className="rounded-[3px] border border-[#303542] bg-[#151821] px-3 py-1.5 text-[12px] font-semibold text-[#dfe3ec]"
-                  href={primaryNode.links.entityHref}
-                >
-                  {t('topology.context-link.entity')}
-                </a>
-              </div>
-              <div className="mt-4 border-t border-[#252832] pt-3">
-                <div className="text-[12px] font-semibold tracking-[0.12em] text-[#7e8494]">{t('topology.context-link.signals')}</div>
-                <div className="mt-2 grid gap-2">
-                  <a
-                    data-topology-context-link="metrics"
-                    className="rounded-[3px] border border-[#31405c] bg-[#182238] px-3 py-1.5 text-[12px] font-semibold text-[#d8e4ff]"
-                    href={primaryNode.links.metricsHref}
-                  >
-                    {t('topology.context-link.metrics')}
-                  </a>
-                  <a
-                    data-topology-context-link="logs"
-                    className="rounded-[3px] border border-[#303542] bg-[#151821] px-3 py-1.5 text-[12px] font-semibold text-[#dfe3ec]"
-                    href={primaryNode.links.logsHref}
-                  >
-                    {t('topology.context-link.logs')}
-                  </a>
-                  <a
-                    data-topology-context-link="traces"
-                    className="rounded-[3px] border border-[#303542] bg-[#151821] px-3 py-1.5 text-[12px] font-semibold text-[#dfe3ec]"
-                    href={primaryNode.links.tracesHref}
-                  >
-                    {t('topology.context-link.traces')}
-                  </a>
-                </div>
-              </div>
-            </div>
-          ) : null}
-        </aside>
-      </section>
-    </main>
-  );
+import TopologyPage from './topology-page';
+import { readTopologyRouteContext, type TopologySearchParams } from '../../lib/topology-surface/query-state';
+
+export default async function TopologyRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<TopologySearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeContext = readTopologyRouteContext(resolvedSearchParams);
+  return <TopologyPage routeContext={routeContext} />;
 }
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
new file mode 100644
index 0000000000..5dd9ec494b
--- /dev/null
+++ b/web-next/app/topology/topology-page.tsx
@@ -0,0 +1,1935 @@
+'use client';
+
+import React from 'react';
+import { useRouter } from 'next/navigation';
+import {
+  HzTopologyActionLink,
+  HzTopologyWorkbenchFrame,
+  HzTopologyWorkbenchHeader,
+  HzTopologyWorkbenchGrid,
+  HzTopologyWorkbenchSlot,
+  HzTopologyCanvas,
+  HzTopologyCanvasAnnotation,
+  HzTopologyCompanionRail,
+  HzTopologyCompanionSection,
+  HzTopologyCompanionJumpList,
+  HzTopologyDetailDrawer,
+  HzTopologyEmptyState,
+  HzTopologyLoadingState,
+  HzTopologyEvidenceList,
+  HzTopologyFilterStrip,
+  HzTopologyFocusTrail,
+  HzTopologyGroupPanel,
+  HzTopologyPathSummary,
+  HzTopologyHoverTooltip,
+  HzTopologyLegend,
+  HzTopologyMetricTable,
+  HzTopologyScopeBar,
+  HzTopologySectionLabel,
+  HzTopologyToolbar,
+  type HzStatusTone,
+  type HzTopologyCanvasDrawerMode,
+  type HzTopologyCanvasFocusDepth,
+  type HzTopologyDetailDrawerAction,
+  type HzTopologyDetailDrawerFact,
+  type HzTopologyGroupPanelItem,
+  type HzTopologyPathSummaryMetric,
+  type HzTopologyHoverTooltipFact,
+  type HzTopologyHoverTooltipMetric,
+  type HzTopologyMetricRow,
+  type HzTopologyMetricTableRenderWindowFilter,
+  type HzTopologyToolbarStateItem
+} from '@hertzbeat/ui';
+import {
+  HzTopologyG6Canvas,
+  buildHzTopologyG6LargeGraphStrategy,
+  buildHzTopologyG6RenderWindow,
+  type HzTopologyG6GraphInput,
+  type HzTopologyG6HoverAnchor
+} from '@hertzbeat/ui/topology-g6';
+import { useI18n } from '../../components/providers/i18n-provider';
+import { api } from '../../lib/api-facade';
+import { buildTopologyApiUrl, loadTopologyGraph, resolveTopologyRelationType } from '../../lib/topology-surface/controller';
+import {
+  buildTopologyServiceMapFromApiGraph,
+  type EntityTopologyApiGraph,
+  type TopologyFilterContext,
+  type TopologyRedMetrics,
+  type TopologyRouteContext,
+  type TopologyServiceEdge,
+  type TopologyServiceNode
+} from '../../lib/topology-surface/view-model';
+
+type TopologyLocalSelectionSource = 'none' | 'node-click' | 'edge-click' | 'table-row-click';
+type TopologyLayoutMode = 'layered-service' | 'force' | 'grid-table';
+
+function findNode(nodes: TopologyServiceNode[], id: string) {
+  return nodes.find(node => node.id === id);
+}
+
+function findEdge(edges: TopologyServiceEdge[], id: string) {
+  return edges.find(edge => edge.id === id);
+}
+
+function formatTimeRange(timeRange: string, t: (key: string) => string) {
+  if (timeRange === 'last-1h') return t('topology.time.last-1h');
+  return timeRange;
+}
+
+function formatTopologyDepth(depth: number | undefined, t: (key: string, params?: Record<string, string | number>) => string) {
+  if (depth === 1) return t('topology.state.depth.one-hop');
+  if (depth === 2) return t('topology.state.depth.two-hop');
+  if (typeof depth === 'number' && Number.isFinite(depth)) {
+    return t('topology.state.depth.n-hop', { depth });
+  }
+  return t('topology.state.depth.auto');
+}
+
+function formatTopologyCanvasFocusDepth(depth: number | undefined): HzTopologyCanvasFocusDepth {
+  if (depth === 1) return '1-hop';
+  if (depth === 2) return '2-hop';
+  if (typeof depth === 'number' && Number.isFinite(depth)) return 'auto';
+  return 'auto';
+}
+
+function formatCompactMetricNumber(value: number, maximumFractionDigits = 2) {
+  return value.toLocaleString('en-US', {
+    maximumFractionDigits,
+    minimumFractionDigits: 0
+  });
+}
+
+function formatRequestRateMetric(value: number | undefined) {
+  return typeof value === 'number' && Number.isFinite(value) ? `${formatCompactMetricNumber(value)}/s` : undefined;
+}
+
+function formatErrorRateMetric(value: number | undefined) {
+  if (typeof value !== 'number' || !Number.isFinite(value)) return undefined;
+  const percentValue = Math.abs(value) <= 1 ? value * 100 : value;
+  return `${formatCompactMetricNumber(percentValue)}%`;
+}
+
+function formatLatencyMetric(value: number | undefined) {
+  return typeof value === 'number' && Number.isFinite(value) ? `${formatCompactMetricNumber(value)} ms` : undefined;
+}
+
+function buildRedMetricRows(metrics: TopologyRedMetrics, t: (key: string) => string) {
+  return [
+    {
+      key: 'request-rate',
+      label: t('topology.red.request-rate'),
+      value: formatRequestRateMetric(metrics.requestRatePerSecond)
+    },
+    {
+      key: 'error-rate',
+      label: t('topology.red.error-rate'),
+      value: formatErrorRateMetric(metrics.errorRate)
+    },
+    {
+      key: 'latency-p95',
+      label: t('topology.red.latency-p95'),
+      value: formatLatencyMetric(metrics.latencyP95Ms)
+    }
+  ].filter((row): row is { key: string; label: string; value: string } => Boolean(row.value));
+}
+
+function buildTopologyHoverTooltipFacts(
+  edge: TopologyServiceEdge,
+  fromNode: TopologyServiceNode,
+  toNode: TopologyServiceNode,
+  routeContext: TopologyRouteContext | undefined,
+  t: (key: string) => string
+): HzTopologyHoverTooltipFact[] {
+  const facts: HzTopologyHoverTooltipFact[] = [
+    {
+      id: 'source',
+      label: t('topology.evidence.row.from'),
+      value: fromNode.label,
+      meta: fromNode.entityId
+    },
+    {
+      id: 'target',
+      label: t('topology.evidence.row.to'),
+      value: toNode.label,
+      meta: toNode.entityId
+    },
+    {
+      id: 'relation-type',
+      label: t('topology.hover.fact.relation-type'),
+      value: edge.relationshipType
+    }
+  ];
+  if (edge.evidence.firstSeen) {
+    facts.push({
+      id: 'first-seen',
+      label: t('topology.hover.fact.first-seen'),
+      value: edge.evidence.firstSeen,
+      meta: edge.evidence.sourceLabel
+    });
+  }
+  facts.push({
+    id: 'last-seen',
+    label: t('topology.hover.fact.last-seen'),
+    value: edge.evidence.lastSeen || routeContext?.timeRange || t('topology.state.depth.auto'),
+    meta: edge.evidence.sourceLabel
+  });
+  const sampleTraceId = edge.evidence.sampleTraceId || routeContext?.traceId;
+  const sampleSpanId = edge.evidence.sampleSpanId || routeContext?.spanId;
+  if (sampleTraceId) {
+    facts.push({
+      id: 'sample-trace',
+      label: t('topology.hover.fact.sample-trace'),
+      value: sampleTraceId,
+      meta: sampleSpanId
+    });
+  }
+  return facts;
+}
+
+function buildTopologyHoverTooltipMetrics(edge: TopologyServiceEdge, t: (key: string) => string): HzTopologyHoverTooltipMetric[] {
+  return buildRedMetricRows(edge.redMetrics, t).map(row => ({
+    id: row.key,
+    label: row.label,
+    value: row.value,
+    tone: row.key === 'error-rate' ? 'warning' : 'info'
+  }));
+}
+
+function buildTopologyPathSummaryMetrics(edge: TopologyServiceEdge, t: (key: string) => string): HzTopologyPathSummaryMetric[] {
+  return buildRedMetricRows(edge.redMetrics, t).map(row => ({
+    id: row.key,
+    label: row.label,
+    value: row.value,
+    tone: row.key === 'error-rate' || row.key === 'latency-p95' ? 'warning' : 'info'
+  }));
+}
+
+function topologyMetricTone(edge: TopologyServiceEdge): HzStatusTone {
+  if (edge.alertImpact === 'critical' || edge.tone === 'red') return 'critical';
+  if (edge.alertImpact === 'warning' || edge.tone === 'orange') return 'warning';
+  if (edge.tone === 'green') return 'success';
+  return 'info';
+}
+
+function topologyNodeHealthTone(node: TopologyServiceNode): HzStatusTone {
+  if (node.health === 'critical' || node.tone === 'red') return 'critical';
+  if (node.health === 'warning' || node.tone === 'orange') return 'warning';
+  if (node.health === 'healthy' || node.tone === 'green') return 'success';
+  return 'neutral';
+}
+
+function buildTopologyNodeNeighborEvidence(
+  node: TopologyServiceNode,
+  nodes: TopologyServiceNode[],
+  edges: TopologyServiceEdge[]
+) {
+  const nodeById = new Map(nodes.map(item => [item.id, item]));
+  const upstreamNodeIds = edges.filter(edge => edge.to === node.id).map(edge => edge.from);
+  const downstreamNodeIds = edges.filter(edge => edge.from === node.id).map(edge => edge.to);
+  const formatLabels = (ids: string[]) => ids.map(id => nodeById.get(id)?.label ?? id).join(', ');
+
+  return {
+    upstreamNodeIds,
+    downstreamNodeIds,
+    upstreamLabels: formatLabels(upstreamNodeIds),
+    downstreamLabels: formatLabels(downstreamNodeIds)
+  };
+}
+
+function countUniqueTopologyValues(values: Array<string | undefined>) {
+  return new Set(values.filter((value): value is string => Boolean(value))).size;
+}
+
+function topologyWorstGroupTone(nodes: TopologyServiceNode[]): HzTopologyGroupPanelItem['worstTone'] {
+  if (nodes.some(node => node.health === 'critical' || node.tone === 'danger')) return 'danger';
+  if (nodes.some(node => node.health === 'warning' || node.tone === 'warning')) return 'warning';
+  if (nodes.some(node => node.health === 'healthy' || node.tone === 'success')) return 'success';
+  return 'neutral';
+}
+
+function buildTopologyGroupPanelItems({
+  nodes,
+  edges,
+  activeSourceLabel,
+  primaryNode,
+  hiddenCount,
+  t
+}: {
+  nodes: TopologyServiceNode[];
+  edges: TopologyServiceEdge[];
+  activeSourceLabel?: string;
+  primaryNode?: TopologyServiceNode;
+  hiddenCount: number;
+  t: (key: string, params?: Record<string, string | number>) => string;
+}): HzTopologyGroupPanelItem[] {
+  const sourceKinds = countUniqueTopologyValues(edges.map(edge => edge.source));
+  return [
+    {
+      id: 'entity-type',
+      label: t('topology.group-panel.entity-type'),
+      value: primaryNode?.entityType ?? t('topology.all-entities'),
+      count: countUniqueTopologyValues(nodes.map(node => node.entityType)),
+      collapsedCount: hiddenCount,
+      collapsedLabel: t('topology.group-panel.collapsed-count', { count: hiddenCount }),
+      worstTone: topologyWorstGroupTone(nodes),
+      active: true,
+      meta: t('topology.group-panel.node-count', { count: nodes.length })
+    },
+    {
+      id: 'environment',
+      label: t('topology.group-panel.environment'),
+      value: nodes[0]?.environment ?? t('topology.environment.all'),
+      count: countUniqueTopologyValues(nodes.map(node => node.environment)),
+      collapsedCount: 0,
+      collapsedLabel: t('topology.group-panel.collapsed-count', { count: 0 }),
+      worstTone: topologyWorstGroupTone(nodes),
+      active: false,
+      meta: t('topology.group-panel.active-scope')
+    },
+    {
+      id: 'source-kind',
+      label: t('topology.group-panel.source-kind'),
+      value: activeSourceLabel ?? t('topology.focus-trail.source.all'),
+      count: sourceKinds,
+      collapsedCount: Math.max(0, sourceKinds - 1),
+      collapsedLabel: t('topology.group-panel.collapsed-count', { count: Math.max(0, sourceKinds - 1) }),
+      worstTone: edges.some(edge => edge.alertImpact === 'critical') ? 'danger' : edges.some(edge => edge.alertImpact === 'warning') ? 'warning' : 'success',
+      active: Boolean(activeSourceLabel),
+      meta: t('topology.group-panel.edge-count', { count: edges.length })
+    }
+  ];
+}
+
+function buildTopologyMetricRows(edges: TopologyServiceEdge[], nodes: TopologyServiceNode[]): HzTopologyMetricRow[] {
+  return edges.map(edge => {
+    const fromNode = findNode(nodes, edge.from);
+    const toNode = findNode(nodes, edge.to);
+    return {
+      id: edge.id,
+      sourceNodeId: edge.from,
+      targetNodeId: edge.to,
+      source: fromNode?.label ?? edge.from,
+      target: toNode?.label ?? edge.to,
+      relationType: edge.label,
+      sourceKind: edge.evidence.sourceLabel,
+      requestRatePerSecond: edge.redMetrics.requestRatePerSecond,
+      requestCount: edge.redMetrics.requestCount,
+      errorRate: edge.redMetrics.errorRate,
+      errorCount: edge.redMetrics.errorCount,
+      latencyP95Ms: edge.redMetrics.latencyP95Ms,
+      latencyAvgMs: edge.redMetrics.latencyAvgMs,
+      evidenceBadges: edge.evidenceBadges,
+      tone: topologyMetricTone(edge)
+    };
+  });
+}
+
+function hasTraceRedMetrics(edge: TopologyServiceEdge) {
+  return [
+    edge.redMetrics.requestRatePerSecond,
+    edge.redMetrics.requestCount,
+    edge.redMetrics.errorRate,
+    edge.redMetrics.errorCount,
+    edge.redMetrics.latencyP95Ms,
+    edge.redMetrics.latencyAvgMs
+  ].some(value => typeof value === 'number' && Number.isFinite(value));
+}
+
+function pickTopologyDetailEdge(map: ReturnType<typeof buildTopologyServiceMapFromApiGraph>) {
+  return map.selectedEdge ?? map.edges.find(hasTraceRedMetrics) ?? map.edges[0];
+}
+
+function appendTopologySelectionParam(params: URLSearchParams, key: string, value: string | undefined) {
+  const trimmed = value?.trim();
+  if (trimmed) params.set(key, trimmed);
+}
+
+function withTopologyGroupByHref(href: string, groupBy: string) {
+  const [pathname, search = ''] = href.split('?');
+  const params = new URLSearchParams(search);
+  params.set('groupBy', groupBy);
+  return `${pathname}?${params.toString()}`;
+}
+
+function buildTopologyNodeSelectionHref(
+  node: TopologyServiceNode,
+  map: ReturnType<typeof buildTopologyServiceMapFromApiGraph>,
+  routeContext?: TopologyRouteContext,
+  options: { depth?: number } = {}
+) {
+  const params = new URLSearchParams();
+  params.set('entityId', node.entityId);
+  params.set('entityName', node.label);
+  params.set('serviceName', node.label);
+  params.set('serviceNamespace', node.namespace);
+  params.set('environment', node.environment || map.filterContext.environment);
+  params.set('timeRange', map.filterContext.timeRange);
+  params.set('viewMode', map.filterContext.viewMode);
+  if (map.filterContext.sourceKind) params.set('sourceKind', map.filterContext.sourceKind);
+  if (map.filterContext.groupBy !== 'none') params.set('groupBy', map.filterContext.groupBy);
+  const nextDepth = options.depth ?? map.apiDepth;
+  if (typeof nextDepth === 'number') params.set('depth', String(nextDepth));
+  appendTopologySelectionParam(params, 'start', routeContext?.start);
+  appendTopologySelectionParam(params, 'end', routeContext?.end);
+  appendTopologySelectionParam(params, 'refresh', routeContext?.refresh);
+  appendTopologySelectionParam(params, 'live', routeContext?.live);
+  appendTopologySelectionParam(params, 'tz', routeContext?.tz);
+  appendTopologySelectionParam(params, 'relationType', routeContext?.relationType);
+  appendTopologySelectionParam(params, 'hideInternal', routeContext?.hideInternal);
+  return `/topology?${params.toString()}`;
+}
+
+function navigateTopologyFocus(href: string | undefined, navigate: (href: string) => void) {
+  if (!href) return;
+  navigate(href);
+}
+
+function buildTopologyScopeHref(baseParams: URLSearchParams, updates: Record<string, string | undefined>) {
+  const params = new URLSearchParams(baseParams);
+  Object.entries(updates).forEach(([key, value]) => {
+    if (!value || (key === 'groupBy' && value === 'none')) {
+      params.delete(key);
+      return;
+    }
+    params.set(key, value);
+  });
+  const query = params.toString();
+  return query ? `/topology?${query}` : '/topology';
+}
+
+const TOPOLOGY_EDGE_FACT_IDS = [
+  'source-entity',
+  'target-entity',
+  'collection-evidence',
+  'alert-impact',
+  'sample-trace',
+  'first-seen',
+  'last-seen'
+];
+
+function buildTopologyEdgeDetailFacts(
+  edge: TopologyServiceEdge,
+  t: (key: string) => string
+): HzTopologyDetailDrawerFact[] {
+  return [
+    ...edge.evidence.rows.map((row, index) => ({
+      id: TOPOLOGY_EDGE_FACT_IDS[index] ?? `evidence-${index + 1}`,
+      label: row.label,
+      value: row.value,
+      meta: row.meta
+    })),
+    ...buildRedMetricRows(edge.redMetrics, t).map(row => ({
+      id: row.key,
+      label: row.label,
+      value: row.value,
+      meta: t('topology.red.source.trace-service-graph'),
+      tone: row.key === 'error-rate' ? 'warning' as const : 'info' as const,
+      factProps: { 'data-topology-edge-red-metric': row.key } as React.HTMLAttributes<HTMLDivElement>
+    }))
+  ];
+}
+
+function buildTopologyEdgeDetailActions(edge: TopologyServiceEdge, t: (key: string) => string): HzTopologyDetailDrawerAction[] {
+  return [
+    {
+      id: 'from-entity',
+      'data-topology-edge-link': 'from-entity',
+      href: edge.links.fromEntityHref,
+      label: t('topology.edge.link.from-entity')
+    },
+    {
+      id: 'to-entity',
+      'data-topology-edge-link': 'to-entity',
+      href: edge.links.toEntityHref,
+      label: t('topology.edge.link.to-entity')
+    },
+    {
+      id: 'alert-impact',
+      'data-topology-edge-link': 'alert-impact',
+      href: edge.links.alertImpactHref,
+      label: t('topology.edge.link.alert-impact'),
+      emphasis: 'primary',
+      copy: edge.evidence.alertImpactCopy,
+      copyProps: { 'data-topology-edge-link-copy': 'alert-impact' } as React.HTMLAttributes<HTMLSpanElement>
+    }
+  ];
+}
+
+function buildTopologyEdgeSignalActions(edge: TopologyServiceEdge, t: (key: string) => string): HzTopologyDetailDrawerAction[] {
+  return [
+    {
+      id: 'metrics',
+      'data-topology-edge-link': 'metrics',
+      href: edge.links.metricsHref,
+      label: t('topology.edge.link.metrics'),
+      emphasis: 'primary'
+    },
+    {
+      id: 'logs',
+      'data-topology-edge-link': 'logs',
+      href: edge.links.logsHref,
+      label: t('topology.edge.link.logs')
+    },
+    {
+      id: 'traces',
+      'data-topology-edge-link': 'traces',
+      href: edge.links.tracesHref,
+      label: t('topology.edge.link.traces')
+    }
+  ];
+}
+
+function buildTopologyNodeDetailFacts(
+  node: TopologyServiceNode,
+  neighborEvidence: ReturnType<typeof buildTopologyNodeNeighborEvidence>,
+  t: (key: string, params?: Record<string, string | number>) => string
+): HzTopologyDetailDrawerFact[] {
+  return [
+    {
+      id: 'entity-id',
+      label: t('topology.current-entity.fact.entity-id'),
+      value: node.entityId,
+      meta: node.entityType
+    },
+    {
+      id: 'namespace',
+      label: t('topology.current-entity.fact.namespace'),
+      value: `${node.namespace} / ${node.environment}`
+    },
+    {
+      id: 'signals',
+      label: t('topology.current-entity.fact.signals'),
+      value: node.signals.join(', ')
+    },
+    {
+      id: 'health',
+      label: t('topology.current-entity.fact.health'),
+      value: node.healthAffordance.label,
+      meta: node.healthAffordance.copy,
+      tone: topologyNodeHealthTone(node),
+      factProps: { 'data-topology-current-entity-health': 'lightweight-service-health' } as React.HTMLAttributes<HTMLDivElement>
+    },
+    {
+      id: 'upstream-dependencies',
+      label: t('topology.current-entity.fact.upstream'),
+      value: neighborEvidence.upstreamLabels || t('topology.current-entity.fact.neighbor.none'),
+      meta: t('topology.current-entity.fact.neighbor-count', { count: neighborEvidence.upstreamNodeIds.length }),
+      factProps: {
+        'data-topology-current-entity-neighbor-evidence': 'upstream',
+        'data-topology-current-entity-neighbor-count': String(neighborEvidence.upstreamNodeIds.length),
+        'data-topology-current-entity-neighbor-node-ids': neighborEvidence.upstreamNodeIds.join(' ') || 'none'
+      } as React.HTMLAttributes<HTMLDivElement>
+    },
+    {
+      id: 'downstream-dependencies',
+      label: t('topology.current-entity.fact.downstream'),
+      value: neighborEvidence.downstreamLabels || t('topology.current-entity.fact.neighbor.none'),
+      meta: t('topology.current-entity.fact.neighbor-count', { count: neighborEvidence.downstreamNodeIds.length }),
+      factProps: {
+        'data-topology-current-entity-neighbor-evidence': 'downstream',
+        'data-topology-current-entity-neighbor-count': String(neighborEvidence.downstreamNodeIds.length),
+        'data-topology-current-entity-neighbor-node-ids': neighborEvidence.downstreamNodeIds.join(' ') || 'none'
+      } as React.HTMLAttributes<HTMLDivElement>
+    },
+    ...buildRedMetricRows(node.redMetrics, t).map(row => ({
+      id: row.key,
+      label: row.label,
+      value: row.value,
+      meta: t('topology.red.source.trace-service-graph'),
+      tone: row.key === 'error-rate' ? 'warning' as const : 'info' as const,
+      factProps: { 'data-topology-current-entity-red-metric': row.key } as React.HTMLAttributes<HTMLDivElement>
+    }))
+  ];
+}
+
+function buildTopologyCurrentNodeHandoffProps(
+  node: TopologyServiceNode,
+  filterContext: TopologyFilterContext,
+  target: string,
+  depth?: number
+) {
+  return {
+    'data-topology-current-entity-handoff-owner': 'hertzbeat-ui-current-node-handoff',
+    'data-topology-current-entity-handoff-target': target,
+    'data-topology-current-entity-handoff-node-id': node.id,
+    'data-topology-current-entity-handoff-entity-id': node.entityId,
+    'data-topology-current-entity-handoff-source-kind': filterContext.sourceKind ?? 'all',
+    'data-topology-current-entity-handoff-group-by': filterContext.groupBy,
+    'data-topology-current-entity-handoff-view-mode': filterContext.viewMode,
+    'data-topology-current-entity-handoff-environment': filterContext.environment,
+    'data-topology-current-entity-handoff-time-range': filterContext.timeRange,
+    'data-topology-current-entity-handoff-depth': depth ?? 'unknown'
+  } as React.AnchorHTMLAttributes<HTMLAnchorElement>;
+}
+
+function buildTopologyNodeDetailActions(
+  node: TopologyServiceNode,
+  filterContext: TopologyFilterContext,
+  depth: number | undefined,
+  t: (key: string) => string
+): HzTopologyDetailDrawerAction[] {
+  return [
+    {
+      id: 'entity',
+      'data-topology-context-link': 'entity',
+      ...buildTopologyCurrentNodeHandoffProps(node, filterContext, 'entity', depth),
+      href: node.links.entityHref,
+      label: t('topology.context-link.entity'),
+      emphasis: 'primary'
+    }
+  ];
+}
+
+function buildTopologyNodeSignalActions(
+  node: TopologyServiceNode,
+  filterContext: TopologyFilterContext,
+  depth: number | undefined,
+  t: (key: string) => string
+): HzTopologyDetailDrawerAction[] {
+  return [
+    {
+      id: 'metrics',
+      'data-topology-context-link': 'metrics',
+      ...buildTopologyCurrentNodeHandoffProps(node, filterContext, 'metrics', depth),
+      href: node.links.metricsHref,
+      label: t('topology.context-link.metrics'),
+      emphasis: 'primary'
+    },
+    {
+      id: 'logs',
+      'data-topology-context-link': 'logs',
+      ...buildTopologyCurrentNodeHandoffProps(node, filterContext, 'logs', depth),
+      href: node.links.logsHref,
+      label: t('topology.context-link.logs')
+    },
+    {
+      id: 'traces',
+      'data-topology-context-link': 'traces',
+      ...buildTopologyCurrentNodeHandoffProps(node, filterContext, 'traces', depth),
+      href: node.links.tracesHref,
+      label: t('topology.context-link.traces')
+    }
+  ];
+}
+
+function buildApiOwnedEmptyGraph(routeContext?: TopologyRouteContext): EntityTopologyApiGraph {
+  const focusEntityId = routeContext?.entityId?.trim();
+  const routeDepth = Number(routeContext?.depth);
+  return {
+    apiBacked: true,
+    focusEntityId: focusEntityId && /^\d+$/.test(focusEntityId) ? focusEntityId : null,
+    depth: Number.isFinite(routeDepth) ? Math.max(0, Math.floor(routeDepth)) : 2,
+    sourceKinds: routeContext?.sourceKind ? [routeContext.sourceKind] : [],
+    nodes: [],
+    edges: [],
+    impactTimeline: []
+  };
+}
+
+const TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;
+const DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;
+
+function resolveTopologyApiTimeoutMs(routeContext?: TopologyRouteContext) {
+  if (routeContext?.sourceKind === 'otlp-trace-call' || routeContext?.viewMode === 'service-call') {
+    return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;
+  }
+  return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;
+}
+
+export default function TopologyPage({
+  routeContext,
+  apiGraph
+}: {
+  routeContext?: TopologyRouteContext;
+  apiGraph?: EntityTopologyApiGraph | null;
+} = {}) {
+  const topologyRouter = useRouter();
+  const { t } = useI18n();
+  const routeContextKey = React.useMemo(() => JSON.stringify(routeContext ?? {}), [routeContext]);
+  const [loadedApiGraph, setLoadedApiGraph] = React.useState<EntityTopologyApiGraph | null | undefined>(apiGraph);
+  const [topologyLayout] = React.useState<TopologyLayoutMode>('layered-service');
+  const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(undefined);
+  const apiOwnedEmptyGraph = React.useMemo(() => buildApiOwnedEmptyGraph(routeContext), [routeContext]);
+
+  React.useEffect(() => {
+    if (apiGraph !== undefined) {
+      setLoadedApiGraph(apiGraph);
+      return undefined;
+    }
+    let mounted = true;
+    void loadTopologyGraph(api.topology.request, routeContext ?? {}, { timeoutMs: resolveTopologyApiTimeoutMs(routeContext) })
+      .then(graph => {
+        if (mounted) setLoadedApiGraph(graph);
+      })
+      .catch(() => {
+        if (mounted) setLoadedApiGraph(null);
+      });
+    return () => {
+      mounted = false;
+    };
+  }, [apiGraph, apiOwnedEmptyGraph, routeContext, routeContextKey]);
+
+  const map = React.useMemo(
+    () => buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t),
+    [apiOwnedEmptyGraph, loadedApiGraph, routeContext, t]
+  );
+  const topologyEffectiveSearchQuery = topologySearchQuery ?? map.filterContext.search;
+  const topologyIsPending = loadedApiGraph === undefined;
+  const topologyTraceCallScope = map.filterContext.sourceKind === 'otlp-trace-call' || map.filterContext.viewMode === 'service-call';
+  const topologyTraceCallMissingEdges =
+    map.dataSource === 'api' &&
+    !topologyIsPending &&
+    loadedApiGraph !== null &&
+    map.filterContext.sourceKind === 'otlp-trace-call' &&
+    map.edges.length === 0;
+  const topologyCanvasNodes = React.useMemo(
+    () => (topologyTraceCallMissingEdges ? [] : map.nodes),
+    [map.nodes, topologyTraceCallMissingEdges]
+  );
+  const topologyCanvasEdges = React.useMemo(
+    () => (topologyTraceCallMissingEdges ? [] : map.edges),
+    [map.edges, topologyTraceCallMissingEdges]
+  );
+  const topologyTraceCallEdges = map.edges.filter(edge => edge.source === 'otlp-trace-call' || edge.relationshipType === 'trace-call');
+  const topologyTraceCallRedEdgeCount = topologyTraceCallEdges.filter(edge =>
+    edge.redMetrics.requestRatePerSecond != null &&
+    edge.redMetrics.errorRate != null &&
+    edge.redMetrics.latencyP95Ms != null
+  ).length;
+  const topologyTraceCallWindowEdgeCount = topologyTraceCallEdges.filter(edge =>
+    Boolean(edge.evidence.firstSeen && edge.evidence.lastSeen)
+  ).length;
+  const topologyTraceCallSampleEdgeCount = topologyTraceCallEdges.filter(edge =>
+    Boolean(edge.evidence.sampleTraceId || edge.evidence.sampleSpanId)
+  ).length;
+  const topologyTraceCallState =
+    topologyIsPending && topologyTraceCallScope
+      ? 'pending'
+      : loadedApiGraph === null && topologyTraceCallScope
+        ? 'degraded'
+        : topologyTraceCallMissingEdges
+          ? 'missing-edges'
+          : topologyTraceCallEdges.length > 0
+            ? 'ready'
+            : 'none';
+  const topologyTraceCallRedState =
+    topologyTraceCallState === 'pending' || topologyTraceCallState === 'degraded' || topologyTraceCallState === 'missing-edges'
+      ? topologyTraceCallState
+      : topologyTraceCallEdges.length === 0
+      ? topologyTraceCallMissingEdges
+        ? 'missing-edges'
+        : 'none'
+      : topologyTraceCallRedEdgeCount === topologyTraceCallEdges.length
+        ? 'ready'
+        : 'partial';
+  const topologyApiRequestPath = buildTopologyApiUrl(routeContext ?? {});
+  const topologyApiRequestTimeoutMs = resolveTopologyApiTimeoutMs(routeContext);
+  const topologyApiScopeRelationType = resolveTopologyRelationType(routeContext ?? {}) ?? 'all';
+  const topologyG6Layout = topologyLayout === 'force' ? 'force' : 'layered-service';
+  const topologyLayoutLabel =
+    topologyLayout === 'force'
+      ? t('topology.state.layout.force')
+      : topologyLayout === 'grid-table'
+        ? t('topology.state.layout.grid-table')
+        : t('topology.state.layout.layered-service');
+  const [topologyLocalSelection, setTopologyLocalSelection] = React.useState<{ nodeId?: string; edgeId?: string; source: TopologyLocalSelectionSource }>({ source: 'none' });
+  const topologyG6Graph = React.useMemo<HzTopologyG6GraphInput>(() => ({
+    nodes: topologyCanvasNodes.map(node => ({
+      id: node.id,
+      label: node.label,
+      entityType: node.entityType,
+      health: node.health,
+      tone: node.tone,
+      focus: node.focus,
+      source: node.source,
+      evidenceBadges: node.evidenceBadges,
+      redMetrics: node.redMetrics,
+      href: buildTopologyNodeSelectionHref(node, map, routeContext),
+      focusHref: buildTopologyNodeSelectionHref(node, map, routeContext, { depth: 1 }),
+      entityHref: node.links.entityHref
+    })),
+    edges: topologyCanvasEdges.map(edge => ({
+      id: edge.id,
+      from: edge.from,
+      to: edge.to,
+      label: edge.label,
+      relationshipType: edge.relationshipType,
+      source: edge.source,
+      tone: edge.tone,
+      focus: edge.focus,
+      selected: edge.selected,
+      evidenceBadges: edge.evidenceBadges,
+      redMetrics: edge.redMetrics,
+      href: edge.drilldownHref
+    }))
+  }), [map, routeContext, topologyCanvasEdges, topologyCanvasNodes]);
+  React.useEffect(() => {
+    setTopologyLocalSelection({ source: 'none' });
+  }, [routeContextKey]);
+  React.useEffect(() => {
+    setTopologySearchQuery(undefined);
+  }, [routeContextKey]);
+  const topologyRoutePrimaryNode = map.nodes.find(node => node.id === map.activeNodeId) ?? map.nodes.find(node => node.id === 'svc-checkout') ?? map.nodes[0];
+  const topologyLocalSelectedNode = topologyLocalSelection.nodeId ? findNode(map.nodes, topologyLocalSelection.nodeId) : undefined;
+  const primaryNode = topologyLocalSelectedNode ?? topologyRoutePrimaryNode;
+  const activeViewModeLabel = map.viewModes.find(mode => mode.active)?.label;
+  const activeSourceLabel = map.filterContext.sourceKind
+    ? map.sources.find(source => source.kind === map.filterContext.sourceKind)?.label
+    : undefined;
+  const activeSourceHref = map.filterContext.sourceKind
+    ? map.sources.find(source => source.kind === map.filterContext.sourceKind)?.href
+    : undefined;
+  const topologyG6GroupItemHrefs =
+    map.filterContext.groupBy === 'source-kind'
+      ? Object.fromEntries(map.sources.map(source => [source.kind, withTopologyGroupByHref(source.href, 'source-kind')]))
+      : {};
+  const topologyG6ResetParams = React.useMemo(() => {
+    const params = new URLSearchParams();
+    params.set('environment', map.filterContext.environment);
+    params.set('timeRange', map.filterContext.timeRange);
+    params.set('viewMode', map.filterContext.viewMode);
+    if (typeof map.apiDepth === 'number') params.set('depth', String(map.apiDepth));
+    appendTopologySelectionParam(params, 'start', routeContext?.start);
+    appendTopologySelectionParam(params, 'end', routeContext?.end);
+    appendTopologySelectionParam(params, 'refresh', routeContext?.refresh);
+    appendTopologySelectionParam(params, 'live', routeContext?.live);
+    appendTopologySelectionParam(params, 'tz', routeContext?.tz);
+    appendTopologySelectionParam(params, 'relationType', routeContext?.relationType);
+    appendTopologySelectionParam(params, 'hideInternal', routeContext?.hideInternal);
+    if (map.filterContext.sourceKind) params.set('sourceKind', map.filterContext.sourceKind);
+    if (map.filterContext.groupBy !== 'none') params.set('groupBy', map.filterContext.groupBy);
+    return params;
+  }, [
+    map.apiDepth,
+    map.filterContext.environment,
+    map.filterContext.groupBy,
+    map.filterContext.sourceKind,
+    map.filterContext.timeRange,
+    map.filterContext.viewMode,
+    routeContext?.end,
+    routeContext?.hideInternal,
+    routeContext?.live,
+    routeContext?.refresh,
+    routeContext?.relationType,
+    routeContext?.start,
+    routeContext?.tz
+  ]);
+  const topologyG6FilterResetParams = new URLSearchParams(topologyG6ResetParams);
+  topologyG6FilterResetParams.delete('groupBy');
+  const topologyG6ResetHref = `/topology?${topologyG6FilterResetParams.toString()}`;
+  const topologyFocusExitParams = new URLSearchParams();
+  topologyFocusExitParams.set('environment', map.filterContext.environment);
+  topologyFocusExitParams.set('timeRange', map.filterContext.timeRange);
+  topologyFocusExitParams.set('viewMode', map.filterContext.viewMode);
+  topologyFocusExitParams.set('depth', '2');
+  appendTopologySelectionParam(topologyFocusExitParams, 'start', routeContext?.start);
+  appendTopologySelectionParam(topologyFocusExitParams, 'end', routeContext?.end);
+  appendTopologySelectionParam(topologyFocusExitParams, 'refresh', routeContext?.refresh);
+  appendTopologySelectionParam(topologyFocusExitParams, 'live', routeContext?.live);
+  appendTopologySelectionParam(topologyFocusExitParams, 'tz', routeContext?.tz);
+  appendTopologySelectionParam(topologyFocusExitParams, 'relationType', routeContext?.relationType);
+  appendTopologySelectionParam(topologyFocusExitParams, 'hideInternal', routeContext?.hideInternal);
+  appendTopologySelectionParam(topologyFocusExitParams, 'edgeId', map.selectedEdgeId ?? routeContext?.edgeId);
+  if (map.filterContext.sourceKind) topologyFocusExitParams.set('sourceKind', map.filterContext.sourceKind);
+  if (map.filterContext.groupBy !== 'none') topologyFocusExitParams.set('groupBy', map.filterContext.groupBy);
+  const topologyFocusExitHref = `/topology?${topologyFocusExitParams.toString()}`;
+  const topologyG6GroupParams = new URLSearchParams(topologyG6ResetParams);
+  topologyG6GroupParams.set('groupBy', 'source-kind');
+  const topologyG6SearchParams = new URLSearchParams(topologyG6ResetParams);
+  if (topologyEffectiveSearchQuery) topologyG6SearchParams.set('search', topologyEffectiveSearchQuery);
+  if (map.filterContext.groupBy !== 'none') topologyG6SearchParams.set('groupBy', map.filterContext.groupBy);
+  const handleTopologyEnvironmentChange = React.useCallback(
+    (value: string) => {
+      navigateTopologyFocus(buildTopologyScopeHref(topologyG6ResetParams, { environment: value }), topologyRouter.push);
+    },
+    [topologyG6ResetParams, topologyRouter.push]
+  );
+  const handleTopologyDepthChange = React.useCallback(
+    (value: string) => {
+      navigateTopologyFocus(buildTopologyScopeHref(topologyG6ResetParams, { depth: value }), topologyRouter.push);
+    },
+    [topologyG6ResetParams, topologyRouter.push]
+  );
+  const handleTopologySourceKindChange = React.useCallback(
+    (value: string) => {
+      navigateTopologyFocus(buildTopologyScopeHref(topologyG6ResetParams, { sourceKind: value === 'all' ? undefined : value }), topologyRouter.push);
+    },
+    [topologyG6ResetParams, topologyRouter.push]
+  );
+  const handleTopologySearchChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
+    setTopologySearchQuery(event.currentTarget.value);
+  }, []);
+  const handleTopologyGroupByChange = React.useCallback(
+    (value: string) => {
+      navigateTopologyFocus(buildTopologyScopeHref(topologyG6ResetParams, { groupBy: value }), topologyRouter.push);
+    },
+    [topologyG6ResetParams, topologyRouter.push]
+  );
+  const handleTopologyResetScope = React.useCallback(
+    (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
+      event.preventDefault();
+      navigateTopologyFocus('/topology', topologyRouter.push);
+    },
+    [topologyRouter.push]
+  );
+  const topologyToolbarSummaryItems = [
+    topologyEffectiveSearchQuery || t('topology.all-entities'),
+    map.filterContext.environment,
+    map.filterContext.timeRange,
+    activeViewModeLabel,
+    activeSourceLabel
+  ].filter((item): item is string => Boolean(item));
+  const topologyFocusState = map.activeNodeId ?? (topologyEffectiveSearchQuery ? 'incoming-context' : 'all');
+  const routeFocusDepth = Number(routeContext?.depth);
+  const topologyFocusTrailDepth = Number.isFinite(routeFocusDepth) ? Math.max(0, Math.floor(routeFocusDepth)) : map.apiDepth ?? 2;
+  const topologyFocusTrailMode = primaryNode || topologyEffectiveSearchQuery || map.apiDepth === 1 ? 'focused' : 'overview';
+  const topologyFocusTrailEntityId = primaryNode?.id ?? topologyFocusState;
+  const topologyToolbarStateItems: HzTopologyToolbarStateItem[] = [
+    {
+      id: 'focus',
+      label: t('topology.state.focus'),
+      value: (primaryNode?.label ?? topologyEffectiveSearchQuery) || t('topology.all-entities'),
+      tone: primaryNode ? topologyNodeHealthTone(primaryNode) : 'neutral',
+      'data-topology-focus-state': topologyFocusState
+    },
+    {
+      id: 'depth',
+      label: t('topology.state.depth'),
+      value: formatTopologyDepth(map.apiDepth, t),
+      'data-topology-depth-state': map.apiDepth ?? 'none'
+    },
+    {
+      id: 'group',
+      label: t('topology.state.group'),
+      value: t(`topology.state.group.${map.filterContext.groupBy}`),
+      'data-topology-group-state': map.filterContext.groupBy
+    }
+  ];
+  const topologyApiNodeCount = loadedApiGraph?.nodes?.length ?? map.nodes.length;
+  const topologyHiddenCount = Math.max(0, topologyApiNodeCount - map.nodes.length);
+  const topologyGroupPanelItems = buildTopologyGroupPanelItems({
+    nodes: map.nodes,
+    edges: map.edges,
+    activeSourceLabel,
+    primaryNode,
+    hiddenCount: topologyHiddenCount,
+    t
+  });
+  const topologyFocusTrailCrumbs = [
+    {
+      id: 'all',
+      href: '/topology',
+      label: t('topology.all-entities'),
+      'data-topology-focus-crumb': 'all'
+    },
+    primaryNode
+      ? {
+          id: 'active-entity',
+          href: primaryNode.links.entityHref,
+          label: primaryNode.label,
+          value: formatTopologyDepth(map.apiDepth, t),
+          active: true,
+          'data-topology-focus-crumb': primaryNode.id
+        }
+      : {
+          id: 'active-entity',
+          href: '/topology',
+          label: topologyEffectiveSearchQuery || t('topology.all-entities'),
+          value: formatTopologyDepth(map.apiDepth, t),
+          active: true,
+          'data-topology-focus-crumb': topologyFocusState
+        }
+  ];
+  const topologyFocusTrailFilters = [
+    {
+      id: 'environment',
+      label: t('topology.focus-trail.filter.environment'),
+      value: map.filterContext.environment,
+      'data-topology-focus-filter-environment': map.filterContext.environment
+    },
+    {
+      id: 'time-range',
+      label: t('topology.focus-trail.filter.time-range'),
+      value: formatTimeRange(map.filterContext.timeRange, t),
+      'data-topology-focus-filter-time-range': map.filterContext.timeRange
+    },
+    {
+      id: 'source',
+      label: t('topology.focus-trail.filter.source'),
+      value: activeSourceLabel ?? t('topology.focus-trail.source.all'),
+      'data-topology-focus-filter-source': map.filterContext.sourceKind ?? 'all'
+    },
+    {
+      id: 'view',
+      label: t('topology.focus-trail.filter.view'),
+      value: activeViewModeLabel ?? map.filterContext.viewMode,
+      'data-topology-focus-filter-view': map.filterContext.viewMode
+    }
+  ];
+  const topologyMetricRows = React.useMemo(() => buildTopologyMetricRows(map.edges, map.nodes), [map.edges, map.nodes]);
+  const [topologyMetricWindowFilter, setTopologyMetricWindowFilter] = React.useState<HzTopologyMetricTableRenderWindowFilter>('all');
+  React.useEffect(() => {
+    setTopologyMetricWindowFilter('all');
+  }, [routeContextKey]);
+  const currentNodeNeighborEvidence = React.useMemo(
+    () => (primaryNode ? buildTopologyNodeNeighborEvidence(primaryNode, map.nodes, map.edges) : undefined),
+    [map.edges, map.nodes, primaryNode]
+  );
+  const [topologyG6HoveredNodeId, setTopologyG6HoveredNodeId] = React.useState<string | undefined>();
+  const [topologyG6HoveredEdgeId, setTopologyG6HoveredEdgeId] = React.useState<string | undefined>();
+  const [topologyG6HoverAnchor, setTopologyG6HoverAnchor] = React.useState<HzTopologyG6HoverAnchor | undefined>();
+  const [topologyCompanionCollapsedSections, setTopologyCompanionCollapsedSections] = React.useState<Record<string, boolean>>({
+    timeline: true
+  });
+  const handleTopologyCompanionCollapsedChange = React.useCallback((sectionId: string, collapsed: boolean) => {
+    setTopologyCompanionCollapsedSections(current => ({ ...current, [sectionId]: collapsed }));
+  }, []);
+  const topologyLocalSelectedEdge = topologyLocalSelection.edgeId ? findEdge(map.edges, topologyLocalSelection.edgeId) : undefined;
+  const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map);
+  const topologyCanvasSelectedEdgeId = topologyLocalSelection.nodeId
+    ? undefined
+    : topologyLocalSelectedEdge?.id ?? map.selectedEdgeId ?? topologyDetailEdge?.id;
+  const topologyMetricSelectedRowId = topologyLocalSelection.nodeId
+    ? undefined
+    : map.selectedEdgeId ?? topologyDetailEdge?.id ?? topologyMetricRows[0]?.id;
+  const topologySearchPriorityNodeId = React.useMemo(() => {
+    const query = map.filterContext.search.trim().toLowerCase();
+    if (!query) return undefined;
+    return topologyG6Graph.nodes.find(node => node.label.toLowerCase().includes(query) || node.id.toLowerCase().includes(query))?.id;
+  }, [map.filterContext.search, topologyG6Graph.nodes]);
+  const topologyRenderWindowPriorityNodeIds = React.useMemo(
+    () =>
+      [
+        topologySearchPriorityNodeId,
+        topologyTraceCallMissingEdges ? undefined : primaryNode?.id,
+        topologyDetailEdge?.from,
+        topologyDetailEdge?.to
+      ].filter((nodeId): nodeId is string => Boolean(nodeId)),
+    [primaryNode?.id, topologyDetailEdge?.from, topologyDetailEdge?.to, topologySearchPriorityNodeId, topologyTraceCallMissingEdges]
+  );
+  const topologyRenderWindowCompanion = React.useMemo(() => {
+    const strategy = buildHzTopologyG6LargeGraphStrategy(topologyG6Graph);
+    return buildHzTopologyG6RenderWindow(topologyG6Graph, strategy, { priorityNodeIds: topologyRenderWindowPriorityNodeIds });
+  }, [topologyG6Graph, topologyRenderWindowPriorityNodeIds]);
+  const topologyHoveredDetailEdge = topologyG6HoveredEdgeId ? findEdge(map.edges, topologyG6HoveredEdgeId) : undefined;
+  const topologyLiveHoverEdge = topologyHoveredDetailEdge;
+  const topologyInvestigationEdge = topologyHoveredDetailEdge ?? topologyDetailEdge;
+  const topologyHoverFromNode = topologyInvestigationEdge ? findNode(map.nodes, topologyInvestigationEdge.from) : undefined;
+  const topologyHoverToNode = topologyInvestigationEdge ? findNode(map.nodes, topologyInvestigationEdge.to) : undefined;
+  const topologyLiveHoverFromNode = topologyLiveHoverEdge ? findNode(map.nodes, topologyLiveHoverEdge.from) : undefined;
+  const topologyLiveHoverToNode = topologyLiveHoverEdge ? findNode(map.nodes, topologyLiveHoverEdge.to) : undefined;
+  const topologyCanvasDrawerMode: HzTopologyCanvasDrawerMode = topologyDetailEdge && primaryNode
+    ? 'node-edge'
+    : topologyDetailEdge
+      ? 'edge'
+      : primaryNode
+        ? 'node'
+        : 'none';
+  const topologyCompanionJumpActiveId = topologyDetailEdge ? 'edge-detail' : primaryNode ? 'current-node' : 'view-mode';
+  const topologyCompanionJumpItems = [
+    {
+      id: 'view-mode',
+      href: '#topology-companion-view-mode',
+      label: t('topology.companion.jump.view'),
+      active: topologyCompanionJumpActiveId === 'view-mode'
+    },
+    {
+      id: 'legend',
+      href: '#topology-companion-legend',
+      label: t('topology.companion.jump.legend'),
+      active: topologyCompanionJumpActiveId === 'legend'
+    },
+    {
+      id: 'edge-red',
+      href: '#topology-companion-edge-red',
+      label: t('topology.companion.jump.red'),
+      active: topologyCompanionJumpActiveId === 'edge-red'
+    },
+    ...(map.impactTimeline.length > 0
+      ? [
+          {
+            id: 'timeline',
+            href: '#topology-companion-timeline',
+            label: t('topology.companion.jump.timeline'),
+            active: topologyCompanionJumpActiveId === 'timeline'
+          }
+        ]
+      : []),
+    ...(topologyDetailEdge
+      ? [
+          {
+            id: 'edge-detail',
+            href: '#topology-companion-edge-detail',
+            label: t('topology.companion.jump.edge'),
+            active: topologyCompanionJumpActiveId === 'edge-detail'
+          }
+        ]
+      : []),
+    ...(primaryNode
+      ? [
+          {
+            id: 'current-node',
+            href: '#topology-companion-current-node',
+            label: t('topology.companion.jump.node'),
+            active: topologyCompanionJumpActiveId === 'current-node'
+          }
+        ]
+      : [])
+  ];
+  const topologyCanvasFocusDepth = formatTopologyCanvasFocusDepth(map.apiDepth);
+  const topologyEvidenceSources =
+    loadedApiGraph?.sourceKinds && loadedApiGraph.sourceKinds.length > 0
+      ? loadedApiGraph.sourceKinds
+      : map.filterContext.sourceKind
+        ? [map.filterContext.sourceKind]
+        : [];
+  const topologyLoadingEvidenceSources = ['api', 'greptime', 'trace', 'relation'];
+  const topologyTraceCallUnavailable = loadedApiGraph === null && topologyTraceCallScope;
+  const topologyShouldShowEmptyState =
+    map.dataSource === 'api' && !topologyIsPending && (map.nodes.length === 0 || topologyTraceCallMissingEdges);
+  const topologyEmptyStateKind = loadedApiGraph === null ? 'degraded' : topologyTraceCallMissingEdges ? 'filtered-empty' : 'api-empty';
+  const topologyEmptyStateDataKind = topologyTraceCallUnavailable
+    ? 'trace-call-degraded'
+    : topologyTraceCallMissingEdges
+      ? 'trace-call-missing'
+      : topologyEmptyStateKind;
+  const topologyEmptyStateEvidenceSources =
+    topologyTraceCallUnavailable
+      ? ['otlp-trace-call', 'greptime', 'trace', 'unavailable']
+      : topologyEmptyStateKind === 'degraded'
+      ? ['api', 'unavailable']
+      : topologyTraceCallMissingEdges
+        ? ['otlp-trace-call', 'greptime', 'trace']
+        : topologyEvidenceSources;
+  const topologyEmptyStateTitle = topologyTraceCallMissingEdges
+    ? t('topology.empty.trace-call.title')
+    : topologyTraceCallUnavailable
+      ? t('topology.degraded.trace-call.title')
+    : topologyEmptyStateKind === 'degraded'
+      ? t('topology.degraded.api.title')
+      : t('topology.empty.api.title');
+  const topologyEmptyStateCopy = topologyTraceCallMissingEdges
+    ? t('topology.empty.trace-call.copy')
+    : topologyTraceCallUnavailable
+      ? t('topology.degraded.trace-call.copy')
+    : topologyEmptyStateKind === 'degraded'
+      ? t('topology.degraded.api.copy')
+      : t('topology.empty.api.copy');
+  const topologyEmptyStateSourceLabel = topologyTraceCallMissingEdges
+    ? t('topology.empty.trace-call.source')
+    : topologyTraceCallUnavailable
+      ? t('topology.degraded.trace-call.source')
+    : topologyEmptyStateKind === 'degraded'
+      ? t('topology.degraded.api.source')
+      : 'API';
+  const topologyLegendSections = React.useMemo(
+    () => [
+      {
+        id: 'status',
+        label: t('topology.legend.status'),
+        items: [
+          {
+            id: 'healthy-node',
+            label: t('topology.legend.status.healthy'),
+            color: '#22c55e',
+            visualSource: 'hertzbeat-status-token' as const,
+            value: t('topology.legend.status.healthy-value')
+          },
+          {
+            id: 'warning-node',
+            label: t('topology.legend.status.warning'),
+            color: '#f59e0b',
+            visualSource: 'hertzbeat-status-token' as const,
+            value: t('topology.legend.status.warning-value')
+          },
+          {
+            id: 'critical-node',
+            label: t('topology.legend.status.critical'),
+            color: '#ef4444',
+            visualSource: 'hertzbeat-status-token' as const,
+            value: t('topology.legend.status.critical-value')
+          }
+        ]
+      },
+      {
+        id: 'interaction',
+        label: t('topology.legend.interaction'),
+        items: [
+          {
+            id: 'selected-node',
+            label: t('topology.legend.interaction.selected-node'),
+            color: '#e5edf8',
+            visualSource: 'hertzbeat-interaction-token' as const,
+            value: t('topology.legend.interaction.selected-node-value')
+          },
+          {
+            id: 'directional-edge',
+            label: t('topology.legend.interaction.directional-edge'),
+            color: '#94a3b8',
+            pattern: 'solid' as const,
+            visualSource: 'hertzbeat-edge-token' as const,
+            value: t('topology.legend.interaction.directional-edge-value')
+          },
+          {
+            id: 'dimmed-edge',
+            label: t('topology.legend.interaction.dimmed-edge'),
+            color: '#94a3b8',
+            pattern: 'muted' as const,
+            visualSource: 'hertzbeat-edge-token' as const,
+            value: t('topology.legend.interaction.dimmed-edge-value')
+          }
+        ]
+      }
+    ],
+    [t]
+  );
+  const topologyEdgeIds = React.useMemo(
+    () => new Set(map.edges.map(edge => edge.id)),
+    [map.edges]
+  );
+  const topologyNodeIds = React.useMemo(
+    () => new Set(map.nodes.map(node => node.id)),
+    [map.nodes]
+  );
+  const topologyNodeFocusHrefById = React.useMemo(
+    () => new Map(map.nodes.map(node => [node.id, buildTopologyNodeSelectionHref(node, map, routeContext, { depth: 1 })])),
+    [map, routeContext]
+  );
+  const handleTopologyG6NodeSelect = React.useCallback(
+    (nodeId: string) => {
+      if (!topologyNodeIds.has(nodeId)) return;
+      setTopologyG6HoveredNodeId(undefined);
+      setTopologyG6HoveredEdgeId(undefined);
+      setTopologyG6HoverAnchor(undefined);
+      setTopologyLocalSelection({ nodeId, edgeId: undefined, source: 'node-click' });
+    },
+    [topologyNodeIds]
+  );
+  const handleTopologyG6NodeFocus = React.useCallback(
+    (nodeId: string) => {
+      navigateTopologyFocus(topologyNodeFocusHrefById.get(nodeId), topologyRouter.push);
+    },
+    [topologyNodeFocusHrefById, topologyRouter.push]
+  );
+  const handleTopologyFocusExit = React.useCallback(
+    (event: React.MouseEvent<HTMLAnchorElement>) => {
+      event.preventDefault();
+      navigateTopologyFocus(topologyFocusExitHref, topologyRouter.push);
+    },
+    [topologyFocusExitHref, topologyRouter.push]
+  );
+  const handleTopologyG6EdgeSelect = React.useCallback(
+    (edgeId: string) => {
+      if (!topologyEdgeIds.has(edgeId)) return;
+      setTopologyG6HoveredNodeId(undefined);
+      setTopologyG6HoveredEdgeId(undefined);
+      setTopologyG6HoverAnchor(undefined);
+      setTopologyLocalSelection({ nodeId: undefined, edgeId, source: 'edge-click' });
+    },
+    [topologyEdgeIds]
+  );
+  const handleTopologyG6NodeHover = React.useCallback((nodeId: string, anchor?: HzTopologyG6HoverAnchor) => {
+    setTopologyG6HoveredNodeId(nodeId);
+    setTopologyG6HoveredEdgeId(undefined);
+    setTopologyG6HoverAnchor(anchor);
+  }, []);
+  const handleTopologyG6EdgeHover = React.useCallback((edgeId: string, anchor?: HzTopologyG6HoverAnchor) => {
+    setTopologyG6HoveredNodeId(undefined);
+    setTopologyG6HoveredEdgeId(edgeId);
+    setTopologyG6HoverAnchor(anchor);
+  }, []);
+  const clearTopologyG6Hover = React.useCallback(() => {
+    setTopologyG6HoveredNodeId(undefined);
+    setTopologyG6HoveredEdgeId(undefined);
+    setTopologyG6HoverAnchor(undefined);
+  }, []);
+  const handleTopologyG6HoverClear = React.useCallback(() => {
+    clearTopologyG6Hover();
+  }, [clearTopologyG6Hover]);
+  const handleTopologyMetricRowSelect = React.useCallback(
+    (row: HzTopologyMetricRow) => {
+      if (!topologyEdgeIds.has(row.id)) return;
+      clearTopologyG6Hover();
+      setTopologyLocalSelection({ nodeId: undefined, edgeId: row.id, source: 'table-row-click' });
+    },
+    [clearTopologyG6Hover, topologyEdgeIds]
+  );
+
+  return (
+    <HzTopologyWorkbenchFrame
+      as="main"
+      data-topology-route="hertzbeat-entity-topology"
+      data-topology-data-source={map.dataSource}
+      data-topology-incoming-context={map.filterContext.hasIncomingContext ? 'entity-filter' : 'none'}
+      data-topology-active-node-id={map.activeNodeId ?? 'none'}
+      data-topology-active-edge-id={map.selectedEdgeId ?? 'none'}
+      data-topology-selection-behavior="in-page-drawer"
+      data-topology-selection-source={topologyLocalSelection.source}
+      data-topology-selection-node-id={topologyLocalSelection.nodeId ?? 'none'}
+      data-topology-selection-edge-id={topologyLocalSelection.edgeId ?? 'none'}
+      data-topology-focus-navigation="explicit-soft-route"
+      data-topology-focus-navigation-owner="hertzbeat-ui-g6-focus-entry"
+      data-topology-focus-navigation-trigger="double-click-node"
+      data-topology-focus-route-preservation="previous-graph-until-api-resolves"
+      data-topology-focus-route-preservation-owner="hertzbeat-ui-g6-focus-entry"
+      data-topology-focus-route-preservation-source="loaded-api-graph-state"
+      data-topology-scope-navigation="explicit-soft-route"
+      data-topology-scope-navigation-owner="hertzbeat-ui-toolbar-scope-controls"
+      data-topology-scope-navigation-preservation="previous-graph-until-api-resolves"
+      data-topology-layout-navigation="in-page-g6-layout"
+      data-topology-layout-navigation-owner="hertzbeat-ui-toolbar-layout-control"
+      data-topology-layout-navigation-url-policy="preserve-current-url"
+      data-topology-search-navigation="in-page-g6-search"
+      data-topology-search-navigation-owner="hertzbeat-ui-toolbar-search-control"
+      data-topology-search-navigation-url-policy="preserve-current-url"
+      data-topology-active-view-mode={map.filterContext.viewMode}
+      data-topology-active-source-kind={map.filterContext.sourceKind ?? 'all'}
+      data-topology-trace-call-state={topologyTraceCallState}
+      data-topology-trace-call-proof-owner="hertzbeat-ui-workbench-frame"
+      data-topology-trace-call-edge-count={topologyTraceCallEdges.length}
+      data-topology-trace-call-red-edge-count={topologyTraceCallRedEdgeCount}
+      data-topology-trace-call-window-edge-count={topologyTraceCallWindowEdgeCount}
+      data-topology-trace-call-sample-edge-count={topologyTraceCallSampleEdgeCount}
+      data-topology-trace-call-red-state={topologyTraceCallRedState}
+      data-topology-api-depth={map.apiDepth ?? 'none'}
+      data-topology-api-scope-owner="hertzbeat-ui-workbench-frame"
+      data-topology-api-request-path={topologyApiRequestPath}
+      data-topology-api-scope-source-kind={map.filterContext.sourceKind ?? 'all'}
+      data-topology-api-scope-relation-type={topologyApiScopeRelationType}
+      data-topology-api-scope-view-mode={map.filterContext.viewMode}
+      data-topology-api-timeout-ms={topologyApiRequestTimeoutMs}
+      data-topology-workbench-frame-owner="hertzbeat-ui-workbench-frame"
+      data-topology-workbench-frame-boundary-owner="hertzbeat-ui-workbench-frame-boundary"
+      density="compact"
+      boundary="route"
+    >
+      <HzTopologyWorkbenchHeader
+        data-topology-workbench-header-owner="hertzbeat-ui-workbench-header"
+        density="operational-compact"
+        copyVisibility="assistive"
+        boundary="none"
+        eyebrow={t('topology.kicker')}
+        title={map.productIdentity}
+        scopeSlot={
+          <HzTopologyScopeBar
+            data-topology-scope-bar-owner="hertzbeat-ui-scope-bar"
+            data-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"
+            data-topology-refresh-action="refresh"
+            boundary="none"
+            items={[
+              {
+                id: 'environment',
+                value: map.filterContext.environment,
+                'data-topology-filter-environment': map.filterContext.environment
+              },
+              {
+                id: 'time-range',
+                value: formatTimeRange(map.filterContext.timeRange, t),
+                'data-topology-filter-time-range': map.filterContext.timeRange
+              }
+            ]}
+            actions={[
+              {
+                id: 'refresh',
+                label: t('topology.refresh'),
+                'data-topology-refresh-action': 'refresh'
+              }
+            ]}
+          />
+        }
+      />
+
+      <HzTopologyFocusTrail
+        data-topology-focus-trail="focused-context"
+        data-topology-focus-trail-owner="hertzbeat-ui-focus-trail"
+        data-topology-focus-trail-boundary-owner="hertzbeat-ui-focus-trail-boundary"
+        data-topology-focus-trail-compactness="graph-dock"
+        focusMode={topologyFocusTrailMode}
+        focusDepth={String(topologyFocusTrailDepth)}
+        focusEntityId={topologyFocusTrailEntityId}
+        label={t('topology.focus-trail.label')}
+        crumbs={topologyFocusTrailCrumbs}
+        filters={topologyFocusTrailFilters}
+        hiddenCountLabel={t('topology.focus-trail.hidden-count', { count: topologyHiddenCount })}
+        hiddenCountProps={{
+          'data-topology-focus-trail-hidden-count-owner': 'hertzbeat-ui-focus-trail-hidden-count'
+        } as React.HTMLAttributes<HTMLSpanElement>}
+        exitAction={{
+          href: topologyFocusExitHref,
+          label: t('topology.focus-trail.exit'),
+          onClick: handleTopologyFocusExit,
+          'data-topology-focus-trail-exit-owner': 'hertzbeat-ui-focus-trail-exit'
+        } as React.AnchorHTMLAttributes<HTMLAnchorElement> & { label: React.ReactNode }}
+        boundary="none"
+        density="graph-dock"
+      />
+
+      <HzTopologyToolbar
+        data-topology-controls="hertzbeat-topology-controls"
+        data-topology-controls-owner="hertzbeat-ui-toolbar"
+        data-topology-toolbar-boundary-owner="hertzbeat-ui-toolbar-boundary"
+        data-topology-toolbar-state-owner="hertzbeat-ui-toolbar-state"
+        data-topology-filter-summary="incoming-context"
+        density="graph-first"
+        boundary="none"
+        environmentLabel={t('topology.environment.aria')}
+        environmentValue={map.filterContext.environment}
+        environmentOptions={[
+          { value: 'all', label: t('topology.environment.all') },
+          { value: 'dev', label: 'dev' },
+          { value: 'prod', label: 'prod' }
+        ]}
+        searchLabel={t('topology.search.aria')}
+        searchPlaceholder={t('topology.search.placeholder')}
+        searchValue={topologyEffectiveSearchQuery}
+        sourceKindLabel={t('topology.focus-trail.filter.source')}
+        sourceKindValue={map.filterContext.sourceKind ?? 'all'}
+        sourceKindOptions={[
+          { value: 'all', label: t('topology.focus-trail.source.all') },
+          ...map.sources.map(source => ({ value: source.kind, label: source.label }))
+        ]}
+        depthLabel={t('topology.state.depth')}
+        depthValue={String(map.apiDepth ?? 2)}
+        depthOptions={[
+          { value: '1', label: t('topology.state.depth.one-hop') },
+          { value: '2', label: t('topology.state.depth.two-hop') },
+          { value: '3', label: t('topology.state.depth.n-hop', { depth: 3 }) }
+        ]}
+        groupByLabel={t('topology.state.group')}
+        groupByValue={map.filterContext.groupBy}
+        groupByOptions={[
+          { value: 'none', label: t('topology.state.group.none') },
+          { value: 'environment', label: t('topology.state.group.environment') },
+          { value: 'source-kind', label: t('topology.state.group.source-kind') },
+          { value: 'entity-type', label: t('topology.state.group.entity-type') }
+        ]}
+        resetLabel={t('topology.toolbar.reset')}
+        resetHref="/topology"
+        onEnvironmentChange={handleTopologyEnvironmentChange}
+        onSourceKindChange={handleTopologySourceKindChange}
+        onDepthChange={handleTopologyDepthChange}
+        onGroupByChange={handleTopologyGroupByChange}
+        onSearchChange={handleTopologySearchChange}
+        onReset={handleTopologyResetScope}
+        summaryLabel={t('topology.current-filter')}
+        summaryItems={topologyToolbarSummaryItems}
+        stateLabel={t('topology.state.label')}
+        stateItems={topologyToolbarStateItems}
+      />
+
+      <HzTopologyWorkbenchGrid data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid" layout="canvas-only">
+        <HzTopologyWorkbenchSlot
+          data-topology-canvas-slot-owner="hertzbeat-ui-workbench-slot"
+          kind="canvas"
+          surface="content"
+        >
+          <HzTopologyCanvas
+            data-topology-canvas="hertzbeat-topology-canvas"
+            data-topology-canvas-owner="hertzbeat-ui-canvas"
+            data-topology-canvas-interaction-owner="hertzbeat-ui-canvas-interaction"
+            data-topology-canvas-interaction-scope-owner="hertzbeat-ui-canvas-interaction-scope"
+            data-topology-canvas-boundary-owner="hertzbeat-ui-canvas-boundary"
+            layout={topologyLayout}
+            interactionMode="inspect"
+            interactionScope="hover-group"
+            hoverMode="neighbor-highlight"
+            drawerMode={topologyCanvasDrawerMode}
+            focusDepth={topologyCanvasFocusDepth}
+            minHeight="workbench"
+            boundary="none"
+          >
+          <HzTopologyCanvasAnnotation
+            data-topology-canvas-annotation-owner="hertzbeat-ui-canvas-annotation"
+            title={topologyLayoutLabel}
+            copy={`${formatTopologyDepth(map.apiDepth, t)} · ${formatTimeRange(map.filterContext.timeRange, t)}`}
+            visibility="assistive"
+          />
+          <HzTopologyG6Canvas
+            graph={topologyG6Graph}
+            selectedNodeId={topologyTraceCallMissingEdges ? undefined : primaryNode?.id}
+            selectedEdgeId={topologyCanvasSelectedEdgeId}
+            hoveredNodeId={topologyG6HoveredNodeId}
+            hoveredEdgeId={topologyG6HoveredEdgeId}
+            searchQuery={topologyEffectiveSearchQuery}
+            filterScope={{
+              environment: map.filterContext.environment,
+              sourceKind: map.filterContext.sourceKind ?? 'all',
+              groupBy: map.filterContext.groupBy,
+              searchQuery: topologyEffectiveSearchQuery
+            }}
+            filterControls={[
+              {
+                id: 'source-kind',
+                kind: 'source-kind',
+                label: t('topology.state.group.source-kind'),
+                value: map.filterContext.sourceKind ?? 'all',
+                href: activeSourceHref ?? topologyG6ResetHref,
+                active: Boolean(map.filterContext.sourceKind)
+              },
+              {
+                id: 'group-by',
+                kind: 'group-by',
+                label: t('topology.state.group'),
+                value: map.filterContext.groupBy,
+                href: `/topology?${topologyG6GroupParams.toString()}`,
+                active: map.filterContext.groupBy !== 'none'
+              },
+              {
+                id: 'search',
+                kind: 'search',
+                label: t('topology.state.focus'),
+                value: topologyEffectiveSearchQuery || t('topology.all-entities'),
+                href: `/topology?${topologyG6SearchParams.toString()}`
+              },
+              {
+                id: 'reset',
+                kind: 'reset',
+                label: t('topology.toolbar.reset'),
+                value: 'all',
+                href: topologyG6ResetHref
+              }
+            ]}
+            groupItemHrefs={topologyG6GroupItemHrefs}
+            legendSlot={
+                <HzTopologyLegend
+                  data-topology-g6-legend-owner="hertzbeat-ui-g6-legend-dock"
+                  data-topology-legend-owner="hertzbeat-ui-legend"
+                  data-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"
+                  title={t('topology.legend.title')}
+                  summaryLabel={t('topology.legend.summary')}
+                  sections={topologyLegendSections}
+                  boundary="flush"
+                  density="canvas-dock"
+                />
+              }
+            layout={topologyG6Layout}
+            height="workbench"
+            overlayMode="non-occluding"
+            summaryLabel={`${t('topology.group-panel.node-count', { count: map.nodes.length })} · ${t('topology.group-panel.edge-count', { count: map.edges.length })}`}
+            fitViewLabel={t('topology.view.fit')}
+            resetViewLabel={t('topology.toolbar.reset')}
+            zoomInLabel={t('topology.view.zoom-in')}
+            zoomOutLabel={t('topology.view.zoom-out')}
+            selectedFocusLabel={t('topology.view.focus-selected')}
+            onNodeSelect={handleTopologyG6NodeSelect}
+            onNodeFocus={handleTopologyG6NodeFocus}
+            onEdgeSelect={handleTopologyG6EdgeSelect}
+            onNodeHover={handleTopologyG6NodeHover}
+            onEdgeHover={handleTopologyG6EdgeHover}
+            onHoverClear={handleTopologyG6HoverClear}
+            data-topology-renderer="antv-g6"
+            data-topology-g6-canvas-owner="hertzbeat-ui-g6-canvas"
+            data-topology-g6-render-window-priority-contract="search-only-no-selection-reorder"
+            data-topology-g6-render-window-priority-owner="hertzbeat-ui-g6-render-window-priority"
+            data-topology-g6-mount-lifecycle-browser-regression="selection-preserve-viewport"
+            data-topology-g6-mount-lifecycle-browser-regression-owner="hertzbeat-ui-g6-mount-lifecycle"
+            data-topology-g6-mount-lifecycle-selection-policy="node-edge-table-drawer-only"
+            data-topology-g6-mount-lifecycle-url-policy="preserve-current-url-on-selection"
+            data-topology-g6-mount-lifecycle-redraw-policy="shared-setData-draw-preserve-viewport"
+          />
+
+          {map.dataSource === 'api' && map.nodes.length === 0 && topologyIsPending ? (
+            <HzTopologyLoadingState
+              data-topology-loading-state="api-pending"
+              data-topology-loading-state-owner="hertzbeat-ui-loading-state"
+              data-topology-loading-boundary-owner="hertzbeat-ui-loading-boundary"
+              title={t('topology.loading.api.title')}
+              copy={t('topology.loading.api.copy')}
+              sourceLabel="API"
+              timeScope={map.filterContext.timeRange}
+              environment={map.filterContext.environment}
+              sourceKind={map.filterContext.sourceKind ?? 'all'}
+              relationType={topologyApiScopeRelationType}
+              focusEntityId={routeContext?.entityId ?? 'none'}
+              depth={map.apiDepth}
+              evidenceSources={topologyLoadingEvidenceSources}
+              rows={3}
+              placement="canvas-center"
+              boundary="canvas"
+            />
+          ) : null}
+
+          {topologyShouldShowEmptyState ? (
+            <HzTopologyEmptyState
+              data-topology-empty-state={topologyEmptyStateDataKind}
+              data-topology-empty-state-owner="hertzbeat-ui-empty-state"
+              data-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"
+              title={topologyEmptyStateTitle}
+              copy={topologyEmptyStateCopy}
+              sourceLabel={topologyEmptyStateSourceLabel}
+              timeScope={map.filterContext.timeRange}
+              environment={map.filterContext.environment}
+              sourceKind={map.filterContext.sourceKind ?? (topologyEmptyStateEvidenceSources.length === 1 ? topologyEmptyStateEvidenceSources[0] : 'all')}
+              relationType={topologyApiScopeRelationType}
+              focusEntityId={routeContext?.entityId ?? 'none'}
+              depth={map.apiDepth}
+              resultCount={topologyTraceCallMissingEdges ? 0 : map.nodes.length}
+              evidenceSources={topologyEmptyStateEvidenceSources}
+              kind={topologyEmptyStateKind}
+              placement="canvas-center"
+              boundary="canvas"
+            />
+          ) : null}
+        {topologyLiveHoverEdge && topologyLiveHoverFromNode && topologyLiveHoverToNode ? (
+          <HzTopologyHoverTooltip
+            data-topology-hover-tooltip-owner="hertzbeat-ui-hover-tooltip"
+            data-topology-hover-edge={topologyLiveHoverEdge.id}
+            kind="edge"
+            title={topologyLiveHoverEdge.evidence.title}
+            summary={topologyLiveHoverEdge.evidence.sourceLabel}
+            visibility="hover"
+            trigger="live-edge-hover"
+            placement={topologyG6HoverAnchor ? 'canvas-anchor' : 'canvas-right-under-toolbar'}
+            anchor={topologyG6HoverAnchor}
+            size={topologyG6HoverAnchor ? 'compact' : 'standard'}
+            facts={buildTopologyHoverTooltipFacts(topologyLiveHoverEdge, topologyLiveHoverFromNode, topologyLiveHoverToNode, routeContext, t)}
+            metrics={buildTopologyHoverTooltipMetrics(topologyLiveHoverEdge, t)}
+            evidenceBadges={topologyLiveHoverEdge.evidenceBadges}
+          />
+        ) : null}
+          </HzTopologyCanvas>
+        </HzTopologyWorkbenchSlot>
+        <HzTopologyWorkbenchSlot
+          data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"
+          data-topology-companion-rail-visibility="hidden-from-layout"
+          kind="companion"
+          surface="content"
+          className="hidden"
+          aria-hidden="true"
+        >
+        <HzTopologyCompanionRail
+          data-topology-companion-rail-owner="hertzbeat-ui-companion-rail"
+          data-topology-companion-rail-boundary-owner="hertzbeat-ui-companion-rail-boundary"
+          data-topology-companion-rail-scope="legend-metrics-timeline-detail"
+          data-topology-companion-rail-priority="graph-first"
+          density="compact"
+          placement="side"
+          boundary="side"
+          priority="graph-first"
+          stickyContext="jump-list"
+        >
+          <HzTopologyCompanionJumpList
+            data-topology-companion-jump-list-owner="hertzbeat-ui-companion-jump-list"
+            ariaLabel={t('topology.companion.jump.aria')}
+            density="graph-first"
+            activeMode="contained-rail-scroll"
+            items={topologyCompanionJumpItems}
+          />
+          <HzTopologyCompanionSection
+            data-topology-companion-section="view-mode"
+            sectionId="view-mode"
+            anchorId="topology-companion-view-mode"
+            density="graph-first"
+          >
+            <HzTopologySectionLabel data-topology-view-mode-label-owner="hertzbeat-ui-section-label">
+              {t('topology.aside.view')}
+            </HzTopologySectionLabel>
+            <HzTopologyFilterStrip
+              data-topology-view-mode-strip-owner="hertzbeat-ui-filter-strip"
+              data-topology-view-mode-strip-boundary-owner="hertzbeat-ui-filter-strip-boundary"
+              variant="view-list"
+              boundary="none"
+              copyVisibility="assistive"
+              items={map.viewModes.map(mode => ({
+                id: mode.key,
+                href: mode.href,
+                label: mode.label,
+                copy: mode.copy,
+                active: mode.active,
+                'aria-current': mode.active ? 'page' : undefined,
+                'data-topology-view-mode': mode.key,
+                'data-topology-view-mode-active': mode.active ? mode.key : 'false'
+              }))}
+            />
+            <HzTopologyActionLink
+              data-topology-alert-impact-link="alert-center"
+              data-topology-alert-impact-link-owner="hertzbeat-ui-action-link"
+              data-topology-alert-impact-link-spacing-owner="hertzbeat-ui-action-link-spacing"
+              id="alert-impact"
+              href={map.alertImpactHref}
+              label={t('topology.alert-impact.open')}
+              copy={t('topology.alert-impact.copy')}
+              emphasis="primary"
+              spacing="none"
+            />
+          </HzTopologyCompanionSection>
+
+          <HzTopologyCompanionSection
+            data-topology-companion-section="legend"
+            sectionId="legend"
+            anchorId="topology-companion-legend"
+            density="graph-first"
+          >
+            <HzTopologyLegend
+              data-topology-legend-owner="hertzbeat-ui-legend"
+              data-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"
+              title={t('topology.legend.title')}
+              summaryLabel={t('topology.legend.summary')}
+              sections={topologyLegendSections}
+              boundary="framed"
+            />
+          </HzTopologyCompanionSection>
+
+          <HzTopologyCompanionSection
+            data-topology-companion-section="edge-red"
+            data-topology-companion-section-collapsible="edge-red"
+            sectionId="edge-red"
+            anchorId="topology-companion-edge-red"
+            density="graph-first"
+            collapsible
+            collapsed={topologyCompanionCollapsedSections['edge-red'] ?? false}
+            collapseLabel={t('topology.companion.section.collapse')}
+            expandLabel={t('topology.companion.section.expand')}
+            onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-red', collapsed)}
+          >
+            <HzTopologyMetricTable
+              data-topology-metric-table-owner="hertzbeat-ui"
+              data-topology-metric-table-scope="edge-red-companion"
+              data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
+              data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
+              data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
+              data-topology-metric-table-filter-behavior="in-page-no-route-change"
+              title={t('topology.metric-table.title')}
+              density="graph-first"
+              rows={topologyMetricRows}
+              selectedRowId={topologyMetricSelectedRowId}
+              renderWindowFilter={topologyMetricWindowFilter}
+              onRenderWindowFilterChange={setTopologyMetricWindowFilter}
+              renderWindowCompanion={{
+                mode: topologyRenderWindowCompanion.mode,
+                totalNodeCount: topologyRenderWindowCompanion.totalNodeCount,
+                renderedNodeCount: topologyRenderWindowCompanion.renderedNodeCount,
+                hiddenNodeCount: topologyRenderWindowCompanion.hiddenNodeCount,
+                visibleNodeBudget: topologyRenderWindowCompanion.visibleNodeBudget,
+                tableCompanion: topologyRenderWindowCompanion.tableCompanion,
+                priorityNodeIds: topologyRenderWindowCompanion.priorityNodeIds,
+                renderedNodeIds: topologyRenderWindowCompanion.graph.nodes.map(node => node.id)
+              }}
+              emptyLabel={t('topology.metric-table.empty')}
+              labels={{
+                edgeCount: t('topology.metric-table.edge-count', { count: topologyMetricRows.length }),
+                requestRate: t('topology.metric-table.request-rate-unit'),
+                errorRate: t('topology.metric-table.error-rate-unit'),
+                latencyP95: t('topology.metric-table.latency-p95-unit'),
+                rowAction: t('topology.metric-table.row-action'),
+                renderWindowFilterAll: t('topology.metric-table.filter.all'),
+                renderWindowFilterVisible: t('topology.metric-table.filter.visible'),
+                renderWindowFilterPartial: t('topology.metric-table.filter.partial'),
+                renderWindowFilterHidden: t('topology.metric-table.filter.hidden'),
+                renderWindowFilterUnknown: t('topology.metric-table.filter.unknown'),
+                rowAriaLabel: row => t('topology.metric-table.open-edge-aria', { edge: String(row.id) })
+              }}
+              onRowSelect={handleTopologyMetricRowSelect}
+              boundary="framed"
+            />
+          </HzTopologyCompanionSection>
+
+          {map.impactTimeline.length > 0 ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="timeline"
+              data-topology-companion-section-collapsible="timeline"
+              sectionId="timeline"
+              anchorId="topology-companion-timeline"
+              density="graph-first"
+              collapsible
+              collapsed={topologyCompanionCollapsedSections.timeline ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('timeline', collapsed)}
+            >
+              <HzTopologyEvidenceList
+                data-topology-impact-timeline="api-evidence"
+                data-topology-impact-timeline-owner="hertzbeat-ui-evidence-list"
+                data-topology-impact-timeline-boundary-owner="hertzbeat-ui-evidence-list-boundary"
+                kind="impact-timeline"
+                title={t('topology.timeline.title')}
+                copy={t('topology.timeline.copy')}
+                boundary="companion-timeline"
+                items={map.impactTimeline.map(event => ({
+                  id: event.id,
+                  label: event.sourceLabel,
+                  value: event.title,
+                  meta: `${event.detail} · ${event.occurredAt} · ${event.actor}`,
+                  tone: event.edgeId ? 'warning' : 'info',
+                  'data-topology-impact-timeline-event': event.id,
+                  'data-topology-impact-timeline-event-type': event.eventType,
+                  'data-topology-impact-timeline-source': event.sourceKind,
+                  'data-topology-impact-timeline-edge': event.edgeId ?? 'none'
+                }))}
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
+
+          {topologyDetailEdge ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="edge-detail"
+              data-topology-companion-section-collapsible="edge-detail"
+              sectionId="edge-detail"
+              anchorId="topology-companion-edge-detail"
+              density="graph-first"
+              collapsible
+              collapsed={topologyCompanionCollapsedSections['edge-detail'] ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-detail', collapsed)}
+            >
+              <HzTopologyDetailDrawer
+                data-topology-edge-evidence-panel={topologyDetailEdge.id}
+                data-topology-edge-evidence-window-owner="hertzbeat-ui-detail-drawer"
+                data-topology-edge-evidence-first-seen={topologyDetailEdge.evidence.firstSeen || 'none'}
+                data-topology-edge-evidence-last-seen={topologyDetailEdge.evidence.lastSeen || 'none'}
+                data-topology-edge-evidence-sample-trace-id={topologyDetailEdge.evidence.sampleTraceId || 'none'}
+                data-topology-edge-evidence-sample-span-id={topologyDetailEdge.evidence.sampleSpanId || 'none'}
+                data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"
+                data-topology-detail-drawer-surface-owner="hertzbeat-ui-detail-surface"
+                kind="edge"
+                density="graph-first"
+                subjectId={topologyDetailEdge.id}
+                sourceId={topologyDetailEdge.from}
+                targetId={topologyDetailEdge.to}
+                relationType={topologyDetailEdge.relationshipType}
+                sourceKind={topologyDetailEdge.source}
+                surface="framed"
+                eyebrow={t('topology.edge.evidence.title')}
+                title={topologyDetailEdge.evidence.title}
+                subtitle={`${topologyDetailEdge.evidence.sourceLabel} · ${topologyDetailEdge.evidence.collectedBy}`}
+                boundary={topologyDetailEdge.evidence.boundary}
+                boundaryProps={{
+                  'data-topology-edge-evidence-boundary': 'roadmap-boundary',
+                  'data-topology-detail-boundary-owner': 'hertzbeat-ui-detail-boundary'
+                } as React.HTMLAttributes<HTMLDivElement>}
+                facts={buildTopologyEdgeDetailFacts(topologyDetailEdge, t)}
+                actions={buildTopologyEdgeDetailActions(topologyDetailEdge, t)}
+                signalActions={buildTopologyEdgeSignalActions(topologyDetailEdge, t)}
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
+
+          {primaryNode ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="current-node"
+              data-topology-companion-section-collapsible="current-node"
+              sectionId="current-node"
+              anchorId="topology-companion-current-node"
+              density="graph-first"
+              collapsible
+              collapsed={topologyCompanionCollapsedSections['current-node'] ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('current-node', collapsed)}
+            >
+              <HzTopologyDetailDrawer
+                data-topology-current-entity-panel={primaryNode.id}
+                data-topology-current-entity-panel-owner="hertzbeat-ui-detail-drawer"
+                data-topology-current-entity-panel-surface-owner="hertzbeat-ui-detail-surface"
+                data-topology-current-entity-neighbor-owner="hertzbeat-ui-detail-neighbor-evidence"
+                data-topology-current-entity-upstream-count={currentNodeNeighborEvidence?.upstreamNodeIds.length ?? 0}
+                data-topology-current-entity-downstream-count={currentNodeNeighborEvidence?.downstreamNodeIds.length ?? 0}
+                data-topology-current-entity-upstream-node-ids={currentNodeNeighborEvidence?.upstreamNodeIds.join(' ') || 'none'}
+                data-topology-current-entity-downstream-node-ids={currentNodeNeighborEvidence?.downstreamNodeIds.join(' ') || 'none'}
+                data-topology-current-entity-group-by={map.filterContext.groupBy}
+                data-topology-current-entity-source-kind={map.filterContext.sourceKind ?? 'all'}
+                data-topology-current-entity-view-mode={map.filterContext.viewMode}
+                kind="node"
+                density="graph-first"
+                subjectId={primaryNode.id}
+                entityType={primaryNode.entityType}
+                sourceKind={primaryNode.source}
+                surface="framed"
+                eyebrow={t('topology.current-entity')}
+                title={primaryNode.label}
+                subtitle={`${primaryNode.entityId} · ${primaryNode.namespace} / ${primaryNode.environment}`}
+                boundary={t('topology.current-entity.boundary')}
+                boundaryProps={{
+                  'data-topology-current-entity-boundary-owner': 'hertzbeat-ui-detail-boundary'
+                } as React.HTMLAttributes<HTMLDivElement>}
+                facts={buildTopologyNodeDetailFacts(
+                  primaryNode,
+                  currentNodeNeighborEvidence ?? buildTopologyNodeNeighborEvidence(primaryNode, map.nodes, map.edges),
+                  t
+                )}
+                actions={buildTopologyNodeDetailActions(primaryNode, map.filterContext, map.apiDepth, t)}
+                signalActions={buildTopologyNodeSignalActions(primaryNode, map.filterContext, map.apiDepth, t)}
+                signalActionsLabel={t('topology.context-link.signals')}
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
+        </HzTopologyCompanionRail>
+        </HzTopologyWorkbenchSlot>
+      </HzTopologyWorkbenchGrid>
+
+      {map.faultContextRows.length > 0 ? (
+        <HzTopologyEvidenceList
+          data-topology-fault-context="incoming-evidence"
+          data-topology-fault-context-owner="hertzbeat-ui-evidence-list"
+          data-topology-fault-context-boundary-owner="hertzbeat-ui-evidence-list-boundary"
+          kind="fault-context"
+          title={t('topology.fault-context.title')}
+          copy={t('topology.fault-context.copy')}
+          boundary="toolbar-context"
+          items={map.faultContextRows.map(row => ({
+            id: row.label,
+            label: row.label,
+            value: row.value,
+            meta: row.meta,
+            tone: 'info',
+            'data-topology-fault-context-row': row.label
+          }))}
+        />
+      ) : null}
+
+      <HzTopologyGroupPanel
+        data-topology-group-panel="large-graph-grouping"
+        data-topology-group-panel-owner="hertzbeat-ui-group-panel"
+        data-topology-group-panel-boundary-owner="hertzbeat-ui-group-panel-boundary"
+        title={t('topology.group-panel.title')}
+        copy={t('topology.group-panel.copy')}
+        groupByLabel={t('topology.group-panel.group-by')}
+        boundary="framed"
+        items={topologyGroupPanelItems}
+        actions={[
+          { id: 'clear-group', href: '/topology', label: t('topology.group-panel.clear-group') },
+          { id: 'open-table', href: '#topology-metric-table', label: t('topology.group-panel.open-table') }
+        ]}
+      />
+
+      {topologyInvestigationEdge && topologyHoverFromNode && topologyHoverToNode ? (
+        <HzTopologyPathSummary
+          data-topology-path-summary="selected-edge-context"
+          data-topology-path-summary-owner="hertzbeat-ui-path-summary"
+          data-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"
+          title={t('topology.path-summary.title')}
+          boundary="section"
+          selectedEdgeId={topologyInvestigationEdge.id}
+          hoveredEdgeId={topologyInvestigationEdge.id}
+          sourceId={topologyHoverFromNode.id}
+          targetId={topologyHoverToNode.id}
+          relationType={topologyInvestigationEdge.relationshipType}
+          sourceKind={topologyInvestigationEdge.source}
+          source={{
+            label: t('topology.path-summary.source'),
+            value: topologyHoverFromNode.label,
+            meta: `${topologyHoverFromNode.entityType} - ${topologyHoverFromNode.namespace}`
+          }}
+          target={{
+            label: t('topology.path-summary.target'),
+            value: topologyHoverToNode.label,
+            meta: `${topologyHoverToNode.entityType} - ${topologyHoverToNode.namespace}`
+          }}
+          relation={{
+            label: t('topology.path-summary.relation'),
+            value: topologyInvestigationEdge.label,
+            meta: topologyInvestigationEdge.evidence.sourceLabel
+          }}
+          directionLabel={t('topology.path-summary.direction')}
+          metrics={buildTopologyPathSummaryMetrics(topologyInvestigationEdge, t)}
+          evidenceBadges={topologyInvestigationEdge.evidenceBadges}
+          actions={[
+            { id: 'focus-path', href: topologyInvestigationEdge.drilldownHref, label: t('topology.path-summary.focus-path') },
+            { id: 'open-trace', href: topologyInvestigationEdge.links.tracesHref, label: t('topology.path-summary.open-trace') }
+          ]}
+        />
+      ) : null}
+    </HzTopologyWorkbenchFrame>
+  );
+}
diff --git a/web-next/components/pages/entity-definition-workspace-surface.test.tsx b/web-next/components/pages/entity-definition-workspace-surface.test.tsx
index 31e28567f4..0665846e38 100644
--- a/web-next/components/pages/entity-definition-workspace-surface.test.tsx
+++ b/web-next/components/pages/entity-definition-workspace-surface.test.tsx
@@ -8,7 +8,68 @@ import { getImportStarterDraft } from './entity-definition-workspace-surface';
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock()
+    t: createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'entities.definition.workspace.kicker': '实体定义',
+        'entities.definition.workspace.title': '编辑实体定义',
+        'entities.definition.workspace.subtitle': '直接编辑当前定义。保存后会同步更新基础信息、遥测绑定和依赖。',
+        'entities.definition.workspace.format-help': '支持 YAML、JSON 和 cURL 请求内容,保存前可先预览解析结果。',
+        'entities.definition.workspace.attribution-check': '归因检查',
+        'entities.definition.workspace.attribution-state.ready': '已就绪',
+        'entities.definition.workspace.attribution-state.missing': '需补齐',
+        'entities.definition.workspace.attribution-state.unknown': '待确认',
+        'entities.definition.workspace.metric.ready': '可直接导入',
+        'entities.definition.workspace.metric.attention': '待完善',
+        'entities.definition.workspace.metric.telemetry-gaps': '缺少遥测绑定',
+        'entities.definition.workspace.current-definition': '当前定义',
+        'entities.definition.workspace.template-title': '自定义模板',
+        'entities.definition.workspace.template-error-help': '定义加载失败后可先确认实体是否存在,再继续编辑。',
+        'entities.definition.workspace.template-empty-title': '暂无模板',
+        'entities.definition.workspace.template-empty-copy': '可复用模板会显示在这里。',
+        'entities.definition.workspace.batch-title': '模板与批量编辑',
+        'entities.definition.workspace.batch-copy': '查看模板、草稿来源和可选操作。',
+        'entities.definition.workspace.batch-expand': '展开',
+        'entities.definition.workspace.activity-title': '最近活动',
+        'entities.definition.workspace.activity-empty-title': '暂无最近活动',
+        'entities.definition.workspace.activity-empty-copy': '预览和导入活动会显示在这里。',
+        'entities.definition.workspace.entry-title': '入口',
+        'entities.definition.workspace.entry-view-entity': '查看实体',
+        'entities.definition.workspace.entry-back-to-form': '回到表单',
+        'entities.definition.message.load-failed': '加载实体定义失败。',
+        'entities.definition.message.entity-not-exist': '实体不存在。',
+        'entities.definition.message.process-failed': '实体定义处理失败。',
+        'entities.definition.message.update-missing-entity': '无法确认要更新的实体定义。',
+        'entities.definition.message.update-failed': '实体定义保存失败。',
+        'entities.definition.message.updated': '实体定义已更新。',
+        'entities.definition.message.backend-fallback': '后端返回:{{message}}',
+        'entity.definition.import.empty-preview': '请先粘贴定义,再预览。',
+        'entity.definition.import.empty-save': '请先粘贴或加载定义,再保存。',
+        'entity.definition.import.empty-import': '请先粘贴定义,再导入。',
+        'entity.definition.import.activity.preview-one': '已预览 1 个定义。',
+        'entity.definition.import.activity.preview-many': '已预览 {{count}} 个定义。',
+        'entity.definition.import.parse-failed': '定义解析失败。',
+        'entity.definition.workspace.single-required': '编辑已有实体时一次只能保存一个定义文档。',
+        'entity.definition.import.blocked': '部分定义仍需处理,请检查后再导入。',
+        'entity.definition.import.success-bundle': '已导入 {{count}} 个实体。',
+        'entity.definition.import.success-single': '实体定义已导入。',
+        'entity.definition.import.create-failed': '实体导入失败。',
+        'entity.definition.import.placeholder.yaml': '粘贴 YAML 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。',
+        'entity.definition.import.placeholder.json': '粘贴 JSON 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。',
+        'entity.definition.import.placeholder.curl': '粘贴从预览或自动化流程生成的 cURL 请求。',
+        'entity.definition.format.label': '导入格式',
+        'entity.definition.format.option.yaml': 'YAML 格式',
+        'entity.definition.format.option.json': 'JSON 格式',
+        'entity.definition.format.option.curl': 'cURL 格式',
+        'entity.definition.action.clear-draft': '清空草稿',
+        'entity.definition.action.preview': '预览定义',
+        'entity.definition.action.save': '保存定义',
+        'entity.definition.action.import': '导入实体',
+        'entities.import.workspace.title': '导入实体定义',
+        'entities.import.workspace.subtitle': '从 YAML、JSON 或 cURL 请求内容导入一个或多个实体定义,先预览再写入目录。',
+        'entities.import.workspace.format-help': '支持 YAML、JSON 和从预览或自动化流程生成的 cURL。导入前会先解析实体基础信息、遥测绑定和依赖关系。'
+      }
+    })
   })
 }));
 
@@ -128,13 +189,150 @@ describe('EntityDefinitionWorkspaceSurface', () => {
     expect(source).toContain('data-entity-import-attribution-preview="definition-attribution-check"');
     expect(source).toContain('data-entity-import-attribution-row={attribute.key}');
     expect(source).toContain('data-entity-import-attribution-state={attribute.state}');
-    expect(source).toContain('归因检查');
+    expect(source).toContain("t('entities.definition.workspace.attribution-check')");
+    expect(source).toContain("t('entities.definition.workspace.attribution-state.ready')");
+    expect(source).toContain("t('entities.definition.workspace.attribution-state.missing')");
+    expect(source).toContain("t('entities.definition.workspace.attribution-state.unknown')");
+    expect(source).not.toContain('>归因检查<');
+    expect(source).not.toContain("return '已就绪'");
+    expect(source).not.toContain("return '需补齐'");
+    expect(source).not.toContain("return '待确认'");
     expect(source).toContain('row.attributionLabel');
     expect(source).toContain('row.attributionRows.map');
+    expect(source).toContain("t('entities.definition.workspace.metric.ready')");
+    expect(source).toContain("t('entities.definition.workspace.metric.attention')");
+    expect(source).toContain("t('entities.definition.workspace.metric.telemetry-gaps')");
+    expect(source).toContain("t('entities.definition.workspace.current-definition')");
+    expect(source).toContain("t('entities.definition.workspace.template-title')");
+    expect(source).toContain("t('entities.definition.workspace.template-error-help')");
+    expect(source).toContain("t('entities.definition.workspace.template-empty-title')");
+    expect(source).toContain("t('entities.definition.workspace.template-empty-copy')");
+    expect(source).toContain("t('entities.definition.workspace.batch-title')");
+    expect(source).toContain("t('entities.definition.workspace.batch-copy')");
+    expect(source).toContain("t('entities.definition.workspace.batch-expand')");
+    expect(source).toContain("t('entities.definition.workspace.activity-title')");
+    expect(source).toContain("t('entities.definition.workspace.activity-empty-title')");
+    expect(source).toContain("t('entities.definition.workspace.activity-empty-copy')");
+    expect(source).toContain("t('entities.definition.workspace.entry-title')");
+    expect(source).toContain("t('entities.definition.workspace.entry-view-entity')");
+    expect(source).toContain("t('entities.definition.workspace.entry-back-to-form')");
+    expect(source).toContain("t('entities.definition.message.load-failed')");
+    expect(source).toContain("t('entities.definition.message.entity-not-exist')");
+    expect(source).toContain("t('entities.definition.message.process-failed')");
+    expect(source).toContain("t('entities.definition.message.update-missing-entity')");
+    expect(source).toContain("t('entities.definition.message.update-failed')");
+    expect(source).toContain("t('entities.definition.message.updated')");
+    expect(source).toContain("t('entity.definition.import.empty-preview')");
+    expect(source).toContain("t('entity.definition.import.empty-save')");
+    expect(source).toContain("t('entity.definition.import.empty-import')");
+    expect(source).toContain("t('entity.definition.import.activity.preview-one')");
+    expect(source).toContain("t('entity.definition.import.activity.preview-many', { count: rows.length })");
+    expect(source).toContain("t('entity.definition.import.parse-failed')");
+    expect(source).toContain("t('entity.definition.workspace.single-required')");
+    expect(source).toContain("t('entity.definition.import.blocked')");
+    expect(source).toContain("t('entity.definition.import.success-bundle', { count: entityIds.length })");
+    expect(source).toContain("t('entity.definition.import.success-single')");
+    expect(source).toContain("t('entity.definition.import.create-failed')");
+    expect(source).toContain("t('entity.definition.import.placeholder.yaml')");
+    expect(source).toContain("t('entity.definition.import.placeholder.json')");
+    expect(source).toContain("t('entity.definition.import.placeholder.curl')");
+    expect(source).toContain('localizeEntityDefinitionMessage(error.message, t)');
+    expect(source).not.toContain("label: '可直接导入'");
+    expect(source).not.toContain("label: '待完善'");
+    expect(source).not.toContain("label: '缺少遥测绑定'");
+    expect(source).not.toContain('>当前定义<');
+    expect(source).not.toContain('>自定义模板<');
+    expect(source).not.toContain('定义加载失败后可先确认实体是否存在,再继续编辑。');
+    expect(source).not.toContain("title: '暂无模板'");
+    expect(source).not.toContain("copy: '可复用模板会显示在这里。'");
+    expect(source).not.toContain('>模板与批量编辑<');
+    expect(source).not.toContain('查看模板、草稿来源和可选操作。');
+    expect(source).not.toContain('>展开<');
+    expect(source).not.toContain('>最近活动<');
+    expect(source).not.toContain("title: '暂无最近活动'");
+    expect(source).not.toContain("copy: '预览和导入活动会显示在这里。'");
+    expect(source).not.toContain('>入口<');
+    expect(source).not.toContain('>查看实体<');
+    expect(source).not.toContain('>回到表单<');
+    expect(source).not.toContain('加载实体定义失败。');
+    expect(source).not.toContain('请先粘贴定义,再预览。');
+    expect(source).not.toContain('已预览 1 个定义。');
+    expect(source).not.toContain('定义解析失败。');
+    expect(source).not.toContain('请先粘贴或加载定义,再保存。');
+    expect(source).not.toContain('请先粘贴定义,再导入。');
+    expect(source).not.toContain('编辑已有实体时一次只能保存一个定义文档。');
+    expect(source).not.toContain('无法确认要更新的实体定义。');
+    expect(source).not.toContain('实体定义已更新。');
+    expect(source).not.toContain('实体定义保存失败。');
+    expect(source).not.toContain('部分定义仍需处理,请检查后再导入。');
+    expect(source).not.toContain('实体定义已导入。');
+    expect(source).not.toContain('实体导入失败。');
+    expect(source).not.toContain('实体不存在。');
+    expect(source).not.toContain('实体定义处理失败。');
+    expect(source).not.toContain('粘贴 YAML 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。');
+    expect(source).not.toContain('粘贴 JSON 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。');
+    expect(source).not.toContain('粘贴从预览或自动化流程生成的 cURL 请求。');
     expect(source).not.toContain('rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] p-3.5 text-left text-[var(--ops-text-secondary)] transition-colors hover:border-[var(--ops-primary)] hover:bg-[var(--ops-surface-hover)] hover:text-[var(--ops-text-primary)]');
     expect(source).not.toContain('rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] p-3.5');
   });
 
+  it('uses localized empty metadata for entity definition empty template and activity rows', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/entity-definition-workspace-surface.tsx'), 'utf8');
+
+    expect(source).toContain("const emptyValue = t('common.none');");
+    expect(source).toContain("t('entities.definition.workspace.template-empty-title')");
+    expect(source).toContain("t('entities.definition.workspace.activity-empty-title')");
+    expect(source).toContain('meta: emptyValue');
+    expect(source).not.toContain("meta: '-'");
+  });
+
+  it('renders entity definition format select labels from runtime messages', async () => {
+    const { EntityDefinitionWorkspaceSurface } = await import('./entity-definition-workspace-surface');
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/entity-definition-workspace-surface.tsx'), 'utf8');
+
+    const html = renderToStaticMarkup(
+      <EntityDefinitionWorkspaceSurface
+        mode="definition"
+        entityId="1"
+        initialContent="kind: service"
+        templates={[]}
+        activities={[]}
+      />
+    );
+
+    expect(source).toContain("t('entity.definition.format.option.yaml')");
+    expect(source).toContain("t('entity.definition.format.option.json')");
+    expect(source).toContain("t('entity.definition.format.option.curl')");
+    expect(source).not.toContain('<option value="yaml">YAML</option>');
+    expect(source).not.toContain('<option value="json">JSON</option>');
+    expect(source).not.toContain('<option value="curl">cURL</option>');
+    expect(html).toContain('YAML 格式');
+    expect(html).toContain('JSON 格式');
+    expect(html).toContain('cURL 格式');
+  });
+
+  it('localizes unknown backend feedback while preserving the raw server message', async () => {
+    const { EntityDefinitionWorkspaceSurface } = await import('./entity-definition-workspace-surface');
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/entity-definition-workspace-surface.tsx'), 'utf8');
+
+    const html = renderToStaticMarkup(
+      <EntityDefinitionWorkspaceSurface
+        mode="definition"
+        entityId="1"
+        initialContent=""
+        initialMessage="Backend parser saw custom-field drift."
+        templates={[]}
+        activities={[]}
+      />
+    );
+
+    expect(source).toContain("t('entities.definition.message.backend-fallback', { message: normalized })");
+    expect(source).not.toContain('default:\\n      return message;');
+    expect(html).toContain('data-entity-definition-load-error="cold-inline"');
+    expect(html).toContain('后端返回:Backend parser saw custom-field drift.');
+    expect(html).toContain('定义加载失败后可先确认实体是否存在,再继续编辑。');
+  });
+
   it('renders the shared edit-definition workspace with the cold full-width workbench contract', async () => {
     const { EntityDefinitionWorkspaceSurface } = await import('./entity-definition-workspace-surface');
 
diff --git a/web-next/components/pages/entity-definition-workspace-surface.tsx b/web-next/components/pages/entity-definition-workspace-surface.tsx
index 66128941a9..608355156e 100644
--- a/web-next/components/pages/entity-definition-workspace-surface.tsx
+++ b/web-next/components/pages/entity-definition-workspace-surface.tsx
@@ -47,14 +47,14 @@ function resolveDefinitionEditorLanguage(format: EntityDefinitionFormat): ColdCo
   }
 }
 
-function attributionStateLabel(state: string) {
+function attributionStateLabel(state: string, t: (key: string) => string) {
   if (state === 'ready') {
-    return '已就绪';
+    return t('entities.definition.workspace.attribution-state.ready');
   }
   if (state === 'missing') {
-    return '需补齐';
+    return t('entities.definition.workspace.attribution-state.missing');
   }
-  return '待确认';
+  return t('entities.definition.workspace.attribution-state.unknown');
 }
 
 function attributionStateClassName(state: string) {
@@ -77,6 +77,7 @@ export function EntityDefinitionWorkspaceSurface({
   initialMessage = null
 }: EntityDefinitionWorkspaceSurfaceProps) {
   const { t } = useI18n();
+  const emptyValue = t('common.none');
   const initialDraftContent = mode === 'import' && initialContent.trim() === '' ? getImportStarterDraft(initialFormat) : initialContent;
   const [content, setContent] = useState(initialDraftContent);
   const [format, setFormat] = useState<EntityDefinitionFormat>(initialFormat);
@@ -101,18 +102,13 @@ export function EntityDefinitionWorkspaceSurface({
     } catch (error) {
       setMessage(
         error instanceof Error
-          ? localizeEntityDefinitionMessage(error.message)
-          : tr('entities.definition.message.load-failed', '加载实体定义失败。')
+          ? localizeEntityDefinitionMessage(error.message, t)
+          : t('entities.definition.message.load-failed')
       );
       setMessageTone('error');
     }
   }
 
-  function tr(key: string, fallback: string, params?: Record<string, string | number | null | undefined>) {
-    const translated = t(key, params);
-    return translated === key ? fallback : translated;
-  }
-
   function clearPreviewState() {
     setPreviewRows([]);
     setPreviewScope('all');
@@ -127,7 +123,7 @@ export function EntityDefinitionWorkspaceSurface({
     const nextContent = content.trim();
     if (nextContent === '') {
       clearPreviewState();
-      setMessage(tr('entity.definition.import.empty', '请先粘贴定义,再预览。'));
+      setMessage(t('entity.definition.import.empty-preview'));
       setMessageTone('error');
       return;
     }
@@ -141,14 +137,13 @@ export function EntityDefinitionWorkspaceSurface({
       setPreviewRows(rows);
       setPreviewScope('all');
       setMessage(
-        tr(
-          'entity.definition.import.activity.preview',
-          rows.length === 1 ? '已预览 1 个定义。' : `已预览 ${rows.length} 个定义。`
-        )
+        rows.length === 1
+          ? t('entity.definition.import.activity.preview-one')
+          : t('entity.definition.import.activity.preview-many', { count: rows.length })
       );
       setMessageTone('success');
     } catch (error) {
-      setMessage(error instanceof Error ? localizeEntityDefinitionMessage(error.message) : tr('entity.definition.import.parse-failed', '定义解析失败。'));
+      setMessage(error instanceof Error ? localizeEntityDefinitionMessage(error.message, t) : t('entity.definition.import.parse-failed'));
       setMessageTone('error');
     } finally {
       setParsing(false);
@@ -159,10 +154,9 @@ export function EntityDefinitionWorkspaceSurface({
     const nextContent = content.trim();
     if (nextContent === '') {
       setMessage(
-        tr(
-          'entity.definition.import.empty',
-          mode === 'definition' ? '请先粘贴或加载定义,再保存。' : '请先粘贴定义,再导入。'
-        )
+        mode === 'definition'
+          ? t('entity.definition.import.empty-save')
+          : t('entity.definition.import.empty-import')
       );
       setMessageTone('error');
       return;
@@ -176,18 +170,13 @@ export function EntityDefinitionWorkspaceSurface({
     if (mode === 'definition') {
       if (previewRows.length > 1) {
         setPreviewScope('all');
-        setMessage(
-          tr(
-            'entity.definition.workspace.single-required',
-            '编辑已有实体时一次只能保存一个定义文档。'
-          )
-        );
+        setMessage(t('entity.definition.workspace.single-required'));
         setMessageTone('error');
         return;
       }
 
       if (entityId == null) {
-        setMessage(tr('entities.definition.message.update-failed', '无法确认要更新的实体定义。'));
+        setMessage(t('entities.definition.message.update-missing-entity'));
         setMessageTone('error');
         return;
       }
@@ -197,10 +186,10 @@ export function EntityDefinitionWorkspaceSurface({
 
       try {
         await apiMessagePut<void>(`/entities/${entityId}/definition`, updateDefinitionPayload(nextContent, format));
-        setMessage(tr('entities.definition.message.updated', '实体定义已更新。'));
+        setMessage(t('entities.definition.message.updated'));
         setMessageTone('success');
       } catch (error) {
-        setMessage(error instanceof Error ? localizeEntityDefinitionMessage(error.message) : tr('entities.definition.message.update-failed', '实体定义保存失败。'));
+        setMessage(error instanceof Error ? localizeEntityDefinitionMessage(error.message, t) : t('entities.definition.message.update-failed'));
         setMessageTone('error');
       } finally {
         setSubmitting(false);
@@ -210,12 +199,7 @@ export function EntityDefinitionWorkspaceSurface({
 
     if (previewRows.some(row => row.gaps.length > 0)) {
       setPreviewScope('attention');
-      setMessage(
-        tr(
-          'entity.definition.import.blocked',
-          '部分定义仍需处理,请检查后再导入。'
-        )
-      );
+      setMessage(t('entity.definition.import.blocked'));
       setMessageTone('error');
       return;
     }
@@ -227,12 +211,12 @@ export function EntityDefinitionWorkspaceSurface({
       const entityIds = await createDefinitionBundle(apiMessagePost, nextContent, format);
       setMessage(
         entityIds.length > 1
-          ? tr('entity.definition.import.success-bundle', `已导入 ${entityIds.length} 个实体。`)
-          : tr('entity.definition.import.success-single', '实体定义已导入。')
+          ? t('entity.definition.import.success-bundle', { count: entityIds.length })
+          : t('entity.definition.import.success-single')
       );
       setMessageTone('success');
     } catch (error) {
-      setMessage(error instanceof Error ? localizeEntityDefinitionMessage(error.message) : tr('entity.definition.import.create-failed', '实体导入失败。'));
+      setMessage(error instanceof Error ? localizeEntityDefinitionMessage(error.message, t) : t('entity.definition.import.create-failed'));
       setMessageTone('error');
     } finally {
       setSubmitting(false);
@@ -261,18 +245,12 @@ export function EntityDefinitionWorkspaceSurface({
     resetStatus();
   }
 
-  const clearDraftCopy =
-    mode === 'definition'
-      ? '清空草稿'
-      : '清空草稿';
-  const previewDefinitionsCopy =
-    mode === 'definition'
-      ? '预览定义'
-      : '预览定义';
+  const clearDraftCopy = t('entity.definition.action.clear-draft');
+  const previewDefinitionsCopy = t('entity.definition.action.preview');
   const submitDefinitionsCopy =
     mode === 'definition'
-      ? '保存定义'
-      : '导入实体';
+      ? t('entity.definition.action.save')
+      : t('entity.definition.action.import');
 
   function renderActionControls() {
     return (
@@ -290,10 +268,10 @@ export function EntityDefinitionWorkspaceSurface({
         {clearDraftCopy}
       </Button>
       <Button size="sm" variant="default" onClick={() => void previewDefinitions()} disabled={parsing || submitting}>
-        {parsing ? tr('common.loading', '加载中...') : previewDefinitionsCopy}
+        {parsing ? t('common.loading') : previewDefinitionsCopy}
       </Button>
       <Button size="sm" variant="primary" onClick={() => void submitDefinitions()} disabled={parsing || submitting}>
-        {submitting ? tr('common.saving', '保存中...') : submitDefinitionsCopy}
+        {submitting ? t('common.saving') : submitDefinitionsCopy}
       </Button>
       </>
     );
@@ -309,7 +287,7 @@ export function EntityDefinitionWorkspaceSurface({
         data-entity-import-attribution-preview="definition-attribution-check"
         className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#101217] p-3"
       >
-        <div className="text-[12px] font-semibold text-[#f5f7fb]">归因检查</div>
+        <div className="text-[12px] font-semibold text-[#f5f7fb]">{t('entities.definition.workspace.attribution-check')}</div>
         <div className="grid gap-2">
           {visiblePreviewRows.map(row => (
             <div key={row.key} className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-2">
@@ -333,7 +311,7 @@ export function EntityDefinitionWorkspaceSurface({
                     <span className="font-semibold text-[#dbe4f0]">{attribute.title}</span>
                     <span className="min-w-0 truncate text-[#98a2b3]">{attribute.copy}</span>
                     <span className={cn('rounded-[3px] border px-1.5 py-0.5 font-semibold', attributionStateClassName(attribute.state))}>
-                      {attributionStateLabel(attribute.state)}
+                      {attributionStateLabel(attribute.state, t)}
                     </span>
                   </div>
                 ))}
@@ -347,13 +325,15 @@ export function EntityDefinitionWorkspaceSurface({
 
   if (mode === 'import') {
     const visual = coldOpsCatalogVisual;
+    const importSubtitle = t('entities.import.workspace.subtitle');
+    const importFormatHelp = t('entities.import.workspace.format-help');
     const readyCount = previewRows.filter(row => row.gaps.length === 0).length;
     const attentionCount = previewRows.filter(row => row.gaps.length > 0).length;
     const telemetryGapCount = previewRows.filter(row => row.gapKeys.includes('telemetry')).length;
     const metricCards = [
-      { label: '可直接导入', value: String(readyCount), icon: FileText },
-      { label: '待完善', value: String(attentionCount), icon: AlertTriangle },
-      { label: '缺少遥测绑定', value: String(telemetryGapCount), icon: Network }
+      { label: t('entities.definition.workspace.metric.ready'), value: String(readyCount), icon: FileText },
+      { label: t('entities.definition.workspace.metric.attention'), value: String(attentionCount), icon: AlertTriangle },
+      { label: t('entities.definition.workspace.metric.telemetry-gaps'), value: String(telemetryGapCount), icon: Network }
     ];
 
     return (
@@ -367,9 +347,9 @@ export function EntityDefinitionWorkspaceSurface({
         className="min-h-[calc(100vh-64px)] bg-[#0b0c0e] px-6 pb-6 pt-4 text-[#dbe4f0]"
       >
         <WorkbenchPage
-          kicker="实体定义"
-          title="导入实体定义"
-          subtitle="从 YAML、JSON 或 cURL 请求内容导入一个或多个实体定义,先预览再写入目录。"
+          kicker={t('entities.definition.workspace.kicker')}
+          title={t('entities.import.workspace.title')}
+          subtitle={importSubtitle}
           tone="operator"
           facts={[]}
           main={
@@ -385,22 +365,22 @@ export function EntityDefinitionWorkspaceSurface({
                     data-entity-definition-import-action-row="cold-inline-actions"
                     className="flex flex-wrap items-end justify-between gap-3 rounded-[4px] border border-[#2b3039] bg-[#101217] px-3 py-3"
                   >
-                    <ToolbarField label="导入格式" className="w-[144px] min-w-[144px] flex-none">
+                    <ToolbarField label={t('entity.definition.format.label')} className="w-[144px] min-w-[144px] flex-none">
                       <ToolbarNativeSelect
                         data-entity-definition-format-select="cold-compact-select"
                         value={format}
                         onChange={event => void handleFormatChange(event.target.value as EntityDefinitionFormat)}
                       >
-                        <option value="yaml">YAML</option>
-                        <option value="json">JSON</option>
-                        <option value="curl">cURL</option>
+                        <option value="yaml">{t('entity.definition.format.option.yaml')}</option>
+                        <option value="json">{t('entity.definition.format.option.json')}</option>
+                        <option value="curl">{t('entity.definition.format.option.curl')}</option>
                       </ToolbarNativeSelect>
                     </ToolbarField>
                     <div className="flex flex-wrap items-center justify-end gap-2">{renderActionControls()}</div>
                   </div>
 
                   <p className="text-[12px] leading-5 text-[#98a2b3]">
-                    支持 YAML、JSON 和从预览或自动化流程生成的 cURL。导入前会先解析实体基础信息、遥测绑定和依赖关系。
+                    {importFormatHelp}
                   </p>
 
                   <ColdCodeEditor
@@ -413,7 +393,7 @@ export function EntityDefinitionWorkspaceSurface({
                     language={resolveDefinitionEditorLanguage(format)}
                     minHeight="520px"
                     onChange={handleDraftChange}
-                    placeholder={resolveImportPlaceholder(tr, format)}
+                    placeholder={resolveImportPlaceholder(t, format)}
                     spellCheck={false}
                   />
 
@@ -453,7 +433,7 @@ export function EntityDefinitionWorkspaceSurface({
 
                   {previewRows.length > 0 ? (
                     <section className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
-                      <div className="text-[12px] font-semibold text-[#f5f7fb]">当前定义</div>
+                      <div className="text-[12px] font-semibold text-[#f5f7fb]">{t('entities.definition.workspace.current-definition')}</div>
                       <div className="grid gap-2">
                         {queueGroups.map(group => (
                           <WorkbenchInsetPanel
@@ -491,17 +471,18 @@ export function EntityDefinitionWorkspaceSurface({
 
   if (mode === 'definition') {
     const visual = coldOpsCatalogVisual;
-    const definitionSubtitle = '直接编辑当前定义。保存后会同步更新基础信息、遥测绑定和依赖。';
+    const definitionSubtitle = t('entities.definition.workspace.subtitle');
+    const definitionFormatHelp = t('entities.definition.workspace.format-help');
     const definitionErrorState = messageTone === 'error' && message != null && content.trim() === '';
-    const definitionPlaceholder = resolveImportPlaceholder(tr, format);
+    const definitionPlaceholder = resolveImportPlaceholder(t, format);
     const readyCount = previewRows.filter(row => row.gaps.length === 0).length;
     const attentionCount = previewRows.filter(row => row.gaps.length > 0).length;
     const telemetryGapCount = previewRows.filter(row => row.gapKeys.includes('telemetry')).length;
-    const visibleMessage = message == null ? null : localizeEntityDefinitionMessage(message);
+    const visibleMessage = message == null ? null : localizeEntityDefinitionMessage(message, t);
     const metricCards = [
-      { label: '可直接导入', value: String(readyCount), icon: FileText },
-      { label: '待完善', value: String(attentionCount), icon: AlertTriangle },
-      { label: '缺少遥测绑定', value: String(telemetryGapCount), icon: Network }
+      { label: t('entities.definition.workspace.metric.ready'), value: String(readyCount), icon: FileText },
+      { label: t('entities.definition.workspace.metric.attention'), value: String(attentionCount), icon: AlertTriangle },
+      { label: t('entities.definition.workspace.metric.telemetry-gaps'), value: String(telemetryGapCount), icon: Network }
     ];
 
     return (
@@ -514,8 +495,8 @@ export function EntityDefinitionWorkspaceSurface({
         className="-mx-4 -mb-3 -mt-4 bg-[#0b0c0e] text-[#dbe4f0] sm:-mx-6"
       >
         <WorkbenchPage
-          kicker="实体定义"
-          title="编辑实体定义"
+          kicker={t('entities.definition.workspace.kicker')}
+          title={t('entities.definition.workspace.title')}
           subtitle={definitionSubtitle}
           tone="operator"
           facts={[]}
@@ -532,22 +513,22 @@ export function EntityDefinitionWorkspaceSurface({
                     data-entity-definition-action-row="cold-inline-actions"
                     className="flex flex-wrap items-end justify-between gap-3 rounded-[4px] border border-[#2b3039] bg-[#101217] px-3 py-3"
                   >
-                    <ToolbarField label="导入格式" className="w-[144px] min-w-[144px] flex-none">
+                    <ToolbarField label={t('entity.definition.format.label')} className="w-[144px] min-w-[144px] flex-none">
                       <ToolbarNativeSelect
                         data-entity-definition-format-select="cold-compact-select"
                         value={format}
                         onChange={event => void handleFormatChange(event.target.value as EntityDefinitionFormat)}
                       >
-                        <option value="yaml">YAML</option>
-                        <option value="json">JSON</option>
-                        <option value="curl">cURL</option>
+                        <option value="yaml">{t('entity.definition.format.option.yaml')}</option>
+                        <option value="json">{t('entity.definition.format.option.json')}</option>
+                        <option value="curl">{t('entity.definition.format.option.curl')}</option>
                       </ToolbarNativeSelect>
                     </ToolbarField>
                     <div className="flex flex-wrap items-center justify-end gap-2">{renderActionControls()}</div>
                   </div>
 
                   <p className="text-[12px] leading-5 text-[#98a2b3]">
-                    {definitionSubtitle} 支持 YAML、JSON 和 cURL 请求内容,保存前可先预览解析结果。
+                    {definitionSubtitle} {definitionFormatHelp}
                   </p>
 
                   <ColdCodeEditor
@@ -601,19 +582,19 @@ export function EntityDefinitionWorkspaceSurface({
                   ) : null}
 
                   <section className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
-                    <div className="text-[12px] font-semibold text-[#f5f7fb]">自定义模板</div>
+                    <div className="text-[12px] font-semibold text-[#f5f7fb]">{t('entities.definition.workspace.template-title')}</div>
                     {definitionErrorState ? (
-                      <p className="text-[12px] leading-5 text-[#98a2b3]">定义加载失败后可先确认实体是否存在,再继续编辑。</p>
+                      <p className="text-[12px] leading-5 text-[#98a2b3]">{t('entities.definition.workspace.template-error-help')}</p>
                     ) : (
                       <RowList
                         rows={
                           templates.length > 0
-                            ? buildTemplateRows(templates)
+                            ? buildTemplateRows(templates, t)
                             : [
                                 {
-                                  title: '暂无模板',
-                                  copy: '可复用模板会显示在这里。',
-                                  meta: '-'
+                                  title: t('entities.definition.workspace.template-empty-title'),
+                                  copy: t('entities.definition.workspace.template-empty-copy'),
+                                  meta: emptyValue
                                 }
                               ]
                         }
@@ -624,10 +605,12 @@ export function EntityDefinitionWorkspaceSurface({
                   <section data-entity-definition-batch-panel="true" className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
                     <div className="flex items-center justify-between gap-3">
                       <div>
-                        <div className="text-[12px] font-semibold text-[#f5f7fb]">模板与批量编辑</div>
-                        <p className="mt-1 text-[12px] leading-5 text-[#98a2b3]">查看模板、草稿来源和可选操作。</p>
+                        <div className="text-[12px] font-semibold text-[#f5f7fb]">{t('entities.definition.workspace.batch-title')}</div>
+                        <p className="mt-1 text-[12px] leading-5 text-[#98a2b3]">{t('entities.definition.workspace.batch-copy')}</p>
                       </div>
-                      <span className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#d8e4ff]">展开</span>
+                      <span className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#d8e4ff]">
+                        {t('entities.definition.workspace.batch-expand')}
+                      </span>
                     </div>
                   </section>
 
@@ -635,7 +618,7 @@ export function EntityDefinitionWorkspaceSurface({
                     <>
                       {previewRows.length > 0 ? (
                         <section className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
-                          <div className="text-[12px] font-semibold text-[#f5f7fb]">当前定义</div>
+                          <div className="text-[12px] font-semibold text-[#f5f7fb]">{t('entities.definition.workspace.current-definition')}</div>
                           <div className="grid gap-2">
                             {queueGroups.map(group => (
                               <WorkbenchInsetPanel
@@ -663,17 +646,17 @@ export function EntityDefinitionWorkspaceSurface({
                         </section>
                       ) : null}
 
-                      <section className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
-                        <div className="text-[12px] font-semibold text-[#f5f7fb]">最近活动</div>
+                      <section data-entity-definition-activity-panel="true" className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
+                        <div className="text-[12px] font-semibold text-[#f5f7fb]">{t('entities.definition.workspace.activity-title')}</div>
                         <RowList
                           rows={
                             activities.length > 0
                               ? buildActivityRows(activities, t)
                               : [
                                   {
-                                    title: '暂无最近活动',
-                                    copy: '预览和导入活动会显示在这里。',
-                                    meta: '-'
+                                    title: t('entities.definition.workspace.activity-empty-title'),
+                                    copy: t('entities.definition.workspace.activity-empty-copy'),
+                                    meta: emptyValue
                                   }
                                 ]
                           }
@@ -682,14 +665,14 @@ export function EntityDefinitionWorkspaceSurface({
                     </>
                   )}
 
-                  <section className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
-                    <div className="text-[12px] font-semibold text-[#f5f7fb]">入口</div>
+                  <section data-entity-definition-entry-panel="true" className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
+                    <div className="text-[12px] font-semibold text-[#f5f7fb]">{t('entities.definition.workspace.entry-title')}</div>
                     <div className="grid gap-2 text-[12px] text-[#dbe4f0]">
                       <Link className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-2.5 py-2" href={entityId != null ? `/entities/${entityId}` : '/entities'}>
-                        查看实体
+                        {t('entities.definition.workspace.entry-view-entity')}
                       </Link>
                       <Link className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-2.5 py-2" href="/entities">
-                        回到表单
+                        {t('entities.definition.workspace.entry-back-to-form')}
                       </Link>
                     </div>
                   </section>
@@ -705,35 +688,40 @@ export function EntityDefinitionWorkspaceSurface({
   return null;
 }
 
-function localizeEntityDefinitionMessage(message: string) {
+function localizeEntityDefinitionMessage(
+  message: string,
+  t: (key: string, params?: Record<string, string>) => string
+) {
   const normalized = message.trim();
+  if (normalized === '') {
+    return t('entities.definition.message.process-failed');
+  }
   switch (normalized) {
     case 'Entity not exist.':
     case 'Entity not exist':
-      return '实体不存在。';
+      return t('entities.definition.message.entity-not-exist');
     case 'Failed to load entity definition.':
     case 'Definition parsing failed.':
-      return '实体定义处理失败。';
+      return t('entities.definition.message.process-failed');
     case 'Entity definition save failed.':
-      return '实体定义保存失败。';
+      return t('entities.definition.message.update-failed');
     default:
-      return message;
+      return t('entities.definition.message.backend-fallback', { message: normalized });
   }
 }
 
 function resolveImportPlaceholder(
-  tr: (key: string, fallback: string, params?: Record<string, string | number | null | undefined>) => string,
+  t: (key: string) => string,
   format: EntityDefinitionFormat
 ) {
-  void tr;
   switch (format) {
     case 'json':
-      return '粘贴 JSON 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。';
+      return t('entity.definition.import.placeholder.json');
     case 'curl':
-      return '粘贴从预览或自动化流程生成的 cURL 请求。';
+      return t('entity.definition.import.placeholder.curl');
     case 'yaml':
     default:
-      return '粘贴 YAML 实体定义,编辑器会解析基础信息、遥测绑定和依赖关系。';
+      return t('entity.definition.import.placeholder.yaml');
   }
 }
 
diff --git a/web-next/components/pages/entity-detail-surface.test.tsx b/web-next/components/pages/entity-detail-surface.test.tsx
index 21d1fd5514..5aa19085de 100644
--- a/web-next/components/pages/entity-detail-surface.test.tsx
+++ b/web-next/components/pages/entity-detail-surface.test.tsx
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import { EntityDetailSurface } from './entity-detail-surface';
 
 vi.mock('next/link', () => ({
   default: ({ href, children, ...props }: any) => (
@@ -27,11 +28,12 @@ const detail = {
       owner: 'platform',
       environment: 'prod',
       system: 'payments',
+      runbook: 'https://runbooks.local/checkout',
       description: 'Checkout service'
     },
     identities: [{ id: 'host' }],
     monitorBinds: [{ id: 1 }],
-    relations: [{ id: 2 }]
+    relations: [{ relationType: 'calls', targetEntityId: 'mysql-1', targetEntityName: 'mysql-prod' }]
   },
   evidenceSummary: {
     collectorOfflineCount: 1,
@@ -53,6 +55,17 @@ const detail = {
     recentTraceCount: 4,
     recentErrorTraceCount: 1
   },
+  unifiedEvidenceSummary: {
+    activeSignalCount: 3,
+    metricsActive: true,
+    logsActive: true,
+    tracesActive: true,
+    metricEvidenceCount: 2,
+    logEvidenceCount: 3,
+    traceEvidenceCount: 4,
+    latestObservedAt: '2026-05-13 14:55:00',
+    activeSignals: ['metrics', 'logs', 'traces']
+  },
   activeAlerts: [{ id: 1 }],
   alertSummary: {
     totalActiveAlerts: 1
@@ -91,6 +104,13 @@ describe('EntityDetailSurface', () => {
     expect(source).toContain('data-entity-detail-template-binding="monitor-template-binding"');
     expect(source).toContain('data-entity-health-model="lightweight-service-health"');
     expect(source).toContain('data-entity-health-slo-mode="lightweight-no-slo-authoring"');
+    expect(source).toContain('data-entity-detail-evidence-model="red-use-read-model"');
+    expect(source).toContain('buildUnifiedEvidenceRows');
+    expect(source).toContain('data-entity-detail-evidence-handoff="alert-topology-runbook"');
+    expect(source).toContain('data-entity-detail-evidence-handoff-row={row.key}');
+    expect(source).toContain('data-entity-detail-evidence-handoff-source={row.evidence}');
+    expect(source).toContain('data-entity-detail-evidence-handoff-count={row.count}');
+    expect(source).toContain('buildEntityEvidenceHandoffRows');
     expect(source).toContain('data-entity-health-collector-handoff="collector-cluster"');
     expect(source).toContain('data-entity-health-collector-freshness="last-seen"');
     expect(source).toContain('buildEntityHealthModel');
@@ -101,9 +121,20 @@ describe('EntityDetailSurface', () => {
     expect(source).toContain("from '../workbench/overlay-dialog'");
     expect(source).toContain('data-entity-detail-delete-confirm-trigger="cold-modal"');
     expect(source).toContain('data-entity-detail-delete-confirm="cold-modal"');
-    expect(source).toContain('确认删除实体');
-    expect(source).toContain('确认删除');
-    expect(source).toContain('取消');
+    expect(source).toContain("translateEntityDetail('entities.detail.delete.title')");
+    expect(source).toContain("translateEntityDetail('entities.detail.delete.kicker')");
+    expect(source).toContain("translateEntityDetail('entities.detail.delete.cancel')");
+    expect(source).toContain("translateEntityDetail('entities.detail.delete.confirm')");
+    expect(source).toContain("translateEntityDetail('entities.detail.delete.copy')");
+    expect(source).toContain("translateEntityDetail('entities.detail.attribution.ready')");
+    expect(source).toContain("translateEntityDetail('entities.detail.attribution.review')");
+    expect(source).toContain("translateEntityDetail('entities.detail.attribution.missing')");
+    expect(source).not.toContain('title="确认删除实体"');
+    expect(source).not.toContain('kicker="对象目录"');
+    expect(source).not.toContain('删除后实体会从对象目录移除');
+    expect(source).not.toContain('已归因');
+    expect(source).not.toContain('待确认');
+    expect(source).not.toContain('缺失');
     expect(source).not.toContain('window.confirm');
     expect(source).not.toContain('confirm(');
     expect(source).toContain('buildEntityContextHandoffLinks');
@@ -119,8 +150,7 @@ describe('EntityDetailSurface', () => {
     expect(source).not.toContain('text-white/55');
   });
 
-  it('renders Chinese cold entity detail copy and compact action rows', async () => {
-    const { EntityDetailSurface } = await import('./entity-detail-surface');
+  it('renders English fallback cold entity detail copy and compact action rows', () => {
     const html = renderToStaticMarkup(
       <EntityDetailSurface detail={detail} actionError={null} isPending={false} onDelete={() => undefined} onRefresh={() => undefined} />
     );
@@ -148,6 +178,15 @@ describe('EntityDetailSurface', () => {
     expect(html).toContain('data-entity-detail-template-binding="monitor-template-binding"');
     expect(html).toContain('data-entity-health-model="lightweight-service-health"');
     expect(html).toContain('data-entity-health-slo-mode="lightweight-no-slo-authoring"');
+    expect(html).toContain('data-entity-detail-evidence-model="red-use-read-model"');
+    expect(html).toContain('data-entity-detail-evidence-handoff="alert-topology-runbook"');
+    expect(html).toContain('data-entity-detail-evidence-handoff-row="alerts"');
+    expect(html).toContain('data-entity-detail-evidence-handoff-source="active-alerts"');
+    expect(html).toContain('data-entity-detail-evidence-handoff-count="1"');
+    expect(html).toContain('data-entity-detail-evidence-handoff-row="topology"');
+    expect(html).toContain('data-entity-detail-evidence-handoff-source="topology-relation"');
+    expect(html).toContain('data-entity-detail-evidence-handoff-row="runbook"');
+    expect(html).toContain('data-entity-detail-evidence-handoff-source="runbook"');
     expect(html).toContain('data-entity-health-collector-handoff="collector-cluster"');
     expect(html).toContain('data-entity-health-collector-freshness="last-seen"');
     expect(html).toContain('href="/setting/collector?entityId=42&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-1h"');
@@ -155,56 +194,70 @@ describe('EntityDetailSurface', () => {
     expect(html).toContain('href="/log/manage?entityId=42&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-1h"');
     expect(html).toContain('href="/trace/manage?entityId=42&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-1h"');
     expect(html).toContain('href="/alert/setting?entityId=42&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-1h"');
+    expect(html).toContain('href="/alert?status=firing&amp;entityId=42&amp;entityName=Checkout+API&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-1h&amp;returnTo=%2Fentities%2F42"');
     expect(html).toContain('href="/monitors?entityId=42&amp;entityName=Checkout+API&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-1h&amp;returnTo=%2Fentities%2F42"');
     expect(html).toContain('href="/topology?entityId=42&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-1h"');
+    expect(html).toContain('topologyTargetId=mysql-1');
+    expect(html).toContain('topologyTargetName=mysql-prod');
+    expect(html).toContain('href="https://runbooks.local/checkout"');
     expect(html).toContain('href="/entities/42/definition?entityId=42&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-1h"');
-    expect(html).toContain('对象优先调查');
-    expect(html).toContain('实体详情');
-    expect(html).toContain('上下文');
-    expect(html).toContain('观测上下文');
-    expect(html).toContain('当前告警');
-    expect(html).toContain('相关信号');
-    expect(html).toContain('上下游关系');
-    expect(html).toContain('采集来源');
-    expect(html).toContain('传统监控绑定');
-    expect(html).toContain('OTLP 归因');
-    expect(html).toContain('候选确认');
-    expect(html).toContain('归因诊断');
-    expect(html).toContain('模板绑定');
-    expect(html).toContain('告警规则');
-    expect(html).toContain('绑定监控');
-    expect(html).toContain('上下游拓扑');
-    expect(html).toContain('轻量健康模型');
-    expect(html).toContain('可用性');
-    expect(html).toContain('错误率');
-    expect(html).toContain('延迟');
-    expect(html).toContain('当前告警');
-    expect(html).toContain('最近异常');
-    expect(html).toContain('采集健康');
-    expect(html).toContain('采集器 1 / 2 在线');
-    expect(html).toContain('任务 11 · 离线 1');
-    expect(html).toContain('最近上报 2026-04-10 18:05:00');
-    expect(html).toContain('健康评分');
-    expect(html).toContain('暂不展开 SLO 编排');
-    expect(html).toContain('下一步');
-    expect(html).toContain('高级入口');
-    expect(html).toContain('全部实体');
-    expect(html).toContain('刷新');
-    expect(html).toContain('编辑定义');
-    expect(html).toContain('删除');
-    expect(html).toContain('编辑');
-    expect(html).toContain('关联指标');
-    expect(html).toContain('先检查异常监控。');
+    expect(html).toContain('Entity-first investigation');
+    expect(html).toContain('Entity detail');
+    expect(html).toContain('Context');
+    expect(html).toContain('Observability context');
+    expect(html).toContain('Current alerts');
+    expect(html).toContain('Related signals');
+    expect(html).toContain('Upstream and downstream');
+    expect(html).toContain('Collection source');
+    expect(html).toContain('Traditional monitor binding');
+    expect(html).toContain('OTLP attribution');
+    expect(html).toContain('Candidate confirmation');
+    expect(html).toContain('Attribution diagnostics');
+    expect(html).toContain('Template binding');
+    expect(html).toContain('Alert rules');
+    expect(html).toContain('Bound monitors');
+    expect(html).toContain('Upstream topology');
+    expect(html).toContain('Lightweight health model');
+    expect(html).toContain('Evidence model');
+    expect(html).toContain('RED / USE read model');
+    expect(html).toContain('Active alert evidence');
+    expect(html).toContain('Topology context');
+    expect(html).toContain('Runbook');
+    expect(html).toContain('3 active signals');
+    expect(html).toContain('metrics · logs · traces');
+    expect(html).toContain('Metrics / USE');
+    expect(html).toContain('Logs / RED');
+    expect(html).toContain('Traces / RED');
+    expect(html).toContain('Availability');
+    expect(html).toContain('Error rate');
+    expect(html).toContain('Latency');
+    expect(html).toContain('Current alerts');
+    expect(html).toContain('Recent anomalies');
+    expect(html).toContain('Collection health');
+    expect(html).toContain('Collectors 1 / 2 online');
+    expect(html).toContain('Tasks 11 · offline 1');
+    expect(html).toContain('Last report 2026-04-10 18:05:00');
+    expect(html).toContain('Health score');
+    expect(html).toContain('without SLO authoring');
+    expect(html).toContain('Next step');
+    expect(html).toContain('Advanced entries');
+    expect(html).toContain('All entities');
+    expect(html).toContain('Refresh');
+    expect(html).toContain('Edit definition');
+    expect(html).toContain('Delete');
+    expect(html).toContain('Edit');
+    expect(html).toContain('Related metrics');
+    expect(html).toContain('Inspect abnormal monitors first.');
     expect(html).not.toContain('Next steps');
     expect(html).not.toContain('Review the summary first');
-    expect(html).not.toContain('Related metrics');
+    expect(html).not.toContain('对象优先调查');
+    expect(html).not.toContain('关联指标');
     expect(html).not.toContain('definition workspace');
     expect(html).not.toContain('data-stage-section');
     expect(html).not.toContain('data-drawer-section');
-  });
+  }, 30000);
 
-  it('renders entity context handoff links with inherited time and monitor context', async () => {
-    const { EntityDetailSurface } = await import('./entity-detail-surface');
+  it('renders entity context handoff links with inherited time and monitor context', () => {
     const html = renderToStaticMarkup(
       <EntityDetailSurface
         detail={detail}
@@ -235,14 +288,15 @@ describe('EntityDetailSurface', () => {
       'href="/topology?entityId=42&amp;serviceName=checkout-api&amp;environment=prod&amp;timeRange=last-45m&amp;start=1713200000000&amp;end=1713202700000&amp;refresh=30&amp;live=false&amp;tz=Asia%2FShanghai&amp;source=monitor&amp;monitorId=632051474676992&amp;monitorName=checkout-http&amp;monitorApp=website&amp;monitorInstance=example.com%3A443"'
     );
     expect(html).toContain('data-entity-detail-inherited-context="route-context"');
-    expect(html).toContain('data-entity-detail-inherited-context-row="时间范围"');
+    expect(html).toContain('data-entity-detail-inherited-context-row="Time range"');
     expect(html).toContain('last-45m');
-    expect(html).toContain('2024/04/16 00:53:20 → 2024/04/16 01:38:20 · refresh 30s · 已暂停 · Asia/Shanghai');
-    expect(html).toContain('data-entity-detail-inherited-context-row="监控实例"');
+    expect(html).toContain('2024/04/16 00:53:20 → 2024/04/16 01:38:20 · Refresh 30s · Paused · Asia/Shanghai');
+    expect(html).toContain('data-entity-detail-inherited-context-row="Monitor instance"');
     expect(html).toContain('checkout-http');
     expect(html).toContain('website · example.com:443 · monitorId 632051474676992');
-    expect(html).toContain('data-entity-detail-inherited-context-row="采集来源"');
-    expect(html).toContain('传统监控');
+    expect(html).toContain('data-entity-detail-inherited-context-row="Source"');
+    expect(html).toContain('Traditional monitoring');
     expect(html).not.toContain('0 个证据');
-  });
+    expect(html).not.toContain('0 evidence');
+  }, 30000);
 });
diff --git a/web-next/components/pages/entity-detail-surface.tsx b/web-next/components/pages/entity-detail-surface.tsx
index c6498125d5..39211566d5 100644
--- a/web-next/components/pages/entity-detail-surface.tsx
+++ b/web-next/components/pages/entity-detail-surface.tsx
@@ -6,6 +6,8 @@ import { Activity, ArrowLeft, Bell, Edit3, FileText, GitBranch, Network, Refresh
 import { Button } from '../ui/button';
 import { OverlayDialog } from '../workbench/overlay-dialog';
 import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
+import { interpolate, type TranslationParams } from '../../lib/i18n';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import {
   buildCollectionSourceRows,
   buildCurrentAlertRows,
@@ -13,12 +15,14 @@ import {
   buildDrilldownRows,
   buildEntityAttributionRows,
   buildEntityContextHandoffLinks,
+  buildEntityEvidenceHandoffRows,
   buildEntityHealthModel,
   buildEntityIncomingContextRows,
   buildNextActionRows,
   buildOverviewRows,
   buildRelationshipRows,
-  buildSummaryRows
+  buildSummaryRows,
+  buildUnifiedEvidenceRows
 } from '../../lib/entity-detail/view-model';
 import type { SignalRouteContext } from '../../lib/signal-route-context';
 import type { EntityDetailDto } from '../../lib/types';
@@ -38,12 +42,19 @@ type DetailRow = {
   freshness?: string;
   href?: string;
   meta: string;
+  tone?: 'success' | 'warning' | 'danger' | 'neutral';
 };
 
 type ContextLinkRow = DetailRow & {
   key: string;
 };
 
+type EvidenceHandoffRow = DetailRow & {
+  key: string;
+  evidence: string;
+  count: number;
+};
+
 type IncomingContextRow = {
   label: string;
   value: string;
@@ -69,9 +80,14 @@ const coldPrimaryLinkClassName =
 const coldLinkButtonClassName =
   'inline-flex h-8 min-w-[104px] items-center justify-center gap-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white';
 
+function translateEntityDetail(key: string, params?: TranslationParams) {
+  const template = SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+  return interpolate(template, params);
+}
+
 function resolveTitle(detail: EntityDetailDto) {
   const entity = detail.entity?.entity || {};
-  return entity.displayName || entity.name || '实体详情';
+  return entity.displayName || entity.name || translateEntityDetail('entities.detail.title.fallback');
 }
 
 function resolveSubtitle(detail: EntityDetailDto) {
@@ -80,51 +96,53 @@ function resolveSubtitle(detail: EntityDetailDto) {
   const hasEvidence = (detail.logSummary?.hintCount ?? 0) > 0 || (detail.traceSummary?.recentTraceCount ?? 0) > 0 || (detail.boundMonitors?.length ?? 0) > 0;
 
   if (activeAlerts > 0) {
-    return '当前实体已有活跃告警,优先确认告警和最近变化。';
+    return translateEntityDetail('entities.detail.subtitle.active-alerts');
   }
   if (downMonitors > 0) {
-    return '已有绑定监控异常,先从监控证据确认影响面。';
+    return translateEntityDetail('entities.detail.subtitle.down-monitors');
   }
   if (hasEvidence) {
-    return '日志、链路或监控证据已关联,可继续排查。';
+    return translateEntityDetail('entities.detail.subtitle.has-evidence');
   }
-  return '先补齐负责人、定义和遥测绑定,让实体成为可用工作台。';
+  return translateEntityDetail('entities.detail.subtitle.needs-context');
 }
 
 function countMetrics(detail: EntityDetailDto) {
   return [
     {
-      label: '绑定监控',
+      label: translateEntityDetail('entities.detail.metric.bound-monitors'),
       value: detail.monitorSummary?.totalBoundMonitors ?? detail.boundMonitors?.length ?? detail.entity?.monitorBinds?.length ?? 0,
       Icon: Network
     },
     {
-      label: '活跃告警',
+      label: translateEntityDetail('entities.detail.metric.active-alerts'),
       value: detail.alertSummary?.totalActiveAlerts ?? detail.activeAlerts?.length ?? detail.evidenceSummary?.activeAlertCount ?? 0,
       Icon: Bell
     },
     {
-      label: '身份标识',
+      label: translateEntityDetail('entities.detail.metric.identities'),
       value: detail.evidenceSummary?.identityCount ?? detail.entity?.identities?.length ?? 0,
       Icon: FileText
     },
     {
-      label: '关联关系',
+      label: translateEntityDetail('entities.detail.metric.relations'),
       value: detail.entity?.relations?.length ?? 0,
       Icon: GitBranch
     }
   ];
 }
 
-function rowStatusClass(row: DetailRow) {
-  const value = `${row.title} ${row.copy} ${row.meta}`;
-  if (value.includes('异常') || value.includes('告警') || value.includes('错误')) {
-    return 'border-[#7f1d1d]/55 bg-[#2a1214] text-[#fca5a5]';
-  }
-  if (value.includes('健康') || value.includes('可用')) {
-    return 'border-[#166534]/45 bg-[#0f2f23] text-[#86efac]';
+function rowStatusClass(tone: DetailRow['tone'] = 'neutral') {
+  switch (tone) {
+    case 'success':
+      return 'border-[#166534]/45 bg-[#0f2f23] text-[#86efac]';
+    case 'warning':
+      return 'border-[#574622] bg-[#1b1710] text-[#facc6b]';
+    case 'danger':
+      return 'border-[#7f1d1d]/55 bg-[#2a1214] text-[#fca5a5]';
+    default:
+      return 'border-[#303743] bg-[#101217] text-[#cbd5e1]';
   }
-  return 'border-[#303743] bg-[#101217] text-[#cbd5e1]';
 }
 
 function attributionStateClass(state: AttributionRow['state']) {
@@ -138,6 +156,17 @@ function attributionStateClass(state: AttributionRow['state']) {
   }
 }
 
+function attributionStateLabel(state: AttributionRow['state']) {
+  switch (state) {
+    case 'ready':
+      return translateEntityDetail('entities.detail.attribution.ready');
+    case 'review':
+      return translateEntityDetail('entities.detail.attribution.review');
+    default:
+      return translateEntityDetail('entities.detail.attribution.missing');
+  }
+}
+
 function AttributionRows({ rows }: { rows: AttributionRow[] }) {
   return (
     <div data-entity-detail-attribution-panel="entity-resource-attribution" className="mb-3 grid gap-2 sm:grid-cols-2">
@@ -154,7 +183,7 @@ function AttributionRows({ rows }: { rows: AttributionRow[] }) {
               <p className="mt-1 truncate text-[12px] leading-5 text-[#8f99ab]">{row.copy}</p>
             </div>
             <span className={`shrink-0 rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${attributionStateClass(row.state)}`}>
-              {row.state === 'ready' ? '已归因' : row.state === 'review' ? '待确认' : '缺失'}
+              {attributionStateLabel(row.state)}
             </span>
           </div>
           <p className="mt-2 truncate text-[11px] leading-4 text-[#7e8494]">{row.meta}</p>
@@ -180,7 +209,7 @@ function RowStack({ rows }: { rows: DetailRow[] }) {
               </p>
             ) : null}
           </div>
-          <span className={`inline-flex max-w-full items-center justify-center truncate rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${rowStatusClass(row)}`}>
+          <span className={`inline-flex max-w-full items-center justify-center truncate rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${rowStatusClass(row.tone)}`}>
             {row.meta}
           </span>
           </>
@@ -272,6 +301,31 @@ function ContextLinkRows({ rows }: { rows: ContextLinkRow[] }) {
   );
 }
 
+function EvidenceHandoffRows({ rows }: { rows: EvidenceHandoffRow[] }) {
+  if (rows.length === 0) return null;
+
+  return (
+    <div data-entity-detail-evidence-handoff="alert-topology-runbook" className="mt-3 grid gap-2 sm:grid-cols-3">
+      {rows.map(row => (
+        <Link
+          key={row.key}
+          data-entity-detail-evidence-handoff-row={row.key}
+          data-entity-detail-evidence-handoff-source={row.evidence}
+          data-entity-detail-evidence-handoff-count={row.count}
+          href={row.href || '#'}
+          className="grid min-h-[64px] gap-1 rounded-[3px] border border-[#252b34] bg-[#101217] px-3 py-2.5 text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151820]"
+        >
+          <span className="text-[12px] font-semibold text-[#eef2f7]">{row.title}</span>
+          <span className="truncate text-[11px] text-[#8f99ab]">{row.copy}</span>
+          <span className={`mt-1 w-fit max-w-full truncate rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold ${rowStatusClass(row.tone)}`}>
+            {row.meta}
+          </span>
+        </Link>
+      ))}
+    </div>
+  );
+}
+
 function InheritedContextRows({ rows }: { rows: IncomingContextRow[] }) {
   if (rows.length === 0) return null;
 
@@ -299,11 +353,13 @@ export function EntityDetailSurface({ detail, routeContext, actionError, isPendi
   const entity = detail.entity?.entity || {};
   const entityId = entity.id;
   const title = resolveTitle(detail);
-  const facts = buildDetailFacts(entity);
+  const facts = buildDetailFacts(entity, translateEntityDetail);
   const metrics = countMetrics(detail);
-  const contextLinks = buildEntityContextHandoffLinks(detail, routeContext || 'last-1h');
-  const incomingContextRows = buildEntityIncomingContextRows(routeContext);
-  const healthRows = buildEntityHealthModel(detail);
+  const contextLinks = buildEntityContextHandoffLinks(detail, routeContext || 'last-1h', translateEntityDetail);
+  const incomingContextRows = buildEntityIncomingContextRows(routeContext, translateEntityDetail);
+  const healthRows = buildEntityHealthModel(detail, translateEntityDetail);
+  const evidenceRows = buildUnifiedEvidenceRows(detail, translateEntityDetail);
+  const evidenceHandoffRows = buildEntityEvidenceHandoffRows(detail, routeContext || 'last-1h', translateEntityDetail);
   const closeDeleteDialog = () => setDeleteDialogOpen(false);
   const submitDeleteDialog = () => {
     if (!entityId) {
@@ -329,24 +385,24 @@ export function EntityDetailSurface({ detail, routeContext, actionError, isPendi
               <div data-entity-detail-header="cold-compact-header" className={coldEntityDetailVisual.panel.hero}>
                 <div className="max-w-[920px]">
                   <div className="flex flex-wrap items-center gap-2">
-                    <span className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">对象优先调查</span>
-                    <span className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] font-semibold text-[#cbd5e1]">实体详情</span>
+                    <span className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">{translateEntityDetail('entities.detail.header.kicker')}</span>
+                    <span className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] font-semibold text-[#cbd5e1]">{translateEntityDetail('entities.detail.header.badge')}</span>
                   </div>
                   <h1 className="mt-2 text-[30px] font-semibold leading-tight text-[#f5f7fb]">{title}</h1>
                   <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">{resolveSubtitle(detail)}</p>
                   <div data-entity-detail-command-row="standard-equal-buttons" className={coldEntityDetailVisual.button.row}>
                     <Link href="/entities" className={coldLinkButtonClassName}>
                       <ArrowLeft className="h-3.5 w-3.5" aria-hidden="true" />
-                      全部实体
+                      {translateEntityDetail('entities.detail.action.all-entities')}
                     </Link>
                     <Button size="sm" variant="default" className={coldButtonClassName} onClick={onRefresh}>
                       <RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />
-                      刷新
+                      {translateEntityDetail('common.refresh')}
                     </Button>
                     {entityId ? (
                       <Link href={`/entities/${entityId}/definition`} className={coldLinkButtonClassName}>
                         <FileText className="h-3.5 w-3.5" aria-hidden="true" />
-                        编辑定义
+                        {translateEntityDetail('entities.detail.action.edit-definition')}
                       </Link>
                     ) : null}
                     {entityId ? (
@@ -360,13 +416,13 @@ export function EntityDetailSurface({ detail, routeContext, actionError, isPendi
                         onClick={() => setDeleteDialogOpen(true)}
                       >
                         <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
-                        删除
+                        {translateEntityDetail('entities.detail.action.delete')}
                       </Button>
                     ) : null}
                     {entityId ? (
                       <Link href={`/entities/${entityId}/edit`} className={coldPrimaryLinkClassName}>
                         <Edit3 className="h-3.5 w-3.5" aria-hidden="true" />
-                        编辑
+                        {translateEntityDetail('entities.detail.action.edit')}
                       </Link>
                     ) : null}
                   </div>
@@ -394,12 +450,16 @@ export function EntityDetailSurface({ detail, routeContext, actionError, isPendi
             className={actionError ? 'mb-5 rounded-[4px] border border-[#7f1d1d]/55 bg-[#2a1214] px-4 py-3 text-[12px] text-[#fca5a5]' : 'hidden'}
             aria-hidden={actionError ? undefined : true}
           >
-            <span className="font-semibold">操作未完成</span>
+            <span className="font-semibold">{translateEntityDetail('entities.detail.error.title')}</span>
             {actionError ? <span className="ml-2">{actionError}</span> : null}
           </div>
 
           <div data-entity-detail-signal-grid="cold-detail-grid" className="grid items-start gap-5 lg:grid-cols-2">
-            <DetailPanel marker="overview" title="上下文" copy="先确认实体身份、归属和运行状态,再进入信号工作台。">
+            <DetailPanel
+              marker="overview"
+              title={translateEntityDetail('entities.detail.panel.overview.title')}
+              copy={translateEntityDetail('entities.detail.panel.overview.copy')}
+            >
               <div data-entity-detail-overview-panel="cold-overview-panel">
                 <div className="mb-3 grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
                   {facts.map(fact => (
@@ -409,11 +469,15 @@ export function EntityDetailSurface({ detail, routeContext, actionError, isPendi
                     </div>
                   ))}
                 </div>
-                <RowStack rows={buildOverviewRows(entity, detail)} />
+                <RowStack rows={buildOverviewRows(entity, detail, translateEntityDetail)} />
               </div>
             </DetailPanel>
 
-            <DetailPanel marker="context" title="观测上下文" copy="所有跳转都携带实体、服务、环境和时间范围,链路相关入口会继续带 trace/span。">
+            <DetailPanel
+              marker="context"
+              title={translateEntityDetail('entities.detail.panel.context.title')}
+              copy={translateEntityDetail('entities.detail.panel.context.copy')}
+            >
               <div data-entity-detail-context-center="hertzbeat-entity-context">
                 <InheritedContextRows rows={incomingContextRows} />
                 <ContextLinkRows rows={contextLinks} />
@@ -422,8 +486,8 @@ export function EntityDetailSurface({ detail, routeContext, actionError, isPendi
 
             <DetailPanel
               marker="health"
-              title="轻量健康模型"
-              copy="用可用性、错误率、延迟、当前告警、最近异常和采集健康形成轻量评分,暂不展开 SLO 编排。"
+              title={translateEntityDetail('entities.detail.panel.health.title')}
+              copy={translateEntityDetail('entities.detail.panel.health.copy')}
             >
               <div
                 data-entity-health-model="lightweight-service-health"
@@ -433,51 +497,86 @@ export function EntityDetailSurface({ detail, routeContext, actionError, isPendi
               </div>
             </DetailPanel>
 
-            <DetailPanel marker="alerts" title="当前告警" copy="先确认正在影响该实体的告警,再决定静默、抑制、分组或通知闭环。">
+            <DetailPanel
+              marker="evidence"
+              title={translateEntityDetail('entities.detail.panel.evidence.title')}
+              copy={translateEntityDetail('entities.detail.panel.evidence.copy')}
+            >
+              <div data-entity-detail-evidence-model="red-use-read-model">
+                <RowStack rows={evidenceRows} />
+                <EvidenceHandoffRows rows={evidenceHandoffRows} />
+              </div>
+            </DetailPanel>
+
+            <DetailPanel
+              marker="alerts"
+              title={translateEntityDetail('entities.detail.panel.alerts.title')}
+              copy={translateEntityDetail('entities.detail.panel.alerts.copy')}
+            >
               <div data-entity-detail-current-alerts="current-alerts">
-                <RowStack rows={buildCurrentAlertRows(detail)} />
+                <RowStack rows={buildCurrentAlertRows(detail, translateEntityDetail)} />
               </div>
             </DetailPanel>
 
-            <DetailPanel marker="next" title="下一步" copy="保留服务端建议,但用当前冷峻控制台语言收口。">
+            <DetailPanel
+              marker="next"
+              title={translateEntityDetail('entities.detail.panel.next.title')}
+              copy={translateEntityDetail('entities.detail.panel.next.copy')}
+            >
               <div data-entity-detail-next-panel="cold-next-panel">
-                <RowStack rows={buildNextActionRows(detail, entityId)} />
+                <RowStack rows={buildNextActionRows(detail, entityId, translateEntityDetail)} />
               </div>
             </DetailPanel>
 
-            <DetailPanel marker="related" title="相关信号" copy="用监控、日志和链路快速判断影响范围。">
+            <DetailPanel
+              marker="related"
+              title={translateEntityDetail('entities.detail.panel.related.title')}
+              copy={translateEntityDetail('entities.detail.panel.related.copy')}
+            >
               <div data-entity-detail-related-panel="cold-related-panel">
-                <RowStack rows={buildSummaryRows(detail)} />
+                <RowStack rows={buildSummaryRows(detail, translateEntityDetail)} />
               </div>
             </DetailPanel>
 
-            <DetailPanel marker="relationships" title="上下游关系" copy="关系来自实体归并、调用链、监控对象归属或 CMDB 标签,后续会进入拓扑影响面。">
+            <DetailPanel
+              marker="relationships"
+              title={translateEntityDetail('entities.detail.panel.relationships.title')}
+              copy={translateEntityDetail('entities.detail.panel.relationships.copy')}
+            >
               <div data-entity-detail-relationships-panel="upstream-downstream">
-                <RowStack rows={buildRelationshipRows(detail)} />
+                <RowStack rows={buildRelationshipRows(detail, translateEntityDetail)} />
               </div>
             </DetailPanel>
 
-            <DetailPanel marker="source" title="采集来源" copy="确认这个实体来自 HertzBeat 监控对象、Collector、OTLP 资源属性还是手工/CMDB 标签。">
+            <DetailPanel
+              marker="source"
+              title={translateEntityDetail('entities.detail.panel.source.title')}
+              copy={translateEntityDetail('entities.detail.panel.source.copy')}
+            >
               <div data-entity-detail-collection-source-panel="source-template-binding">
-                <AttributionRows rows={buildEntityAttributionRows(detail)} />
+                <AttributionRows rows={buildEntityAttributionRows(detail, translateEntityDetail)} />
                 <div data-entity-detail-template-binding="monitor-template-binding">
-                  <RowStack rows={buildCollectionSourceRows(detail)} />
+                  <RowStack rows={buildCollectionSourceRows(detail, translateEntityDetail)} />
                 </div>
               </div>
             </DetailPanel>
 
-            <DetailPanel marker="drilldown" title="高级入口" copy="只保留真正有用的深层入口,不增加右侧统计装饰。">
+            <DetailPanel
+              marker="drilldown"
+              title={translateEntityDetail('entities.detail.panel.drilldown.title')}
+              copy={translateEntityDetail('entities.detail.panel.drilldown.copy')}
+            >
               <div data-entity-detail-drilldown-panel="cold-drilldown-panel">
-                <RouteRows rows={buildDrilldownRows(entityId)} />
+                <RouteRows rows={buildDrilldownRows(entityId, translateEntityDetail)} />
               </div>
             </DetailPanel>
 
             <div className="rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-4 text-[12px] leading-5 text-[#8f99ab] lg:col-span-2">
               <div className="mb-2 flex items-center gap-2 text-[12px] font-semibold text-[#eef2f7]">
                 <Activity className="h-3.5 w-3.5 text-[#858d9a]" aria-hidden="true" />
-                处置节奏
+                {translateEntityDetail('entities.detail.disposition.title')}
               </div>
-              告警、监控和日志入口从这里分流;详情页只做对象上下文,不复制目录页的右侧统计栏。
+              {translateEntityDetail('entities.detail.disposition.copy')}
             </div>
           </div>
         </div>
@@ -486,24 +585,22 @@ export function EntityDetailSurface({ detail, routeContext, actionError, isPendi
       <OverlayDialog
         open={deleteDialogOpen}
         onClose={closeDeleteDialog}
-        title="确认删除实体"
-        kicker="对象目录"
+        title={translateEntityDetail('entities.detail.delete.title')}
+        kicker={translateEntityDetail('entities.detail.delete.kicker')}
         maxWidthClassName="max-w-xl"
         footer={
           <div className="flex justify-end gap-2">
             <Button size="sm" variant="default" className={coldButtonClassName} onClick={closeDeleteDialog}>
-              取消
+              {translateEntityDetail('entities.detail.delete.cancel')}
             </Button>
             <Button size="sm" variant="default" className={coldDangerButtonClassName} disabled={isPending} onClick={submitDeleteDialog}>
-              确认删除
+              {translateEntityDetail('entities.detail.delete.confirm')}
             </Button>
           </div>
         }
       >
         <div data-entity-detail-delete-confirm="cold-modal" className="space-y-3 text-[12px] leading-6 text-[#a9b0bb]">
-          <p>
-            删除后实体会从对象目录移除,相关监控绑定、遥测上下文和告警排查入口需要重新归并。
-          </p>
+          <p>{translateEntityDetail('entities.detail.delete.copy')}</p>
           <div className="rounded-[3px] border border-[#3f2228] bg-[#181013] px-3 py-2 text-[#f0b8c1]">
             {title} {entityId ? <span className="text-[#8f99ab]">#{entityId}</span> : null}
           </div>
diff --git a/web-next/components/pages/entity-discovery-surface.test.tsx b/web-next/components/pages/entity-discovery-surface.test.tsx
index 071cc2ab66..5d42bfd33c 100644
--- a/web-next/components/pages/entity-discovery-surface.test.tsx
+++ b/web-next/components/pages/entity-discovery-surface.test.tsx
@@ -7,7 +7,55 @@ import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock()
+    t: createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'entities.discovery.workspace.kicker': '对象优先调查',
+        'entities.discovery.workspace.title': '遥测发现',
+        'entities.discovery.workspace.subtitle': '先搜索一组需要治理的监控线索,再决定归并、完善归属,还是送入定义工作台继续收口。',
+        'entities.discovery.action.import': '从定义创建',
+        'entities.discovery.action.create': '创建实体',
+        'entities.discovery.action.catalog': '对象目录',
+        'entities.discovery.action.search': '搜索',
+        'entities.discovery.action.clear': '清空',
+        'entities.discovery.search.placeholder': '搜索监控名称或实例',
+        'entities.discovery.search.error': '遥测发现查询失败',
+        'entities.discovery.candidate.title': 'OTLP 候选实体',
+        'entities.discovery.candidate.copy': '保留 OTLP 资源身份,确认后再写入对象目录。',
+        'entities.discovery.candidate.action.create': '创建实体草稿',
+        'entities.discovery.candidate.identity': '资源身份',
+        'entities.discovery.candidate.namespace': '命名空间',
+        'entities.discovery.candidate.environment': '环境',
+        'entities.discovery.policy.title': '治理筛选与共享策略',
+        'entities.discovery.policy.copy': '按已有负责人、系统和环境预设给线索完善上下文。',
+        'entities.discovery.policy.latest-activity': '最近活动 {{activity}}',
+        'entities.discovery.policy.empty': '暂无共享策略,先搜索一条监控线索。',
+        'entities.discovery.catalog.owner': '负责人',
+        'entities.discovery.catalog.system': '系统',
+        'entities.discovery.catalog.environment': '环境',
+        'entities.discovery.metric.clues': '线索',
+        'entities.discovery.metric.matched': '已匹配',
+        'entities.discovery.metric.create-suggested': '建议新建',
+        'entities.discovery.metric.catalog-sources': '目录来源',
+        'entities.discovery.table.title': '发现线索',
+        'entities.discovery.table.result-count': '{{count}} 条结果',
+        'entities.discovery.table.waiting': '等待搜索',
+        'entities.discovery.table.column.clue': '线索',
+        'entities.discovery.table.column.instance': '实例',
+        'entities.discovery.table.column.status': '状态',
+        'entities.discovery.table.column.owner': '归属',
+        'entities.discovery.table.column.system': '系统',
+        'entities.discovery.table.column.environment': '环境',
+        'entities.discovery.table.column.attribution': '归因',
+        'entities.discovery.table.column.action': '操作',
+        'entities.discovery.empty.title': '先搜索一组需要治理的监控线索',
+        'entities.discovery.empty.search.copy': '没有找到匹配的监控线索。',
+        'entities.discovery.empty.idle.copy': '先搜索一条监控,再把它转成实体草稿。',
+        'entities.discovery.activity.preset-synced': '预设已同步',
+        'entities.discovery.activity.shared-governance-updated': '共享治理已更新',
+        'entities.discovery.activity.empty': '暂无活动'
+      }
+    })
   })
 }));
 
@@ -208,5 +256,45 @@ describe('EntityDiscoverySurface', () => {
     expect(html).not.toContain('data-workbench-page');
     expect(html).not.toContain('Governance presets');
     expect(html).not.toContain('Catalog suggestions');
-  });
+  }, 30000);
+
+  it('preserves OTLP candidate identity as confirmation context without inventing entity state', async () => {
+    const { EntityDiscoverySurface } = await import('./entity-discovery-surface');
+
+    const html = renderToStaticMarkup(
+      <EntityDiscoverySurface
+        presets={[]}
+        activities={[]}
+        catalog={{ owners: [], systems: [], environments: [] } as any}
+        candidateContext={{
+          source: 'otlp-candidate',
+          identityKey: 'service.name',
+          identityValue: 'billing',
+          serviceName: 'billing-api',
+          serviceNamespace: 'commerce',
+          environment: 'prod',
+          search: 'billing-api'
+        }}
+      />
+    );
+
+    expect(html).toContain('data-entity-discovery-otlp-candidate="query-context"');
+    expect(html).toContain('data-entity-discovery-candidate-identity-key="service.name"');
+    expect(html).toContain('data-entity-discovery-candidate-identity-value="billing"');
+    expect(html).toContain('data-entity-discovery-candidate-service="billing-api"');
+    expect(html).toContain('data-entity-discovery-candidate-namespace="commerce"');
+    expect(html).toContain('data-entity-discovery-candidate-environment="prod"');
+    expect(html).toContain('OTLP 候选实体');
+    expect(html).toContain('service.name = billing');
+    expect(html).toContain('命名空间 · commerce');
+    expect(html).toContain('环境 · prod');
+    expect(html).toContain('value="billing-api"');
+    expect(html).toContain('data-entity-discovery-candidate-action="create-draft"');
+    expect(html).toContain(
+      'href="/entities/new?source=otlp-candidate&amp;identityKey=service.name&amp;identityValue=billing&amp;serviceName=billing-api&amp;serviceNamespace=commerce&amp;environment=prod"'
+    );
+    expect(html).not.toContain('已归属');
+    expect(html).not.toContain('健康正常');
+    expect(html).not.toContain('拓扑已确认');
+  }, 30000);
 });
diff --git a/web-next/components/pages/entity-discovery-surface.tsx b/web-next/components/pages/entity-discovery-surface.tsx
index 2ca1e1e04e..613074320e 100644
--- a/web-next/components/pages/entity-discovery-surface.tsx
+++ b/web-next/components/pages/entity-discovery-surface.tsx
@@ -1,22 +1,45 @@
 'use client';
 
-import React, { useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 import Link from 'next/link';
 import { Database, Network, Plus, Search, Upload, Users } from 'lucide-react';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { apiMessageGet } from '@/lib/api-client';
 import { searchDiscoveryMonitors } from '@/lib/entity-discovery/controller';
 import { buildDiscoveryTableRows } from '@/lib/entity-discovery/view-model';
-import type { DiscoveryTableRow } from '@/lib/entity-discovery/view-model';
+import type { DiscoveryStatusTone, DiscoveryTableRow } from '@/lib/entity-discovery/view-model';
 import type { EntityCatalogSuggestions, EntityDiscoveryGovernanceActivity, EntityDiscoveryGovernancePreset, Monitor } from '@/lib/types';
 import { SearchRow } from '../ui/search-row';
 import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
-import { resolveDiscoverySearchSubmission } from '../../lib/entity-discovery/search-state';
+import {
+  buildDiscoveryCandidateActionHref,
+  resolveDiscoverySearchSubmission,
+  type DiscoveryCandidateContext
+} from '../../lib/entity-discovery/search-state';
 
 type EntityDiscoverySurfaceProps = {
   presets: EntityDiscoveryGovernancePreset[];
   activities: EntityDiscoveryGovernanceActivity[];
   catalog: EntityCatalogSuggestions;
+  candidateContext?: DiscoveryCandidateContext | null;
+};
+
+type EntityDiscoveryTranslator = ReturnType<typeof useI18n>['t'];
+
+type EntityDiscoveryTableCopy = {
+  title: string;
+  resultStatus: string;
+  emptyTitle: string;
+  columns: {
+    clue: string;
+    instance: string;
+    status: string;
+    owner: string;
+    system: string;
+    environment: string;
+    attribution: string;
+    action: string;
+  };
 };
 
 const coldEntityDiscoveryVisual = coldOpsCatalogVisual;
@@ -27,14 +50,14 @@ const coldPrimaryLinkClassName =
 const coldLinkButtonClassName =
   'inline-flex h-8 min-w-[104px] items-center justify-center gap-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white';
 
-function statusClassName(status: string) {
-  if (status === '正常' || status === '已启用' || status === '成功') {
+function statusClassName(tone: DiscoveryStatusTone) {
+  if (tone === 'success') {
     return 'border-[#166534]/45 bg-[#0f2f23] text-[#86efac]';
   }
-  if (status === '待确认' || status === '待处理') {
+  if (tone === 'warning') {
     return 'border-[#7c4a03]/50 bg-[#261903] text-[#fbbf24]';
   }
-  if (status === '异常') {
+  if (tone === 'critical') {
     return 'border-[#7f1d1d]/55 bg-[#2a1214] text-[#fca5a5]';
   }
   return 'border-[#303743] bg-[#101217] text-[#cbd5e1]';
@@ -50,40 +73,40 @@ function attributionClassName(state: DiscoveryTableRow['attributionState']) {
   return 'border-[#303743] bg-[#101217] text-[#cbd5e1]';
 }
 
-function localizeActivitySummary(summary?: string | null) {
+function localizeActivitySummary(summary: string | null | undefined, t: EntityDiscoveryTranslator) {
   const normalized = summary?.trim();
   switch (normalized) {
     case 'preset synced':
-      return '预设已同步';
+      return t('entities.discovery.activity.preset-synced');
     case 'shared governance updated':
-      return '共享治理已更新';
+      return t('entities.discovery.activity.shared-governance-updated');
     default:
-      return normalized || '暂无活动';
+      return normalized || t('entities.discovery.activity.empty');
   }
 }
 
-function EntityDiscoveryTable({ rows, emptyCopy }: { rows: DiscoveryTableRow[]; emptyCopy: string }) {
+function EntityDiscoveryTable({ rows, emptyCopy, copy }: { rows: DiscoveryTableRow[]; emptyCopy: string; copy: EntityDiscoveryTableCopy }) {
   return (
     <div
       data-entity-discovery-table-shell="cold-dense-table"
       className="overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
     >
       <div className="flex min-h-[42px] items-center justify-between gap-3 border-b border-[#252b34] bg-[#101217] px-3 text-[12px] text-[#8f99ab]">
-        <span className="font-semibold text-[#dbe4f0]">发现线索</span>
-        <span>{rows.length > 0 ? `${rows.length} 条结果` : '等待搜索'}</span>
+        <span className="font-semibold text-[#dbe4f0]">{copy.title}</span>
+        <span>{copy.resultStatus}</span>
       </div>
       <div className="overflow-x-auto">
         <table data-entity-discovery-table="cold-discovery-table" className="min-w-[1120px] w-full table-fixed border-collapse text-left text-[12px] text-[#a9b0bb]">
           <thead className="border-b border-[#252b34] bg-[#101217] text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">
             <tr>
-              <th className="w-[20%] px-3 py-2.5">线索</th>
-              <th className="w-[14%] px-3 py-2.5">实例</th>
-              <th className="w-[10%] px-3 py-2.5">状态</th>
-              <th className="w-[11%] px-3 py-2.5">归属</th>
-              <th className="w-[12%] px-3 py-2.5">系统</th>
-              <th className="w-[9%] px-3 py-2.5">环境</th>
-              <th className="w-[15%] px-3 py-2.5">归因</th>
-              <th className="w-[136px] px-3 py-2.5">操作</th>
+              <th className="w-[20%] px-3 py-2.5">{copy.columns.clue}</th>
+              <th className="w-[14%] px-3 py-2.5">{copy.columns.instance}</th>
+              <th className="w-[10%] px-3 py-2.5">{copy.columns.status}</th>
+              <th className="w-[11%] px-3 py-2.5">{copy.columns.owner}</th>
+              <th className="w-[12%] px-3 py-2.5">{copy.columns.system}</th>
+              <th className="w-[9%] px-3 py-2.5">{copy.columns.environment}</th>
+              <th className="w-[15%] px-3 py-2.5">{copy.columns.attribution}</th>
+              <th className="w-[136px] px-3 py-2.5">{copy.columns.action}</th>
             </tr>
           </thead>
           <tbody>
@@ -98,7 +121,7 @@ function EntityDiscoveryTable({ rows, emptyCopy }: { rows: DiscoveryTableRow[];
                   </td>
                   <td className="px-3 py-2.5 text-[#a9b0bb]">{row.instance}</td>
                   <td className="px-3 py-2.5">
-                    <span className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${statusClassName(row.status)}`}>
+                    <span className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${statusClassName(row.statusTone)}`}>
                       {row.status}
                     </span>
                   </td>
@@ -129,7 +152,7 @@ function EntityDiscoveryTable({ rows, emptyCopy }: { rows: DiscoveryTableRow[];
                     <span className="inline-flex h-10 w-10 items-center justify-center rounded-[4px] border border-[#303743] bg-[#101217] text-[#cbd5e1]">
                       <Search className="h-5 w-5" aria-hidden="true" />
                     </span>
-                    <div className="text-[13px] font-semibold text-[#eef2f7]">先搜索一组需要治理的监控线索</div>
+                    <div className="text-[13px] font-semibold text-[#eef2f7]">{copy.emptyTitle}</div>
                     <div className="text-[12px] leading-5 text-[#8f99ab]">{emptyCopy}</div>
                   </div>
                 </td>
@@ -142,34 +165,57 @@ function EntityDiscoveryTable({ rows, emptyCopy }: { rows: DiscoveryTableRow[];
   );
 }
 
-export function EntityDiscoverySurface({ presets, activities, catalog }: EntityDiscoverySurfaceProps) {
+export function EntityDiscoverySurface({ presets, activities, catalog, candidateContext = null }: EntityDiscoverySurfaceProps) {
   const { t } = useI18n();
-  const [search, setSearch] = useState('');
+  const candidateSearch = candidateContext?.search ?? '';
+  const [search, setSearch] = useState(candidateSearch);
   const [results, setResults] = useState<Monitor[]>([]);
   const [searched, setSearched] = useState(false);
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
 
-  function tr(key: string, fallback: string) {
-    const translated = t(key);
-    return translated === key ? fallback : translated;
-  }
+  useEffect(() => {
+    if (candidateSearch !== '') {
+      setSearch(candidateSearch);
+    }
+  }, [candidateSearch]);
 
   const rows = useMemo(
-    () => (searched && results.length > 0 ? buildDiscoveryTableRows(results, presets, catalog) : []),
-    [catalog, presets, results, searched]
+    () => (searched && results.length > 0 ? buildDiscoveryTableRows(results, presets, catalog, t) : []),
+    [catalog, presets, results, searched, t]
   );
   const sourceCount = (catalog.systems?.length || 0) + (catalog.environments?.length || 0);
-  const latestActivity = localizeActivitySummary(activities[0]?.summary);
-  const searchLabel = tr('entities.discovery.action.search', '搜索');
-  const clearLabel = tr('entities.discovery.action.clear', '清空');
+  const latestActivity = localizeActivitySummary(activities[0]?.summary, t);
+  const searchLabel = t('entities.discovery.action.search');
+  const clearLabel = t('entities.discovery.action.clear');
+  const ownerChipLabel = t('entities.discovery.catalog.owner');
+  const systemChipLabel = t('entities.discovery.catalog.system');
+  const environmentChipLabel = t('entities.discovery.catalog.environment');
   const catalogChips = [
-    ...(catalog.owners || []).slice(0, 2).map(owner => ({ label: owner, type: '负责人' })),
-    ...(catalog.systems || []).slice(0, 2).map(system => ({ label: system, type: '系统' })),
-    ...(catalog.environments || []).slice(0, 1).map(environment => ({ label: environment, type: '环境' }))
+    ...(catalog.owners || []).slice(0, 2).map(owner => ({ label: owner, type: ownerChipLabel })),
+    ...(catalog.systems || []).slice(0, 2).map(system => ({ label: system, type: systemChipLabel })),
+    ...(catalog.environments || []).slice(0, 1).map(environment => ({ label: environment, type: environmentChipLabel }))
   ];
-  const matchedCount = rows.filter(row => row.owner !== '-').length;
-  const createCount = rows.filter(row => row.owner === '-').length;
+  const matchedCount = rows.filter(
+    row => row.attributionState === 'merge' || row.attributionState === 'resolved' || row.attributionState === 'preset'
+  ).length;
+  const createCount = rows.filter(row => row.attributionState === 'create' || row.attributionState === 'review').length;
+  const candidateActionHref = candidateContext != null ? buildDiscoveryCandidateActionHref(candidateContext) : null;
+  const tableCopy: EntityDiscoveryTableCopy = {
+    title: t('entities.discovery.table.title'),
+    resultStatus: rows.length > 0 ? t('entities.discovery.table.result-count', { count: rows.length }) : t('entities.discovery.table.waiting'),
+    emptyTitle: t('entities.discovery.empty.title'),
+    columns: {
+      clue: t('entities.discovery.table.column.clue'),
+      instance: t('entities.discovery.table.column.instance'),
+      status: t('entities.discovery.table.column.status'),
+      owner: t('entities.discovery.table.column.owner'),
+      system: t('entities.discovery.table.column.system'),
+      environment: t('entities.discovery.table.column.environment'),
+      attribution: t('entities.discovery.table.column.attribution'),
+      action: t('entities.discovery.table.column.action')
+    }
+  };
 
   async function runSearch() {
     const submission = resolveDiscoverySearchSubmission(search);
@@ -189,7 +235,7 @@ export function EntityDiscoverySurface({ presets, activities, catalog }: EntityD
       setResults(await searchDiscoveryMonitors(apiMessageGet, submission.normalizedSearch));
     } catch (nextError) {
       setResults([]);
-      setError(nextError instanceof Error ? nextError.message : tr('entities.discovery.search.error', '遥测发现查询失败'));
+      setError(nextError instanceof Error ? nextError.message : t('entities.discovery.search.error'));
     } finally {
       setLoading(false);
     }
@@ -215,23 +261,23 @@ export function EntityDiscoverySurface({ presets, activities, catalog }: EntityD
           <div className="mb-5">
             <div data-entity-discovery-header="cold-compact-header" className={coldEntityDiscoveryVisual.panel.hero}>
               <div className="max-w-[880px]">
-                <div className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">对象优先调查</div>
-                <h1 className="mt-2 text-[30px] font-semibold leading-tight text-[#f5f7fb]">遥测发现</h1>
+                <div className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">{t('entities.discovery.workspace.kicker')}</div>
+                <h1 className="mt-2 text-[30px] font-semibold leading-tight text-[#f5f7fb]">{t('entities.discovery.workspace.title')}</h1>
                 <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">
-                  先搜索一组需要治理的监控线索,再决定归并、完善归属,还是送入定义工作台继续收口。
+                  {t('entities.discovery.workspace.subtitle')}
                 </p>
                 <div data-entity-discovery-command-row="standard-equal-buttons" className={coldEntityDiscoveryVisual.button.row}>
                   <Link href="/entities/import" className={coldPrimaryLinkClassName}>
                     <Upload className="h-3.5 w-3.5" aria-hidden="true" />
-                    从定义创建
+                    {t('entities.discovery.action.import')}
                   </Link>
                   <Link href="/entities/new" className={coldLinkButtonClassName}>
                     <Plus className="h-3.5 w-3.5" aria-hidden="true" />
-                    创建实体
+                    {t('entities.discovery.action.create')}
                   </Link>
                   <Link href="/entities" className={coldLinkButtonClassName}>
                     <Network className="h-3.5 w-3.5" aria-hidden="true" />
-                    对象目录
+                    {t('entities.discovery.action.catalog')}
                   </Link>
                 </div>
               </div>
@@ -241,10 +287,10 @@ export function EntityDiscoverySurface({ presets, activities, catalog }: EntityD
           <div className="space-y-5">
             <div data-entity-discovery-count-strip="cold-inline-counts" className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
               {[
-                ['线索', rows.length, Search],
-                ['已匹配', matchedCount, Users],
-                ['建议新建', createCount, Plus],
-                ['目录来源', sourceCount, Database]
+                [t('entities.discovery.metric.clues'), rows.length, Search],
+                [t('entities.discovery.metric.matched'), matchedCount, Users],
+                [t('entities.discovery.metric.create-suggested'), createCount, Plus],
+                [t('entities.discovery.metric.catalog-sources'), sourceCount, Database]
               ].map(([label, value, Icon]) => {
                 const MetricIcon = Icon as typeof Search;
                 return (
@@ -267,8 +313,8 @@ export function EntityDiscoverySurface({ presets, activities, catalog }: EntityD
                 data-entity-discovery-toolbar="cold-search-row"
                 data-entity-discovery-search-owner="shared-search-row"
                 value={search}
-                placeholder="搜索监控名称或实例"
-                searchLabel={loading ? tr('common.loading', '加载中...') : searchLabel}
+                placeholder={t('entities.discovery.search.placeholder')}
+                searchLabel={loading ? t('common.loading') : searchLabel}
                 clearLabel={clearLabel}
                 inputWidthClassName="w-[360px]"
                 searchDisabled={loading}
@@ -278,6 +324,46 @@ export function EntityDiscoverySurface({ presets, activities, catalog }: EntityD
                 onClear={clearSearch}
               />
 
+              {candidateContext != null && candidateActionHref != null ? (
+                <div
+                  data-entity-discovery-otlp-candidate="query-context"
+                  data-entity-discovery-candidate-identity-key={candidateContext.identityKey}
+                  data-entity-discovery-candidate-identity-value={candidateContext.identityValue}
+                  data-entity-discovery-candidate-service={candidateContext.serviceName}
+                  data-entity-discovery-candidate-namespace={candidateContext.serviceNamespace}
+                  data-entity-discovery-candidate-environment={candidateContext.environment}
+                  className="flex flex-wrap items-center justify-between gap-3 rounded-[4px] border border-[#2b3039] bg-[#101217] px-3 py-2.5 text-[12px] text-[#a9b0bb]"
+                >
+                  <div className="min-w-0">
+                    <div className="text-[13px] font-semibold text-[#eef2f7]">{t('entities.discovery.candidate.title')}</div>
+                    <p className="mt-1 text-[12px] leading-5 text-[#8f99ab]">{t('entities.discovery.candidate.copy')}</p>
+                    <div className="mt-2 flex flex-wrap gap-2 text-[11px] text-[#cbd5e1]">
+                      <span className="rounded-[3px] border border-[#303743] bg-[#0b0c0e] px-2 py-1">
+                        {t('entities.discovery.candidate.identity')} · {candidateContext.identityKey} = {candidateContext.identityValue}
+                      </span>
+                      {candidateContext.serviceNamespace != null ? (
+                        <span className="rounded-[3px] border border-[#303743] bg-[#0b0c0e] px-2 py-1">
+                          {t('entities.discovery.candidate.namespace')} · {candidateContext.serviceNamespace}
+                        </span>
+                      ) : null}
+                      {candidateContext.environment != null ? (
+                        <span className="rounded-[3px] border border-[#303743] bg-[#0b0c0e] px-2 py-1">
+                          {t('entities.discovery.candidate.environment')} · {candidateContext.environment}
+                        </span>
+                      ) : null}
+                    </div>
+                  </div>
+                  <Link
+                    href={candidateActionHref}
+                    data-entity-discovery-candidate-action="create-draft"
+                    className={coldPrimaryLinkClassName}
+                  >
+                    <Plus className="h-3.5 w-3.5" aria-hidden="true" />
+                    {t('entities.discovery.candidate.action.create')}
+                  </Link>
+                </div>
+              ) : null}
+
               {error ? (
                 <div className="rounded-[3px] border border-rose-300/25 bg-rose-300/10 px-3 py-2 text-[12px] text-rose-100">{error}</div>
               ) : null}
@@ -285,10 +371,10 @@ export function EntityDiscoverySurface({ presets, activities, catalog }: EntityD
               <div data-entity-discovery-policy-panel="cold-policy-strip" className="rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3">
                 <div className="flex flex-wrap items-center justify-between gap-3">
                   <div>
-                    <div className="text-[13px] font-semibold text-[#eef2f7]">治理筛选与共享策略</div>
-                    <p className="mt-1 text-[12px] leading-5 text-[#8f99ab]">按已有负责人、系统和环境预设给线索完善上下文。</p>
+                    <div className="text-[13px] font-semibold text-[#eef2f7]">{t('entities.discovery.policy.title')}</div>
+                    <p className="mt-1 text-[12px] leading-5 text-[#8f99ab]">{t('entities.discovery.policy.copy')}</p>
                   </div>
-                  <div className="text-[11px] text-[#858d9a]">最近活动 {latestActivity}</div>
+                  <div className="text-[11px] text-[#858d9a]">{t('entities.discovery.policy.latest-activity', { activity: latestActivity })}</div>
                 </div>
                 <div data-entity-discovery-source-chips="cold-inline-chips" className="mt-3 flex flex-wrap gap-2 text-[11px] text-[#cbd5e1]">
                   {catalogChips.length > 0 ? (
@@ -298,17 +384,18 @@ export function EntityDiscoverySurface({ presets, activities, catalog }: EntityD
                       </span>
                     ))
                   ) : (
-                    <span className="text-[#858d9a]">暂无共享策略,先搜索一条监控线索。</span>
+                    <span className="text-[#858d9a]">{t('entities.discovery.policy.empty')}</span>
                   )}
                 </div>
               </div>
 
               <EntityDiscoveryTable
                 rows={rows}
+                copy={tableCopy}
                 emptyCopy={
                   searched
-                    ? tr('entities.discovery.empty.search.copy', '没有找到匹配的监控线索。')
-                    : tr('entities.discovery.empty.idle.copy', '先搜索一条监控,再把它转成实体草稿。')
+                    ? t('entities.discovery.empty.search.copy')
+                    : t('entities.discovery.empty.idle.copy')
                 }
               />
             </section>
diff --git a/web-next/components/pages/entity-editor-surface.test.tsx b/web-next/components/pages/entity-editor-surface.test.tsx
index 4e40b1fc34..6c7ac9a843 100644
--- a/web-next/components/pages/entity-editor-surface.test.tsx
+++ b/web-next/components/pages/entity-editor-surface.test.tsx
@@ -19,13 +19,128 @@ vi.mock('@/components/providers/i18n-provider', () => ({
       locale: 'zh-CN',
       overrides: {
         'common.button.cancel': '取消',
+        'common.cancel': '取消',
+        'common.save': '保存',
         'common.saving': '保存中',
         'common.remove': '移除',
         'common.item': '项目',
         'common.save-success': '保存成功',
         'common.save-failed': '保存失败',
         'entities.editor.json.invalid': '{{label}} 第 {{index}} 项不是合法 JSON',
-        'entities.editor.message.create-success': '已创建实体 #{{id}}'
+        'entities.editor.message.create-success': '已创建实体 #{{id}}',
+        'entities.editor.submit.create': '创建实体',
+        'entities.editor.stage.basic.label': '基本信息',
+        'entities.editor.stage.basic.description': '先确认类型、名称和核心元数据。',
+        'entities.editor.stage.ownership.label': '归属与处置',
+        'entities.editor.stage.ownership.description': '补齐负责人、联系人和处置入口。',
+        'entities.editor.stage.signals.label': '证据关联',
+        'entities.editor.stage.signals.description': '绑定监控、身份标识和遥测证据。',
+        'entities.editor.stage.relations.label': '关系与扩展',
+        'entities.editor.stage.relations.description': '记录上下游关系、标签和扩展字段。',
+        'entities.editor.route-tab.detail': '实体详情',
+        'entities.editor.route-tab.monitor': '监控中心',
+        'entities.editor.route-tab.logs': '日志管理与统计',
+        'entities.editor.route-tab.traces': '链路',
+        'entities.editor.shell.kicker': '实体',
+        'entities.editor.shell.title.new': '新建实体',
+        'entities.editor.shell.title.edit': '编辑实体',
+        'entities.editor.shell.copy.new': '先在页面中建立实体记录,再逐步补齐证据、负责人和依赖关系。',
+        'entities.editor.shell.copy.edit': '先在页面中建立实体记录,再逐步补充证据、负责人和依赖关系。',
+        'entities.editor.shell.freshness.new': '0% 最近更新',
+        'entities.editor.shell.freshness.edit': '75% 最近更新',
+        'entities.editor.shell.all-entities': '全部实体',
+        'entities.editor.shell.definition-workspace': '定义工作区',
+        'entities.editor.shell.entity-label': '实体',
+        'entities.editor.shell.preview.show': '显示快照',
+        'entities.editor.shell.preview.hide': '隐藏快照',
+        'entities.editor.shell.definition-version': '定义版本 · HertzBeat v1 / OTel 资源规范',
+        'entities.editor.shell.name-label': '名称',
+        'entities.editor.shell.name-placeholder': '实体唯一名称',
+        'entities.editor.shell.type-label': '类型',
+        'entities.editor.shell.entry-source-label': '录入来源',
+        'entities.editor.shell.stage-label': '编辑阶段',
+        'entities.editor.shell.preview-rail.title': '实体元数据',
+        'entities.editor.shell.preview-rail.copy': '查看或粘贴定义,应用到表单。',
+        'entities.editor.shell.definition-content.title': '定义内容',
+        'entities.editor.shell.definition-content.copy': '查看或粘贴定义,应用到表单。',
+        'entities.editor.mode.editor': '表单',
+        'entities.editor.type.system.label': '系统',
+        'entities.editor.type.system.description': '业务域或产品线',
+        'entities.editor.type.service.label': '服务',
+        'entities.editor.type.service.description': '对外提供业务能力的应用或接口',
+        'entities.editor.type.host.label': '主机',
+        'entities.editor.type.host.description': '承载服务的运行节点',
+        'entities.editor.type.database.label': '数据库',
+        'entities.editor.type.database.description': '数据持久化实例',
+        'entities.editor.type.queue.label': '队列',
+        'entities.editor.type.queue.description': '异步消息通道',
+        'entities.editor.type.middleware.label': '中间件',
+        'entities.editor.type.middleware.description': '共享基础组件',
+        'entities.editor.type.api.label': 'API',
+        'entities.editor.type.api.description': '接口或网关资源',
+        'entities.editor.type.endpoint.label': '端点',
+        'entities.editor.type.endpoint.description': '可观测访问入口',
+        'entities.editor.type.device.label': '设备',
+        'entities.editor.type.device.description': '网络或边缘设备',
+        'entities.editor.type.k8s-workload.label': 'K8s 工作负载',
+        'entities.editor.type.k8s-workload.description': 'K8s 原生工作负载',
+        'entities.editor.entry-source.manual': '页面录入',
+        'entities.editor.entry-source.telemetry': '遥测发现',
+        'entities.editor.entry-source.definition': '定义优先',
+        'entities.editor.telemetry-handoff.title': '遥测发现',
+        'entities.editor.telemetry-handoff.copy': '先通过监控和 OTel 资源字段识别实体,再补充基础信息。',
+        'entities.editor.telemetry-handoff.monitor-count': '{{count}} 个监控绑定',
+        'entities.editor.telemetry-handoff.identity-count': '{{count}} 个身份标识',
+        'entities.editor.telemetry-handoff.attribution-check': '归因检查',
+        'entities.editor.field.display-name': '显示名称',
+        'entities.editor.field.namespace': '命名空间',
+        'entities.editor.field.environment': '环境',
+        'entities.editor.field.subtype': '子类型',
+        'entities.editor.field.owner': '负责人',
+        'entities.editor.field.system': '所属系统',
+        'entities.editor.field.source': '来源',
+        'entities.editor.field.description': '描述',
+        'entities.editor.field.description-placeholder': '例如负责交易结算和支付编排。',
+        'entities.editor.field.runbook': '运行手册',
+        'entities.editor.placeholder.display-name': '例如 Checkout API',
+        'entities.editor.placeholder.namespace': '例如 commerce',
+        'entities.editor.placeholder.environment': '例如 prod',
+        'entities.editor.placeholder.subtype': '例如 http',
+        'entities.editor.placeholder.owner': '例如 payments-team',
+        'entities.editor.placeholder.system': '例如 website',
+        'entities.editor.placeholder.source': 'manual',
+        'entities.editor.contact.title': '联系人',
+        'entities.editor.contact.add': '新增联系人',
+        'entities.editor.contact.name-placeholder': '联系人',
+        'entities.editor.contact.type-placeholder': '类型',
+        'entities.editor.contact.value-placeholder': '联系方式',
+        'entities.editor.contact.contact-placeholder': '联系字段',
+        'entities.editor.link.title': '链接',
+        'entities.editor.link.add': '新增链接',
+        'entities.editor.link.name-placeholder': '链接名称',
+        'entities.editor.link.type-placeholder': '类型',
+        'entities.editor.link.provider-placeholder': '提供方',
+        'entities.editor.link.url-placeholder': '链接地址',
+        'entities.editor.collection.identities': '身份标识',
+        'entities.editor.collection.identities.add': '新增身份标识',
+        'entities.editor.collection.monitor-binds': '监控绑定',
+        'entities.editor.collection.monitor-binds.add': '新增监控绑定',
+        'entities.editor.collection.relations': '关系',
+        'entities.editor.collection.relations.add': '新增关系',
+        'entities.editor.relation.component-of': '上级组件',
+        'entities.editor.relation.components': '下级组件',
+        'entities.editor.relation.implemented-by': '实现服务',
+        'entities.editor.relation.languages': '开发语言',
+        'entities.editor.relation.component-of-placeholder': 'payment-platform, commerce',
+        'entities.editor.relation.components-placeholder': 'checkout-ui, order-router',
+        'entities.editor.relation.implemented-by-placeholder': 'checkout-api, order-worker',
+        'entities.editor.relation.languages-placeholder': 'java, typescript',
+        'entities.editor.relation.labels': '标签',
+        'entities.editor.relation.label-key': '标签键',
+        'entities.editor.relation.label-value': '标签值',
+        'entities.editor.relation.label-add': '新增标签',
+        'entities.editor.definition.aria.yaml': 'YAML 定义快照',
+        'entities.editor.definition.aria.json': 'JSON 定义快照'
       }
     })
   })
@@ -390,7 +505,7 @@ describe('EntityEditorSurface', () => {
     expect(source).not.toContain('<KeyValueEditor label="Labels"');
     expect(source).not.toContain('Provider"');
     expect(source).not.toContain('Telemetry Discovery</div>');
-  });
+  }, 30000);
 
   it('renders the edit shell with the same composer structure, save semantics, and definition handoff', async () => {
     const { EntityEditorSurface } = await import('./entity-editor-surface');
@@ -501,4 +616,53 @@ describe('EntityEditorSurface', () => {
     expect(html).not.toContain('Telemetry Discovery');
     expect(html).not.toContain('bound monitor');
   });
+
+  it('renders an OTLP candidate draft handoff with identity return context and no monitor requirement', async () => {
+    const { EntityEditorSurface } = await import('./entity-editor-surface');
+
+    const html = renderToStaticMarkup(
+      <EntityEditorSurface
+        initial={{
+          entity: {
+            type: 'service',
+            name: 'billing-api',
+            displayName: 'billing-api',
+            namespace: 'commerce',
+            environment: 'prod',
+            owner: '',
+            source: 'otel_resource',
+            labels: {
+              'hertzbeat.discovery.source': 'otlp-candidate'
+            }
+          },
+          identities: [
+            { identityType: 'otel_resource', identityKey: 'service.name', identityValue: 'billing', primaryIdentity: true },
+            { identityType: 'otel_resource', identityKey: 'service.namespace', identityValue: 'commerce' },
+            { identityType: 'otel_resource', identityKey: 'deployment.environment.name', identityValue: 'prod' }
+          ],
+          monitorBinds: [],
+          relations: []
+        }}
+        mode="new"
+        catalogSuggestions={{ owners: ['platform'], systems: ['commerce'], environments: ['prod'] }}
+      />
+    );
+
+    expect(html).toContain('data-entity-editor-telemetry-handoff="true"');
+    expect(html).toContain('data-entity-editor-telemetry-handoff-source="otlp-candidate"');
+    expect(html).toContain('0 个监控绑定');
+    expect(html).toContain('3 个身份标识');
+    expect(html).toContain(
+      'href="/entities/discovery?identityKey=service.name&amp;identityValue=billing&amp;serviceName=billing-api&amp;serviceNamespace=commerce&amp;environment=prod"'
+    );
+    expect(html).toContain('aria-label="名称"');
+    expect(html).toContain('value="billing-api"');
+    expect(html).toContain('aria-label="命名空间"');
+    expect(html).toContain('value="commerce"');
+    expect(html).toContain('aria-label="环境"');
+    expect(html).toContain('value="prod"');
+    expect(html).not.toContain('已归属');
+    expect(html).not.toContain('健康正常');
+    expect(html).not.toContain('拓扑已确认');
+  }, 30000);
 });
diff --git a/web-next/components/pages/entity-editor-surface.tsx b/web-next/components/pages/entity-editor-surface.tsx
index 9111d183bd..8f39ff6f69 100644
--- a/web-next/components/pages/entity-editor-surface.tsx
+++ b/web-next/components/pages/entity-editor-surface.tsx
@@ -16,7 +16,7 @@ import {
   Server,
   type LucideIcon
 } from 'lucide-react';
-import { useCallback, useMemo, useState } from 'react';
+import { useMemo, useState } from 'react';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { Button, buttonVariants } from '@/components/ui/button';
 import { Input } from '@/components/ui/input';
@@ -71,14 +71,14 @@ function hasAnyStructuredValue(item: object) {
   });
 }
 
-function attributionStateLabel(state: string) {
+function attributionStateLabel(state: string, t: (key: string) => string) {
   if (state === 'ready') {
-    return '已就绪';
+    return t('entities.editor.attribution.state.ready');
   }
   if (state === 'missing') {
-    return '需补齐';
+    return t('entities.editor.attribution.state.missing');
   }
-  return '待确认';
+  return t('entities.editor.attribution.state.review');
 }
 
 function attributionStateClassName(state: string) {
@@ -114,6 +114,60 @@ function buildEntityDefinitionPreview(payload: EntityDto, format: 'yaml' | 'json
   return lines.join('\n');
 }
 
+function readIdentityValue(identities: unknown[], identityKey: string) {
+  for (const identity of identities) {
+    if (typeof identity !== 'object' || identity == null) {
+      continue;
+    }
+    const record = identity as Record<string, unknown>;
+    const key = typeof record.identityKey === 'string' ? record.identityKey : typeof record.key === 'string' ? record.key : null;
+    const value = typeof record.identityValue === 'string' ? record.identityValue : typeof record.value === 'string' ? record.value : null;
+    if (key === identityKey && value != null && value.trim() !== '') {
+      return value.trim();
+    }
+  }
+  return undefined;
+}
+
+function buildOtlpCandidateDiscoveryHref(payload: EntityDto) {
+  const labels = payload.entity.labels || {};
+  if (labels['hertzbeat.discovery.source'] !== 'otlp-candidate') {
+    return null;
+  }
+
+  const identities = Array.isArray(payload.identities) ? payload.identities : [];
+  const identityKey =
+    identities
+      .map(identity => (typeof identity === 'object' && identity != null ? (identity as Record<string, unknown>).identityKey : null))
+      .find(key => key === 'service.name' || key === 'host.name' || key === 'k8s.workload.name' || key === 'endpoint.url') ||
+    (typeof (identities[0] as Record<string, unknown> | undefined)?.identityKey === 'string'
+      ? (identities[0] as Record<string, unknown>).identityKey
+      : null);
+  const identityValue = typeof identityKey === 'string' ? readIdentityValue(identities, identityKey) : undefined;
+
+  if (typeof identityKey !== 'string' || identityValue == null) {
+    return null;
+  }
+
+  const params = new URLSearchParams();
+  params.set('identityKey', identityKey);
+  params.set('identityValue', identityValue);
+
+  if (hasNonEmptyText(payload.entity.name)) {
+    params.set('serviceName', payload.entity.name!.trim());
+  }
+  const namespace = payload.entity.namespace || readIdentityValue(identities, 'service.namespace');
+  if (hasNonEmptyText(namespace)) {
+    params.set('serviceNamespace', namespace!.trim());
+  }
+  const environment = payload.entity.environment || readIdentityValue(identities, 'deployment.environment.name');
+  if (hasNonEmptyText(environment)) {
+    params.set('environment', environment!.trim());
+  }
+
+  return `/entities/discovery?${params.toString()}`;
+}
+
 function MultiValueField({
   label,
   value,
@@ -425,11 +479,6 @@ export function EntityEditorSurface({
 }) {
   const { t } = useI18n();
 
-  const tr = useCallback((key: string, fallback: string) => {
-    const translated = t(key);
-    return translated === key ? fallback : translated;
-  }, [t]);
-
   const initialFormState = buildEntityEditorFormState(initial);
   const [draft, setDraft] = useState<EntityDto>(initial);
   const [labelRows, setLabelRows] = useState<KeyValueDraft[]>(initialFormState.labelRows);
@@ -509,6 +558,7 @@ export function EntityEditorSurface({
     const monitorBinds = Array.isArray(previewPayload.monitorBinds) ? previewPayload.monitorBinds : [];
     const identities = Array.isArray(previewPayload.identities) ? previewPayload.identities : [];
     const isTelemetryDraft = (previewPayload.entity.source || '').trim() === 'otel_resource';
+    const handoffSource = previewPayload.entity.labels?.['hertzbeat.discovery.source'] === 'otlp-candidate' ? 'otlp-candidate' : 'telemetry';
 
     if (!isTelemetryDraft && monitorBinds.length === 0 && identities.length === 0) {
       return null;
@@ -521,34 +571,40 @@ export function EntityEditorSurface({
         : undefined;
 
     return {
-      title: tr('entity.entry-source.telemetry', '遥测发现'),
-      copy: tr(
-        'entity.entry-source.telemetry.desc',
-        '先通过监控和 OTel 资源字段识别实体,再补充基础信息。'
-      ),
+      title: t('entities.editor.telemetry-handoff.title'),
+      copy: t('entities.editor.telemetry-handoff.copy'),
       monitorCount: monitorBinds.length,
+      monitorCountLabel: t('entities.editor.telemetry-handoff.monitor-count', {
+        count: monitorBinds.length
+      }),
       identityCount: identities.length,
+      identityCountLabel: t('entities.editor.telemetry-handoff.identity-count', {
+        count: identities.length
+      }),
+      source: handoffSource,
       discoveryHref:
-        resolvedMonitorId != null
+        handoffSource === 'otlp-candidate'
+          ? buildOtlpCandidateDiscoveryHref(previewPayload) || '/entities/discovery'
+          : resolvedMonitorId != null
           ? `/entities/discovery?source=telemetry&monitorId=${encodeURIComponent(String(resolvedMonitorId))}`
           : '/entities/discovery'
     };
-  }, [previewPayload, tr]);
+  }, [previewPayload, t]);
 
-  const attributionRows = useMemo(() => buildEntityEditorAttributionRows(previewPayload), [previewPayload]);
+  const attributionRows = useMemo(() => buildEntityEditorAttributionRows(previewPayload, t), [previewPayload, t]);
 
   const stageRows = useMemo<EntityEditorStageRow[]>(
     () => [
       {
         key: 'basic',
-        title: tr('entity.guide.profile', '基本信息'),
-        description: tr('entity.guide.profile.desc', '选择实体类型,并填写稳定的实体名称。'),
+        title: t('entities.editor.stage.basic.label'),
+        description: t('entities.editor.stage.basic.description'),
         done: hasNonEmptyText(draft.entity.type) && hasNonEmptyText(draft.entity.name)
       },
       {
         key: 'ownership',
-        title: tr('entity.guide.owner', '归属处置'),
-        description: tr('entity.guide.owner.desc', '补齐负责人或运行手册,让值班人员知道下一步动作。'),
+        title: t('entities.editor.stage.ownership.label'),
+        description: t('entities.editor.stage.ownership.description'),
         done:
           hasNonEmptyText(draft.entity.owner) ||
           hasNonEmptyText(draft.entity.runbook) ||
@@ -558,14 +614,14 @@ export function EntityEditorSurface({
       },
       {
         key: 'signals',
-        title: tr('entity.guide.evidence', '证据关联'),
-        description: tr('entity.guide.evidence.desc', '关联监控和身份标识,让告警与日志可以汇聚到该实体。'),
+        title: t('entities.editor.stage.signals.label'),
+        description: t('entities.editor.stage.signals.description'),
         done: (previewPayload.identities?.length || 0) > 0 || (previewPayload.monitorBinds?.length || 0) > 0
       },
       {
         key: 'relations',
-        title: tr('entity.guide.context', '关系上下文'),
-        description: tr('entity.guide.context.desc', '补充拓扑、备注和标签,让故障复盘更容易阅读。'),
+        title: t('entities.editor.stage.relations.label'),
+        description: t('entities.editor.stage.relations.description'),
         done:
           hasNonEmptyText(componentOfText) ||
           hasNonEmptyText(componentsText) ||
@@ -578,7 +634,7 @@ export function EntityEditorSurface({
           (previewPayload.relations?.length || 0) > 0
       }
     ],
-    [componentOfText, componentsText, contacts, draft.entity.criticality, draft.entity.inheritFrom, draft.entity.name, draft.entity.owner, draft.entity.runbook, draft.entity.type, implementedByText, labelRows, languagesText, links, owners, previewPayload.identities, previewPayload.monitorBinds, previewPayload.relations, tagsText, tr]
+    [componentOfText, componentsText, contacts, draft.entity.criticality, draft.entity.inheritFrom, draft.entity.name, draft.entity.owner, draft.entity.runbook, draft.entity.type, implementedByText, labelRows, languagesText, links, owners, previewPayload.identities, previewPayload.monitorBinds, previewPayload.relations, tagsText, t]
   );
 
   const isCompleteContextStage = mode === 'edit' && Boolean(entityId);
@@ -644,39 +700,62 @@ export function EntityEditorSurface({
 
   if (mode === 'new' || mode === 'edit') {
     const isEditMode = mode === 'edit';
-    const editorTitle = isEditMode ? '编辑实体' : '新建实体';
+    const editorTitle = isEditMode ? t('entities.editor.shell.title.edit') : t('entities.editor.shell.title.new');
     const editorCopy = isEditMode
-      ? '先在页面中建立实体记录,再逐步补充证据、负责人和依赖关系。'
-      : '先在页面中建立实体记录,再逐步补齐证据、负责人和依赖关系。';
-    const editorFreshness = isEditMode ? '75% 最近更新' : '0% 最近更新';
-    const submitLabel = isEditMode ? '保存' : '创建实体';
+      ? t('entities.editor.shell.copy.edit')
+      : t('entities.editor.shell.copy.new');
+    const editorFreshness = isEditMode ? t('entities.editor.shell.freshness.edit') : t('entities.editor.shell.freshness.new');
+    const submitLabel = isEditMode ? t('common.save') : t('entities.editor.submit.create');
+    const entityNameLabel = t('entities.editor.shell.name-label');
     const coldFieldLabelTextClassName = 'text-[12px] font-semibold text-[#8d95a5]';
     const coldInputClassName =
       'h-8 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] text-[#dbe4f0] placeholder:text-[#6f7787] focus-visible:border-[#4e74f8] focus-visible:bg-[#151923] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[rgba(78,116,248,0.12)]';
     const coldNameInputClassName =
       'h-8 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[13px] text-[#f5f7fb] placeholder:text-[#6f7787] focus-visible:border-[#4e74f8] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[rgba(78,116,248,0.12)]';
     const entityTypeCards: EntityTypeCard[] = [
-      { value: 'system', label: '系统', description: '业务域或产品线', icon: Network },
-      { value: 'service', label: '服务', description: '对外提供业务能力的应用或接口', icon: Cable },
-      { value: 'host', label: '主机', description: '承载服务的运行节点', icon: Monitor },
-      { value: 'database', label: '数据库', description: '数据持久化实例', icon: Database },
-      { value: 'queue', label: '队列', description: '异步消息通道', icon: ListTree },
-      { value: 'middleware', label: '中间件', description: '共享基础组件', icon: Server },
-      { value: 'api', label: 'API', description: '接口或网关资源', icon: Braces },
-      { value: 'endpoint', label: '端点', description: '可观测访问入口', icon: Globe2 },
-      { value: 'device', label: '设备', description: '网络或边缘设备', icon: Layers3 },
-      { value: 'k8s_workload', label: 'K8s 工作负载', description: 'K8s 原生工作负载', icon: Boxes }
+      { value: 'system', label: t('entities.editor.type.system.label'), description: t('entities.editor.type.system.description'), icon: Network },
+      { value: 'service', label: t('entities.editor.type.service.label'), description: t('entities.editor.type.service.description'), icon: Cable },
+      { value: 'host', label: t('entities.editor.type.host.label'), description: t('entities.editor.type.host.description'), icon: Monitor },
+      { value: 'database', label: t('entities.editor.type.database.label'), description: t('entities.editor.type.database.description'), icon: Database },
+      { value: 'queue', label: t('entities.editor.type.queue.label'), description: t('entities.editor.type.queue.description'), icon: ListTree },
+      { value: 'middleware', label: t('entities.editor.type.middleware.label'), description: t('entities.editor.type.middleware.description'), icon: Server },
+      { value: 'api', label: t('entities.editor.type.api.label'), description: t('entities.editor.type.api.description'), icon: Braces },
+      { value: 'endpoint', label: t('entities.editor.type.endpoint.label'), description: t('entities.editor.type.endpoint.description'), icon: Globe2 },
+      { value: 'device', label: t('entities.editor.type.device.label'), description: t('entities.editor.type.device.description'), icon: Layers3 },
+      { value: 'k8s_workload', label: t('entities.editor.type.k8s-workload.label'), description: t('entities.editor.type.k8s-workload.description'), icon: Boxes }
     ];
     const entrySources: Array<{ value: 'manual' | 'telemetry' | 'definition'; label: string; icon: LucideIcon }> = [
-      { value: 'manual', label: '页面录入', icon: PencilLine },
-      { value: 'telemetry', label: '遥测发现', icon: Network },
-      { value: 'definition', label: '定义优先', icon: Braces }
+      { value: 'manual', label: t('entities.editor.entry-source.manual'), icon: PencilLine },
+      { value: 'telemetry', label: t('entities.editor.entry-source.telemetry'), icon: Network },
+      { value: 'definition', label: t('entities.editor.entry-source.definition'), icon: Braces }
     ];
     const editorStages = [
-      { key: 'basic' as const, label: '基本信息', description: '先确认类型、名称和核心元数据。' },
-      { key: 'ownership' as const, label: '归属与处置', description: '补齐负责人、联系人和处置入口。' },
-      { key: 'signals' as const, label: '证据关联', description: '绑定监控、身份标识和遥测证据。' },
-      { key: 'relations' as const, label: '关系与扩展', description: '记录上下游关系、标签和扩展字段。' }
+      {
+        key: 'basic' as const,
+        label: t('entities.editor.stage.basic.label'),
+        description: t('entities.editor.stage.basic.description')
+      },
+      {
+        key: 'ownership' as const,
+        label: t('entities.editor.stage.ownership.label'),
+        description: t('entities.editor.stage.ownership.description')
+      },
+      {
+        key: 'signals' as const,
+        label: t('entities.editor.stage.signals.label'),
+        description: t('entities.editor.stage.signals.description')
+      },
+      {
+        key: 'relations' as const,
+        label: t('entities.editor.stage.relations.label'),
+        description: t('entities.editor.stage.relations.description')
+      }
+    ];
+    const routeTabs = [
+      { key: 'detail', label: t('entities.editor.route-tab.detail') },
+      { key: 'monitor', label: t('entities.editor.route-tab.monitor') },
+      { key: 'logs', label: t('entities.editor.route-tab.logs') },
+      { key: 'traces', label: t('entities.editor.route-tab.traces') }
     ];
     const activeStageCopy = editorStages.find(stage => stage.key === activeStage) || editorStages[0];
     const selectedEntrySource =
@@ -688,6 +767,11 @@ export function EntityEditorSurface({
     const bodyDataAttrs = detailsExpanded
       ? { 'data-entity-editor-body': 'cold-single-stage' }
       : { 'data-entity-editor-body-placement': 'cold-deferred-body' };
+    const definitionModeTabs = [
+      { value: 'editor', label: t('entities.editor.mode.editor') },
+      { value: 'yaml', label: 'YAML' },
+      { value: 'json', label: 'JSON' }
+    ];
 
     const renderField = (
       label: string,
@@ -724,14 +808,14 @@ export function EntityEditorSurface({
           className="grid gap-3 text-[#dbe4f0]"
         >
           <header data-entity-editor-header="cold-compact-header" className="grid gap-2">
-            <div className="text-[12px] font-medium text-[#98a2b3]">实体</div>
+            <div className="text-[12px] font-medium text-[#98a2b3]">{t('entities.editor.shell.kicker')}</div>
             <div data-entity-editor-header-rhythm="cold-compact" className="mb-1 grid gap-2">
               <div className="text-[24px] font-semibold leading-none text-[#f5f7fb]">{editorTitle}</div>
               <p className="text-[13px] leading-5 text-[#a8b0bf]">{editorCopy}</p>
               <div data-entity-editor-route-tabs="cold-segmented-tabs" className="flex flex-wrap gap-2">
-                {['实体详情', '监控中心', '日志管理与统计', '链路'].map((tab, index) => (
+                {routeTabs.map((tab, index) => (
                   <span
-                    key={tab}
+                    key={tab.key}
                     className={cn(
                       'inline-flex h-8 items-center border px-3 text-[12px] font-semibold',
                       index === 0
@@ -739,7 +823,7 @@ export function EntityEditorSurface({
                         : 'rounded-[3px] border-[#2b3039] bg-[#101217] text-[#98a2b3]'
                     )}
                   >
-                    {tab}
+                    {tab.label}
                   </span>
                 ))}
               </div>
@@ -759,7 +843,7 @@ export function EntityEditorSurface({
                 <div className="grid gap-2">
                   <Link href="/entities" className="inline-flex items-center gap-2 text-[12px] font-semibold text-[#d8e4ff]">
                     <span aria-hidden="true">←</span>
-                    全部实体
+                    {t('entities.editor.shell.all-entities')}
                   </Link>
                   {isEditMode && entityId ? (
                     <Link
@@ -767,11 +851,11 @@ export function EntityEditorSurface({
                       data-entity-editor-definition-handoff="cold-hidden"
                       className="sr-only"
                     >
-                      定义工作区
+                      {t('entities.editor.shell.definition-workspace')}
                     </Link>
                   ) : null}
                   <div className="grid gap-1">
-                    <div className="text-[11px] font-semibold text-[#8d95a5]">实体</div>
+                    <div className="text-[11px] font-semibold text-[#8d95a5]">{t('entities.editor.shell.entity-label')}</div>
                     <div className="text-[18px] font-semibold leading-none text-[#f5f7fb]">{editorTitle}</div>
                   </div>
                 </div>
@@ -785,10 +869,10 @@ export function EntityEditorSurface({
                     onClick={() => setPreviewRailCollapsed(current => !current)}
                   >
                     <ListTree size={14} aria-hidden="true" data-entity-editor-preview-toggle-icon="cold" />
-                    {previewRailCollapsed ? '显示快照' : '隐藏快照'}
+                    {previewRailCollapsed ? t('entities.editor.shell.preview.show') : t('entities.editor.shell.preview.hide')}
                   </Button>
                   <span className="inline-flex min-h-8 items-center rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[11px] text-[#98a2b3]">
-                    定义版本 · HertzBeat v1 / OTel 资源规范
+                    {t('entities.editor.shell.definition-version')}
                   </span>
                   <span className="inline-flex min-h-8 items-center rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[11px] text-[#98a2b3]">
                     {editorFreshness}
@@ -798,14 +882,14 @@ export function EntityEditorSurface({
 
               <div className="grid gap-2 px-4 pt-3">
                 <label className="grid gap-2">
-                  <span className={coldFieldLabelTextClassName}>名称</span>
+                  <span className={coldFieldLabelTextClassName}>{entityNameLabel}</span>
                   <Input
-                    aria-label="名称"
+                    aria-label={entityNameLabel}
                     className={coldNameInputClassName}
                     required
                     maxLength={128}
                     value={draft.entity.name || ''}
-                    placeholder="实体唯一名称"
+                    placeholder={t('entities.editor.shell.name-placeholder')}
                     onChange={event => updateEntityField('name', event.target.value)}
                   />
                 </label>
@@ -814,7 +898,7 @@ export function EntityEditorSurface({
               <div className="grid grid-cols-[minmax(0,1fr)_380px] gap-4 px-4 pb-3 pt-2 max-xl:grid-cols-1">
                 <div className="grid gap-4">
                   <div className="grid gap-2">
-                    <div className="text-[11px] font-semibold text-[#8d95a5]">类型</div>
+                    <div className="text-[11px] font-semibold text-[#8d95a5]">{t('entities.editor.shell.type-label')}</div>
                     <div
                       data-entity-editor-type-strip="cold-catalog-grid"
                       data-entity-editor-type-strip-layout="cold-compact-grid"
@@ -852,7 +936,7 @@ export function EntityEditorSurface({
                   </div>
 
                   <div className="flex flex-wrap items-center justify-between gap-3">
-                    <div className="text-[11px] font-semibold text-[#8d95a5]">录入来源</div>
+                    <div className="text-[11px] font-semibold text-[#8d95a5]">{t('entities.editor.shell.entry-source-label')}</div>
                     <div data-entity-editor-entry-strip="cold-segmented-pills" className="inline-flex rounded-[3px] border border-[#2b3039] bg-[#101217] p-[3px]">
                       {entrySources.map(source => {
                         const EntryIcon = source.icon;
@@ -879,7 +963,7 @@ export function EntityEditorSurface({
                   </div>
 
                   <div className="grid gap-2">
-                    <div className="text-[11px] font-semibold text-[#8d95a5]">编辑阶段</div>
+                    <div className="text-[11px] font-semibold text-[#8d95a5]">{t('entities.editor.shell.stage-label')}</div>
                     <div
                       data-entity-editor-stage-strip="cold-stage-grid"
                       data-entity-editor-edit-stage-posture={isCompleteContextStage ? 'cold-complete-context' : undefined}
@@ -928,8 +1012,8 @@ export function EntityEditorSurface({
                     data-entity-editor-preview-rail="cold-inline-preview"
                     data-entity-editor-preview-rail-density="cold-inline-preview"
                   >
-                    <div className="text-[14px] font-semibold text-[#f5f7fb]">实体元数据</div>
-                    <p className="mt-2 text-[13px] leading-5 text-[#98a2b3]">查看或粘贴定义,应用到表单。</p>
+                    <div className="text-[14px] font-semibold text-[#f5f7fb]">{t('entities.editor.shell.preview-rail.title')}</div>
+                    <p className="mt-2 text-[13px] leading-5 text-[#98a2b3]">{t('entities.editor.shell.preview-rail.copy')}</p>
                   </aside>
                 ) : null}
               </div>
@@ -941,15 +1025,11 @@ export function EntityEditorSurface({
               className="mt-3 flex flex-wrap items-end justify-between gap-3 border-t border-[#2b3039] pt-3"
             >
               <div>
-                <div className="text-[12px] font-semibold text-[#dbe4f0]">定义内容</div>
-                <div className="mt-1 text-[12px] leading-5 text-[#98a2b3]">查看或粘贴定义,应用到表单。</div>
+                <div className="text-[12px] font-semibold text-[#dbe4f0]">{t('entities.editor.shell.definition-content.title')}</div>
+                <div className="mt-1 text-[12px] leading-5 text-[#98a2b3]">{t('entities.editor.shell.definition-content.copy')}</div>
               </div>
               <div data-entity-editor-definition-tabs="cold-bottom-tabs" className="inline-flex overflow-hidden rounded-[3px] border border-[#2b3039]">
-                {[
-                  ['editor', '表单'],
-                  ['yaml', 'YAML'],
-                  ['json', 'JSON']
-                ].map(([value, label]) => (
+                {definitionModeTabs.map(({ value, label }) => (
                   <button
                     key={value}
                     type="button"
@@ -986,6 +1066,7 @@ export function EntityEditorSurface({
             {telemetryHandoff ? (
               <div
                 data-entity-editor-telemetry-handoff="true"
+                data-entity-editor-telemetry-handoff-source={telemetryHandoff.source}
                 className="mb-4 grid gap-3 rounded-[4px] border border-[#2b3039] bg-[#101217] px-3 py-3"
               >
                 <div className="flex flex-wrap items-center justify-between gap-3">
@@ -995,10 +1076,10 @@ export function EntityEditorSurface({
                   </div>
                   <div className="flex flex-wrap gap-2 text-[12px] font-semibold">
                     <span className="rounded-[3px] border border-[#31405c] bg-[#182238] px-2.5 py-1 text-[#d8e4ff]">
-                      {`${telemetryHandoff.monitorCount} 个监控绑定`}
+                      {telemetryHandoff.monitorCountLabel}
                     </span>
                     <span className="rounded-[3px] border border-[#2b3039] bg-[#0b0c0e] px-2.5 py-1 text-[#98a2b3]">
-                      {`${telemetryHandoff.identityCount} 个身份标识`}
+                      {telemetryHandoff.identityCountLabel}
                     </span>
                     <Link href={telemetryHandoff.discoveryHref} className="rounded-[3px] border border-[#31405c] bg-[#182238] px-2.5 py-1 text-[#d8e4ff]">
                       {telemetryHandoff.title}
@@ -1009,7 +1090,7 @@ export function EntityEditorSurface({
                   data-entity-editor-attribution-panel="telemetry-attribution-check"
                   className="grid gap-2 border-t border-[#2b3039] pt-3"
                 >
-                  <div className="text-[12px] font-semibold text-[#dbe4f0]">归因检查</div>
+                  <div className="text-[12px] font-semibold text-[#dbe4f0]">{t('entities.editor.telemetry-handoff.attribution-check')}</div>
                   <div className="grid gap-2 md:grid-cols-5">
                     {attributionRows.map(row => (
                       <div
@@ -1021,7 +1102,7 @@ export function EntityEditorSurface({
                         <div className="flex items-center justify-between gap-2">
                           <div className="text-[12px] font-semibold text-[#f5f7fb]">{row.title}</div>
                           <span className={cn('rounded-[3px] border px-1.5 py-0.5 text-[10px] font-semibold', attributionStateClassName(row.state))}>
-                            {attributionStateLabel(row.state)}
+                            {attributionStateLabel(row.state, t)}
                           </span>
                         </div>
                         <div className="text-[12px] text-[#dbe4f0]">{row.copy}</div>
@@ -1044,21 +1125,21 @@ export function EntityEditorSurface({
                 {activeStage === 'basic' ? (
                   <>
                     <div className="grid gap-3 md:grid-cols-3">
-                      {renderField('显示名称', draft.entity.displayName || '', '例如 Checkout API', value => updateEntityField('displayName', value))}
-                      {renderField('命名空间', draft.entity.namespace || '', '例如 commerce', value => updateEntityField('namespace', value))}
-                      {renderField('环境', draft.entity.environment || '', '例如 prod', value => updateEntityField('environment', value))}
-                      {renderField('子类型', draft.entity.subtype || '', '例如 http', value => updateEntityField('subtype', value))}
-                      {renderField('负责人', draft.entity.owner || '', '例如 payments-team', value => updateEntityField('owner', value))}
-                      {renderField('所属系统', draft.entity.system || '', '例如 website', value => updateEntityField('system', value))}
-                      {renderField('来源', draft.entity.source || '', 'manual', value => updateEntityField('source', value))}
+                      {renderField(t('entities.editor.field.display-name'), draft.entity.displayName || '', t('entities.editor.placeholder.display-name'), value => updateEntityField('displayName', value))}
+                      {renderField(t('entities.editor.field.namespace'), draft.entity.namespace || '', t('entities.editor.placeholder.namespace'), value => updateEntityField('namespace', value))}
+                      {renderField(t('entities.editor.field.environment'), draft.entity.environment || '', t('entities.editor.placeholder.environment'), value => updateEntityField('environment', value))}
+                      {renderField(t('entities.editor.field.subtype'), draft.entity.subtype || '', t('entities.editor.placeholder.subtype'), value => updateEntityField('subtype', value))}
+                      {renderField(t('entities.editor.field.owner'), draft.entity.owner || '', t('entities.editor.placeholder.owner'), value => updateEntityField('owner', value))}
+                      {renderField(t('entities.editor.field.system'), draft.entity.system || '', t('entities.editor.placeholder.system'), value => updateEntityField('system', value))}
+                      {renderField(t('entities.editor.field.source'), draft.entity.source || '', t('entities.editor.placeholder.source'), value => updateEntityField('source', value))}
                     </div>
                     <label className="grid gap-2">
-                      <span className={coldFieldLabelTextClassName}>描述</span>
+                      <span className={coldFieldLabelTextClassName}>{t('entities.editor.field.description')}</span>
                       <Textarea
                         data-entity-editor-description-textarea="cold-textarea"
                         className="min-h-[112px] border-[#2b3039] bg-[#101217] text-[#dbe4f0] placeholder:text-[#6f7787] focus-visible:border-[#4e74f8]"
                         value={draft.entity.description || ''}
-                        placeholder="例如负责交易结算和支付编排。"
+                        placeholder={t('entities.editor.field.description-placeholder')}
                         onChange={event => updateEntityField('description', event.target.value)}
                       />
                     </label>
@@ -1067,48 +1148,48 @@ export function EntityEditorSurface({
 
                 {activeStage === 'ownership' ? (
                   <div className="grid gap-3 md:grid-cols-2">
-                    {renderField('负责人', draft.entity.owner || '', 'payments-team', value => updateEntityField('owner', value))}
-                    {renderField('运行手册', draft.entity.runbook || '', 'https://runbook.internal/...', value => updateEntityField('runbook', value))}
+                    {renderField(t('entities.editor.field.owner'), draft.entity.owner || '', 'payments-team', value => updateEntityField('owner', value))}
+                    {renderField(t('entities.editor.field.runbook'), draft.entity.runbook || '', 'https://runbook.internal/...', value => updateEntityField('runbook', value))}
                     <ContactEditor
                       value={contacts}
                       onChange={setContacts}
-                      title="联系人"
-                      addLabel="新增联系人"
+                      title={t('entities.editor.contact.title')}
+                      addLabel={t('entities.editor.contact.add')}
                       deleteLabel={t('common.remove')}
-                      namePlaceholder="联系人"
-                      typePlaceholder="类型"
-                      valuePlaceholder="联系方式"
-                      contactPlaceholder="联系字段"
+                      namePlaceholder={t('entities.editor.contact.name-placeholder')}
+                      typePlaceholder={t('entities.editor.contact.type-placeholder')}
+                      valuePlaceholder={t('entities.editor.contact.value-placeholder')}
+                      contactPlaceholder={t('entities.editor.contact.contact-placeholder')}
                     />
                     <LinkEditor
                       value={links}
                       onChange={setLinks}
-                      title="链接"
-                      addLabel="新增链接"
+                      title={t('entities.editor.link.title')}
+                      addLabel={t('entities.editor.link.add')}
                       deleteLabel={t('common.remove')}
-                      namePlaceholder="链接名称"
-                      typePlaceholder="类型"
-                      providerPlaceholder="提供方"
-                      urlPlaceholder="链接地址"
+                      namePlaceholder={t('entities.editor.link.name-placeholder')}
+                      typePlaceholder={t('entities.editor.link.type-placeholder')}
+                      providerPlaceholder={t('entities.editor.link.provider-placeholder')}
+                      urlPlaceholder={t('entities.editor.link.url-placeholder')}
                     />
                   </div>
                 ) : null}
 
                 {activeStage === 'signals' ? (
                   <div className="grid gap-3">
-                    <JsonObjectListEditor label="身份标识" value={identitiesItems} addCopy="新增身份标识" onChange={setIdentitiesItems} itemLabel={t('common.item')} deleteLabel={t('common.remove')} />
-                    <JsonObjectListEditor label="监控绑定" value={monitorBindItems} addCopy="新增监控绑定" onChange={setMonitorBindItems} itemLabel={t('common.item')} deleteLabel={t('common.remove')} />
-                    <JsonObjectListEditor label="关系" value={relationItems} addCopy="新增关系" onChange={setRelationItems} itemLabel={t('common.item')} deleteLabel={t('common.remove')} />
+                    <JsonObjectListEditor label={t('entities.editor.collection.identities')} value={identitiesItems} addCopy={t('entities.editor.collection.identities.add')} onChange={setIdentitiesItems} itemLabel={t('common.item')} deleteLabel={t('common.remove')} />
+                    <JsonObjectListEditor label={t('entities.editor.collection.monitor-binds')} value={monitorBindItems} addCopy={t('entities.editor.collection.monitor-binds.add')} onChange={setMonitorBindItems} itemLabel={t('common.item')} deleteLabel={t('common.remove')} />
+                    <JsonObjectListEditor label={t('entities.editor.collection.relations')} value={relationItems} addCopy={t('entities.editor.collection.relations.add')} onChange={setRelationItems} itemLabel={t('common.item')} deleteLabel={t('common.remove')} />
                   </div>
                 ) : null}
 
                 {activeStage === 'relations' ? (
                   <div className="grid gap-3 md:grid-cols-2">
-                    <MultiValueField label="上级组件" value={componentOfText} placeholder="payment-platform, commerce" onChange={setComponentOfText} />
-                    <MultiValueField label="下级组件" value={componentsText} placeholder="checkout-ui, order-router" onChange={setComponentsText} />
-                    <MultiValueField label="实现服务" value={implementedByText} placeholder="checkout-api, order-worker" onChange={setImplementedByText} />
-                    <MultiValueField label="开发语言" value={languagesText} placeholder="java, typescript" onChange={setLanguagesText} />
-                    <KeyValueEditor label="标签" rows={labelRows} onChange={setLabelRows} keyPlaceholder="标签键" valuePlaceholder="标签值" deleteLabel={t('common.remove')} addLabel="新增标签" />
+                    <MultiValueField label={t('entities.editor.relation.component-of')} value={componentOfText} placeholder={t('entities.editor.relation.component-of-placeholder')} onChange={setComponentOfText} />
+                    <MultiValueField label={t('entities.editor.relation.components')} value={componentsText} placeholder={t('entities.editor.relation.components-placeholder')} onChange={setComponentsText} />
+                    <MultiValueField label={t('entities.editor.relation.implemented-by')} value={implementedByText} placeholder={t('entities.editor.relation.implemented-by-placeholder')} onChange={setImplementedByText} />
+                    <MultiValueField label={t('entities.editor.relation.languages')} value={languagesText} placeholder={t('entities.editor.relation.languages-placeholder')} onChange={setLanguagesText} />
+                    <KeyValueEditor label={t('entities.editor.relation.labels')} rows={labelRows} onChange={setLabelRows} keyPlaceholder={t('entities.editor.relation.label-key')} valuePlaceholder={t('entities.editor.relation.label-value')} deleteLabel={t('common.remove')} addLabel={t('entities.editor.relation.label-add')} />
                   </div>
                 ) : null}
               </div>
@@ -1120,7 +1201,7 @@ export function EntityEditorSurface({
                   data-entity-editor-definition-code-editor="preview"
                   language={editorSurfaceMode === 'yaml' ? 'yaml' : 'json'}
                   minHeight="280px"
-                  ariaLabel={editorSurfaceMode === 'yaml' ? 'YAML 定义快照' : 'JSON 定义快照'}
+                  ariaLabel={editorSurfaceMode === 'yaml' ? t('entities.editor.definition.aria.yaml') : t('entities.editor.definition.aria.json')}
                   value={definitionPreview}
                 />
               </div>
@@ -1142,10 +1223,10 @@ export function EntityEditorSurface({
             )}
           >
             <Link href="/entities" className={buttonVariants({ variant: 'subtle', size: 'sm' })}>
-              取消
+              {t('common.cancel')}
             </Link>
             <Button type="submit" size="sm" variant="primary" disabled={saving}>
-              {saving ? '保存中' : submitLabel}
+              {saving ? t('common.saving') : submitLabel}
             </Button>
           </div>
         </div>
diff --git a/web-next/components/pages/entity-list-surface.test.tsx b/web-next/components/pages/entity-list-surface.test.tsx
index b627ff21fb..29f5fc3eec 100644
--- a/web-next/components/pages/entity-list-surface.test.tsx
+++ b/web-next/components/pages/entity-list-surface.test.tsx
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 import { EntityListSurface } from './entity-list-surface';
 
 vi.mock('next/link', () => ({
@@ -22,19 +23,7 @@ vi.mock('../ui/search-row', async () => {
   return actual;
 });
 
-const t = (key: string) =>
-  ({
-    'entities.list.title': '对象目录',
-    'entities.list.environment.select': '选择环境',
-    'entities.list.environment.all': '全部环境',
-    'entities.list.column.object': '对象',
-    'entities.list.column.status': '状态',
-    'entities.list.empty.title': '暂无实体',
-    'entities.list.empty.copy': '创建实体或导入定义后会出现在这里。',
-    'common.search': '搜索',
-    'common.refresh': '刷新',
-    'common.clear': '清除'
-  })[key] || key;
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('EntityListSurface', () => {
   it('owns the OTLP cold-matte entity admin/list shell without a copied right rail', () => {
@@ -49,6 +38,7 @@ describe('EntityListSurface', () => {
             type: '服务',
             environment: '本地',
             status: '健康',
+            statusTone: 'success',
             health: {
               score: 84,
               scoreText: '84 / 100',
diff --git a/web-next/components/pages/entity-list-surface.tsx b/web-next/components/pages/entity-list-surface.tsx
index c7780827b6..d597ee44fb 100644
--- a/web-next/components/pages/entity-list-surface.tsx
+++ b/web-next/components/pages/entity-list-surface.tsx
@@ -17,6 +17,7 @@ export type EntityListTableRow = {
   type: string;
   environment: string;
   status: string;
+  statusTone: 'success' | 'warning' | 'danger' | 'neutral';
   health?: LightweightEntityHealthAffordance;
   monitorCount: string;
   activeAlertCount: string;
@@ -58,21 +59,19 @@ const coldSmallButtonClassName =
 const coldEvidenceBadgeClassName =
   'rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] leading-4 text-[#cbd5e1]';
 
-function statusClassName(status: string) {
-  if (status === 'healthy' || status === '健康' || status.includes('正常')) {
+function statusClassName(statusTone: EntityListTableRow['statusTone']) {
+  if (statusTone === 'success') {
     return 'border-[#166534]/45 bg-[#0f2f23] text-[#86efac]';
   }
-  if (status.includes('告警') || status.includes('异常') || status.toLowerCase().includes('critical')) {
+  if (statusTone === 'danger') {
     return 'border-[#7f1d1d]/55 bg-[#2a1214] text-[#fca5a5]';
   }
+  if (statusTone === 'warning') {
+    return 'border-[#854d0e]/55 bg-[#2a1c0c] text-[#facc15]';
+  }
   return 'border-[#303743] bg-[#101217] text-[#cbd5e1]';
 }
 
-function resolveCopy(t: Translator, key: string, fallback: string): string {
-  const value = t(key);
-  return value === key ? fallback : value;
-}
-
 function EmptyTableRow({ t }: { t: Translator }) {
   return (
     <tr data-entity-list-empty-state="cold-table-empty" className="border-t border-[#252b34] bg-[#0b0c0e]">
@@ -82,10 +81,10 @@ function EmptyTableRow({ t }: { t: Translator }) {
             <Inbox className="h-5 w-5" aria-hidden="true" />
           </span>
           <div className="text-[13px] font-semibold text-[#eef2f7]">
-            {resolveCopy(t, 'entities.list.empty.title', '暂无实体')}
+            {t('entities.list.empty.title')}
           </div>
           <div className="text-[12px] leading-5 text-[#8f99ab]">
-            {resolveCopy(t, 'entities.list.empty.copy', '创建实体或导入定义后会出现在这里。')}
+            {t('entities.list.empty.copy')}
           </div>
         </div>
       </td>
@@ -108,7 +107,12 @@ export function EntityListSurface({
   onRefresh,
   onReset
 }: EntityListSurfaceProps) {
-  const title = resolveCopy(t, 'entities.list.title', '对象目录');
+  const metricItems = [
+    { label: t('entities.list.metric.total'), value: total, Icon: Network },
+    { label: t('entities.list.metric.abnormal'), value: abnormalCount, Icon: Bell },
+    { label: t('entities.list.metric.alerting'), value: alertingCount, Icon: Bell },
+    { label: t('entities.list.metric.linked'), value: linkedCount, Icon: GitBranch }
+  ];
 
   return (
     <main
@@ -122,10 +126,10 @@ export function EntityListSurface({
           <div className="mb-5">
             <div data-entity-list-header="cold-compact-header" className={coldEntityVisual.panel.hero}>
               <div className="max-w-[880px]">
-                <div className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">对象优先调查</div>
-                <h1 className="mt-2 text-[30px] font-semibold leading-tight text-[#f5f7fb]">{title}</h1>
+                <div className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">{t('entities.list.kicker')}</div>
+                <h1 className="mt-2 text-[30px] font-semibold leading-tight text-[#f5f7fb]">{t('entities.list.title')}</h1>
                 <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">
-                  围绕服务、资源与实体定位问题并进入调查,先按对象筛出风险,再决定进入日志、链路、指标还是告警工作台。
+                  {t('entities.list.subtitle')}
                 </p>
                 <div data-entity-list-command-row="standard-equal-buttons" className={coldEntityVisual.button.row}>
                   <Button size="sm" variant="default" className={coldButtonClassName} onClick={onRefresh}>
@@ -134,15 +138,15 @@ export function EntityListSurface({
                   </Button>
                   <Link href="/entities/new" className={coldPrimaryLinkClassName}>
                     <Plus className="h-3.5 w-3.5" aria-hidden="true" />
-                    创建实体
+                    {t('entities.list.action.create')}
                   </Link>
                   <Link href="/entities/discovery" className={coldLinkButtonClassName}>
                     <Network className="h-3.5 w-3.5" aria-hidden="true" />
-                    从遥测发现
+                    {t('entities.list.action.discovery')}
                   </Link>
                   <Link href="/entities/import" className={coldLinkButtonClassName}>
                     <Upload className="h-3.5 w-3.5" aria-hidden="true" />
-                    导入定义
+                    {t('entities.list.action.import')}
                   </Link>
                 </div>
               </div>
@@ -151,16 +155,11 @@ export function EntityListSurface({
 
           <div data-entity-list-admin-layout="full-width-admin-list" className="space-y-5">
             <div data-entity-list-count-strip="cold-inline-counts" className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
-              {[
-                ['实体总数', total, Network],
-                ['活跃异常对象', abnormalCount, Bell],
-                ['高风险对象', alertingCount, Bell],
-                ['有关联对象', linkedCount, GitBranch]
-              ].map(([label, value, Icon]) => {
-                const MetricIcon = Icon as typeof Network;
+              {metricItems.map(({ label, value, Icon }) => {
+                const MetricIcon = Icon;
                 return (
                   <div
-                    key={String(label)}
+                    key={label}
                     className="flex min-h-[36px] items-center justify-between gap-3 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] px-3 text-[12px] text-[#a9b0bb]"
                   >
                     <span className="inline-flex min-w-0 items-center gap-2">
@@ -177,7 +176,7 @@ export function EntityListSurface({
               <SearchRow
                 data-entity-list-toolbar="cold-table-toolbar"
                 value={draft.search}
-                placeholder="搜索实体名称、命名空间、负责人"
+                placeholder={t('entities.list.search.placeholder')}
                 searchLabel={t('common.search')}
                 inputWidthClassName="w-[420px]"
                 onValueChange={value => onDraftChange({ search: value })}
@@ -213,19 +212,19 @@ export function EntityListSurface({
                 className="overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
               >
                 <div className="flex min-h-[42px] items-center justify-between gap-3 border-b border-[#252b34] bg-[#101217] px-3 text-[12px] text-[#8f99ab]">
-                  <span className="font-semibold text-[#dbe4f0]">{total} 个实体</span>
-                  <span>显示 {rangeFrom}-{rangeTo} / {total}</span>
+                  <span className="font-semibold text-[#dbe4f0]">{t('entities.list.table.total', { total })}</span>
+                  <span>{t('entities.list.table.range', { from: rangeFrom, to: rangeTo, total })}</span>
                 </div>
                 <div className="overflow-x-auto">
                   <table data-entity-list-table="cold-entity-table" className="min-w-[1040px] w-full table-fixed border-collapse text-left text-[12px] text-[#a9b0bb]">
                     <thead className="border-b border-[#252b34] bg-[#101217] text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">
                       <tr>
                         <th className="w-[24%] px-3 py-2.5">{t('entities.list.column.object')}</th>
-                        <th className="w-[14%] px-3 py-2.5">负责人</th>
-                        <th className="w-[18%] px-3 py-2.5">进展</th>
-                        <th className="w-[24%] px-3 py-2.5">证据关联</th>
+                        <th className="w-[14%] px-3 py-2.5">{t('entities.list.column.owner')}</th>
+                        <th className="w-[18%] px-3 py-2.5">{t('entities.list.column.progress')}</th>
+                        <th className="w-[24%] px-3 py-2.5">{t('entities.list.column.evidence')}</th>
                         <th className="w-[12%] px-3 py-2.5">{t('entities.list.column.status')}</th>
-                        <th className="w-[150px] px-3 py-2.5">下一步动作</th>
+                        <th className="w-[150px] px-3 py-2.5">{t('entities.list.column.next-action')}</th>
                       </tr>
                     </thead>
                     <tbody>
@@ -239,7 +238,7 @@ export function EntityListSurface({
                               {row.type} · {row.environment}
                             </div>
                           </td>
-                          <td className="px-3 py-2.5 font-semibold text-[#dbe4f0]">未设置</td>
+                          <td className="px-3 py-2.5 font-semibold text-[#dbe4f0]">{t('entities.list.row.owner.unset')}</td>
                           <td className="px-3 py-2.5">
                             {row.health ? (
                               <div
@@ -253,22 +252,24 @@ export function EntityListSurface({
                             ) : (
                               <>
                                 <div className="font-semibold text-[#eef2f7]">
-                                  {row.status === 'healthy' || row.status === '健康' ? '40%' : '20%'} · 待完善
+                                  {t('entities.list.row.progress.incomplete', {
+                                    percent: row.statusTone === 'success' ? '40%' : '20%'
+                                  })}
                                 </div>
-                                <div className="mt-1 text-[11px] text-[#858d9a]">当前缺少负责人、处置手册</div>
+                                <div className="mt-1 text-[11px] text-[#858d9a]">{t('entities.list.row.progress.missing')}</div>
                               </>
                             )}
                           </td>
                           <td className="px-3 py-2.5">
                             <div className="flex flex-wrap gap-1.5">
-                              <span className={coldEvidenceBadgeClassName}>{row.monitorCount} 监控</span>
-                              <span className={coldEvidenceBadgeClassName}>{row.activeAlertCount} 告警</span>
-                              <span className={coldEvidenceBadgeClassName}>{row.relationCount} 身份标识</span>
+                              <span className={coldEvidenceBadgeClassName}>{t('entities.list.row.evidence.monitors', { count: row.monitorCount })}</span>
+                              <span className={coldEvidenceBadgeClassName}>{t('entities.list.row.evidence.alerts', { count: row.activeAlertCount })}</span>
+                              <span className={coldEvidenceBadgeClassName}>{t('entities.list.row.evidence.identities', { count: row.relationCount })}</span>
                             </div>
-                            <div className="mt-2 text-[11px] text-[#858d9a]">最近更新 {row.updatedAt}</div>
+                            <div className="mt-2 text-[11px] text-[#858d9a]">{t('entities.list.row.evidence.updated', { time: row.updatedAt })}</div>
                           </td>
                           <td className="px-3 py-2.5">
-                            <span className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${statusClassName(row.status)}`}>
+                            <span className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${statusClassName(row.statusTone)}`}>
                               {row.status}
                             </span>
                           </td>
@@ -279,13 +280,13 @@ export function EntityListSurface({
                                 data-entity-list-row-owner-action="text-only"
                                 className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:text-white"
                               >
-                                设置负责人
+                                {t('entities.list.row.action.owner')}
                               </Link>
                               <Link href="/log/manage" className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:text-white">
-                                日志线索
+                                {t('entities.list.row.action.logs')}
                               </Link>
                               <Link href="/trace/manage" className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-1 text-[11px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:text-white">
-                                链路证据
+                                {t('entities.list.row.action.traces')}
                               </Link>
                             </div>
                           </td>
diff --git a/web-next/lib/api-facade.test.ts b/web-next/lib/api-facade.test.ts
new file mode 100644
index 0000000000..41b8a510c0
--- /dev/null
+++ b/web-next/lib/api-facade.test.ts
@@ -0,0 +1,993 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { api } from './api-facade';
+
+const fetchMock = vi.fn<typeof fetch>();
+
+function mockApiMessagePayload(data: unknown) {
+  fetchMock.mockResolvedValueOnce(
+    new Response(JSON.stringify({ code: 0, data }), {
+      status: 200,
+      headers: { 'Content-Type': 'application/json' }
+    })
+  );
+}
+
+describe('api facade', () => {
+  afterEach(() => {
+    fetchMock.mockReset();
+    vi.unstubAllGlobals();
+  });
+
+  it('keeps monitor list reads behind the BFF message client', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ content: [] });
+
+    await expect(api.monitors.list({ pageIndex: 0, pageSize: 8, status: '2' })).resolves.toEqual({ content: [] });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/manage?pageIndex=0&pageSize=8&status=2',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor page reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ content: [{ id: 42 }], totalElements: 1 });
+
+    await expect(
+      api.monitors.page({
+        search: ' mysql ',
+        app: 'mysql',
+        labels: '',
+        status: '2',
+        pageIndex: '0',
+        pageSize: '8',
+        entityId: '',
+        entityName: '',
+        returnTo: ''
+      })
+    ).resolves.toEqual({ content: [{ id: 42 }], totalElements: 1 });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors?pageIndex=0&pageSize=8&search=mysql&app=mysql&status=2',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor detail reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ id: 42, app: 'website' });
+
+    await expect(api.monitors.detail(42)).resolves.toEqual({ id: 42, app: 'website' });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitor/42',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor editor draft reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ monitor: { id: 42, app: 'mysql' }, params: [], collector: null });
+    mockApiMessagePayload({ content: [{ collector: { name: 'edge-a' } }] });
+    mockApiMessagePayload([{ field: 'host', type: 'text' }]);
+
+    await expect(api.monitors.editorDetail(42)).resolves.toEqual({
+      monitor: { id: 42, app: 'mysql' },
+      params: [],
+      collector: null
+    });
+    await expect(api.monitors.editorCollectors()).resolves.toEqual({ content: [{ collector: { name: 'edge-a' } }] });
+    await expect(api.monitors.editorParamDefines('mysql')).resolves.toEqual([{ field: 'host', type: 'text' }]);
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/monitor/42',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/collector',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      3,
+      '/api/apps/mysql/params',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor optional detail reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload(['cpu']);
+    mockApiMessagePayload({ enabled: true, url: 'https://grafana.example/d/abc' });
+
+    await expect(api.monitors.favoriteMetrics(42)).resolves.toEqual(['cpu']);
+    await expect(api.monitors.grafanaDashboard(42)).resolves.toEqual({ enabled: true, url: 'https://grafana.example/d/abc' });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/metrics/favorite/42',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/grafana/dashboard?monitorId=42',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor detail favorite and Grafana mutations through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ ok: true });
+    mockApiMessagePayload({ ok: true });
+    mockApiMessagePayload({ ok: true });
+
+    await expect(api.monitors.addFavoriteMetric(42, 'cpu.usage')).resolves.toEqual({ ok: true });
+    await expect(api.monitors.removeFavoriteMetric(42, 'cpu.usage')).resolves.toEqual({ ok: true });
+    await expect(api.monitors.deleteGrafanaDashboard(42)).resolves.toEqual({ ok: true });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/metrics/favorite/42/cpu.usage',
+      expect.objectContaining({
+        method: 'POST',
+        credentials: 'same-origin',
+        body: 'null'
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/metrics/favorite/42/cpu.usage',
+      expect.objectContaining({
+        method: 'DELETE',
+        credentials: 'same-origin'
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      3,
+      '/api/grafana/dashboard?monitorId=42',
+      expect.objectContaining({
+        method: 'DELETE',
+        credentials: 'same-origin'
+      })
+    );
+  });
+
+  it('maps monitor realtime metric reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ values: [{ field: 'usage' }] });
+
+    await expect(api.monitors.realtimeMetric(42, 'cpu')).resolves.toEqual({ values: [{ field: 'usage' }] });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitor/42/metrics/cpu',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor history catalog reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ available: true });
+    mockApiMessagePayload({ metrics: [{ name: 'cpu', fields: [{ type: 0, field: 'usage' }] }] });
+
+    await expect(api.monitors.warehouseStorageStatus()).resolves.toEqual({ available: true });
+    await expect(
+      api.monitors.historyMetricCatalogDefine({ id: 42, app: 'prometheus', scrape: 'prometheus', name: 'node-exporter' } as any)
+    ).resolves.toEqual({ metrics: [{ name: 'cpu', fields: [{ type: 0, field: 'usage' }] }] });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/warehouse/storage/status',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/apps/42/define/dynamic',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor history metric reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ values: { usage: [{ origin: '72' }] } });
+
+    await expect(
+      api.monitors.historyMetric(
+        { id: 42, instance: '127.0.0.1:8080', app: 'prometheus', name: 'node-exporter' } as any,
+        { metrics: 'cpu', metric: 'usage' },
+        { history: '1h', interval: true }
+      )
+    ).resolves.toEqual({ values: { usage: [{ origin: '72' }] } });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitor/127.0.0.1%3A8080/metric/_prometheus_node-exporter.cpu.usage?history=1h&interval=true',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps monitor editor mutations through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ id: 42 });
+    mockApiMessagePayload({ id: 42 });
+    mockApiMessagePayload({ available: true });
+
+    const savePayload = { monitor: { app: 'website' }, params: [], grafanaDashboard: { enabled: false } };
+    const detectPayload = { monitor: { app: 'website' }, params: [] };
+
+    await expect(api.monitors.create(savePayload)).resolves.toEqual({ id: 42 });
+    await expect(api.monitors.update(savePayload)).resolves.toEqual({ id: 42 });
+    await expect(api.monitors.detect(detectPayload)).resolves.toEqual({ available: true });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/monitor',
+      expect.objectContaining({
+        method: 'POST',
+        credentials: 'same-origin',
+        body: JSON.stringify(savePayload)
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/monitor',
+      expect.objectContaining({
+        method: 'PUT',
+        credentials: 'same-origin',
+        body: JSON.stringify(savePayload)
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      3,
+      '/api/monitor/detect',
+      expect.objectContaining({
+        method: 'POST',
+        credentials: 'same-origin',
+        body: JSON.stringify(detectPayload)
+      })
+    );
+  });
+
+  it('maps monitor import uploads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ imported: 1 });
+    const body = new FormData();
+    body.append('file', 'name: mysql');
+
+    await expect(api.monitors.import(body)).resolves.toEqual({ imported: 1 });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/monitors/import',
+      expect.objectContaining({
+        method: 'POST',
+        credentials: 'same-origin',
+        body
+      })
+    );
+    expect((fetchMock.mock.calls[0]?.[1]?.headers as Record<string, string> | undefined)?.['Content-Type']).toBeUndefined();
+  });
+
+  it('maps monitor export downloads through the domain facade while preserving response headers', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    fetchMock
+      .mockResolvedValueOnce(
+        new Response(new Blob(['{}'], { type: 'application/json' }), {
+          status: 200,
+          headers: {
+            'Content-Disposition': 'attachment; filename=monitors.json',
+            'Content-Type': 'application/octet-stream'
+          }
+        })
+      )
+      .mockResolvedValueOnce(
+        new Response(new Blob(['id,name'], { type: 'text/csv' }), {
+          status: 200,
+          headers: {
+            'Content-Disposition': 'attachment; filename=monitors-all.xlsx',
+            'Content-Type': 'application/octet-stream'
+          }
+        })
+      );
+
+    await expect(api.monitors.exportResponse([1, 2], 'JSON')).resolves.toBeInstanceOf(Response);
+    await expect(api.monitors.exportAllResponse('EXCEL')).resolves.toBeInstanceOf(Response);
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/monitors/export?ids=1&ids=2&type=JSON',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/monitors/export/all?type=EXCEL',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps topology graph reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ nodes: [], edges: [], apiBacked: true });
+
+    await expect(api.topology.graph({ entityId: '501', sourceKind: 'otlp-trace-call' })).resolves.toMatchObject({
+      apiBacked: true
+    });
+
+    expect(String(fetchMock.mock.calls[0]?.[0])).toContain('/api/topology?focusEntityId=501');
+    expect(String(fetchMock.mock.calls[0]?.[0])).toContain('sourceKind=otlp-trace-call');
+    expect(String(fetchMock.mock.calls[0]?.[0])).toContain('relationType=trace-call');
+  });
+
+  it('maps entity detail reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ entity: { entity: { id: 123 } } });
+    mockApiMessagePayload({ content: [{ id: 9, labels: { service: 'checkout' } }], totalElements: 1 });
+
+    await expect(api.entities.detail(123)).resolves.toEqual({ entity: { entity: { id: 123 } } });
+    await expect(api.entities.alerts(123, { pageIndex: 0, pageSize: 20, status: 'firing' })).resolves.toEqual({
+      content: [{ id: 9, labels: { service: 'checkout' } }],
+      totalElements: 1
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/entities/123/detail',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/entities/123/alerts?pageIndex=0&pageSize=20&status=firing',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps entity editor draft reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ entity: { id: 123 } });
+
+    await expect(api.entities.editorEntity(123)).resolves.toEqual({ entity: { id: 123 } });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/entities/123',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps entity list reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ content: [], totalElements: 0 });
+
+    await expect(api.entities.list({ search: ' checkout ', type: 'service', status: 'healthy' })).resolves.toEqual({
+      content: [],
+      totalElements: 0
+    });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/entities?pageIndex=0&pageSize=8&sort=gmtUpdate&order=desc&search=checkout&type=service&status=healthy',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps entity editor catalog suggestions through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ owners: ['platform'] });
+
+    await expect(api.entities.catalogSuggestions()).resolves.toEqual({ owners: ['platform'] });
+
+    expect(fetchMock).toHaveBeenCalledWith(
+      '/api/entities/catalog-suggestions?limit=120',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps entity definition workspace reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload('kind: Entity');
+    mockApiMessagePayload([{ id: 1, summary: 'updated definition' }]);
+    mockApiMessagePayload([{ id: 'base-template', name: 'Base template' }]);
+
+    await expect(api.entities.definition(42, 'yaml')).resolves.toBe('kind: Entity');
+    await expect(api.entities.definitionActivities(42)).resolves.toEqual([{ id: 1, summary: 'updated definition' }]);
+    await expect(api.entities.definitionTemplates()).resolves.toEqual([{ id: 'base-template', name: 'Base template' }]);
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/entities/42/definition?format=yaml',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/entities/definition-activities?entityId=42&limit=8',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      3,
+      '/api/entities/definition/templates?limit=8',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps entity import workspace reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload([{ id: 'base-template', name: 'Base template' }]);
+    mockApiMessagePayload([{ id: 1, summary: 'imported service' }]);
+
+    await expect(api.entities.importTemplates()).resolves.toEqual([{ id: 'base-template', name: 'Base template' }]);
+    await expect(api.entities.importActivities()).resolves.toEqual([{ id: 1, summary: 'imported service' }]);
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/entities/definition/templates?limit=8',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/entities/definition-activities?limit=8',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps entity discovery workspace reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload([{ id: 'preset-a', name: 'Default preset' }]);
+    mockApiMessagePayload([{ id: 'activity-a', summary: 'synced preset' }]);
+
+    await expect(api.entities.discoveryGovernancePresets()).resolves.toEqual([{ id: 'preset-a', name: 'Default preset' }]);
+    await expect(api.entities.discoveryGovernanceActivities()).resolves.toEqual([{ id: 'activity-a', summary: 'synced preset' }]);
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/entities/discovery/governance-presets?limit=8',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/entities/discovery/governance-activities?limit=8',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps overview console reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ apps: [{ app: 'website', size: 2 }] });
+    mockApiMessagePayload({ content: [{ id: 1, content: 'latency spike' }] });
+
+    await expect(api.overview.summary()).resolves.toEqual({ apps: [{ app: 'website', size: 2 }] });
+    await expect(api.overview.alerts({ pageIndex: 0, pageSize: 6, sort: 'gmtUpdate', order: 'desc' })).resolves.toEqual({
+      content: [{ id: 1, content: 'latency spike' }]
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/summary',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/alerts?pageIndex=0&pageSize=6&sort=gmtUpdate&order=desc',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+  });
+
+  it('maps alert center group mutations through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload(undefined);
+    mockApiMessagePayload(undefined);
+
+    await expect(api.alerts.groupStatus('acknowledged', [7, 8])).resolves.toBeUndefined();
+    await expect(api.alerts.groupClose(7)).resolves.toBeUndefined();
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/alerts/group/status/acknowledged?ids=7&ids=8',
+      expect.objectContaining({
+        method: 'PUT',
+        credentials: 'same-origin',
+        body: 'null'
+      })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/alerts/group?ids=7',
+      expect.objectContaining({
+        method: 'DELETE',
+        credentials: 'same-origin'
+      })
+    );
+  });
+
+  it('maps alert center first-screen reads through the domain facade', async () => {
+    vi.stubGlobal('fetch', fetchMock);
+    mockApiMessagePayload({ total: 4, priorityCriticalNum: 1 });
+    mockApiMessagePayload({ content: [{ id: 9, content: 'checkout down' }], totalElements: 1, pageIndex: 0, pageSize: 8 });
+
+    await expect(api.alerts.summary()).resolves.toEqual({ total: 4, priorityCriticalNum: 1 });
+    await expect(
+      api.alerts.groupAlerts({
+        search: ' checkout ',
+        status: ' firing ',
+        severity: ' critical ',
+        pageIndex: 0,
+        pageSize: 8,
+        entityId: '',
+        entityName: '',
+        returnTo: ''
+      })
+    ).resolves.toEqual({
+      content: [{ id: 9, content: 'checkout down' }],
+      totalElements: 1,
+      pageIndex: 0,
+      pageSize: 8
+    });
+
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      1,
+      '/api/alerts/summary',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
+    );
+    expect(fetchMock).toHaveBeenNthCalledWith(
+      2,
+      '/api/alerts/group?pageIndex=0&pageSize=8&sort=gmtUpdate&order=desc&search=checkout&status=firing&severity=critical',
+      expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
... 8102 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 02/50: feat(web-next): add shared UI lab foundation

Posted by zh...@apache.org.
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 01c57a50d32ff185d6a40c29c79892d7ce542003
Author: Logic <zq...@dromara.org>
AuthorDate: Thu May 28 23:58:25 2026 +0800

    feat(web-next): add shared UI lab foundation
---
 web-next/app/ui-lab/page.test.tsx                  |  3969 ++++
 web-next/app/ui-lab/page.tsx                       | 11153 +++++++++++
 web-next/package-lock.json                         |   510 +-
 web-next/package.json                              |     2 +
 web-next/packages/design-tokens/package.json       |    12 +
 web-next/packages/design-tokens/src/themes.css     |    51 +
 web-next/packages/design-tokens/src/tokens.css     |    36 +
 web-next/packages/design-tokens/src/tokens.test.ts |    30 +
 .../hertzbeat-ui/src/chart-interaction.test.tsx    |   172 +
 web-next/packages/hertzbeat-ui/src/index.test.tsx  |  8284 ++++++++
 web-next/packages/hertzbeat-ui/src/index.tsx       | 18761 +++++++++++++++++++
 .../src/mutation-workflow.interaction.test.tsx     |   152 +
 .../src/select-menu.interaction.test.tsx           |    86 +
 .../src/time-range-toolbar.interaction.test.tsx    |    99 +
 .../src/topology-companion.interaction.test.tsx    |   216 +
 .../packages/hertzbeat-ui/src/topology-g6.test.tsx |  1180 ++
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx |  2372 +++
 .../src/trace-waterfall.interaction.test.tsx       |    67 +
 web-next/tailwind.config.ts                        |     3 +-
 web-next/tsconfig.json                             |     6 +
 web-next/vitest.config.ts                          |     2 +
 21 files changed, 47159 insertions(+), 4 deletions(-)

diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
new file mode 100644
index 0000000000..27435228ff
--- /dev/null
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -0,0 +1,3969 @@
+import { readFileSync } from 'node:fs';
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it, vi } from 'vitest';
+
+vi.mock('@uiw/react-codemirror', () => ({
+  default: ({ value, readOnly, extensions, height, minHeight, basicSetup, theme, onChange, ...props }: any) => (
+    <div
+      data-testid={props['data-testid']}
+      data-mocked-codemirror="true"
+      data-readonly={readOnly ? 'true' : 'false'}
+      data-height={height}
+      data-min-height={minHeight}
+      data-basic-setup={basicSetup ? 'true' : 'false'}
+      data-theme={typeof theme === 'string' ? theme : theme ? 'custom-dark' : 'missing'}
+      data-extension-count={Array.isArray(extensions) ? String(extensions.length) : '0'}
+      onClick={() => onChange?.(`${value}\nnext`)}
+    >
+      {value}
+    </div>
+  )
+}));
+
+vi.mock('@codemirror/lang-yaml', () => ({ yaml: () => 'yaml-extension' }));
+vi.mock('@codemirror/lang-json', () => ({ json: () => 'json-extension' }));
+vi.mock('@codemirror/lang-html', () => ({ html: () => 'html-extension' }));
+vi.mock('@codemirror/lang-javascript', () => ({ javascript: () => 'javascript-extension' }));
+vi.mock('@codemirror/view', () => ({
+  EditorView: {
+    lineWrapping: 'line-wrapping-extension',
+    theme: () => 'theme-extension'
+  }
+}));
+vi.mock('@codemirror/state', () => ({
+  EditorState: {
+    readOnly: {
+      of: () => 'readonly-extension'
+    }
+  }
+}));
+
+vi.mock('@/components/providers/i18n-provider', () => {
+  const messages: Record<string, string> = {
+    '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-years': 'Previous years',
+    'time.range.next-years': 'Next years',
+    'time.range.previous-month': 'Previous month',
+    'time.range.next-month': 'Next month',
+    'time.range.decrease': 'Decrease',
+    'time.range.increase': 'Increase',
+    'time.range.refresh': 'Refresh interval',
+    'time.range.manual-refresh': 'Manual',
+    'time.range.preset.last-15m': 'Last 15 minutes',
+    'time.range.preset.last-1h': 'Last 1 hour',
+    'time.range.preset.last-6h': 'Last 6 hours',
+    'time.range.preset.last-24h': 'Last 24 hours',
+    'time.range.timezone': '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',
+    'monitor.detail.auto-refresh': 'Auto Refresh After {{time}} s',
+    'monitor.detail.close-refresh': 'Close Auto Refresh',
+    'monitor.detail.config-refresh': 'Set Auto Refresh For {{time}} s',
+    'common.clear': 'Clear',
+    'common.week.1': 'Monday',
+    'common.week.2': 'Tuesday',
+    'common.week.3': 'Wednesday',
+    'common.week.4': 'Thursday',
+    'common.week.5': 'Friday',
+    'common.week.6': 'Saturday',
+    'common.week.7': 'Sunday',
+    'common.month.1': 'January',
+    'common.month.2': 'February',
+    'common.month.3': 'March',
+    'common.month.4': 'April',
+    'common.month.5': 'May',
+    'common.month.6': 'June',
+    'common.month.7': 'July',
+    'common.month.8': 'August',
+    'common.month.9': 'September',
+    'common.month.10': 'October',
+    'common.month.11': 'November',
+    'common.month.12': 'December',
+    'ui-lab.time-range.simulate-zoom': 'Simulate chart zoom',
+    'ui-lab.time-range.apply-query': 'Apply as query time',
+    'ui-lab.time-range.previous-query': 'Previous query: now-1h to now',
+    'ui-lab.time-range.release-window': 'Release validation window'
+  };
+
+  return {
+    useI18n: () => ({
+      t: (key: string, params?: Record<string, string | number | null | undefined>) =>
+        (messages[key] || key).replace(/\{\{\s*([\w.-]+)\s*\}\}|\{([\w.-]+)\}/g, (_, doubleKey: string | undefined, singleKey: string | undefined) => {
+          const value = params?.[doubleKey ?? singleKey ?? ''];
+          return value == null ? '' : String(value);
+        })
+    })
+  };
+});
+
+import HertzBeatUiLabPage from './page';
+
+const uiLabPageSource = readFileSync(new URL('./page.tsx', import.meta.url), 'utf8');
+const globalsCssSource = readFileSync(new URL('../globals.css', import.meta.url), 'utf8');
+
+function renderUiLabPage() {
+  return renderToStaticMarkup(<HertzBeatUiLabPage />);
+}
+
+describe('HertzBeat UI lab page', () => {
+  it('renders the @hertzbeat/ui showcase with the monitor picker and YML workspace patterns', () => {
+    const html = renderUiLabPage();
+    const source = readFileSync('app/ui-lab/page.tsx', 'utf8');
+
+    expect(html).toContain('@hertzbeat/ui explorer');
+    expect(html).toContain('data-hz-ui="explorer-frame"');
+    expect(html).toContain('data-hz-density="operator-compact"');
+    expect(html).toContain('data-hz-viewport-guard="single-column-first"');
+    expect(html).toContain('data-hz-ui="skip-link"');
+    expect(html).toContain('href="#hz-ui-lab-main"');
+    expect(html).toContain('id="hz-ui-lab-main"');
+    expect(html).toContain('aria-label="HertzBeat UI Lab workbench"');
+    expect(html).toContain('data-hz-layout-region="filter-rail"');
+    expect(html).toContain('aria-label="UI lab filters"');
+    expect(html).toContain('data-hz-ui="query-bar"');
+    expect(html).toContain('data-hz-ui="filter-workbench"');
+    expect(html).toContain('Active filters');
+    expect(html).toContain('Filter builder');
+    expect(html).toContain('data-hz-ui="filter-builder"');
+    expect(html).toContain('Add filter');
+    expect(html).toContain('OR');
+    expect(html).toContain('Attributes');
+    expect(html).toContain('Group by');
+    expect(html).toContain('Saved views');
+    expect(html).toContain('Query plan');
+    expect(html).toContain('data-hz-ui="result-controls"');
+    expect(html).toContain('data-hz-ui="select-menu"');
+    expect(html).toContain('data-hz-ui="select-trigger"');
+    expect(html).toContain('data-hz-feedback-variant="embedded"');
+    expect(html).toContain('data-hz-ui-lab-control-baseline="28-button-32-field-tiers"');
+    expect(html).toContain('data-hz-ui-lab-button-link="shared"');
+    expect(html).toContain('data-monitor-history-download-owner="hertzbeat-ui-button-link"');
+    expect(html).toContain('data-hz-ui-lab-trace-handoff-action="collector"');
+    expect(html).toContain('data-hz-ui-lab-trace-handoff-action-icon="collector"');
+    expect(html).toContain('data-trace-handoff-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-ui-lab-action-group="shared"');
+    expect(html).toContain('data-hz-ui-lab-action-group-layout="full-end"');
+    expect(html).toContain('data-hz-action-group-layout="full-end"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-action-menu="angular-ellipsis-dropdown"');
+    expect(html).toContain('data-monitor-row-actions="angular-ellipsis-dropdown"');
+    expect(html).toContain('data-monitor-row-actions-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-monitor-row-action-menu="angular-ellipsis-dropdown"');
+    expect(html).toContain('data-monitor-row-action-menu-layer="overlay-visible-above-table"');
+    expect(html).toContain('data-monitor-row-action-menu-clearance="floating-overlay-no-table-crop"');
+    expect(html).toContain('data-monitor-row-action-menu-trigger-owner="hertzbeat-ui-icon-button"');
+    expect(html).toContain('data-monitor-row-action-menu-panel="angular-nz-dropdown-menu"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-app-filter="angular-app-tag-link"');
+    expect(html).toContain('data-monitor-row-app-filter-link="angular-app-tag-link"');
+    expect(html).toContain('data-monitor-row-app-filter-target="/monitors?app=mysql"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-instance-copy="angular-host-copy"');
+    expect(html).toContain('data-monitor-row-instance-copy="angular-host-copy"');
+    expect(html).toContain('data-monitor-row-instance-copy-owner="hertzbeat-ui-icon-button"');
+    expect(html).toContain('data-monitor-row-instance-copy-target="127.0.0.1:80"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-scrape-display="angular-service-discovery"');
+    expect(html).toContain('data-monitor-row-scrape-display="angular-service-discovery"');
+    expect(html).toContain('data-monitor-row-scrape-display-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-monitor-row-scrape-value="http_sd"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-label-color="angular-render-label-color"');
+    expect(html).toContain('data-monitor-row-label-color="angular-render-label-color"');
+    expect(html).toContain('data-monitor-row-labels-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-entity-triage="angular-entity-reason"');
+    expect(html).toContain('data-monitor-row-entity-triage="angular-entity-reason"');
+    expect(html).toContain('data-monitor-row-entity-triage-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-monitor-row-entity-triage-status="down"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-entity-response-action="angular-inline-host-action"');
+    expect(html).toContain('data-monitor-row-entity-response-action="angular-inline-host-action"');
+    expect(html).toContain('data-monitor-row-entity-response-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-entity-support-actions="angular-platform-support-action-bar"');
+    expect(html).toContain('data-monitor-row-entity-support-actions="angular-platform-support-action-bar"');
+    expect(html).toContain('data-monitor-row-entity-support-actions-owner="hertzbeat-ui-button-link"');
+    expect(html).toContain('data-monitor-row-entity-support-action="related-logs"');
+    expect(html).toContain('data-monitor-row-entity-support-action="related-traces"');
+    expect(html).toContain('data-monitor-row-entity-support-action="code"');
+    expect(html).toContain('data-monitor-row-entity-support-disabled="missing-code-navigation"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-detail-action="shared"');
+    expect(html).toContain('data-monitor-row-detail-action-owner="hertzbeat-ui-button-link"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-copy-lifecycle="angular-copy-success-refresh-unavailable-refresh"');
+    expect(html).toContain('data-monitor-row-copy-lifecycle="angular-copy-success-refresh-unavailable-refresh"');
+    expect(html).toContain('data-monitor-row-copy-lifecycle-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-response-action="pause"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-response-action="enable"');
+    expect(html).toContain('data-monitor-row-response-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-response-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-hz-ui-lab-monitor-response-confirm-closable="angular-nz-closable-false"');
+    expect(html).toContain('data-hz-ui-lab-monitor-response-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(html).toContain('data-monitor-row-response-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-monitor-row-response-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-monitor-row-response-confirm-action="pause"');
+    expect(html).toContain('data-monitor-row-response-confirm-closable="angular-nz-closable-false"');
+    expect(html).toContain('data-monitor-row-response-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(html).toContain('data-monitor-row-response-confirm-selected-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-monitor-batch-response-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-hz-ui-lab-monitor-batch-response-confirm-closable="angular-nz-closable-false"');
+    expect(html).toContain('data-hz-ui-lab-monitor-batch-response-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(html).toContain('data-monitor-batch-response-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-monitor-batch-response-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-monitor-batch-response-confirm-action="pause"');
+    expect(html).toContain('data-monitor-batch-response-confirm-closable="angular-nz-closable-false"');
+    expect(html).toContain('data-monitor-batch-response-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(html).toContain('data-monitor-batch-response-confirm-selected-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-monitor-row-delete-action="shared"');
+    expect(html).toContain('data-monitor-row-delete-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-control-baseline-kind="primitive-form-action"');
+    expect(html).toContain('data-hz-control-baseline-component="HzButton"');
+    expect(html).toContain('data-hz-control-baseline-component="HzPanelSurface"');
+    expect(html).toContain('data-hz-ui-lab-panel-surface="shared"');
+    expect(html).toContain('data-hz-ui="panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="query"');
+    expect(html).toContain('data-hz-ui-lab-panel-surface-padding="query"');
+    expect(html).toContain('data-hz-panel-surface-padding="header"');
+    expect(html).toContain('data-hz-ui-lab-panel-surface-padding="header"');
+    expect(html).toContain('data-hz-ui-lab-log-header-panel-surface="shared"');
+    expect(html).toContain('data-log-header-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="chart"');
+    expect(html).toContain('data-hz-ui-lab-panel-surface-padding="chart"');
+    expect(html).toContain('data-hz-ui-lab-log-chart-panel-surface="shared"');
+    expect(html).toContain('data-log-chart-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-control-baseline-component="HzAiChatModalSurface"');
+    expect(html).toContain('data-hz-control-baseline-component="HzHeaderIconButton"');
+    expect(html).toContain('data-hz-ui-lab-header-mute="shared"');
+    expect(html).toContain('data-hz-ui-lab-header-mute-save-lifecycle="angular-success-only-state-update"');
+    expect(html).toContain('data-app-frame-mute-save-lifecycle="angular-success-only-state-update"');
+    expect(html).toContain('data-app-frame-mute-save-lifecycle-owner="route-action-state-contract"');
+    expect(html).toContain('data-hz-ui="header-icon-button"');
+    expect(html).toContain('data-hz-header-icon-button-owner="hertzbeat-ui-header-icon-button"');
+    expect(html).toContain('data-hz-header-icon-button-density="angular-header-item"');
+    expect(html).toContain('data-app-frame-mute-state="muted"');
+    expect(html).toContain('data-hz-control-baseline-component="HzHeaderRealtimeNotice"');
+    expect(html).toContain('data-hz-ui-lab-header-realtime-notice="shared"');
+    expect(html).toContain('data-hz-ui-lab-header-realtime-sse="angular-alert-and-manager-sse"');
+    expect(html).toContain('data-app-frame-header-realtime-sse-contract="angular-alert-and-manager-sse"');
+    expect(html).toContain('data-app-frame-header-realtime-sse-owner="route-realtime-sse-contract"');
+    expect(html).toContain('data-app-frame-header-realtime-alert-source="/api/alert/sse/subscribe"');
+    expect(html).toContain('data-app-frame-header-realtime-manager-source="/api/manager/sse/subscribe"');
+    expect(html).toContain('data-hz-ui="header-realtime-notice"');
+    expect(html).toContain('data-hz-header-realtime-notice-owner="hertzbeat-ui-header-realtime-notice"');
+    expect(html).toContain('data-hz-header-realtime-notice-density="angular-notice-sse"');
+    expect(html).toContain('data-hz-header-realtime-notice-status="live"');
+    expect(html).toContain('data-hz-control-baseline-component="HzHeaderMenuAction"');
+    expect(html).toContain('data-hz-ui-lab-header-fullscreen-action="shared"');
+    expect(html).toContain('data-hz-ui="header-menu-action"');
+    expect(html).toContain('data-hz-header-menu-action-owner="hertzbeat-ui-header-menu-action"');
+    expect(html).toContain('data-hz-header-menu-action-density="angular-header-menu-item"');
+    expect(html).toContain('data-hz-header-menu-action-state="active"');
+    expect(html).toContain('data-hz-ui-lab-setting-config-apply="angular-apply-notify-reload"');
+    expect(html).toContain('data-setting-config-apply-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-setting-config-runtime-locale="underscore-to-hyphen"');
+    expect(html).toContain('data-hz-ui-lab-setting-config-selects="angular-400px-centered-bold"');
+    expect(html).toContain('data-setting-config-select-contract="angular-400px-centered-bold"');
+    expect(html).toContain('data-settings-form-select-width="angular-400px"');
+    expect(html).toContain('data-settings-form-select-style="angular-centered-bold"');
+    expect(html).toContain('data-settings-form-select-dropdown-style="angular-bold-larger"');
+    expect(html).toContain('data-setting-config-timezone-search-contract="angular-nz-show-search"');
+    expect(html).toContain('data-hz-ui-lab-setting-config-timezone-search="angular-nz-show-search"');
+    expect(html).toContain('data-setting-config-timezone-dropdown-width-contract="angular-dropdown-match-select-width-false"');
+    expect(html).toContain('data-hz-ui-lab-object-store-apply="angular-apply-notify"');
+    expect(html).toContain('data-setting-object-store-apply-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-object-store-provider-reset="angular-reset-config-on-type-change"');
+    expect(html).toContain('data-setting-object-store-type-change-contract="angular-reset-config-on-type-change"');
+    expect(html).toContain('data-setting-object-store-provider-select-contract="angular-centered-bold-dropdown"');
+    expect(html).toContain('data-setting-object-store-provider-select="angular-centered-bold-dropdown"');
+    expect(html).toContain('data-setting-object-store-type-change="angular-reset-config-on-type-change"');
+    expect(html).toContain('data-hz-ui-lab-object-store-obs-fields="angular-obs-fields-required"');
+    expect(html).toContain('data-hz-ui-lab-setting-server-apply="angular-apply-notify"');
+    expect(html).toContain('data-settings-server-apply-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-setting-server-dialog-layout="angular-width-40-percent"');
+    expect(html).toContain('data-settings-server-dialog-width-contract="angular-width-40-percent"');
+    expect(html).toContain('data-settings-server-dialog-field-layout-contract="angular-label-7-control-12"');
+    expect(html).toContain('data-settings-server-dialog-mask-contract="angular-mask-closable-false"');
+    expect(html).toContain('data-hz-ui-lab-setting-server-dialog-form="angular-label-7-control-12"');
+    expect(html).toContain('data-settings-dialog-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-settings-dialog-label-span="7"');
+    expect(html).toContain('data-settings-dialog-control-span="12"');
+    expect(html).toContain('data-hz-ui-lab-settings-shell="angular-settings-shell"');
+    expect(html).toContain('data-settings-console-shell-owner="shared-settings-console-shell"');
+    expect(html).toContain('data-settings-console-route-contract="angular-settings-shell"');
+    expect(html).toContain('data-settings-console-menu-contract="angular-config-server-object-store-token"');
+    expect(html).toContain('data-settings-console-menu-mode-contract="angular-inline-horizontal-responsive"');
+    expect(html).toContain('data-settings-console-menu-mode="angular-responsive-inline-horizontal"');
+    expect(html).toContain('data-settings-console-active-title-contract="angular-active-child-title"');
+    expect(html).toContain('data-settings-console-active-title="System config"');
+    expect(html).toContain('data-settings-console-selected-contract="angular-nz-selected"');
+    expect(html).toContain('data-hz-ui-lab-settings-active-title="angular-active-child-title"');
+    expect(html).toContain('data-settings-console-active-title-owner="shared-settings-console-shell"');
+    expect(html).toContain('data-hz-ui-lab-setting-token-generate="angular-generate-token-modal"');
+    expect(html).toContain('data-setting-token-generate-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-setting-token-generate-dialog-layout-contract="angular-vertical-form"');
+    expect(html).toContain('data-setting-token-generated-dialog-width-contract="angular-width-50-percent"');
+    expect(html).toContain('data-setting-token-generated-dialog-mask-contract="angular-mask-closable-false"');
+    expect(html).toContain('data-hz-ui-lab-setting-token-generate-form="angular-vertical-form"');
+    expect(html).toContain('data-setting-token-generate-form-layout="angular-vertical-form"');
+    expect(html).toContain('data-settings-dialog-field-layout="angular-vertical-form"');
+    expect(html).toContain('data-settings-dialog-label-span="vertical"');
+    expect(html).toContain('data-settings-dialog-control-span="vertical"');
+    expect(html).toContain('data-hz-ui-lab-setting-token-copy-feedback="angular-copy-notify"');
+    expect(html).toContain('data-setting-token-copy-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-setting-token-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-setting-token-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-setting-token-load-failure="angular-load-failed-retry"');
+    expect(html).toContain('data-setting-token-load-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-control-baseline-component="HzLocaleMenuOption"');
+    expect(html).toContain('data-hz-ui-lab-locale-menu-option="shared"');
+    expect(html).toContain('data-hz-ui-lab-locale-reload="angular-load-use-layout-reload"');
+    expect(html).toContain('data-app-frame-locale-reload-contract="angular-load-use-layout-reload"');
+    expect(html).toContain('data-app-frame-locale-reload-owner="route-locale-reload-contract"');
+    expect(html).toContain('data-hz-ui="locale-menu-option"');
+    expect(html).toContain('data-hz-locale-menu-option-owner="hertzbeat-ui-locale-menu-option"');
+    expect(html).toContain('data-hz-locale-menu-option-density="angular-header-locale-item"');
+    expect(html).toContain('data-hz-locale-menu-option-selected="true"');
+    expect(html).toContain('data-hz-control-baseline-component="HzUserMenuAction"');
+    expect(html).toContain('data-hz-ui-lab-user-menu-action="setting"');
+    expect(html).toContain('data-hz-ui-lab-user-menu-action="logout"');
+    expect(html).toContain('data-hz-ui-lab-user-logout-lifecycle-contract="angular-clear-then-passport-login"');
+    expect(html).toContain('data-app-frame-user-logout-lifecycle-contract="angular-clear-then-passport-login"');
+    expect(html).toContain('data-hz-ui-lab-user-logout-lifecycle="angular-clear-then-passport-login"');
+    expect(html).toContain('data-app-frame-user-logout-lifecycle="angular-clear-then-passport-login"');
+    expect(html).toContain('data-app-frame-user-logout-lifecycle-owner="route-session-contract"');
+    expect(html).toContain('data-hz-ui-lab-user-menu-action="about"');
+    expect(html).toContain('data-hz-ui="user-menu-action"');
+    expect(html).toContain('data-hz-user-menu-action-owner="hertzbeat-ui-user-menu-action"');
+    expect(html).toContain('data-hz-user-menu-action-density="angular-user-menu-item"');
+    expect(html).toContain('data-hz-user-menu-action-item="setting"');
+    expect(html).toContain('data-hz-user-menu-action-item="logout"');
+    expect(html).toContain('data-hz-user-menu-action-item="about"');
+    expect(html).toContain('data-hz-control-baseline-component="HzPassportLockSurface"');
+    expect(html).toContain('data-hz-ui-lab-passport-lock="shared"');
+    expect(html).toContain('data-hz-ui-lab-passport-lock-avatar="angular-settings-user-avatar"');
+    expect(html).toContain('data-hz-ui-lab-passport-lock-session="angular-lock-preserve-session"');
+    expect(html).toContain('data-passport-lock-session-contract="angular-lock-preserve-session"');
+    expect(html).toContain('data-hz-ui-lab-passport-lock-submit-lifecycle="angular-mark-dirty-required-then-dashboard"');
+    expect(html).toContain('data-passport-lock-submit-lifecycle-contract="angular-mark-dirty-required-then-dashboard"');
+    expect(html).toContain('data-passport-lock-submit-lifecycle-owner="hertzbeat-ui-passport-lock"');
+    expect(html).toContain('data-passport-lock-redirect-contract="angular-dashboard-next-overview"');
+    expect(html).toContain('data-hz-ui-lab-passport-lock-required-mode="angular-required-no-trim"');
+    expect(html).toContain('data-passport-lock-required-mode-contract="angular-required-no-trim"');
+    expect(html).toContain('data-passport-lock-required-mode-owner="hertzbeat-ui-passport-lock"');
+    expect(html).toContain('data-hz-ui="passport-lock-surface"');
+    expect(html).toContain('data-hz-passport-lock-owner="hertzbeat-ui-passport-lock"');
+    expect(html).toContain('data-hz-passport-lock-density="angular-lock-card"');
+    expect(html).toContain('data-hz-passport-lock-submit-lifecycle="angular-mark-dirty-required-then-dashboard"');
+    expect(html).toContain('data-hz-passport-lock-required-fields="password"');
+    expect(html).toContain('data-hz-passport-lock-required-mode="angular-required-no-trim"');
+    expect(html).toContain('data-hz-passport-lock-redirect="angular-dashboard-next-overview"');
+    expect(html).toContain('data-hz-passport-lock-submit-disabled="angular-invalid-disabled"');
+    expect(html).toContain('data-hz-passport-lock-avatar="angular-floating"');
+    expect(html).toContain('data-hz-passport-lock-avatar-source="settings-user-avatar"');
+    expect(html).toContain('data-hz-passport-lock-avatar-img="settings-user-avatar"');
+    expect(html).toContain('data-hz-passport-lock-submit-state="disabled"');
+    expect(html).toContain('data-hz-control-baseline-component="HzPassportLoginNotice"');
+    expect(html).toContain('data-hz-control-baseline-component="HzPassportLoginActionFrame"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"');
+    expect(html).toContain('data-passport-login-submit-lifecycle-contract="angular-required-default-warning-session-bootstrap-redirect"');
+    expect(html).toContain('data-passport-login-submit-lifecycle-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-required-mode="angular-required-no-trim"');
+    expect(html).toContain('data-passport-login-required-mode-contract="angular-required-no-trim"');
+    expect(html).toContain('data-passport-login-required-mode-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-session-user-name="angular-raw-identifier"');
+    expect(html).toContain('data-passport-login-session-user-name-contract="angular-raw-identifier"');
+    expect(html).toContain('data-passport-login-session-user-name-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-default-password-lifecycle="angular-sticky-until-submit"');
+    expect(html).toContain('data-passport-login-default-password-lifecycle-contract="angular-sticky-until-submit"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-startup-failure="angular-exception-500"');
+    expect(html).toContain('data-passport-login-startup-failure-contract="angular-exception-500"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-redirect-fallback="angular-root-fallback"');
+    expect(html).toContain('data-passport-login-redirect-fallback-contract="angular-root-fallback"');
+    expect(html).toContain('data-hz-ui="passport-login-action-frame"');
+    expect(html).toContain('data-hz-passport-login-action-owner="hertzbeat-ui-passport-login-action"');
+    expect(html).toContain('data-hz-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"');
+    expect(html).toContain('data-hz-passport-login-required-fields="identifier-credential"');
+    expect(html).toContain('data-hz-passport-login-required-mode="angular-required-no-trim"');
+    expect(html).toContain('data-hz-passport-login-default-password="angular-first-submit-warning"');
+    expect(html).toContain('data-hz-passport-login-default-password-lifecycle="angular-sticky-until-submit"');
+    expect(html).toContain('data-hz-passport-login-token-boundary="bff-cookie-no-localstorage"');
+    expect(html).toContain('data-hz-passport-login-session-bootstrap="angular-startup-load-after-success"');
+    expect(html).toContain('data-hz-passport-login-session-user-name="angular-raw-identifier"');
+    expect(html).toContain('data-hz-passport-login-startup-failure="angular-exception-500"');
+    expect(html).toContain('data-hz-passport-login-redirect="angular-referrer-non-passport-fallback"');
+    expect(html).toContain('data-hz-passport-login-redirect-fallback="angular-root-fallback"');
+    expect(html).toContain('data-hz-passport-login-remember-default="true"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-remember="true"');
+    expect(html).toContain('data-hz-control-baseline-component="HzPassportSessionClearFrame"');
+    expect(html).toContain('data-hz-ui-lab-passport-session-clear="angular-token-service-clear-on-passport-entry"');
+    expect(html).toContain('data-passport-session-clear-contract="angular-token-service-clear-on-passport-entry"');
+    expect(html).toContain('data-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"');
+    expect(html).toContain('data-hz-ui="passport-session-clear-frame"');
+    expect(html).toContain('data-hz-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"');
+    expect(html).toContain('data-hz-passport-session-clear-lifecycle="angular-token-service-clear-on-passport-entry"');
+    expect(html).toContain('data-hz-passport-session-clear-scope="client-marker-user-snapshot"');
+    expect(html).toContain('data-hz-passport-session-clear-boundary="no-api-logout-on-entry"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-notice="shared"');
+    expect(html).toContain('data-hz-ui="passport-login-notice"');
+    expect(html).toContain('data-hz-passport-login-notice-owner="hertzbeat-ui-passport-login-notice"');
+    expect(html).toContain('data-hz-passport-login-notice-density="angular-warning-alert"');
+    expect(html).toContain('data-hz-passport-login-notice-link="account-modify"');
+    expect(html).toContain('data-hz-control-baseline-component="HzPassportLoginValidationNotice"');
+    expect(html).toContain('data-hz-ui-lab-passport-login-validation="shared"');
+    expect(html).toContain('data-hz-ui="passport-login-validation-notice"');
+    expect(html).toContain('data-hz-passport-login-validation-owner="hertzbeat-ui-passport-login-validation"');
+    expect(html).toContain('data-hz-passport-login-validation-density="angular-error-alert"');
+    expect(html).toContain('data-hz-passport-login-validation-tone="danger"');
+    expect(html).toContain('data-hz-control-baseline-component="HzAboutModalSurface"');
+    expect(html).toContain('data-hz-ui-lab-about-modal="shared"');
+    expect(html).toContain('data-hz-ui-lab-about-closable="angular-nz-closable-false"');
+    expect(html).toContain('data-app-frame-about-closable-contract="angular-nz-closable-false"');
+    expect(html).toContain('data-app-frame-about-cancel-contract="angular-on-cancel"');
+    expect(html).toContain('data-hz-ui="about-modal-surface"');
+    expect(html).toContain('data-hz-about-modal-owner="hertzbeat-ui-about-modal"');
+    expect(html).toContain('data-hz-about-modal-density="angular-about-modal"');
+    expect(html).toContain('data-hz-about-modal-closable="false"');
+    expect(html).toContain('data-hz-about-modal-point="6"');
+    expect(html).toContain('data-hz-about-modal-release="shared"');
+    expect(html).toContain('data-hz-about-modal-not-show="shared"');
+    expect(html).toContain('data-hz-ui-lab-ai-chat-modal="shared"');
+    expect(html).toContain('data-hz-ui-lab-ai-chat-initial-message="angular-open-modal-initial-message"');
+    expect(html).toContain('data-hz-ui-lab-ai-chat-config-save-lifecycle="angular-validate-save-close-refresh"');
+    expect(html).toContain('data-hz-ui-lab-ai-chat-conversation-action-lifecycle="angular-create-select-delete-fallback"');
+    expect(html).toContain('data-hz-ui-lab-ai-chat-schedule-action-lifecycle="angular-load-create-toggle-revert-confirm-update-delete"');
+    expect(html).toContain('data-hz-ui-lab-ai-chat-stream-history-lifecycle="angular-push-user-placeholder-sse-skill-report-refresh"');
+    expect(html).toContain('data-hz-ui="ai-chat-modal-surface"');
+    expect(html).toContain('data-hz-ai-chat-owner="hertzbeat-ui-ai-chat-modal"');
+    expect(html).toContain('data-hz-ai-chat-initial-message-contract="angular-open-modal-initial-message"');
+    expect(html).toContain('data-hz-ai-chat-config-save-lifecycle="angular-validate-save-close-refresh"');
+    expect(html).toContain('data-hz-ai-chat-conversation-action-lifecycle="angular-create-select-delete-fallback"');
+    expect(html).toContain('data-hz-ai-chat-schedule-action-lifecycle="angular-load-create-toggle-revert-confirm-update-delete"');
+    expect(html).toContain('data-hz-ai-chat-stream-history-lifecycle="angular-push-user-placeholder-sse-skill-report-refresh"');
+    expect(html).toContain('data-app-frame-ai-chat-owner="hertzbeat-ui-ai-chat-modal"');
+    expect(html).toContain('data-app-frame-ai-chat-initial-message-contract="angular-open-modal-initial-message"');
+    expect(html).toContain('data-app-frame-ai-chat-initial-message-owner="route-ai-chat-modal-contract"');
+    expect(html).toContain('data-app-frame-ai-chat-config-save-lifecycle-contract="angular-validate-save-close-refresh"');
+    expect(html).toContain('data-app-frame-ai-chat-config-save-lifecycle-owner="route-ai-chat-config-contract"');
+    expect(html).toContain('data-app-frame-ai-chat-conversation-action-lifecycle-contract="angular-create-select-delete-fallback"');
+    expect(html).toContain('data-app-frame-ai-chat-conversation-action-lifecycle-owner="route-ai-chat-conversation-contract"');
+    expect(html).toContain('data-app-frame-ai-chat-schedule-action-lifecycle-contract="angular-load-create-toggle-revert-confirm-update-delete"');
+    expect(html).toContain('data-app-frame-ai-chat-schedule-action-lifecycle-owner="route-ai-chat-schedule-contract"');
+    expect(html).toContain('data-app-frame-ai-chat-stream-history-lifecycle-contract="angular-push-user-placeholder-sse-skill-report-refresh"');
+    expect(html).toContain('data-app-frame-ai-chat-stream-history-lifecycle-owner="route-ai-chat-stream-contract"');
+    expect(html).toContain('data-hz-ai-chat-new-chat-status="idle"');
+    expect(html).toContain('data-hz-ai-chat-sidebar="conversation-list"');
+    expect(html).toContain('data-hz-ai-chat-conversation-delete="shared"');
+    expect(html).toContain('data-hz-ai-chat-conversation-delete-confirm="shared"');
+    expect(html).toContain('data-hz-ai-chat-conversation-delete-status="confirming"');
+    expect(html).toContain('data-hz-ai-chat-conversation-status="ready"');
+    expect(html).toContain('data-hz-ai-chat-conversation-count="2"');
+    expect(html).toContain('data-hz-ai-chat-conversation-action-lifecycle="angular-create-select-delete-fallback"');
+    expect(html).toContain('Investigate checkout errors');
+    expect(html).toContain('data-hz-ai-chat-main="message-flow"');
+    expect(html).toContain('data-hz-ai-chat-message-status="ready"');
+    expect(html).toContain('data-hz-ai-chat-message-count="3"');
+    expect(html).toContain('data-hz-ai-chat-stream-history-lifecycle="angular-push-user-placeholder-sse-skill-report-refresh"');
+    expect(html).toContain('Why did checkout latency spike?');
+    expect(html).toContain('data-hz-ai-chat-initial-message="true"');
+    expect(html).toContain('data-hz-ai-chat-initial-message-source="angular-open-modal"');
+    expect(html).toContain('data-hz-ai-chat-streaming-indicator="shared"');
+    expect(html).toContain('data-hz-ai-chat-streaming-status="typing"');
+    expect(html).toContain('data-hz-ai-chat-config-open="shared"');
+    expect(html).toContain('data-hz-ai-chat-config-panel="shared"');
+    expect(html).toContain('data-hz-ai-chat-config-owner="hertzbeat-ui-ai-chat-config"');
+    expect(html).toContain('data-hz-ai-chat-config-status="ready"');
+    expect(html).toContain('data-hz-ai-chat-config-required-fields="api-key-provider-base-url-model"');
+    expect(html).toContain('data-hz-ai-chat-config-provider="shared"');
+    expect(html).toContain('data-hz-ai-chat-config-api-key="shared"');
+    expect(html).toContain('data-hz-ai-chat-config-base-url="shared"');
+    expect(html).toContain('data-hz-ai-chat-config-model="shared"');
+    expect(html).toContain('Provider configuration loaded from /config/provider');
+    expect(html).toContain('data-hz-ai-chat-schedule-open="shared"');
+    expect(html).toContain('data-hz-ai-chat-schedule-panel="shared"');
+    expect(html).toContain('data-hz-ai-chat-schedule-owner="hertzbeat-ui-ai-chat-schedule"');
+    expect(html).toContain('data-hz-ai-chat-schedule-status="ready"');
+    expect(html).toContain('data-hz-ai-chat-schedule-required-fields="sop-name-cron-expression"');
+    expect(html).toContain('data-hz-ai-chat-schedule-list="shared"');
+    expect(html).toContain('data-hz-ai-chat-schedule-count="2"');
+    expect(html).toContain('data-hz-ai-chat-schedule-toggle="shared"');
+    expect(html).toContain('data-hz-ai-chat-schedule-delete-status="confirming"');
+    expect(html).toContain('data-hz-ai-chat-schedule-delete-confirm="shared"');
+    expect(html).toContain('data-hz-ai-chat-schedule-delete-submit="shared"');
+    expect(html).toContain('data-hz-ai-chat-schedule-edit-form="shared"');
+    expect(html).toContain('data-hz-ai-chat-schedule-create-form="shared"');
+    expect(html).toContain('data-hz-ai-chat-schedule-cron-help="shared"');
+    expect(html).toContain('Schedules loaded from /ai/schedule/conversation/7');
+    expect(html).toContain('daily-health');
+    expect(html).toContain('weekly-report');
+    expect(html).toContain('data-hz-ai-chat-input-shell="shared"');
+    expect(html).toContain('data-hz-ai-chat-input-mode="editable"');
+    expect(html).toContain('data-hz-ai-chat-send-lifecycle="angular-push-user-placeholder-sse"');
+    expect(html).toContain('data-hz-ai-chat-send-status="sending"');
+    expect(html).toContain('value="Summarize checkout errors"');
+    expect(html).toContain('data-hz-panel-surface-padding="view-switch"');
+    expect(html).toContain('data-hz-ui-lab-panel-surface-padding="view-switch"');
+    expect(html).toContain('data-hz-ui-lab-log-view-switch-panel-surface="shared"');
+    expect(html).toContain('data-log-view-switch-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('px-4 py-3');
+    expect(html).toContain('px-5 py-4');
+    expect(html).toContain('px-4 py-4');
+    expect(html).toContain('data-hz-panel-surface-clip="true"');
+    expect(html).toContain('data-hz-ui-lab-panel-surface-clip="shared"');
+    expect(html).toContain('data-hz-control-baseline-component="HzStateNotice"');
+    expect(html).toContain('data-hz-ui-lab-handoff-hint-surface="shared"');
+    expect(html).toContain('data-hz-state-variant="hint"');
+    expect(html).toContain('data-hz-state-hint-owner="hertzbeat-ui-state-notice"');
+    expect(html).toContain('data-hz-control-baseline-component="HzStatusBadge"');
+    expect(html).toContain('data-hz-ui-lab-attribution-diagnostic-badge="shared"');
+    expect(html).toContain('data-hz-status-size="xs"');
+    expect(html).toContain('data-hz-ui-lab-trace-table-count-badge="shared"');
+    expect(html).toContain('data-hz-ui-lab-stream-status-badge="shared"');
+    expect(html).toContain('data-hz-status-size="md"');
+    expect(html).toContain('data-hz-control-baseline-component="HzSelect"');
+    expect(html).toContain('data-hz-control-baseline-component="HzInput"');
+    expect(html).toContain('data-hz-control-baseline-component="HzNumberStepper"');
+    expect(html).toContain('data-hz-control-baseline-component="HzCheckbox"');
+    expect(html).toContain('data-hz-control-baseline-component="HzSwitch"');
+    expect(html).toContain('data-hz-ui-lab-monitor-disabled-selection="shared"');
+    expect(html).toContain('data-monitor-disabled-selection-owner="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-hz-ui-lab-monitor-unavailable-refresh="shared"');
+    expect(html).toContain('data-monitor-unavailable-refresh-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-control-baseline-component="HzFileInput"');
+    expect(html).toContain('data-hz-ui-lab-scrollbar-baseline="thin-dark-platform"');
+    expect(html).toContain('data-hz-ui-lab-scrollbar-owner="hertzbeat-ui-scrollbar"');
+    expect(html).toContain('data-hz-ui-lab-control-stack="shared"');
+    expect(html).toContain('data-hz-ui-lab-inline-control-stack="shared"');
+    expect(html).toContain('data-signal-query-control-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-ui-lab-trace-search-field-frame="shared"');
+    expect(html).toContain('data-trace-search-field-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-ui="search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-icon="true"');
+    expect(html).toContain('data-hz-ui-lab-trace-search-field-icon="shared"');
+    expect(html).toContain('data-trace-search-field-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-hz-ui="search-field-icon"');
+    expect(html).toContain('data-hz-search-field-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-hz-search-field-icon-size="md"');
+    expect(html).toContain('data-hz-ui-lab-trace-search-input-inset="search-icon"');
+    expect(html).toContain('data-hz-input-inset="search-icon"');
+    expect(html).toContain('data-hz-ui-lab-metrics-search-field-frame="shared"');
+    expect(html).toContain('data-metrics-search-field-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-ui-lab-metrics-search-field-icon="shared"');
+    expect(html).toContain('data-metrics-search-field-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-hz-ui-lab-metrics-search-input="shared"');
+    expect(html).toContain('data-metrics-search-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-metrics-context-input="service-name"');
+    expect(html).toContain('data-hz-ui-lab-metrics-context-input="namespace"');
+    expect(html).toContain('data-hz-ui-lab-metrics-context-input="trace-id"');
+    expect(html).toContain('data-metrics-context-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-log-search-field-frame="shared"');
+    expect(html).toContain('data-log-search-field-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-width="log-query"');
+    expect(html).toContain('data-hz-ui-lab-log-search-field-icon="shared"');
+    expect(html).toContain('data-log-search-field-icon-owner="hertzbeat-ui-search-field-icon"');
+    expect(html).toContain('data-hz-ui-lab-log-search-input="shared"');
+    expect(html).toContain('data-log-search-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-width="log-query-expression"');
+    expect(html).toContain('data-hz-ui-lab-log-severity-select="shared"');
+    expect(html).toContain('data-log-severity-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-select-width="log-severity"');
+    expect(html).toContain('data-hz-select-trigger-tone="signal-query"');
+    expect(html).toContain('data-hz-ui-lab-log-query-token-input="trace-id"');
+    expect(html).toContain('data-hz-ui-lab-log-query-token-input="span-id"');
+    expect(html).toContain('data-log-query-token-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-width="log-query-token"');
+    expect(html).toContain('data-hz-ui-lab-log-query-body-input="shared"');
+    expect(html).toContain('data-log-query-body-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-width="log-query-body"');
+    expect(html).toContain('data-hz-ui-lab-metrics-aggregation-select="shared"');
+    expect(html).toContain('data-metrics-aggregation-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-select-width="metrics-aggregation"');
+    expect(html).toContain('data-hz-ui-lab-metrics-group-by-select="shared"');
+    expect(html).toContain('data-metrics-group-by-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-select-width="metrics-group-by"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-action-group="shared"');
+    expect(html).toContain('data-metrics-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-action-icon="run"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-action-icon="reset"');
+    expect(html).toContain('data-metrics-query-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-ui-lab-log-query-panel-surface="shared"');
+    expect(html).toContain('data-log-query-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-ui-lab-log-query-panel-control-stack="shared"');
+    expect(html).toContain('data-log-query-panel-control-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-ui-lab-log-query-panel-search-frame="shared"');
+    expect(html).toContain('data-log-query-panel-search-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-ui-lab-log-query-panel-severity-select="shared"');
+    expect(html).toContain('data-log-query-panel-severity-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-log-query-panel-token-input="trace-id"');
+    expect(html).toContain('data-log-query-panel-token-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-log-query-panel-body-input="shared"');
+    expect(html).toContain('data-log-query-panel-body-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-surface="shared"');
+    expect(html).toContain('data-metrics-query-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="query"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-control-stack="shared"');
+    expect(html).toContain('data-metrics-query-panel-control-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-control-stack-layout="inline-wrap"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-search-frame="shared"');
+    expect(html).toContain('data-metrics-query-panel-search-frame-owner="hertzbeat-ui-search-field-frame"');
+    expect(html).toContain('data-hz-search-field-frame-width="metrics-query"');
+    expect(html).toContain('relative min-w-[320px] max-w-[560px] flex-1');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-input="shared"');
+    expect(html).toContain('data-metrics-query-panel-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-width="metrics-query-expression"');
+    expect(html).toContain('w-full font-mono');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-aggregation-select="shared"');
+    expect(html).toContain('data-metrics-query-panel-aggregation-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-context-stack="shared"');
+    expect(html).toContain('data-metrics-query-panel-context-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-control-stack-spacing="top-2"');
+    expect(html).toContain('flex min-w-0 flex-wrap items-center gap-2 mt-2');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-context-input="service-name"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-context-input="namespace"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-context-input="environment"');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-context-input="trace-id"');
+    expect(html).toContain('data-metrics-query-panel-context-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-input-width="metrics-context"');
+    expect(html).toContain('data-hz-input-width="metrics-context-compact"');
+    expect(html).toContain('data-hz-input-width="metrics-trace-id"');
+    expect(html).toContain('min-w-[220px] max-w-[360px] flex-1 font-mono');
+    expect(html).toContain('data-hz-ui-lab-metrics-query-panel-action-group="shared"');
+    expect(html).toContain('data-metrics-query-panel-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-ui-lab-metrics-handoff-actions="shared"');
+    expect(html).toContain('data-metrics-handoff-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-action-group-layout="grid-2"');
+    expect(html).toContain('data-hz-ui-lab-metrics-handoff-action="entity"');
+    expect(html).toContain('data-hz-ui-lab-metrics-handoff-action="alerts"');
+    expect(html).toContain('data-hz-button-link-layout="full"');
+    expect(html).toContain('data-hz-ui-lab-metrics-disabled-handoff-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(html).toContain('data-hz-disabled-action-shell-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(html).toContain('data-hz-disabled-action-shell-layout="full"');
+    expect(html).toContain('inline-flex w-full');
+    expect(html).toContain('data-hz-button-layout="full"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-summary="shared"');
+    expect(html).toContain('data-metrics-series-summary-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-count-badge="shared"');
+    expect(html).toContain('data-metrics-series-count-badge-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-summary-strip="shared"');
+    expect(html).toContain('data-metrics-series-summary-strip-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-summary-cell="service"');
+    expect(html).toContain('data-metrics-series-summary-cell-owner="hertzbeat-ui-stat-cell"');
+    expect(html).toContain('data-hz-stat-frame="inset"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-data-table="shared"');
+    expect(html).toContain('data-metrics-series-data-table-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-table-panel-surface="shared"');
+    expect(html).toContain('data-metrics-series-table-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-table-header="shared"');
+    expect(html).toContain('data-metrics-series-table-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-ui="panel-header"');
+    expect(html).toContain('data-hz-panel-header-density="operator-compact"');
+    expect(html).toContain('data-hz-panel-header-chrome="transparent-topless"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-table-count-badge="shared"');
+    expect(html).toContain('data-metrics-series-table-count-badge-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).toContain('data-hz-panel-surface-clip="true"');
+    expect(html).toContain('data-hz-data-table-variant="embedded"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-row="checkout-latency"');
+    expect(html).toContain('data-metrics-series-row-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-detail-layout="shared"');
+    expect(html).toContain('data-metrics-series-detail-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="metrics-series-detail"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-detail-layout-series="shared"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-detail-layout-detail="shared"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-empty-state="shared"');
+    expect(html).toContain('data-metrics-series-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(html).toContain('data-hz-ui="empty-state"');
+    expect(html).toContain('data-hz-empty-state-layout="table-panel"');
+    expect(html).toContain('data-hz-ui-lab-metrics-trend-empty-state="shared"');
+    expect(html).toContain('data-metrics-trend-empty-state-owner="hertzbeat-ui-state-notice"');
+    expect(html).toContain('data-hz-state-frame="trend-empty"');
+    expect(html).toContain('flex h-full min-w-0 flex-1 flex-col justify-center border-dashed border-[#2a303a] bg-[#0c1016] text-center');
+    expect(html).toContain('data-hz-ui-lab-metrics-trend-frame="shared"');
+    expect(html).toContain('data-metrics-trend-frame-owner="hertzbeat-ui-trend-frame"');
+    expect(html).toContain('data-hz-ui-lab-metrics-trend-bars-frame="shared"');
+    expect(html).toContain('data-metrics-trend-bars-frame-owner="hertzbeat-ui-trend-frame"');
+    expect(html).toContain('data-hz-ui-lab-metrics-trend-bar="shared"');
+    expect(html).toContain('data-metrics-trend-bar-owner="hertzbeat-ui-trend-bar"');
+    expect(html).toContain('data-hz-ui="trend-frame"');
+    expect(html).toContain('data-hz-ui="trend-bar"');
+    expect(html).toContain('data-hz-trend-frame-density="operator-compact"');
+    expect(html).toContain('data-hz-trend-bar-density="operator-compact"');
+    expect(html).toContain('data-hz-ui-lab-metrics-trend-sample-helper="shared"');
+    expect(html).toContain('data-metrics-trend-sample-helper-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-hz-ui="data-meta-text"');
+    expect(html).toContain('data-hz-data-meta-spacing="trend-helper"');
+    expect(html).toContain('mt-2 text-[11px] text-[#6d7788]');
+    expect(html).toContain('3 sample points');
+    expect(html).toContain('data-hz-ui="state-notice"');
+    expect(html).toContain('data-hz-state-variant="hint"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-header-layout="shared"');
+    expect(html).toContain('data-metrics-chart-header-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="metrics-chart-toolbar"');
+    expect(html).toContain('mb-3 gap-2 text-[12px] text-[#8792a5] sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-title-label="shared"');
+    expect(html).toContain('data-metrics-chart-title-label-owner="hertzbeat-ui-panel-title-label"');
+    expect(html).toContain('data-hz-ui="panel-title-label"');
+    expect(html).toContain('data-hz-panel-title-label-density="operator-compact"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-toolbar-actions="shared"');
+    expect(html).toContain('data-metrics-chart-toolbar-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-action-group-layout="end-wrap"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-panel-surface="shared"');
+    expect(html).toContain('data-metrics-chart-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-panel-variant="chart-inner"');
+    expect(html).toContain('data-metrics-chart-panel-variant-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-ui-lab-metrics-echarts-panel="shared"');
+    expect(html).toContain('data-metrics-echarts-panel-owner="hertzbeat-ui-echarts-panel"');
+    expect(html).toContain('data-hz-ui-lab-metrics-echarts-edge="shared"');
+    expect(html).toContain('data-hz-echarts-edge="metrics-chart"');
+    expect(html).toContain('data-hz-ui="panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="chart-inner"');
+    expect(html).toContain('data-hz-panel-surface-variant="chart-inner"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-band-surface="shared"');
+    expect(html).toContain('data-metrics-chart-band-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-padding="chart"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-stack-layout="shared"');
+    expect(html).toContain('data-metrics-chart-stack-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="chart-stack"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-fact-chips="shared"');
+    expect(html).toContain('data-metrics-chart-fact-chips-owner="hertzbeat-ui-toolbar-chips"');
+    expect(html).toContain('data-hz-chip-group-align="end"');
+    expect(html).toContain('justify-end');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-fact-chip="shared"');
+    expect(html).toContain('data-metrics-chart-fact-chip-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-ui="chip-group"');
+    expect(html).toContain('data-hz-status-badge-layout="metric-fact"');
+    expect(html).toContain('data-hz-status-badge-layout="zoom-draft"');
+    expect(html).toContain('data-hz-status-badge-part="label"');
+    expect(html).toContain('data-hz-status-badge-part="value"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-toolbar-count="shared"');
+    expect(html).toContain('data-metrics-chart-toolbar-count-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-action-group-layout="end-wrap"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-series-count-badge="shared"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-zoom-actions="shared"');
+    expect(html).toContain('data-metrics-chart-zoom-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-zoom-draft="shared"');
+    expect(html).toContain('data-metrics-chart-zoom-draft-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-ui-lab-metrics-chart-zoom-apply="shared"');
+    expect(html).toContain('data-hz-ui="button"');
+    expect(html).toContain('data-hz-control-height="28"');
+    expect(html).not.toContain('data-hz-ui-lab-metrics-chart-zoom-apply="shared" class="');
+    expect(html).not.toContain('class="h-7 px-2" data-hz-ui-lab-metrics-chart-zoom-apply="shared"');
+    expect(html).toContain('data-hz-ui-lab-metrics-secondary-context-section="shared"');
+    expect(html).toContain('data-metrics-secondary-context-section-owner="hertzbeat-ui-panel-section"');
+    expect(html).toContain('data-hz-panel-section-spacing="stack-2"');
+    expect(html).toContain('data-hz-ui-lab-metrics-collapsible-section="shared"');
+    expect(html).toContain('data-metrics-collapsible-section-owner="hertzbeat-ui-collapsible-section"');
+    expect(html).toContain('data-hz-ui-lab-metrics-collapsible-shell="shared"');
+    expect(html).toContain('data-hz-ui="collapsible-section"');
+    expect(html).toContain('data-hz-collapsible-owner="hertzbeat-ui-collapsible-section"');
+    expect(html).toContain('data-hz-collapsible-surface="inset"');
+    expect(html).toContain('data-hz-collapsible-summary-owner="hertzbeat-ui-collapsible-section"');
+    expect(html).toContain('data-hz-ui-lab-metrics-linked-record-handoff="shared"');
+    expect(html).toContain('data-metrics-linked-record-handoff-owner="hertzbeat-ui-context-handoff"');
+    expect(html).toContain('data-hz-ui="context-handoff"');
+    expect(html).toContain('data-hz-context-handoff-owner="hertzbeat-ui-context-handoff"');
+    expect(html).toContain('data-hz-context-handoff-frame="flush"');
+    expect(html).toContain('data-hz-ui-lab-metrics-linked-record-action="logs"');
+    expect(html).toContain('data-hz-ui-lab-metrics-linked-record-action="traces"');
+    expect(html).toContain('data-hz-ui-lab-metrics-linked-record-action="alerts"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-panel-surface="shared"');
+    expect(html).toContain('data-metrics-detail-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-clip="true"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-panel-stickiness="top-4"');
+    expect(html).toContain('data-metrics-detail-panel-stickiness-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-panel-surface-stickiness="top-4"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-panel-empty="shared"');
+    expect(html).toContain('data-metrics-detail-panel-empty-owner="hertzbeat-ui-assistive-marker"');
+    expect(html).toContain('data-hz-ui="assistive-marker"');
+    expect(html).toContain('data-hz-assistive-marker-visibility="sr-only"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-panel-header="shared"');
+    expect(html).toContain('data-metrics-detail-panel-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-ui="panel-header"');
+    expect(html).toContain('data-hz-panel-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-panel-header-chrome="transparent-framed"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-rows="shared"');
+    expect(html).toContain('data-metrics-detail-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-hz-detail-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-stats="shared"');
+    expect(html).toContain('data-metrics-detail-stats-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-stat="non-empty"');
+    expect(html).toContain('data-metrics-detail-stat-owner="hertzbeat-ui-stat-cell"');
+    expect(html).toContain('data-hz-ui-lab-metrics-attribution-diagnostics="shared"');
+    expect(html).toContain('data-metrics-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(html).toContain('data-hz-attribute-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(html).toContain('data-hz-attribute-diagnostics-frame="embedded"');
+    expect(html).toContain('data-hz-ui-lab-metrics-entity-context-rows="shared"');
+    expect(html).toContain('data-metrics-entity-context-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-hz-detail-rows-padding="compact-y"');
+    expect(html).toContain('data-hz-ui-lab-trace-query-token-field="shared"');
+    expect(html).toContain('data-trace-query-token-field-owner="hertzbeat-ui-query-token-field"');
+    expect(html).toContain('data-hz-ui="query-token-field"');
+    expect(html).toContain('data-hz-query-token-field-owner="hertzbeat-ui-query-token-field"');
+    expect(html).toContain('data-hz-query-token-field-width="compact"');
+    expect(html).toContain('data-hz-query-token-input-owner="hertzbeat-ui-query-token-field"');
+    expect(html).toContain('data-hz-ui-lab-trace-query-status-select="shared"');
+    expect(html).toContain('data-trace-query-status-select-owner="hertzbeat-ui-query-status-select"');
+    expect(html).toContain('data-hz-query-status-select-owner="hertzbeat-ui-query-status-select"');
+    expect(html).toContain('data-hz-query-status-select-width="compact"');
+    expect(html).toContain('data-hz-ui-lab-trace-query-action-group="shared"');
+    expect(html).toContain('data-trace-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-query-action-group-owner="hertzbeat-ui-query-action-group"');
+    expect(html).toContain('data-hz-query-action-group-kind="run-reset"');
+    expect(html).toContain('data-hz-ui-lab-trace-query-action-icon="run"');
+    expect(html).toContain('data-hz-ui-lab-trace-query-action-icon="reset"');
+    expect(html).toContain('data-trace-query-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-button-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-button-icon-size="md"');
+    expect(html).toContain('data-hz-ui-lab-time-control-stack="shared"');
+    expect(html).toContain('data-signal-time-control-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-ui-lab-metrics-time-control-stack="shared"');
+    expect(html).toContain('data-metrics-time-control-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-ui-lab-time-range-control="narrow-rail"');
+    expect(html).toContain('data-signal-time-range-control-owner="hertzbeat-shared-time-range-control"');
+    expect(html).toContain('data-metrics-time-range-control-owner="hertzbeat-shared-time-range-control"');
+    expect(html).toContain('data-time-range-control="hertzbeat-shared"');
+    expect(html).toContain('data-time-range-control-align="end"');
+    expect(html).toContain('data-hz-ui-lab-trace-header-copy="shared"');
+    expect(html).toContain('data-trace-header-copy-owner="hertzbeat-ui-workbench-header-copy"');
+    expect(html).toContain('data-hz-ui="workbench-header-copy"');
+    expect(html).toContain('data-hz-workbench-header-copy-owner="hertzbeat-ui-workbench-header-copy"');
+    expect(html).toContain('data-hz-ui-lab-signal-workbench-shell="shared"');
+    expect(html).toContain('data-signal-workbench-shell-owner="hertzbeat-ui-signal-workbench-shell"');
+    expect(html).toContain('data-hz-ui="signal-workbench-shell"');
+    expect(html).toContain('data-hz-signal-workbench-shell-owner="hertzbeat-ui-signal-workbench-shell"');
+    expect(html).toContain('data-hz-signal-workbench-shell-layout="default"');
+    expect(html).toContain('data-hz-signal-workbench-shell-content="true"');
+    expect(html).toContain('data-hz-signal-workbench-shell-content-layout="default"');
+    expect(html).toContain('data-hz-ui-lab-metrics-workbench-shell="shared"');
+    expect(html).toContain('data-metrics-workbench-shell-owner="hertzbeat-ui-signal-workbench-shell"');
+    expect(html).toContain('data-hz-signal-workbench-shell-layout="metrics-workbench"');
+    expect(html).toContain('data-hz-signal-workbench-shell-content-layout="metrics-workbench"');
+    expect(html).toContain('data-hz-ui-lab-metrics-workbench-shell-panel="shared"');
+    expect(html).toContain('data-metrics-workbench-shell-panel-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('flex flex-col gap-3 px-3 pb-3 pt-0');
+    expect(html).toContain('class="contents"');
+    expect(html).toContain('data-hz-ui-lab-log-detail-action-stack="shared"');
+    expect(html).toContain('data-log-detail-action-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-hz-ui-lab-log-detail-aside="shared"');
+    expect(html).toContain('data-log-detail-aside-owner="hertzbeat-ui-detail-aside"');
+    expect(html).toContain('data-hz-ui="detail-aside"');
+    expect(html).toContain('data-hz-detail-aside-owner="hertzbeat-ui-detail-aside"');
+    expect(html).toContain('data-hz-ui-lab-log-detail-body-stack="shared"');
+    expect(html).toContain('data-log-detail-body-stack-owner="hertzbeat-ui-detail-body-stack"');
+    expect(html).toContain('data-hz-ui="detail-body-stack"');
+    expect(html).toContain('data-hz-detail-body-stack-owner="hertzbeat-ui-detail-body-stack"');
+    expect(html).toContain('data-hz-control-stack-layout="end-inline"');
+    expect(html).toContain('data-hz-control-stack-layout="stack"');
+    expect(html).toContain('data-hz-control-stack-layout="inline-wrap"');
+    const sharedButtonMarkup = html.match(/<button[^>]*data-hz-ui="button"[^>]*>/g) ?? [];
+    expect(sharedButtonMarkup.length).toBeGreaterThan(0);
+    sharedButtonMarkup.forEach(markup => {
+      if (markup.includes('data-hz-table-row-action-owner="hertzbeat-ui-table-row-action-button"')) {
+        expect(markup).toContain('data-hz-control-height="24"');
+        expect(markup).toContain('h-6');
+        expect(markup).toContain('data-hz-table-row-action-width="root-span"');
+      } else if (markup.includes('data-hz-control-height="24"')) {
+        expect(markup).toContain('data-hz-control-height="24"');
+        expect(markup).toContain('h-6');
+      } else if (markup.includes('data-hz-control-height="32"')) {
+        expect(markup).toContain('data-hz-control-height="32"');
+        expect(markup).toContain('h-8');
+      } else {
+        expect(markup).toContain('data-hz-control-height="28"');
+        expect(markup).toContain('h-7');
+      }
+      expect(markup).toMatch(/data-hz-control-edge="(?:flat|solid)"/);
+      expect(markup).toMatch(/data-hz-button-tier="(?:flat-neutral|solid-primary|solid-danger)"/);
+      expect(markup).not.toContain('bg-[#1a1013]');
+      expect(markup).not.toContain('min-w-[84px]');
+      expect(markup).not.toContain('min-w-[104px]');
+    });
+    const sharedSelectTriggerMarkup = html.match(/<button[^>]*data-hz-ui="select-trigger"[^>]*>/g) ?? [];
+    expect(sharedSelectTriggerMarkup.length).toBeGreaterThan(0);
+    sharedSelectTriggerMarkup.forEach(markup => {
+      expect(markup).toContain('data-hz-control-height="32"');
+      expect(markup).not.toContain('data-hz-control-height="28"');
+      expect(markup).toContain('data-hz-control-edge="lined"');
+      expect(markup).not.toContain('bg-[var(--hz-ui-control)]');
+    });
+    expect(html).toContain('data-hz-ui-lab-monitor-import-file-input="shared"');
+    expect(html).toContain('data-hz-ui="file-input"');
+    expect(html).toContain('data-hz-ui-lab-monitor-host-name-autofill="angular-new-host-change"');
+    expect(html).toContain('data-monitor-editor-host-name-autofill-contract="angular-new-host-change"');
+    expect(html).toContain('data-monitor-editor-host-name-autofill-target="monitor-name"');
+    expect(html).toContain('data-hz-ui-lab-monitor-grafana-template-upload="angular-json-template-upload"');
+    expect(html).toContain('data-monitor-grafana-template-upload="angular-json-template-upload"');
+    expect(html).toContain('data-monitor-grafana-template-upload-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-monitor-grafana-template-upload-trigger-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-password-param="angular-app-multi-func-password"');
+    expect(html).toContain('data-monitor-param-password-input="password"');
+    expect(html).toContain('data-monitor-param-password-contract="angular-app-multi-func-password"');
+    expect(html).toContain('data-monitor-editor-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-monitor-boolean-param="angular-nz-switch"');
+    expect(html).toContain('data-monitor-param-switch="ssl"');
+    expect(html).toContain('data-monitor-param-boolean-contract="angular-nz-switch"');
+    expect(html).toContain('data-monitor-editor-switch-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-hz-ui-lab-monitor-textarea-param="angular-nz-input-textarea-rows-8"');
+    expect(html).toContain('data-monitor-param-textarea="description"');
+    expect(html).toContain('data-monitor-param-textarea-contract="angular-nz-input-textarea-rows-8"');
+    expect(html).toContain('data-monitor-editor-textarea-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-hz-ui-lab-monitor-filter-bar="shared"');
+    expect(html).toContain('data-hz-ui-lab-data-meta-text="shared"');
+    expect(html).toContain('data-hz-ui="data-meta-text"');
+    expect(html).toContain('data-hz-data-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-hz-ui-lab-data-cell-text="shared"');
+    expect(html).toContain('data-hz-ui="data-cell-text"');
+    expect(html).toContain('data-hz-data-cell-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-hz-data-cell-tone="success"');
+    expect(html).toContain('data-hz-data-cell-tone="muted"');
+    expect(html).toContain('data-hz-data-cell-casing="plain"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-entity="shared"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-entity-owner="hertzbeat-ui-data-cell-stack"');
+    expect(html).toContain('data-hz-ui="data-cell-stack"');
+    expect(html).toContain('data-hz-data-cell-stack-owner="hertzbeat-ui-data-cell-stack"');
+    expect(html).toContain('data-hz-data-cell-stack-width="metrics-entity"');
+    expect(html).toContain('block min-w-[140px]');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-entity-label-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-entity-meta-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-latest-owner="hertzbeat-ui-data-cell-text"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-summary-section="shared"');
+    expect(html).toContain('data-metrics-series-summary-section-owner="hertzbeat-ui-panel-section"');
+    expect(html).toContain('data-hz-ui="panel-section"');
+    expect(html).toContain('data-hz-panel-section-owner="hertzbeat-ui-panel-section"');
+    expect(html).toContain('data-hz-panel-section-padding="summary"');
+    expect(html).toContain('data-hz-ui-lab-metrics-series-summary-strip="shared"');
+    expect(html).toContain('data-metrics-series-summary-strip-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-hz-stat-strip-frame="panel-solid"');
+    expect(html).toContain('data-hz-stat-frame="flush"');
+    expect(html).toContain('data-hz-stat-strip-frame="panel-inset"');
+    expect(html).toContain('data-hz-stat-strip-spacing="compact"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-body-section="shared"');
+    expect(html).toContain('data-metrics-detail-body-section-owner="hertzbeat-ui-panel-section"');
+    expect(html).toContain('data-hz-panel-section-divider="none"');
+    expect(html).toContain('data-hz-detail-rows-offset="top"');
+    expect(html).toContain('data-hz-ui-lab-metrics-selected-series-context-rows="shared"');
+    expect(html).toContain('data-metrics-selected-series-context-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-hz-ui-lab-metrics-selected-series-evidence-rows="shared"');
+    expect(html).toContain('data-metrics-selected-series-evidence-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-hz-detail-rows-boundary="top"');
+    expect(html).toContain('data-hz-ui-lab-metrics-handoff-action-section="shared"');
+    expect(html).toContain('data-metrics-handoff-action-section-owner="hertzbeat-ui-panel-section"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-handoff-actions="shared"');
+    expect(html).toContain('data-metrics-detail-handoff-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-button-link-layout="full"');
+    expect(html).toContain('data-hz-ui-lab-metrics-detail-handoff-actions="shared"');
+    expect(html).toContain('data-metrics-detail-handoff-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-panel-section-divider="top"');
+    expect(html).toContain('data-hz-ui-lab-trace-data-cell-text="shared"');
+    expect(html).toContain('data-hz-ui-lab-trace-data-cell="start"');
+    expect(html).toContain('data-hz-ui-lab-trace-data-cell="service"');
+    expect(html).toContain('data-hz-ui-lab-trace-data-cell="duration"');
+    expect(html).toContain('data-hz-ui-lab-trace-data-cell="trace-id"');
+    expect(html).toContain('data-hz-data-cell-variant="timestamp"');
+    expect(html).toContain('data-hz-data-cell-variant="identifier"');
+    expect(html).toContain('data-hz-data-cell-width="trace-id"');
+    expect(html).toContain('data-hz-ui-lab-trace-table-row-action="shared"');
+    expect(html).toContain('data-hz-ui-lab-trace-row-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-table-row-action-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-table-row-action-width="root-span"');
+    expect(html).toContain('data-hz-ui-lab-disabled-action-shell="shared"');
+    expect(html).toContain('data-hz-ui-lab-disabled-action-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(html).toContain('data-hz-ui="disabled-action-shell"');
+    expect(html).toContain('data-hz-disabled-action-shell-owner="hertzbeat-ui-disabled-action-shell"');
+    expect(html).toContain('data-hz-ui-lab-trace-table-empty-state="shared"');
+    expect(html).toContain('data-signal-table-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(html).toContain('data-hz-empty-state-layout="table-panel"');
+    expect(html).toContain('data-hz-ui="monitor-filter-bar"');
+    expect(html).toContain('Search monitor name / instance');
+    expect(html).toContain('MySQL');
+    expect(html).toContain('data-hz-ui-lab-monitor-filter-picker="shared"');
+    expect(html).toContain('data-monitor-filter-picker-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-app-picker-trigger="shared"');
+    expect(html).toContain('data-monitor-app-picker-trigger-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-app-autosubmit="shared"');
+    expect(html).toContain('data-monitor-app-filter-autosubmit-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-monitor-search-enter-submit="shared"');
+    expect(html).toContain('data-monitor-search-enter-submit-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-monitor-search-clear="shared"');
+    expect(html).toContain('data-monitor-search-clear-owner="hertzbeat-ui-icon-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-label-filter="shared"');
+    expect(html).toContain('data-monitor-label-filter-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-monitor-label-enter-submit="shared"');
+    expect(html).toContain('data-monitor-label-enter-submit-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-monitor-label-clear="shared"');
+    expect(html).toContain('data-monitor-label-clear-owner="hertzbeat-ui-icon-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-status-autosubmit="shared"');
+    expect(html).toContain('data-monitor-status-filter-autosubmit-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-monitor-manual-refresh="shared"');
+    expect(html).toContain('data-monitor-manual-refresh-owner="hertzbeat-ui-icon-button"');
+    expect(html).toContain('data-monitor-manual-refresh-action="sync"');
+    expect(html).toContain('data-hz-ui-lab-monitor-auto-refresh="shared"');
+    expect(html).toContain('data-monitor-auto-refresh-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-monitor-auto-refresh-interval-ms="120000"');
+    expect(html).toContain('data-hz-ui-lab-monitor-page-jump="shared"');
+    expect(html).toContain('data-monitor-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-page-jump="shared"');
+    expect(html).toContain('data-alert-group-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-alert-group-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-select-current-page="angular-select-all"');
+    expect(html).toContain('data-alert-group-select-current-page-owner="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-no-select-delete="angular-warning"');
+    expect(html).toContain('data-alert-group-no-select-delete-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-enable-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-group-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-enable-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-group-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-group-enable-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-group-enable-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-save-feedback="angular-new-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-save-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-group-save-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-save-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-save-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-group-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-group-save-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-alert-group-save-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-group-save-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-edit-load-feedback="angular-edit-fail"');
+    expect(html).toContain('data-alert-group-edit-load-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-required-validation="angular-required-timers"');
+    expect(html).toContain('data-alert-group-required-validation-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-non-negative-validation="angular-nzmin-timers"');
+    expect(html).toContain('data-alert-group-non-negative-validation-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-alert-group-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-alert-group-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-group-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-alert-group-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-label-save-feedback="angular-new-notify"');
+    expect(html).toContain('data-hz-ui-lab-label-save-feedback="angular-edit-notify"');
+    expect(html).toContain('data-label-save-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-label-save-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-hz-ui-lab-label-save-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-label-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-save-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-label-save-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-label-save-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-label-save-loading="angular-nz-ok-loading"');
+    expect(html).toContain('data-label-save-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-hz-ui-lab-label-save-trim="angular-trim-before-save"');
+    expect(html).toContain('data-label-save-trim-owner="label-manage-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-optional-save-payload="angular-preserve-undefined-optional-fields"');
+    expect(html).toContain('data-label-optional-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-type-save-payload="angular-preserve-implicit-type"');
+    expect(html).toContain('data-label-type-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-display-format="angular-format-label-name-raw-tag-value"');
+    expect(html).toContain('data-label-display-format-owner="label-view-model"');
+    expect(html).toContain('data-hz-ui-lab-label-description-display="angular-truthy-description-raw"');
+    expect(html).toContain('data-label-description-display-owner="label-manage-surface"');
+    expect(html).toContain('data-hz-ui-lab-label-dialog-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-label-dialog-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-label-dialog-width="angular-width-30-percent"');
+    expect(html).toContain('data-label-dialog-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-label-dialog-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-label-dialog-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-label-preview-visibility="angular-name-defined-preview"');
+    expect(html).toContain('data-label-dialog-preview-visibility-owner="route-form-state"');
+    expect(html).toContain('data-hz-ui-lab-label-preview-frame="angular-inline-tag-no-extra-frame"');
+    expect(html).toContain('data-label-dialog-preview-frame-owner="route-form-field-grid"');
+    expect(html).toContain('data-label-dialog-preview-chrome-mode="angular-tag-only-no-extra-frame"');
+    expect(html).toContain('data-label-dialog-preview-chrome-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-hz-ui-lab-label-edit-reference="angular-edit-direct-reference"');
+    expect(html).toContain('data-label-edit-reference-contract="angular-edit-direct-reference"');
+    expect(html).toContain('data-label-edit-reference-owner="route-form-state"');
+    expect(html).toContain('data-hz-ui-lab-label-copy-feedback="angular-copy-notify"');
+    expect(html).toContain('data-hz-ui-lab-label-copy-feedback="angular-copy-fail"');
+    expect(html).toContain('data-label-copy-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-label-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-confirm-state="angular-modal-confirm-state"');
+    expect(html).toContain('data-label-delete-confirm-state-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-confirm-close="angular-close-before-delete-result"');
+    expect(html).toContain('data-label-delete-confirm-close-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-label-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-label-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-label-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-query="angular-repeated-ids-query"');
+    expect(html).toContain('data-label-delete-query-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-label-load-failure-owner="label-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-card-loading="angular-table-loading"');
+    expect(html).toContain('data-label-card-loading-owner="label-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-name-validation="angular-required-before-submit"');
+    expect(html).toContain('data-label-name-validation-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-name-validation-trigger="angular-ok-marks-dirty"');
+    expect(html).toContain('data-label-name-validation-trigger-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-name-validation-raw="angular-required-before-trim"');
+    expect(html).toContain('data-label-name-validation-raw-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-query="angular-load-all-labels"');
+    expect(html).toContain('data-label-query-owner="label-query-state"');
+    expect(html).toContain('data-hz-ui-lab-label-query-param-order="angular-page-index-size-type-search"');
+    expect(html).toContain('data-label-query-param-order-contract="angular-page-index-size-type-search"');
+    expect(html).toContain('data-label-query-param-order-owner="label-query-state"');
+    expect(html).toContain('data-hz-ui-lab-label-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-label-search-submit-owner="cold-search-row"');
+    expect(html).toContain('data-hz-ui-lab-label-card-grid="angular-card-grid"');
+    expect(html).toContain('data-label-card-grid-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-label-card-column-contract="nz-xs-12-sm-8-md-6-lg-4"');
+    expect(html).toContain('data-hz-ui-lab-label-card="angular-card"');
+    expect(html).toContain('data-label-card-shell="angular-card"');
+    expect(html).toContain('data-label-card-size="small"');
+    expect(html).toContain('data-hz-ui-lab-label-monitor-handoff="angular-routerlink-monitors-labels"');
+    expect(html).toContain('data-label-monitor-handoff-owner="next-monitor-query-link"');
+    expect(html).toContain('data-hz-ui-lab-label-color="angular-render-label-color"');
+    expect(html).toContain('data-label-color-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-hz-label-color-token="geekblue"');
+    expect(html).toContain('data-label-card-description-display="angular-truthy-description-raw"');
+    expect(html).toContain('data-label-card-description-display-owner="label-manage-surface"');
+    expect(html).toContain('data-hz-ui-lab-label-row-actions="angular-card-actions-contextual"');
+    expect(html).toContain('data-label-row-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-label-card-actions="angular-card-actions"');
+    expect(html).toContain('data-label-row-action="copy"');
+    expect(html).toContain('data-label-row-action="edit"');
+    expect(html).toContain('data-label-row-action="delete"');
+    expect(html).toContain('data-label-row-action-owner="row-contextual-icon-button"');
+    expect(html).toContain('data-label-row-action-label="team:ops"');
+    expect(html).toContain('Copy label team:ops');
+    expect(html).toContain('Edit label team:ops');
+    expect(html).toContain('Delete label team:ops');
+    expect(html).toContain('data-hz-ui-lab-plugin-enable-feedback="angular-edit-notify"');
+    expect(html).toContain('data-plugin-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-enable-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-plugin-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-enable-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-plugin-enable-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-plugin-enable-optimistic="angular-toggle-mutates-row-before-put"');
+    expect(html).toContain('data-plugin-enable-optimistic-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-table-loading="angular-load-plugins-table"');
+    expect(html).toContain('data-plugin-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-plugin-toggle-loading-owner="angular-nz-table-loading"');
+    expect(html).toContain('data-hz-ui-lab-plugin-selection-reset="angular-load-clears-selection"');
+    expect(html).toContain('data-plugin-selection-reset-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-toolbar-delete-menu="angular-toolbar-ellipsis-delete"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-open="false"');
+    expect(html).toContain('aria-expanded="false"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-clearance-panel="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-delete-selected-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-ui-lab-plugin-row-actions="angular-row-actions-contextual"');
+    expect(html).toContain('data-plugin-row-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-ui-lab-plugin-row-delete-menu="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-plugin-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-row-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-plugin-row-delete-menu-open="false"');
+    expect(html).toContain('data-plugin-row-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-row-delete-menu-clearance-panel="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-row-delete-menu-panel-open="false"');
+    expect(html).toContain('data-plugin-delete-one-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-row-action="params"');
+    expect(html).toContain('data-plugin-row-action="delete"');
+    expect(html).toContain('data-plugin-row-action-owner="row-contextual-icon-button"');
+    expect(html).toContain('data-plugin-row-action-label="smtp"');
+    expect(html).toContain('Edit plugin params smtp');
+    expect(html).toContain('More plugin row actions');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-warning="angular-no-select-warning"');
+    expect(html).toContain('data-plugin-delete-warning-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-plugin-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-plugin-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-plugin-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-plugin-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-query="angular-repeated-ids-query"');
+    expect(html).toContain('data-plugin-delete-query-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-clamp="angular-update-page-index"');
+    expect(html).toContain('data-plugin-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-table-columns="angular-five-column-edit-actions"');
+    expect(html).toContain('data-plugin-table-columns-owner="angular-nz-table"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-action-visibility="angular-paramcount-zero-only"');
+    expect(html).toContain('data-plugin-param-action-visibility-owner="plugin-view-model"');
+    expect(html).toContain('data-hz-ui-lab-plugin-table-stable-height="viewport-fill-on-empty-or-short"');
+    expect(html).toContain('data-plugin-table-stable-height-owner="route-layout-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-plugin-load-failure-owner="plugin-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-validation="angular-required-before-submit"');
+    expect(html).toContain('data-plugin-upload-validation-owner="route-validation-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-invalid-submit="angular-mark-dirty-keep-open"');
+    expect(html).toContain('data-plugin-upload-invalid-submit-owner="route-validation-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-cancel="angular-cancel-preserves-form"');
+    expect(html).toContain('data-plugin-upload-cancel-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-cancel-pending="angular-cancel-allowed-during-ok-loading"');
+    expect(html).toContain('data-plugin-upload-cancel-pending-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-file-remove="angular-nz-remove-clears-jar"');
+    expect(html).toContain('data-plugin-upload-file-remove-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-plugin-upload-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-width="angular-width-30-percent"');
+    expect(html).toContain('data-plugin-upload-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-field-layout="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-field-layout-contract="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-plugin-upload-field-layout="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-label-span="8"');
+    expect(html).toContain('data-plugin-upload-control-span="14"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-status-control="angular-nz-switch"');
+    expect(html).toContain('data-plugin-upload-status-control-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-plugin-upload-status-control="angular-nz-switch"');
+    expect(html).toContain('data-hz-ui="switch"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-payload="angular-form-data"');
+    expect(html).toContain('data-plugin-upload-payload-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-name-payload="angular-raw-name-no-trim"');
+    expect(html).toContain('data-plugin-upload-name-payload-owner="plugin-manage-controller"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-save-lifecycle="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-upload-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-success-reset="angular-reset-form-after-success"');
+    expect(html).toContain('data-plugin-upload-success-reset-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-loading="angular-nz-ok-loading"');
+    expect(html).toContain('data-plugin-upload-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-feedback="angular-new-notify"');
+    expect(html).toContain('data-plugin-upload-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-plugin-upload-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-plugin-upload-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-load="angular-params-define-modal"');
+    expect(html).toContain('data-plugin-param-form-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-empty="angular-empty-params-modal"');
+    expect(html).toContain('data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-width="angular-default-modal-width"');
+    expect(html).toContain('data-plugin-param-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-field-layout="angular-label-7-control-8"');
+    expect(html).toContain('data-plugin-param-field-layout-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-advanced-fields="angular-configurable-and-multi-func"');
+    expect(html).toContain('data-plugin-param-advanced-fields-owner="hertzbeat-ui-advanced-param-controls"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-radio="angular-nz-radio-group"');
+    expect(html).toContain('data-plugin-param-radio-owner="hertzbeat-ui-radio-button-group"');
+    expect(html).toContain('data-plugin-param-radio="angular-nz-radio-group"');
+    expect(html).toContain('data-plugin-param-radio-field="authType"');
+    expect(html).toContain('data-hz-ui="radio-button-group"');
+    expect(html).toContain('data-hz-radio-button-option="basic"');
+    expect(html).toContain('data-hz-radio-button-checked="true"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-textarea="angular-textarea-rows-8"');
+    expect(html).toContain('data-plugin-param-textarea-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-plugin-param-textarea="angular-textarea-rows-8"');
+    expect(html).toContain('data-plugin-param-textarea-rows="8"');
+    expect(html).toContain('rows="8"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-key-value="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-configurable-field="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-key-value-editor="headers"');
+    expect(html).toContain('data-plugin-param-key-value-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-hz-ui="key-value-editor"');
+    expect(html).toContain('data-plugin-param-key-value-input="key"');
+    expect(html).toContain('data-plugin-param-key-value-input="value"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-labels="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-labels-editor="labels"');
+    expect(html).toContain('data-plugin-param-labels-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-plugin-param-labels-input="key"');
+    expect(html).toContain('data-plugin-param-labels-input="value"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-plugin-param-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-plugin-param-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-pending-editable="angular-controls-remain-enabled"');
+    expect(html).toContain('data-plugin-param-pending-editable="angular-controls-remain-enabled"');
+    expect(html).toContain('data-plugin-param-pending-editable-owner="angular-param-modal"');
+    expect(html).toContain('data-cold-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-cold-label-selector-record-row="service:checkout"');
+    expect(html).toContain('data-cold-label-selector-key-input="searchable-key"');
+    expect(html).toContain('data-cold-label-selector-value-input="searchable-value"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-metrics-field="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-metrics-field-editor="metrics"');
+    expect(html).toContain('data-plugin-param-metrics-field-owner="hertzbeat-ui-configurable-field-editor"');
+    expect(html).toContain('data-hz-ui="configurable-field-editor"');
+    expect(html).toContain('data-hz-configurable-field-input="field"');
+    expect(html).toContain('data-hz-configurable-field-input="unit"');
+    expect(html).toContain('data-hz-configurable-field-input="type"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="field"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="unit"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="type"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-multi-func-input="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-multi-func-input="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-multi-func-type="array"');
+    expect(html).toContain('data-plugin-param-array="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-array-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-param-multi-func-clear="angular-allow-clear"');
+    expect(html).toContain('data-plugin-param-multi-func-clear-owner="hertzbeat-ui-input-affordance"');
+    expect(html).toContain('data-hz-ui-lab-plugin-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-plugin-search-submit-contract="angular-enter-and-clear"');
+    expect(html).toContain('data-plugin-search-clear-contract="angular-cleared-load"');
+    expect(html).toContain('data-plugin-search-clear-owner="shared-search-row"');
+    expect(html).toContain('data-hz-ui-lab-collector-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-collector-search-submit-contract="angular-enter-and-clear"');
+    expect(html).toContain('data-collector-search-clear-contract="angular-cleared-load"');
+    expect(html).toContain('data-collector-search-clear-owner="shared-search-row"');
+    expect(html).toContain('data-hz-ui-lab-plugin-query-param-order="angular-page-index-size-search"');
+    expect(html).toContain('data-plugin-query-param-order-contract="angular-page-index-size-search"');
+    expect(html).toContain('data-plugin-query-param-order-owner="plugin-query-state"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-plugin-param-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-save="angular-params-post"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-payload="angular-object-values-payload"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-save-lifecycle="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-param-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-param-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-plugin-param-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-param-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-plugin-param-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-save-loading="angular-no-ok-loading"');
+    expect(html).toContain('data-plugin-param-save-loading-owner="angular-modal-ok-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-file-list="angular-before-upload-single-replace"');
+    expect(html).toContain('data-plugin-upload-file-list-contract="angular-before-upload-single-replace"');
+    expect(html).toContain('data-plugin-upload-file-list-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-plugin-upload-file-remove="angular-nz-remove-clears-jar"');
+    expect(html).toContain('data-plugin-upload-file-remove-state="selected"');
+    expect(html).toContain('data-hz-ui-lab-plugin-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-plugin-page-jump="shared"');
+    expect(html).toContain('data-plugin-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-warning="angular-no-select-warning"');
+    expect(html).toContain('data-collector-delete-warning-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-collector-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-collector-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-collector-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-collector-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-clamp="angular-update-page-index"');
+    expect(html).toContain('data-collector-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-collector-selection-reset="angular-load-clears-selection"');
+    expect(html).toContain('data-collector-selection-reset-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-collector-operate-warning="angular-no-select-online-offline"');
+    expect(html).toContain('data-collector-operate-warning-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-select-all-contract="angular-header-includes-disabled-default"');
+    expect(html).toContain('data-collector-select-all-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-row-delete-menu="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-collector-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-collector-row-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-collector-row-delete-menu-open="false"');
+    expect(html).toContain('data-collector-row-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(html).toContain('data-collector-row-delete-menu-clearance-panel="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-collector-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-collector-row-delete-menu-panel-open="false"');
+    expect(html).toContain('data-collector-delete-one-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-ui-lab-collector-operate-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-collector-operate-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-collector-operate-feedback="angular-operate-notify"');
+    expect(html).toContain('data-collector-operate-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-operate-failure="angular-operate-fail-notification"');
+    expect(html).toContain('data-collector-operate-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-operate-feedback-title="common.notify.operate-fail"');
+    expect(html).toContain('data-collector-operate-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-collector-mutation-loading="angular-nz-table-loading"');
+    expect(html).toContain('data-collector-table-loading-owner="angular-nz-table-loading"');
+    expect(html).toContain('data-hz-ui-lab-collector-table-loading="angular-load-collectors-table"');
+    expect(html).toContain('data-collector-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-hz-ui-lab-collector-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-collector-load-failure-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-copy-feedback="angular-copy-success-duration-800"');
+    expect(html).toContain('data-collector-deploy-copy-feedback-duration-ms="800"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-identity="angular-generate-identity"');
+    expect(html).toContain('data-collector-deploy-identity-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-failure="angular-apply-fail-notification"');
+    expect(html).toContain('data-collector-deploy-feedback-title="common.notify.apply-fail"');
+    expect(html).toContain('data-collector-deploy-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-validation="angular-submit-marks-required"');
+    expect(html).toContain('data-collector-deploy-validation-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-validation-raw="angular-required-before-trim"');
+    expect(html).toContain('data-collector-deploy-validation-raw-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-loading="angular-nz-ok-loading"');
+    expect(html).toContain('data-collector-deploy-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-collector-deploy-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-close-reset="angular-close-clears-name"');
+    expect(html).toContain('data-collector-deploy-close-reset-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-close-pending-result="angular-close-ignores-late-generate"');
+    expect(html).toContain('data-collector-deploy-close-pending-result-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-width="angular-width-45-percent"');
+    expect(html).toContain('data-collector-deploy-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-name-lock="angular-disable-after-identity"');
+    expect(html).toContain('data-collector-deploy-field-layout-contract="angular-label-7-control-12"');
+    expect(html).toContain('data-collector-deploy-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-collector-deploy-name-lock-owner="angular-ngmodel-input"');
+    expect(html).toContain('data-collector-deploy-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-collector-deploy-label-span="7"');
+    expect(html).toContain('data-collector-deploy-control-span="12"');
+    expect(html).toContain('data-collector-deploy-name-lock="angular-disable-after-identity"');
+    expect(html).toContain('data-collector-deploy-name-disabled="true"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-result-close="angular-danger-close-after-identity"');
+    expect(html).toContain('data-collector-deploy-result-close-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-collector-deploy-result-close-reset="angular-close-clears-name"');
+    expect(html).toContain('data-collector-deploy-result-close-reset-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-docker-shell="angular-docker-shell"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-package-shell="angular-package-shell"');
+    expect(html).toContain('data-collector-deploy-code-owner="hertzbeat-ui-code-editor"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-package-link="angular-github-releases-link"');
+    expect(html).toContain('data-collector-deploy-package-link-owner="angular-nz-modal"');
+    expect(html).toContain('href="https://github.com/apache/hertzbeat/releases"');
+    expect(html).toContain('data-hz-ui-lab-collector-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-collector-page-jump="shared"');
+    expect(html).toContain('data-collector-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-collector-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-page-jump="shared"');
+    expect(html).toContain('data-alert-center-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-alert-center-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-page-jump="shared"');
+    expect(html).toContain('data-alert-inhibit-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-alert-inhibit-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-no-select-delete="angular-warning"');
+    expect(html).toContain('data-alert-inhibit-no-select-delete-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-alert-inhibit-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-alert-inhibit-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-inhibit-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-alert-inhibit-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-enable-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-inhibit-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-enable-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-inhibit-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-inhibit-enable-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-inhibit-enable-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-save-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-save-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-inhibit-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-inhibit-save-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-alert-inhibit-save-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-inhibit-save-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-match-mode="angular-entity-noise-controls"');
+    expect(html).toContain('data-alert-inhibit-match-mode-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-created-outside-matched="angular-authoring-notice"');
+    expect(html).toContain('data-alert-inhibit-created-outside-matched-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-entity-prefill="angular-alert-common-labels"');
+    expect(html).toContain('data-alert-inhibit-entity-prefill-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-page-jump="shared"');
+    expect(html).toContain('data-alert-silence-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-alert-silence-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-no-select-delete="angular-warning"');
+    expect(html).toContain('data-alert-silence-no-select-delete-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-alert-silence-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-alert-silence-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-silence-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-alert-silence-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-enable-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-silence-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-enable-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-silence-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-silence-enable-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-silence-enable-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-save-feedback="angular-new-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-save-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-silence-save-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-save-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-save-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-silence-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-silence-save-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-alert-silence-save-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-silence-save-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-edit-load-feedback="angular-edit-fail"');
+    expect(html).toContain('data-alert-silence-edit-load-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-required-validation="angular-name-labels-only"');
+    expect(html).toContain('data-alert-silence-required-validation-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-match-mode="angular-entity-noise-controls"');
+    expect(html).toContain('data-alert-silence-match-mode-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-created-outside-matched="angular-authoring-notice"');
+    expect(html).toContain('data-alert-silence-created-outside-matched-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-entity-prefill="angular-alert-common-labels"');
+    expect(html).toContain('data-alert-silence-entity-prefill-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-page-jump="shared"');
+    expect(html).toContain('data-alert-notice-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-alert-notice-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-token-normalizer="angular-on-change"');
+    expect(html).toContain('data-alert-notice-receiver-token-normalizer-owner="route-form-field"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-query="backend-paginated"');
+    expect(html).toContain('data-alert-notice-template-query-owner="route-query-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-display-names="angular-save-payload"');
+    expect(html).toContain('data-alert-notice-rule-display-names-owner="route-payload-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-edit-display-names="angular-detail-options"');
+    expect(html).toContain('data-alert-notice-rule-edit-display-names-owner="route-payload-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-edit-option-seeding="angular-detail-options"');
+    expect(html).toContain('data-alert-notice-rule-edit-option-seeding-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-edit-detail="angular-detail-fetch"');
+    expect(html).toContain('data-alert-notice-rule-edit-detail-owner="route-detail-fetch-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-save-loading="angular-nz-ok-loading"');
+    expect(html).toContain('data-alert-notice-rule-save-loading-owner="route-modal-ok-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-save-failure="angular-notify-title-detail"');
+    expect(html).toContain('data-alert-notice-rule-save-failure-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-alert-notice-rule-save-failure-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-notice-rule-save-failure-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-template-display="angular-template-id-fallback"');
+    expect(html).toContain('data-alert-notice-rule-template-display-owner="route-table-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-receiver-display="angular-array-interpolation"');
+    expect(html).toContain('data-alert-notice-rule-receiver-display-owner="route-table-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-alert-notice-rule-search-submit-owner="cold-search-row"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-alert-notice-receiver-search-submit-owner="cold-search-row"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-alert-notice-template-search-submit-owner="cold-search-row"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-sync="angular-load-table"');
+    expect(html).toContain('data-alert-notice-receiver-sync-owner="route-refresh-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-sync="angular-load-table"');
+    expect(html).toContain('data-alert-notice-rule-sync-owner="route-refresh-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-sync="angular-load-table"');
+    expect(html).toContain('data-alert-notice-template-sync-owner="route-refresh-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-single-switch-frame="none"');
+    expect(html).toContain('data-alert-notice-rule-single-switch-frame-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-period-default-days="angular-all-days"');
+    expect(html).toContain('data-alert-notice-rule-period-default-days-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-period-limit-state="angular-independent-isLimit"');
+    expect(html).toContain('data-alert-notice-rule-period-limit-state-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-time-default="angular-empty-new-rule"');
+    expect(html).toContain('data-alert-notice-rule-time-default-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-optional-period-time="angular-form-validity"');
+    expect(html).toContain('data-alert-notice-rule-optional-period-time-owner="route-validation-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-template-type-filter="angular-selected-receiver-type"');
+    expect(html).toContain('data-alert-notice-rule-template-type-filter-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-template-active-type="angular-switch-receiver"');
+    expect(html).toContain('data-alert-notice-rule-template-active-type-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-single-switch-visual="unframed-switch"');
+    expect(html).toContain('data-alert-notice-rule-switch="filter-all"');
+    expect(html).toContain('转发所有');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-table-switch-update="angular-edit-notify"');
+    expect(html).toContain('data-alert-notice-rule-table-switch-update-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-test-validation="angular-backend-owned"');
+    expect(html).toContain('data-alert-notice-receiver-test-validation-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-default-type="angular-email"');
+    expect(html).toContain('data-alert-notice-receiver-default-type-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-alert-notice-delete-feedback-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-alert-notice-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-save-feedback="angular-new-edit-notify"');
+    expect(html).toContain('data-alert-notice-save-feedback-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-success-next="angular-policy-next"');
+    expect(html).toContain('data-alert-notice-receiver-success-next-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-save-failure-close="angular-transport-error-close"');
+    expect(html).toContain('data-alert-notice-receiver-save-failure-close-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-save-failure="angular-notify-title-detail"');
+    expect(html).toContain('data-alert-notice-receiver-save-failure-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-alert-notice-receiver-save-failure-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-notice-receiver-save-failure-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-edit-load-feedback="angular-edit-fail"');
+    expect(html).toContain('data-alert-notice-edit-load-feedback-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-edit-detail="angular-detail-fetch"');
+    expect(html).toContain('data-alert-notice-receiver-edit-detail-owner="route-detail-fetch-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-edit-detail="angular-detail-fetch"');
+    expect(html).toContain('data-alert-notice-template-edit-detail-owner="route-detail-fetch-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-save-failure="angular-notify-title-detail"');
+    expect(html).toContain('data-alert-notice-template-save-failure-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-alert-notice-template-save-failure-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-notice-template-save-failure-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-viewer-return="angular-cancel-return"');
+    expect(html).toContain('data-alert-notice-template-viewer-return-owner="route-modal-footer-contract"');
+    expect(html).toContain('data-alert-notice-template-viewer-ok="none"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-type-required="angular-required-select"');
+    expect(html).toContain('data-alert-notice-template-type-required-owner="route-validation-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-telegram-label="angular-template-telegram"');
+    expect(html).toContain('data-alert-notice-template-telegram-label-owner="route-i18n-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-code-editor="shared"');
+    expect(html).toContain('data-alert-notice-template-code-editor-owner="hertzbeat-ui-code-editor"');
+    expect(html).toContain('data-alert-notice-template-viewer-code-editor="readonly-code-editor"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-delete-clamp="angular-update-page-index"');
+    expect(html).toContain('data-alert-notice-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-hz-pagination-page-jump="number-input"');
+    expect(html).toContain('data-hz-monitor-filter-field="type-picker"');
+    expect(html).toContain('All status');
+    expect(html).toContain('Time range');
+    expect(html).toContain('Auto refresh');
+    expect(html).toContain('Visible columns');
+    expect(html).toContain('data-hz-ui="time-distribution-chart"');
+    expect(html).toContain('data-hz-ui="time-series-chart"');
+    expect(html).toContain('data-hz-ui="status-timeline"');
+    expect(html).toContain('Signal volume');
+    expect(html).toContain('data-hz-ui="chart-time-window-handoff"');
+    expect(html).toContain('Chart window');
+    expect(html).toContain('data-hz-chart-window-source="signal-volume"');
+    expect(html).toContain('data-hz-bucket-selected="true"');
+    expect(html).toContain('data-hz-chart-crosshair="p95:14:15"');
+    expect(html).toContain('data-hz-chart-legend="metrics"');
+    expect(html).toContain('aria-label="Toggle segment alerts"');
+    expect(html).toContain('data-hz-chart-legend="p95"');
+    expect(html).toContain('aria-label="Toggle series p95"');
+    expect(html).toContain('Collector latency');
+    expect(html).toContain('Availability timeline');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-chart="shared"');
+    expect(html).toContain('data-hz-ui="metric-time-series-panel"');
+    expect(html).toContain('data-hz-chart-kind="metric-time-series-echarts"');
+    expect(html).toContain('data-hz-chart-runtime="echarts"');
+    expect(html).toContain('data-hz-echarts-datazoom-feedback="change-callback"');
+    expect(html).toContain('data-hz-echarts-datazoom-interaction="native-live-drag"');
+    expect(html).toContain('Monitor history · responseTime');
+    expect(html).toContain('data-monitor-history-zoom-apply="local-to-query-time"');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-time-toolbar="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-refresh-contract="angular-first-page-reload"');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-selection-reset="angular-chart-reload"');
+    expect(html).toContain('data-hz-ui-lab-time-foundation="shared-platform-time-context"');
+    expect(html).toContain('data-hz-time-range-toolbar-owner="hertzbeat-ui-time-range-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-density="operator-single-row-tight"');
+    expect(html).toContain('data-hz-time-range-toolbar-control-height="28"');
+    expect(html).toContain('data-hz-time-range-toolbar-action-mode="icon-text"');
+    expect(html).toContain('data-hz-time-range-toolbar-action="refresh"');
+    expect(html).toContain('data-hz-time-range-toolbar-action="reset"');
+    expect(html).toContain('data-hz-time-range-toolbar-action="apply"');
+    expect(html).toContain('Refresh now');
+    expect(html).toContain('Reset');
+    expect(html).toContain('Apply');
+    expect(html).toContain('data-hz-ui-lab-time-foundation-scope="monitor-metrics-logs-traces-alerts-otlp"');
+    expect(html).toContain('data-hz-ui-lab-time-foundation-models="quick-relative-absolute-recent-custom-validation"');
+    expect(html).toContain('data-hz-ui-lab-time-foundation-url-contract="from-to-refresh-live-timezone"');
+    expect(html).toContain('data-hz-ui-lab-time-foundation-datazoom="preview-apply-reset"');
+    expect(html).toContain('data-hz-ui-lab-time-foundation-datazoom-live="echarts-native"');
+    expect(html).toContain('data-hz-ui-lab-time-foundation-readiness="platform-grade-monitor-history"');
+    expect(html).toContain('data-hz-ui-lab-time-foundation-checks="single-line-dark-quick-relative-absolute-manual-timezone-refresh-recent-custom-validation-datazoom"');
+    expect(html).toContain('data-hz-expression-calendar-layer="day"');
+    expect(html).toContain('data-hz-expression-calendar-layer-action="month"');
+    expect(html).toContain('data-hz-expression-time-input-mode="manual-stepper"');
+    expect(html).toContain('data-hz-expression-time-stepper-owner="hertzbeat-ui-time-foundation-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-state="preview"');
+    expect(html).toContain('data-hz-time-range-toolbar-preview-source="chart-datazoom"');
+    expect(html).toContain('data-hz-time-range-toolbar-refresh-mode="auto"');
+    expect(html).toContain('data-hz-time-range-toolbar-refresh-interval="30"');
+    expect(html).toContain('data-hz-time-range-toolbar-live-state="running"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-mode="named"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-value="Asia/Shanghai"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-route-key="timezone"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-control="shared-timezone-select"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-owner="hertzbeat-ui-time-range-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-options="local-named"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-option="local"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-option-route-key="local"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-option="Asia/Shanghai"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-option-route-key="timezone"');
+    expect(html).not.toContain('data-hz-time-range-toolbar-refresh-interval="30s"');
+    expect(html).toContain('data-hz-ui-lab-time-foundation-datazoom-handoff="shared"');
+    expect(html).toContain('data-hz-ui="time-range-preview-handoff"');
+    expect(html).toContain('data-hz-time-range-preview-handoff-state="preview"');
+    expect(html).toContain('data-hz-time-range-preview-handoff-action="simulate"');
+    expect(html).toContain('data-hz-time-range-preview-handoff-action="apply"');
+    expect(html).toContain('data-hz-time-range-preview-handoff-action="reset"');
+    expect(html).toContain('data-monitor-history-time-toolbar-owner="hertzbeat-ui-time-range-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-layout="compact-lined-controls"');
+    expect(html).toContain('data-hz-time-range-toolbar-time-entry="single-expression-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-preset-placement="picker-panel"');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute="picker-panel"');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute-layout="single-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute-input-mode="manual-text-with-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-model="expression-from-to"');
+    expect(html).toContain('data-hz-expression-time-range-expression-mode="from-to"');
+    expect(html).toContain('now-1h - now');
+    expect(html).toContain('data-hz-time-range-toolbar-rail-layout="nowrap"');
+    expect(html).toContain('data-hz-ui="expression-time-range-picker"');
+    expect(html).toContain('data-hz-expression-time-range-picker-layout="expression-single-range"');
+    expect(html).toContain('data-hz-expression-time-range-trigger="single-range"');
+    expect(html).toContain('data-hz-expression-time-range-picker-panel-width="560"');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute-control="expression-time-range-picker"');
+    expect(html).toContain('data-hz-expression-time-range-picker-panel-tone="graphite-black"');
+    expect(html).toContain('data-hz-expression-time-range-panel-structure="quick-absolute-recent-custom"');
+    expect(html).toContain('data-hz-expression-time-range-picker-panel-height="bounded-520"');
+    expect(html).toContain('data-hz-expression-time-range-side-rail-scroll="bounded"');
+    expect(html).toContain('data-hz-expression-time-range-relative-entry="manual-from-to-fields"');
+    expect(html).toContain('data-hz-expression-time-range-relative-visibility="merged-with-absolute-fields"');
+    expect(html).toContain('data-hz-expression-time-range-manual-fields-layout="one-line-two-column"');
+    expect(html).toContain('data-hz-expression-time-range-manual-fields-density="compact"');
+    expect(html).toContain('data-hz-expression-time-range-manual-input-width="readable-datetime"');
+    expect(html).toContain('data-hz-expression-time-range-manual-field-height="28"');
+    expect(html).not.toContain('data-hz-expression-time-range-panel-section="relative-expression"');
+    expect(html).toContain('data-hz-expression-time-range-panel-section="recent-ranges"');
+    expect(html).toContain('data-hz-expression-time-range-recent-ranges="persistent"');
+    expect(html).toContain('data-hz-expression-time-range-recent-range-owner="hertzbeat-ui-time-foundation-picker"');
+    expect(html).toContain('data-hz-expression-time-range-recent-range-route-model="expression-from-to"');
+    expect(html).toContain('data-hz-expression-time-range-recent-range-refresh="30"');
+    expect(html).toContain('data-hz-expression-time-range-recent-range-timezone="Asia/Shanghai"');
+    expect(html).toContain('data-hz-expression-time-range-panel-section="custom-range"');
+    expect(html).toContain('data-hz-expression-time-range-custom-range="save-current-range"');
+    expect(html).toContain('data-hz-expression-time-range-custom-ranges="persistent"');
+    expect(html).toContain('data-hz-expression-time-range-custom-name-input="true"');
+    expect(html).toContain('data-hz-expression-time-range-custom-save-owner="hertzbeat-ui-time-foundation-picker"');
+    expect(html).toContain('data-hz-expression-time-range-custom-range-owner="hertzbeat-ui-time-foundation-picker"');
+    expect(html).toContain('data-hz-expression-time-range-custom-range-route-model="expression-from-to"');
+    expect(html).toContain('data-hz-expression-time-range-custom-range-refresh="30"');
+    expect(html).toContain('data-hz-expression-time-range-custom-range-timezone="Asia/Shanghai"');
+    expect(html).toContain('data-hz-expression-time-range-action="clear"');
+    expect(html).toContain('data-hz-expression-time-range-action-owner="hertzbeat-ui-time-foundation-picker"');
+    expect(html).toContain('data-hz-expression-time-range-action="apply"');
+    expect(html).toContain('data-hz-expression-time-range-apply-owner="hertzbeat-ui-time-foundation-picker"');
+    expect(html).toContain('data-hz-expression-time-range-validation-owner="hertzbeat-ui-time-foundation-picker"');
+    expect(html).not.toContain('type="datetime-local"');
+    expect(html).not.toContain('data-monitor-history-time-range-select="true"');
+    expect(html).toContain('data-monitor-history-datazoom-feedback="time-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-family="signal-handoff-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-action-family="signal-handoff-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-select-family="signal-handoff-toolbar"');
+    expect(html).not.toContain('Apply monitor history time range');
+    expect(html).not.toContain('Reset monitor history time range');
+    const historyToolbarStart = html.indexOf('data-hz-ui-lab-monitor-history-time-toolbar="shared"');
+    const historyToolbarEnd = html.indexOf('data-hz-ui-lab-monitor-history-mode-tabs="shared"', historyToolbarStart);
+    const historyToolbarHtml = html.slice(historyToolbarStart, historyToolbarEnd);
+    expect(historyToolbarHtml).not.toContain('data-hz-time-range-toolbar-layout="stacked-operator-controls"');
+    expect(historyToolbarHtml).not.toContain('border-y border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)]');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-mode-tabs="shared"');
+    expect(html).toContain('data-monitor-history-mode-owner="hertzbeat-ui-tabs"');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-controls="shared"');
+    expect(html).toContain('data-hz-action-group-layout="split"');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-action-stack="shared"');
+    expect(html).toContain('data-hz-action-group-layout="stack"');
+    expect(html).toContain('data-hz-ui-lab-monitor-fullscreen-frame="shared"');
+    expect(html).toContain('data-hz-ui="underline-toggle"');
+    expect(html).toContain('data-hz-underline-toggle-owner="hertzbeat-ui-underline-toggle"');
+    expect(html).toContain('data-hz-control-edge="bottom-underline"');
+    expect(html).not.toContain('data-hz-ui="chip-toggle"');
+    expect(html).not.toContain('data-hz-chip-toggle-owner');
+    expect(html).toContain('data-hz-ui-lab-monitor-basic-card="shared"');
+    expect(html).toContain('data-hz-ui="monitor-basic-card"');
+    expect(html).toContain('data-monitor-basic-owner="hertzbeat-ui-basic-card"');
+    expect(html).toContain('data-monitor-basic-edit-action="hertzbeat-ui-icon-action"');
+    expect(html).toContain('data-monitor-basic-stage-surface="hertzbeat-ui-basic-card"');
+    expect(html).toContain('data-monitor-basic-card-chrome="hertzbeat-ui-basic-card"');
+    expect(html).toContain('data-monitor-basic-card-tone="neutral-graphite"');
+    expect(html).toContain('bg-[var(--hz-ui-surface-graphite)]');
+    expect(html).not.toContain('data-monitor-basic-card-tone="blue-raised"');
+    expect(html).not.toContain('data-monitor-basic-card-chrome="angular-card-box"');
+    expect(html).not.toContain('data-monitor-basic-grid-item="angular-first-card"');
+    expect(html).toContain('data-hz-ui="monitor-basic-summary"');
+    expect(html).toContain('data-monitor-basic-summary-owner="hertzbeat-ui-basic-summary"');
+    expect(html).toContain('data-monitor-basic-density="shared-basic-summary"');
+    expect(html).toContain('data-monitor-basic-meta-density="hertzbeat-ui-rows"');
+    expect(html).not.toContain('data-monitor-basic-density="angular-cardless"');
+    expect(html).not.toContain('data-monitor-basic-meta-density="angular-rows"');
+    expect(html).toContain('Monitoring Basic');
+    expect(html).toContain('data-hz-ui-lab-monitor-signal-list="shared"');
+    expect(html).toContain('data-hz-ui="monitor-detail-signal-list"');
+    expect(html).toContain('data-monitor-detail-signal-list-owner="hertzbeat-ui-signal-list"');
+    expect(html).toContain('data-monitor-detail-signal-list-rhythm="shared-tight"');
+    expect(html).toContain('data-monitor-detail-signal-list-geometry="shared-two-column-metric-cards"');
+    expect(html).toContain('data-hz-ui-lab-monitor-realtime-payload-errors="card-local"');
+    expect(html).toContain('data-monitor-detail-realtime-payload-errors="card-local"');
+    expect(html).toContain('data-hz-ui-lab-monitor-realtime-selection-reset="angular-table-reload"');
+    expect(html).toContain('data-hz-ui-lab-monitor-metric-card="shared"');
+    expect(html).toContain('data-hz-ui="monitor-metric-card-grid"');
+    expect(html).toContain('data-hz-ui="monitor-metric-card"');
+    expect(html).toContain('data-hz-ui-lab-monitor-incremental-load="angular-sentinel"');
+    expect(html).toContain('data-hz-ui="monitor-incremental-load-footer"');
+    expect(html).toContain('data-hz-monitor-incremental-owner="hertzbeat-ui-incremental-load-footer"');
+    expect(html).toContain('data-monitor-detail-incremental-mode="angular-sentinel"');
+    expect(html).toContain('data-hz-monitor-incremental-sentinel="true"');
+    expect(html).toContain('data-hz-ui-lab-monitor-realtime-toolbar="shared"');
+    expect(html).toContain('data-hz-ui="monitor-realtime-toolbar"');
+    expect(html).toContain('data-monitor-realtime-toolbar-owner="hertzbeat-ui-realtime-toolbar"');
+    expect(html).toContain('data-monitor-realtime-action-density="hertzbeat-ui-compact-actions"');
+    expect(html).toContain('data-hz-ui-lab-monitor-realtime-inspector="shared"');
+    expect(html).toContain('data-hz-ui="monitor-realtime-inspector"');
+    expect(html).toContain('data-monitor-realtime-inspector-owner="hertzbeat-ui-realtime-inspector"');
+    expect(html).toContain('data-hz-ui-lab-monitor-realtime-row-nav="shared"');
+    expect(html).toContain('data-hz-ui="monitor-realtime-row-navigator"');
+    expect(html).toContain('data-monitor-realtime-row-nav-owner="hertzbeat-ui-row-navigator"');
+    expect(html).not.toContain('data-monitor-realtime-action-density="angular-plain-meta"');
+    expect(html).not.toContain('data-monitor-detail-signal-card-chrome="angular-card-box"');
+    expect(html).not.toContain('data-selected="true"');
+    expect(html).toContain('data-monitor-detail-metric-card-owner="hertzbeat-ui-metric-card"');
+    expect(html).toContain('Realtime metric · basic');
+    expect(html).toContain('data-hz-monitor-metric-table-row="metric-fields"');
+    expect(html).toContain('data-monitor-detail-signal-selected-style="left-rail"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorites="shared"');
+    expect(html).toContain('data-hz-ui="monitor-favorite-surface"');
+    expect(html).toContain('data-monitor-detail-favorite-owner="hertzbeat-ui-favorite-surface"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-feedback="angular-copy"');
+    expect(html).toContain('data-monitor-detail-favorite-feedback-copy="angular-favorite-copy"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-set-semantics="angular-set"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-active-fallback="angular-subselector-sticky"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-history-reload-reset="angular-chart-reload"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-empty-contract="angular-subselector-empty"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-empty-teardown="angular-empty-no-sentinel"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-history-source="favorite-history-chart-payloads"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-history-controls="angular-chart-toolbox"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-realtime-incremental-load="angular-favorite-sentinel"');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-incremental-load="angular-chart-sentinel"');
+    expect(html).toContain('data-hz-ui-lab-monitor-favorite-history-incremental-load="angular-favorite-chart-sentinel"');
+    expect(html).toContain('data-monitor-detail-incremental-mode="angular-chart-sentinel"');
+    expect(html).toContain('data-monitor-detail-incremental-mode="angular-favorite-sentinel"');
+    expect(html).toContain('data-monitor-detail-incremental-mode="angular-favorite-chart-sentinel"');
+    expect(html).toContain('Added to favorites successfully');
+    expect(html).toContain('No history favorites');
+    expect(html).toContain('Favorite scope');
+    expect(html).toContain('Realtime favorites');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-refresh-toolbar="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-scrape-context="angular-nonstatic-scrape-as-app"');
+    expect(html).toContain('data-monitor-detail-scrape-context="angular-nonstatic-scrape-as-app"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-history-catalog-load="angular-tab-click-lazy"');
+    expect(html).toContain('data-monitor-detail-history-catalog-load="angular-tab-click-lazy"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-history-tab-load="angular-click-load-metric-chart"');
+    expect(html).toContain('data-monitor-detail-history-tab-load="angular-click-load-metric-chart"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-realtime-tab-load="angular-click-load-real-time-metric"');
+    expect(html).toContain('data-monitor-detail-realtime-tab-load="angular-click-load-real-time-metric"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-favorite-tab-load="angular-click-load-favorite-metrics"');
+    expect(html).toContain('data-monitor-detail-favorite-tab-load="angular-click-load-favorite-metrics"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-grafana-fallback-tab="angular-previous-data-tab"');
+    expect(html).toContain('data-monitor-detail-grafana-fallback-tab="angular-previous-data-tab"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-default-refresh="angular-deadline-90"');
+    expect(html).toContain('data-hz-ui-lab-monitor-signal-handoff="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-refresh-select="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-refresh-options="angular-config-refresh-copy"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-countdown-reset="angular-deadline-reset"');
+    expect(html).toContain('data-monitor-detail-countdown-reset-contract="angular-deadline-reset"');
+    expect(html).toContain('data-monitor-detail-refresh-options-contract="angular-config-refresh-copy"');
+    expect(html).toContain('data-monitor-detail-refresh-copy-contract="angular-deadline-label"');
+    expect(html).toContain('data-monitor-detail-default-refresh-contract="angular-deadline-90"');
+    expect(html).toContain('data-monitor-detail-refresh-current-option="angular-default-deadline-current"');
+    expect(html).toContain('Set Auto Refresh For 30 s');
+    expect(html).toContain('Close Auto Refresh');
+    expect(html).toContain('Auto Refresh After 90 s');
+    expect(source).toContain("const [refreshIntervalId, setRefreshIntervalId] = React.useState('30s');");
+    expect(source).toContain("const [monitorDetailRefreshIntervalId, setMonitorDetailRefreshIntervalId] = React.useState('90s');");
+    expect(source).toContain('selectedRefresh={monitorDetailRefreshIntervalId}');
+    expect(source).toContain('selectedRefreshIntervalId={refreshIntervalId}');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-tab-extra="angular-refresh-help"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-help-action="angular-docs-help"');
+    expect(html).toContain('data-monitor-detail-tab-extra-contract="angular-refresh-help"');
+    expect(html).toContain('data-monitor-detail-help-action="angular-docs-help"');
+    expect(html).toContain('data-monitor-detail-help-owner="hertzbeat-ui-icon-link"');
+    expect(html).toContain('data-monitor-detail-help-target="https://hertzbeat.apache.org/docs/help/http"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-grafana-refresh-target="previous-data-tab"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-grafana-action-contract="angular-edit-config-delete-dashboard"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-grafana-actions="shared"');
+    expect(html).toContain('data-monitor-detail-grafana-actions-contract="angular-dashboard-actions"');
+    expect(html).toContain('data-monitor-detail-grafana-config-action="monitor-edit-grafana"');
+    expect(html).toContain('data-monitor-detail-grafana-delete-action="delete-dashboard"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-grafana-delete-teardown="angular-hide-tab"');
+    expect(html).toContain('data-hz-ui="monitor-refresh-toolbar"');
+    expect(html).toContain('data-hz-monitor-refresh-toolbar-layout="single-row-compact"');
+    expect(html).toContain('data-monitor-refresh-control-baseline="control-32-lined"');
+    expect(html).toContain('data-hz-control-height="32"');
+    expect(html).toContain('data-hz-control-edge="lined"');
+    expect(html).not.toContain('data-monitor-refresh-action-density="bordered"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-tabs="shared"');
+    expect(html).toContain('data-hz-ui="monitor-detail-tabs"');
+    expect(html).toContain('data-monitor-detail-tabs-owner="hertzbeat-ui-monitor-detail-tabs"');
+    expect(html).toContain('data-monitor-detail-tabs-variant="bottom-underline-switch"');
+    expect(html).toContain('data-monitor-detail-tabs-family="top-tab-underline"');
+    expect(html).toContain('data-monitor-detail-tab-underline="true"');
+    expect(html).toContain('data-hz-control-edge="bottom-underline"');
+    expect(html).not.toContain('data-monitor-detail-tabs-variant="compact-toolbar-buttons"');
+    expect(html).not.toContain('data-monitor-detail-tab-toolbar-button="true"');
+    expect(html).not.toContain('data-monitor-detail-tab-accent-card="true"');
+    expect(html).not.toContain('data-monitor-detail-tab-card="true"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-stage="shared"');
+    expect(html).toContain('data-hz-ui="monitor-detail-stage"');
+    expect(html).toContain('data-monitor-detail-stage-owner="hertzbeat-ui-detail-stage"');
+    expect(html).toContain('data-monitor-detail-stage-rhythm="shared-stack"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-tab-sequence="shared"');
+    expect(html).toContain('data-hz-ui="monitor-detail-tab-sequence"');
+    expect(html).toContain('data-monitor-detail-tab-sequence-owner="hertzbeat-ui-detail-tab-sequence"');
+    expect(html).toContain('data-monitor-detail-tab-sequence="shared-tight"');
+    expect(html).not.toContain('monitor-detail-tab-sequence--angular-tight');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-tab-panel="shared"');
+    expect(html).toContain('data-hz-ui="monitor-detail-tab-panel"');
+    expect(html).toContain('data-monitor-console-tab-panel-owner="hertzbeat-ui-detail-tab-panel"');
+    expect(html).toContain('data-monitor-console-tab-panel-rhythm="shared-tight"');
+    expect(html).toContain('data-monitor-tab-body-surface="hertzbeat-ui-detail-tab-panel"');
+    expect(html).not.toContain('data-monitor-console-tab-panel-rhythm="angular-tight"');
+    expect(html).not.toContain('data-monitor-tab-body-surface="angular-tab-content-direct"');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-chart-grid="shared"');
+    expect(html).toContain('data-hz-ui="monitor-history-chart-grid"');
+    expect(html).toContain('data-hz-ui="monitor-history-chart-card"');
+    expect(html).toContain('data-monitor-history-chart-grid-owner="hertzbeat-ui-history-chart-grid"');
+    expect(html).toContain('data-monitor-history-card-chrome="hertzbeat-ui-history-chart-inline"');
+    expect(html).toContain('data-hz-metric-timeseries-variant="inline"');
+    expect(html).toContain('data-hz-chart-surface-variant="inline"');
+    expect(html).toContain('data-monitor-history-chart-treatment="collector-latency-inline"');
+    expect(html).not.toContain('angular-chart-cards');
+    expect(html).not.toContain('angular-card-box');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-selectable-rows="shared"');
+    expect(html).toContain('data-hz-ui="selectable-rows"');
+    expect(html).toContain('data-hz-selectable-row-owner="hertzbeat-ui-selectable-rows"');
+    expect(html).toContain('data-hz-selectable-row-style="left-rail"');
+    expect(html).toContain('data-hz-ui-lab-monitor-history-detail-rows="shared"');
+    expect(html).toContain('data-hz-ui="detail-rows"');
+    expect(html).toContain('data-hz-detail-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-hz-detail-rows-style="flat-evidence"');
+    expect(html).toContain('data-hz-ui-lab-monitor-panel-primitives="shared"');
+    expect(html).toContain('data-hz-ui="monitor-stat-grid"');
+    expect(html).toContain('data-hz-ui="monitor-signal-bars"');
+    expect(html).toContain('data-monitor-signal-handoff-owner="hertzbeat-ui-icon-link-group"');
+    expect(html).toContain('data-monitor-signal-handoff-link-owner="hertzbeat-ui-icon-link"');
+    expect(html).toContain('data-hz-ui-lab-monitor-inline-context="shared"');
+    expect(html).toContain('data-monitor-detail-context-mark-owner="hertzbeat-ui-inline-context-mark"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-list-return="angular-app-filter"');
+    expect(html).toContain('data-monitor-detail-list-return="angular-app-filter"');
+    expect(html).toContain('data-monitor-detail-list-return-target="/monitors?app=mysql"');
+    expect(html).toContain('data-hz-ui-lab-monitor-app-definition-link="angular-monitor-app"');
+    expect(html).toContain('data-monitor-detail-app-definition-link="angular-monitor-app"');
+    expect(html).toContain('data-monitor-detail-app-definition-target="/setting/define?app=mysql"');
+    expect(html).toContain('data-hz-ui-lab-monitor-breadcrumb="shared"');
+    expect(html).toContain('data-hz-monitor-breadcrumb-owner="hertzbeat-ui-monitor-breadcrumb"');
+    expect(html).toContain('data-hz-ui="inline-context-mark"');
+    expect(html).not.toContain('data-hz-ui="breadcrumb-chip"');
+    expect(html).toContain('data-monitor-refresh-toolbar-owner="hertzbeat-ui-refresh-toolbar"');
+    expect(html).toContain('data-monitor-refresh-badge-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-monitor-refresh-action-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-monitor-refresh-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-form="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-default-app="website"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-app-source="angular-route-context-hidden-field"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-static-host-position="angular-before-name"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-field-order="angular-monitor-form-sequence"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-detect-payload="angular-monitor-collector-params-no-grafana"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-save-payload="angular-monitor-collector-params-grafana"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-payload-param-merge="angular-params-advanced-sdparams"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-payload-host-instance="angular-host-param-as-instance"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-service-discovery-params="angular-nonstatic-only"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-scrape-reload="angular-reset-on-user-scrape-change"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-ssl-port-notice="angular-info-notification"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-advanced-visible="angular-visible-param-only"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-collector-selection="angular-collectors-selection-tags"');
+    expect(html).toContain('data-monitor-editor-collector-selection="angular-collectors-selection-tags"');
+    expect(html).toContain('data-monitor-editor-collector-selection-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-monitor-editor-collector-tags="angular-status-ip-mode-tags"');
+    expect(html).toContain('data-monitor-editor-collector-tag="ip"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-interval-stepper="angular-min-step-max-by-app"');
+    expect(html).toContain('data-monitor-interval-stepper-contract="angular-min-step-max-by-app"');
+    expect(html).toContain('data-monitor-interval-stepper-min="10"');
+    expect(html).toContain('data-monitor-interval-stepper-max="604800"');
+    expect(html).toContain('data-monitor-interval-stepper-step="10"');
+    expect(html).toContain('data-monitor-interval-stepper-unit="common.time.unit.second"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-detect-cron-validation="angular-detect-skips-cron-format"');
+    expect(html).toContain('data-monitor-editor-detect-cron-validation="angular-detect-skips-cron-format"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-cron-required="angular-required-before-detect-save"');
+    expect(html).toContain('data-monitor-editor-cron-required="angular-required-before-detect-save"');
+    expect(html).toContain('data-monitor-editor-input="cron-expression"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-advanced-collapse-demo="angular-ghost-collapse-dashed-trigger"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-advanced-visible-demo="angular-visible-param-only"');
+    expect(html).toContain('data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"');
+    expect(html).toContain('data-monitor-editor-advanced-toggle="angular-dashed-collapse-trigger"');
+    expect(html).toContain('data-monitor-editor-advanced-collapse-state="collapsed"');
+    expect(html).toContain('data-hz-ui-lab-monitor-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-monitor-editor-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-monitor-editor-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-cold-label-selector-record-row="team:platform"');
+    expect(html).toContain('data-cold-label-selector-key-input="searchable-key"');
+    expect(html).toContain('data-monitor-editor-scrape-param-order="angular-after-scrape-before-name"');
+    expect(html).toContain('data-monitor-editor-scrape-reload-contract="angular-reset-on-user-scrape-change"');
+    expect(html).toContain('data-monitor-editor-static-host-field="angular-before-name"');
+    expect(html).toContain('data-monitor-editor-runtime-order="angular-after-advanced-before-labels"');
+    expect(html).toContain('data-hz-ui="monitor-editor-form"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-header="shared"');
+    expect(html).toContain('data-hz-ui="monitor-editor-header"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-action-bar="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-cancel-return="legacy-list-root"');
+    expect(html).toContain('data-monitor-editor-save-return="angular-app-list"');
+    expect(html).toContain('data-monitor-editor-save-return-target="/monitors?app=website"');
+    expect(html).toContain('data-monitor-editor-save-notification-contract="angular-success-before-return"');
+    expect(html).toContain('data-monitor-editor-cancel-return-target="/monitors"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-busy-feedback="angular-spin-tip"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-busy-contract="angular-spin-tip"');
+    expect(html).toContain('data-monitor-editor-feedback="busy"');
+    expect(html).toContain('data-monitor-editor-busy-contract="angular-spin-tip"');
+    expect(html).toContain('data-monitor-editor-busy-phase="detecting"');
+    expect(html).toContain('data-monitor-editor-detect-busy-label="Available Detecting"');
+    expect(html).toContain('data-monitor-editor-submit-busy-label="Loading..."');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-validation-feedback="angular-form-required"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-validation-contract="angular-form-required"');
+    expect(html).toContain('data-monitor-editor-validation-contract="angular-form-required"');
+    expect(html).toContain('data-monitor-editor-blocked-action="save"');
+    expect(html).toContain('data-monitor-editor-blocked-before-api="true"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-api-error-feedback="angular-message-msg"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-api-error-contract="angular-message-msg"');
+    expect(html).toContain('data-monitor-editor-api-error-contract="angular-message-msg"');
+    expect(html).toContain('Backend said the host is unreachable');
+    expect(html).toContain('data-hz-ui="monitor-editor-action-bar"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-section="shared"');
+    expect(html).toContain('data-hz-ui="monitor-editor-section"');
+    expect(html).toContain('Monitor editor section');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-field-grid="shared"');
+    expect(html).toContain('data-hz-ui="monitor-editor-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-monitor-field-controls="shared"');
+    expect(html).toContain('data-hz-ui="field"');
+    expect(html).toContain('data-hz-ui="input"');
+    expect(html).toContain('data-hz-ui="select"');
+    expect(html).toContain('data-hz-ui-lab-monitor-textarea="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-description-textarea-limit="angular-textarea-limit-100"');
+    expect(html).toContain('data-monitor-description-textarea-limit="angular-textarea-limit-100"');
+    expect(html).toContain('data-hz-ui="textarea"');
+    expect(html).toContain('data-hz-ui="textarea-count"');
+    expect(html).toContain('data-hz-textarea-count-max="100"');
+    expect(html).toContain('data-hz-ui-lab-status-incident-message-input="angular-textarea"');
+    expect(html).toContain('data-status-incident-message-input-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-status-incident-message-placeholder-contract="angular-state-placeholder"');
+    expect(html).toContain('data-status-incident-message-placeholder="status.incident.message.tip.2"');
+    expect(html).toContain('data-hz-ui-lab-status-incident-component-picker="angular-checkbox-group"');
+    expect(html).toContain('data-status-incident-component-picker="angular-checkbox-group"');
+    expect(html).toContain('data-status-incident-component-picker-owner="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-status-incident-component-option="42"');
+    expect(html).toContain('data-hz-ui-lab-status-incident-state-control="angular-radio-buttons"');
+    expect(html).toContain('data-status-incident-state-control="angular-radio-buttons"');
+    expect(html).toContain('data-status-incident-state-owner="hertzbeat-ui-radio-button-group"');
+    expect(html).toContain('data-hz-ui="radio-button-group"');
+    expect(html).toContain('data-hz-radio-button-option="2"');
+    expect(html).toContain('data-hz-radio-button-checked="true"');
+    expect(html).toContain('data-hz-ui-lab-monitor-checkbox="shared"');
+    expect(html).toContain('data-hz-ui="checkbox"');
+    expect(html).toContain('data-hz-ui-lab-monitor-number-stepper="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-number-param-bounds="angular-nz-input-number--1000-65535-step-1"');
+    expect(html).toContain('data-monitor-param-number-stepper="port"');
+    expect(html).toContain('data-monitor-param-number-contract="angular-nz-input-number--1000-65535-step-1"');
+    expect(html).toContain('data-monitor-param-number-min="-1000"');
+    expect(html).toContain('data-monitor-param-number-max="65535"');
+    expect(html).toContain('data-monitor-param-number-step="1"');
+    expect(html).toContain('data-hz-ui-lab-monitor-radio-param="angular-nz-radio-group-button-solid"');
+    expect(html).toContain('data-monitor-param-radio="authType"');
+    expect(html).toContain('data-monitor-param-radio-contract="angular-nz-radio-group-button-solid"');
+    expect(html).toContain('data-monitor-editor-radio-owner="hertzbeat-ui-radio-button-group"');
+    expect(html).toContain('data-hz-ui-lab-monitor-labels-param="angular-app-configurable-field-key-value"');
+    expect(html).toContain('data-monitor-param-labels-editor="labels"');
+    expect(html).toContain('data-monitor-param-labels-contract="angular-app-configurable-field-key-value"');
+    expect(html).toContain('data-monitor-editor-labels-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-monitor-param-labels-input="key"');
+    expect(html).toContain('data-monitor-param-labels-input="value"');
+    expect(html).toContain('data-hz-ui-lab-monitor-metrics-field-param="angular-app-configurable-field-field-unit-type"');
+    expect(html).toContain('data-monitor-param-metrics-field-editor="metrics"');
+    expect(html).toContain('data-monitor-param-metrics-field-contract="angular-app-configurable-field-field-unit-type"');
+    expect(html).toContain('data-monitor-editor-metrics-field-owner="hertzbeat-ui-configurable-field-editor"');
+    expect(html).toContain('data-monitor-param-metrics-field-input="field"');
+    expect(html).toContain('data-monitor-param-metrics-field-input="unit"');
+    expect(html).toContain('data-monitor-param-metrics-field-input="type"');
+    expect(html).toContain('data-hz-ui="number-stepper"');
+    expect(html).toContain('data-hz-ui-lab-monitor-key-value-editor="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-key-value-actions="shared"');
+    expect(html).toContain('data-hz-ui="key-value-editor"');
+    expect(html).toContain('data-hz-key-value-action="add"');
+    expect(html).toContain('data-hz-key-value-action-visibility="emphasized"');
+    expect(html).toContain('data-hz-key-value-action-layout="footer-command"');
+    expect(html).toContain('data-hz-ui="heatmap-chart"');
+    expect(html).toContain('data-hz-ui="log-stream"');
+    expect(html).toContain('data-hz-ui="trace-waterfall"');
+    expect(html).toContain('Latency heatmap');
+    expect(html).toContain('Log stream');
+    expect(html).toContain('Trace waterfall');
+    expect(html).toContain('data-hz-ui="service-dependency-graph"');
+    expect(html).toContain('Service dependency');
+    expect(html).toContain('data-hz-service-node="collector"');
+    expect(html).toContain('data-hz-service-edge="collector-&gt;warehouse"');
+    expect(html).toContain('data-hz-ui-lab-action-workbench="shared"');
+    expect(html).toContain('data-hz-ui-lab-actions-placeholder-replacement="api-backed-workbench"');
+    expect(html).toContain('data-actions-legacy-open-context="adapter-boundary-panel"');
+    expect(html).toContain('data-actions-legacy-entity-handoff="/entities"');
+    expect(html).toContain('data-hz-ui="action-workbench"');
+    expect(html).toContain('data-hz-action-workbench-owner="hertzbeat-ui-action-workbench"');
+    expect(html).toContain('data-hz-action-workbench-density="operator-compact"');
+    expect(html).toContain('data-hz-action-workbench-style="cold-matte-hard-edge"');
+    expect(html).toContain('data-actions-adapter-boundary="adapter-pending"');
+    expect(html).toContain('data-actions-suggested-action-confirm="manual-required"');
+    expect(html).toContain('data-actions-catalog="manual-action-catalog-api"');
+    expect(html).toContain('data-actions-catalog-owner="next-actions-catalog-bff"');
+    expect(html).toContain('data-actions-catalog-endpoint="/api/actions/catalog?limit=8"');
+    expect(html).toContain('data-actions-catalog-manager-backed="false"');
+    expect(html).toContain('data-actions-catalog-execution-mode="manual-approval-draft-only"');
+    expect(html).toContain('data-actions-catalog-execution-allowed="false"');
+    expect(html).toContain('data-actions-approval-draft="manual-approval-draft-api"');
+    expect(html).toContain('data-actions-approval-draft-owner="next-actions-approval-draft-bff"');
+    expect(html).toContain('data-actions-approval-draft-execution-mode="manual-approval-draft-only"');
+    expect(html).toContain('data-actions-approval-draft-execution-allowed="false"');
+    expect(html).toContain('data-actions-approval-draft-queue="manual-approval-draft-read-api"');
+    expect(html).toContain('data-actions-approval-draft-queue-owner="next-actions-approval-draft-bff"');
+    expect(html).toContain('data-actions-approval-draft-queue-endpoint="/api/actions/approval-drafts?limit=8"');
+    expect(html).toContain('data-actions-approval-draft-queue-manager-backed="false"');
+    expect(html).toContain('data-actions-approval-draft-queue-execution-mode="manual-approval-draft-only"');
+    expect(html).toContain('data-actions-approval-draft-queue-execution-allowed="false"');
+    expect(html).toContain('data-actions-approval-draft-queue-item-count="1"');
+    expect(html).toContain('data-actions-approval-draft-queue-item="approval-draft-ui-lab-approved"');
+    expect(html).toContain('data-actions-approval-draft-queue-item-state="approval-draft-approved"');
+    expect(html).toContain('data-actions-approval-draft-queue-item-execution-state="not-executed"');
+    expect(html).toContain('data-actions-approval-draft-queue-item-adapter-owner="next-actions-approval-decision-bff"');
+    expect(html).toContain('data-actions-approval-decision="manual-approval-decision-api"');
+    expect(html).toContain('data-actions-approval-decision-owner="next-actions-approval-decision-bff"');
+    expect(html).toContain('data-actions-approval-decision-endpoint="/api/actions/approval-drafts/:draftId/decision"');
+    expect(html).toContain('data-actions-approval-decision-manager-backed="false"');
+    expect(html).toContain('data-actions-approval-decision-execution-mode="manual-approval-draft-only"');
+    expect(html).toContain('data-actions-approval-decision-execution-allowed="false"');
+    expect(html).toContain('Automation actions');
+    expect(html).toContain('Execution adapter pending');
+    expect(html).toContain('data-hz-ui-lab-incident-workbench="shared"');
+    expect(html).toContain('data-hz-ui-lab-incident-query-contract="angular-search-pagination"');
+    expect(html).toContain('data-incidents-query-contract="angular-search-pagination"');
+    expect(html).toContain('data-incidents-query-label="page=0 size=8"');
+    expect(html).toContain('data-hz-ui="incident-workbench"');
+    expect(html).toContain('data-hz-incident-workbench-owner="hertzbeat-ui-incident-workbench"');
+    expect(html).toContain('data-hz-incident-workbench-density="operator-compact"');
+    expect(html).toContain('data-hz-incident-workbench-style="cold-matte-hard-edge"');
+    expect(html).toContain('data-hz-incident-workbench-table="shared"');
+    expect(html).toContain('data-hz-incident-transition-actions="shared"');
+    expect(html).toContain('data-hz-incident-transition-owner="hertzbeat-ui-incident-transition-actions"');
+    expect(html).toContain('data-hz-incident-transition-density="operator-compact"');
+    expect(html).toContain('data-hz-incident-transition-action="identified"');
+    expect(html).toContain('data-hz-incident-transition-action="monitoring"');
+    expect(html).toContain('data-hz-incident-transition-action="resolved"');
+    expect(html).toContain('data-hz-incident-timeline-item="incident-event-1"');
+    expect(html).toContain('data-hz-incident-owner-item="incident-owner-commerce"');
+    expect(html).toContain('Incident workbench');
+    expect(html).toContain('Checkout latency spike');
+    expect(html).toContain('data-hz-ui-lab-status-setting-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-status-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-status-setting-mutation-feedback="angular-notify-keys"');
+    expect(html).toContain('data-status-setting-mutation-feedback-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-status-setting-tab-refresh="angular-nz-tab-click-load"');
+    expect(html).toContain('data-status-tab-refresh-owner="route-refresh-contract"');
+    expect(html).toContain('data-hz-ui-lab-status-incident-refresh="angular-sync-incidence"');
+    expect(html).toContain('data-status-incident-refresh-owner="route-refresh-contract"');
+    expect(html).toContain('data-hz-ui-lab-status-row-delete-menu="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-status-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-ui-lab-status-public-link="angular-tab-toolbar-conditional"');
+    expect(html).toContain('data-status-public-link-owner="status-tab-toolbar"');
+    expect(html).toContain('data-hz-ui-lab-status-incident-pagination="angular-search-pagination"');
+    expect(html).toContain('data-status-incident-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-hz-ui-lab-status-incident-failure-close="angular-cancel-on-api-failure"');
+    expect(html).toContain('data-status-incident-failure-close-owner="status-route-action-feedback"');
+    expect(html).toContain('data-hz-ui-lab-status-incident-history="angular-collapse-desc"');
+    expect(html).toContain('data-status-incident-history-contract="angular-collapse-desc"');
+    expect(html).toContain('data-status-incident-history-owner="hertzbeat-ui-status-incident-history"');
+    expect(html).toContain('data-hz-ui="status-incident-history"');
+    expect(html).toContain('data-hz-ui-lab-public-status-api-contract="angular-public-status"');
+    expect(html).toContain('data-public-status-api-owner="status-center-public-controller"');
+    expect(html).toContain('data-public-status-mode-switch-contract="component-incident"');
+    expect(html).toContain('data-hz-ui-lab-topology-toolbar="shared"');
+    expect(html).toContain('data-hz-ui-lab-topology-toolbar-density="graph-first"');
+    expect(html).toContain('data-hz-ui="topology-toolbar"');
+    expect(html).toContain('data-hz-topology-primitive="toolbar"');
+    expect(html).toContain('data-hz-topology-toolbar-density="graph-first"');
+    expect(html).toContain('data-hz-topology-toolbar-first-viewport-priority="canvas"');
+    expect(html).toContain('data-hz-topology-toolbar-row-contract="single-row-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-row-contract-owner="hertzbeat-ui-toolbar-row-contract"');
+    expect(html).toContain('data-hz-topology-toolbar-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
+    expect(html).toContain('data-hz-topology-toolbar-secondary-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-toolbar-boundary="none"');
+    expect(html).toContain('data-hz-topology-toolbar-boundary-owner="hertzbeat-ui-toolbar-boundary"');
+    expect(html).toContain('data-hz-topology-toolbar-action-policy="scope-controls-only"');
+    expect(html).toContain('data-hz-topology-toolbar-canvas-action-policy="in-canvas-g6-toolbar"');
+    expect(html).toContain('data-hz-topology-toolbar-chrome="frameless"');
+    expect(html).toContain('data-hz-topology-toolbar-frame="none"');
+    expect(html).toContain('data-hz-topology-control="environment"');
+    expect(html).toContain('data-hz-topology-control-owner="hertzbeat-ui-toolbar-control"');
+    expect(html).toContain('data-hz-topology-control="search"');
+    expect(html).toContain('data-hz-topology-control="source-kind"');
+    expect(html).not.toContain('data-hz-topology-control="fit-view"');
+    expect(html).not.toContain('data-hz-topology-control="locate-entity"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip="source-depth-group-reset"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-owner="hertzbeat-ui-toolbar-control-strip"');
+    expect(html).toContain('data-hz-topology-control="depth"');
+    expect(html).toContain('data-hz-topology-control-depth-owner="hertzbeat-ui-toolbar-depth-control"');
+    expect(html).toContain('data-hz-topology-control-depth-value="2"');
+    expect(html).not.toContain('data-hz-topology-control="layout"');
+    expect(html).toContain('data-hz-topology-control="group-by"');
+    expect(html).toContain('data-hz-topology-control-group-owner="hertzbeat-ui-toolbar-group-control"');
+    expect(html).toContain('data-hz-topology-control-group-value="none"');
+    expect(html).toContain('data-hz-topology-control="reset-scope"');
+    expect(html).toContain('data-hz-topology-control-reset-owner="hertzbeat-ui-toolbar-reset-control"');
+    expect(html).toContain('data-hz-topology-toolbar-summary-owner="hertzbeat-ui-toolbar-summary"');
+    expect(html).toContain('data-hz-topology-toolbar-summary-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-toolbar-summary-label-owner="hertzbeat-ui-toolbar-summary-label"');
+    expect(html).toContain('data-hz-topology-summary-item-owner="hertzbeat-ui-toolbar-summary-item"');
+    expect(html).toContain('data-hz-topology-toolbar-state="focus-depth-group"');
+    expect(html).toContain('data-hz-topology-toolbar-state-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-toolbar-state-owner="hertzbeat-ui-toolbar-state"');
+    expect(html).toContain('data-hz-topology-state-label-owner="hertzbeat-ui-toolbar-state-label"');
+    expect(html).toContain('data-hz-topology-state-item="focus"');
+    expect(html).toContain('data-hz-topology-state-item-owner="hertzbeat-ui-toolbar-state-item"');
+    expect(html).toContain('data-hz-topology-state-indicator-owner="hertzbeat-ui-toolbar-state-indicator"');
+    expect(html).toContain('data-hz-topology-state-item-label-owner="hertzbeat-ui-toolbar-state-item-label"');
+    expect(html).toContain('data-hz-topology-state-item-value-owner="hertzbeat-ui-toolbar-state-item-value"');
+    expect(html).toContain('data-hz-topology-state-item="depth"');
+    expect(html).not.toContain('data-hz-topology-state-item="layout"');
+    expect(html).toContain('data-hz-topology-state-item="group"');
+    expect(html).toContain('data-hz-ui-lab-topology-scope-bar="shared"');
+    expect(html).toContain('data-hz-ui="topology-scope-bar"');
+    expect(html).toContain('data-hz-topology-primitive="scope-bar"');
+    expect(html).toContain('data-hz-topology-scope-bar-boundary="section"');
+    expect(html).toContain('data-hz-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"');
+    expect(html).toContain('data-hz-topology-scope-item="environment"');
+    expect(html).toContain('data-hz-topology-scope-item-owner="hertzbeat-ui-scope-item"');
+    expect(html).toContain('data-hz-topology-scope-item-label-owner="hertzbeat-ui-scope-item-label"');
+    expect(html).toContain('data-hz-topology-scope-item-value-owner="hertzbeat-ui-scope-item-value"');
+    expect(html).toContain('data-hz-topology-scope-action="refresh"');
+    expect(html).toContain('data-hz-topology-scope-action-owner="hertzbeat-ui-scope-action"');
+    expect(html).toContain('data-hz-ui-lab-topology-focus-trail="shared"');
+    expect(html).toContain('data-hz-ui="topology-focus-trail"');
+    expect(html).toContain('data-hz-topology-primitive="focus-trail"');
+    expect(html).toContain('data-hz-topology-focus-trail-mode="focused"');
+    expect(html).toContain('data-hz-topology-focus-trail-mode-owner="hertzbeat-ui-focus-trail-mode"');
+    expect(html).toContain('data-hz-topology-focus-trail-depth="2"');
+    expect(html).toContain('data-hz-topology-focus-trail-depth-owner="hertzbeat-ui-focus-trail-depth"');
+    expect(html).toContain('data-hz-topology-focus-trail-entity="checkout-api"');
+    expect(html).toContain('data-hz-topology-focus-trail-entity-owner="hertzbeat-ui-focus-trail-entity"');
+    expect(html).toContain('data-hz-topology-focus-trail-density="graph-dock"');
+    expect(html).toContain('data-hz-topology-focus-trail-layout="single-line-nowrap"');
+    expect(html).toContain('data-hz-topology-focus-trail-height-contract="one-compact-row"');
+    expect(html).toContain('data-hz-topology-focus-trail-occlusion="none"');
+    expect(html).toContain('data-hz-topology-focus-trail-position-contract="document-flow"');
+    expect(html).toContain('data-hz-topology-focus-trail-priority="canvas"');
+    expect(html).toContain('data-hz-topology-focus-trail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-focus-trail-visual-weight-owner="hertzbeat-ui-focus-trail-visual-weight"');
+    expect(html).toContain('data-hz-topology-focus-trail-boundary="section"');
+    expect(html).toContain('data-hz-topology-focus-trail-boundary-owner="hertzbeat-ui-focus-trail-boundary"');
+    expect(html).toContain('data-hz-topology-focus-trail-label-owner="hertzbeat-ui-focus-trail-label"');
+    expect(html).toContain('data-hz-topology-focus-crumbs-owner="hertzbeat-ui-focus-trail-crumbs"');
+    expect(html).toContain('data-hz-topology-focus-crumb="all"');
+    expect(html).toContain('data-hz-topology-focus-crumb="checkout-api"');
+    expect(html).toContain('data-hz-topology-focus-crumb-active="true"');
+    expect(html).toContain('data-hz-topology-focus-crumb-owner="hertzbeat-ui-focus-trail-crumb"');
+    expect(html).toContain('data-hz-topology-focus-crumb-label-owner="hertzbeat-ui-focus-trail-crumb-label"');
+    expect(html).toContain('data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value"');
+    expect(html).toContain('data-hz-topology-focus-filters-owner="hertzbeat-ui-focus-trail-filters"');
+    expect(html).toContain('data-hz-topology-focus-filter="environment"');
+    expect(html).toContain('data-hz-topology-focus-filter="source"');
+    expect(html).toContain('data-hz-topology-focus-filter-owner="hertzbeat-ui-focus-trail-filter"');
+    expect(html).toContain('data-hz-topology-focus-filter-label-owner="hertzbeat-ui-focus-trail-filter-label"');
+    expect(html).toContain('data-hz-topology-focus-filter-value-owner="hertzbeat-ui-focus-trail-filter-value"');
+    expect(html).toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).toContain('data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit"');
+    expect(html).toContain('data-hz-topology-focus-exit-href="/ui-lab#topology"');
+    expect(html).toContain('data-hz-topology-focus-exit-href-owner="hertzbeat-ui-focus-trail-exit-href"');
+    expect(html).toContain('data-hz-ui-lab-topology-group-panel="shared"');
+    expect(html).toContain('data-hz-ui="topology-group-panel"');
+    expect(html).toContain('data-hz-topology-primitive="group-panel"');
+    expect(html).toContain('data-hz-topology-group-panel-boundary="section"');
+    expect(html).toContain('data-hz-topology-group-panel-boundary-owner="hertzbeat-ui-group-panel-boundary"');
+    expect(html).toContain('data-hz-topology-group-panel-header-owner="hertzbeat-ui-group-panel-header"');
+    expect(html).toContain('data-hz-topology-group-panel-group-by-owner="hertzbeat-ui-group-panel-group-by"');
+    expect(html).toContain('data-hz-topology-group-panel-items-owner="hertzbeat-ui-group-panel-items"');
+    expect(html).toContain('data-hz-topology-group-panel-item="prod-services"');
+    expect(html).toContain('data-hz-topology-group-panel-item-owner="hertzbeat-ui-group-panel-item"');
+    expect(html).toContain('data-hz-topology-group-panel-item-active="true"');
+    expect(html).toContain('data-hz-topology-group-panel-item-worst-tone="danger"');
+    expect(html).toContain('data-hz-topology-group-panel-item-collapsed-count="7"');
+    expect(html).toContain('data-hz-topology-group-panel-action="clear-group"');
+    expect(html).toContain('data-hz-topology-group-panel-action-owner="hertzbeat-ui-group-panel-action"');
+    expect(html).toContain('data-hz-ui-lab-topology-path-summary="shared"');
+    expect(html).toContain('data-hz-ui="topology-path-summary"');
+    expect(html).toContain('data-hz-topology-primitive="path-summary"');
+    expect(html).toContain('data-hz-topology-path-summary-boundary="section"');
+    expect(html).toContain('data-hz-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"');
+    expect(html).toContain('data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"');
+    expect(html).toContain('data-hz-topology-path-selected-edge="checkout-orders"');
+    expect(html).toContain('data-hz-topology-path-hovered-edge="checkout-orders"');
+    expect(html).toContain('data-hz-topology-path-source-id="svc-checkout"');
+    expect(html).toContain('data-hz-topology-path-target-id="db-orders"');
+    expect(html).toContain('data-hz-topology-path-relation-type="HTTP call"');
+    expect(html).toContain('data-hz-topology-path-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-path-endpoint="source"');
+    expect(html).toContain('data-hz-topology-path-endpoint="target"');
+    expect(html).toContain('data-hz-topology-path-arrow-owner="hertzbeat-ui-path-summary-arrow"');
+    expect(html).toContain('data-hz-topology-path-relation-owner="hertzbeat-ui-path-summary-relation"');
+    expect(html).toContain('data-hz-topology-path-metric="latency-p95"');
+    expect(html).toContain('data-hz-topology-path-badge="trace"');
+    expect(html).toContain('data-hz-topology-path-action="focus-path"');
+    expect(html).toContain('data-hz-topology-path-action-owner="hertzbeat-ui-path-summary-action"');
+    expect(html).toContain('data-hz-ui-lab-topology-filter-strip="shared"');
+    expect(html).toContain('data-hz-ui="topology-filter-strip"');
+    expect(html).toContain('data-hz-topology-primitive="filter-strip"');
+    expect(html).toContain('data-hz-topology-filter-strip-variant="source-grid"');
+    expect(html).toContain('data-hz-topology-filter-strip-boundary="section"');
+    expect(html).toContain('data-hz-topology-filter-strip-boundary-owner="hertzbeat-ui-filter-strip-boundary"');
+    expect(html).toContain('data-hz-topology-filter-item="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-filter-item-owner="hertzbeat-ui-filter-strip-item"');
+    expect(html).toContain('data-hz-topology-filter-item-label-owner="hertzbeat-ui-filter-strip-label"');
+    expect(html).toContain('data-hz-topology-filter-item-copy-owner="hertzbeat-ui-filter-strip-copy"');
+    expect(html).toContain('data-hz-topology-filter-item-active="true"');
+    expect(html).toContain('data-hz-ui-lab-topology-source-rail="shared"');
+    expect(html).toContain('data-hz-topology-filter-strip-variant="source-rail"');
+    expect(html).toContain('data-hz-topology-filter-strip-layout="single-line-wrap"');
+    expect(html).toContain('data-hz-topology-filter-strip-density="compact-rail"');
+    expect(html).toContain('data-hz-topology-filter-strip-height-contract="one-control-row-preferred"');
+    expect(html).toContain('data-hz-ui-lab-topology-view-filter-strip="shared"');
+    expect(html).toContain('data-hz-topology-filter-strip-variant="view-list"');
+    expect(html).toContain('data-hz-topology-filter-strip-boundary="section"');
+    expect(html).toContain('data-hz-topology-filter-strip-copy-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-filter-strip-copy-visibility-owner="hertzbeat-ui-filter-strip-copy-visibility"');
+    expect(html).toContain('data-hz-topology-filter-item="service-call"');
+    expect(html).toContain('data-hz-ui-lab-topology-action-link="shared"');
+    expect(html).toContain('data-hz-ui="topology-action-link"');
+    expect(html).toContain('data-hz-topology-primitive="action-link"');
+    expect(html).toContain('data-hz-topology-action-link="alert-impact"');
+    expect(html).toContain('data-hz-topology-action-link-spacing-owner="hertzbeat-ui-action-link-spacing"');
+    expect(html).toContain('data-hz-topology-action-link-label-owner="hertzbeat-ui-action-link-label"');
+    expect(html).toContain('data-hz-topology-action-link-copy-owner="hertzbeat-ui-action-link-copy"');
+    expect(html).toContain('data-hz-ui-lab-topology-canvas="shared"');
+    expect(html).toContain('data-hz-ui-lab-topology-workbench-canvas-slot="shared"');
+    expect(html).toContain('data-hz-ui-lab-topology-workbench-companion-slot="shared"');
+    expect(html).toContain('data-hz-ui="topology-workbench-slot"');
+    expect(html).toContain('data-hz-topology-primitive="workbench-slot"');
+    expect(html).toContain('data-hz-topology-workbench-slot-owner="hertzbeat-ui-workbench-slot"');
+    expect(html).toContain('data-hz-topology-workbench-slot-kind="canvas"');
+    expect(html).toContain('data-hz-topology-workbench-slot-kind="companion"');
+    expect(html).toContain('data-hz-topology-workbench-slot-surface="placeholder"');
+    expect(source).not.toContain('min-h-[120px] border-t border-[var(--hz-ui-line-soft)] bg-[#08090c] px-3 py-2');
+    expect(source).not.toContain('border-l border-[var(--hz-ui-line-soft)] bg-[#0b0c0f] px-3 py-2');
+    expect(html).toContain('data-hz-ui="topology-canvas"');
+    expect(html).toContain('data-hz-topology-primitive="canvas"');
+    expect(html).toContain('data-hz-topology-canvas-layout="layered-service"');
+    expect(html).toContain('data-hz-topology-canvas-layout-owner="hertzbeat-ui-canvas-layout"');
+    expect(html).toContain('data-hz-topology-canvas-hover-mode="neighbor-highlight"');
+    expect(html).toContain('data-hz-topology-canvas-drawer-mode="node-edge"');
+    expect(html).toContain('data-hz-topology-canvas-focus-depth="2-hop"');
+    expect(html).toContain('data-hz-topology-canvas-interaction-scope="hover-group"');
+    expect(html).toContain('data-hz-topology-canvas-interaction-owner="hertzbeat-ui-canvas-interaction"');
+    expect(html).toContain('data-hz-topology-canvas-interaction-scope-owner="hertzbeat-ui-canvas-interaction-scope"');
+    expect(html).toContain('data-hz-topology-canvas-boundary="section"');
+    expect(html).toContain('data-hz-topology-canvas-boundary-owner="hertzbeat-ui-canvas-boundary"');
+    expect(html).toContain('data-hz-topology-canvas-min-height-owner="hertzbeat-ui-canvas-min-height"');
+    expect(html).not.toContain('className="border-t border-[var(--hz-ui-line-soft)]"');
+    expect(html).toContain('data-hz-ui-lab-topology-canvas-annotation="shared"');
+    expect(html).toContain('data-hz-ui="topology-canvas-annotation"');
+    expect(html).toContain('data-hz-topology-primitive="canvas-annotation"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-placement="top-left"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-owner="hertzbeat-ui-canvas-annotation"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-occlusion="none"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-hit-test="pass-through"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-title-owner="hertzbeat-ui-canvas-annotation-title"');
+    expect(html).toContain('data-hz-topology-canvas-annotation-copy-owner="hertzbeat-ui-canvas-annotation-copy"');
+    expect(source).not.toContain('className="absolute left-6 top-6 text-[11px] font-semibold text-[#8f99ab]"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-canvas="grafana-inspired-shared"');
+    expect(html).toContain('data-hz-ui="topology-g6-canvas"');
+    expect(html).toContain('data-hz-topology-primitive="g6-canvas"');
+    expect(html).toContain('data-hz-topology-g6-canvas-owner="hertzbeat-ui-g6-canvas"');
+    expect(html).toContain('data-hz-topology-g6-visual-language="hertzbeat-dark-hard-canvas"');
+    expect(html).toContain('data-hz-topology-g6-canvas-background="neutral-graphite"');
+    expect(html).toContain('data-hz-topology-g6-canvas-radius="hard-2px"');
+    expect(html).toContain('data-hz-topology-g6-node-visual="circle-icon-label"');
+    expect(html).toContain('data-hz-topology-g6-node-icon-source="lucide-entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-g6-node-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-g6-node-icon-no-handdrawn="true"');
+    expect(html).toContain('data-hz-topology-g6-engine="antv-g6"');
+    expect(html).toContain('data-hz-topology-g6-interaction-owner="hertzbeat-ui-g6-interaction"');
+    expect(html).toContain('data-hz-topology-g6-auto-fit="initial-only"');
+    expect(html).toContain('data-hz-topology-g6-auto-fit-owner="hertzbeat-ui-g6-auto-fit"');
+    expect(html).toContain('data-hz-topology-g6-viewport-telemetry="live"');
+    expect(html).toContain('data-hz-topology-g6-viewport-telemetry-owner="hertzbeat-ui-g6-viewport-telemetry"');
+    expect(html).toContain('data-hz-topology-g6-viewport-telemetry-timing="after-action-settled"');
+    expect(html).toContain('data-hz-topology-g6-viewport-telemetry-timing-owner="hertzbeat-ui-g6-viewport-telemetry-timing"');
+    expect(html).toContain('data-hz-topology-g6-command-bridge="custom-event"');
+    expect(html).toContain('data-hz-topology-g6-command-bridge-owner="hertzbeat-ui-g6-command-bridge"');
+    expect(html).toContain('data-hz-topology-g6-command-event="hz-topology-g6-viewport-command"');
+    expect(html).toContain('data-hz-topology-g6-command-request="idle"');
+    expect(html).toContain('data-hz-topology-g6-command-request-owner="hertzbeat-ui-g6-command-request"');
+    expect(html).toContain('data-hz-topology-g6-wheel-mode="manual-clamped-g6-zoom"');
+    expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
+    expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
+    expect(html).toContain('data-hz-topology-g6-wheel-origin="pointer-clamped"');
+    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-4.8"');
+    expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts="plus-minus-zero-fit"');
+    expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts-owner="hertzbeat-ui-g6-keyboard"');
+    expect(html).toContain('data-hz-topology-g6-keyboard-actions="zoom-in zoom-out reset-view fit-view"');
+    expect(html).toContain('data-hz-topology-g6-minimap="viewport-overview"');
+    expect(html).toContain('data-hz-topology-g6-minimap-owner="hertzbeat-ui-g6-minimap"');
+    expect(html).toContain('data-hz-topology-g6-minimap-visibility="visible"');
+    expect(html).toContain('data-hz-topology-g6-minimap-node-count="7"');
+    expect(html).toContain('data-hz-topology-g6-minimap-edge-count="6"');
+    expect(html).toContain('data-hz-topology-g6-minimap-viewport-source="initial"');
+    expect(html).toContain('data-hz-topology-g6-node-selection="drawer-sync"');
+    expect(html).toContain('data-hz-topology-g6-edge-selection="drawer-sync"');
+    expect(source).toContain('const [topologyG6LabSelection, setTopologyG6LabSelection]');
+    expect(source).toContain('handleTopologyG6LabNodeSelect');
+    expect(source).toContain('handleTopologyG6LabEdgeSelect');
+    expect(source).toContain('onNodeSelect={handleTopologyG6LabNodeSelect}');
+    expect(source).toContain('onEdgeSelect={handleTopologyG6LabEdgeSelect}');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-selection-mode="in-page-click-drawer-demo"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-selected-node="svc-checkout"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-selected-edge="web-checkout"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-selection-source="initial"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-highlight="metadata-only"');
+    expect(html).toContain('data-hz-topology-g6-hover-state-engine="metadata-only-no-g6-state"');
+    expect(html).toContain('data-hz-topology-g6-hover-owner="hertzbeat-ui-g6-hover"');
+    expect(html).toContain('data-hz-topology-g6-hover-source="node"');
+    expect(html).toContain('data-hz-topology-g6-hovered-node="svc-checkout"');
+    expect(html).toContain('data-hz-topology-g6-active-focus-source="hover-node"');
+    expect(html).toContain('data-hz-topology-g6-focus-entry="double-click-node"');
+    expect(html).toContain('data-hz-topology-g6-focus-entry-owner="hertzbeat-ui-g6-focus-entry"');
+    expect(html).toContain('data-hz-topology-g6-focus-depth-target="1-hop"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-focus-owner="hertzbeat-ui-g6-neighbor-focus"');
+    expect(html).toContain('data-hz-topology-g6-focused-node="svc-checkout"');
+    expect(html).toContain('data-hz-topology-g6-upstream-node-count="2"');
+    expect(html).toContain('data-hz-topology-g6-downstream-node-count="4"');
+    expect(html).toContain('data-hz-topology-g6-node-neighbor-owner="hertzbeat-ui-g6-node-neighbor"');
+    expect(html).toContain('data-hz-topology-g6-edge-neighbor-owner="hertzbeat-ui-g6-edge-neighbor"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-role="upstream"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-role="downstream"');
+    expect(html).toContain('data-hz-topology-g6-neighbor-role="outgoing"');
+    expect(html).toContain('data-hz-topology-g6-node-count="7"');
+    expect(html).toContain('data-hz-topology-g6-edge-count="6"');
+    expect(html).toContain('data-hz-topology-g6-action="fit-view"');
+    expect(html).toContain('data-hz-topology-g6-filter-owner="hertzbeat-ui-g6-filter"');
+    expect(html).toContain('data-hz-topology-g6-filter-environment="prod"');
+    expect(html).toContain('data-hz-topology-g6-filter-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-g6-filter-group-by="source-kind"');
+    expect(html).toContain('data-hz-topology-g6-filter-source-match="true"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-owner="hertzbeat-ui-g6-filter-control"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-count="4"');
+    expect(html).toContain('data-hz-topology-g6-filter-control="source-kind"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-kind="source-kind"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-value="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-g6-filter-control="group-by"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-kind="group-by"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-value="source-kind"');
+    expect(html).toContain('data-hz-topology-g6-filter-control="search"');
+    expect(html).toContain('data-hz-topology-g6-filter-control="reset"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-group-summary="shared"');
+    expect(html).toContain('data-hz-topology-g6-group-owner="hertzbeat-ui-g6-group"');
+    expect(html).toContain('data-hz-topology-g6-group-surface-owner="hertzbeat-ui-g6-group-surface"');
+    expect(html).toContain('data-hz-topology-g6-group-item="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-g6-group-item-action-owner="hertzbeat-ui-g6-group-item-action"');
+    expect(html).toContain('href="/topology?sourceKind=otlp-trace-call&amp;environment=prod&amp;groupBy=source-kind"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-non-occluding="shared"');
+    expect(html).toContain('data-hz-topology-g6-overlay-mode="non-occluding"');
+    expect(html).toContain('data-hz-topology-g6-summary-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-g6-group-surface-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-surface-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-visibility="visible"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-contract="50-200-500"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-canvas="500-node-windowed-shared"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-50="50"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-200="200"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-500="500"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-200-strategy="force-with-grouping"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-strategy="grouped-table-companion"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-filtering="required"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-table-companion="required"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-render-window="500-node-windowed-shared"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-mode="windowed"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-rendered="200"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-hidden="300"');
+    expect(html).toContain('data-hz-topology-g6-large-graph-owner="hertzbeat-ui-g6-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-render-window-owner="hertzbeat-ui-g6-render-window"');
+    expect(html).toContain('data-hz-topology-g6-render-window-mode="windowed"');
+    expect(html).toContain('data-hz-topology-g6-render-window-visible-node-budget="200"');
+    expect(html).toContain('data-hz-topology-g6-render-window-rendered-node-count="200"');
+    expect(html).toContain('data-hz-topology-g6-render-window-hidden-node-count="300"');
+    expect(html).toContain('data-hz-topology-g6-render-window-table-companion="required"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-owner="hertzbeat-ui-g6-mount-lifecycle"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-policy="structure-layout-height-only"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-redraw-policy="setData-draw-preserve-viewport"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key=');
+    expect(html).toContain('data-hz-topology-g6-render-window-priority-node-count="1"');
+    expect(html).toContain('data-hz-topology-g6-render-window-priority-node-ids="scale-svc-420"');
+    expect(source).toContain('const [topologyG6ScaleLabSelection, setTopologyG6ScaleLabSelection]');
+    expect(source).toContain('handleTopologyG6ScaleLabNodeSelect');
+    expect(source).toContain('handleTopologyG6ScaleLabEdgeSelect');
+    expect(source).toContain('onNodeSelect={handleTopologyG6ScaleLabNodeSelect}');
+    expect(source).toContain('onEdgeSelect={handleTopologyG6ScaleLabEdgeSelect}');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-selection-mode="in-page-click-drawer-demo"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-selected-node="scale-svc-420"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-selected-edge="none"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-selection-source="initial"');
+    expect(html).toContain(
+      'data-hz-ui-lab-topology-g6-scale-browser-regression="zoom-hover-select-table-stable"'
+    );
+    expect(html).toContain(
+      'data-hz-ui-lab-topology-g6-scale-browser-regression-owner="hertzbeat-ui-g6-scale-regression"'
+    );
+    expect(html).toContain(
+      'data-hz-ui-lab-topology-g6-scale-browser-regression-invariants="no-url-change no-remount viewport-preserved table-companion"'
+    );
+    expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="500"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-node-count="200"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-hidden-node-count="300"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="required"');
+    expect(html).toContain('data-hz-topology-metric-table-priority-node-ids="scale-svc-420"');
+    expect(html).toContain('data-hz-topology-metric-table-visible-row-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-partial-row-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-unknown-row-count="0"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-filter="all"');
+    expect(html).toContain('data-hz-topology-metric-table-filtered-row-count="3"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-owner="hertzbeat-ui-metric-table-filter-control"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
+    expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="partial"');
+    expect(html).toContain('data-hz-topology-edge-row-source-node-id="scale-svc-420"');
+    expect(html).toContain('data-hz-topology-edge-row-target-node-id="scale-svc-421"');
+    expect(html).toContain('data-hz-topology-edge-row-source-visible="true"');
+    expect(html).toContain('data-hz-topology-edge-row-target-visible="false"');
+    expect(html).toContain('data-hz-topology-g6-search-owner="hertzbeat-ui-g6-search"');
+    expect(html).toContain('data-hz-topology-g6-search-query="Service 420"');
+    expect(html).toContain('data-hz-topology-g6-search-match-count="1"');
+    expect(html).toContain('data-hz-topology-g6-search-first-match="scale-svc-420"');
+    expect(html).toContain('data-hz-topology-g6-action="focus-search-result"');
+    expect(html).toContain('data-hz-topology-g6-selected-focus-owner="hertzbeat-ui-g6-selected-focus"');
+    expect(html).toContain('data-hz-topology-g6-selected-focus-target="scale-svc-420"');
+    expect(html).toContain('data-hz-topology-g6-selected-focus-status="ready"');
+    expect(html).toContain('data-hz-topology-g6-action="focus-selected-node"');
+    expect(html).toContain('data-hz-topology-g6-focus-action-owner="hertzbeat-ui-g6-focus-action"');
+    expect(html).toContain('data-hz-topology-g6-focus-action-behavior="explicit-toolbar-recenter"');
+    expect(html).toContain('data-hz-topology-g6-focus-action-source="none"');
+    expect(html).toContain('data-hz-topology-g6-focus-action-target="none"');
+    expect(html).toContain('data-hz-topology-g6-scale-owner="hertzbeat-ui-g6-scale"');
+    expect(html).toContain('data-hz-topology-g6-scale-tier="stress"');
+    expect(html).toContain('data-hz-topology-g6-scale-node-count="500"');
+    expect(html).toContain('data-hz-ui-lab-topology-graph-layer="shared"');
+    expect(html).toContain('data-hz-ui="topology-graph-layer"');
+    expect(html).toContain('data-hz-topology-primitive="graph-layer"');
+    expect(html).toContain('data-hz-topology-graph-layer="svg-edge-layer"');
+    expect(html).toContain('data-hz-topology-graph-layer-owner="hertzbeat-ui-graph-layer"');
+    expect(html).toContain('data-hz-ui-lab-topology-node="shared"');
+    expect(html).toContain('data-hz-ui="topology-node"');
+    expect(html).toContain('data-hz-topology-primitive="node"');
+    expect(html).toContain('data-hz-topology-node-owner="hertzbeat-ui-node"');
+    expect(html).toContain('data-hz-topology-node-focus="active"');
+    expect(html).toContain('data-hz-topology-node-evidence-badges="trace relation"');
+    expect(html).toContain('data-hz-topology-node-label-owner="hertzbeat-ui-node-label"');
+    expect(html).toContain('data-hz-topology-node-health-owner="hertzbeat-ui-node-health"');
+    expect(html).toContain('data-hz-topology-node-health-label-owner="hertzbeat-ui-node-health-label"');
+    expect(html).toContain('data-hz-topology-node-red-owner="hertzbeat-ui-node-red"');
+    expect(html).toContain('data-hz-topology-node-red-metric-owner="hertzbeat-ui-node-red-metric"');
+    expect(html).toContain('data-hz-topology-node-badge-list-owner="hertzbeat-ui-node-badge-list"');
+    expect(html).toContain('data-hz-topology-node-badge-owner="hertzbeat-ui-node-badge"');
+    expect(html).toContain('data-hz-ui-lab-topology-edge="shared"');
+    expect(html).toContain('data-hz-ui="topology-edge"');
+    expect(html).toContain('data-hz-topology-edge-owner="hertzbeat-ui-edge"');
+    expect(html).toContain('data-hz-topology-edge-variant="line"');
+    expect(html).toContain('data-hz-topology-edge-line-owner="hertzbeat-ui-edge-line"');
+    expect(html).toContain('data-hz-topology-edge-path-owner="hertzbeat-ui-edge-path"');
+    expect(html).toContain('data-hz-topology-edge-arrow-owner="hertzbeat-ui-edge-arrow"');
+    expect(html).toContain('data-hz-topology-edge-variant="drilldown"');
+    expect(html).toContain('data-hz-topology-edge-drilldown-owner="hertzbeat-ui-edge-drilldown"');
+    expect(html).toContain('data-hz-topology-edge-hit-target-owner="hertzbeat-ui-edge-hit-target"');
+    expect(html).toContain('data-hz-topology-edge-evidence-badges="trace alert"');
+    expect(html).toContain('data-hz-topology-edge-red-owner="hertzbeat-ui-edge-red"');
+    expect(html).toContain('data-hz-topology-edge-badge-owner="hertzbeat-ui-edge-badge"');
+    expect(html).toContain('data-hz-ui-lab-topology-hover-tooltip="shared"');
+    expect(html).toContain('data-hz-ui="topology-hover-tooltip"');
+    expect(html).toContain('data-hz-topology-primitive="hover-tooltip"');
+    expect(html).toContain('data-hz-topology-hover-kind="edge"');
+    expect(html).toContain('data-hz-topology-hover-trigger="live-edge-hover"');
+    expect(html).toContain('data-hz-topology-hover-trigger-owner="hertzbeat-ui-hover-trigger"');
+    expect(html).toContain('data-hz-topology-hover-placement="canvas-anchor"');
+    expect(html).toContain('data-hz-topology-hover-collision-safe="cursor-anchor-clamped"');
+    expect(html).toContain('data-hz-topology-hover-anchor-collision-boundary="canvas"');
+    expect(html).toContain('data-hz-topology-hover-anchor-owner="hertzbeat-ui-hover-anchor"');
+    expect(html).toContain('data-hz-topology-hover-anchor-source="g6-pointer"');
+    expect(html).toContain('left:clamp(12px,var(--hz-topology-hover-x),calc(100% - var(--hz-topology-hover-width)))');
+    expect(html).toContain('top:clamp(52px,var(--hz-topology-hover-y),calc(100% - var(--hz-topology-hover-height)))');
+    expect(html).toContain('data-hz-topology-hover-size="compact"');
+    expect(html).toContain('data-hz-topology-hover-surface-owner="hertzbeat-ui-hover-surface"');
+    expect(html).toContain('data-hz-topology-hover-header-owner="hertzbeat-ui-hover-header"');
+    expect(html).toContain('data-hz-topology-hover-title-owner="hertzbeat-ui-hover-title"');
+    expect(html).toContain('data-hz-topology-hover-summary-owner="hertzbeat-ui-hover-summary"');
+    expect(html).toContain('data-hz-topology-hover-fact-grid-owner="hertzbeat-ui-hover-fact-grid"');
+    expect(html).toContain('data-hz-topology-hover-fact="sample-trace"');
+    expect(html).toContain('data-hz-topology-hover-fact-owner="hertzbeat-ui-hover-fact"');
+    expect(html).toContain('data-hz-topology-hover-fact-label-owner="hertzbeat-ui-hover-fact-label"');
+    expect(html).toContain('data-hz-topology-hover-fact-value-owner="hertzbeat-ui-hover-fact-value"');
+    expect(html).toContain('data-hz-topology-hover-fact-meta-owner="hertzbeat-ui-hover-fact-meta"');
+    expect(html).toContain('data-hz-topology-hover-metric-grid-owner="hertzbeat-ui-hover-metric-grid"');
+    expect(html).toContain('data-hz-topology-hover-metric="latency-p95"');
+    expect(html).toContain('data-hz-topology-hover-metric-owner="hertzbeat-ui-hover-metric"');
+    expect(html).toContain('data-hz-topology-hover-metric-indicator-owner="hertzbeat-ui-hover-metric-indicator"');
+    expect(html).toContain('data-hz-topology-hover-metric-label-owner="hertzbeat-ui-hover-metric-label"');
+    expect(html).toContain('data-hz-topology-hover-metric-value-owner="hertzbeat-ui-hover-metric-value"');
+    expect(html).toContain('data-hz-topology-hover-badge-list-owner="hertzbeat-ui-hover-badge-list"');
+    expect(html).toContain('data-hz-topology-hover-badge-owner="hertzbeat-ui-hover-badge"');
+    expect(html).toContain('data-hz-ui-lab-topology-workbench-frame="shared"');
+    expect(html).toContain('data-hz-ui="topology-workbench-frame"');
+    expect(html).toContain('data-hz-topology-primitive="workbench-frame"');
+    expect(html).toContain('data-hz-topology-workbench-boundary="section"');
+    expect(html).toContain('data-hz-topology-workbench-frame-boundary-owner="hertzbeat-ui-workbench-frame-boundary"');
+    expect(html).toContain('data-hz-ui-lab-topology-workbench-header="shared"');
+    expect(html).toContain('data-hz-ui="topology-workbench-header"');
+    expect(html).toContain('data-hz-topology-primitive="workbench-header"');
+    expect(html).toContain('data-hz-topology-workbench-header-owner="hertzbeat-ui-workbench-header"');
+    expect(html).toContain('data-hz-topology-workbench-header-density="operational-compact"');
+    expect(html).toContain('data-hz-topology-workbench-header-density-owner="hertzbeat-ui-workbench-header-density"');
+    expect(html).toContain('data-hz-topology-workbench-header-boundary="none"');
+    expect(html).toContain('data-hz-topology-workbench-header-boundary-owner="hertzbeat-ui-workbench-header-boundary"');
+    expect(html).toContain('data-hz-topology-workbench-header-alignment="shared-control-grid"');
+    expect(html).toContain('data-hz-topology-workbench-header-inset="16px"');
+    expect(html).toContain('data-hz-topology-workbench-header-control-height="28px"');
+    expect(html).toContain('data-hz-topology-workbench-copy-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-workbench-eyebrow-owner="hertzbeat-ui-workbench-eyebrow"');
+    expect(html).toContain('data-hz-topology-workbench-title-owner="hertzbeat-ui-workbench-title"');
+    expect(html).toContain('data-hz-topology-workbench-copy-owner="hertzbeat-ui-workbench-copy"');
+    expect(html).toContain('data-hz-topology-workbench-scope-slot-owner="hertzbeat-ui-workbench-scope-slot"');
+    expect(html).toContain('data-hz-topology-workbench-source-slot-owner="hertzbeat-ui-workbench-source-slot"');
+    expect(html).toContain('data-hz-ui-lab-topology-workbench-grid="shared"');
+    expect(html).toContain('data-hz-ui="topology-workbench-grid"');
+    expect(html).toContain('data-hz-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
+    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-legend-dock="shared"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-owner="hertzbeat-ui-g6-legend-dock"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-placement="canvas-bottom-left"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-occlusion="inside-canvas-low-interruption"');
+    expect(html).toContain('data-hz-topology-legend-density="canvas-dock"');
+    expect(html).toContain('data-hz-topology-legend-layout="inline-g6-dock"');
+    expect(html).toContain('data-hz-topology-legend-occlusion="low"');
+    expect(html).toContain('data-hz-ui-lab-topology-companion-rail="shared"');
+    expect(html).toContain('data-hz-ui-lab-topology-companion-rail-priority="graph-first"');
+    expect(html).toContain('data-hz-ui="topology-companion-rail"');
+    expect(html).toContain('data-hz-topology-primitive="companion-rail"');
+    expect(html).toContain('data-hz-topology-companion-priority="graph-first"');
+    expect(html).toContain('data-hz-topology-companion-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-companion-scroll="contained"');
+    expect(html).toContain('data-hz-topology-companion-scroll-owner="hertzbeat-ui-companion-rail-scroll"');
+    expect(html).toContain('data-hz-topology-companion-viewport-contract="graph-height"');
+    expect(html).toContain('data-hz-topology-companion-sticky-context="jump-list"');
+    expect(html).toContain('data-hz-topology-companion-sticky-context-owner="hertzbeat-ui-companion-rail-sticky-context"');
+    expect(html).toContain('data-hz-topology-companion-sticky-target="topology-companion-jump-list"');
+    expect(html).toContain('data-hz-topology-companion-sticky-target-owner="hertzbeat-ui-companion-rail-sticky-target"');
+    expect(html).toContain('data-hz-ui-lab-topology-companion-jump-list="shared"');
+    expect(html).toContain('data-hz-ui="topology-companion-jump-list"');
+    expect(html).toContain('data-hz-topology-primitive="companion-jump-list"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-owner="hertzbeat-ui-companion-jump-list"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-density="graph-first"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-interaction="anchor-jump"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-sticky="top"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-scroll-scope="contained-rail"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-scroll-scope-owner="hertzbeat-ui-companion-jump-list-scroll-scope"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-active-mode="contained-rail-scroll"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-active-mode-owner="hertzbeat-ui-companion-jump-list-active-mode"');
+    expect(html).toContain('data-hz-topology-companion-jump-item="legend"');
+    expect(html).toContain('data-hz-topology-companion-jump-href="#topology-companion-legend"');
+    expect(html).toContain('data-hz-topology-companion-jump-scroll-owner="hertzbeat-ui-companion-jump-scroll"');
+    expect(html).toContain('data-hz-topology-companion-jump-active-source="manual"');
+    expect(html).toContain('data-hz-ui-lab-topology-companion-section="legend"');
+    expect(html).toContain('data-hz-ui="topology-companion-section"');
+    expect(html).toContain('data-hz-topology-primitive="companion-section"');
+    expect(html).toContain('data-hz-topology-companion-section-id="legend"');
+    expect(html).toContain('data-hz-topology-companion-section-anchor="topology-companion-legend"');
+    expect(html).toContain('data-hz-topology-companion-section-owner="hertzbeat-ui-companion-section"');
+    expect(html).toContain('data-hz-topology-companion-section-anchor-owner="hertzbeat-ui-companion-section-anchor"');
+    expect(html).toContain('data-hz-ui-lab-topology-companion-section-collapsible="shared"');
+    expect(html).toContain('data-hz-topology-companion-section-collapsible="true"');
+    expect(html).toContain('data-hz-topology-companion-section-toggle-owner="hertzbeat-ui-companion-section-toggle"');
+    expect(html).toContain('data-hz-topology-companion-section-body-owner="hertzbeat-ui-companion-section-body"');
+    expect(html).toContain('data-hz-topology-companion-spacing="shared-stack"');
+    expect(html).toContain('data-hz-topology-companion-spacing-owner="hertzbeat-ui-companion-rail-spacing"');
+    expect(html).toContain('data-hz-topology-companion-boundary="stack-section"');
+    expect(html).toContain('data-hz-topology-companion-boundary-owner="hertzbeat-ui-companion-rail-boundary"');
+    expect(html).toContain('data-hz-topology-companion-content-owner="hertzbeat-ui-companion-rail-content"');
+    expect(html).not.toContain('className="border-x-0 border-t border-[var(--hz-ui-line-soft)]"');
+    expect(html).toContain('data-hz-ui-lab-topology-section-label="shared"');
+    expect(html).toContain('data-hz-ui="topology-section-label"');
+    expect(html).toContain('data-hz-topology-primitive="section-label"');
+    expect(html).toContain('data-hz-topology-section-label-density="compact"');
+    expect(html).toContain('data-hz-topology-section-label-owner="hertzbeat-ui-section-label"');
+    expect(html).toContain('data-hz-topology-section-label-text-owner="hertzbeat-ui-section-label-text"');
+    expect(html).toContain('data-hz-ui-lab-topology-legend="shared"');
+    expect(html).toContain('data-hz-ui="topology-legend"');
+    expect(html).toContain('data-hz-topology-primitive="legend"');
+    expect(html).toContain('data-hz-topology-legend-boundary="flush"');
+    expect(html).toContain('data-hz-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"');
+    expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
+    expect(html).toContain('data-hz-topology-legend-section="status"');
+    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
+    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
+    expect(html).toContain('data-hz-topology-legend-section="interaction"');
+    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
+    expect(html).toContain('data-hz-topology-legend-swatch-shape="line"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
+    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
+    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
+    expect(html).toContain('data-hz-topology-legend-swatch-owner="hertzbeat-ui-legend-swatch"');
+    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
+    expect(html).not.toContain('data-hz-topology-legend-pattern="dashed"');
+    expect(html).toContain('data-hz-ui-lab-topology-detail-drawer="shared"');
+    expect(html).toContain('data-hz-ui="topology-detail-drawer"');
+    expect(html).toContain('data-hz-topology-primitive="detail-drawer"');
+    expect(html).toContain('data-hz-topology-detail-kind="edge"');
+    expect(html).toContain('data-hz-topology-detail-surface="flush"');
+    expect(html).toContain('data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"');
+    expect(html).toContain('data-hz-topology-detail-identity-owner="hertzbeat-ui-detail-identity"');
+    expect(html).toContain('data-hz-topology-detail-subject-id="api-warehouse"');
+    expect(html).toContain('data-hz-topology-detail-source-id="svc-api"');
+    expect(html).toContain('data-hz-topology-detail-target-id="db-warehouse"');
+    expect(html).toContain('data-hz-topology-detail-relation-type="trace-call"');
+    expect(html).toContain('data-hz-topology-detail-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-detail-density="graph-first"');
+    expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
+    expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-header-owner="hertzbeat-ui-detail-header"');
+    expect(html).toContain('data-hz-topology-detail-eyebrow-owner="hertzbeat-ui-detail-eyebrow"');
+    expect(html).toContain('data-hz-topology-detail-title-owner="hertzbeat-ui-detail-title"');
+    expect(html).toContain('data-hz-topology-detail-subtitle-owner="hertzbeat-ui-detail-subtitle"');
+    expect(html).toContain('data-hz-ui-lab-topology-detail-boundary="shared"');
+    expect(html).toContain('data-hz-topology-detail-boundary="context"');
+    expect(html).toContain('data-hz-topology-detail-boundary-owner="hertzbeat-ui-detail-boundary"');
+    expect(html).toContain('data-hz-topology-detail-boundary-copy-owner="hertzbeat-ui-detail-boundary-copy"');
+    expect(html).toContain('data-hz-topology-detail-fact-group-owner="hertzbeat-ui-detail-fact-group"');
+    expect(html).toContain('data-hz-topology-detail-fact-density="compressed"');
+    expect(html).toContain('data-hz-topology-detail-fact-owner="hertzbeat-ui-detail-fact"');
+    expect(html).toContain('data-hz-topology-detail-fact-label-owner="hertzbeat-ui-detail-fact-label"');
+    expect(html).toContain('data-hz-topology-detail-fact-value-owner="hertzbeat-ui-detail-fact-value"');
+    expect(html).toContain('data-hz-topology-detail-fact-meta-owner="hertzbeat-ui-detail-fact-meta"');
+    expect(html).toContain('data-hz-topology-detail-fact="request-rate"');
+    expect(html).toContain('data-hz-topology-detail-action-group-owner="hertzbeat-ui-detail-action-group"');
+    expect(html).toContain('data-hz-topology-detail-action-link-owner="hertzbeat-ui-detail-action-link"');
+    expect(html).toContain('data-hz-topology-detail-action-label-owner="hertzbeat-ui-detail-action-label"');
+    expect(html).toContain('data-hz-topology-detail-action="alert-impact"');
+    expect(html).toContain('data-hz-topology-detail-action-copy-owner="hertzbeat-ui-detail-action-copy"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-group-owner="hertzbeat-ui-detail-signal-action-group"');
+    expect(html).toContain('data-hz-topology-detail-signal-label-owner="hertzbeat-ui-detail-signal-label"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-link-owner="hertzbeat-ui-detail-signal-action-link"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-label-owner="hertzbeat-ui-detail-signal-action-label"');
+    expect(html).toContain('data-hz-topology-detail-signal-action="traces"');
+    expect(html).toContain('data-hz-ui-lab-topology-node-detail-drawer="shared"');
+    expect(html).toContain('data-hz-topology-detail-kind="node"');
+    expect(html).toContain('data-hz-topology-detail-surface="flush"');
+    expect(html).toContain('data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"');
+    expect(html).toContain('data-hz-topology-detail-subject-id="svc-checkout"');
+    expect(html).toContain('data-hz-topology-detail-entity-type="service"');
+    expect(html).toContain('data-hz-topology-detail-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-detail-density="graph-first"');
+    expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
+    expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-header-owner="hertzbeat-ui-detail-header"');
+    expect(html).toContain('data-hz-topology-detail-eyebrow-owner="hertzbeat-ui-detail-eyebrow"');
+    expect(html).toContain('data-hz-topology-detail-title-owner="hertzbeat-ui-detail-title"');
+    expect(html).toContain('data-hz-topology-detail-subtitle-owner="hertzbeat-ui-detail-subtitle"');
+    expect(html).toContain('data-hz-ui-lab-topology-node-detail-boundary="shared"');
+    expect(html).toContain('data-hz-topology-detail-boundary-copy-owner="hertzbeat-ui-detail-boundary-copy"');
+    expect(html).toContain('data-hz-topology-detail-fact-group-owner="hertzbeat-ui-detail-fact-group"');
+    expect(html).toContain('data-hz-topology-detail-fact-owner="hertzbeat-ui-detail-fact"');
+    expect(html).toContain('data-hz-topology-detail-fact-label-owner="hertzbeat-ui-detail-fact-label"');
+    expect(html).toContain('data-hz-topology-detail-fact-value-owner="hertzbeat-ui-detail-fact-value"');
+    expect(html).toContain('data-hz-topology-detail-fact-meta-owner="hertzbeat-ui-detail-fact-meta"');
+    expect(html).toContain('data-hz-topology-detail-fact="health"');
+    expect(html).toContain('data-hz-topology-detail-action-group-owner="hertzbeat-ui-detail-action-group"');
+    expect(html).toContain('data-hz-topology-detail-action-link-owner="hertzbeat-ui-detail-action-link"');
+    expect(html).toContain('data-hz-topology-detail-action-label-owner="hertzbeat-ui-detail-action-label"');
+    expect(html).toContain('data-hz-topology-detail-action="entity"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-group-owner="hertzbeat-ui-detail-signal-action-group"');
+    expect(html).toContain('data-hz-topology-detail-signal-label="signals"');
+    expect(html).toContain('data-hz-topology-detail-signal-label-owner="hertzbeat-ui-detail-signal-label"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-link-owner="hertzbeat-ui-detail-signal-action-link"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-label-owner="hertzbeat-ui-detail-signal-action-label"');
+    expect(html).toContain('data-hz-topology-detail-signal-action="metrics"');
+    expect(html).toContain('data-hz-ui-lab-topology-action-link="shared"');
+    expect(html).toContain('data-hz-topology-action-link-spacing="inset"');
+    expect(html).toContain('data-hz-ui-lab-topology-evidence-list="shared"');
+    expect(html).toContain('data-hz-ui="topology-evidence-list"');
+    expect(html).toContain('data-hz-topology-primitive="evidence-list"');
+    expect(html).toContain('data-hz-topology-evidence-list-kind="impact-timeline"');
+    expect(html).toContain('data-hz-topology-evidence-list-boundary="companion-timeline"');
+    expect(html).toContain('data-hz-topology-evidence-list-boundary-owner="hertzbeat-ui-evidence-list-boundary"');
+    expect(html).toContain('data-hz-topology-evidence-header-owner="hertzbeat-ui-evidence-list-header"');
+    expect(html).toContain('data-hz-topology-evidence-title-owner="hertzbeat-ui-evidence-list-title"');
+    expect(html).toContain('data-hz-topology-evidence-copy-owner="hertzbeat-ui-evidence-list-copy"');
+    expect(html).toContain('data-hz-topology-evidence-count-owner="hertzbeat-ui-evidence-list-count"');
+    expect(html).toContain('data-hz-topology-evidence-item="activity:901"');
+    expect(html).toContain('data-hz-topology-evidence-item-owner="hertzbeat-ui-evidence-list-item"');
+    expect(html).toContain('data-hz-topology-evidence-item-label-owner="hertzbeat-ui-evidence-list-item-label"');
+    expect(html).toContain('data-hz-topology-evidence-item-value-owner="hertzbeat-ui-evidence-list-item-value"');
+    expect(html).toContain('data-hz-topology-evidence-item-meta-owner="hertzbeat-ui-evidence-list-item-meta"');
+    expect(html).toContain('data-hz-ui="topology-metric-table"');
+    expect(html).toContain('data-hz-topology-primitive="metric-table"');
+    expect(html).toContain('data-hz-topology-metric-rows="3"');
+    expect(html).toContain('data-hz-ui-lab-topology-metric-table="shared"');
+    expect(html).toContain('data-hz-topology-metric-table-density="graph-first"');
+    expect(html).toContain('data-hz-topology-metric-table-density-owner="hertzbeat-ui-metric-table-density"');
+    expect(html).toContain('data-hz-topology-metric-table-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-metric-table-row-density="compressed-red"');
+    expect(html).toContain('data-hz-topology-metric-table-boundary="flush"');
+    expect(html).toContain('data-hz-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"');
+    expect(html).toContain('data-hz-topology-metric-table-header-owner="hertzbeat-ui-metric-table-header"');
+    expect(html).toContain('data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"');
+    expect(html).toContain('data-hz-topology-metric-table-interaction="row-select-detail"');
+    expect(html).toContain('data-hz-ui-lab-topology-metric-table-scale-selection="edge-sync"');
+    expect(html).toContain('data-hz-ui-lab-topology-metric-table-scale-selection-owner="hertzbeat-ui-metric-table-scale-selection"');
+    expect(html).toContain('data-hz-topology-edge-row="scale-edge-420-421"');
+    expect(html).toContain('data-hz-topology-edge-selected="true"');
+    expect(html).toContain('data-hz-topology-metric-table-route-owner="hertzbeat-ui-metric-table-route"');
+    expect(html).toContain('data-hz-topology-metric-table-badge-owner="hertzbeat-ui-metric-table-badge"');
+    expect(html).toContain('data-hz-topology-metric-table-cell-owner="hertzbeat-ui-metric-table-cell"');
+    expect(html).toContain('data-hz-topology-metric-table-value-owner="hertzbeat-ui-metric-table-value"');
+    expect(html).toContain('data-hz-topology-metric-table-label-owner="hertzbeat-ui-metric-table-label"');
+    expect(html).toContain('data-hz-topology-edge-action="scale-edge-420-421"');
+    expect(html).toContain('data-hz-topology-metric-table-action-owner="hertzbeat-ui-metric-table-action"');
+    expect(uiLabPageSource).toContain('handleTopologyG6ScaleLabTableRowSelect');
+    expect(uiLabPageSource).toContain("setTopologyG6ScaleLabSelection({ edgeId: row.id, source: 'table-row-click' });");
+    expect(html).toContain('Inspect edge');
+    expect(html).toContain('Topology RED ranking');
+    expect(html).toContain('data-hz-ui-lab-topology-empty-state="shared"');
+    expect(html).toContain('data-hz-ui="topology-empty-state"');
+    expect(html).toContain('data-hz-topology-primitive="empty-state"');
+    expect(html).toContain('data-hz-topology-empty-boundary="flush"');
+    expect(html).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
+    expect(html).toContain('data-hz-topology-empty-scope-owner="hertzbeat-ui-empty-scope"');
+    expect(html).toContain('data-hz-topology-empty-environment="prod"');
+    expect(html).toContain('data-hz-topology-empty-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-empty-relation-type="trace-call"');
+    expect(html).toContain('data-hz-topology-empty-focus-entity-id="service:commerce/checkout"');
+    expect(html).toContain('data-hz-topology-empty-depth="2"');
+    expect(html).toContain('data-hz-topology-empty-result-count="0"');
+    expect(html).toContain('data-hz-topology-empty-evidence-sources="trace relation monitor incident"');
+    expect(html).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
+    expect(html).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
+    expect(html).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
+    expect(html).toContain('data-hz-topology-empty-source-owner="hertzbeat-ui-empty-source"');
+    expect(html).toContain('data-hz-topology-empty-time-scope-owner="hertzbeat-ui-empty-time-scope"');
+    expect(html).toContain('No topology evidence');
+    expect(html).toContain('data-hz-ui-lab-topology-degraded-state="shared"');
+    expect(html).toContain('data-hz-topology-empty-kind="degraded"');
+    expect(html).toContain('data-hz-topology-empty-source="Topology API unavailable"');
+    expect(html).toContain('data-hz-topology-empty-evidence-sources="api greptime trace relation"');
+    expect(html).toContain('Topology API unavailable');
+    expect(html).toContain('data-hz-ui-lab-topology-loading-state="shared"');
+    expect(html).toContain('data-hz-ui="topology-loading-state"');
+    expect(html).toContain('data-hz-topology-primitive="loading-state"');
+    expect(html).toContain('data-hz-topology-loading-boundary="flush"');
+    expect(html).toContain('data-hz-topology-loading-scope-owner="hertzbeat-ui-loading-scope"');
+    expect(html).toContain('data-hz-topology-loading-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-loading-evidence-sources="api greptime trace relation"');
+    expect(html).toContain('Loading topology evidence');
+    expect(html).toContain('data-hz-ui="trace-list"');
+    expect(html).toContain('Recent traces');
+    expect(html).toContain('data-hz-trace-row="trace-1428"');
+    expect(html).toContain('data-hz-ui="trace-detail-drawer"');
+    expect(html).toContain('data-hz-trace-detail-id="trace-1428"');
+    expect(html).toContain('Trace evidence');
+    expect(html).toContain('data-hz-ui="trace-event-timeline"');
+    expect(html).toContain('Span events');
+    expect(html).toContain('mysql.connection.retry');
+    expect(html).toContain('data-hz-ui="trace-selection-summary"');
+    expect(html).toContain('Selected span');
+    expect(html).toContain('data-hz-selected-trace-span="collector"');
+    expect(html).toContain('data-hz-trace-event-selected="true"');
+    expect(html).toContain('aria-label="Select trace span scheduler"');
+    expect(html).toContain('data-hz-waterfall-scale="global"');
+    expect(html).toContain('data-hz-span-link="api-&gt;collector"');
+    expect(html).toContain('data-hz-span-event-marker="retry"');
+    expect(html).toContain('data-hz-span-critical-path="true"');
+    expect(html).toContain('data-hz-ui="log-volume-chart"');
+    expect(html).toContain('Log volume');
+    expect(html).toContain('data-hz-ui="log-level-distribution"');
+    expect(html).toContain('Log level distribution');
+    expect(html).toContain('data-hz-ui="trace-latency-distribution"');
+    expect(html).toContain('Trace latency distribution');
+    expect(html).toContain('data-hz-ui="trace-span-table"');
+    expect(html).toContain('Span table');
+    expect(html).toContain('data-hz-ui="state-notice"');
+    expect(html).toContain('data-hz-state-tone="warning"');
+    expect(html).toContain('data-hz-state-variant="embedded"');
+    expect(html).toContain('Query partially delayed');
+    expect(html).toContain('data-hz-ui="mutation-bar"');
+    expect(html).toContain('data-hz-mutation-status="dirty"');
+    expect(html).toContain('data-hz-dirty-field="app-mysql.yml"');
+    expect(html).toContain('data-hz-validation-issue="schema-valid"');
+    expect(html).toContain('data-hz-ui="inline-feedback"');
+    expect(html).toContain('data-hz-ui="danger-confirm"');
+    expect(html).toContain('aria-label="Open dangerous action Reset YAML draft"');
+    expect(html).toContain('data-hz-ui-lab-monitor-delete-confirm="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-delete-page-recovery="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-delete-failure-closes-dialog="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-delete-confirm-closable="angular-nz-closable-false"');
+    expect(html).toContain('data-hz-ui-lab-monitor-delete-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(html).toContain('data-monitor-delete-confirm-closable-contract="angular-nz-closable-false"');
+    expect(html).toContain('data-monitor-delete-confirm-ok-contract="angular-nz-ok-danger-primary"');
+    expect(html).toContain('data-monitor-delete-confirm-modal-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('Delete monitors');
+    expect(html).toContain('data-hz-ui="batch-toolbar"');
+    expect(html).toContain('data-hz-batch-selection-count="2"');
+    expect(html).toContain('data-hz-ui-lab-monitor-batch-response-filter="angular-status-filtered-selection"');
+    expect(html).toContain('data-monitor-batch-response-filter-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-hz-ui-lab-monitor-batch-more-menu="angular-toolbar-ellipsis-menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-hz-batch-overflow-mode="angular-ellipsis-menu"');
+    expect(html).toContain('data-hz-batch-overflow-trigger="angular-ellipsis-menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-trigger="angular-ellipsis-menu"');
+    expect(html).toContain('data-hz-batch-overflow-panel="angular-nz-dropdown-menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-panel="angular-nz-dropdown-menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-clearance="floating-overlay-no-table-crop"');
+    expect(html).toContain('data-hz-batch-action="enable"');
+    expect(html).toContain('data-hz-batch-action="pause"');
+    expect(html).toContain('data-hz-batch-action-presentation="menu"');
+    expect(html).toContain('data-monitor-batch-more-menu-action="enable"');
+    expect(html).toContain('data-monitor-batch-more-menu-action="pause"');
+    expect(html).toContain('data-monitor-batch-more-menu-action="import"');
+    expect(html).toContain('data-monitor-batch-response-filter="angular-status-filtered-selection"');
+    expect(html).toContain('data-monitor-batch-response-eligible-status="paused"');
+    expect(html).toContain('data-monitor-batch-response-eligible-status="active"');
+    expect(html).toContain('data-monitor-batch-response-action-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-hz-batch-action="export-type"');
+    expect(html).toContain('data-hz-ui-lab-monitor-export-type-trigger="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-export-type-trigger-flow="angular-trigger-type-modal-before-download"');
+    expect(html).toContain('data-monitor-export-type-trigger-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-hz-batch-action="export-all"');
+    expect(html).toContain('data-hz-ui-lab-monitor-export-all-trigger="shared"');
+    expect(uiLabPageSource).toContain('<HzExportTypeDialog');
+    expect(uiLabPageSource).toContain('data-hz-ui-lab-monitor-export-type-dialog="shared"');
+    expect(uiLabPageSource).toContain('data-monitor-export-type-dialog-owner="hertzbeat-ui-export-type-dialog"');
+    expect(uiLabPageSource).toContain('data-monitor-export-type-dialog-contract="angular-nz-modal-600-no-footer"');
+    expect(uiLabPageSource).toContain('data-monitor-export-type-dialog-flow="angular-trigger-type-modal-before-download"');
+    expect(uiLabPageSource).toContain("'data-hz-ui-lab-monitor-export-type-option': 'json'");
+    expect(uiLabPageSource).toContain("'data-hz-ui-lab-monitor-export-type-option': 'excel'");
+    expect(html).toContain('data-hz-ui-lab-monitor-import-feedback="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-import-clears-selection="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-import-failure-retains-selection="shared"');
+    expect(html).toContain('data-monitor-import-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('Import [monitors-prod.yml] submitted');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-import-export="shared"');
+    expect(html).toContain('data-alert-setting-batch-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-alert-setting-import-export-contract="angular-import-export"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-export-success="angular-download-closes-dialog-no-toast"');
+    expect(html).toContain('data-alert-setting-export-success-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-export-loading="angular-selected-type-only"');
+    expect(html).toContain('data-alert-setting-export-loading-owner="route-action-feedback-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-search-translation="angular-app-entry-search"');
+    expect(html).toContain('data-alert-setting-search-translation-contract="angular-app-entry-search"');
+    expect(html).toContain('data-alert-setting-search-translation-owner="alert-setting-query-state"');
+    expect(html).toContain('data-alert-setting-search-translation-source="/apps/defines"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-pagination="angular-nz-table-server"');
+    expect(html).toContain('data-alert-setting-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-alert-setting-pagination-contract="angular-page-index-size"');
+    expect(html).toContain('data-alert-setting-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-alert-setting-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-export-trigger="shared"');
+    expect(html).toContain('data-alert-setting-export-trigger-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-import-trigger="shared"');
+    expect(html).toContain('data-alert-setting-import-trigger-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-alert-setting-delete-trigger-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-import-feedback="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-entity-batch="shared"');
+    expect(html).toContain('data-alert-center-entity-batch-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-alert-center-batch-toolbar="selected-entity-alerts"');
+    expect(html).toContain('data-alert-center-batch-select-page="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-alert-center-batch-action="acknowledge-selected"');
+    expect(html).toContain('data-alert-center-batch-action="unacknowledge-selected"');
+    expect(html).toContain('data-alert-center-batch-action="resolve-selected"');
+    expect(html).toContain('data-alert-center-batch-action="reopen-selected"');
+    expect(html).toContain('data-alert-center-batch-action="silence-selected"');
+    expect(html).toContain('data-alert-center-batch-action="inhibit-selected"');
+    expect(html).toContain('data-alert-center-batch-dialog-source="selected-groups"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-rule-submit="quick-dialog-submit"');
+    expect(html).toContain('data-alert-center-rule-submit-owner="alert-center-quick-dialog"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-rule-create-feedback="angular-new-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-rule-create-feedback="angular-new-fail"');
+    expect(html).toContain('data-alert-center-rule-create-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-rule-selection-count="angular-group-count"');
+    expect(html).toContain('data-alert-center-rule-selection-count-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-inhibit-defaults="angular-drop-severity-equal-allowlist"');
+    expect(html).toContain('data-alert-center-inhibit-defaults-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-batch-confirm="angular-status-confirm"');
+    expect(html).toContain('data-alert-center-batch-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-row-delete-confirm="angular-single-delete-confirm"');
+    expect(html).toContain('data-alert-center-row-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-rule-validation="angular-required-before-submit"');
+    expect(html).toContain('data-alert-center-rule-validation-owner="alert-center-quick-dialog"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-response-return="angular-response-result"');
+    expect(html).toContain('data-alert-center-response-return-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-noise-action-label="angular-view-or-create"');
+    expect(html).toContain('data-alert-noise-control-action-label="angular-view-or-create"');
+    expect(html).toContain('data-alert-noise-control-action-label-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-sse-refresh="angular-alert-event-refresh"');
+    expect(html).toContain('data-alert-center-sse-owner="hertzbeat-ui-header-realtime-notice"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-sse-highlight="angular-new-alert"');
+    expect(html).toContain('data-alert-center-sse-highlight-owner="route-alert-card"');
+    expect(html).toContain('data-alert-group-realtime-state="new"');
+    expect(html).toContain('data-alert-group-realtime-owner="angular-new-alert"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-mark-feedback="angular-mark-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-acknowledged-actions="angular-unacknowledge-resolve"');
+    expect(html).toContain('data-alert-center-acknowledged-actions-owner="route-alert-card"');
+    expect(html).toContain('data-alert-group-action-status="acknowledged"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-alert-center-action-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-delete-clamp="angular-update-page-index"');
+    expect(html).toContain('data-alert-center-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-post-action-filter="angular-retain-filter"');
+    expect(html).toContain('data-alert-center-post-action-filter-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-facts-strip="angular-platform-facts-strip"');
+    expect(html).toContain('data-alert-center-facts-strip-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-alert-center-fact="total"');
+    expect(html).toContain('data-alert-center-fact="firing"');
+    expect(html).toContain('data-alert-center-fact="acknowledged"');
+    expect(html).toContain('data-alert-center-fact="resolved"');
+    expect(html).toContain('data-alert-center-fact-owner="hertzbeat-ui-stat-cell"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-annotation-detail="angular-detail-section"');
+    expect(html).toContain('data-alert-card-annotations="angular-detail-section"');
+    expect(html).toContain('data-alert-card-annotations-owner="route-alert-card"');
+    expect(html).toContain('data-alert-card-annotation="summary"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-time-detail="angular-first-last-end"');
+    expect(html).toContain('data-alert-card-time-detail="angular-first-last-end"');
+    expect(html).toContain('data-alert-card-time-detail-owner="route-alert-card"');
+    expect(html).toContain('data-alert-card-time-row="first"');
+    expect(html).toContain('data-alert-card-time-row="last"');
+    expect(html).toContain('data-alert-card-time-row="end"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-status-detail="angular-status-section"');
+    expect(html).toContain('data-alert-card-status-detail="angular-status-section"');
+    expect(html).toContain('data-alert-card-status-detail-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-alert-card-status-badge="angular-status-tag"');
+    expect(html).toContain('data-alert-card-status-value="Firing"');
+    expect(html).toContain('data-alert-setting-action-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-alert-setting-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-setting-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-alert-setting-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-no-select-delete="angular-warning"');
+    expect(html).toContain('data-alert-setting-no-select-delete-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-no-select-export="angular-warning"');
+    expect(html).toContain('data-alert-setting-no-select-export-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-enable-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-setting-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-setting-enable-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-setting-enable-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-save-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-save-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-setting-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-setting-save-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-alert-setting-save-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-setting-save-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-import-input="shared"');
+    expect(html).toContain('data-alert-setting-import-input-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('Import [alert-defines.json] submitted');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-import-success="angular-import-success-notification"');
+    expect(html).toContain('data-alert-setting-import-success-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-setting-import-feedback-title="common.notify.import-success"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-import-failure="angular-import-fail-notification"');
+    expect(html).toContain('data-alert-setting-import-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-setting-import-feedback-title="common.notify.import-fail"');
+    expect(html).toContain('data-alert-setting-import-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-export-failure="angular-export-fail-notification"');
+    expect(html).toContain('data-alert-setting-export-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-setting-export-feedback-title="common.notify.export-fail"');
+    expect(html).toContain('data-alert-setting-export-feedback-detail="backend-message"');
+    expect(uiLabPageSource).toContain('data-hz-ui-lab-alert-setting-export-type-dialog="shared"');
+    expect(uiLabPageSource).toContain('data-alert-setting-export-type-dialog-owner="hertzbeat-ui-export-type-dialog"');
+    expect(uiLabPageSource).toContain('data-alert-setting-export-loading-contract="angular-selected-type-only"');
+    expect(uiLabPageSource).toContain("'data-alert-setting-export-loading': 'json-selected-only'");
+    expect(uiLabPageSource).toContain("'data-alert-setting-export-loading': 'excel-selected-only'");
+    expect(uiLabPageSource).toContain("'data-hz-ui-lab-alert-setting-export-type-option': 'json'");
+    expect(uiLabPageSource).toContain("'data-hz-ui-lab-alert-setting-export-type-option': 'excel'");
+    expect(html).toContain('Export all...');
+    expect(html).not.toContain('Export all JSON');
+    expect(html).toContain('data-hz-ui-lab-monitor-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-select-current-page="angular-select-all"');
+    expect(html).toContain('data-alert-group-select-current-page-owner="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-no-select-delete="angular-warning"');
+    expect(html).toContain('data-alert-group-no-select-delete-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-enable-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-group-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-save-feedback="angular-new-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-save-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-group-save-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-edit-load-feedback="angular-edit-fail"');
+    expect(html).toContain('data-alert-group-edit-load-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-required-validation="angular-required-timers"');
+    expect(html).toContain('data-alert-group-required-validation-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-non-negative-validation="angular-nzmin-timers"');
+    expect(html).toContain('data-alert-group-non-negative-validation-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-group-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-alert-group-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-label-save-feedback="angular-new-notify"');
+    expect(html).toContain('data-hz-ui-lab-label-save-feedback="angular-edit-notify"');
+    expect(html).toContain('data-label-save-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-label-save-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-hz-ui-lab-label-save-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-label-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-save-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-label-save-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-label-save-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-label-save-loading="angular-nz-ok-loading"');
+    expect(html).toContain('data-label-save-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-hz-ui-lab-label-save-trim="angular-trim-before-save"');
+    expect(html).toContain('data-label-save-trim-owner="label-manage-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-optional-save-payload="angular-preserve-undefined-optional-fields"');
+    expect(html).toContain('data-label-optional-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-type-save-payload="angular-preserve-implicit-type"');
+    expect(html).toContain('data-label-type-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-display-format="angular-format-label-name-raw-tag-value"');
+    expect(html).toContain('data-label-display-format-owner="label-view-model"');
+    expect(html).toContain('data-hz-ui-lab-label-description-display="angular-truthy-description-raw"');
+    expect(html).toContain('data-label-description-display-owner="label-manage-surface"');
+    expect(html).toContain('data-hz-ui-lab-label-dialog-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-label-dialog-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-label-dialog-width="angular-width-30-percent"');
+    expect(html).toContain('data-label-dialog-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-label-dialog-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-label-dialog-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-label-preview-visibility="angular-name-defined-preview"');
+    expect(html).toContain('data-label-dialog-preview-visibility-owner="route-form-state"');
+    expect(html).toContain('data-hz-ui-lab-label-preview-frame="angular-inline-tag-no-extra-frame"');
+    expect(html).toContain('data-label-dialog-preview-frame-owner="route-form-field-grid"');
+    expect(html).toContain('data-label-dialog-preview-chrome-mode="angular-tag-only-no-extra-frame"');
+    expect(html).toContain('data-label-dialog-preview-chrome-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-hz-ui-lab-label-edit-reference="angular-edit-direct-reference"');
+    expect(html).toContain('data-label-edit-reference-contract="angular-edit-direct-reference"');
+    expect(html).toContain('data-label-edit-reference-owner="route-form-state"');
+    expect(html).toContain('data-hz-ui-lab-label-copy-feedback="angular-copy-notify"');
+    expect(html).toContain('data-hz-ui-lab-label-copy-feedback="angular-copy-fail"');
+    expect(html).toContain('data-label-copy-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-label-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-confirm-state="angular-modal-confirm-state"');
+    expect(html).toContain('data-label-delete-confirm-state-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-confirm-close="angular-close-before-delete-result"');
+    expect(html).toContain('data-label-delete-confirm-close-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-label-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-label-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-label-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-label-delete-query="angular-repeated-ids-query"');
+    expect(html).toContain('data-label-delete-query-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-label-load-failure-owner="label-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-card-loading="angular-table-loading"');
+    expect(html).toContain('data-label-card-loading-owner="label-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-label-name-validation="angular-required-before-submit"');
+    expect(html).toContain('data-label-name-validation-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-name-validation-trigger="angular-ok-marks-dirty"');
+    expect(html).toContain('data-label-name-validation-trigger-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-name-validation-raw="angular-required-before-trim"');
+    expect(html).toContain('data-label-name-validation-raw-owner="route-form-contract"');
+    expect(html).toContain('data-hz-ui-lab-label-query="angular-load-all-labels"');
+    expect(html).toContain('data-label-query-owner="label-query-state"');
+    expect(html).toContain('data-hz-ui-lab-label-query-param-order="angular-page-index-size-type-search"');
+    expect(html).toContain('data-label-query-param-order-contract="angular-page-index-size-type-search"');
+    expect(html).toContain('data-label-query-param-order-owner="label-query-state"');
+    expect(html).toContain('data-hz-ui-lab-label-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-label-search-submit-owner="cold-search-row"');
+    expect(html).toContain('data-hz-ui-lab-label-card-grid="angular-card-grid"');
+    expect(html).toContain('data-label-card-grid-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-label-card-column-contract="nz-xs-12-sm-8-md-6-lg-4"');
+    expect(html).toContain('data-hz-ui-lab-label-card="angular-card"');
+    expect(html).toContain('data-label-card-shell="angular-card"');
+    expect(html).toContain('data-label-card-size="small"');
+    expect(html).toContain('data-hz-ui-lab-label-monitor-handoff="angular-routerlink-monitors-labels"');
+    expect(html).toContain('data-label-monitor-handoff-owner="next-monitor-query-link"');
+    expect(html).toContain('data-hz-ui-lab-label-color="angular-render-label-color"');
+    expect(html).toContain('data-label-color-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-hz-label-color-token="geekblue"');
+    expect(html).toContain('data-label-card-description-display="angular-truthy-description-raw"');
+    expect(html).toContain('data-label-card-description-display-owner="label-manage-surface"');
+    expect(html).toContain('data-hz-ui-lab-plugin-enable-feedback="angular-edit-notify"');
+    expect(html).toContain('data-plugin-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-enable-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-plugin-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-enable-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-plugin-enable-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-plugin-enable-optimistic="angular-toggle-mutates-row-before-put"');
+    expect(html).toContain('data-plugin-enable-optimistic-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-table-loading="angular-load-plugins-table"');
+    expect(html).toContain('data-plugin-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-plugin-toggle-loading-owner="angular-nz-table-loading"');
+    expect(html).toContain('data-hz-ui-lab-plugin-selection-reset="angular-load-clears-selection"');
+    expect(html).toContain('data-plugin-selection-reset-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-toolbar-delete-menu="angular-toolbar-ellipsis-delete"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-delete-selected-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-ui-lab-plugin-row-delete-menu="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-plugin-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-row-delete-menu-open="false"');
+    expect(html).toContain('data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-row-delete-menu-panel-open="false"');
+    expect(html).toContain('data-plugin-delete-one-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-warning="angular-no-select-warning"');
+    expect(html).toContain('data-plugin-delete-warning-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-plugin-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-plugin-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-plugin-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-plugin-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-query="angular-repeated-ids-query"');
+    expect(html).toContain('data-plugin-delete-query-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-delete-clamp="angular-update-page-index"');
+    expect(html).toContain('data-plugin-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-table-columns="angular-five-column-edit-actions"');
+    expect(html).toContain('data-plugin-table-columns-owner="angular-nz-table"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-action-visibility="angular-paramcount-zero-only"');
+    expect(html).toContain('data-plugin-param-action-visibility-owner="plugin-view-model"');
+    expect(html).toContain('data-hz-ui-lab-plugin-table-stable-height="viewport-fill-on-empty-or-short"');
+    expect(html).toContain('data-plugin-table-stable-height-owner="route-layout-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-plugin-load-failure-owner="plugin-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-validation="angular-required-before-submit"');
+    expect(html).toContain('data-plugin-upload-validation-owner="route-validation-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-invalid-submit="angular-mark-dirty-keep-open"');
+    expect(html).toContain('data-plugin-upload-invalid-submit-owner="route-validation-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-cancel="angular-cancel-preserves-form"');
+    expect(html).toContain('data-plugin-upload-cancel-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-cancel-pending="angular-cancel-allowed-during-ok-loading"');
+    expect(html).toContain('data-plugin-upload-cancel-pending-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-file-remove="angular-nz-remove-clears-jar"');
+    expect(html).toContain('data-plugin-upload-file-remove-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-plugin-upload-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-width="angular-width-30-percent"');
+    expect(html).toContain('data-plugin-upload-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-field-layout="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-field-layout-contract="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-plugin-upload-field-layout="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-label-span="8"');
+    expect(html).toContain('data-plugin-upload-control-span="14"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-status-control="angular-nz-switch"');
+    expect(html).toContain('data-plugin-upload-status-control-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-payload="angular-form-data"');
+    expect(html).toContain('data-plugin-upload-payload-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-name-payload="angular-raw-name-no-trim"');
+    expect(html).toContain('data-plugin-upload-name-payload-owner="plugin-manage-controller"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-save-lifecycle="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-upload-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-success-reset="angular-reset-form-after-success"');
+    expect(html).toContain('data-plugin-upload-success-reset-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-loading="angular-nz-ok-loading"');
+    expect(html).toContain('data-plugin-upload-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-feedback="angular-new-notify"');
+    expect(html).toContain('data-plugin-upload-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-plugin-upload-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-plugin-upload-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-load="angular-params-define-modal"');
+    expect(html).toContain('data-plugin-param-form-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-empty="angular-empty-params-modal"');
+    expect(html).toContain('data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-width="angular-default-modal-width"');
+    expect(html).toContain('data-plugin-param-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-field-layout="angular-label-7-control-8"');
+    expect(html).toContain('data-plugin-param-field-layout-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-advanced-fields="angular-configurable-and-multi-func"');
+    expect(html).toContain('data-plugin-param-advanced-fields-owner="hertzbeat-ui-advanced-param-controls"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-radio="angular-nz-radio-group"');
+    expect(html).toContain('data-plugin-param-radio-owner="hertzbeat-ui-radio-button-group"');
+    expect(html).toContain('data-plugin-param-radio="angular-nz-radio-group"');
+    expect(html).toContain('data-plugin-param-radio-field="authType"');
+    expect(html).toContain('data-hz-ui="radio-button-group"');
+    expect(html).toContain('data-hz-radio-button-option="basic"');
+    expect(html).toContain('data-hz-radio-button-checked="true"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-textarea="angular-textarea-rows-8"');
+    expect(html).toContain('data-plugin-param-textarea-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-plugin-param-textarea="angular-textarea-rows-8"');
+    expect(html).toContain('data-plugin-param-textarea-rows="8"');
+    expect(html).toContain('rows="8"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-key-value="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-configurable-field="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-key-value-editor="headers"');
+    expect(html).toContain('data-plugin-param-key-value-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-hz-ui="key-value-editor"');
+    expect(html).toContain('data-plugin-param-key-value-input="key"');
+    expect(html).toContain('data-plugin-param-key-value-input="value"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-labels="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-labels-editor="labels"');
+    expect(html).toContain('data-plugin-param-labels-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-plugin-param-labels-input="key"');
+    expect(html).toContain('data-plugin-param-labels-input="value"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-plugin-param-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-plugin-param-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-pending-editable="angular-controls-remain-enabled"');
+    expect(html).toContain('data-plugin-param-pending-editable="angular-controls-remain-enabled"');
+    expect(html).toContain('data-plugin-param-pending-editable-owner="angular-param-modal"');
+    expect(html).toContain('data-cold-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-cold-label-selector-record-row="service:checkout"');
+    expect(html).toContain('data-cold-label-selector-key-input="searchable-key"');
+    expect(html).toContain('data-cold-label-selector-value-input="searchable-value"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-metrics-field="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-metrics-field-editor="metrics"');
+    expect(html).toContain('data-plugin-param-metrics-field-owner="hertzbeat-ui-configurable-field-editor"');
+    expect(html).toContain('data-hz-ui="configurable-field-editor"');
+    expect(html).toContain('data-hz-configurable-field-input="field"');
+    expect(html).toContain('data-hz-configurable-field-input="unit"');
+    expect(html).toContain('data-hz-configurable-field-input="type"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="field"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="unit"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="type"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-multi-func-input="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-multi-func-input="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-multi-func-type="array"');
+    expect(html).toContain('data-plugin-param-array="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-array-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-param-multi-func-clear="angular-allow-clear"');
+    expect(html).toContain('data-plugin-param-multi-func-clear-owner="hertzbeat-ui-input-affordance"');
+    expect(html).toContain('data-hz-ui-lab-plugin-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-plugin-search-submit-contract="angular-enter-and-clear"');
+    expect(html).toContain('data-plugin-search-clear-contract="angular-cleared-load"');
+    expect(html).toContain('data-plugin-search-clear-owner="shared-search-row"');
+    expect(html).toContain('data-hz-ui-lab-collector-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-collector-search-submit-contract="angular-enter-and-clear"');
+    expect(html).toContain('data-collector-search-clear-contract="angular-cleared-load"');
+    expect(html).toContain('data-collector-search-clear-owner="shared-search-row"');
+    expect(html).toContain('data-hz-ui-lab-plugin-query-param-order="angular-page-index-size-search"');
+    expect(html).toContain('data-plugin-query-param-order-contract="angular-page-index-size-search"');
+    expect(html).toContain('data-plugin-query-param-order-owner="plugin-query-state"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-plugin-param-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-save="angular-params-post"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-payload="angular-object-values-payload"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-save-lifecycle="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-param-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-param-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-plugin-param-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-param-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-plugin-param-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-plugin-param-save-loading="angular-no-ok-loading"');
+    expect(html).toContain('data-plugin-param-save-loading-owner="angular-modal-ok-contract"');
+    expect(html).toContain('data-hz-ui-lab-plugin-upload-file-list="angular-before-upload-single-replace"');
+    expect(html).toContain('data-plugin-upload-file-list-contract="angular-before-upload-single-replace"');
+    expect(html).toContain('data-plugin-upload-file-list-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-plugin-upload-file-remove="angular-nz-remove-clears-jar"');
+    expect(html).toContain('data-plugin-upload-file-remove-state="selected"');
+    expect(html).toContain('data-hz-ui-lab-plugin-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-plugin-page-jump="shared"');
+    expect(html).toContain('data-plugin-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-warning="angular-no-select-warning"');
+    expect(html).toContain('data-collector-delete-warning-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-collector-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-collector-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-collector-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-collector-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-collector-delete-clamp="angular-update-page-index"');
+    expect(html).toContain('data-collector-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-collector-selection-reset="angular-load-clears-selection"');
+    expect(html).toContain('data-collector-selection-reset-owner="route-state-contract"');
+    expect(html).toContain('data-hz-ui-lab-collector-operate-warning="angular-no-select-online-offline"');
+    expect(html).toContain('data-collector-operate-warning-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-select-all-contract="angular-header-includes-disabled-default"');
+    expect(html).toContain('data-collector-select-all-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-row-delete-menu="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-collector-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-collector-row-delete-menu-open="false"');
+    expect(html).toContain('data-collector-row-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(html).toContain('data-collector-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-collector-row-delete-menu-panel-open="false"');
+    expect(html).toContain('data-collector-delete-one-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-hz-ui-lab-collector-operate-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-collector-operate-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-hz-ui-lab-collector-operate-feedback="angular-operate-notify"');
+    expect(html).toContain('data-collector-operate-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-operate-failure="angular-operate-fail-notification"');
+    expect(html).toContain('data-collector-operate-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-operate-feedback-title="common.notify.operate-fail"');
+    expect(html).toContain('data-collector-operate-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-collector-mutation-loading="angular-nz-table-loading"');
+    expect(html).toContain('data-collector-table-loading-owner="angular-nz-table-loading"');
+    expect(html).toContain('data-hz-ui-lab-collector-table-loading="angular-load-collectors-table"');
+    expect(html).toContain('data-collector-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-hz-ui-lab-collector-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-collector-load-failure-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-copy-feedback="angular-copy-success-duration-800"');
+    expect(html).toContain('data-collector-deploy-copy-feedback-duration-ms="800"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-identity="angular-generate-identity"');
+    expect(html).toContain('data-collector-deploy-identity-owner="route-mutation-contract"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-failure="angular-apply-fail-notification"');
+    expect(html).toContain('data-collector-deploy-feedback-title="common.notify.apply-fail"');
+    expect(html).toContain('data-collector-deploy-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-validation="angular-submit-marks-required"');
+    expect(html).toContain('data-collector-deploy-validation-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-validation-raw="angular-required-before-trim"');
+    expect(html).toContain('data-collector-deploy-validation-raw-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-loading="angular-nz-ok-loading"');
+    expect(html).toContain('data-collector-deploy-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-collector-deploy-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-close-reset="angular-close-clears-name"');
+    expect(html).toContain('data-collector-deploy-close-reset-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-close-pending-result="angular-close-ignores-late-generate"');
+    expect(html).toContain('data-collector-deploy-close-pending-result-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-width="angular-width-45-percent"');
+    expect(html).toContain('data-collector-deploy-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-name-lock="angular-disable-after-identity"');
+    expect(html).toContain('data-collector-deploy-field-layout-contract="angular-label-7-control-12"');
+    expect(html).toContain('data-collector-deploy-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-collector-deploy-name-lock-owner="angular-ngmodel-input"');
+    expect(html).toContain('data-collector-deploy-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-collector-deploy-label-span="7"');
+    expect(html).toContain('data-collector-deploy-control-span="12"');
+    expect(html).toContain('data-collector-deploy-name-lock="angular-disable-after-identity"');
+    expect(html).toContain('data-collector-deploy-name-disabled="true"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-result-close="angular-danger-close-after-identity"');
+    expect(html).toContain('data-collector-deploy-result-close-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-collector-deploy-result-close-reset="angular-close-clears-name"');
+    expect(html).toContain('data-collector-deploy-result-close-reset-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-docker-shell="angular-docker-shell"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-package-shell="angular-package-shell"');
+    expect(html).toContain('data-collector-deploy-code-owner="hertzbeat-ui-code-editor"');
+    expect(html).toContain('data-hz-ui-lab-collector-deploy-package-link="angular-github-releases-link"');
+    expect(html).toContain('href="https://github.com/apache/hertzbeat/releases"');
+    expect(html).toContain('data-hz-ui-lab-collector-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-collector-page-jump="shared"');
+    expect(html).toContain('data-collector-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-collector-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-delete-clamp="angular-update-page-index"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-rule-create-feedback="angular-new-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-rule-create-feedback="angular-new-fail"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-rule-selection-count="angular-group-count"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-inhibit-defaults="angular-drop-severity-equal-allowlist"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-batch-confirm="angular-status-confirm"');
+    expect(html).toContain('data-hz-ui-lab-alert-center-row-delete-confirm="angular-single-delete-confirm"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-no-select-delete="angular-warning"');
+    expect(html).toContain('data-alert-inhibit-no-select-delete-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-alert-inhibit-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-match-mode="angular-entity-noise-controls"');
+    expect(html).toContain('data-alert-inhibit-match-mode-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-created-outside-matched="angular-authoring-notice"');
+    expect(html).toContain('data-alert-inhibit-created-outside-matched-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-inhibit-entity-prefill="angular-alert-common-labels"');
+    expect(html).toContain('data-alert-inhibit-entity-prefill-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-no-select-delete="angular-warning"');
+    expect(html).toContain('data-alert-silence-no-select-delete-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-alert-silence-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-alert-silence-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-silence-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-alert-silence-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-enable-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-silence-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-enable-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-silence-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-silence-enable-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-silence-enable-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-save-feedback="angular-new-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-save-feedback="angular-edit-notify"');
+    expect(html).toContain('data-alert-silence-save-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-save-failure="angular-new-fail-notification"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-save-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-alert-silence-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-alert-silence-save-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-alert-silence-save-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-alert-silence-save-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-edit-load-feedback="angular-edit-fail"');
+    expect(html).toContain('data-alert-silence-edit-load-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-required-validation="angular-name-labels-only"');
+    expect(html).toContain('data-alert-silence-required-validation-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-match-mode="angular-entity-noise-controls"');
+    expect(html).toContain('data-alert-silence-match-mode-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-created-outside-matched="angular-authoring-notice"');
+    expect(html).toContain('data-alert-silence-created-outside-matched-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-silence-entity-prefill="angular-alert-common-labels"');
+    expect(html).toContain('data-alert-silence-entity-prefill-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-pagination="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-token-normalizer="angular-on-change"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-query="backend-paginated"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-display-names="angular-save-payload"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-single-switch-frame="none"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-single-switch-visual="unframed-switch"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-rule-table-switch-update="angular-edit-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-test-validation="angular-backend-owned"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-default-type="angular-email"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-delete-feedback="angular-delete-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-save-feedback="angular-new-edit-notify"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-receiver-success-next="angular-policy-next"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-edit-load-feedback="angular-edit-fail"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-type-required="angular-required-select"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-telegram-label="angular-template-telegram"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-template-code-editor="shared"');
+    expect(html).toContain('data-hz-code-editor-runtime="codemirror"');
+    expect(html).toContain('data-hz-code-editor-readonly="true"');
+    expect(html).toContain('data-hz-ui-lab-alert-notice-delete-clamp="angular-update-page-index"');
+    expect(html).toContain('data-hz-ui="pagination-bar"');
+    expect(html).toContain('data-hz-pagination-page-size="select-menu"');
+    expect(html).toContain('data-hz-pagination-action="previous"');
+    expect(html).toContain('data-hz-pagination-action="next"');
+    expect(html).toContain('data-hz-ui="toast-stack"');
+    expect(html).toContain('data-hz-toast="template-dirty"');
+    expect(html).toContain('data-hz-ui="loading-state"');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-loading-state="shared"');
+    expect(html).toContain('Loading history');
+    expect(html).toContain('data-hz-ui-lab-monitor-editor-error-state="shared"');
+    expect(html).toContain('data-monitor-editor-route-state-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-monitor-editor-route-state-feedback="error"');
+    expect(html).toContain('data-monitor-editor-route-state-retry-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-route-state="shared"');
+    expect(html).toContain('data-monitor-detail-route-state-owner="hertzbeat-ui-loading-state"');
+    expect(html).toContain('data-monitor-detail-route-state-loading="true"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-error-state="shared"');
+    expect(html).toContain('data-monitor-detail-route-state-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-monitor-detail-route-state-feedback="error"');
+    expect(html).toContain('data-monitor-detail-route-state-retry-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-hz-ui="empty-state"');
+    expect(html).toContain('data-hz-ui-lab-monitor-detail-states="shared"');
+    expect(html).toContain('No matching logs');
+    expect(html).toContain('data-hz-ui="stat-cell"');
+    expect(html).toContain('data-hz-ui-lab-signal-summary-stat="shared"');
+    expect(html).toContain('data-hz-stat-variant="tile"');
+    expect(html).toContain('Collection success');
+    expect(html).toContain('data-hz-ui-lab-dialog-stage-stats="shared"');
+    expect(html).toContain('data-signal-dialog-stage-stats-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-hz-ui="stat-strip"');
+    expect(html).toContain('data-hz-stat-strip-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-hz-stat-strip-columns="4"');
+    expect(html).toContain('data-hz-stat-density="compact"');
+    expect(html).toContain('min-h-[64px]');
+    expect(html).toContain('data-signal-dialog-stage-stat-owner="hertzbeat-ui-stat-cell"');
+    expect(html).toContain('Current span');
+    expect(html).toContain('Error spans');
+    expect(html).toContain('data-hz-ui-lab-dialog-toolbar-chips="shared"');
+    expect(html).toContain('data-signal-dialog-toolbar-chips-owner="hertzbeat-ui-toolbar-chips"');
+    expect(html).toContain('data-hz-ui="chip-group"');
+    expect(html).toContain('data-hz-chip-group-owner="hertzbeat-ui-toolbar-chips"');
+    expect(html).toContain('data-hz-chip-group-boundary="top"');
+    expect(html).toContain('data-signal-dialog-toolbar-badge-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-ui-lab-trace-drawer-meta-item="shared"');
+    expect(html).toContain('data-signal-dialog-toolbar-meta-owner="hertzbeat-ui-dialog-meta-item"');
+    expect(html).toContain('data-hz-ui-lab-dialog-subtitle-meta="shared"');
+    expect(html).toContain('data-signal-dialog-subtitle-owner="hertzbeat-ui-dialog-meta-item"');
+    expect(html).toContain('data-signal-dialog-stage-meta-owner="hertzbeat-ui-dialog-meta-item"');
+    expect(html).toContain('data-hz-dialog-meta-item-owner="hertzbeat-ui-dialog-meta-item"');
+    expect(html).toContain('data-hz-dialog-meta-item-width="trace-id"');
+    expect(html).toContain('data-hz-dialog-meta-item-width="duration"');
+    expect(html).toContain('data-hz-ui-lab-dialog-action-group="shared"');
+    expect(html).toContain('data-signal-dialog-action-group-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-ui="action-group"');
+    expect(html).toContain('data-hz-ui-lab-dialog-body-layout="shared"');
+    expect(html).toContain('data-signal-dialog-body-layout-owner="hertzbeat-ui-dialog-body-layout"');
+    expect(html).toContain('data-hz-ui-lab-dialog-split-layout="shared"');
+    expect(html).toContain('data-signal-dialog-split-layout-owner="hertzbeat-ui-dialog-body-layout"');
+    expect(html).toContain('data-hz-ui-lab-dialog-side-stack="shared"');
+    expect(html).toContain('data-signal-dialog-side-stack-owner="hertzbeat-ui-dialog-body-layout"');
+    expect(html).toContain('data-hz-ui-lab-dialog-waterfall-layout="shared"');
+    expect(html).toContain('data-signal-dialog-waterfall-layout-owner="hertzbeat-ui-dialog-body-layout"');
+    expect(html).toContain('data-hz-ui-lab-dialog-waterfall-side-stack="shared"');
+    expect(html).toContain('data-signal-dialog-waterfall-side-stack-owner="hertzbeat-ui-dialog-body-layout"');
+    expect(html).toContain('data-hz-ui="dialog-body-layout"');
+    expect(html).toContain('data-hz-dialog-body-layout-variant="split-detail"');
+    expect(html).toContain('data-hz-dialog-body-layout-variant="waterfall-detail"');
+    expect(html).toContain('data-hz-dialog-body-layout-variant="side-stack"');
+    expect(html).toContain('Waterfall selected span');
+    expect(html).toContain('traceId · trace-20260523');
+    expect(html).toContain('data-hz-ui-lab-dialog-event-detail="shared"');
+    expect(html).toContain('data-signal-dialog-event-detail-owner="hertzbeat-ui-dialog-event-notice"');
+    expect(html).toContain('data-hz-dialog-event-notice-owner="hertzbeat-ui-dialog-event-notice"');
+    expect(html).toContain('data-hz-dialog-event-notice-layout="grid-cell"');
+    expect(html).toContain('data-hz-ui="dialog-event-text"');
+    expect(html).toContain('data-hz-dialog-event-text-owner="hertzbeat-ui-dialog-event-text"');
+    expect(html).toContain('data-hz-dialog-event-text-variant="copy"');
+    expect(html).toContain('data-hz-dialog-event-text-variant="meta"');
+    expect(html).toContain('data-hz-ui-lab-dialog-event-detail-text-owner="hertzbeat-ui-dialog-event-text"');
+    expect(html).toContain('data-signal-dialog-event-detail-copy="span-event-not-span"');
+    expect(html).toContain('data-signal-dialog-event-detail-action="view-span"');
+    expect(html).toContain('data-hz-ui-lab-signal-panel-header="shared"');
+    expect(html).toContain('data-signal-panel-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-ui-lab-signal-detail-panel-header="shared"');
+    expect(html).toContain('data-signal-detail-panel-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-ui-lab-signal-header-actions-layout="shared"');
+    expect(html).toContain('data-signal-header-actions-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui-lab-log-header-return-action-group="shared"');
+    expect(html).toContain('data-log-header-return-action-group-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-ui-lab-log-header-return-action="shared"');
+    expect(html).toContain('data-log-header-return-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-ui-lab-metrics-header-return-action="shared"');
+    expect(html).toContain('data-metrics-header-return-action-owner="hertzbeat-ui-button-link"');
+    expect(html).toContain('data-metrics-header-return-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-ui-lab-metrics-header-panel-surface="shared"');
+    expect(html).toContain('data-metrics-header-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-ui-lab-metrics-header-layout="shared"');
+    expect(html).toContain('data-metrics-header-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="metrics-header"');
+    expect(html).toContain('data-hz-ui-lab-metrics-header-copy="shared"');
+    expect(html).toContain('data-metrics-header-copy-owner="hertzbeat-ui-workbench-header-copy"');
+    expect(html).toContain('data-hz-ui="workbench-header-copy"');
+    expect(html).toContain('data-hz-workbench-header-copy-density="compact"');
+    expect(html).toContain('data-hz-ui-lab-metrics-header-context-strip="shared"');
+    expect(html).toContain('data-metrics-header-context-strip-owner="hertzbeat-ui-toolbar-chips"');
+    expect(html).toContain('data-hz-chip-group-spacing="top-3"');
+    expect(html).toContain('flex min-w-0 flex-wrap items-center gap-1.5 mt-3');
+    expect(html).toContain('data-hz-ui-lab-metrics-header-context-pill="shared"');
+    expect(html).toContain('data-metrics-header-context-pill-owner="hertzbeat-ui-status-badge"');
+    expect(html).toContain('data-hz-status-badge-layout="context-pill"');
+    expect(html).toContain('data-hz-status-badge-part="label"');
+    expect(html).toContain('data-hz-status-badge-part="value"');
+    expect(html).toContain('max-w-[220px] gap-2');
+    expect(html).toContain('shrink-0 font-semibold text-[#7f8a9d]');
+    expect(html).toContain('truncate font-semibold text-[#dbe5f3]');
+    expect(html).toContain('data-hz-ui="chip-group"');
+    expect(html).toContain('data-hz-chip-group-density="compact"');
+    expect(html).toContain('data-hz-ui-lab-metrics-header-panel-return-action-group="shared"');
+    expect(html).toContain('data-metrics-header-panel-return-action-group-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-ui-lab-signal-time-toolbar-layout="shared"');
+    expect(html).toContain('data-signal-time-toolbar-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui-lab-metrics-header-toolbar-slot="shared"');
+    expect(html).toContain('data-metrics-header-toolbar-slot-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="header-toolbar-slot"');
+    expect(html).toContain('data-hz-ui-lab-metrics-time-toolbar-layout="shared"');
+    expect(html).toContain('data-metrics-time-toolbar-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('grid min-w-0 ml-auto w-full max-w-[1120px] justify-end gap-2 xl:w-auto');
+    expect(html).toContain('data-hz-ui-lab-time-toolbar-actions="shared"');
+    expect(html).toContain('data-hz-ui-lab-metrics-time-toolbar-actions="shared"');
+    expect(html).toContain('data-metrics-time-toolbar-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-ui-lab-log-view-switch-layout="shared"');
+    expect(html).toContain('data-log-view-switch-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui-lab-log-view-switch-actions="shared"');
+    expect(html).toContain('data-log-view-switch-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-ui-lab-log-stream-stage-layout="shared"');
+    expect(html).toContain('data-hz-ui-lab-log-stream-stage-panel-surface="shared"');
+    expect(html).toContain('data-log-stream-stage-panel-surface-owner="hertzbeat-ui-panel-surface"');
+    expect(html).toContain('data-hz-ui-lab-panel-surface-clip="stream-stage"');
+    expect(html).toContain('data-log-stream-stage-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui-lab-log-stream-stage-viewport="shared"');
+    expect(html).toContain('data-hz-ui-lab-log-stream-row="shared"');
+    expect(html).toContain('data-log-stream-row-owner="hertzbeat-ui-log-stream-row"');
+    expect(html).toContain('data-hz-ui="log-stream-live-row"');
+    expect(html).toContain('data-hz-log-stream-row-variant="compact-live-row"');
+    expect(html).toContain('data-hz-ui-lab-log-stream-stage-detail="shared"');
+    expect(html).toContain('data-log-stream-stage-detail-owner="hertzbeat-ui-detail-aside"');
+    expect(html).toContain('data-hz-ui-lab-log-stream-stage-detail-body="shared"');
+    expect(html).toContain('data-log-stream-stage-detail-body-owner="hertzbeat-ui-detail-body-stack"');
+    expect(html).toContain('data-hz-ui-lab-signal-summary-trend-layout="shared"');
+    expect(html).toContain('data-signal-summary-trend-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui-lab-summary-trend-bars="shared"');
+    expect(html).toContain('data-hz-ui-lab-signal-table-detail-layout="shared"');
+    expect(html).toContain('data-signal-table-detail-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui-lab-signal-detail-stack-layout="shared"');
+    expect(html).toContain('data-signal-detail-stack-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui-lab-detail-stack-diagnostics="shared"');
+    expect(html).toContain('data-hz-ui-lab-signal-detail-footer-layout="shared"');
+    expect(html).toContain('data-signal-detail-footer-layout-owner="hertzbeat-ui-workbench-layout"');
+    expect(html).toContain('data-hz-ui-lab-detail-footer-alert-hint="shared"');
+    expect(html).toContain('data-hz-ui-lab-detail-footer-action="shared"');
+    expect(html).toContain('data-hz-ui-lab-detail-footer-action-icon="logs"');
+    expect(html).toContain('data-trace-detail-footer-action-icon-owner="hertzbeat-ui-button-icon"');
+    expect(html).toContain('data-hz-ui="workbench-layout"');
+    expect(html).toContain('data-hz-workbench-layout-variant="header-actions"');
+    expect(html).toContain('data-hz-workbench-layout-variant="time-toolbar"');
+    expect(html).toContain('data-hz-workbench-layout-variant="view-switch"');
+    expect(html).toContain('data-hz-workbench-layout-variant="stream-stage"');
+    expect(html).toContain('data-hz-panel-surface-clip="true"');
+    expect(html).toContain('data-hz-workbench-layout-variant="summary-trend"');
+    expect(html).toContain('data-hz-workbench-layout-variant="detail-stack"');
+    expect(html).toContain('data-hz-workbench-layout-variant="detail-footer"');
+    expect(html).toContain('data-hz-workbench-layout-variant="table-detail"');
+    expect(html).toContain('data-hz-ui-lab-signal-stream-stage-header="shared"');
+    expect(html).toContain('data-signal-stream-stage-header-owner="hertzbeat-ui-panel-header"');
+    expect(html).toContain('data-hz-ui-lab-signal-stream-notice="shared"');
+    expect(html).toContain('data-signal-stream-notice-owner="hertzbeat-ui-state-notice"');
+    expect(html).toContain('data-hz-ui-lab-signal-stream-helper="shared"');
+    expect(html).toContain('data-signal-stream-helper-owner="hertzbeat-ui-state-notice"');
+    expect(html).toContain('data-hz-ui="panel-header"');
+    expect(html).toContain('data-hz-ui="state-notice"');
+    expect(html).toContain('Recent signals');
+    expect(html).toContain('Detail panel');
+    expect(html).toContain('Checkout API');
+    expect(html).toContain('Live log stream');
+    expect(html).toContain('Stream is paused; buffered entries stay visible until you resume.');
+    expect(html).toContain('data-hz-ui="signal-trend-bars"');
+    expect(html).toContain('data-hz-ui-lab-signal-trend-bars="shared"');
+    expect(html).toContain('Signal trend');
+    expect(html).toContain('data-hz-ui-lab-signal-detail-rows="shared"');
+    expect(html).toContain('data-signal-detail-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('Signal evidence');
+    expect(html).toContain('data-hz-ui-lab-signal-dialog-detail-rows="shared"');
+    expect(html).toContain('data-signal-dialog-detail-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('Dialog selection');
+    expect(html).toContain('data-hz-ui-lab-signal-dialog-warning="shared"');
+    expect(html).toContain('data-signal-dialog-warning-owner="hertzbeat-ui-state-notice"');
+    expect(html).toContain('This log was detached from the live stream; inspect the preserved payload before handoff.');
+    expect(html).toContain('data-hz-ui="attribute-diagnostics"');
+    expect(html).toContain('data-hz-ui-lab-attribute-diagnostics="shared"');
+    expect(html).toContain('data-signal-attribute-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(html).toContain('Attribution diagnostics');
+    expect(html).toContain('data-hz-ui-lab-dialog-attribute-diagnostics="shared"');
+    expect(html).toContain('data-signal-dialog-attribute-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"');
+    expect(html).toContain('Dialog attribution');
+    expect(html).toContain('data-hz-ui-lab-signal-empty-state="shared"');
+    expect(html).toContain('data-signal-empty-state-owner="hertzbeat-ui-empty-state"');
+    expect(html).toContain('No matching signals');
+    expect(html).toContain('data-hz-ui-lab-signal-data-table="shared"');
+    expect(html).toContain('data-signal-data-table-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).toContain('data-signal-data-table-row="log-001"');
+    expect(html).toContain('data-hz-ui="bar-gauge"');
+    expect(html).toContain('Collector saturation');
+    expect(html).toContain('data-hz-ui="threshold-rail"');
+    expect(html).toContain('p95 latency');
+    expect(html).toContain('Time series');
+    expect(html).toContain('aria-label="Hide Resource column"');
+    expect(html).toContain('aria-label="Pin Collector column"');
+    expect(html).toContain('data-hz-ui="field-value-actions"');
+    expect(html).toContain('aria-label="Include collector collector-a"');
+    expect(html).toContain('data-hz-ui="field-insights"');
+    expect(html).toContain('Field stats');
+    expect(html).toContain('Show context');
+    expect(html).toContain('Drilldown');
+    expect(html).toContain('data-hz-ui="query-history"');
+    expect(html).toContain('Query history');
+    expect(html).toContain('data-hz-ui="saved-view-compare"');
+    expect(html).toContain('Saved view compare');
+    expect(html).toContain('data-hz-ui="investigation-notes"');
+    expect(html).toContain('Investigation notes');
+    expect(html).toContain('collector-a p95 spike aligns with Greptime write lag');
+    expect(html).toContain('data-hz-ui="command-palette"');
+    expect(html).toContain('Command palette');
+    expect(html).toContain('data-hz-ui="context-handoff"');
+    expect(html).toContain('Context handoff');
+    expect(html).toContain('Entity detail');
+    expect(html).toContain('Alert evidence');
+    expect(html).toContain('data-hz-ui="foundation-guide"');
+    expect(html).toContain('data-hz-foundation-status="ready"');
+    expect(html).toContain('Foundation readiness');
+    expect(html).toContain('data-hz-foundation-section="tokens"');
+    expect(html).toContain('data-hz-ui-lab-monitor-control-band="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-evidence-frame="shared"');
+    expect(html).toContain('data-hz-ui="monitor-control-band"');
+    expect(html).toContain('data-hz-control-band-variant="embedded"');
+    expect(html).toContain('data-hz-ui-lab-monitor-media-frame="shared"');
+    expect(html).toContain('data-hz-evidence-frame-variant="media"');
+    expect(html).toContain('Compare scope');
+    expect(html).toContain('--hz-ui-line-soft');
+    expect(html).toContain('data-hz-foundation-section="forbidden-patterns"');
+    expect(html).toContain('nested-card-shells');
+    expect(html).toContain('data-hz-foundation-section="component-usage"');
+    expect(html).toContain('HzExplorerFrame');
+    expect(html).toContain('data-hz-foundation-section="stop-criteria"');
+    expect(html).toContain('M5 guide readiness');
+    expect(html).toContain('data-hz-ui="inspector-drawer"');
+    expect(html).toContain('data-hz-ui="inspector-drawer-overlay"');
+    expect(html).toContain('data-hz-inspector-open="false"');
+    expect(html).toContain('aria-label="Open selected resource inspector"');
+    expect(html).toContain('data-hz-row-clickable="true"');
+    expect(html).toContain('data-hz-row-selected="true"');
+    expect(html).toContain('Selected resource');
+    expect(html).toContain('Latency p95');
+    expect(html).toContain('Safe actions');
+    expect(html).toContain('data-hz-ui="yaml-workspace"');
+    expect(html).toContain('data-hz-yaml-layout="rail"');
+    expect(html).toContain('data-ui-lab-yaml-column="rail"');
+    expect(html).toContain('data-hz-ui="template-picker"');
+    expect(html).toContain('data-hz-ui-lab-monitor-code-editor-frame="shared"');
+    expect(html).toContain('data-hz-ui-lab-monitor-code-editor="shared"');
+    expect(html).toContain('data-hz-ui="code-editor-frame"');
+    expect(html).toContain('data-hz-code-editor-body="true"');
+    expect(html).toContain('data-hz-code-editor-runtime="codemirror"');
+    expect(html).toContain('data-hz-code-editor-language="json"');
+    expect(html).toContain('data-hz-ui="yaml-editor-shell"');
+    expect(html).toContain('data-hz-yaml-editor-runtime="external"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-route-state="angular-current-app-url-retained"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-theme-init="angular-theme-service-initial"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-theme-switch="angular-nz-switch-code-editor-theme"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-theme-switch-demo="angular-nz-switch-code-editor-theme"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-menu-filter="angular-monitor-select-list-label-only"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-menu-filtered="angular-hide-prometheus-system"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-menu-loading="angular-monitor-select-list-loading"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-menu-loading-demo="angular-monitor-select-list-loading"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-save-failure="angular-apply-fail-notification"');
+    expect(html).toContain('data-setting-define-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-setting-define-save-feedback-title="common.notify.apply-fail"');
+    expect(html).toContain('data-setting-define-save-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-setting-define-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-setting-define-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-setting-define-delete-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-visibility-failure="angular-apply-fail-notification"');
+    expect(html).toContain('data-setting-define-visibility-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-setting-define-visibility-feedback-title="common.notify.apply-fail"');
+    expect(html).toContain('data-setting-define-visibility-feedback-detail="backend-message"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-new-draft="angular-locale-comment-five-newlines"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-new-action="angular-current-app-reset-url-retained"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-menu-select="angular-router-navigate-app-query"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-menu-select-query="angular-replace-with-app-only"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-delete-success-edit-state="angular-preserve-is-editing"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-save-visibility="angular-code-diff-independent-of-editing"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-save-reload="angular-load-app-define-content-after-save"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-startup-reload="angular-startup-load-after-success"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-startup-reload-failure="angular-fire-and-forget"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-visibility-loading="angular-save-loading"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-editor-loading="angular-nz-code-editor-loading"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-editor-loading-demo="angular-nz-code-editor-loading"');
+    expect(html).toContain('data-setting-define-theme-contract="angular-theme-service-initial"');
+    expect(html).toContain('data-setting-define-theme-owner="angular-theme-service"');
+    expect(html).toContain('data-setting-define-theme-mode="dark-ops"');
+    expect(html).toContain('data-setting-define-theme-switch-contract="angular-nz-switch-code-editor-theme"');
+    expect(html).toContain('data-setting-define-theme-switch-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-setting-define-theme-switch-state="vs-dark"');
+    expect(html).toContain('data-setting-define-theme-toggle="angular-nz-switch"');
+    expect(html).toContain('data-setting-define-theme-toggle-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-setting-define-theme-toggle-state="vs-dark"');
+    expect(html).toContain('data-hz-ui="switch"');
+    expect(html).toContain('data-hz-switch-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-setting-define-menu-filter-contract="angular-monitor-select-list-label-only"');
+    expect(html).toContain('data-setting-define-menu-filter-owner="hertzbeat-ui-template-picker"');
+    expect(html).toContain('data-setting-define-menu-filtered-contract="angular-hide-prometheus-system"');
+    expect(html).toContain('data-setting-define-menu-filtered-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-menu-loading-contract="angular-monitor-select-list-loading"');
+    expect(html).toContain('data-setting-define-menu-loading-owner="hertzbeat-ui-template-picker"');
+    expect(html).toContain('data-setting-define-menu-loading="false"');
+    expect(html).toContain('data-setting-define-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-setting-define-load-failure-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-load-failure="none"');
+    expect(html).toContain('data-hz-template-loading-state="angular-monitor-select-list-loading"');
+    expect(html).toContain('data-hz-template-loading-state-owner="hertzbeat-ui-template-picker"');
+    expect(html).toContain('data-setting-define-new-draft-contract="angular-locale-comment-five-newlines"');
+    expect(html).toContain('data-setting-define-new-draft-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-new-draft-state="new-template"');
+    expect(html).toContain('data-setting-define-new-action-contract="angular-current-app-reset-url-retained"');
+    expect(html).toContain('data-setting-define-new-action-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-new-action-state="available"');
+    expect(html).toContain('data-setting-define-menu-select-contract="angular-router-navigate-app-query"');
+    expect(html).toContain('data-setting-define-menu-select-owner="hertzbeat-ui-template-picker"');
+    expect(html).toContain('data-setting-define-menu-select-query-contract="angular-replace-with-app-only"');
+    expect(html).toContain('data-setting-define-menu-select-query-owner="setting-define-page-router"');
+    expect(html).toContain('data-hz-template-filter-contract="angular-monitor-select-list-label-only"');
+    expect(html).toContain('data-hz-template-filter-owner="hertzbeat-ui-template-picker"');
+    expect(html).toContain('data-hz-template-filter-match="label"');
+    expect(html).toContain('data-ui-lab-code-editor="cold-code-editor"');
+    expect(html).toContain('data-cold-code-editor="codemirror"');
+    expect(html).toContain('data-cold-code-editor-language="yaml"');
+    expect(html).toContain('data-cold-code-editor-theme="vs-dark"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-editor-theme="angular-vs-dark"');
+    expect(html).toContain('data-setting-define-editor-theme="vs-dark"');
+    expect(html).toContain('data-setting-define-editor-theme-owner="angular-nz-code-editor-theme"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-editor-options="angular-yaml-vs-folding-automatic-layout"');
+    expect(html).toContain('data-setting-define-editor-option-contract="angular-yaml-vs-folding-automatic-layout"');
+    expect(html).toContain('data-setting-define-editor-option-owner="cold-code-editor"');
+    expect(html).toContain('data-setting-define-editor-loading-contract="angular-nz-code-editor-loading"');
+    expect(html).toContain('data-setting-define-editor-loading-owner="cold-code-editor"');
+    expect(html).toContain('data-setting-define-editor-loading="false"');
+    expect(html).toContain('data-setting-define-editor-loading-save-contract="angular-save-hidden-while-editor-loading"');
+    expect(html).toContain('data-setting-define-editor-folding="true"');
+    expect(html).toContain('data-setting-define-editor-automatic-layout="true"');
+    expect(html).toContain('data-cold-code-editor-folding="true"');
+    expect(html).toContain('data-cold-code-editor-automatic-layout="true"');
+    expect(html).toContain('data-ui-lab-code-editor-loading="cold-code-editor"');
+    expect(html).toContain('data-cold-code-editor-loading="true"');
+    expect(html).toContain('data-cold-code-editor-loading-state="angular-nz-code-editor-loading"');
+    expect(html).toContain('data-cold-code-editor-loading-state-owner="cold-code-editor"');
+    expect(html).toContain('Loading template YAML');
+    expect(html).toContain('data-hz-ui-lab-setting-define-confirm="angular-save-delete-visibility"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-confirm-title="angular-title-copy"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-confirm-closable="angular-nz-closable-false"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-confirm-ok="angular-nz-ok-danger-primary"');
+    expect(html).toContain('data-setting-define-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-setting-define-confirm-closable-contract="angular-nz-closable-false"');
+    expect(html).toContain('data-setting-define-confirm-ok-contract="angular-nz-ok-danger-primary"');
+    expect(html).toContain('data-setting-define-delete-success-edit-state-contract="angular-preserve-is-editing"');
+    expect(html).toContain('data-setting-define-save-visibility-contract="angular-code-diff-independent-of-editing"');
+    expect(html).toContain('data-setting-define-save-reload-contract="angular-load-app-define-content-after-save"');
+    expect(html).toContain('data-setting-define-save-reload-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-save-reload-scope="existing-template"');
+    expect(html).toContain('data-setting-define-startup-reload-contract="angular-startup-load-after-success"');
+    expect(html).toContain('data-setting-define-startup-reload-owner="startup-service"');
+    expect(html).toContain('data-setting-define-startup-reload-scope="save-delete-visibility-success"');
+    expect(html).toContain('data-setting-define-startup-reload-failure-contract="angular-fire-and-forget"');
+    expect(html).toContain('data-setting-define-startup-reload-failure-owner="startup-service"');
+    expect(html).toContain('data-setting-define-template-visibility-loading-contract="angular-save-loading"');
+    expect(html).toContain('data-setting-define-template-visibility-loading-owner="setting-define-controller"');
+    expect(html).toContain('data-hz-confirm-closable="false"');
+    expect(html).toContain('data-hz-confirm-ok-danger="true"');
+    expect(html).toContain('data-hz-confirm-ok-type="primary"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-visibility-action="angular-row-contextual"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-visibility-contract="angular-hide-true-or-undefined-contextual"');
+    expect(html).toContain('data-setting-define-template-visibility-contract="angular-hide-true-or-undefined-contextual"');
+    expect(html).toContain('data-setting-define-template-visibility-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-setting-define-template-visibility-label="MySQL"');
+    expect(html).toContain('data-setting-define-template-visibility-action="hide"');
+    expect(html).toContain('data-setting-define-template-visibility-next-hide="true"');
+    expect(html).toContain('aria-label="Hide template MySQL"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-delete-action-label="angular-current-app-id"');
+    expect(html).toContain('data-hz-ui-lab-setting-define-monitor-link="angular-routerlink-monitors-app"');
+    expect(html).toContain('data-setting-define-monitor-link-contract="angular-routerlink-monitors-app"');
+    expect(html).toContain('data-setting-define-monitor-link-owner="hertzbeat-ui-button-link"');
+    expect(html).toContain('data-setting-define-monitor-link-app="mysql"');
+    expect(html).toContain('href="/monitors?app=mysql"');
+    expect(html).toContain('data-setting-define-delete-action-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-setting-define-delete-action-label="mysql"');
+    expect(html).toContain('Delete mysql');
+    expect(html).toContain('data-hz-ui-lab-setting-define-save-loading="angular-nz-loading"');
+    expect(html).toContain('data-setting-define-save-loading-owner="angular-nz-loading"');
+    expect(html).toContain('data-setting-define-save-pending="true"');
+    expect(html).toContain('data-cold-code-editor-license="codemirror-mit"');
+    expect(html).toContain('data-mocked-codemirror="true"');
+    expect(html).not.toContain('连接、慢查询、可用性和复制状态');
+    expect(html).not.toContain('连接池、事务、锁等待和复制延迟');
+    expect(html).not.toContain('CPU、内存、磁盘、进程和端口');
+    expect(html).toContain('var(--hz-ui-line-soft)');
+    expect(html).toContain('var(--hz-ui-line-faint)');
+    expect(html).toContain('新增监控');
+    expect(html).toContain('Monitor results');
+    expect(html).toContain('2xl:grid-cols-[minmax(0,1fr)_520px]');
+    expect((html.match(/data-hz-ui="status-badge"/g) || []).length).toBeGreaterThanOrEqual(6);
+    expect(html).not.toContain('lg:grid-cols-[174px_minmax(0,1fr)]');
+    expect(html).not.toContain('border-[#242a34]');
+    expect(html).not.toContain('rounded-[16px]');
+    expect(html).not.toContain('rounded-[14px]');
+    expect(html).not.toContain('rounded-[12px]');
+  });
+
+  it('demonstrates the shared thin dark scrollbar baseline instead of the old thick rail', () => {
+    const html = renderUiLabPage();
+
+    expect(html).toContain('data-hz-ui-lab-scrollbar-baseline="thin-dark-platform"');
+    expect(html).toContain('class="hb-scrollbar');
+    expect(html).toContain('data-hz-ui-lab-log-stream-viewport="shared"');
+    expect(html).toContain('data-log-stream-viewport-owner="hertzbeat-ui-scroll-viewport"');
+    expect(html).toContain('data-hz-ui="scroll-viewport"');
+    expect(html).toContain('data-hz-scroll-viewport-variant="log-stream"');
+    expect(globalsCssSource).toContain('--hz-ui-scrollbar-size: 7px;');
+    expect(globalsCssSource).toContain('--hz-ui-scrollbar-thumb: rgba(166, 178, 195, 0.18);');
+    expect(globalsCssSource).toContain('width: var(--hz-ui-scrollbar-size);');
+    expect(globalsCssSource).toContain('height: var(--hz-ui-scrollbar-size);');
+    expect(globalsCssSource).not.toContain('width: 10px;');
+    expect(globalsCssSource).not.toContain('height: 10px;');
+    expect(globalsCssSource).not.toContain('linear-gradient(180deg, hsl(var(--accent)), hsl(var(--secondary)))');
+  });
+
+  it('keeps the time range foundation demo labels on i18n contracts', () => {
+    expect(uiLabPageSource).toContain("import { useI18n } from '@/components/providers/i18n-provider';");
+    expect(uiLabPageSource).toContain('const { t } = useI18n();');
+    expect(uiLabPageSource).toContain("preset: t('time.range.preset')");
+    expect(uiLabPageSource).toContain("applyAria: t('time.range.apply-aria')");
+    expect(uiLabPageSource).toContain("resetAria: t('time.range.reset-aria')");
+    expect(uiLabPageSource).toContain("label: t('ui-lab.time-range.previous-query')");
+    expect(uiLabPageSource).toContain("label: t('ui-lab.time-range.release-window')");
+    expect(uiLabPageSource).not.toContain("applyAria: 'Apply monitor history time range'");
+    expect(uiLabPageSource).not.toContain("resetAria: 'Reset monitor history time range'");
+    expect(uiLabPageSource).not.toContain("label: 'Previous query: now-1h to now'");
+    expect(uiLabPageSource).not.toContain("label: 'Release validation window'");
+  });
+});
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
new file mode 100644
index 0000000000..fc01c95888
--- /dev/null
+++ b/web-next/app/ui-lab/page.tsx
@@ -0,0 +1,11153 @@
+'use client';
+
+import * as React from 'react';
+import Link from 'next/link';
+import type { EChartsOption } from 'echarts';
+import { Activity, ArrowRight, BarChart3, CheckCircle2, Copy as CopyIcon, Database, FileCode2, FileText, GitBranch, HardDrive, HelpCircle, LogOut, MapPin, Megaphone, Minimize2, MoreHorizontal, Network, PauseCircle, Pencil, Play, PlayCircle, Plus, Power, RefreshCw, RotateCcw, Search, Server, SlidersHorizontal, Star, Timer, Trash2, Wrench, X } from 'lucide-react';
+import {
+  HzBarGauge,
+  HzAttributeDiagnostics,
+  HzActionGroup,
+  HzActionWorkbench,
+  HzAiChatModalSurface,
+  HzAboutModalSurface,
+  HzAssistiveMarker,
+  HzBatchToolbar,
+  HzButton,
+  HzButtonIcon,
+  HzButtonLink,
+  HzCheckbox,
+  HzChipGroup,
+  HzCollapsibleSection,
+  HzCommandPalette,
+  HzConfigurableFieldEditor,
+  HzContextHandoff,
+  HzControlStack,
+  HzDataCellStack,
+  HzCodeEditor,
+  HzDataCellText,
+  HzDataMetaText,
+  HzDataTable,
+  HzDialogBodyLayout,
+  HzDialogEventNotice,
+  HzDialogEventText,
+  HzDialogMetaItem,
+  HzDisabledActionShell,
+  HzDetailAside,
+  HzDetailBodyStack,
+  HzDetailRows,
+  HzDangerConfirm,
+  HzConfirmDialog,
+  HzEmptyState,
+  HzExplorerFrame,
+  HzEChartsPanel,
+  HzExportTypeDialog,
+  HzFileInput,
+  HzField,
+  HzFieldInsights,
+  HzFieldValueActions,
+  HzFilterWorkbench,
+  HzFoundationGuide,
+  HzHeaderIconButton,
+  HzHeaderMenuAction,
+  HzHeaderRealtimeNotice,
+  HzLocaleMenuOption,
+  HzPassportLoginActionFrame,
+  HzPassportLoginNotice,
+  HzPassportLoginValidationNotice,
+  HzPassportSessionClearFrame,
+  HzUserMenuAction,
+  HzIconButton,
+  HzIconLink,
+  HzPassportLockSurface,
+  HzIncidentWorkbench,
+  HzHeatmapChart,
+  HzInspectorDrawer,
+  HzInvestigationNotes,
+  HzKeyValueEditor,
+  HzInput,
+  HzLabelTag,
+  HzInlineContextMark,
+  HzInlineFeedback,
+  HzLoadingState,
+  HzLogLevelDistribution,
+  HzLogStreamLiveRow,
+  HzLogStream,
+  HzLogVolumeChart,
+  HzMetricStrip,
+  HzMonitorFavoritePane,
+  HzMonitorFavoriteSurface,
+  HzMonitorFilterBar,
+  HzMonitorBasicCard,
+  HzMonitorBasicSummary,
+  HzMonitorBreadcrumb,
+  HzMonitorDetailConsoleShell,
+  HzMonitorDetailWorkbenchFrame,
+  HzMonitorFullscreenFrame,
+  HzMonitorDetailStage,
+  HzMonitorDetailSignalList,
+  HzMonitorDetailTabLabel,
+  HzMonitorDetailTabPanel,
+  HzMonitorDetailTabSequence,
+  HzMonitorDetailTabs,
+  HzMonitorEditorActionBar,
+  HzMonitorEditorFieldGrid,
+  HzMonitorEditorForm,
+  HzMonitorEditorHeader,
+  HzMonitorEditorSection,
+  HzMonitorHistoryChartCard,
+  HzMonitorHistoryChartGrid,
+  HzMonitorControlBand,
+  HzMonitorEvidenceFrame,
+  HzMonitorSignalBars,
+  HzMonitorStatGrid,
+  HzMonitorIncrementalLoadFooter,
+  HzMonitorMetricCard,
+  HzMonitorMetricCardGrid,
+  HzMonitorMetricFavoriteAction,
+  HzMonitorRealtimeInspector,
+  HzMonitorRowNavigator,
+  HzMonitorRealtimeRowNavigator,
+  HzMonitorRealtimeToolbar,
+  HzMonitorRefreshToolbar,
+  HzMutationBar,
+  HzNumberStepper,
+  HzPaginationBar,
+  HzPanelHeader,
+  HzPanelSection,
+  HzPanelSurface,
+  HzPanelTitleLabel,
+  HzQueryBar,
+  HzQueryActionGroup,
+  HzQueryHistory,
+  HzQueryStatusSelect,
+  HzQueryTokenField,
+  HzRadioButtonGroup,
+  HzResultControls,
+  HzSavedViewCompare,
+  HzScrollViewport,
+  HzSearchFieldFrame,
+  HzSearchFieldIcon,
+  HzSegmentedTabs,
+  HzSignalTrendBars,
+  HzSignalWorkbenchShell,
+  HzSelectableRows,
+  HzSelect,
+  HzStateNotice,
+  HzStatCell,
+  HzStatStrip,
+  HzStatusBadge,
+  HzStatusIncidentHistory,
+  HzStatusTimeline,
+  HzSwitch,
+  HzTableRowActionButton,
+  HzTemplatePicker,
+  HzThresholdRail,
+  HzTimeDistributionChart,
+  HzTimeRangePreviewHandoff,
+  HzTimeRangeToolbar,
+  HzTimeSeriesChart,
+  HzTextarea,
+  HzTrendBar,
+  HzTrendFrame,
+  HzTopologyActionLink,
+  HzTopologyWorkbenchFrame,
+  HzTopologyWorkbenchHeader,
+  HzTopologyWorkbenchGrid,
+  HzTopologyWorkbenchSlot,
+  HzTopologyCanvas,
+  HzTopologyCanvasAnnotation,
+  HzTopologyGraphLayer,
+  HzTopologyCompanionRail,
+  HzTopologyCompanionSection,
+  HzTopologyCompanionJumpList,
+  HzTopologyToolbar,
+  HzTopologyEmptyState,
+  HzTopologyLoadingState,
+  HzTopologyFilterStrip,
+  HzTopologyFocusTrail,
+  HzTopologyGroupPanel,
+  HzTopologyPathSummary,
+  HzTopologyScopeBar,
+  HzTopologyMetricTable,
+  HzTopologyNode,
+  HzTopologyEdge,
+  HzTopologyHoverTooltip,
+  HzTopologySectionLabel,
+  HzTopologyEvidenceList,
+  HzTopologyLegend,
+  HzTopologyDetailDrawer,
+  type HzTopologyMetricRow,
+  type HzTopologyMetricTableRenderWindowFilter,
+  HzUnderlineToggle,
+  HzWorkbenchHeaderCopy,
+  HzWorkbenchLayout,
+  HzServiceDependencyGraph,
+  HzTraceDetailDrawer,
+  HzTraceEventTimeline,
+  HzTraceLatencyDistribution,
+  HzTraceList,
+  HzTraceSpanTable,
+  HzTraceWaterfall,
+  HzToastStack,
+  HzTypePickerDialog,
+  HzYamlWorkspace,
+  type HzFilterBuilderLogic,
+  type HzFilterClause,
+  type HzFilterFacet,
+  type HzFilterFacetGroup,
+  type HzFilterFacetValue,
+  type HzFilterGroup,
+  type HzTimeRangeToolbarValue,
+  type HzTemplateCategory,
+  type HzToastItem
+} from '@hertzbeat/ui';
+import {
+  HzTopologyG6Canvas,
+  buildHzTopologyG6LargeGraphStrategy,
+  buildHzTopologyG6RenderWindow,
+  buildHzTopologyG6ScaleFixture,
+  buildHzTopologyG6ScaleProfile,
+  type HzTopologyG6GraphInput
+} from '@hertzbeat/ui/topology-g6';
+import { AlertNoticeRuleSwitch } from '../../components/pages/alert-notice-rule-fields';
+import { ColdCodeEditor } from '@/components/ui/cold-code-editor';
+import { LabelRecordInput } from '@/components/ui/label-record-input';
+import { SearchRow } from '@/components/ui/search-row';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { SettingsConsoleShell } from '@/components/settings/settings-console-shell';
+import { SettingsDialogField, SettingsDialogForm, SettingsDialogInput, SettingsDialogSelect, SettingsDialogToggle } from '@/components/settings/settings-dialog-form';
+import { SettingsForm, SettingsFormField, SettingsFormInput, SettingsFormSelect } from '@/components/settings/settings-form';
+import { TimeRangeControl } from '@/components/observability/time-range-control';
+
+const monitorTypeCategories: HzTemplateCategory[] = [
+  {
+    id: 'db',
+    label: '数据库监控',
+    items: [
+      {
+        id: 'mysql',
+        label: 'MySQL',
+        description: '连接、慢查询、可用性和复制状态',
+        meta: 'JDBC',
+        icon: <Database size={13} />
+      },
+      {
+        id: 'postgresql',
+        label: 'PostgreSQL',
+        description: '连接池、事务、锁等待和复制延迟',
+        meta: 'JDBC',
+        icon: <Database size={13} />
+      }
+    ]
+  },
+  {
+    id: 'os',
+    label: '操作系统监控',
+    items: [
+      {
+        id: 'linux',
+        label: 'Linux',
+        description: 'CPU、内存、磁盘、进程和端口',
+        meta: 'SSH',
+        icon: <Server size={13} />
+      },
+      {
+        id: 'windows',
+        label: 'Windows',
+        description: 'WMI、服务状态和性能计数器',
+        meta: 'WMI',
+        icon: <HardDrive size={13} />
+      }
+    ]
+  },
+  {
+    id: 'bigdata',
+    label: '大数据监控',
+    items: [
+      {
+        id: 'flink_on_yarn',
+        label: 'Flink on Yarn',
+        description: 'Job、TaskManager、Checkpoint 和队列资源',
+        meta: 'REST',
+        icon: <Activity size={13} />
+      }
+    ]
+  },
+  {
+    id: 'network',
+    label: '网络监控',
+    items: [
+      {
+        id: 'snmp',
+        label: 'SNMP Device',
+        description: '接口、流量、错误包和设备状态',
+        meta: 'SNMP',
+        icon: <Network size={13} />
+      }
+    ]
+  }
+];
+
+const yamlCategories: HzTemplateCategory[] = monitorTypeCategories.map(category => ({
+  ...category,
+  items: category.items.map(item => ({
+    ...item,
+    meta: 'app-' + item.id + '.yml',
+    status:
+      item.id === 'mysql' || item.id === 'linux' ? (
+        <span className="font-mono text-[10px] text-[#9cc9aa]">shown</span>
+      ) : (
+        <span className="font-mono text-[10px] text-[#727b8c]">hidden</span>
+      ),
+    action: (
+      <HzButton
+        size="sm"
+        intent="ghost"
+        aria-label={`${item.id === 'mysql' || item.id === 'linux' ? 'Hide' : 'Show'} template ${item.label}`}
+        title={`${item.id === 'mysql' || item.id === 'linux' ? 'Hide' : 'Show'} template ${item.label}`}
+        data-hz-ui-lab-setting-define-visibility-action="angular-row-contextual"
+        data-hz-ui-lab-setting-define-visibility-contract="angular-hide-true-or-undefined-contextual"
+        data-setting-define-template-visibility-contract="angular-hide-true-or-undefined-contextual"
+        data-setting-define-template-visibility-owner="hertzbeat-ui-button"
+        data-setting-define-template-visibility-label={item.label}
+        data-setting-define-template-visibility-action={item.id === 'mysql' || item.id === 'linux' ? 'hide' : 'show'}
+        data-setting-define-template-visibility-next-hide={item.id === 'mysql' || item.id === 'linux' ? 'true' : 'false'}
+        onClick={event => event.stopPropagation()}
+      >
+        {item.id === 'mysql' || item.id === 'linux' ? 'Hide' : 'Show'}
+      </HzButton>
+    )
+  }))
+}));
+
+const topologyG6LabGraph: HzTopologyG6GraphInput = {
+  nodes: [
+    {
+      id: 'svc-web',
+      label: 'Web',
+      entityType: 'service',
+      health: 'healthy',
+      tone: 'success',
+      focus: 'related',
+      source: 'otlp-trace-call',
+      evidenceBadges: ['trace'],
+      redMetrics: { requestRatePerSecond: 18.6, errorRate: 0.004, latencyP95Ms: 44 }
+    },
+    {
+      id: 'svc-checkout',
+      label: 'Checkout API',
+      entityType: 'service',
+      health: 'warning',
+      tone: 'warning',
+      focus: 'active',
+      source: 'otlp-trace-call',
+      evidenceBadges: ['trace', 'relation'],
+      redMetrics: { requestRatePerSecond: 12.34, errorRate: 0.042, latencyP95Ms: 180 }
+    },
+    {
+      id: 'svc-payment',
+      label: 'Payment API',
+      entityType: 'service',
+      health: 'healthy',
+      tone: 'success',
+      focus: 'related',
+      source: 'otlp-trace-call',
+      evidenceBadges: ['trace'],
+      redMetrics: { requestRatePerSecond: 7.1, errorRate: 0.012, latencyP95Ms: 92 }
+    },
+    {
+      id: 'queue-events',
+      label: 'Events Queue',
+      entityType: 'middleware',
+      health: 'healthy',
+      tone: 'blue',
+      focus: 'normal',
+      source: 'template-dependency',
+      evidenceBadges: ['queue'],
+      redMetrics: { requestRatePerSecond: 5.8, errorRate: 0, latencyP95Ms: 28 }
+    },
+    {
+      id: 'db-orders',
+      label: 'Orders DB',
+      entityType: 'database',
+      health: 'critical',
+      tone: 'danger',
+      focus: 'related',
+      source: 'database-middleware-connection',
+      evidenceBadges: ['db', 'alert'],
+      redMetrics: { requestRatePerSecond: 6.8, errorRate: 0.11, latencyP95Ms: 320 }
+    },
+    {
+      id: 'cache-cart',
+      label: 'Cart Cache',
+      entityType: 'middleware',
+      health: 'warning',
+      tone: 'orange',
+      focus: 'normal',
+      source: 'database-middleware-connection',
+      evidenceBadges: ['cache'],
+      redMetrics: { requestRatePerSecond: 9.5, errorRate: 0.026, latencyP95Ms: 76 }
+    },
+    {
+      id: 'monitor-http',
+      label: 'HTTP Monitor',
+      entityType: 'monitor',
+      health: 'healthy',
+      tone: 'purple',
+      focus: 'normal',
+      source: 'monitor-ownership',
+      evidenceBadges: ['monitor'],
+      redMetrics: { requestRatePerSecond: 1, errorRate: 0, latencyP95Ms: 18 }
+    }
+  ],
+  edges: [
+    {
+      id: 'web-checkout',
+      from: 'svc-web',
+      to: 'svc-checkout',
+      label: 'HTTP call',
+      relationshipType: 'trace-call',
+      source: 'otlp-trace-call',
+      tone: 'orange',
+      focus: 'active-path',
+      selected: true,
+      evidenceBadges: ['trace'],
+      redMetrics: { requestRatePerSecond: 12.34, errorRate: 0.042, latencyP95Ms: 180 }
+    },
+    {
+      id: 'checkout-payment',
+      from: 'svc-checkout',
+      to: 'svc-payment',
+      label: 'HTTP call',
+      relationshipType: 'trace-call',
+      source: 'otlp-trace-call',
+      tone: 'green',
+      focus: 'active-path',
+      evidenceBadges: ['trace'],
+      redMetrics: { requestRatePerSecond: 7.1, errorRate: 0.012, latencyP95Ms: 92 }
+    },
+    {
+      id: 'checkout-orders',
+      from: 'svc-checkout',
+      to: 'db-orders',
+      label: 'database connection',
+      relationshipType: 'database-connection',
+      source: 'database-middleware-connection',
+      tone: 'red',
+      focus: 'active-path',
+      evidenceBadges: ['db', 'alert'],
+      redMetrics: { requestRatePerSecond: 6.8, errorRate: 0.11, latencyP95Ms: 320 }
+    },
+    {
+      id: 'checkout-cache',
+      from: 'svc-checkout',
+      to: 'cache-cart',
+      label: 'cache call',
+      relationshipType: 'middleware-connection',
+      source: 'database-middleware-connection',
+      tone: 'orange',
+      evidenceBadges: ['cache'],
+      redMetrics: { requestRatePerSecond: 9.5, errorRate: 0.026, latencyP95Ms: 76 }
+    },
+    {
+      id: 'checkout-events',
+      from: 'svc-checkout',
+      to: 'queue-events',
+      label: 'publish',
+      relationshipType: 'template-dependency',
+      source: 'template-dependency',
+      tone: 'blue',
+      evidenceBadges: ['queue'],
+      redMetrics: { requestRatePerSecond: 5.8, errorRate: 0, latencyP95Ms: 28 }
+    },
+    {
+      id: 'monitor-checkout',
+      from: 'monitor-http',
+      to: 'svc-checkout',
+      label: 'monitor ownership',
+      relationshipType: 'monitors',
+      source: 'monitor-ownership',
+      tone: 'purple',
+      focus: 'context-muted',
+      evidenceBadges: ['monitor'],
+      redMetrics: { requestRatePerSecond: 1, errorRate: 0, latencyP95Ms: 18 }
+    }
+  ]
+};
+
+const topologyG6ScaleLabGraph = buildHzTopologyG6ScaleFixture(50, {
+  environment: 'prod',
+  namespace: 'commerce',
+  prefix: 'scale'
+});
+const topologyG6ScaleLabGraph500 = buildHzTopologyG6ScaleFixture(500, {
+  environment: 'prod',
+  namespace: 'commerce',
+  prefix: 'scale'
+});
+const topologyG6ScaleLabProfile50 = buildHzTopologyG6ScaleProfile(topologyG6ScaleLabGraph);
+const topologyG6ScaleLabProfile200 = buildHzTopologyG6ScaleProfile(buildHzTopologyG6ScaleFixture(200));
+const topologyG6ScaleLabProfile500 = buildHzTopologyG6ScaleProfile(topologyG6ScaleLabGraph500);
+const topologyG6LargeGraphStrategy200 = buildHzTopologyG6LargeGraphStrategy(buildHzTopologyG6ScaleFixture(200));
+const topologyG6LargeGraphStrategy500 = buildHzTopologyG6LargeGraphStrategy(topologyG6ScaleLabGraph500);
+const topologyG6ScaleLabRenderWindow500 = buildHzTopologyG6RenderWindow(topologyG6ScaleLabGraph500, topologyG6LargeGraphStrategy500, {
+  priorityNodeIds: ['scale-svc-420']
+});
+
+const monitorRows = [
+  { name: 'mysql-prod-01', app: 'MySQL', collector: 'collector-a', signal: 'metrics', status: '可用', latency: '38 ms', tone: 'success' as const },
+  { name: 'linux-edge-03', app: 'Linux', collector: 'collector-b', signal: 'metrics/logs', status: '采集中', latency: '72 ms', tone: 'info' as const },
+  { name: 'flink-yarn-main', app: 'Flink on Yarn', collector: 'collector-a', signal: 'metrics', status: '告警中', latency: '118 ms', tone: 'warning' as const },
+  { name: 'snmp-core-sw-02', app: 'SNMP Device', collector: 'collector-c', signal: 'metrics', status: '可用', latency: '45 ms', tone: 'success' as const }
+];
+
+const filterGroups: HzFilterGroup[] = [
+  {
+    id: 'domain',
+    label: 'Domain',
+    options: [
+      { id: 'all', label: 'All monitors', count: 128, active: true },
+      { id: 'database', label: 'Database', count: 24 },
+      { id: 'host', label: 'Host', count: 46 },
+      { id: 'bigdata', label: 'Big data', count: 9 },
+      { id: 'network', label: 'Network', count: 18 }
+    ]
+  },
+  {
+    id: 'status',
+    label: 'Status',
+    options: [
+      { id: 'up', label: 'Available', count: 116 },
+      { id: 'warn', label: 'Warning', count: 7 },
+      { id: 'down', label: 'Down', count: 5 }
+    ]
+  },
+  {
+    id: 'collector',
+    label: 'Collector',
+    options: [
+      { id: 'collector-a', label: 'collector-a', count: 58 },
+      { id: 'collector-b', label: 'collector-b', count: 44 },
+      { id: 'collector-c', label: 'collector-c', count: 26 }
+    ]
+  }
+];
+
+const filterFacetGroups: HzFilterFacetGroup[] = [
+  {
+    id: 'resource',
+    label: 'Resource attributes',
+    facets: [
+      {
+        id: 'resource.type',
+        label: 'resource.type',
+        type: 'keyword',
+        values: [
+          { id: 'mysql', label: 'mysql', count: 24, active: true },
+          { id: 'linux', label: 'linux', count: 46, active: true },
+          { id: 'flink', label: 'flink_on_yarn', count: 9 },
+          { id: 'snmp', label: 'snmp', count: 18 }
+        ]
+      },
+      {
+        id: 'collector',
+        label: 'collector',
+        type: 'keyword',
+        values: [
+          { id: 'collector-a', label: 'collector-a', count: 58 },
+          { id: 'collector-b', label: 'collector-b', count: 44 },
+          { id: 'collector-c', label: 'collector-c', count: 26 }
+        ]
+      }
+    ]
+  },
+  {
+    id: 'signal',
+    label: 'Signals',
+    facets: [
+      {
+        id: 'signal',
+        label: 'signal',
+        type: 'keyword',
+        values: [
+          { id: 'metrics', label: 'metrics', count: 123, active: true },
+          { id: 'logs', label: 'logs', count: 41 },
+          { id: 'traces', label: 'traces', count: 32 }
+        ]
+      },
+      {
+        id: 'status',
+        label: 'status',
+        type: 'enum',
+        values: [
+          { id: 'available', label: 'available', count: 116 },
+          { id: 'warning', label: 'warning', count: 7 },
+          { id: 'down', label: 'down', count: 5 }
+        ]
+      }
+    ]
+  }
+];
+
+const initialFilterClauses: HzFilterClause[] = [
+  { id: 'resource.type-IN-mysql-linux', field: 'resource.type', operator: 'IN', value: 'mysql, linux' },
+  { id: 'status-neq-down', field: 'status', operator: '!=', value: 'down' }
+];
+
+const savedFilterViewPresets: Array<{
+  id: string;
+  label: string;
+  description: string;
+  clauses: HzFilterClause[];
+}> = [
+  {
+    id: 'baseline',
+    label: 'Baseline',
+    description: '资源类型 + 非宕机基线',
+    clauses: initialFilterClauses
+  },
+  {
+    id: 'collector-latency',
+    label: 'Collector latency',
+    description: '聚焦 collector-a 延迟',
+    clauses: [
+      { id: 'collector-IN-collector-a', field: 'collector', operator: 'IN', value: 'collector-a' },
+      { id: 'latency-gt-80ms', field: 'latency', operator: '>', value: '80ms' }
+    ]
+  },
+  {
+    id: 'open-alerts',
+    label: 'Open alerts',
+    description: '只看告警与采集中资源',
+    clauses: [{ id: 'status-IN-warning-collecting', field: 'status', operator: 'IN', value: 'warning, collecting' }]
+  }
+];
+
+const groupByFilters = [
+  { id: 'resource.type', label: 'resource.type' },
+  { id: 'collector', label: 'collector' }
+];
+
+const filterQueryPlan = {
+  aggregate: 'count()',
+  orderBy: 'latency desc',
+  limit: 100
+};
+
+const filterBuilderFields = [
+  { id: 'resource.type', label: 'resource.type' },
+  { id: 'collector', label: 'collector' },
+  { id: 'signal', label: 'signal' },
+  { id: 'status', label: 'status' },
+  { id: 'latency', label: 'latency' }
+];
+
+const filterBuilderOperators = [
+  { id: 'IN', label: 'IN' },
+  { id: 'NOT_IN', label: 'NOT_IN' },
+  { id: '=', label: '=' },
+  { id: '!=', label: '!=' },
+  { id: 'CONTAINS', label: 'CONTAINS' },
+  { id: 'EXISTS', label: 'EXISTS' },
+  { id: '>', label: '>' },
+  { id: '<', label: '<' }
+];
+
+const resultTimeRanges = [
+  { id: '15m', label: 'Last 15 minutes' },
+  { id: '1h', label: 'Last 1 hour' },
+  { id: '6h', label: 'Last 6 hours' },
+  { id: '24h', label: 'Last 24 hours' }
+];
+
+const resultRefreshIntervals = [
+  { id: 'off', label: 'Off' },
+  { id: '15s', label: '15s' },
+  { id: '30s', label: '30s' },
+  { id: '1m', label: '1m' }
+];
+
+const uiLabDefaultTimeFoundationValue: HzTimeRangeToolbarValue = {
+  timeRange: 'last-15m',
+  from: 'now-1h',
+  to: 'now',
+  start: '2026-05-17T10:32:38',
+  end: '2026-05-17T10:57:54',
+  refresh: '30',
+  tz: 'Asia/Shanghai'
+};
+
+const uiLabChartZoomPreviewValue: HzTimeRangeToolbarValue = {
+  timeRange: 'last-1h',
+  from: '2026-05-17 15:30:00',
+  to: '2026-05-17 16:30:00',
+  refresh: '30',
+  tz: 'Asia/Shanghai'
+};
+
+const uiLabTraceTimeRangeControlValue = {
+  timeRange: 'last-30m',
+  refresh: '',
+  live: 'false',
+  tz: 'Asia/Shanghai'
+};
+
+function uiLabRefreshIdToTimeContextValue(value: string) {
+  if (value === 'off') return '';
+  const seconds = /^(\d+)s$/.exec(value);
+  if (seconds) return seconds[1];
+  const minutes = /^(\d+)m$/.exec(value);
+  if (minutes) return String(Number(minutes[1]) * 60);
+  return value;
+}
+
+function uiLabTimeContextRefreshToRefreshId(value: string | undefined) {
+  if (!value) return 'off';
+  const seconds = Number(value);
+  if (Number.isFinite(seconds) && seconds > 0) {
+    return seconds % 60 === 0 ? `${seconds / 60}m` : `${seconds}s`;
+  }
+  return value;
+}
+
+type UiLabTranslator = (key: string) => string;
+
+function buildUiLabTimeFoundationLabels(t: UiLabTranslator) {
+  return {
+    preset: t('time.range.preset'),
+    start: t('time.range.start'),
+    end: t('time.range.end'),
+    from: t('time.range.from'),
+    to: t('time.range.to'),
+    absoluteTitle: t('time.range.absolute-title'),
+    quickRanges: t('time.range.quick-ranges'),
+    relativeTitle: t('time.range.relative'),
+    recentRanges: t('time.range.recent-ranges'),
+    customRange: t('time.range.custom-range'),
+    customName: t('time.range.custom-name'),
+    saveCustomRange: t('time.range.save-custom-range'),
+    deleteCustomRange: t('time.range.delete-custom-range'),
+    validationValid: t('time.range.validation-valid'),
+    validationInvalid: t('time.range.validation-invalid'),
+    year: t('time.range.year'),
+    month: t('time.range.month'),
+    weekdays: [
+      t('common.week.1'),
+      t('common.week.2'),
+      t('common.week.3'),
+      t('common.week.4'),
+      t('common.week.5'),
+      t('common.week.6'),
+      t('common.week.7')
+    ],
+    months: [
+      t('common.month.1'),
+      t('common.month.2'),
+      t('common.month.3'),
+      t('common.month.4'),
+      t('common.month.5'),
+      t('common.month.6'),
+      t('common.month.7'),
+      t('common.month.8'),
+      t('common.month.9'),
+      t('common.month.10'),
+      t('common.month.11'),
+      t('common.month.12')
+    ],
+    date: t('time.range.date'),
+    hour: t('time.range.hour'),
+    minute: t('time.range.minute'),
+    second: t('time.range.second'),
+    previousMonth: t('time.range.previous-month'),
+    nextMonth: t('time.range.next-month'),
+    previousYears: t('time.range.previous-years'),
+    nextYears: t('time.range.next-years'),
+    decrease: t('time.range.decrease'),
+    increase: t('time.range.increase'),
+    clear: t('common.clear'),
+    absolutePlaceholder: t('time.range.unset'),
+    refresh: t('time.range.refresh'),
+    timezone: t('time.range.timezone'),
+    apply: t('time.range.apply'),
+    applyAria: t('time.range.apply-aria'),
+    refreshAction: t('time.range.refresh-action'),
+    reset: t('time.range.reset'),
+    resetAria: t('time.range.reset-aria')
+  };
+}
+
+const resultViewModes = [
+  { id: 'list', label: 'List' },
+  { id: 'timeseries', label: 'Time series' },
+  { id: 'table', label: 'Table' }
+];
+
+const monitorFilterTypeOptions = [
+  { value: '', label: 'All types' },
+  { value: 'mysql', label: 'MySQL' },
+  { value: 'website', label: 'Website' },
+  { value: 'linux', label: 'Linux' }
+];
+
+const monitorFilterStatusOptions = [
+  { value: '', label: 'All status' },
+  { value: '1', label: '正常' },
+  { value: '2', label: '宕机' },
+  { value: '0', label: '暂停' }
+];
+
+const initialResultColumns = [
+  { id: 'resource', label: 'Resource', visible: true, pinned: true },
+  { id: 'type', label: 'Type', visible: true },
+  { id: 'collector', label: 'Collector', visible: true },
+  { id: 'signal', label: 'Signal', visible: true },
+  { id: 'status', label: 'Status', visible: true },
+  { id: 'latency', label: 'Latency', visible: true }
+];
+
+const collectorFieldStats = [
+  { id: 'collector-a', label: 'collector-a', count: 58, tone: 'success' as const },
+  { id: 'collector-b', label: 'collector-b', count: 44, tone: 'info' as const },
+  { id: 'collector-c', label: 'collector-c', count: 26, tone: 'neutral' as const }
+];
+
+const signalVolumeBuckets = [
+  {
+    id: '14:00',
+    label: '14:00',
+    segments: [
+      { id: 'metrics', label: 'metrics', value: 82, tone: 'success' as const },
+      { id: 'logs', label: 'logs', value: 31, tone: 'info' as const },
+      { id: 'alerts', label: 'alerts', value: 2, tone: 'warning' as const }
+    ]
+  },
+  {
+    id: '14:05',
+    label: '14:05',
+    segments: [
+      { id: 'metrics', label: 'metrics', value: 77, tone: 'success' as const },
+      { id: 'logs', label: 'logs', value: 40, tone: 'info' as const },
+      { id: 'alerts', label: 'alerts', value: 5, tone: 'warning' as const }
+    ]
+  },
+  {
+    id: '14:10',
+    label: '14:10',
+    segments: [
+      { id: 'metrics', label: 'metrics', value: 91, tone: 'success' as const },
+      { id: 'logs', label: 'logs', value: 37, tone: 'info' as const },
+      { id: 'alerts', label: 'alerts', value: 3, tone: 'warning' as const }
+    ]
+  },
+  {
+    id: '14:15',
+    label: '14:15',
+    segments: [
+      { id: 'metrics', label: 'metrics', value: 84, tone: 'success' as const },
+      { id: 'logs', label: 'logs', value: 46, tone: 'info' as const },
+      { id: 'alerts', label: 'alerts', value: 7, tone: 'warning' as const }
+    ]
+  },
+  {
+    id: '14:20',
+    label: '14:20',
+    segments: [
+      { id: 'metrics', label: 'metrics', value: 88, tone: 'success' as const },
+      { id: 'logs', label: 'logs', value: 42, tone: 'info' as const },
+      { id: 'alerts', label: 'alerts', value: 4, tone: 'warning' as const }
+    ]
+  },
+  {
+    id: '14:25',
+    label: '14:25',
+    segments: [
+      { id: 'metrics', label: 'metrics', value: 93, tone: 'success' as const },
+      { id: 'logs', label: 'logs', value: 35, tone: 'info' as const },
+      { id: 'alerts', label: 'alerts', value: 2, tone: 'warning' as const }
+    ]
+  }
+];
+
+const collectorLatencySeries = [
+  {
+    id: 'p50',
+    label: 'p50',
+    tone: 'info' as const,
+    points: [
+      { label: '14:00', value: 42 },
+      { label: '14:05', value: 51 },
+      { label: '14:10', value: 48 },
+      { label: '14:15', value: 56 },
+      { label: '14:20', value: 52 },
+      { label: '14:25', value: 49 }
+    ]
+  },
+  {
+    id: 'p95',
+    label: 'p95',
+    tone: 'warning' as const,
+    points: [
+      { label: '14:00', value: 92 },
+      { label: '14:05', value: 118 },
+      { label: '14:10', value: 104 },
+      { label: '14:15', value: 126 },
+      { label: '14:20', value: 111 },
+      { label: '14:25', value: 96 }
+    ]
+  }
+];
+
+const monitorHistoryEChartsOption: EChartsOption = {
+  backgroundColor: 'transparent',
+  grid: { top: 16, right: 14, bottom: 44, left: 44 },
+  tooltip: {
+    trigger: 'axis',
+    backgroundColor: '#0d121b',
+    borderColor: '#273044',
+    textStyle: { color: '#dbe4f0' }
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: false,
+    data: ['14:00', '14:05', '14:10', '14:15', '14:20', '14:25'],
+    axisLine: { lineStyle: { color: '#273044' } },
+    axisTick: { show: false },
+    axisLabel: { color: '#8f99ab', fontSize: 10 }
+  },
+  yAxis: {
+    type: 'value',
+    axisLine: { show: false },
+    axisTick: { show: false },
+    axisLabel: { color: '#8f99ab', fontSize: 10 },
+    splitLine: { lineStyle: { color: '#1c2434' } }
+  },
+  dataZoom: [
+    {
+      type: 'slider',
+      height: 16,
+      bottom: 12,
+      borderColor: '#273044',
+      backgroundColor: '#080c12',
+      fillerColor: 'rgba(124, 147, 219, 0.18)',
+      handleStyle: { color: '#7c93db' },
+      textStyle: { color: '#727b8c' }
+    }
+  ],
+  series: [
+    {
+      name: 'origin',
+      type: 'line',
+      smooth: true,
+      symbol: 'circle',
+      symbolSize: 4,
+      data: [118, 126, 121, 134, 128, 139],
+      lineStyle: { color: '#7c93db', width: 1.4 },
+      itemStyle: { color: '#7c93db' },
+      areaStyle: { color: 'rgba(124, 147, 219, 0.12)' }
+    }
+  ]
+};
+
+const availabilityTimelineRows = [
+  {
+    id: 'mysql-prod-01',
+    label: 'mysql-prod-01',
+    states: [
+      { id: 'available', label: 'available', tone: 'success' as const, width: 76 },
+      { id: 'warning', label: 'warn', tone: 'warning' as const, width: 24 }
+    ]
+  },
+  {
+    id: 'linux-edge-03',
+    label: 'linux-edge-03',
+    states: [
+      { id: 'collecting', label: 'collecting', tone: 'info' as const, width: 68 },
+      { id: 'available', label: 'available', tone: 'success' as const, width: 32 }
+    ]
+  },
+  {
+    id: 'flink-yarn-main',
+    label: 'flink-yarn-main',
+    states: [
+      { id: 'available', label: 'available', tone: 'success' as const, width: 52 },
+      { id: 'warning', label: 'warning', tone: 'warning' as const, width: 36 },
+      { id: 'down', label: 'down', tone: 'critical' as const, width: 12 }
+    ]
+  }
+];
+
+const latencyHeatmapBuckets = [
+  {
+    id: '14:00',
+    label: '14:00',
+    cells: [
+      { id: 'under-50', label: '<50ms', value: 24, tone: 'success' as const },
+      { id: '50-100', label: '50-100', value: 18, tone: 'info' as const },
+      { id: '100-250', label: '100-250', value: 7, tone: 'warning' as const },
+      { id: 'over-250', label: '>250ms', value: 1, tone: 'critical' as const }
+    ]
+  },
+  {
+    id: '14:05',
+    label: '14:05',
+    cells: [
+      { id: 'under-50', label: '<50ms', value: 21, tone: 'success' as const },
+      { id: '50-100', label: '50-100', value: 22, tone: 'info' as const },
+      { id: '100-250', label: '100-250', value: 9, tone: 'warning' as const },
+      { id: 'over-250', label: '>250ms', value: 4, tone: 'critical' as const }
+    ]
+  },
+  {
+    id: '14:10',
+    label: '14:10',
+    cells: [
+      { id: 'under-50', label: '<50ms', value: 19, tone: 'success' as const },
+      { id: '50-100', label: '50-100', value: 25, tone: 'info' as const },
+      { id: '100-250', label: '100-250', value: 11, tone: 'warning' as const },
+      { id: 'over-250', label: '>250ms', value: 5, tone: 'critical' as const }
+    ]
+  },
+  {
+    id: '14:15',
+    label: '14:15',
+    cells: [
+      { id: 'under-50', label: '<50ms', value: 16, tone: 'success' as const },
+      { id: '50-100', label: '50-100', value: 27, tone: 'info' as const },
+      { id: '100-250', label: '100-250', value: 14, tone: 'warning' as const },
+      { id: 'over-250', label: '>250ms', value: 7, tone: 'critical' as const }
+    ]
+  },
+  {
+    id: '14:20',
+    label: '14:20',
+    cells: [
+      { id: 'under-50', label: '<50ms', value: 22, tone: 'success' as const },
+      { id: '50-100', label: '50-100', value: 20, tone: 'info' as const },
+      { id: '100-250', label: '100-250', value: 10, tone: 'warning' as const },
+      { id: 'over-250', label: '>250ms', value: 3, tone: 'critical' as const }
+    ]
+  }
+];
+
+const logStreamRows = [
+  {
+    id: 'log-warn-1',
+    timestamp: '14:14:08',
+    level: 'warn',
+    source: 'collector-a',
+    message: 'mysql-prod-01 response time above threshold',
+    attributes: 'resource.type=mysql'
+  },
+  {
+    id: 'log-info-1',
+    timestamp: '14:14:12',
+    level: 'info',
+    source: 'collector-b',
+    message: 'linux-edge-03 metrics batch accepted',
+    attributes: 'signal=metrics'
+  },
+  {
+    id: 'log-error-1',
+    timestamp: '14:14:18',
+    level: 'error',
+    source: 'collector-a',
+    message: 'flink-yarn-main scrape timeout',
+    attributes: 'resource.type=flink'
+  }
+];
+
+const logVolumeBuckets = [
+  {
+    id: '14:00',
+    label: '14:00',
+    segments: [
+      { id: 'info', label: 'info', value: 42, tone: 'info' as const },
+      { id: 'warn', label: 'warn', value: 6, tone: 'warning' as const },
+      { id: 'error', label: 'error', value: 2, tone: 'critical' as const }
+    ]
+  },
+  {
+    id: '14:05',
+    label: '14:05',
+    segments: [
+      { id: 'info', label: 'info', value: 36, tone: 'info' as const },
+      { id: 'warn', label: 'warn', value: 11, tone: 'warning' as const },
+      { id: 'error', label: 'error', value: 5, tone: 'critical' as const }
+    ]
+  },
+  {
+    id: '14:10',
+    label: '14:10',
+    segments: [
+      { id: 'info', label: 'info', value: 45, tone: 'info' as const },
+      { id: 'warn', label: 'warn', value: 8, tone: 'warning' as const },
+      { id: 'error', label: 'error', value: 3, tone: 'critical' as const }
+    ]
+  },
+  {
+    id: '14:15',
+    label: '14:15',
+    segments: [
+      { id: 'info', label: 'info', value: 33, tone: 'info' as const },
+      { id: 'warn', label: 'warn', value: 15, tone: 'warning' as const },
+      { id: 'error', label: 'error', value: 7, tone: 'critical' as const }
+    ]
+  }
+];
+
+const logLevelDistribution = [
+  { id: 'debug', label: 'debug', value: 12, tone: 'neutral' as const },
+  { id: 'info', label: 'info', value: 156, tone: 'info' as const },
+  { id: 'warn', label: 'warn', value: 40, tone: 'warning' as const },
+  { id: 'error', label: 'error', value: 17, tone: 'critical' as const }
+];
+
+const traceWaterfallSpans = [
+  { id: 'api', service: 'hertzbeat-api', operation: 'POST /api/monitors/detect', startMs: 0, durationMs: 126, selfMs: 18, depth: 0, tone: 'info' as const },
+  { id: 'scheduler', service: 'scheduler', operation: 'dispatch.collector-a', startMs: 18, durationMs: 22, selfMs: 9, depth: 1, parentId: 'api', tone: 'neutral' as const },
+  { id: 'collector', service: 'collector-a', operation: 'mysql.collect', startMs: 42, durationMs: 68, selfMs: 52, depth: 1, parentId: 'api', status: 'retrying', tone: 'warning' as const },
+  { id: 'warehouse', service: 'warehouse', operation: 'greptime.write', startMs: 104, durationMs: 18, selfMs: 12, depth: 2, parentId: 'collector', tone: 'success' as const }
+];
+
+const traceSpanEvents = [
+  {
+    id: 'retry',
+    spanId: 'collector',
+    timestampMs: 68,
+    name: 'mysql.connection.retry',
+    tone: 'warning' as const,
+    attributes: [
+      { label: 'attempt', value: '2' },
+      { label: 'peer', value: '10.0.4.21:3306' }
+    ]
+  },
+  {
+    id: 'slow-query',
+    spanId: 'collector',
+    timestampMs: 96,
+    name: 'db.statement.slow',
+    tone: 'critical' as const,
+    attributes: [
+      { label: 'statement', value: 'select monitor_history' },
+      { label: 'elapsed', value: '120ms', tone: 'critical' as const }
+    ]
+  },
+  {
+    id: 'write-ok',
+    spanId: 'warehouse',
+    timestampMs: 118,
+    name: 'greptime.write.ack',
+    tone: 'success' as const,
+    attributes: [{ label: 'rows', value: '24' }]
+  }
+];
+
+const traceServiceNodes = [
+  { id: 'api', label: 'hertzbeat-api', role: 'entrypoint', value: '126ms', tone: 'info' as const },
+  { id: 'scheduler', label: 'scheduler', role: 'dispatch', value: '22ms', tone: 'neutral' as const },
+  { id: 'collector', label: 'collector-a', role: 'mysql collector', value: '68ms', tone: 'warning' as const },
+  { id: 'warehouse', label: 'warehouse', role: 'greptime write', value: '18ms', tone: 'success' as const }
+];
+
+const traceServiceEdges = [
+  { id: 'api-scheduler', from: 'api', to: 'scheduler', label: 'dispatch.collector-a', latencyMs: 22, calls: 128, errorRate: 0, tone: 'neutral' as const },
+  { id: 'api-collector', from: 'api', to: 'collector', label: 'mysql.collect', latencyMs: 68, calls: 128, errorRate: 3.1, tone: 'warning' as const },
+  { id: 'collector-warehouse', from: 'collector', to: 'warehouse', label: 'greptime.write', latencyMs: 18, calls: 124, errorRate: 0, tone: 'success' as const }
+];
+
+const topologyMetricRows = [
+  {
+    id: 'scale-edge-420-421',
+    sourceNodeId: 'scale-svc-420',
+    targetNodeId: 'scale-svc-421',
+    source: 'hertzbeat-api',
+    target: 'collector-a',
+    relationType: 'trace-call',
+    sourceKind: 'otlp-trace-call',
+    requestRatePerSecond: 2.13,
+    requestCount: 128,
+    errorRate: 0.031,
+    errorCount: 4,
+    latencyP95Ms: 96,
+    latencyAvgMs: 42,
+    evidenceBadges: ['trace', 'alert'],
+    tone: 'warning' as const
+  },
+  {
+    id: 'scale-edge-421-422',
+    sourceNodeId: 'scale-svc-421',
+    targetNodeId: 'scale-svc-422',
+    source: 'collector-a',
+    target: 'warehouse',
+    relationType: 'trace-call',
+    sourceKind: 'otlp-trace-call',
+    requestRatePerSecond: 2.06,
+    requestCount: 124,
+    errorRate: 0,
+    errorCount: 0,
+    latencyP95Ms: 22,
+    latencyAvgMs: 13,
+    evidenceBadges: ['trace'],
+    tone: 'success' as const
+  },
+  {
+    id: 'scale-edge-020-021',
+    sourceNodeId: 'scale-svc-020',
+    targetNodeId: 'scale-svc-021',
+    source: 'hertzbeat-api',
+    target: 'scheduler',
+    relationType: 'monitor-ownership',
+    sourceKind: 'monitor-ownership',
+    requestRatePerSecond: 1.8,
+    requestCount: 108,
+    errorRate: 0,
+    errorCount: 0,
+    latencyP95Ms: 31,
+    latencyAvgMs: 18,
+    evidenceBadges: ['monitor'],
+    tone: 'neutral' as const
+  }
+];
+
+const traceListItems = [
+  {
+    id: 'trace-1428',
+    service: 'hertzbeat-api',
+    operation: 'POST /api/monitors/detect',
+    startTime: '14:28:04',
+    durationMs: 126,
+    spanCount: 4,
+    errorCount: 1,
+    rootCause: 'collector retry · mysql connection',
+    tone: 'warning' as const
+  },
+  {
+    id: 'trace-1427',
+    service: 'collector-a',
+    operation: 'mysql.collect',
+    startTime: '14:27:45',
+    durationMs: 74,
+    spanCount: 3,
+    errorCount: 0,
+    rootCause: 'slow query recovered',
+    tone: 'success' as const
+  },
+  {
+    id: 'trace-1426',
+    service: 'warehouse',
+    operation: 'greptime.write',
+    startTime: '14:26:12',
+    durationMs: 41,
+    spanCount: 2,
+    errorCount: 0,
+    tone: 'info' as const
+  }
+];
+
+const traceLatencyBuckets = [
+  { id: 'under-50', label: '<50ms', value: 19, tone: 'success' as const },
+  { id: '50-100', label: '50-100ms', value: 28, tone: 'info' as const },
+  { id: '100-250', label: '100-250ms', value: 16, tone: 'warning' as const },
+  { id: 'over-250', label: '>250ms', value: 5, tone: 'critical' as const }
+];
+
+const queryHistoryItems = [
+  {
+    id: 'run-1428',
+    query: "collector IN 'collector-a' AND latency > '80ms'",
+    time: '14:28',
+    duration: '118ms',
+    resultCount: 24,
+    tone: 'warning' as const,
+    active: true,
+    meta: 'manual drilldown'
+  },
+  {
+    id: 'run-1419',
+    query: "resource.type IN 'mysql' AND status != 'down'",
+    time: '14:19',
+    duration: '72ms',
+    resultCount: 128,
+    tone: 'success' as const,
+    meta: 'baseline'
+  },
+  {
+    id: 'run-1406',
+    query: "status IN 'warning, collecting'",
+    time: '14:06',
+    duration: '96ms',
+    resultCount: 7,
+    tone: 'critical' as const,
+    meta: 'alert triage'
+  }
+];
+
+const savedViewCompareDeltas = [
+  { id: 'active-monitors', label: 'Active monitors', value: '-12', tone: 'warning' as const },
+  { id: 'open-alerts', label: 'Open alerts', value: '+7', tone: 'critical' as const },
+  { id: 'p95-latency', label: 'Collector p95', value: '+46ms', tone: 'warning' as const }
+];
+
+const investigationNotes = [
+  {
+    id: 'note-greptime-lag',
+    author: 'SRE',
+    time: '14:31',
+    body: 'collector-a p95 spike aligns with Greptime write lag; keep metrics/logs in window before opening trace-only drilldown.',
+    tone: 'warning' as const,
+    tags: ['collector-a', 'greptime', 'p95']
+  },
+  {
+    id: 'note-template-context',
+    author: 'Monitor owner',
+    time: '14:22',
+    body: 'mysql-prod-01 template changed responseTime threshold from 120ms to 180ms yesterday.',
+    tone: 'info' as const,
+    tags: ['mysql-prod-01', 'threshold']
+  }
+];
+
+const commandPaletteItems = [
+  {
+    id: 'open-entity',
+    title: 'Open entity mysql-prod-01',
+    description: 'Carry query, time range, and field context into entity detail.',
+    shortcut: 'G E',
+    tone: 'info' as const,
+    active: true,
+    meta: 'entity'
+  },
+  {
+    id: 'open-alert-evidence',
+    title: 'Open alert evidence',
+    description: 'View active alerts with the same collector and resource filters.',
+    shortcut: 'G A',
+    tone: 'critical' as const,
+    meta: '7 open'
+  },
+  {
+    id: 'create-runbook-step',
+    title: 'Create runbook step',
+    description: 'Attach current query, notes, and YML template context.',
+    shortcut: 'R N',
+    tone: 'warning' as const,
+    meta: 'safe action'
+  }
+];
+
+const contextHandoffTargets = [
+  {
+    id: 'entity-detail',
+    label: 'Entity detail',
+    description: 'Metrics, logs, traces, alerts, owner, and topology context.',
+    meta: '5 signals',
+    tone: 'info' as const,
+    current: true
+  },
+  {
+    id: 'alert-evidence',
+    label: 'Alert evidence',
+    description: 'Open alerts constrained to the same entity and time window.',
+    meta: '7 open',
+    tone: 'critical' as const
+  },
+  {
+    id: 'topology',
+    label: 'Topology',
+    description: 'Collector, database, and downstream dependency neighborhood.',
+    meta: '3 hops',
+    tone: 'neutral' as const
+  },
+  {
+    id: 'yaml-template',
+    label: 'YAML template',
+    description: 'Open app-mysql.yml beside the current investigation evidence.',
+    meta: 'schema valid',
+    tone: 'success' as const
+  }
+];
+
+const incidentWorkbenchRows = [
+  {
+    id: 'inc-204',
+    title: 'Checkout latency spike',
+    severity: 'critical' as const,
+    stage: 'mitigating',
+    service: 'checkout-api',
+    owner: 'commerce-sre',
+    openedAt: '2026-05-23 09:10',
+    blastRadius: 'prod checkout'
+  },
+  {
+    id: 'inc-203',
+    title: 'Collector write lag',
+    severity: 'warning' as const,
+    stage: 'monitoring',
+    service: 'collector-a',
+    owner: 'observability',
+    openedAt: '2026-05-23 08:42',
+    blastRadius: 'metrics ingest'
+  }
+];
+
+const incidentTimelineRows = [
+  {
+    id: 'incident-event-1',
+    title: 'Mitigation started',
+    copy: 'Status page update added and trace evidence attached.',
+    meta: '09:14',
+    tone: 'warning' as const
+  },
+  {
+    id: 'incident-event-2',
+    title: 'Owner acknowledged',
+    copy: 'Commerce SRE took primary response ownership.',
+    meta: '09:12',
+    tone: 'info' as const
+  }
+];
+
+const incidentOwnershipRows = [
+  {
+    id: 'incident-owner-commerce',
+    owner: 'commerce-sre',
+    queue: 'primary',
+    copy: 'Owns customer checkout mitigation.',
+    meta: 'on call',
+    tone: 'info' as const
+  },
+  {
+    id: 'incident-owner-observability',
+    owner: 'observability',
+    queue: 'support',
+    copy: 'Keeps trace/log evidence pinned to the incident.',
+    meta: 'handoff',
+    tone: 'neutral' as const
+  }
+];
+
+const inspectorSections = [
+  {
+    id: 'evidence',
+    title: 'Evidence',
+    description: 'Current selection keeps metrics, logs, traces, and template context together.',
+    items: [
+      { id: 'metric-p95', label: 'Metric threshold', value: 'responseTime p95 = 118ms', meta: 'last 15m', tone: 'warning' as const },
+      { id: 'log-slow-query', label: 'Log signal', value: 'slow query detected on mysql-prod-01', meta: '14:31', tone: 'critical' as const },
+      { id: 'trace-collector', label: 'Trace span', value: 'mysql.collect duration 72ms', meta: 'collector-a', tone: 'info' as const }
+    ]
+  },
+  {
+    id: 'safe-actions',
+    title: 'Safe actions',
+    description: 'Operator commands stay explicit and reviewable before execution.',
+    items: [
+      { id: 'open-runbook', label: 'Open runbook', value: 'Attach query, notes, and selected template.', meta: 'safe', tone: 'success' as const },
+      { id: 'create-alert', label: 'Create alert draft', value: 'Use resource.type, collector, and current time range.', meta: 'draft', tone: 'warning' as const }
+    ]
+  }
+];
+
+function stringifyFilterValue(value: React.ReactNode) {
+  return typeof value === 'string' || typeof value === 'number' ? String(value) : '';
+}
+
+function formatFilterQuery(clauses: HzFilterClause[], logic: HzFilterBuilderLogic = 'AND') {
+  return clauses
+    .map(clause => {
+      const value = stringifyFilterValue(clause.value);
+      return clause.operator === 'EXISTS' ? `${clause.field} EXISTS` : `${clause.field} ${clause.operator} '${value}'`;
+    })
+    .join(` ${logic} `);
+}
+
+function createFacetClause(facet: HzFilterFacet, value: HzFilterFacetValue, operator: 'IN' | 'NOT_IN'): HzFilterClause {
+  const label = stringifyFilterValue(value.label) || value.id;
+  return {
+    id: `${facet.id}-${operator}-${value.id}`,
+    field: String(facet.label),
+    operator,
+    value: label
+  };
+}
+
+function createFieldClause(field: string, value: string, operator: 'IN' | 'NOT_IN'): HzFilterClause {
+  return {
+    id: `${field}-${operator}-${value}`,
+    field,
+    operator,
+    value
+  };
+}
+
+const defaultYaml = `app: mysql
+name:
+  zh-CN: MySQL数据库
+  en-US: MySQL Database
+params:
+  - field: host
+    type: host
+    required: true
+metrics:
+  - name: basic
+    priority: 0
+    fields:
+      - field: responseTime
+        type: number
+      - field: status
+        type: string`;
+
+function findTemplate(categories: HzTemplateCategory[], id: string) {
+  return categories.flatMap(category => category.items).find(item => item.id === id) || categories[0]?.items[0];
+}
+
+export default function HertzBeatUiLabPage() {
+  const { t } = useI18n();
+  const [activeTab, setActiveTab] = React.useState('monitors');
+  const [typePickerOpen, setTypePickerOpen] = React.useState(false);
+  const [monitorExportDialogOpen, setMonitorExportDialogOpen] = React.useState(false);
+  const [alertSettingExportDialogOpen, setAlertSettingExportDialogOpen] = React.useState(false);
+  const [typeSearch, setTypeSearch] = React.useState('');
+  const [selectedType, setSelectedType] = React.useState('mysql');
+  const [templateSearch, setTemplateSearch] = React.useState('');
+  const [selectedTemplate, setSelectedTemplate] = React.useState('mysql');
+  const [attributeSearch, setAttributeSearch] = React.useState('');
+  const [savedViews, setSavedViews] = React.useState(savedFilterViewPresets);
+  const [activeSavedViewId, setActiveSavedViewId] = React.useState('baseline');
+  const [filterClauses, setFilterClauses] = React.useState<HzFilterClause[]>(initialFilterClauses);
+  const [filterLogic, setFilterLogic] = React.useState<HzFilterBuilderLogic>('AND');
+  const [query, setQuery] = React.useState(formatFilterQuery(initialFilterClauses));
+  const [timeRangeId, setTimeRangeId] = React.useState('15m');
+  const [refreshIntervalId, setRefreshIntervalId] = React.useState('30s');
+  const [monitorDetailRefreshIntervalId, setMonitorDetailRefreshIntervalId] = React.useState('90s');
+  const [timeFoundationValue, setTimeFoundationValue] = React.useState<HzTimeRangeToolbarValue>(uiLabDefaultTimeFoundationValue);
+  const [timeFoundationPreviewSource, setTimeFoundationPreviewSource] = React.useState<string | undefined>('chart-datazoom');
+  const [viewModeId, setViewModeId] = React.useState('list');
+  const [resultColumns, setResultColumns] = React.useState(initialResultColumns);
+  const [monitorFilterSearch, setMonitorFilterSearch] = React.useState('mysql');
+  const [monitorFilterLabels, setMonitorFilterLabels] = React.useState('team=platform');
+  const [monitorFilterType, setMonitorFilterType] = React.useState('mysql');
+  const [monitorFilterStatus, setMonitorFilterStatus] = React.useState('');
+  const [monitorDetailTabId, setMonitorDetailTabId] = React.useState('realtime');
+  const [monitorEditorAdvancedOpen, setMonitorEditorAdvancedOpen] = React.useState(false);
+  const [noticeRuleFilterAllDemo, setNoticeRuleFilterAllDemo] = React.useState(true);
+  const [monitorRowActionMenuOpen, setMonitorRowActionMenuOpen] = React.useState(false);
+  const [pluginToolbarActionMenuOpen, setPluginToolbarActionMenuOpen] = React.useState(false);
+  const [pluginRowActionMenuOpen, setPluginRowActionMenuOpen] = React.useState(false);
+  const [collectorRowActionMenuOpen, setCollectorRowActionMenuOpen] = React.useState(false);
+  const [historyAggregationModeId, setHistoryAggregationModeId] = React.useState('raw');
+  const [historyFullscreenOpen, setHistoryFullscreenOpen] = React.useState(false);
+  const [favoriteMode, setFavoriteMode] = React.useState('realtime');
+  const [selectedSignalBucketId, setSelectedSignalBucketId] = React.useState('14:15');
+  const [selectedLatencyPointId, setSelectedLatencyPointId] = React.useState('p95:14:15');
+  const [hiddenSignalSegmentIds, setHiddenSignalSegmentIds] = React.useState<string[]>([]);
+  const [hiddenLatencySeriesIds, setHiddenLatencySeriesIds] = React.useState<string[]>([]);
+  const [chartWindowSource, setChartWindowSource] = React.useState('signal-volume');
+  const [contextMessage, setContextMessage] = React.useState('上下文窗口: collector-a · 最近 15 分钟 · metrics/logs');
+  const [topologyCompanionCollapsedSections, setTopologyCompanionCollapsedSections] = React.useState<Record<string, boolean>>({
+    timeline: true
+  });
+  const [topologyMetricWindowFilter, setTopologyMetricWindowFilter] = React.useState<HzTopologyMetricTableRenderWindowFilter>('all');
+  const [topologyG6LabSelection, setTopologyG6LabSelection] = React.useState({
+    nodeId: 'svc-checkout',
+    edgeId: 'web-checkout',
+    source: 'initial'
+  });
+  const [topologyG6ScaleLabSelection, setTopologyG6ScaleLabSelection] = React.useState<{
+    nodeId?: string;
+    edgeId?: string;
+    source: string;
+  }>({
+    nodeId: 'scale-svc-420',
+    source: 'initial'
+  });
+  const [selectedMonitorName, setSelectedMonitorName] = React.useState(monitorRows[0].name);
+  const [selectedTraceSpanId, setSelectedTraceSpanId] = React.useState('collector');
+  const [selectedTraceId, setSelectedTraceId] = React.useState('trace-1428');
+  const [traceDetailOpen, setTraceDetailOpen] = React.useState(false);
+  const [inspectorOpen, setInspectorOpen] = React.useState(false);
+  const [mutationStatus, setMutationStatus] = React.useState<'clean' | 'dirty' | 'saving' | 'saved' | 'failed'>('dirty');
+  const [dangerConfirmOpen, setDangerConfirmOpen] = React.useState(false);
+  const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
+  const handleTopologyCompanionCollapsedChange = React.useCallback((sectionId: string, collapsed: boolean) => {
+    setTopologyCompanionCollapsedSections(current => ({ ...current, [sectionId]: collapsed }));
+    setContextMessage(`Topology companion ${sectionId}: ${collapsed ? 'collapsed' : 'expanded'}`);
+  }, []);
+  const handleTopologyG6LabNodeSelect = React.useCallback((nodeId: string) => {
+    setTopologyG6LabSelection({ nodeId, edgeId: '', source: 'node-click' });
+    setContextMessage(`Topology G6 node selected: ${nodeId}`);
+  }, []);
+  const handleTopologyG6LabEdgeSelect = React.useCallback((edgeId: string) => {
+    setTopologyG6LabSelection({ nodeId: '', edgeId, source: 'edge-click' });
+    setContextMessage(`Topology G6 edge selected: ${edgeId}`);
+  }, []);
+  const handleTopologyG6ScaleLabNodeSelect = React.useCallback((nodeId: string) => {
+    setTopologyG6ScaleLabSelection({ nodeId, source: 'node-click' });
+    setContextMessage(`Topology G6 scale node selected: ${nodeId}`);
+  }, []);
+  const handleTopologyG6ScaleLabEdgeSelect = React.useCallback((edgeId: string) => {
+    setTopologyG6ScaleLabSelection({ edgeId, source: 'edge-click' });
+    setContextMessage(`Topology G6 scale edge selected: ${edgeId}`);
+  }, []);
+  const handleTopologyG6ScaleLabTableRowSelect = React.useCallback((row: HzTopologyMetricRow) => {
+    setTopologyG6ScaleLabSelection({ edgeId: row.id, source: 'table-row-click' });
+    setContextMessage(`Topology G6 scale table edge selected: ${row.id}`);
+  }, []);
+  const timeFoundationPresets = React.useMemo(
+    () => [
+      { value: 'last-15m', label: t('time.range.preset.last-15m') },
+      { value: 'last-1h', label: t('time.range.preset.last-1h') },
+      { value: 'last-6h', label: t('time.range.preset.last-6h') },
+      { value: 'last-24h', label: t('time.range.preset.last-24h') }
+    ],
+    [t]
+  );
+  const timeFoundationRefreshOptions = React.useMemo(
+    () =>
+      resultRefreshIntervals.map(option => ({
+        value: uiLabRefreshIdToTimeContextValue(option.id),
+        label: option.id === 'off' ? t('time.range.manual-refresh') : option.label
+      })),
+    [t]
+  );
+  const timeFoundationRecentRanges = React.useMemo(
+    () => [
+      {
+        id: 'ui-lab-now-1h',
+        label: t('ui-lab.time-range.previous-query'),
+        from: 'now-1h',
+        to: 'now',
+        refresh: '30',
+        tz: 'Asia/Shanghai'
+      }
+    ],
+    [t]
+  );
+  const timeFoundationCustomRanges = React.useMemo(
+    () => [
+      {
+        id: 'ui-lab-release-window',
+        label: t('ui-lab.time-range.release-window'),
+        from: 'now-2h',
+        to: 'now',
+        refresh: '30',
+        tz: 'Asia/Shanghai'
+      }
+    ],
+    [t]
+  );
+  const timeFoundationLabels = React.useMemo(() => buildUiLabTimeFoundationLabels(t), [t]);
+  const [toastItems, setToastItems] = React.useState<HzToastItem[]>([
+    { id: 'template-dirty', tone: 'warning' as const, title: 'Template draft changed', description: 'Review schema and save before applying.', meta: 'local' }
+  ]);
+  const [code, setCode] = React.useState(defaultYaml);
+  const selectedTypeItem = findTemplate(monitorTypeCategories, selectedType);
+  const selectedTemplateItem = findTemplate(yamlCategories, selectedTemplate);
+  const selectedMonitor = monitorRows.find(row => row.name === selectedMonitorName) || monitorRows[0];
+  const selectedTrace = traceListItems.find(trace => trace.id === selectedTraceId) || traceListItems[0];
+  const selectedTraceSpan = traceWaterfallSpans.find(span => span.id === selectedTraceSpanId) || traceWaterfallSpans[0];
+  const selectedTraceSpanEvents = traceSpanEvents.filter(event => event.spanId === selectedTraceSpan.id);
+  const selectedSignalBucket = signalVolumeBuckets.find(bucket => bucket.id === selectedSignalBucketId) || signalVolumeBuckets[0];
+  const selectedLatencyPoint = collectorLatencySeries.flatMap(series =>
+    series.points.map(point => ({
+      id: `${series.id}:${point.label}`,
+      series,
+      point
+    }))
+  ).find(item => item.id === selectedLatencyPointId);
+  const selectedInspectorFacts = [
+    { label: 'Latency p95', value: selectedMonitor.latency, meta: 'warn at 120ms', tone: selectedMonitor.tone },
+    { label: 'Open alerts', value: selectedMonitor.tone === 'warning' ? '7' : '0', meta: selectedMonitor.tone === 'warning' ? '2 critical' : 'none', tone: selectedMonitor.tone },
+    { label: 'Collector', value: selectedMonitor.collector, meta: '58 resources', tone: 'info' as const },
+    { label: 'Template', value: `app-${selectedMonitor.app.toLowerCase().replaceAll(' ', '_')}.yml`, meta: 'schema valid', tone: 'success' as const }
+  ];
+  const selectedTraceFacts = [
+    { label: 'Duration', value: `${selectedTrace.durationMs}ms`, meta: 'global scale', tone: selectedTrace.tone },
+    { label: 'Spans', value: selectedTrace.spanCount, meta: 'current trace', tone: 'info' as const },
+    { label: 'Errors', value: selectedTrace.errorCount || 0, meta: selectedTrace.errorCount ? 'needs review' : 'clean', tone: selectedTrace.errorCount ? 'warning' as const : 'success' as const },
+    { label: 'Start', value: selectedTrace.startTime, meta: 'selected window', tone: 'neutral' as const }
+  ];
+  const yamlValidationIssues = code.includes('app:')
+    ? [{ id: 'schema-valid', field: 'app-mysql.yml', message: 'Schema valid for local draft.', tone: 'success' as const }]
+    : [{ id: 'schema-missing-app', field: 'app', message: 'Template app key is required before save.', tone: 'critical' as const }];
+  const yamlFeedbackTone = mutationStatus === 'failed' ? 'critical' : mutationStatus === 'saved' ? 'success' : code.includes('app:') ? 'success' : 'critical';
+  const yamlFeedbackTitle = mutationStatus === 'failed' ? 'Save failed' : mutationStatus === 'saved' ? 'Saved locally' : code.includes('app:') ? 'Schema valid' : 'Schema error';
+  const yamlFeedbackDescription =
+    mutationStatus === 'failed'
+      ? 'Collector rejected the draft. Keep editing or discard changes.'
+      : mutationStatus === 'saved'
+        ? 'Template draft is ready for apply review.'
+        : code.includes('app:')
+          ? 'app-mysql.yml is locally valid and has unsaved changes.'
+          : 'Add the app key before saving this template.';
+  const addFilterClause = React.useCallback((clause: HzFilterClause) => {
+    setFilterClauses(previous => {
+      const next = [clause, ...previous.filter(item => item.id !== clause.id)];
+      setQuery(formatFilterQuery(next, filterLogic));
+      return next;
+    });
+    setActiveSavedViewId('custom');
+  }, [filterLogic]);
+  const removeFilterClause = React.useCallback((id: string) => {
+    setFilterClauses(previous => {
+      const next = previous.filter(item => item.id !== id);
+      setQuery(formatFilterQuery(next, filterLogic));
+      return next;
+    });
+  }, [filterLogic]);
+  const clearFilterClauses = React.useCallback(() => {
+    setFilterClauses([]);
+    setQuery('');
+    setActiveSavedViewId('custom');
+  }, []);
+  const changeFilterLogic = React.useCallback(
+    (logic: HzFilterBuilderLogic) => {
+      setFilterLogic(logic);
+      setQuery(formatFilterQuery(filterClauses, logic));
+      setActiveSavedViewId('custom');
+    },
+    [filterClauses]
+  );
+  const selectSavedView = React.useCallback(
+    (id: string) => {
+      const view = savedViews.find(item => item.id === id);
+      if (!view) return;
+      setActiveSavedViewId(id);
+      setFilterClauses(view.clauses);
+      setFilterLogic('AND');
+      setQuery(formatFilterQuery(view.clauses));
+    },
+    [savedViews]
+  );
+  const saveCurrentView = React.useCallback(() => {
+    const currentView = {
+      id: 'custom',
+      label: 'Current view',
+      description: `${filterClauses.length} filters · ${groupByFilters.length} groups`,
+      clauses: filterClauses
+    };
+    setSavedViews(previous => [currentView, ...previous.filter(item => item.id !== 'custom')]);
+    setActiveSavedViewId('custom');
+  }, [filterClauses]);
+  const toggleResultColumn = React.useCallback((id: string) => {
+    setResultColumns(previous =>
+      previous.map(column =>
+        column.id === id
+          ? {
+              ...column,
+              visible: !column.visible,
+              pinned: column.visible ? false : column.pinned
+            }
+          : column
+      )
+    );
+  }, []);
+  const pinResultColumn = React.useCallback((id: string) => {
+    setResultColumns(previous =>
+      previous.map(column =>
+        column.id === id
+          ? {
+              ...column,
+              visible: true,
+              pinned: !column.pinned
+            }
+          : column
+      )
+    );
+  }, []);
+  const addBuilderClause = React.useCallback(
+    ({ field, operator, value }: { field: string; operator: string; value: string }) => {
+      const normalizedValue = value || (operator === 'EXISTS' ? 'present' : '');
+      if (!normalizedValue && operator !== 'EXISTS') return;
+      addFilterClause({
+        id: `${field}-${operator}-${normalizedValue}`,
+        field,
+        operator,
+        value: normalizedValue
+      });
+    },
+    [addFilterClause]
+  );
+  const changeYamlCode = React.useCallback((value: string) => {
+    setCode(value);
+    setMutationStatus('dirty');
+    setToastItems([{ id: 'template-dirty', tone: 'warning', title: 'Template draft changed', description: 'Review schema and save before applying.', meta: 'local' }]);
+  }, []);
+  const saveYamlDraft = React.useCallback(() => {
+    if (!code.includes('app:')) {
+      setMutationStatus('failed');
+      setToastItems([{ id: 'template-save-failed', tone: 'critical', title: 'Save failed', description: 'Template app key is required.' }]);
+      return;
+    }
+    setMutationStatus('saved');
+    setToastItems([{ id: 'template-saved', tone: 'success', title: 'Template saved', description: 'app-mysql.yml updated locally.', meta: 'now' }]);
+  }, [code]);
+  const discardYamlDraft = React.useCallback(() => {
+    setCode(defaultYaml);
+    setMutationStatus('clean');
+    setToastItems([{ id: 'template-discarded', tone: 'neutral', title: 'Draft discarded', description: 'YAML reverted to the last saved baseline.' }]);
+  }, []);
+  const resetYamlDraft = React.useCallback(() => {
+    setCode(defaultYaml);
+    setMutationStatus('clean');
+    setDangerConfirmOpen(false);
+    setToastItems([{ id: 'template-reset', tone: 'warning', title: 'YAML draft reset', description: 'Local edits were removed from the lab.' }]);
+  }, []);
+  const visibleResultColumnIds = new Set(resultColumns.filter(column => column.visible).map(column => column.id));
+  const resultColumnDefinitions = [
+    { key: 'resource', header: 'Resource', render: row => <span className="font-semibold">{row.name}</span> },
+    { key: 'type', header: 'Type', render: row => row.app },
+    {
+      key: 'collector',
+      header: 'Collector',
+      render: row => (
+        <HzFieldValueActions
+          field="collector"
+          value={row.collector}
+          onInclude={() => addFilterClause(createFieldClause('collector', row.collector, 'IN'))}
+          onExclude={() => addFilterClause(createFieldClause('collector', row.collector, 'NOT_IN'))}
+        />
+      )
+    },
+    {
+      key: 'signal',
+      header: 'Signal',
+      render: row => (
+        <HzFieldValueActions
+          field="signal"
+          value={<span className="font-mono text-[#9ca3af]">{row.signal}</span>}
+          valueLabel={row.signal}
+          onInclude={() => addFilterClause(createFieldClause('signal', row.signal, 'IN'))}
+          onExclude={() => addFilterClause(createFieldClause('signal', row.signal, 'NOT_IN'))}
+        />
+      )
+    },
+    { key: 'status', header: 'Status', render: row => <HzStatusBadge tone={row.tone}>{row.status}</HzStatusBadge> },
+    { key: 'latency', header: 'Latency', render: row => <span className="font-mono text-[#cbd5e1]">{row.latency}</span> }
+  ].filter(column => visibleResultColumnIds.has(column.key));
+
+  return (
+    <HzExplorerFrame
+      eyebrow="@hertzbeat/ui explorer"
+      title="HertzBeat UI Lab"
+      description="统一查看监控资源、模板目录、采集状态与 YAML 定义。"
+      mainId="hz-ui-lab-main"
+      mainLabel="HertzBeat UI Lab workbench"
+      filterRailLabel="UI lab filters"
+      actions={
+        <>
+          <HzIconButton
+            label="Refresh monitor list"
+            intent="ghost"
+            onClick={() => setContextMessage('Monitor list refresh')}
+            data-hz-ui-lab-monitor-manual-refresh="shared"
+            data-monitor-manual-refresh-owner="hertzbeat-ui-icon-button"
+            data-monitor-manual-refresh-action="sync"
+          >
+            <RefreshCw size={13} />
+          </HzIconButton>
+          <HzButton
+            intent="secondary"
+            onClick={() => setTypePickerOpen(true)}
+            data-hz-ui-lab-monitor-app-picker-trigger="shared"
+            data-monitor-app-picker-trigger-owner="hertzbeat-ui-button"
+          >
+            <Plus size={14} />
+            新增监控
+          </HzButton>
+          <HzButton intent="primary">
+            <CheckCircle2 size={14} />
+            UI 基线
+          </HzButton>
+        </>
+      }
+      tabs={
+        <HzSegmentedTabs
+          activeId={activeTab}
+          onSelect={setActiveTab}
+          items={[
+            { id: 'monitors', label: 'Monitors', count: 128 },
+            { id: 'templates', label: 'Templates', count: 42 },
+            { id: 'signals', label: 'Signals', count: 3 }
+          ]}
+        />
+      }
+      filterRail={
+        <HzFilterWorkbench
+          activeClauses={filterClauses}
+          builderFields={filterBuilderFields}
+          builderOperators={filterBuilderOperators}
+          builderLogic={filterLogic}
+          groupBy={groupByFilters}
+          savedViews={savedViews.map(view => ({
+            id: view.id,
+            label: view.label,
+            description: view.description,
+            active: view.id === activeSavedViewId
+          }))}
+          queryPlan={filterQueryPlan}
+          quickGroups={filterGroups}
+          facetGroups={filterFacetGroups}
+          attributeSearch={attributeSearch}
+          onAttributeSearchChange={setAttributeSearch}
+          onIncludeValue={(facet, value) => addFilterClause(createFacetClause(facet, value, 'IN'))}
+          onExcludeValue={(facet, value) => addFilterClause(createFacetClause(facet, value, 'NOT_IN'))}
+          onBuilderLogicChange={changeFilterLogic}
+          onAddBuilderClause={addBuilderClause}
+          onSelectSavedView={selectSavedView}
+          onSaveCurrentView={saveCurrentView}
+          onClearClause={removeFilterClause}
+          onClearAll={clearFilterClauses}
+        />
+      }
+      queryBar={
+        <div className="grid min-w-0" data-hz-ui-lab-monitor-filter-bar="shared">
+          <section
+            className="grid min-w-0 gap-2 border-b border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] px-3 py-2"
+            data-hz-ui-lab-control-baseline="28-button-32-field-tiers"
+            data-hz-control-baseline-kind="primitive-form-action"
+          >
+            <div className="flex min-w-0 flex-wrap items-center gap-2">
+              <span className="min-w-0 truncate font-mono text-[10px] uppercase tracking-[0.12em] text-[#727b8c]">
+                28px buttons · 32px fields
+              </span>
+              <HzButton size="sm" data-hz-control-baseline-component="HzButton">
+                Secondary
+              </HzButton>
+              <HzButton size="sm" intent="ghost" data-hz-control-baseline-component="HzButton">
+                Ghost
+              </HzButton>
+              <HzButtonLink
+                href="#ui-lab-history-download"
+                size="xs"
+                data-hz-ui-lab-button-link="shared"
+                data-hz-ui-lab-button-link-component="component-ready"
+                data-monitor-history-download-owner="hertzbeat-ui-button-link"
+              >
+                Download SVG
+              </HzButtonLink>
+              <HzButtonLink
+                href="#ui-lab-trace-handoff"
+                size="md"
+                data-hz-ui-lab-trace-handoff-action="collector"
+                data-trace-handoff-action-icon-owner="hertzbeat-ui-button-icon"
+              >
+                <HzButtonIcon
+                  icon={Server}
+                  data-hz-ui-lab-trace-handoff-action-icon="collector"
+                  data-trace-handoff-action-icon-owner="hertzbeat-ui-button-icon"
+                />
+                Collector
+              </HzButtonLink>
+              <HzUnderlineToggle
+                selected
+                selectionAttrName="data-control-underline-selected"
+                data-hz-control-baseline-component="HzUnderlineToggle"
+                onClick={() => setContextMessage('Baseline underline')}
+              >Templates</HzUnderlineToggle>
+              <Link
+                href="/monitors?app=mysql"
+                className="inline-flex max-w-full"
+                data-hz-ui-lab-monitor-row-app-filter="angular-app-tag-link"
+                data-monitor-row-app-filter-link="angular-app-tag-link"
+                data-monitor-row-app-filter-target="/monitors?app=mysql"
+              >
+                <HzDataCellText
+                  variant="type"
+                  tone="muted"
+                  data-monitor-row-type-owner="hertzbeat-ui-data-cell-text"
+                  data-monitor-row-type="mysql"
+              >
+                MySQL
+              </HzDataCellText>
+              </Link>
+              <div
+                className="inline-flex min-w-0 items-center gap-1"
+                data-hz-ui-lab-monitor-row-instance-copy="angular-host-copy"
+                data-monitor-row-instance-copy-layout="angular-host-copy"
+              >
+                <HzDataCellText
+                  variant="copy"
+                  data-monitor-row-copy-owner="hertzbeat-ui-data-cell-text"
+                  data-monitor-row-copy="127.0.0.1:80"
+                >
+                  127.0.0.1:80
+                </HzDataCellText>
+                <HzIconButton
+                  label="Click to copy"
+                  intent="ghost"
+                  data-monitor-row-instance-copy="angular-host-copy"
+                  data-monitor-row-instance-copy-owner="hertzbeat-ui-icon-button"
+                  data-monitor-row-instance-copy-target="127.0.0.1:80"
+                >
+                  <CopyIcon size={13} />
+                </HzIconButton>
+              </div>
+              <div
+                className="inline-flex min-w-0 items-center gap-1"
+                data-hz-ui-lab-monitor-row-scrape-display="angular-service-discovery"
+              >
+                <HzDataCellText
+                  variant="copy"
+                  data-monitor-row-copy-owner="hertzbeat-ui-data-cell-text"
+                  data-monitor-row-copy="Http Service Discovery"
+                  data-monitor-row-scrape-display="angular-service-discovery"
+                  data-monitor-row-scrape-display-owner="hertzbeat-ui-data-cell-text"
+                  data-monitor-row-scrape-value="http_sd"
+                >
+                  <GitBranch size={12} aria-hidden="true" data-monitor-row-scrape-icon="partition" />
+                  Http Service Discovery
+                </HzDataCellText>
+              </div>
+              <HzLabelTag
+                colorToken="geekblue"
+                data-hz-ui-lab-monitor-row-label-color="angular-render-label-color"
+                data-monitor-row-label-color="angular-render-label-color"
+                data-monitor-row-labels-owner="hertzbeat-ui-label-tag"
+              >
+                team:platform
+              </HzLabelTag>
+              <HzDataCellText
+                variant="meta"
+                display="block"
+                spacing="stack"
+                casing="plain"
+                data-hz-ui-lab-monitor-row-entity-triage="angular-entity-reason"
+                data-monitor-row-entity-triage="angular-entity-reason"
+                data-monitor-row-entity-triage-owner="hertzbeat-ui-data-cell-text"
+                data-monitor-row-entity-triage-status="down"
+              >
+                This monitor is unhealthy. Check it first.
+              </HzDataCellText>
+              <HzButton
+                size="xs"
+                intent="secondary"
+                data-hz-ui-lab-monitor-row-entity-response-action="angular-inline-host-action"
+                data-monitor-row-entity-response-action="angular-inline-host-action"
+                data-monitor-row-entity-response-owner="hertzbeat-ui-button"
+                data-monitor-row-response-action="pause"
+              >
+                <PauseCircle size={13} />
+                Pause
+              </HzButton>
+              <div
+                className="flex flex-wrap gap-1.5"
+                data-hz-ui-lab-monitor-row-entity-support-actions="angular-platform-support-action-bar"
+                data-monitor-row-entity-support-actions="angular-platform-support-action-bar"
+                data-monitor-row-entity-support-actions-owner="hertzbeat-ui-button-link"
+              >
+                <HzButtonLink
+                  href="#ui-lab-monitor-row-related-logs"
+                  size="xs"
+                  intent="secondary"
+                  data-monitor-row-entity-support-action="related-logs"
+                  data-monitor-row-entity-support-target="/log/manage?entityId=42&search=checkout-api&returnTo=%2Fmonitors"
+                >
+                  <FileText size={13} />
+                  Open related logs
+                </HzButtonLink>
+                <HzButtonLink
+                  href="#ui-lab-monitor-row-related-traces"
+                  size="xs"
+                  intent="secondary"
+                  data-monitor-row-entity-support-action="related-traces"
+                  data-monitor-row-entity-support-target="/trace/manage?entityId=42&serviceName=checkout-api&returnTo=%2Fmonitors"
+                >
+                  <Network size={13} />
+                  Open related traces
+                </HzButtonLink>
+                <HzButton
+                  size="xs"
+                  intent="secondary"
+                  disabled
+                  data-monitor-row-entity-support-action="code"
+                  data-monitor-row-entity-support-disabled="missing-code-navigation"
+                >
+                  <FileCode2 size={13} />
+                  Open code
+                </HzButton>
+              </div>
+              <HzActionGroup
+                density="compact-icons"
+                layout="full-end"
+                data-hz-ui-lab-action-group="shared"
+                data-hz-ui-lab-action-group-layout="full-end"
+              >
+                <div
+                  className="relative flex justify-end"
+                  data-hz-ui-lab-monitor-row-action-menu="angular-ellipsis-dropdown"
+                  data-monitor-row-actions="angular-ellipsis-dropdown"
+                  data-monitor-row-actions-owner="hertzbeat-ui-table-row-action-button"
+                  data-monitor-row-action-menu="angular-ellipsis-dropdown"
+                  data-monitor-row-action-menu-open={monitorRowActionMenuOpen ? 'true' : 'false'}
+                  data-monitor-row-action-menu-layer="overlay-visible-above-table"
+                  data-monitor-row-action-menu-clearance="floating-overlay-no-table-crop"
+                >
+                  <HzIconButton
+                    label="Open monitor row actions"
+                    intent="ghost"
+                    aria-expanded={monitorRowActionMenuOpen}
+                    onClick={() => setMonitorRowActionMenuOpen(open => !open)}
+                    data-monitor-row-action-menu-trigger="angular-ellipsis-dropdown"
+                    data-monitor-row-action-menu-trigger-owner="hertzbeat-ui-icon-button"
+                    data-monitor-row-action-menu-open={monitorRowActionMenuOpen ? 'true' : 'false'}
+                  >
+                    <MoreHorizontal size={14} />
+                  </HzIconButton>
+                  <div
+                    hidden={!monitorRowActionMenuOpen}
+                    className="absolute right-0 top-7 z-30 grid min-w-[176px] gap-1 border border-[var(--hz-ui-line-strong)] bg-[var(--hz-ui-surface-raised)] p-1 shadow-[0_18px_42px_rgba(0,0,0,0.42)]"
+                    data-monitor-row-action-menu-panel="angular-nz-dropdown-menu"
+                    data-monitor-row-action-menu-panel-open={monitorRowActionMenuOpen ? 'true' : 'false'}
+                    data-monitor-row-action-menu-layer-panel="overlay-visible-above-table"
+                    data-monitor-row-action-menu-clearance-panel="floating-overlay-no-table-crop"
+                  >
+                  <HzButtonLink
+                    href="#ui-lab-monitor-detail"
+                    size="xs"
+                    intent="ghost"
+                    layout="full"
+                    data-hz-ui-lab-monitor-row-detail-action="shared"
+                    data-monitor-row-detail-action-owner="hertzbeat-ui-button-link"
+                    data-monitor-row-action-owner="hertzbeat-ui-button-link"
+                    data-monitor-row-action="detail"
+                  >
+                    <BarChart3 size={13} />
+                    Open monitor detail
+                  </HzButtonLink>
+                  <HzButtonLink
+                    href="#ui-lab-monitor-edit"
+                    size="xs"
+                    intent="ghost"
+                    layout="full"
+                    data-monitor-row-action-owner="hertzbeat-ui-button-link"
+                    data-monitor-row-action="edit"
+                  >
+                    <Pencil size={13} />
+                    Edit monitor
+                  </HzButtonLink>
+                  <HzTableRowActionButton
+                    width="root-span"
+                    data-hz-ui-lab-monitor-row-copy-lifecycle="angular-copy-success-refresh-unavailable-refresh"
+                    data-monitor-row-copy-lifecycle="angular-copy-success-refresh-unavailable-refresh"
+                    data-monitor-row-copy-lifecycle-owner="hertzbeat-ui-table-row-action-button"
+                    data-monitor-row-action-owner="hertzbeat-ui-table-row-action-button"
+                    data-monitor-row-action="copy"
+                  >
+                    <CopyIcon size={13} />
+                    Copy monitor
+                  </HzTableRowActionButton>
+                  <HzTableRowActionButton
+                    width="root-span"
+                    data-hz-ui-lab-monitor-row-response-action="pause"
+                    data-monitor-row-response-action-owner="hertzbeat-ui-table-row-action-button"
+                    data-monitor-row-response-action="pause"
+                  >
+                    <PauseCircle size={13} />
+                    Pause monitor
+                  </HzTableRowActionButton>
+                  <HzTableRowActionButton
+                    width="root-span"
+                    data-hz-ui-lab-monitor-row-response-action="enable"
+                    data-monitor-row-response-action-owner="hertzbeat-ui-table-row-action-button"
+                    data-monitor-row-response-action="enable"
+                  >
+                    <PlayCircle size={13} />
+                    Enable monitor
+                  </HzTableRowActionButton>
+                  <HzTableRowActionButton
+                    width="root-span"
+                    intent="danger"
+                    data-hz-ui-lab-monitor-row-delete-action="shared"
+                    data-monitor-row-delete-action-owner="hertzbeat-ui-table-row-action-button"
+                    data-monitor-row-delete-action="single"
+                  >
+                    <Trash2 size={13} />
+                    Delete monitor
+                  </HzTableRowActionButton>
+                  </div>
+                </div>
+              </HzActionGroup>
+              <HzConfirmDialog
+                open
+                tone="critical"
+                kicker="Monitor center"
+                title="Please confirm whether to cancel monitor!"
+                bodyRhythm="stack"
+                cancelLabel="Cancel"
+                confirmLabel="OK"
+                onClose={() => undefined}
+                onConfirm={() => undefined}
+                data-hz-ui-lab-monitor-row-response-confirm="angular-modal-confirm"
+                data-hz-ui-lab-monitor-response-confirm-closable="angular-nz-closable-false"
+                data-hz-ui-lab-monitor-response-confirm-ok="angular-nz-ok-danger-primary"
+                data-monitor-row-response-confirm="angular-modal-confirm"
+                data-monitor-row-response-confirm-owner="hertzbeat-ui-confirm-dialog"
+                data-monitor-row-response-confirm-action="pause"
+                data-monitor-row-response-confirm-target="checkout-api"
+                data-monitor-row-response-confirm-closable="angular-nz-closable-false"
+                data-monitor-row-response-confirm-ok="angular-nz-ok-danger-primary"
+              >
+                <div data-monitor-row-response-confirm-body="angular-single-row-confirm">
+                  <HzInlineFeedback
+                    tone="critical"
+                    title="checkout-api"
+                    data-monitor-row-response-confirm-selected-owner="hertzbeat-ui-inline-feedback"
+                  />
+                </div>
+              </HzConfirmDialog>
+              <HzHeaderIconButton
+                label="Unmute notifications"
+                state="active"
+                data-hz-control-baseline-component="HzHeaderIconButton"
+                data-hz-ui-lab-header-mute="shared"
+                data-hz-ui-lab-header-mute-save-lifecycle="angular-success-only-state-update"
+                data-app-frame-mute-save-lifecycle="angular-success-only-state-update"
+                data-app-frame-mute-save-lifecycle-owner="route-action-state-contract"
+                data-app-frame-icon-trigger="mute"
+                data-app-frame-mute-state="muted"
+              >
+                <Megaphone size={16} data-hz-ui-lab-header-mute-icon="shared" />
+              </HzHeaderIconButton>
+              <HzHeaderRealtimeNotice
+                status="live"
+                title="New Alert"
+                description="checkout-service latency breach"
+                meta="ALERT_EVENT"
+                data-hz-control-baseline-component="HzHeaderRealtimeNotice"
+                data-hz-ui-lab-header-realtime-notice="shared"
+                data-hz-ui-lab-header-realtime-sse="angular-alert-and-manager-sse"
+                data-app-frame-header-realtime-sse-contract="angular-alert-and-manager-sse"
+                data-app-frame-header-realtime-sse-owner="route-realtime-sse-contract"
+                data-app-frame-header-realtime-alert-source="/api/alert/sse/subscribe"
+                data-app-frame-header-realtime-manager-source="/api/manager/sse/subscribe"
+                data-app-frame-header-realtime-notice-owner="hertzbeat-ui-header-realtime-notice"
+              />
+              <div className="w-[220px] rounded-[4px] border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-1">
+                <HzHeaderMenuAction
+                  label="Exit fullscreen"
+                  state="active"
+                  data-hz-control-baseline-component="HzHeaderMenuAction"
+                  data-hz-ui-lab-header-fullscreen-action="shared"
+                  data-app-frame-settings-fullscreen-action="angular-toggle"
+                >
+                  <Minimize2 size={14} data-hz-ui-lab-header-fullscreen-icon="exit" />
+                </HzHeaderMenuAction>
+                <HzLocaleMenuOption
+                  abbr="CN"
+                  label="Simplified Chinese"
+                  selected
+                  data-hz-control-baseline-component="HzLocaleMenuOption"
+                  data-hz-ui-lab-locale-menu-option="shared"
+                  data-hz-ui-lab-locale-reload="angular-load-use-layout-reload"
+                  data-app-frame-locale-option="zh-CN"
+                  data-app-frame-locale-reload-contract="angular-load-use-layout-reload"
+                  data-app-frame-locale-reload-owner="route-locale-reload-contract"
+                />
+              </div>
+              <div className="w-[180px] rounded-[4px] border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-1">
+                <div
+                  data-hz-ui-lab-user-logout-lifecycle-contract="angular-clear-then-passport-login"
+                  data-app-frame-user-logout-lifecycle-contract="angular-clear-then-passport-login"
+                  data-app-frame-user-logout-lifecycle-owner="route-session-contract"
+                />
+                <HzUserMenuAction
+                  item="setting"
+                  label="System settings"
+                  data-hz-control-baseline-component="HzUserMenuAction"
+                  data-hz-ui-lab-user-menu-action="setting"
+                  data-app-frame-user-action="setting"
+                >
+                  <Wrench size={14} data-hz-ui-lab-user-menu-icon="tool" />
+                </HzUserMenuAction>
+                <HzUserMenuAction
+                  item="logout"
+                  label="Logout"
+                  data-hz-control-baseline-component="HzUserMenuAction"
+                  data-hz-ui-lab-user-menu-action="logout"
+                  data-hz-ui-lab-user-logout-lifecycle="angular-clear-then-passport-login"
+                  data-app-frame-user-action="logout"
+                  data-app-frame-user-logout-lifecycle="angular-clear-then-passport-login"
+                  data-app-frame-user-logout-lifecycle-owner="route-session-contract"
+                >
+                  <LogOut size={14} data-hz-ui-lab-user-menu-icon="logout" />
+                </HzUserMenuAction>
+              <HzUserMenuAction
+                item="about"
+                label="About"
+                data-hz-control-baseline-component="HzUserMenuAction"
+                data-hz-ui-lab-user-menu-action="about"
+                  data-app-frame-user-action="about"
+              >
+                <MapPin size={14} data-hz-ui-lab-user-menu-icon="environment" />
+              </HzUserMenuAction>
+            </div>
+              <div
+                data-hz-ui-lab-settings-shell="angular-settings-shell"
+                data-settings-console-shell-owner="shared-settings-console-shell"
+                className="min-w-0"
+              >
+                <SettingsConsoleShell
+                  activeHref="/setting/settings/config"
+                  contentLabel="Settings content"
+                  kicker="Settings"
+                  navigationLabel="Settings sections"
+                  title="Settings console"
+                  subtitle="Angular-compatible settings navigation with config, server, object store, and token sections."
+                  items={[
+                    { href: '/setting/settings/config', label: 'System config' },
+                    { href: '/setting/settings/server', label: 'Message server' },
+                    { href: '/setting/settings/object-store', label: 'Object store' },
+                    { href: '/setting/settings/token', label: 'Token management' }
+                  ]}
+                >
+                  <div
+                    data-hz-ui-lab-settings-active-title="angular-active-child-title"
+                    data-settings-console-active-title-owner="shared-settings-console-shell"
+                    className="text-[13px] font-semibold text-[#eef2f7]"
+                  >
+                    System config
+                  </div>
+                </SettingsConsoleShell>
+              </div>
+              <HzInlineFeedback
+                tone="success"
+                title="Applied successfully"
+                description="System config save reloads language, theme, and workbench state after the Angular /config/system apply contract."
+                meta="zh_CN -> zh-CN"
+                variant="embedded"
+                data-hz-control-baseline-component="HzInlineFeedback"
+                data-hz-ui-lab-setting-config-apply="angular-apply-notify-reload"
+                data-setting-config-apply-feedback-owner="hertzbeat-ui-inline-feedback"
+                data-setting-config-runtime-locale="underscore-to-hyphen"
+              />
+              <SettingsForm
+                data-hz-ui-lab-setting-config-selects="angular-400px-centered-bold"
+                data-setting-config-form="cold-settings-form"
+                data-setting-config-select-contract="angular-400px-centered-bold"
+                data-setting-config-timezone-search-contract="angular-nz-show-search"
+                data-setting-config-timezone-dropdown-width-contract="angular-dropdown-match-select-width-false"
+              >
+                <SettingsFormField label="System language">
+                  <SettingsFormSelect value="zh_CN" data-setting-config-select-kind="locale" onChange={() => undefined}>
+                    <option value="en_US">English(en_US)</option>
+                    <option value="zh_CN">Simplified Chinese(zh_CN)</option>
+                  </SettingsFormSelect>
+                </SettingsFormField>
+                <SettingsFormField label="System timezone">
+                  <SettingsFormSelect
+                    value="Asia/Shanghai"
+                    searchable
+                    searchPlaceholder="System timezone"
+                    data-setting-config-select-kind="timezone"
+                    data-hz-ui-lab-setting-config-timezone-search="angular-nz-show-search"
+                    onChange={() => undefined}
+                  >
+                    <option value="Asia/Shanghai">Asia/Shanghai (+08:00) Shanghai</option>
+                    <option value="UTC">UTC (+00:00) UTC</option>
+                  </SettingsFormSelect>
+                </SettingsFormField>
+                <SettingsFormField label="System theme">
+                  <SettingsFormSelect value="dark-ops" data-setting-config-select-kind="theme" onChange={() => undefined}>
+                    <option value="light-ops">Default theme</option>
+                    <option value="dark-ops">Dark theme</option>
+                    <option value="compact">Compact theme</option>
+                  </SettingsFormSelect>
+                </SettingsFormField>
+              </SettingsForm>
+              <HzInlineFeedback
+                tone="success"
+                title="Applied successfully"
+                description="Object store saves /config/oss with the Angular apply notification contract."
+                meta="OBS / hertzbeat"
+                variant="embedded"
+                data-hz-control-baseline-component="HzInlineFeedback"
+                data-hz-ui-lab-object-store-apply="angular-apply-notify"
+                data-setting-object-store-apply-feedback-owner="hertzbeat-ui-inline-feedback"
+              />
+              <SettingsForm
+                data-hz-ui-lab-object-store-provider-reset="angular-reset-config-on-type-change"
+                data-setting-object-store-type-change-contract="angular-reset-config-on-type-change"
+                data-setting-object-store-provider-select-contract="angular-centered-bold-dropdown"
+              >
+                <SettingsFormField label="File service provider">
+                  <SettingsFormSelect
+                    value="OBS"
+                    data-setting-object-store-provider-select="angular-centered-bold-dropdown"
+                    data-setting-object-store-type-change="angular-reset-config-on-type-change"
+                    onChange={() => undefined}
+                  >
+                    <option value="DATABASE">Database</option>
+                    <option value="FILE">Local file</option>
+                    <option value="OBS">Huawei OBS</option>
+                  </SettingsFormSelect>
+                </SettingsFormField>
+                <div data-hz-ui-lab-object-store-obs-fields="angular-obs-fields-required">
+                  <SettingsFormField label="AccessKey">
+                    <SettingsFormInput value="ak" readOnly />
+                  </SettingsFormField>
+                </div>
+              </SettingsForm>
+              <HzInlineFeedback
+                tone="success"
+                title="Applied successfully"
+                description="Message server saves /config/email and /config/sms with the Angular apply notification contract."
+                meta="email + sms"
+                variant="embedded"
+                data-hz-control-baseline-component="HzInlineFeedback"
+                data-hz-ui-lab-setting-server-apply="angular-apply-notify"
+                data-settings-server-apply-feedback-owner="hertzbeat-ui-inline-feedback"
+              />
+              <div
+                className="w-[min(92vw,520px)] rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-4 md:w-[40vw]"
+                data-hz-ui-lab-setting-server-dialog-layout="angular-width-40-percent"
+                data-settings-server-dialog-width-contract="angular-width-40-percent"
+                data-settings-server-dialog-field-layout-contract="angular-label-7-control-12"
+                data-settings-server-dialog-mask-contract="angular-mask-closable-false"
+              >
+                <SettingsDialogForm data-hz-ui-lab-setting-server-dialog-form="angular-label-7-control-12">
+                  <SettingsDialogField label="Email server address" required>
+                    <SettingsDialogInput value="smtp.example.com" readOnly />
+                  </SettingsDialogField>
+                  <SettingsDialogField label="Email port" required>
+                    <SettingsDialogInput value="587" readOnly />
+                  </SettingsDialogField>
+                  <SettingsDialogField label="SMS type" required>
+                    <SettingsDialogSelect value="tencent" onChange={() => undefined}>
+                      <option value="tencent">Tencent SMS</option>
+                      <option value="twilio">Twilio Sms</option>
+                    </SettingsDialogSelect>
+                  </SettingsDialogField>
+                  <SettingsDialogField label="Enable" required>
+                    <SettingsDialogToggle checked onCheckedChange={() => undefined} />
+                  </SettingsDialogField>
+                </SettingsDialogForm>
+              </div>
+              <div
+                className="grid gap-2 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-3"
+                data-hz-ui-lab-setting-token-generate="angular-generate-token-modal"
+                data-setting-token-generate-owner="hertzbeat-ui-inline-feedback"
+                data-setting-token-generate-dialog-layout-contract="angular-vertical-form"
+                data-setting-token-generated-dialog-width-contract="angular-width-50-percent"
+                data-setting-token-generated-dialog-mask-contract="angular-mask-closable-false"
+              >
+                <SettingsDialogForm
+                  data-hz-ui-lab-setting-token-generate-form="angular-vertical-form"
+                  data-setting-token-generate-form-layout="angular-vertical-form"
+                >
+                  <SettingsDialogField label="Token name" required layout="vertical">
+                    <SettingsDialogInput value="codex-preview-token" readOnly />
+                  </SettingsDialogField>
+                  <SettingsDialogField label="Expire time" layout="vertical">
+                    <SettingsDialogSelect value="-1" onChange={() => undefined}>
+                      <option value="-1">Never expire</option>
+                      <option value="604800">7 days</option>
+                    </SettingsDialogSelect>
+                  </SettingsDialogField>
+                </SettingsDialogForm>
+                <div className="flex flex-wrap items-center justify-between gap-2">
+                  <div>
+                    <div className="text-[12px] font-semibold text-[#f5f7fb]">Generated token</div>
+                    <div className="text-[11px] text-[#98a2b3]">Visible once after /account/token/generate succeeds.</div>
+                  </div>
+                  <HzButton size="sm" variant="secondary" data-hz-ui-lab-setting-token-copy-action="shared">
+                    Copy
+                  </HzButton>
+                </div>
+                <code className="block rounded-[3px] border border-[#2b3039] bg-[#101217] px-2 py-1.5 text-[12px] text-[#dbe4f0]">
+                  hb_generated_token_xxx
+                </code>
+                <HzInlineFeedback
+                  tone="success"
+                  title="Copy Success!"
+                  variant="embedded"
+                  data-hz-control-baseline-component="HzInlineFeedback"
+                  data-hz-ui-lab-setting-token-copy-feedback="angular-copy-notify"
+                  data-setting-token-copy-feedback-owner="hertzbeat-ui-inline-feedback"
+                />
+                <HzInlineFeedback
+                  tone="critical"
+                  title="Delete token requires confirmation"
+                  description="Token deletion keeps Angular modal.confirm semantics before DELETE /account/token/{id}."
+                  variant="embedded"
+                  data-hz-control-baseline-component="HzInlineFeedback"
+                  data-hz-ui-lab-setting-token-delete-confirm="angular-modal-confirm"
+                  data-setting-token-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+                />
+                <HzInlineFeedback
+                  tone="warning"
+                  title="Token list is temporarily unavailable"
+                  description="The token route keeps Angular loadFailed + retry behavior instead of falling into a generic workbench error shell."
+                  variant="embedded"
+                  data-hz-control-baseline-component="HzInlineFeedback"
+                  data-hz-ui-lab-setting-token-load-failure="angular-load-failed-retry"
+                  data-setting-token-load-failure-owner="hertzbeat-ui-inline-feedback"
+                />
+              </div>
+              <HzPassportLockSurface
+                title="Unlock"
+                passwordLabel="Password"
+                passwordPlaceholder="Input password"
+                buttonLabel="Unlock"
+                password=""
+                avatarSrc="./assets/img/avatar.svg"
+                avatarAlt="ops-admin"
+                disabled
+                data-hz-control-baseline-component="HzPassportLockSurface"
+                data-hz-ui-lab-passport-lock="shared"
+                data-hz-ui-lab-passport-lock-avatar="angular-settings-user-avatar"
+                data-hz-ui-lab-passport-lock-session="angular-lock-preserve-session"
+                data-passport-lock-panel-owner="hertzbeat-ui-passport-lock"
+                data-passport-lock-session-contract="angular-lock-preserve-session"
+                data-hz-ui-lab-passport-lock-submit-lifecycle="angular-mark-dirty-required-then-dashboard"
+                data-passport-lock-submit-lifecycle-contract="angular-mark-dirty-required-then-dashboard"
+                data-passport-lock-submit-lifecycle-owner="hertzbeat-ui-passport-lock"
+                data-passport-lock-redirect-contract="angular-dashboard-next-overview"
+                data-hz-ui-lab-passport-lock-required-mode="angular-required-no-trim"
+                data-passport-lock-required-mode-contract="angular-required-no-trim"
+                data-passport-lock-required-mode-owner="hertzbeat-ui-passport-lock"
+              />
+              <HzPassportLoginActionFrame
+                className="grid gap-3 rounded-[4px] border border-[#2b3039] bg-[#101217] p-3"
+                data-hz-control-baseline-component="HzPassportLoginActionFrame"
+                data-hz-ui-lab-passport-login-submit-lifecycle="angular-required-default-warning-session-bootstrap-redirect"
+                data-hz-ui-lab-passport-login-required-mode="angular-required-no-trim"
+                data-hz-ui-lab-passport-login-session-user-name="angular-raw-identifier"
+                data-hz-ui-lab-passport-login-default-password-lifecycle="angular-sticky-until-submit"
+                data-hz-ui-lab-passport-login-startup-failure="angular-exception-500"
+                data-hz-ui-lab-passport-login-redirect-fallback="angular-root-fallback"
+                data-passport-login-submit-lifecycle-contract="angular-required-default-warning-session-bootstrap-redirect"
+                data-passport-login-required-mode-contract="angular-required-no-trim"
+                data-passport-login-required-mode-owner="hertzbeat-ui-passport-login-action"
+                data-passport-login-session-user-name-contract="angular-raw-identifier"
+                data-passport-login-session-user-name-owner="hertzbeat-ui-passport-login-action"
+                data-passport-login-default-password-lifecycle-contract="angular-sticky-until-submit"
+                data-passport-login-startup-failure-contract="angular-exception-500"
+                data-passport-login-redirect-fallback-contract="angular-root-fallback"
+                data-passport-login-submit-lifecycle-owner="hertzbeat-ui-passport-login-action"
+              >
+                <input
+                  readOnly
+                  value="ops-admin"
+                  className="h-9 rounded-[3px] border border-[#2b3039] bg-[#0b0c0e] px-3 text-[12px] text-[#dbe4f0]"
+                  data-hz-ui-lab-passport-login-identifier="required"
+                />
+                <input
+                  readOnly
+                  value="custom-secret"
+                  type="password"
+                  className="h-9 rounded-[3px] border border-[#2b3039] bg-[#0b0c0e] px-3 text-[12px] text-[#dbe4f0]"
+                  data-hz-ui-lab-passport-login-credential="required"
+                />
+                <div className="flex items-center justify-between gap-3">
+                  <HzCheckbox checked readOnly label="Remember me" data-hz-ui-lab-passport-login-remember="true" />
+                  <HzButton size="sm" variant="primary" data-hz-ui-lab-passport-login-submit="shared">
+                    Login
+                  </HzButton>
+                </div>
+              </HzPassportLoginActionFrame>
+              <HzPassportSessionClearFrame
+                className="rounded-[4px] border border-[#2b3039] bg-[#101217] p-3 text-[12px] text-[#dbe4f0]"
+                data-hz-control-baseline-component="HzPassportSessionClearFrame"
+                data-hz-ui-lab-passport-session-clear="angular-token-service-clear-on-passport-entry"
+                data-passport-session-clear-contract="angular-token-service-clear-on-passport-entry"
+                data-passport-session-clear-owner="hertzbeat-ui-passport-session-clear"
+              >
+                Passport entry clears the client session marker and cached user snapshot before credential login.
+              </HzPassportSessionClearFrame>
+              <HzPassportLoginNotice
+                copy="Please update the initial default password in time!"
+                href="https://hertzbeat.apache.org/docs/start/account-modify"
+                data-hz-control-baseline-component="HzPassportLoginNotice"
+                data-hz-ui-lab-passport-login-notice="shared"
+                data-passport-login-notice-owner="hertzbeat-ui-passport-login-notice"
+              />
+              <HzPassportLoginValidationNotice
+                title="attention"
+                copy="Please enter your username"
+                data-hz-control-baseline-component="HzPassportLoginValidationNotice"
+                data-hz-ui-lab-passport-login-validation="shared"
+                data-passport-login-validation-owner="hertzbeat-ui-passport-login-validation"
+              />
+              <HzAboutModalSurface
+                open
+                className="relative inset-auto z-auto min-w-[620px] bg-transparent p-0"
+                data-hz-control-baseline-component="HzAboutModalSurface"
+                data-hz-ui-lab-about-modal="shared"
+                data-hz-ui-lab-about-closable="angular-nz-closable-false"
+                data-app-frame-about-closable-contract="angular-nz-closable-false"
+                data-app-frame-about-cancel-contract="angular-on-cancel"
+                title="HertzBeat is an open source observability platform"
+                points={[
+                  'Collect metrics from applications, databases, operating systems, middleware, and network devices.',
+                  'Receive OTLP logs with resource, entity, trace, and alert context.',
+                  'Handle threshold and external alerts in one place.',
+                  'Notify the channels your operations team uses.',
+                  'Use monitoring templates to define collection protocols and thresholds.',
+                  'Scale collector clusters across private network zones.'
+                ]}
+                help="Thanks for using HertzBeat."
+                version="v1.8.0"
+                releaseHref="https://github.com/apache/hertzbeat/releases/tag/v1.8.0"
+                copyright="Copyright © 2026 | Apache HertzBeat™"
+                notShowLabel="Do not show next login"
+                notShowChecked
+                onNotShowChange={() => undefined}
+                closeLabel="Close dialog"
+                communityLinks={[
+                  { href: 'https://github.com/apache/hertzbeat', label: 'GitHub' },
+                  { href: 'https://github.com/apache/hertzbeat/issues', label: 'Issue' },
+                  { href: 'https://hertzbeat.apache.org/docs/', label: 'Doc' },
+                  { href: 'https://hertzbeat.apache.org/docs/start/upgrade', label: 'Upgrade' }
+                ]}
+              />
+              <HzPanelSurface
+                data-hz-control-baseline-component="HzPanelSurface"
+                data-hz-ui-lab-panel-surface="shared"
+                data-hz-ui-lab-panel-surface-padding="query"
+                className="min-w-[132px]"
+                padding="query"
+              >
+                <span className="block truncate text-[11px] font-semibold text-[#98a2b3]">Panel</span>
+              </HzPanelSurface>
+              <HzPanelSurface
+                data-hz-control-baseline-component="HzPanelSurface"
+                data-hz-ui-lab-panel-surface="shared"
+                data-hz-ui-lab-panel-surface-padding="header"
+                data-hz-ui-lab-log-header-panel-surface="shared"
+                data-log-header-panel-surface-owner="hertzbeat-ui-panel-surface"
+                className="min-w-[148px]"
+                padding="header"
+              >
+                <span className="block truncate text-[11px] font-semibold text-[#98a2b3]">Header panel</span>
+              </HzPanelSurface>
+              <HzPanelSurface
+                data-hz-control-baseline-component="HzPanelSurface"
+                data-hz-ui-lab-panel-surface="shared"
+                data-hz-ui-lab-panel-surface-padding="chart"
+                data-hz-ui-lab-log-chart-panel-surface="shared"
+                data-log-chart-panel-surface-owner="hertzbeat-ui-panel-surface"
+                className="min-w-[142px]"
+                padding="chart"
+              >
+                <span className="block truncate text-[11px] font-semibold text-[#98a2b3]">Chart panel</span>
+              </HzPanelSurface>
+              <HzAiChatModalSurface
+                data-hz-control-baseline-component="HzAiChatModalSurface"
+                data-hz-ui-lab-ai-chat-modal="shared"
+                data-hz-ui-lab-ai-chat-initial-message="angular-open-modal-initial-message"
+                data-hz-ui-lab-ai-chat-config-save-lifecycle="angular-validate-save-close-refresh"
+                data-hz-ui-lab-ai-chat-conversation-action-lifecycle="angular-create-select-delete-fallback"
+                data-hz-ui-lab-ai-chat-schedule-action-lifecycle="angular-load-create-toggle-revert-confirm-update-delete"
+                data-hz-ui-lab-ai-chat-stream-history-lifecycle="angular-push-user-placeholder-sse-skill-report-refresh"
+                data-app-frame-ai-chat-owner="hertzbeat-ui-ai-chat-modal"
+                data-app-frame-ai-chat-initial-message-contract="angular-open-modal-initial-message"
+                data-app-frame-ai-chat-initial-message-owner="route-ai-chat-modal-contract"
+                data-app-frame-ai-chat-config-save-lifecycle-contract="angular-validate-save-close-refresh"
+                data-app-frame-ai-chat-config-save-lifecycle-owner="route-ai-chat-config-contract"
+                data-app-frame-ai-chat-conversation-action-lifecycle-contract="angular-create-select-delete-fallback"
+                data-app-frame-ai-chat-conversation-action-lifecycle-owner="route-ai-chat-conversation-contract"
+                data-app-frame-ai-chat-schedule-action-lifecycle-contract="angular-load-create-toggle-revert-confirm-update-delete"
+                data-app-frame-ai-chat-schedule-action-lifecycle-owner="route-ai-chat-schedule-contract"
+                data-app-frame-ai-chat-stream-history-lifecycle-contract="angular-push-user-placeholder-sse-skill-report-refresh"
+                data-app-frame-ai-chat-stream-history-lifecycle-owner="route-ai-chat-stream-contract"
+                className="relative inset-auto z-auto h-[520px] min-w-[680px] bg-transparent p-0"
+                title="AI assistant"
+                subtitle="Angular shell modal parity"
+                conversationsTitle="Conversations"
+                newChatLabel="New chat"
+                newChatStatus="idle"
+                deleteLabel="Delete conversation"
+                deleteConfirmLabel="Delete"
+                deleteCancelLabel="Cancel"
+                deleteStatus="confirming"
+                deleteConversationId="collector-review"
+                welcomeTitle="Ask HertzBeat"
+                welcomeDescription="Use the shared shell for conversation list, message flow, and prompt entry before wiring route-specific streaming behavior."
+                inputPlaceholder="Ask assistant"
+                inputValue="Summarize checkout errors"
+                inputHint="Shared modal surface, route wiring supplies the live stream."
+                closeLabel="Close dialog"
+                sendLabel="Send prompt"
+                sendStatus="sending"
+                streamingLabel="Typing..."
+                configOpen
+                configTitle="AI Provider Configuration"
+                configDescription="Configure the provider used by the AI assistant."
+                configStatus="ready"
+                configStatusLabel="Provider configuration loaded from /config/provider"
+                configTriggerLabel="Modify API key"
+                configProviderLabel="AI Provider"
+                configProviderHelp="Choose your AI provider (OpenAI, ZhiPu, or ZAI)"
+                configApiKeyLabel="API Key"
+                configApiKeyHelp="Your Provider API key. The key will be validated when saved."
+                configBaseUrlLabel="Base URL"
+                configBaseUrlHelp="Custom API endpoint URL. Leave empty to use default for selected provider."
+                configModelLabel="Model"
+                configModelHelp="Model name to use. Leave empty to use default for selected provider."
+                configResetLabel="Reset to default values"
+                configSaveLabel="Save"
+                configCancelLabel="Cancel"
+                configProviderOptions={[
+                  { value: 'openai', label: 'OpenAI', defaultBaseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4' },
+                  { value: 'zai', label: 'ZAI', defaultBaseUrl: 'https://api.z.ai/api/paas/v4', defaultModel: 'glm-4.6' },
+                  { value: 'zhipu', label: 'ZhiPu', defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4', defaultModel: 'glm-4.6' }
+                ]}
+                configValue={{
+                  code: 'openai',
+                  apiKey: 'sk-lab',
+                  baseUrl: 'https://api.openai.com/v1',
+                  model: 'gpt-4'
+                }}
+                scheduleOpen
+                scheduleStatus="ready"
+                scheduleStatusLabel="Schedules loaded from /ai/schedule/conversation/7"
+                scheduleTriggerLabel="Schedule"
+                scheduleTitle="Schedule Configuration"
+                scheduleConfiguredTitle="Configured Schedules"
+                scheduleCreateTitle="Create Schedule"
+                scheduleAddTitle="Add New Task"
+                scheduleSkillLabel="Skill"
+                scheduleSkillSelectLabel="Select Skill"
+                scheduleSkillPlaceholder="Select skill to execute"
+                scheduleCronLabel="Execution Time (Cron Expression)"
+                scheduleCronHelp="Format: seconds minutes hours day month week"
+                scheduleCronCommonLabel="Common: daily 9am"
+                scheduleCronMondayLabel="Monday 9am"
+                scheduleStatusColumnLabel="Status"
+                scheduleActionLabel="Action"
+                scheduleEditLabel="Edit"
+                scheduleDeleteLabel="Delete"
+                scheduleDeleteConfirmLabel="Delete"
+                scheduleDeleteCancelLabel="Cancel"
+                scheduleDeleteStatus="confirming"
+                scheduleDeleteScheduleId="daily-health"
+                scheduleSaveLabel="Save"
+                scheduleCancelLabel="Cancel"
+                scheduleCreateLabel="Create Schedule"
+                scheduleRows={[
+                  { id: 'daily-health', sopName: 'daily-health', cronExpression: '0 0 9 * * ?', enabled: true },
+                  { id: 'weekly-report', sopName: 'weekly-report', cronExpression: '0 0 9 ? * MON', enabled: false }
+                ]}
+                scheduleSkills={[
+                  { value: 'daily-health', label: 'daily-health - Daily health report' },
+                  { value: 'weekly-report', label: 'weekly-report - Weekly report' }
+                ]}
+                scheduleDraft={{ sopName: 'daily-health', cronExpression: '0 0 9 * * ?', enabled: true }}
+                scheduleEditDraft={{ id: 'weekly-report', sopName: 'weekly-report', cronExpression: '0 0 9 ? * MON', enabled: false }}
+                initialMessageLabel="Initial request"
+                initialMessage="Show unhealthy monitors"
+                previewMessages={[
+                  { role: 'assistant', label: 'Assistant', content: 'I can inspect monitors, alerts, logs, traces, and settings.' }
+                ]}
+                conversationMessages={[
+                  { role: 'user', label: 'User', content: 'Why did checkout latency spike?' },
+                  { role: 'assistant', label: 'Assistant', content: 'The selected history loads into the shared message flow before streaming proof.' },
+                  { role: 'assistant', label: 'Assistant', content: '' }
+                ]}
+                messageStatus="ready"
+                messageStatusLabel="Selected conversation history loaded from /api/chat/conversations/{id}"
+                conversationStatus="ready"
+                conversationStatusLabel="2 conversations loaded from /api/chat/conversations"
+                conversations={[
+                  { id: 'checkout-errors', title: 'Investigate checkout errors', subtitle: 'Updated just now', active: true },
+                  { id: 'collector-review', title: 'Collector health review', subtitle: '2 hours ago' }
+                ]}
+                onConversationDeleteRequest={() => {}}
+                onConversationDeleteCancel={() => {}}
+                onConversationDeleteConfirm={() => {}}
+                onInputChange={() => {}}
+                onSendMessage={() => {}}
+                onConfigOpen={() => {}}
+                onConfigClose={() => {}}
+                onConfigSave={() => {}}
+                onConfigResetDefaults={() => {}}
+                onConfigChange={() => {}}
+                onScheduleOpen={() => {}}
+                onScheduleClose={() => {}}
+                onScheduleDraftChange={() => {}}
+                onScheduleCreate={() => {}}
+                onScheduleToggle={() => {}}
+                onScheduleEditStart={() => {}}
+                onScheduleEditCancel={() => {}}
+                onScheduleEditChange={() => {}}
+                onScheduleUpdate={() => {}}
+                onScheduleDeleteRequest={() => {}}
+                onScheduleDeleteCancel={() => {}}
+                onScheduleDeleteConfirm={() => {}}
+              />
+              <HzStateNotice
+                data-hz-control-baseline-component="HzStateNotice"
+                data-hz-ui-lab-handoff-hint-surface="shared"
+                tone="info"
+                title="Handoff context follows the selected signal."
+                variant="hint"
+                className="min-w-[220px] max-w-[260px]"
+              />
+              <HzStatusBadge
+                data-hz-control-baseline-component="HzStatusBadge"
+                data-hz-ui-lab-attribution-diagnostic-badge="shared"
+                tone="success"
+                size="xs"
+              >
+                Present
+              </HzStatusBadge>
+              <HzStatusBadge
+                data-hz-control-baseline-component="HzStatusBadge"
+                data-hz-ui-lab-trace-table-count-badge="shared"
+                tone="neutral"
+                size="xs"
+              >
+                12 traces
+              </HzStatusBadge>
+              <HzStatusBadge
+                data-hz-control-baseline-component="HzStatusBadge"
+                data-hz-ui-lab-stream-status-badge="shared"
+                tone="warning"
+                size="md"
+              >
+                Connecting
+              </HzStatusBadge>
+              <div className="min-w-[132px]" data-hz-control-baseline-component="HzSelect">
+                <HzSelect
+                  aria-label="Baseline select"
+                  value="manual"
+                  options={[
+                    { value: 'manual', label: 'Manual' },
+                    { value: 'auto', label: 'Auto' }
+                  ]}
+                  onChange={() => setContextMessage('Baseline select')}
+                />
+              </div>
+              <HzInput
+                className="w-[150px]"
+                value="collector-a"
+                readOnly
+                data-hz-control-baseline-component="HzInput"
+                aria-label="Baseline input"
+              />
+              <div
+                className="hb-scrollbar h-7 w-[148px] overflow-x-auto border border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-control)] px-2"
+                data-hz-ui-lab-scrollbar-baseline="thin-dark-platform"
+                data-hz-ui-lab-scrollbar-owner="hertzbeat-ui-scrollbar"
+              >
+                <span className="whitespace-nowrap font-mono text-[10px] leading-7 text-[#8f99ab]">Thin dark scrollbar baseline</span>
+              </div>
+              <HzScrollViewport
+                variant="log-stream"
+                data-hz-ui-lab-log-stream-viewport="shared"
+                data-log-stream-viewport-owner="hertzbeat-ui-scroll-viewport"
+                className="h-24 w-[150px]"
+              >
+                <div className="divide-y divide-[var(--hz-ui-line-soft)]">
+                  {['checkout timeout', 'trace linked', 'collector ok', 'service ready'].map(item => (
+                    <div key={item} className="px-2 py-2 font-mono text-[11px] text-[var(--hz-ui-text-muted)]">
+                      {item}
+                    </div>
+                  ))}
+                </div>
+              </HzScrollViewport>
+              <HzControlStack
+                className="w-[150px]"
+                data-hz-ui-lab-control-stack="shared"
+                data-monitor-realtime-search-stack-owner="hertzbeat-ui-control-stack"
+              >
+                <HzInput
+                  value="basic"
+                  readOnly
+                  data-hz-control-baseline-component="HzInput"
+                  aria-label="Realtime row search"
+                />
+                <HzDataMetaText
+                  display="block"
+                  casing="plain"
+                  data-monitor-realtime-search-count-owner="hertzbeat-ui-data-meta-text"
+                >
+                  3 / 8 rows
+                </HzDataMetaText>
+              </HzControlStack>
+              <HzPanelSurface
+                padding="query"
+                data-hz-ui-lab-metrics-query-panel-surface="shared"
+                data-metrics-query-panel-surface-owner="hertzbeat-ui-panel-surface"
+                className="w-full max-w-[720px]"
+              >
+                <HzControlStack
+                  layout="inline-wrap"
+                  data-hz-ui-lab-metrics-query-panel-control-stack="shared"
+                  data-metrics-query-panel-control-stack-owner="hertzbeat-ui-control-stack"
+                >
+                  <HzSearchFieldFrame
+                    width="metrics-query"
+                    icon={(
+                      <HzSearchFieldIcon
+                        icon={Search}
+                        data-hz-ui-lab-metrics-query-panel-search-icon="shared"
+                        data-metrics-query-panel-search-icon-owner="hertzbeat-ui-search-field-icon"
+                      />
+                    )}
+                    data-hz-ui-lab-metrics-query-panel-search-frame="shared"
+                    data-metrics-query-panel-search-frame-owner="hertzbeat-ui-search-field-frame"
+                  >
+                    <HzInput
+                      value="http.server.duration"
+                      readOnly
+                      inset="search-icon"
+                      width="metrics-query-expression"
+                      data-hz-ui-lab-metrics-query-panel-input="shared"
+                      data-metrics-query-panel-input-owner="hertzbeat-ui-input"
+                      aria-label="Metrics panel query expression"
+                    />
+                  </HzSearchFieldFrame>
+                  <HzSelect
+                    aria-label="Metrics panel aggregation"
+                    value="avg"
+                    width="metrics-aggregation"
+                    data-hz-ui-lab-metrics-query-panel-aggregation-select="shared"
+                    data-metrics-query-panel-aggregation-select-owner="hertzbeat-ui-select"
+                    options={[
+                      { value: 'avg', label: 'Avg' },
+                      { value: 'sum', label: 'Sum' },
+                      { value: 'max', label: 'Max' }
+                    ]}
+                  />
+                  <HzQueryActionGroup
+                    data-hz-ui-lab-metrics-query-panel-action-group="shared"
+                    data-metrics-query-panel-action-group-owner="hertzbeat-ui-query-action-group"
+                  >
+                    <HzButton size="md" intent="primary" onClick={clearFilterClauses}>
+                      <HzButtonIcon
+                        icon={Play}
+                        data-hz-ui-lab-metrics-query-panel-action-icon="run"
+                        data-metrics-query-panel-action-icon-owner="hertzbeat-ui-button-icon"
+                      />
+                      Run
+                    </HzButton>
+                    <HzButtonLink href="#" size="md">
+                      <HzButtonIcon
+                        icon={RotateCcw}
+                        data-hz-ui-lab-metrics-query-panel-action-icon="reset"
+                        data-metrics-query-panel-action-icon-owner="hertzbeat-ui-button-icon"
+                      />
+                      Reset
+                    </HzButtonLink>
+                  </HzQueryActionGroup>
+                </HzControlStack>
+                <HzControlStack
+                  layout="inline-wrap"
+                  spacing="top-2"
+                  data-hz-ui-lab-metrics-query-panel-context-stack="shared"
+                  data-metrics-query-panel-context-stack-owner="hertzbeat-ui-control-stack"
+                >
+                  <HzInput
+                    value="checkout-service"
+                    readOnly
+                    width="metrics-context"
+                    data-hz-ui-lab-metrics-query-panel-context-input="service-name"
+                    data-metrics-query-panel-context-input-owner="hertzbeat-ui-input"
+                    aria-label="Metrics panel service filter"
+                  />
+                  <HzInput
+                    value="storefront"
+                    readOnly
+                    width="metrics-context"
+                    data-hz-ui-lab-metrics-query-panel-context-input="namespace"
+                    data-metrics-query-panel-context-input-owner="hertzbeat-ui-input"
+                    aria-label="Metrics panel namespace filter"
+                  />
+                  <HzInput
+                    value="dev"
+                    readOnly
+                    width="metrics-context-compact"
+                    data-hz-ui-lab-metrics-query-panel-context-input="environment"
+                    data-metrics-query-panel-context-input-owner="hertzbeat-ui-input"
+                    aria-label="Metrics panel environment filter"
+                  />
+                  <HzInput
+                    value="trace-20260524"
+                    readOnly
+                    width="metrics-trace-id"
+                    data-hz-ui-lab-metrics-query-panel-context-input="trace-id"
+                    data-metrics-query-panel-context-input-owner="hertzbeat-ui-input"
+                    aria-label="Metrics panel trace filter"
+                  />
+                </HzControlStack>
+              </HzPanelSurface>
+              <HzPanelSurface
+                padding="query"
+                data-hz-ui-lab-log-query-panel-surface="shared"
+                data-log-query-panel-surface-owner="hertzbeat-ui-panel-surface"
+                className="w-full max-w-[760px]"
+              >
+                <HzControlStack
+                  layout="inline-wrap"
+                  data-hz-ui-lab-log-query-panel-control-stack="shared"
+                  data-log-query-panel-control-stack-owner="hertzbeat-ui-control-stack"
+                >
+                  <HzSearchFieldFrame
+                    width="log-query"
+                    icon={(
+                      <HzSearchFieldIcon
+                        icon={Search}
+                        data-hz-ui-lab-log-query-panel-search-icon="shared"
+                        data-log-query-panel-search-icon-owner="hertzbeat-ui-search-field-icon"
+                      />
+                    )}
+                    data-hz-ui-lab-log-query-panel-search-frame="shared"
+                    data-log-query-panel-search-frame-owner="hertzbeat-ui-search-field-frame"
+                  >
+                    <HzInput
+                      value="service.name = checkout"
+                      readOnly
+                      inset="search-icon"
+                      width="log-query-expression"
+                      data-hz-ui-lab-log-query-panel-search-input="shared"
+                      data-log-query-panel-search-input-owner="hertzbeat-ui-input"
+                      aria-label="Log query panel search"
+                    />
+                  </HzSearchFieldFrame>
+                  <HzSelect
+                    aria-label="Log query panel severity"
+                    value="ERROR"
+                    width="log-severity"
+                    triggerTone="signal-query"
+                    data-hz-ui-lab-log-query-panel-severity-select="shared"
+                    data-log-query-panel-severity-select-owner="hertzbeat-ui-select"
+                    options={[
+                      { value: 'all', label: 'All levels' },
+                      { value: 'ERROR', label: 'ERROR' },
+                      { value: 'WARN', label: 'WARN' },
+                      { value: 'INFO', label: 'INFO' }
+                    ]}
+                  />
+                  <HzInput
+                    value="trace-heartbeat-waterfall-live"
+                    readOnly
+                    width="log-query-token"
+                    data-hz-ui-lab-log-query-panel-token-input="trace-id"
+                    data-log-query-panel-token-input-owner="hertzbeat-ui-input"
+                    aria-label="Log query panel trace id"
+                  />
+                  <HzInput
+                    value="checkout timeout"
+                    readOnly
+                    width="log-query-body"
+                    data-hz-ui-lab-log-query-panel-body-input="shared"
+                    data-log-query-panel-body-input-owner="hertzbeat-ui-input"
+                    aria-label="Log query panel body"
+                  />
+                </HzControlStack>
+              </HzPanelSurface>
+              <HzControlStack
+                layout="inline-wrap"
+                data-hz-ui-lab-inline-control-stack="shared"
+                data-signal-query-control-stack-owner="hertzbeat-ui-control-stack"
+              >
+                <HzSearchFieldFrame
+                  className="min-w-[180px] max-w-[220px]"
+                  icon={(
+                    <HzSearchFieldIcon
+                      icon={Search}
+                      data-hz-ui-lab-trace-search-field-icon="shared"
+                      data-trace-search-field-icon-owner="hertzbeat-ui-search-field-icon"
+                    />
+                  )}
+                  data-hz-ui-lab-trace-search-field-frame="shared"
+                  data-trace-search-field-frame-owner="hertzbeat-ui-search-field-frame"
+                >
+                  <HzInput
+                    value="checkout"
+                    readOnly
+                    inset="search-icon"
+                    data-hz-control-baseline-component="HzInput"
+                    data-hz-ui-lab-trace-search-input-inset="search-icon"
+                    aria-label="Trace query service"
+                  />
+                </HzSearchFieldFrame>
+                <HzSearchFieldFrame
+                  width="log-query"
+                  icon={(
+                    <HzSearchFieldIcon
+                      icon={Search}
+                      data-hz-ui-lab-log-search-field-icon="shared"
+                      data-log-search-field-icon-owner="hertzbeat-ui-search-field-icon"
+                    />
+                  )}
+                  data-hz-ui-lab-log-search-field-frame="shared"
+                  data-log-search-field-frame-owner="hertzbeat-ui-search-field-frame"
+                >
+                  <HzInput
+                    value="service.name = checkout"
+                    readOnly
+                    inset="search-icon"
+                    width="log-query-expression"
+                    data-hz-control-baseline-component="HzInput"
+                    data-hz-ui-lab-log-search-input="shared"
+                    data-log-search-input-owner="hertzbeat-ui-input"
+                    aria-label="Log query search"
+                  />
+                </HzSearchFieldFrame>
+                <HzSelect
+                  aria-label="Log severity filter"
+                  value="WARN"
+                  width="log-severity"
+                  triggerTone="signal-query"
+                  data-hz-ui-lab-log-severity-select="shared"
+                  data-log-severity-select-owner="hertzbeat-ui-select"
+                  options={[
+                    { value: 'all', label: 'All levels' },
+                    { value: 'ERROR', label: 'ERROR' },
+                    { value: 'WARN', label: 'WARN' },
+                    { value: 'INFO', label: 'INFO' }
+                  ]}
+                />
+                <HzInput
+                  value="trace-heartbeat-waterfall-live"
+                  readOnly
+                  width="log-query-token"
+                  data-hz-control-baseline-component="HzInput"
+                  data-hz-ui-lab-log-query-token-input="trace-id"
+                  data-log-query-token-input-owner="hertzbeat-ui-input"
+                  aria-label="Log query trace id"
+                />
+                <HzInput
+                  value="span-db-live"
+                  readOnly
+                  width="log-query-token"
+                  data-hz-control-baseline-component="HzInput"
+                  data-hz-ui-lab-log-query-token-input="span-id"
+                  data-log-query-token-input-owner="hertzbeat-ui-input"
+                  aria-label="Log query span id"
+                />
+                <HzInput
+                  value="checkout timeout"
+                  readOnly
+                  width="log-query-body"
+                  data-hz-control-baseline-component="HzInput"
+                  data-hz-ui-lab-log-query-body-input="shared"
+                  data-log-query-body-input-owner="hertzbeat-ui-input"
+                  aria-label="Log query body"
+                />
+                <HzSearchFieldFrame
+                  className="min-w-[180px] max-w-[240px]"
+                  icon={(
+                    <HzSearchFieldIcon
+                      icon={Search}
+                      data-hz-ui-lab-metrics-search-field-icon="shared"
+                      data-metrics-search-field-icon-owner="hertzbeat-ui-search-field-icon"
+                    />
+                  )}
+                  data-hz-ui-lab-metrics-search-field-frame="shared"
+                  data-metrics-search-field-frame-owner="hertzbeat-ui-search-field-frame"
+                >
+                  <HzInput
+                    value="http.server.duration"
+                    readOnly
+                    inset="search-icon"
+                    data-hz-control-baseline-component="HzInput"
+                    data-hz-ui-lab-metrics-search-input="shared"
+                    data-metrics-search-input-owner="hertzbeat-ui-input"
+                    aria-label="Metrics query expression"
+                  />
+                </HzSearchFieldFrame>
+                <HzInput
+                  value="checkout-service"
+                  readOnly
+                  className="w-[180px]"
+                  data-hz-control-baseline-component="HzInput"
+                  data-hz-ui-lab-metrics-context-input="service-name"
+                  data-metrics-context-input-owner="hertzbeat-ui-input"
+                  aria-label="Metrics service filter"
+                />
+                <HzInput
+                  value="storefront"
+                  readOnly
+                  className="w-[160px]"
+                  data-hz-ui-lab-metrics-context-input="namespace"
+                  data-metrics-context-input-owner="hertzbeat-ui-input"
+                  aria-label="Metrics namespace filter"
+                />
+                <HzInput
+                  value="trace-20260524"
+                  readOnly
+                  className="min-w-[180px] max-w-[240px] flex-1 font-mono"
+                  data-hz-ui-lab-metrics-context-input="trace-id"
+                  data-metrics-context-input-owner="hertzbeat-ui-input"
+                  aria-label="Metrics trace filter"
+                />
+                <HzSelect
+                  aria-label="Metrics aggregation"
+                  value="sum"
+                  width="metrics-aggregation"
+                  data-hz-ui-lab-metrics-aggregation-select="shared"
+                  data-metrics-aggregation-select-owner="hertzbeat-ui-select"
+                  options={[
+                    { value: 'avg', label: 'Avg' },
+                    { value: 'sum', label: 'Sum' },
+                    { value: 'max', label: 'Max' }
+                  ]}
+                />
+                <HzSelect
+                  aria-label="Metrics group by"
+                  value="service_name"
+                  width="metrics-group-by"
+                  data-hz-ui-lab-metrics-group-by-select="shared"
+                  data-metrics-group-by-select-owner="hertzbeat-ui-select"
+                  options={[
+                    { value: 'service_name', label: 'Service' },
+                    { value: 'service_namespace', label: 'Namespace' }
+                  ]}
+                />
+                <HzQueryActionGroup
+                  data-hz-ui-lab-metrics-query-action-group="shared"
+                  data-metrics-query-action-group-owner="hertzbeat-ui-query-action-group"
+                >
+                  <HzButton size="md" intent="primary" onClick={clearFilterClauses}>
+                    <HzButtonIcon
+                      icon={Play}
+                      data-hz-ui-lab-metrics-query-action-icon="run"
+                      data-metrics-query-action-icon-owner="hertzbeat-ui-button-icon"
+                    />
+                    Run
+                  </HzButton>
+                  <HzButtonLink href="#" size="md">
+                    <HzButtonIcon
+                      icon={RotateCcw}
+                      data-hz-ui-lab-metrics-query-action-icon="reset"
+                      data-metrics-query-action-icon-owner="hertzbeat-ui-button-icon"
+                    />
+                    Reset
+                  </HzButtonLink>
+                </HzQueryActionGroup>
+                <HzActionGroup
+                  data-hz-ui-lab-metrics-handoff-actions="shared"
+                  data-metrics-handoff-actions-owner="hertzbeat-ui-action-group"
+                  layout="grid-2"
+                >
+                  <HzButtonLink href="#" size="md" layout="full" data-hz-ui-lab-metrics-handoff-action="entity">
+                    Entity
+                  </HzButtonLink>
+                  <HzButtonLink href="#" size="md" layout="full" data-hz-ui-lab-metrics-handoff-action="alerts">
+                    Alerts
+                  </HzButtonLink>
+                  <HzDisabledActionShell
+                    title="Missing entity id"
+                    data-hz-ui-lab-metrics-disabled-handoff-owner="hertzbeat-ui-disabled-action-shell"
+                    layout="full"
+                  >
+                    <HzButton size="md" layout="full" disabled>
+                      Entity
+                    </HzButton>
+                  </HzDisabledActionShell>
+                </HzActionGroup>
+                <div
+                  className="w-[520px] border-t border-[var(--hz-ui-line-soft)] px-0 py-2"
+                  data-hz-ui-lab-metrics-series-summary="shared"
+                  data-metrics-series-summary-owner="hertzbeat-ui-stat-strip"
+                >
+                  <div className="mb-2 flex items-center justify-between gap-2 px-1">
+                    <span className="text-[12px] font-semibold text-[#8792a5]">Metrics series</span>
+                    <HzStatusBadge
+                      tone="neutral"
+                      size="xs"
+                      data-hz-ui-lab-metrics-series-count-badge="shared"
+                      data-metrics-series-count-badge-owner="hertzbeat-ui-status-badge"
+                    >
+                      2 rows
+                    </HzStatusBadge>
+                  </div>
+                  <HzStatStrip
+                    columns={4}
+                    data-hz-ui-lab-metrics-series-summary-strip="shared"
+                    data-metrics-series-summary-strip-owner="hertzbeat-ui-stat-strip"
+                    frame="panel-solid"
+                  >
+                    {[
+                      { id: 'service', label: 'Service', value: 'checkout-api' },
+                      { id: 'namespace', label: 'Namespace', value: 'storefront' },
+                      { id: 'environment', label: 'Environment', value: 'prod' },
+                      { id: 'group', label: 'Group by', value: 'service' },
+                      { id: 'series', label: 'Series', value: '2 series' }
+                    ].map(item => (
+                      <HzStatCell
+                        key={item.id}
+                        data-hz-ui-lab-metrics-series-summary-cell={item.id}
+                        data-metrics-series-summary-cell-owner="hertzbeat-ui-stat-cell"
+                        label={item.label}
+                        value={item.value}
+                        variant="tile"
+                        density="compact"
+                        frame="flush"
+                      />
+                    ))}
+                  </HzStatStrip>
+                </div>
+                <HzPanelSurface
+                  clip
+                  className="w-[520px]"
+                  data-hz-ui-lab-metrics-series-data-table="shared"
+                  data-metrics-series-data-table-owner="hertzbeat-ui-data-table"
+                  data-hz-ui-lab-metrics-series-table-panel-surface="shared"
+                  data-metrics-series-table-panel-surface-owner="hertzbeat-ui-panel-surface"
+                >
+                  <HzPanelHeader
+                    data-hz-ui-lab-metrics-series-table-header="shared"
+                    data-metrics-series-table-header-owner="hertzbeat-ui-panel-header"
+                    chrome="transparent-topless"
+                    title="Metrics series"
+                    meta={
+                      <HzStatusBadge
+                        tone="neutral"
+                        size="xs"
+                        data-hz-ui-lab-metrics-series-table-count-badge="shared"
+                        data-metrics-series-table-count-badge-owner="hertzbeat-ui-status-badge"
+                      >
+                        2 rows
+                      </HzStatusBadge>
+                    }
+                  />
+                  <HzPanelSection
+                    data-hz-ui-lab-metrics-series-summary-section="shared"
+                    data-metrics-series-summary-section-owner="hertzbeat-ui-panel-section"
+                  >
+                    <HzStatStrip
+                      columns={4}
+                      frame="panel-inset"
+                      spacing="compact"
+                      data-hz-ui-lab-metrics-series-summary-strip="shared"
+                      data-metrics-series-summary-strip-owner="hertzbeat-ui-stat-strip"
+                    >
+                      {[
+                        { label: 'Service', value: 'checkout-api' },
+                        { label: 'Namespace', value: 'storefront' },
+                        { label: 'Entity', value: 'Checkout API' },
+                        { label: 'Metric', value: 'latency' }
+                      ].map(item => (
+                        <HzStatCell
+                          key={item.label}
+                          label={item.label}
+                          value={item.value}
+                          variant="tile"
+                          density="compact"
+                          data-hz-ui-lab-metrics-series-summary-cell={item.label}
+                          data-metrics-series-summary-cell-owner="hertzbeat-ui-stat-cell"
+                          frame="inset"
+                        />
+                      ))}
+                    </HzStatStrip>
+                  </HzPanelSection>
+                  <HzPanelSection
+                    divider="none"
+                    data-hz-ui-lab-metrics-detail-body-section="shared"
+                    data-metrics-detail-body-section-owner="hertzbeat-ui-panel-section"
+                  >
+                    <HzDetailRows
+                      offset="top"
+                      heading="Selected series"
+                      data-hz-ui-lab-metrics-selected-series-context-rows="shared"
+                      data-metrics-selected-series-context-rows-owner="hertzbeat-ui-detail-rows"
+                      rows={[
+                        { label: 'Metric', value: 'checkout.latency', meta: 'real sample evidence' },
+                        { label: 'Entity', value: 'Checkout API', meta: 'entityId=7' }
+                      ]}
+                    />
+                    <HzDetailRows
+                      boundary="top"
+                      heading="Metrics evidence"
+                      data-hz-ui-lab-metrics-selected-series-evidence-rows="shared"
+                      data-metrics-selected-series-evidence-rows-owner="hertzbeat-ui-detail-rows"
+                      rows={[
+                        { key: 'sample-window', title: 'Sample window', copy: '2026-05-24 02:20', meta: 'real samples' },
+                        { key: 'trace', title: 'Trace', copy: 'trace-20260524', meta: 'selected series' }
+                      ]}
+                    />
+                  </HzPanelSection>
+                  <HzPanelSection
+                    divider="top"
+                    data-hz-ui-lab-metrics-handoff-action-section="shared"
+                    data-metrics-handoff-action-section-owner="hertzbeat-ui-panel-section"
+                  >
+                    <HzActionGroup
+                      layout="grid-2"
+                      data-hz-ui-lab-metrics-detail-handoff-actions="shared"
+                      data-metrics-detail-handoff-actions-owner="hertzbeat-ui-action-group"
+                    >
+                      <HzButtonLink href="#" size="md" layout="full">
+                        Entity
+                      </HzButtonLink>
+                      <HzButtonLink href="#" size="md" layout="full">
+                        Alerts
+                      </HzButtonLink>
+                      <HzButtonLink href="#" size="md" layout="full">
+                        Logs
+                      </HzButtonLink>
+                      <HzButtonLink href="#" size="md" layout="full">
+                        Traces
+                      </HzButtonLink>
+                    </HzActionGroup>
+                  </HzPanelSection>
+                  <HzDataTable
+                    variant="embedded"
+                    rows={[
+                      { id: 'checkout-latency', name: 'checkout.latency', service: 'checkout-api', entity: 'Checkout API', entityMeta: 'entityId=7', latest: '128ms', points: 42, time: '02:55:00' },
+                      { id: 'payment-errors', name: 'payment.errors', service: 'payment-api', entity: 'Payment API', entityMeta: 'entityId=9', latest: '2', points: 16, time: '02:54:30' }
+                    ]}
+                    getRowKey={row => row.id}
+                    selectedRowKey="checkout-latency"
+                    onRowClick={row => setContextMessage(`Metrics series · ${row.name} · ${row.service}`)}
+                    getRowProps={row => ({
+                      'data-hz-ui-lab-metrics-series-row': row.id,
+                      'data-metrics-series-row-owner': 'hertzbeat-ui-data-table'
+                    })}
+                    columns={[
+                      {
+                        key: 'metric',
+                        header: 'Metric',
+                        render: row => <HzDataCellText variant="title" display="block">{row.name}</HzDataCellText>
+                      },
+                      {
+                        key: 'service',
+                        header: 'Service',
+                        render: row => <HzDataCellText variant="value">{row.service}</HzDataCellText>
+                      },
+                      {
+                        key: 'entity',
+                        header: 'Entity',
+                        render: row => (
+                          <HzDataCellStack
+                            display="block"
+                            width="metrics-entity"
+                            data-hz-ui-lab-metrics-series-entity="shared"
+                            data-hz-ui-lab-metrics-series-entity-owner="hertzbeat-ui-data-cell-stack"
+                          >
+                            <HzDataCellText
+                              variant="value"
+                              display="block"
+                              tone="strong"
+                              weight="semibold"
+                              data-hz-ui-lab-metrics-series-entity-label-owner="hertzbeat-ui-data-cell-text"
+                            >
+                              {row.entity}
+                            </HzDataCellText>
+                            <HzDataCellText
+                              variant="meta"
+                              display="block"
+                              spacing="stack-tight"
+                              casing="plain"
+                              tone="success"
+                              data-hz-ui-lab-metrics-series-entity-meta-owner="hertzbeat-ui-data-cell-text"
+                            >
+                              {row.entityMeta}
+                            </HzDataCellText>
+                          </HzDataCellStack>
+                        )
+                      },
+                      {
+                        key: 'latest',
+                        header: 'Latest',
+                        render: row => <HzDataCellText variant="value" font="mono" tone="bright" data-hz-ui-lab-metrics-series-latest-owner="hertzbeat-ui-data-cell-text">{row.latest}</HzDataCellText>
+                      },
+                      {
+                        key: 'points',
+                        header: 'Points',
+                        render: row => <HzDataCellText variant="value">{row.points}</HzDataCellText>
+                      },
+                      {
+                        key: 'time',
+                        header: 'Last seen',
+                        render: row => <HzDataCellText variant="timestamp">{row.time}</HzDataCellText>
+                      }
+                    ]}
+                  />
+                </HzPanelSurface>
+                <HzWorkbenchLayout
+                  as="div"
+                  variant="metrics-series-detail"
+                  data-hz-ui-lab-metrics-series-detail-layout="shared"
+                  data-metrics-series-detail-layout-owner="hertzbeat-ui-workbench-layout"
+                  className="w-[860px] border-t border-[var(--hz-ui-line-soft)] py-2"
+                >
+                  <HzPanelSurface
+                    data-hz-ui-lab-metrics-series-detail-layout-series="shared"
+                    data-metrics-series-detail-layout-series-owner="hertzbeat-ui-panel-surface"
+                    padding="chart-inner"
+                  >
+                    <HzDataCellText variant="title" display="block">Metrics series</HzDataCellText>
+                    <HzDataCellText variant="meta" display="block" spacing="stack-tight">Shared split grid primary column</HzDataCellText>
+                  </HzPanelSurface>
+                  <HzPanelSurface
+                    data-hz-ui-lab-metrics-series-detail-layout-detail="shared"
+                    data-metrics-series-detail-layout-detail-owner="hertzbeat-ui-panel-surface"
+                    padding="chart-inner"
+                  >
+                    <HzDataCellText variant="title" display="block">Selected series</HzDataCellText>
+                    <HzDataCellText variant="meta" display="block" spacing="stack-tight">320px secondary inspector</HzDataCellText>
+                  </HzPanelSurface>
+                </HzWorkbenchLayout>
+                <HzEmptyState
+                  data-hz-ui-lab-metrics-series-empty-state="shared"
+                  data-metrics-series-empty-state-owner="hertzbeat-ui-empty-state"
+                  title="No metrics series"
+                  description="Confirm time range, entity attribution, collector, and monitor template before inspecting metrics."
+                  layout="table-panel"
+                  className="w-[520px] border-t border-[var(--hz-ui-line-soft)]"
+                />
+                <HzTrendFrame
+                  data-hz-ui-lab-metrics-trend-frame="shared"
+                  data-metrics-trend-frame-owner="hertzbeat-ui-trend-frame"
+                  className="w-[520px] border-t border-[var(--hz-ui-line-soft)] py-2"
+                >
+                  <HzStateNotice
+                    data-hz-ui-lab-metrics-trend-empty-state="shared"
+                    data-metrics-trend-empty-state-owner="hertzbeat-ui-state-notice"
+                    tone="info"
+                    variant="hint"
+                    frame="trend-empty"
+                    title="No metrics trend"
+                    description="Run a query to show real sample points."
+                  />
+                </HzTrendFrame>
+                <HzTrendFrame
+                  data-hz-ui-lab-metrics-trend-bars-frame="shared"
+                  data-metrics-trend-bars-frame-owner="hertzbeat-ui-trend-frame"
+                  className="w-[520px] border-t border-[var(--hz-ui-line-soft)] py-2"
+                >
+                  {[
+                    { id: 'p95', label: 'p95 latency · 128ms', heightPct: 68 },
+                    { id: 'errors', label: 'errors · 2', heightPct: 34 },
+                    { id: 'throughput', label: 'throughput · 320/s', heightPct: 82 }
+                  ].map(item => (
+                    <HzTrendBar
+                      key={item.id}
+                      heightPct={item.heightPct}
+                      title={item.label}
+                      data-hz-ui-lab-metrics-trend-bar="shared"
+                      data-metrics-trend-bar-owner="hertzbeat-ui-trend-bar"
+                    />
+                  ))}
+                </HzTrendFrame>
+                <HzDataMetaText
+                  data-hz-ui-lab-metrics-trend-sample-helper="shared"
+                  data-metrics-trend-sample-helper-owner="hertzbeat-ui-data-meta-text"
+                  display="block"
+                  casing="plain"
+                  spacing="trend-helper"
+                >
+                  3 sample points
+                </HzDataMetaText>
+                <HzWorkbenchLayout
+                  as="div"
+                  variant="metrics-chart-toolbar"
+                  data-hz-ui-lab-metrics-chart-header-layout="shared"
+                  data-metrics-chart-header-layout-owner="hertzbeat-ui-workbench-layout"
+                  className="w-[520px] border-t border-[var(--hz-ui-line-soft)] py-2"
+                >
+                  <HzPanelTitleLabel
+                    icon={BarChart3}
+                    data-hz-ui-lab-metrics-chart-title-label="shared"
+                    data-metrics-chart-title-label-owner="hertzbeat-ui-panel-title-label"
+                  >
+                    Trend band
+                  </HzPanelTitleLabel>
+                  <HzActionGroup
+                    layout="end-wrap"
+                    data-hz-ui-lab-metrics-chart-toolbar-actions="shared"
+                    data-metrics-chart-toolbar-actions-owner="hertzbeat-ui-action-group"
+                  >
+                    <HzStatusBadge tone="neutral" size="xs">0 series</HzStatusBadge>
+                  </HzActionGroup>
+                </HzWorkbenchLayout>
+                <HzPanelSurface
+                  data-hz-ui-lab-metrics-chart-panel-surface="shared"
+                  data-metrics-chart-panel-surface-owner="hertzbeat-ui-panel-surface"
+                  data-hz-ui-lab-metrics-chart-panel-variant="chart-inner"
+                  data-metrics-chart-panel-variant-owner="hertzbeat-ui-panel-surface"
+                  padding="chart-inner"
+                  variant="chart-inner"
+                  className="w-[520px]"
+                >
+                  <div className="h-16 border-y border-[#252b35]" />
+                </HzPanelSurface>
+                <HzEChartsPanel
+                  data-hz-ui-lab-metrics-echarts-panel="shared"
+                  data-metrics-echarts-panel-owner="hertzbeat-ui-echarts-panel"
+                  data-hz-ui-lab-metrics-echarts-edge="shared"
+                  option={monitorHistoryEChartsOption}
+                  edge="metrics-chart"
+                  height={96}
+                  tone="operator"
+                  className="w-[520px]"
+                />
+                <HzPanelSurface
+                  data-hz-ui-lab-metrics-chart-band-surface="shared"
+                  data-metrics-chart-band-surface-owner="hertzbeat-ui-panel-surface"
+                  padding="chart"
+                  className="w-[520px]"
+                >
+                  <HzWorkbenchLayout
+                    as="div"
+                    variant="chart-stack"
+                    data-hz-ui-lab-metrics-chart-stack-layout="shared"
+                    data-metrics-chart-stack-layout-owner="hertzbeat-ui-workbench-layout"
+                  >
+                    <div className="h-10 rounded-[3px] border border-[#252b35] bg-[#10141b]" />
+                  </HzWorkbenchLayout>
+                </HzPanelSurface>
+                <HzChipGroup
+                  data-hz-ui-lab-metrics-chart-fact-chips="shared"
+                  data-metrics-chart-fact-chips-owner="hertzbeat-ui-toolbar-chips"
+                  align="end"
+                  density="compact"
+                  className="w-[520px] border-t border-[var(--hz-ui-line-soft)] py-2"
+                >
+                  {[
+                    ['points', '24'],
+                    ['latest', '128ms'],
+                    ['range', '30m']
+                  ].map(([label, value]) => (
+                    <HzStatusBadge
+                      key={label}
+                      tone="neutral"
+                      size="sm"
+                      layout="metric-fact"
+                      label={label}
+                      value={value}
+                      data-hz-ui-lab-metrics-chart-fact-chip="shared"
+                      data-metrics-chart-fact-chip-owner="hertzbeat-ui-status-badge"
+                    />
+                  ))}
+                </HzChipGroup>
+                <HzActionGroup
+                  layout="end-wrap"
+                  data-hz-ui-lab-metrics-chart-toolbar-count="shared"
+                  data-metrics-chart-toolbar-count-owner="hertzbeat-ui-action-group"
+                  className="w-[520px] border-t border-[var(--hz-ui-line-soft)] py-2"
+                >
+                  <HzStatusBadge
+                    tone="neutral"
+                    size="xs"
+                    data-hz-ui-lab-metrics-chart-series-count-badge="shared"
+                  >
+                    0 series
+                  </HzStatusBadge>
+                </HzActionGroup>
+                <HzActionGroup
+                  layout="end-wrap"
+                  data-hz-ui-lab-metrics-chart-zoom-actions="shared"
+                  data-metrics-chart-zoom-actions-owner="hertzbeat-ui-action-group"
+                  className="w-[520px] border-t border-[var(--hz-ui-line-soft)] py-2"
+                >
+                  <HzStatusBadge
+                    tone="info"
+                    size="sm"
+                    layout="zoom-draft"
+                    label="Zoom draft"
+                    value="2026-05-24 03:41:00 - 03:42:00"
+                    valueFont="mono"
+                    data-hz-ui-lab-metrics-chart-zoom-draft="shared"
+                    data-metrics-chart-zoom-draft-owner="hertzbeat-ui-status-badge"
+                  />
+                  <HzButton
+                    intent="ghost"
+                    size="sm"
+                    data-hz-ui-lab-metrics-chart-zoom-apply="shared"
+                  >
+                    Apply zoom
+                  </HzButton>
+                </HzActionGroup>
+                <HzPanelSection
+                  divider="top"
+                  spacing="stack-2"
+                  className="w-[320px]"
+                  data-hz-ui-lab-metrics-secondary-context-section="shared"
+                  data-metrics-secondary-context-section-owner="hertzbeat-ui-panel-section"
+                  data-hz-ui-lab-metrics-collapsible-section="shared"
+                  data-metrics-collapsible-section-owner="hertzbeat-ui-collapsible-section"
+                >
+                  <HzCollapsibleSection
+                    title="Metrics evidence"
+                    meta="logs / traces / alerts"
+                    surface="inset"
+                    data-hz-ui-lab-metrics-collapsible-shell="shared"
+                  >
+                    <HzDetailRows
+                      heading="Collapsible body"
+                      rows={[
+                        { key: 'logs', title: 'Logs', copy: '12 records', meta: 'linked span' },
+                        { key: 'traces', title: 'Trace', copy: 'trace-20260524', meta: 'waterfall' }
+                      ]}
+                    />
+                  </HzCollapsibleSection>
+                </HzPanelSection>
+                <div
+                  className="w-[320px] border-t border-[var(--hz-ui-line-soft)] px-0 py-2"
+                  data-hz-ui-lab-metrics-linked-record-handoff="shared"
+                  data-metrics-linked-record-handoff-owner="hertzbeat-ui-context-handoff"
+                >
+                  <HzContextHandoff
+                    title="Metrics linked records"
+                    context="logs / traces / alerts"
+                    frame="flush"
+                    targets={[
+                      {
+                        id: 'logs',
+                        label: <span data-hz-ui-lab-metrics-linked-record-action="logs">Historical logs</span>,
+                        description: 'Locate logs at the selected span and time window.',
+                        meta: '12 rows',
+                        href: '#metrics-logs',
+                        tone: 'neutral'
+                      },
+                      {
+                        id: 'traces',
+                        label: <span data-hz-ui-lab-metrics-linked-record-action="traces">Trace waterfall</span>,
+                        description: 'Open the full trace while preserving the span.',
+                        meta: 'trace-20260524',
+                        href: '#metrics-traces',
+                        tone: 'info'
+                      },
+                      {
+                        id: 'alerts',
+                        label: <span data-hz-ui-lab-metrics-linked-record-action="alerts">Alert handling</span>,
+                        description: 'Carry entity, service, and metric evidence into alerts.',
+                        meta: '2 open',
+                        href: '#metrics-alerts',
+                        tone: 'critical'
+                      }
+                    ]}
+                  />
+                </div>
+                <HzPanelSurface
+                  clip
+                  data-hz-ui-lab-metrics-detail-panel-surface="shared"
+                  data-metrics-detail-panel-surface-owner="hertzbeat-ui-panel-surface"
+                  data-hz-ui-lab-metrics-detail-panel-stickiness="top-4"
+                  data-metrics-detail-panel-stickiness-owner="hertzbeat-ui-panel-surface"
+                  stickiness="top-4"
+                  className="w-[320px]"
+                >
+                  <HzPanelHeader
+                    data-hz-ui-lab-metrics-detail-panel-header="shared"
+                    data-metrics-detail-panel-header-owner="hertzbeat-ui-panel-header"
+                    chrome="transparent-framed"
+                    eyebrow="Metrics detail"
+                    title="checkout.latency"
+                    subtitle="checkout-api · Checkout API · selected series"
+                    meta={<HzStatusBadge tone="success" size="xs">selected</HzStatusBadge>}
+                  />
+                  <div
+                    className="border-t border-[var(--hz-ui-line-soft)] px-0 py-2"
+                    data-hz-ui-lab-metrics-detail-rows="shared"
+                  >
+                    <HzDetailRows
+                      heading="Metrics evidence"
+                      rows={[
+                        { key: 'entity', title: 'Entity', copy: 'Checkout API', meta: 'hertzbeat.entity_id=7' },
+                        { key: 'sample-window', title: 'Sample window', copy: '2026-05-24 02:20', meta: 'real samples' },
+                        { key: 'trace', title: 'Trace', copy: 'trace-20260524', meta: 'selected series' }
+                      ]}
+                      data-metrics-detail-rows-owner="hertzbeat-ui-detail-rows"
+                    />
+                  </div>
+                </HzPanelSurface>
+                <HzAssistiveMarker
+                  data-hz-ui-lab-metrics-detail-panel-empty="shared"
+                  data-metrics-detail-panel-empty-owner="hertzbeat-ui-assistive-marker"
+                />
+                <HzStatStrip
+                  columns={3}
+                  className="w-[320px] rounded-[3px] border border-[var(--hz-ui-line-soft)] p-1"
+                  data-hz-ui-lab-metrics-detail-stats="shared"
+                  data-metrics-detail-stats-owner="hertzbeat-ui-stat-strip"
+                >
+                  {[
+                    { id: 'non-empty', label: 'Non-empty', value: '2', tone: 'success' as const },
+                    { id: 'total', label: 'Series', value: '3', tone: 'info' as const },
+                    { id: 'state', label: 'State', value: 'Ready', tone: 'neutral' as const }
+                  ].map(item => (
+                    <HzStatCell
+                      key={item.id}
+                      data-hz-ui-lab-metrics-detail-stat={item.id}
+                      data-metrics-detail-stat-owner="hertzbeat-ui-stat-cell"
+                      label={item.label}
+                      value={item.value}
+                      tone={item.tone}
+                      variant="tile"
+                      density="compact"
+                      className="border-0"
+                    />
+                  ))}
+                </HzStatStrip>
+                <HzAttributeDiagnostics
+                  data-hz-ui-lab-metrics-attribution-diagnostics="shared"
+                  data-metrics-attribution-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"
+                  title="Metrics attribution"
+                  namespaceLabel="hertzbeat.*"
+                  frame="embedded"
+                  rows={[
+                    {
+                      key: 'metrics-entity',
+                      label: 'hertzbeat.entity_id',
+                      value: '7',
+                      meta: 'present',
+                      state: 'present',
+                      stateLabel: 'Present',
+                      tone: 'success'
+                    },
+                    {
+                      key: 'metrics-workspace',
+                      label: 'hertzbeat.workspace_id',
+                      value: '-',
+                      meta: 'missing',
+                      state: 'missing',
+                      stateLabel: 'Missing',
+                      tone: 'critical'
+                    }
+                  ]}
+                  className="w-[320px]"
+                />
+                <div
+                  className="w-[320px] border-t border-[var(--hz-ui-line-soft)] px-0"
+                  data-hz-ui-lab-metrics-entity-context-rows="shared"
+                >
+                  <HzDetailRows
+                    heading="Entity context"
+                    padding="compact-y"
+                    rows={[
+                      { key: 'entity', title: 'Entity', copy: 'Checkout API', meta: 'entityId 7' },
+                      { key: 'service', title: 'Service', copy: 'checkout-service', meta: 'storefront' },
+                      { key: 'source', title: 'Source', copy: 'OTLP', meta: 'metrics route' }
+                    ]}
+                    data-metrics-entity-context-rows-owner="hertzbeat-ui-detail-rows"
+                  />
+                </div>
+                <HzQueryTokenField
+                  width="compact"
+                  value="trace-20260523"
+                  readOnly
+                  aria-label="Trace query token"
+                  fieldProps={{
+                    'data-hz-ui-lab-trace-query-token-field': 'shared',
+                    'data-trace-query-token-field-owner': 'hertzbeat-ui-query-token-field'
+                  }}
+                />
+                <HzQueryStatusSelect
+                  width="compact"
+                  value="error"
+                  aria-label="Trace query status"
+                  data-hz-ui-lab-trace-query-status-select="shared"
+                  data-trace-query-status-select-owner="hertzbeat-ui-query-status-select"
+                  options={[
+                    { value: 'all', label: 'All' },
+                    { value: 'error', label: 'Errors' }
+                  ]}
+                />
+                <HzQueryActionGroup
+                  data-hz-ui-lab-trace-query-action-group="shared"
+                  data-trace-query-action-group-owner="hertzbeat-ui-query-action-group"
+                >
+                  <HzButton size="sm" intent="primary" onClick={clearFilterClauses}>
+                    <HzButtonIcon
+                      icon={Play}
+                      data-hz-ui-lab-trace-query-action-icon="run"
+                      data-trace-query-action-icon-owner="hertzbeat-ui-button-icon"
+                    />
+                    Run
+                  </HzButton>
+                  <HzButton size="sm" intent="secondary" onClick={() => setContextMessage('Trace query reset')}>
+                    <HzButtonIcon
+                      icon={RotateCcw}
+                      data-hz-ui-lab-trace-query-action-icon="reset"
+                      data-trace-query-action-icon-owner="hertzbeat-ui-button-icon"
+                    />
+                    Reset
+                  </HzButton>
+                </HzQueryActionGroup>
+              </HzControlStack>
+              <HzControlStack
+                layout="end-inline"
+                data-hz-ui-lab-time-control-stack="shared"
+                data-signal-time-control-stack-owner="hertzbeat-ui-control-stack"
+                data-hz-ui-lab-metrics-time-control-stack="shared"
+                data-metrics-time-control-stack-owner="hertzbeat-ui-control-stack"
+              >
+                <HzDataMetaText data-hz-ui-lab-time-control-stack-copy="shared" display="block" casing="plain">
+                  Right-aligned time rail
+                </HzDataMetaText>
+                <TimeRangeControl
+                  value={uiLabTraceTimeRangeControlValue}
+                  onApply={() => undefined}
+                  onRefresh={() => undefined}
+                  showAbsoluteFields
+                  variant="narrow-rail"
+                  data-hz-ui-lab-time-range-control="narrow-rail"
+                  data-signal-time-range-control-owner="hertzbeat-shared-time-range-control"
+                  data-metrics-time-range-control-owner="hertzbeat-shared-time-range-control"
+                />
+              </HzControlStack>
+              <HzWorkbenchHeaderCopy
+                className="w-[220px]"
+                eyebrow="Observability"
+                title="Trace workbench"
+                copy="Compact route heading for signal workbenches."
+                data-hz-ui-lab-trace-header-copy="shared"
+                data-trace-header-copy-owner="hertzbeat-ui-workbench-header-copy"
+              />
+              <HzSignalWorkbenchShell
+                as="section"
+                className="min-h-0 w-[220px]"
+                contentClassName="px-3 py-3"
+                data-hz-ui-lab-signal-workbench-shell="shared"
+                data-signal-workbench-shell-owner="hertzbeat-ui-signal-workbench-shell"
+              >
+                <HzPanelSurface className="px-3 py-3">
+                  <HzDataMetaText display="block" casing="plain">Shared signal shell</HzDataMetaText>
+                </HzPanelSurface>
+              </HzSignalWorkbenchShell>
+              <HzSignalWorkbenchShell
+                as="section"
+                className="min-h-0 w-[240px]"
+                layout="metrics-workbench"
+                data-hz-ui-lab-metrics-workbench-shell="shared"
+                data-metrics-workbench-shell-owner="hertzbeat-ui-signal-workbench-shell"
+              >
+                <HzPanelSurface
+                  padding="query"
+                  data-hz-ui-lab-metrics-workbench-shell-panel="shared"
+                  data-metrics-workbench-shell-panel-owner="hertzbeat-ui-panel-surface"
+                >
+                  <HzDataMetaText display="block" casing="plain">Shared metrics shell</HzDataMetaText>
+                </HzPanelSurface>
+              </HzSignalWorkbenchShell>
+              <HzControlStack
+                className="w-[150px]"
+                data-hz-ui-lab-log-detail-action-stack="shared"
+                data-log-detail-action-stack-owner="hertzbeat-ui-control-stack"
+              >
+                <HzButton size="sm" intent="secondary">
+                  Open log
+                </HzButton>
+                <HzButton size="sm" intent="secondary">
+                  Open trace
+                </HzButton>
+              </HzControlStack>
+              <HzDetailAside
+                className="w-[150px]"
+                data-hz-ui-lab-log-detail-aside="shared"
+                data-log-detail-aside-owner="hertzbeat-ui-detail-aside"
+              >
+                <HzPanelHeader
+                  data-hz-ui-lab-log-detail-aside-header="shared"
+                  eyebrow="Selected"
+                  title="checkout"
+                  className="-mx-4 -mt-4 border-x-0 border-t-0 bg-transparent"
+                />
+                <HzDetailBodyStack
+                  data-hz-ui-lab-log-detail-body-stack="shared"
+                  data-log-detail-body-stack-owner="hertzbeat-ui-detail-body-stack"
+                >
+                  <HzDetailRows rows={[{ key: 'trace', title: 'traceId', copy: 'trace-20260523' }]} />
+                </HzDetailBodyStack>
+              </HzDetailAside>
+              <div className="w-[112px]" data-hz-control-baseline-component="HzNumberStepper">
+                <HzNumberStepper value="60" onValueChange={() => setContextMessage('Baseline stepper')} />
+              </div>
+              <span data-hz-control-baseline-component="HzCheckbox">
+                <HzCheckbox defaultChecked label="Enabled" />
+              </span>
+              <span data-hz-control-baseline-component="HzSwitch">
+                <HzSwitch checked label="Running" />
+              </span>
+              <span
+                data-hz-control-baseline-component="HzFileInput"
+                data-hz-ui-lab-monitor-import-file-input="shared"
+              >
+                <HzFileInput aria-label="Baseline import file input" />
+              </span>
+            </div>
+          </section>
+          <HzMonitorFilterBar
+            searchLabel="Search"
+            searchPlaceholder="Search monitor name / instance"
+            searchValue={monitorFilterSearch}
+            onSearchChange={setMonitorFilterSearch}
+            searchClearLabel="Clear search"
+            onSearchClear={() => {
+              setMonitorFilterSearch('');
+              setContextMessage('Monitor search cleared');
+            }}
+            searchClearButtonProps={
+              {
+                'data-hz-ui-lab-monitor-search-clear': 'shared',
+                'data-monitor-search-clear-owner': 'hertzbeat-ui-icon-button'
+              } as React.ComponentProps<typeof HzMonitorFilterBar>['searchClearButtonProps']
+            }
+            searchInputProps={
+              {
+                'data-hz-ui-lab-monitor-search-enter-submit': 'shared',
+                'data-monitor-search-enter-submit-owner': 'hertzbeat-ui-input'
+              } as React.ComponentProps<typeof HzMonitorFilterBar>['searchInputProps']
+            }
+            labelFilterLabel="Labels"
+            labelFilterPlaceholder="Filter by labels"
+            labelFilterValue={monitorFilterLabels}
+            onLabelFilterChange={value => {
+              setMonitorFilterLabels(value);
+              setContextMessage('Monitor labels filter changed');
+            }}
+            labelFilterClearLabel="Clear labels"
+            onLabelFilterClear={() => {
+              setMonitorFilterLabels('');
+              setContextMessage('Monitor labels cleared');
+            }}
+            labelFilterClearButtonProps={
+              {
+                'data-hz-ui-lab-monitor-label-clear': 'shared',
+                'data-monitor-label-clear-owner': 'hertzbeat-ui-icon-button'
+              } as React.ComponentProps<typeof HzMonitorFilterBar>['labelFilterClearButtonProps']
+            }
+            labelFilterInputProps={
+              {
+                'data-hz-ui-lab-monitor-label-filter': 'shared',
+                'data-monitor-label-filter-owner': 'hertzbeat-ui-input',
+                'data-hz-ui-lab-monitor-label-enter-submit': 'shared',
+                'data-monitor-label-enter-submit-owner': 'hertzbeat-ui-input'
+              } as React.ComponentProps<typeof HzMonitorFilterBar>['labelFilterInputProps']
+            }
+            typeLabel="Monitor type"
+            typeValue={monitorFilterType}
+            typeOptions={monitorFilterTypeOptions}
+            onTypeChange={setMonitorFilterType}
+            typeSelectProps={
+              {
+                'data-hz-ui-lab-monitor-app-autosubmit': 'shared',
+                'data-monitor-app-filter-autosubmit-owner': 'hertzbeat-ui-select'
+              } as React.ComponentProps<typeof HzMonitorFilterBar>['typeSelectProps']
+            }
+            typePickerLabel="Browse monitor apps"
+            onTypePickerOpen={() => setContextMessage('Monitor app picker opened')}
+            typePickerButtonProps={
+              {
+                'data-hz-ui-lab-monitor-filter-picker': 'shared',
+                'data-monitor-filter-picker-owner': 'hertzbeat-ui-button'
+              } as React.ButtonHTMLAttributes<HTMLButtonElement>
+            }
+            statusLabel="Status"
+            statusValue={monitorFilterStatus}
+            statusOptions={monitorFilterStatusOptions}
+            onStatusChange={setMonitorFilterStatus}
+            statusSelectProps={
+              {
+                'data-hz-ui-lab-monitor-status-autosubmit': 'shared',
+                'data-monitor-status-filter-autosubmit-owner': 'hertzbeat-ui-select'
+              } as React.ComponentProps<typeof HzMonitorFilterBar>['statusSelectProps']
+            }
+            applyLabel="Apply"
+            clearLabel="Clear"
+            onApply={() => setContextMessage(`Monitor filter · ${monitorFilterType || 'all'} · ${monitorFilterStatus || 'all'}`)}
+            onClear={() => {
+              setMonitorFilterSearch('');
+              setMonitorFilterLabels('');
+              setMonitorFilterType('');
+              setMonitorFilterStatus('');
+            }}
+          />
+          <HzInlineFeedback
+            tone="info"
+            title="Monitor auto refresh"
+            description="Refreshes the current monitor query every 120 seconds."
+            meta="120s"
+            variant="embedded"
+            data-hz-ui-lab-monitor-auto-refresh="shared"
+            data-monitor-auto-refresh-owner="hertzbeat-ui-inline-feedback"
+            data-monitor-auto-refresh-interval-ms="120000"
+          />
+          <HzQueryBar
+            query={query}
+            onQueryChange={setQuery}
+            actions={
+              <>
+                <HzButton size="sm">
+                  <Search size={13} />
+                  Run
+                </HzButton>
+                <HzButton size="sm" intent="ghost">
+                  <SlidersHorizontal size={13} />
+                  Options
+                </HzButton>
+              </>
+            }
+          />
+        </div>
+      }
+      metricStrip={
+        <HzMetricStrip
+          items={[
+            { label: 'Active monitors', value: '123', hint: '+8 today' },
+            { label: 'Templates', value: '42', hint: '6 hidden' },
+            { label: 'Collector p95', value: '118ms', hint: 'stable' },
+            { label: 'Open alerts', value: '7', hint: '2 critical' }
+          ]}
+        />
+      }
+    >
+      <div
+        className="grid min-w-0 xl:grid-cols-[minmax(0,1fr)_500px] 2xl:grid-cols-[minmax(0,1fr)_520px]"
+        data-ui-lab-surface="signoz-inspired-explorer"
+      >
+        <section className="min-w-0 border-r border-[var(--hz-ui-line-soft)]">
+          <HzResultControls
+            timeRanges={resultTimeRanges}
+            selectedTimeRangeId={timeRangeId}
+            onTimeRangeChange={setTimeRangeId}
+            refreshIntervals={resultRefreshIntervals}
+            selectedRefreshIntervalId={refreshIntervalId}
+            onRefreshIntervalChange={setRefreshIntervalId}
+            viewModes={resultViewModes}
+            selectedViewModeId={viewModeId}
+            onViewModeChange={setViewModeId}
+            columns={resultColumns}
+            onToggleColumn={toggleResultColumn}
+            onPinColumn={pinResultColumn}
+          />
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)] lg:grid-cols-3">
+            <HzStatCell
+              data-hz-ui-lab-signal-summary-stat="shared"
+              label="Collection success"
+              value="99.2"
+              unit="%"
+              detail="last 15m · 128 monitors"
+              trend="+0.4"
+              tone="success"
+              variant="tile"
+              className="border-x-0 border-y-0 lg:border-r lg:border-[var(--hz-ui-line-soft)]"
+            />
+            <HzStatStrip
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] p-2 lg:col-span-2 lg:border-r lg:border-t-0"
+              data-hz-ui-lab-dialog-stage-stats="shared"
+              data-signal-dialog-stage-stats-owner="hertzbeat-ui-stat-strip"
+            >
+              {[
+                { id: 'span', label: 'Current span', value: 'db.query', tone: 'info' as const },
+                { id: 'errors', label: 'Error spans', value: '1', tone: 'critical' as const },
+                { id: 'events', label: 'Events', value: '3', tone: 'neutral' as const },
+                { id: 'links', label: 'Links', value: '2', tone: 'neutral' as const }
+              ].map(item => (
+                <HzStatCell
+                  key={item.id}
+                  data-signal-dialog-stage-stat-owner="hertzbeat-ui-stat-cell"
+                  data-signal-dialog-stage-stat={item.id}
+                  label={item.label}
+                  value={item.value}
+                  tone={item.tone}
+                  variant="tile"
+                  density="compact"
+                />
+              ))}
+            </HzStatStrip>
+            <HzChipGroup
+              boundary="top"
+              className="lg:border-r"
+              data-hz-ui-lab-dialog-toolbar-chips="shared"
+              data-signal-dialog-toolbar-chips-owner="hertzbeat-ui-toolbar-chips"
+            >
+              <HzDialogMetaItem
+                width="service"
+                data-hz-ui-lab-dialog-subtitle-meta="shared"
+                data-signal-dialog-subtitle-owner="hertzbeat-ui-dialog-meta-item"
+              >
+                checkout-api
+              </HzDialogMetaItem>
+              <HzStatusBadge
+                data-signal-dialog-toolbar-badge-owner="hertzbeat-ui-status-badge"
+                tone="neutral"
+                size="xs"
+              >
+                JSON
+              </HzStatusBadge>
+              <HzStatusBadge
+                data-signal-dialog-toolbar-badge-owner="hertzbeat-ui-status-badge"
+                tone="info"
+                size="xs"
+              >
+                SPAN
+              </HzStatusBadge>
+              <HzDialogMetaItem
+                width="trace-id"
+                data-hz-ui-lab-trace-drawer-meta-item="shared"
+                data-signal-dialog-toolbar-meta-owner="hertzbeat-ui-dialog-meta-item"
+              >
+                traceId · trace-20260523
+              </HzDialogMetaItem>
+              <HzDialogMetaItem
+                width="duration"
+                data-signal-dialog-stage-meta-owner="hertzbeat-ui-dialog-meta-item"
+              >
+                checkout · 120ms
+              </HzDialogMetaItem>
+            </HzChipGroup>
+            <HzActionGroup
+              data-hz-ui-lab-dialog-action-group="shared"
+              data-signal-dialog-action-group-owner="hertzbeat-ui-action-group"
+              density="inline"
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] p-2 lg:border-r lg:border-t-0"
+            >
+              <HzButton size="sm" intent="ghost" onClick={clearFilterClauses}>
+                View log
+              </HzButton>
+              <HzButton size="sm" intent="ghost" onClick={clearFilterClauses}>
+                Open trace
+              </HzButton>
+            </HzActionGroup>
+            <HzDialogBodyLayout
+              data-hz-ui-lab-dialog-body-layout="shared"
+              data-signal-dialog-body-layout-owner="hertzbeat-ui-dialog-body-layout"
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] p-2 lg:border-r lg:border-t-0"
+            >
+              <HzDialogBodyLayout
+                variant="split-detail"
+                data-hz-ui-lab-dialog-split-layout="shared"
+                data-signal-dialog-split-layout-owner="hertzbeat-ui-dialog-body-layout"
+              >
+                <div className="min-w-0 text-[12px] font-semibold text-[var(--hz-ui-text)]">Trace timeline</div>
+                <HzDialogBodyLayout
+                  variant="side-stack"
+                  data-hz-ui-lab-dialog-side-stack="shared"
+                  data-signal-dialog-side-stack-owner="hertzbeat-ui-dialog-body-layout"
+                >
+                  <HzDataMetaText display="block" casing="plain">
+                    Selected span facts
+                  </HzDataMetaText>
+                </HzDialogBodyLayout>
+              </HzDialogBodyLayout>
+              <HzDialogBodyLayout
+                variant="waterfall-detail"
+                data-hz-ui-lab-dialog-waterfall-layout="shared"
+                data-signal-dialog-waterfall-layout-owner="hertzbeat-ui-dialog-body-layout"
+              >
+                <div className="min-w-0 text-[12px] font-semibold text-[var(--hz-ui-text)]">Waterfall lanes</div>
+                <HzDialogBodyLayout
+                  variant="side-stack"
+                  data-hz-ui-lab-dialog-waterfall-side-stack="shared"
+                  data-signal-dialog-waterfall-side-stack-owner="hertzbeat-ui-dialog-body-layout"
+                >
+                  <HzDataMetaText display="block" casing="plain">
+                    Waterfall selected span
+                  </HzDataMetaText>
+                </HzDialogBodyLayout>
+              </HzDialogBodyLayout>
+            </HzDialogBodyLayout>
+            <HzDialogEventNotice
+              layout="grid-cell"
+              data-hz-ui-lab-dialog-event-detail="shared"
+              data-signal-dialog-event-detail-owner="hertzbeat-ui-dialog-event-notice"
+              title="exception"
+              description={(
+                <HzDialogEventText
+                  data-signal-dialog-event-detail-copy="span-event-not-span"
+                  data-hz-ui-lab-dialog-event-detail-text-owner="hertzbeat-ui-dialog-event-text"
+                >
+                  Event marker belongs to the selected span timeline.
+                </HzDialogEventText>
+              )}
+              meta={(
+                <HzDialogEventText
+                  variant="meta"
+                  data-signal-dialog-event-detail-meta="span-event-label"
+                  data-hz-ui-lab-dialog-event-detail-text-owner="hertzbeat-ui-dialog-event-text"
+                >
+                  Span event
+                </HzDialogEventText>
+              )}
+              actions={(
+                <HzButton
+                  data-signal-dialog-event-detail-action="view-span"
+                  size="sm"
+                  intent="ghost"
+                  onClick={clearFilterClauses}
+                >
+                  View span
+                </HzButton>
+              )}
+              tone="info"
+              variant="hint"
+            />
+            <HzPanelHeader
+              data-hz-ui-lab-signal-panel-header="shared"
+              data-signal-panel-header-owner="hertzbeat-ui-panel-header"
+              title={(
+                <>
+                  <FileText className="h-4 w-4" aria-hidden="true" />
+                  Recent signals
+                </>
+              )}
+              subtitle="Dense table header with shared meta/action slot."
+              meta={<HzStatusBadge tone="neutral" size="xs">8 rows</HzStatusBadge>}
+              className="border-x-0 border-b border-t-0 bg-transparent lg:border-r"
+            />
+            <HzPanelHeader
+              data-hz-ui-lab-signal-detail-panel-header="shared"
+              data-signal-detail-panel-header-owner="hertzbeat-ui-panel-header"
+              eyebrow="Detail panel"
+              title="Checkout API"
+              subtitle="trace-20260523 · span-root"
+              meta={<HzStatusBadge tone="success" size="xs">selected</HzStatusBadge>}
+              className="border-x-0 border-b border-t-0 bg-transparent lg:border-r"
+            />
+            <HzPanelSurface
+              padding="query"
+              data-hz-ui-lab-metrics-header-panel-surface="shared"
+              data-metrics-header-panel-surface-owner="hertzbeat-ui-panel-surface"
+              className="w-full max-w-[1120px]"
+            >
+              <HzWorkbenchLayout
+                as="div"
+                variant="metrics-header"
+                data-hz-ui-lab-metrics-header-layout="shared"
+                data-metrics-header-layout-owner="hertzbeat-ui-workbench-layout"
+              >
+                <HzWorkbenchHeaderCopy
+                  density="compact"
+                  eyebrow="Metrics workbench"
+                  title="OTLP metrics"
+                  data-hz-ui-lab-metrics-header-copy="shared"
+                  data-metrics-header-copy-owner="hertzbeat-ui-workbench-header-copy"
+                >
+                  <HzChipGroup
+                    density="compact"
+                    spacing="top-3"
+                    data-hz-ui-lab-metrics-header-context-strip="shared"
+                    data-metrics-header-context-strip-owner="hertzbeat-ui-toolbar-chips"
+                  >
+                    <HzStatusBadge
+                      tone="neutral"
+                      size="sm"
+                      layout="context-pill"
+                      label="service"
+                      value="checkout-api"
+                      data-hz-ui-lab-metrics-header-context-pill="shared"
+                      data-metrics-header-context-pill-owner="hertzbeat-ui-status-badge"
+                    />
+                    <HzStatusBadge
+                      tone="info"
+                      size="sm"
+                      layout="context-pill"
+                      label="namespace"
+                      value="storefront"
+                      data-hz-ui-lab-metrics-header-context-pill="shared"
+                      data-metrics-header-context-pill-owner="hertzbeat-ui-status-badge"
+                    />
+                  </HzChipGroup>
+                </HzWorkbenchHeaderCopy>
+                <HzActionGroup
+                  data-hz-ui-lab-metrics-header-panel-return-action-group="shared"
+                  data-metrics-header-panel-return-action-group-owner="hertzbeat-ui-action-group"
+                  layout="full-end"
+                >
+                  <HzButtonLink
+                    data-hz-ui-lab-metrics-header-panel-return-action="shared"
+                    data-metrics-header-panel-return-action-owner="hertzbeat-ui-button-link"
+                    href="#"
+                    size="md"
+                  >
+                    <HzButtonIcon
+                      icon={GitBranch}
+                      data-hz-ui-lab-metrics-header-panel-return-action-icon="shared"
+                      data-metrics-header-panel-return-action-icon-owner="hertzbeat-ui-button-icon"
+                    />
+                    Metrics return
+                  </HzButtonLink>
+                </HzActionGroup>
+              </HzWorkbenchLayout>
+            </HzPanelSurface>
+            <HzWorkbenchLayout
+              as="div"
+              variant="header-actions"
+              data-hz-ui-lab-signal-header-actions-layout="shared"
+              data-signal-header-actions-layout-owner="hertzbeat-ui-workbench-layout"
+              className="border-x-0 border-b border-t-0 p-2 lg:border-r"
+            >
+              <div className="min-w-0">
+                <HzDataMetaText display="block" casing="upper">
+                  Trace workbench
+                </HzDataMetaText>
+                <div className="mt-1 text-[13px] font-semibold text-[var(--hz-ui-text)]">Header title and copy</div>
+              </div>
+              <HzActionGroup
+                data-hz-ui-lab-log-header-return-action-group="shared"
+                data-log-header-return-action-group-owner="hertzbeat-ui-action-group"
+                layout="full-end"
+              >
+                <HzButtonLink
+                  data-hz-ui-lab-log-header-return-action="shared"
+                  data-log-header-return-action-owner="hertzbeat-ui-button-link"
+                  href="#"
+                  size="md"
+                >
+                  <HzButtonIcon
+                    icon={GitBranch}
+                    data-hz-ui-lab-log-header-return-action-icon="shared"
+                    data-log-header-return-action-icon-owner="hertzbeat-ui-button-icon"
+                  />
+                  Return
+                </HzButtonLink>
+                <HzButtonLink
+                  data-hz-ui-lab-metrics-header-return-action="shared"
+                  data-metrics-header-return-action-owner="hertzbeat-ui-button-link"
+                  href="#"
+                  size="md"
+                >
+                  <HzButtonIcon
+                    icon={GitBranch}
+                    data-hz-ui-lab-metrics-header-return-action-icon="shared"
+                    data-metrics-header-return-action-icon-owner="hertzbeat-ui-button-icon"
+                  />
+                  Metrics return
+                </HzButtonLink>
+                <HzButton size="sm" intent="ghost">Collector</HzButton>
+                <HzButton size="sm" intent="secondary">Template</HzButton>
+              </HzActionGroup>
+            </HzWorkbenchLayout>
+            <HzWorkbenchLayout
+              as="div"
+              variant="header-toolbar-slot"
+              data-hz-ui-lab-metrics-header-toolbar-slot="shared"
+              data-metrics-header-toolbar-slot-owner="hertzbeat-ui-workbench-layout"
+              className="border-x-0 border-b border-t-0 px-3 py-2 lg:border-r"
+            >
+              <HzWorkbenchLayout
+                as="div"
+                variant="time-toolbar"
+                data-hz-ui-lab-signal-time-toolbar-layout="shared"
+                data-signal-time-toolbar-layout-owner="hertzbeat-ui-workbench-layout"
+                data-hz-ui-lab-metrics-time-toolbar-layout="shared"
+                data-metrics-time-toolbar-layout-owner="hertzbeat-ui-workbench-layout"
+              >
+                <HzDataMetaText data-hz-ui-lab-time-toolbar-copy="shared" display="block" casing="plain">
+                  Last 30 minutes / Asia/Shanghai / manual refresh
+                </HzDataMetaText>
+                <HzActionGroup
+                  data-hz-ui-lab-time-toolbar-actions="shared"
+                  data-hz-ui-lab-metrics-time-toolbar-actions="shared"
+                  data-metrics-time-toolbar-actions-owner="hertzbeat-ui-action-group"
+                  className="justify-end"
+                >
+                  <HzButtonLink href="#" size="md">
+                    Refresh
+                  </HzButtonLink>
+                </HzActionGroup>
+              </HzWorkbenchLayout>
+            </HzWorkbenchLayout>
+            <HzPanelSurface
+              data-hz-ui-lab-panel-surface="shared"
+              data-hz-ui-lab-panel-surface-padding="view-switch"
+              data-hz-ui-lab-log-view-switch-panel-surface="shared"
+              data-log-view-switch-panel-surface-owner="hertzbeat-ui-panel-surface"
+              padding="view-switch"
+            >
+              <HzWorkbenchLayout
+                as="div"
+                variant="view-switch"
+                data-hz-ui-lab-log-view-switch-layout="shared"
+                data-log-view-switch-layout-owner="hertzbeat-ui-workbench-layout"
+              >
+                <HzDataMetaText data-hz-ui-lab-log-view-switch-title="shared" display="block" casing="plain">
+                  Stream / history
+                </HzDataMetaText>
+                <HzActionGroup
+                  layout="end-wrap"
+                  data-hz-ui-lab-log-view-switch-actions="shared"
+                  data-log-view-switch-actions-owner="hertzbeat-ui-action-group"
+                >
+                  <HzButton size="sm" intent="primary">Stream</HzButton>
+                  <HzButton size="sm" intent="secondary">History</HzButton>
+                </HzActionGroup>
+              </HzWorkbenchLayout>
+            </HzPanelSurface>
+            <HzPanelSurface
+              data-hz-ui-lab-log-stream-stage-panel-surface="shared"
+              data-log-stream-stage-panel-surface-owner="hertzbeat-ui-panel-surface"
+              data-hz-ui-lab-panel-surface-clip="stream-stage"
+              clip
+            >
+              <HzWorkbenchLayout
+                as="div"
+                variant="stream-stage"
+                data-hz-ui-lab-log-stream-stage-layout="shared"
+                data-log-stream-stage-layout-owner="hertzbeat-ui-workbench-layout"
+                className="border-x-0 border-b border-t-0 lg:border-r"
+              >
+                <div data-hz-ui-lab-log-stream-stage-viewport="shared" className="hb-scrollbar max-h-[620px] overflow-auto">
+                  <div className="divide-y divide-[var(--hz-ui-line-soft)]">
+                    {['ERROR', 'WARN', 'INFO'].map((severity, index) => (
+                      <HzLogStreamLiveRow
+                        key={severity}
+                        selected={index === 0}
+                        className="h-10 text-[12px]"
+                        data-hz-ui-lab-log-stream-row="shared"
+                        data-log-stream-row-owner="hertzbeat-ui-log-stream-row"
+                        data-hz-ui-lab-log-stream-stage-row={severity.toLowerCase()}
+                      >
+                        <HzStatusBadge tone={index === 0 ? 'critical' : index === 1 ? 'warning' : 'info'}>
+                          {severity}
+                        </HzStatusBadge>
+                        <span className="truncate font-mono text-[11px] text-[#7f8a9d]">21:24:{16 + index}</span>
+                        <span className="min-w-0 truncate font-mono text-[var(--hz-ui-text)]">checkout event {index + 1}</span>
+                        <span className="hidden min-w-0 items-center gap-2 text-[11px] text-[#7f8a9d] sm:flex">
+                          checkout-api
+                        </span>
+                      </HzLogStreamLiveRow>
+                    ))}
+                  </div>
+                </div>
+                <HzDetailAside
+                  data-hz-ui-lab-log-stream-stage-detail="shared"
+                  data-log-stream-stage-detail-owner="hertzbeat-ui-detail-aside"
+                >
+                  <HzPanelHeader
+                    data-hz-ui-lab-log-stream-stage-detail-header="shared"
+                    eyebrow="Selected log"
+                    title="checkout timeout"
+                    className="-mx-4 -mt-4 border-x-0 border-t-0 bg-transparent"
+                  />
+                  <HzDetailBodyStack
+                    data-hz-ui-lab-log-stream-stage-detail-body="shared"
+                    data-log-stream-stage-detail-body-owner="hertzbeat-ui-detail-body-stack"
+                  >
+                    <HzDetailRows rows={[{ key: 'trace', title: 'traceId', copy: 'trace-20260523' }]} />
+                  </HzDetailBodyStack>
+                </HzDetailAside>
+              </HzWorkbenchLayout>
+            </HzPanelSurface>
+            <HzWorkbenchLayout
+              as="div"
+              variant="summary-trend"
+              data-hz-ui-lab-signal-summary-trend-layout="shared"
+              data-signal-summary-trend-layout-owner="hertzbeat-ui-workbench-layout"
+              className="border-x-0 border-b border-t-0 p-2 lg:border-r"
+            >
+              {[
+                { id: 'total', label: 'Traces', value: 42, tone: 'info' as const },
+                { id: 'errors', label: 'Errors', value: 3, tone: 'warning' as const },
+                { id: 'rows', label: 'Rows', value: 8, tone: 'neutral' as const },
+                { id: 'latest', label: 'Latest', value: 'now', tone: 'success' as const }
+              ].map(item => (
+                <HzStatCell
+                  key={item.id}
+                  label={item.label}
+                  value={item.value}
+                  tone={item.tone}
+                  variant="tile"
+                  data-hz-ui-lab-summary-trend-stat={item.id}
+                />
+              ))}
+              <HzSignalTrendBars
+                data-hz-ui-lab-summary-trend-bars="shared"
+                title="Trace trend"
+                meta="4 points"
+                bars={[
+                  { id: '00:00', label: '00:00', value: 1, tone: 'info' },
+                  { id: '00:05', label: '00:05', value: 4, tone: 'success' },
+                  { id: '00:10', label: '00:10', value: 2, tone: 'warning' },
+                  { id: '00:15', label: '00:15', value: 6, tone: 'critical' }
+                ]}
+              />
+            </HzWorkbenchLayout>
+            <HzWorkbenchLayout
+              variant="table-detail"
+              data-hz-ui-lab-signal-table-detail-layout="shared"
+              data-signal-table-detail-layout-owner="hertzbeat-ui-workbench-layout"
+              className="border-x-0 border-b border-t-0 p-2 lg:border-r"
+            >
+              <HzPanelSurface
+                className="min-h-20 px-3 py-2 shadow-none"
+                clip
+                data-hz-ui-lab-table-detail-panel="table"
+                data-hz-ui-lab-panel-surface-clip="shared"
+              >
+                <HzDataMetaText display="block" casing="plain">
+                  Recent traces table
+                </HzDataMetaText>
+              </HzPanelSurface>
+              <HzPanelSurface
+                className="min-h-20 px-3 py-2 shadow-none"
+                clip
+                data-hz-ui-lab-table-detail-panel="detail"
+                data-hz-ui-lab-panel-surface-clip="shared"
+              >
+                <HzDataMetaText display="block" casing="plain">
+                  Selected trace detail
+                </HzDataMetaText>
+              </HzPanelSurface>
+            </HzWorkbenchLayout>
+            <HzWorkbenchLayout
+              as="div"
+              variant="detail-stack"
+              data-hz-ui-lab-signal-detail-stack-layout="shared"
+              data-signal-detail-stack-layout-owner="hertzbeat-ui-workbench-layout"
+              className="border-x-0 border-b border-t-0 lg:border-r"
+            >
+              <HzDetailRows
+                data-hz-ui-lab-detail-stack-rows="shared"
+                rows={[
+                  { key: 'service', title: 'Service', copy: 'checkout-api' },
+                  { key: 'duration', title: 'Duration', copy: '120ms' }
+                ]}
+              />
+              <HzAttributeDiagnostics
+                data-hz-ui-lab-detail-stack-diagnostics="shared"
+                title="Attribution diagnostics"
+                rows={[
+                  {
+                    key: 'entity-id',
+                    label: 'hertzbeat.entity_id',
+                    value: 'entity-checkout',
+                    meta: 'Resolved from resource',
+                    state: 'present',
+                    stateLabel: 'Present',
+                    tone: 'success'
+                  }
+                ]}
+              />
+            </HzWorkbenchLayout>
+            <HzWorkbenchLayout
+              as="div"
+              variant="detail-footer"
+              data-hz-ui-lab-signal-detail-footer-layout="shared"
+              data-signal-detail-footer-layout-owner="hertzbeat-ui-workbench-layout"
+              className="border-x-0 border-b lg:border-r"
+            >
+              <HzStateNotice
+                data-hz-ui-lab-detail-footer-alert-hint="shared"
+                title="Alert context remains attached to this trace."
+                variant="hint"
+              />
+              <HzStateNotice
+                data-hz-ui-lab-detail-footer-signal-hint="shared"
+                title="Trace, log, and metric handoffs use the same context."
+                variant="hint"
+              />
+              <HzButtonLink data-hz-ui-lab-detail-footer-action="shared" href="#" size="md">
+                <HzButtonIcon
+                  icon={Timer}
+                  data-hz-ui-lab-detail-footer-action-icon="logs"
+                  data-trace-detail-footer-action-icon-owner="hertzbeat-ui-button-icon"
+                />
+                Open logs
+              </HzButtonLink>
+            </HzWorkbenchLayout>
+            <HzPanelHeader
+              data-hz-ui-lab-signal-stream-stage-header="shared"
+              data-signal-stream-stage-header-owner="hertzbeat-ui-panel-header"
+              eyebrow="Log stream"
+              title="Live log stream"
+              meta={(
+                <>
+                  <HzStatusBadge tone="success" size="xs">connected</HzStatusBadge>
+                  <HzStatusBadge tone="neutral" size="xs">128 rows</HzStatusBadge>
+                </>
+              )}
+              actions={(
+                <>
+                  <HzButton size="sm" intent="secondary">
+                    <RefreshCw className="h-4 w-4" aria-hidden="true" />
+                    Reconnect
+                  </HzButton>
+                  <HzButton size="sm" intent="ghost">
+                    <Play className="h-4 w-4" aria-hidden="true" />
+                    Pause
+                  </HzButton>
+                </>
+              )}
+              className="border-x-0 border-b border-t-0 bg-transparent lg:border-r"
+            />
+            <HzStateNotice
+              data-hz-ui-lab-signal-stream-notice="shared"
+              data-signal-stream-notice-owner="hertzbeat-ui-state-notice"
+              data-signal-stream-notice-kind="paused"
+              tone="warning"
+              title="Stream is paused; buffered entries stay visible until you resume."
+              variant="embedded"
+              className="border-x-0 border-b border-t-0 bg-[#17140b] lg:border-r"
+            />
+            <HzStateNotice
+              data-hz-ui-lab-signal-stream-helper="shared"
+              data-signal-stream-helper-owner="hertzbeat-ui-state-notice"
+              data-signal-stream-helper-kind="selected-empty"
+              tone="info"
+              title="Select any log to inspect full content, trace context, and entity context."
+              variant="hint"
+              className="border-x-0 border-b border-t-0 bg-transparent lg:border-r"
+            />
+            <HzSignalTrendBars
+              data-hz-ui-lab-signal-trend-bars="shared"
+              title="Signal trend"
+              meta="4 points"
+              bars={[
+                { id: '00:00', label: '00:00', value: 2, tone: 'info' },
+                { id: '00:05', label: '00:05', value: 5, tone: 'success' },
+                { id: '00:10', label: '00:10', value: 3, tone: 'warning' },
+                { id: '00:15', label: '00:15', value: 7, tone: 'critical' }
+              ]}
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] lg:border-r lg:border-t-0"
+            />
+            <div
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] px-0 py-2 lg:border-r lg:border-t-0"
+              data-hz-ui-lab-signal-detail-rows="shared"
+            >
+              <HzDetailRows
+                heading="Signal evidence"
+                rows={[
+                  { key: 'service', title: 'Service', copy: 'checkout-api', meta: 'resource.service.name' },
+                  { key: 'trace', title: 'Trace ID', copy: 'trace-20260523', meta: 'current selection' }
+                ]}
+                data-signal-detail-rows-owner="hertzbeat-ui-detail-rows"
+              />
+            </div>
+            <div
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] px-0 py-2 lg:border-r lg:border-t-0"
+              data-hz-ui-lab-signal-dialog-detail-rows="shared"
+            >
+              <HzDetailRows
+                heading="Dialog selection"
+                rows={[
+                  { key: 'span', title: 'Selected span', copy: 'payment.validate', meta: 'side drawer' },
+                  { key: 'event', title: 'Selected event', copy: 'exception', meta: '+72 ms' }
+                ]}
+                data-signal-dialog-detail-rows-owner="hertzbeat-ui-detail-rows"
+              />
+            </div>
+            <HzStateNotice
+              data-hz-ui-lab-signal-dialog-warning="shared"
+              data-signal-dialog-warning-owner="hertzbeat-ui-state-notice"
+              data-signal-dialog-warning-kind="attached-state"
+              tone="warning"
+              title="This log was detached from the live stream; inspect the preserved payload before handoff."
+              variant="embedded"
+              className="rounded-[6px] border border-[rgba(255,184,77,.35)] bg-[rgba(255,184,77,.08)]"
+            />
+            <HzAttributeDiagnostics
+              data-hz-ui-lab-attribute-diagnostics="shared"
+              data-signal-attribute-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"
+              title="Attribution diagnostics"
+              namespaceLabel="hertzbeat.*"
+              rows={[
+                {
+                  key: 'entity',
+                  label: 'hertzbeat.entity_id',
+                  value: 'entity-1',
+                  meta: 'present',
+                  state: 'present',
+                  stateLabel: 'Present',
+                  tone: 'success'
+                },
+                {
+                  key: 'collector',
+                  label: 'hertzbeat.collector',
+                  value: '-',
+                  meta: 'missing',
+                  state: 'missing',
+                  stateLabel: 'Missing',
+                  tone: 'critical'
+                }
+              ]}
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] lg:border-r lg:border-t-0"
+            />
+            <HzAttributeDiagnostics
+              data-hz-ui-lab-dialog-attribute-diagnostics="shared"
+              data-signal-dialog-attribute-diagnostics-owner="hertzbeat-ui-attribute-diagnostics"
+              title="Dialog attribution"
+              namespaceLabel="hertzbeat.*"
+              rows={[
+                {
+                  key: 'dialog-entity',
+                  label: 'hertzbeat.entity_id',
+                  value: 'entity-dialog',
+                  meta: 'selected log',
+                  state: 'present',
+                  stateLabel: 'Present',
+                  tone: 'success'
+                },
+                {
+                  key: 'dialog-template',
+                  label: 'hertzbeat.template',
+                  value: '-',
+                  meta: 'disabled handoff',
+                  state: 'missing',
+                  stateLabel: 'Missing',
+                  tone: 'critical'
+                }
+              ]}
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] lg:border-r lg:border-t-0"
+            />
+            <HzEmptyState
+              data-hz-ui-lab-signal-empty-state="shared"
+              data-signal-empty-state-owner="hertzbeat-ui-empty-state"
+              title="No matching signals"
+              description="Widen the time range or clear signal filters."
+              actions={<HzButton size="sm" intent="ghost" onClick={clearFilterClauses}>Clear</HzButton>}
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] lg:border-r lg:border-t-0"
+            />
+            <div
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] lg:col-span-2 lg:border-r"
+              data-hz-ui-lab-signal-data-table="shared"
+              data-signal-data-table-owner="hertzbeat-ui-data-table"
+            >
+              <HzDataTable
+                variant="embedded"
+                rows={[
+                  { id: 'log-001', time: '16:48:12', service: 'checkout-api', state: 'ERROR', trace: 'trace-91f2' },
+                  { id: 'trace-002', time: '16:48:10', service: 'payment-api', state: 'OK', trace: 'trace-77a4' }
+                ]}
+                getRowKey={row => row.id}
+                selectedRowKey="log-001"
+                onRowClick={row => setContextMessage(`Signal row · ${row.service} · ${row.trace}`)}
+                getRowProps={row => ({ 'data-signal-data-table-row': row.id })}
+                columns={[
+                  { key: 'time', header: 'Time', width: '112px', render: row => <span className="font-mono">{row.time}</span> },
+                  { key: 'service', header: 'Service', render: row => <span className="font-semibold text-[#edf3fb]">{row.service}</span> },
+                  {
+                    key: 'state',
+                    header: 'State',
+                    width: '96px',
+                    render: row => <HzStatusBadge tone={row.state === 'ERROR' ? 'critical' : 'success'} size="xs">{row.state}</HzStatusBadge>
+                  },
+                  { key: 'trace', header: 'Trace', width: '140px', render: row => <span className="font-mono text-[#8f99ab]">{row.trace}</span> }
+                ]}
+              />
+            </div>
+            <HzBarGauge
+              title="Collector saturation"
+              value={68}
+              min={0}
+              max={100}
+              unit="%"
+              tone="info"
+              thresholds={[
+                { value: 80, label: 'warn', tone: 'warning' },
+                { value: 90, label: 'critical', tone: 'critical' }
+              ]}
+              detail="collector-a pool"
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] lg:border-r lg:border-t-0"
+            />
+            <HzThresholdRail
+              title="p95 latency"
+              value={118}
+              min={0}
+              max={200}
+              unit="ms"
+              thresholds={[
+                { value: 120, label: 'warn', tone: 'warning' },
+                { value: 180, label: 'critical', tone: 'critical' }
+              ]}
+              className="border-x-0 border-y-0 border-t border-[var(--hz-ui-line-soft)] lg:border-t-0"
+            />
+          </div>
+          <div
+            className="grid min-w-0 gap-3 border-b border-[var(--hz-ui-line-soft)] px-3 py-3 lg:grid-cols-[minmax(0,1fr)_260px]"
+            data-hz-ui-lab-monitor-panel-primitives="shared"
+          >
+            <HzMonitorStatGrid
+              items={[
+                { label: 'Latest value', value: '25' },
+                { label: 'Delta', value: '+10', tone: 'success' },
+                { label: 'Range', value: '10 - 29' }
+              ]}
+            />
+            <HzMonitorSignalBars
+              items={[
+                { label: 'usage', value: '72 %', widthPercent: 72, tone: 'info' },
+                { label: 'idle', value: '28 %', widthPercent: 28, tone: 'neutral' }
+              ]}
+            />
+          </div>
+          <HzTimeDistributionChart
+            title="Signal volume"
+            buckets={signalVolumeBuckets}
+            selectedBucketId={selectedSignalBucket.id}
+            hiddenSegmentIds={hiddenSignalSegmentIds}
+            onBucketSelect={bucket => {
+              setSelectedSignalBucketId(bucket.id);
+              setChartWindowSource('signal-volume');
+              setContextMessage(`图表窗口: ${bucket.id} · signal volume`);
+            }}
+            onLegendToggle={segment => {
+              const hidden = hiddenSignalSegmentIds.includes(segment.id);
+              setHiddenSignalSegmentIds(previous => (hidden ? previous.filter(id => id !== segment.id) : [...previous, segment.id]));
+              setContextMessage(`图例: ${segment.id} ${hidden ? 'shown' : 'hidden'} · signal volume`);
+            }}
+            className="border-x-0 border-t-0"
+          />
+          <div
+            className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-b border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface-soft)] px-3 py-2 text-[11px]"
+            data-hz-ui="chart-time-window-handoff"
+            data-hz-chart-window-source={chartWindowSource}
+            data-hz-chart-window-bucket={selectedSignalBucket.id}
+            data-hz-chart-window-point={selectedLatencyPoint?.id || ''}
+          >
+            <div className="min-w-0">
+              <div className="truncate text-[12px] font-semibold text-[#f3f6fb]">Chart window</div>
+              <div className="mt-0.5 truncate font-mono text-[10px] text-[#8f99ab]">
+                {selectedSignalBucket.label} · {selectedLatencyPoint?.series.label || 'series'} {selectedLatencyPoint?.point.value || '-'}ms
+              </div>
+            </div>
+            <div className="shrink-0 font-mono text-[10px] text-[#727b8c]">handoff</div>
+          </div>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)] 2xl:grid-cols-[minmax(0,1fr)_300px]">
+            <HzTimeSeriesChart
+              title="Collector latency"
+              unit="milliseconds"
+              series={collectorLatencySeries}
+              selectedPointId={selectedLatencyPointId}
+              hiddenSeriesIds={hiddenLatencySeriesIds}
+              onPointSelect={(point, series) => {
+                setSelectedLatencyPointId(`${series.id}:${point.label}`);
+                setSelectedSignalBucketId(point.label);
+                setChartWindowSource('collector-latency');
+                setContextMessage(`图表窗口: ${series.id} ${point.label} · ${point.value}ms`);
+              }}
+              onLegendToggle={series => {
+                const hidden = hiddenLatencySeriesIds.includes(series.id);
+                setHiddenLatencySeriesIds(previous => (hidden ? previous.filter(id => id !== series.id) : [...previous, series.id]));
+                setContextMessage(`图例: ${series.id} ${hidden ? 'shown' : 'hidden'} · collector latency`);
+              }}
+              height={128}
+              className="border-x-0 border-y-0 2xl:border-r 2xl:border-[var(--hz-ui-line-soft)]"
+            />
+            <HzStatusTimeline
+              title="Availability timeline"
+              rows={availabilityTimelineRows}
+              className="border-x-0 border-b-0 border-t border-[var(--hz-ui-line-soft)] 2xl:border-t-0"
+            />
+          </div>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)]" data-hz-ui-lab-monitor-history-chart="shared">
+            <div
+              className="min-w-0"
+              data-hz-ui-lab-monitor-history-time-toolbar="shared"
+              data-hz-ui-lab-time-foundation="shared-platform-time-context"
+              data-hz-ui-lab-time-foundation-scope="monitor-metrics-logs-traces-alerts-otlp"
+              data-hz-ui-lab-time-foundation-models="quick-relative-absolute-recent-custom-validation"
+              data-hz-ui-lab-time-foundation-url-contract="from-to-refresh-live-timezone"
+              data-hz-ui-lab-time-foundation-datazoom="preview-apply-reset"
+              data-hz-ui-lab-time-foundation-datazoom-live="echarts-native"
+              data-hz-ui-lab-time-foundation-readiness="platform-grade-monitor-history"
+              data-hz-ui-lab-time-foundation-checks="single-line-dark-quick-relative-absolute-manual-timezone-refresh-recent-custom-validation-datazoom"
+              data-monitor-history-time-toolbar-owner="hertzbeat-ui-time-range-toolbar"
+              data-monitor-history-datazoom-feedback="time-toolbar"
+            >
+              <HzTimeRangePreviewHandoff
+                state={timeFoundationPreviewSource ? 'preview' : 'applied'}
+                source={timeFoundationPreviewSource || 'query-time'}
+                from={timeFoundationValue.from || timeFoundationValue.start}
+                to={timeFoundationValue.to || timeFoundationValue.end}
+                simulateLabel={t('ui-lab.time-range.simulate-zoom')}
+                applyLabel={t('ui-lab.time-range.apply-query')}
+                resetLabel={t('time.range.reset')}
+                onSimulate={() => {
+                  setTimeFoundationValue(uiLabChartZoomPreviewValue);
+                  setTimeFoundationPreviewSource('chart-datazoom');
+                  setTimeRangeId('1h');
+                  setRefreshIntervalId('30s');
+                  setContextMessage('Monitor history zoom preview');
+                }}
+                onApply={() => {
+                  setTimeFoundationPreviewSource(undefined);
+                  setContextMessage('Monitor history zoom applied');
+                }}
+                onReset={() => {
+                  setTimeFoundationValue(uiLabDefaultTimeFoundationValue);
+                  setTimeFoundationPreviewSource(undefined);
+                  setTimeRangeId('15m');
+                  setRefreshIntervalId('30s');
+                  setContextMessage('Monitor history zoom reset');
+                }}
+                className="relative z-[60] bg-[var(--hz-ui-surface-graphite)] px-3"
+                data-hz-ui-lab-time-foundation-datazoom-handoff="shared"
+              />
+              <HzTimeRangeToolbar
+                value={timeFoundationValue}
+                showAbsoluteFields
+                absoluteFieldsLayout="inline"
+                absoluteInputMode="datetime-local"
+                timeRangePickerMode="single"
+                railLayout="nowrap"
+                previewSource={timeFoundationPreviewSource}
+                timePickerDefaultOpen
+                presets={timeFoundationPresets}
+                refreshOptions={timeFoundationRefreshOptions}
+                recentRanges={timeFoundationRecentRanges}
+                recentStorageKey="hertzbeat.uiLab.timeRange.recent"
+                customRanges={timeFoundationCustomRanges}
+                customStorageKey="hertzbeat.uiLab.timeRange.custom"
+                labels={timeFoundationLabels}
+                onApply={nextValue => {
+                  const nextRange = (nextValue.timeRange || 'last-15m').replace(/^last-/, '');
+                  setTimeFoundationValue(nextValue);
+                  setTimeFoundationPreviewSource(undefined);
+                  setTimeRangeId(nextRange);
+                  setRefreshIntervalId(uiLabTimeContextRefreshToRefreshId(nextValue.refresh));
+                  setContextMessage(`Monitor history time · ${nextRange} · ${nextValue.refresh || 'manual'}`);
+                }}
+                onRefresh={() => setContextMessage(`Monitor history refresh · ${timeRangeId}`)}
+                onReset={() => {
+                  setTimeFoundationValue(uiLabDefaultTimeFoundationValue);
+                  setTimeFoundationPreviewSource(undefined);
+                  setTimeRangeId('15m');
+                  setRefreshIntervalId('30s');
+                  setContextMessage('Monitor history time reset');
+                }}
+                presetOptionDataAttribute="data-monitor-history-time-range-option"
+                refreshActionProps={{ 'data-monitor-history-refresh-action': 'true' } as React.ButtonHTMLAttributes<HTMLButtonElement>}
+              />
+            </div>
+            <div
+              className="flex min-h-10 items-center border-b border-[var(--hz-ui-line-soft)] px-3 py-2"
+              data-hz-ui-lab-monitor-history-mode-tabs="shared"
+              data-monitor-history-mode-owner="hertzbeat-ui-tabs"
+              data-monitor-history-mode-active={historyAggregationModeId === 'aggregated' ? 'aggregated' : 'raw'}
+            >
+              <HzSegmentedTabs
+                activeId={historyAggregationModeId}
+                items={[
+                  { id: 'raw', label: 'Raw' },
+                  { id: 'aggregated', label: 'Aggregated' }
+                ]}
+                onSelect={value => {
+                  setHistoryAggregationModeId(value);
+                  setContextMessage(`Monitor history mode · ${value}`);
+                }}
+              />
+            </div>
+            <HzActionGroup
+              layout="split"
+              data-hz-ui-lab-monitor-history-controls="shared"
+              data-hz-ui-lab-monitor-fullscreen-frame="shared"
+            >
+              <HzActionGroup density="inline">
+                <HzUnderlineToggle
+                  selected
+                  selectionAttrName="data-chart-selected"
+                  data-monitor-history-toggle-owner="hertzbeat-ui-underline-toggle"
+                  onClick={() => setContextMessage('Monitor history chart series · mean')}
+                >
+                  Mean
+                </HzUnderlineToggle>
+                <HzUnderlineToggle
+                  selected={false}
+                  selectionAttrName="data-chart-selected"
+                  data-monitor-history-toggle-owner="hertzbeat-ui-underline-toggle"
+                  onClick={() => setContextMessage('Monitor history chart series · max')}
+                >
+                  Max
+                </HzUnderlineToggle>
+              </HzActionGroup>
+              <HzActionGroup density="inline">
+                <HzButton
+                  size="sm"
+                  intent="ghost"
+                  data-monitor-history-control-owner="hertzbeat-ui-button"
+                  onClick={() => setContextMessage('Monitor history latest point')}
+                >
+                  Latest point
+                </HzButton>
+                <HzButton
+                  size="sm"
+                  intent="ghost"
+                  data-monitor-history-control-owner="hertzbeat-ui-button"
+                  onClick={() => setHistoryFullscreenOpen(true)}
+                >
+                  Fullscreen
+                </HzButton>
+              </HzActionGroup>
+            </HzActionGroup>
+            <HzActionGroup
+              layout="stack"
+              density="inline"
+              data-hz-ui-lab-monitor-history-action-stack="shared"
+              data-monitor-history-line-action-stack-owner="hertzbeat-ui-action-group"
+            >
+              <HzActionGroup density="inline">
+                <HzButton
+                  size="sm"
+                  intent="ghost"
+                  data-monitor-history-line-control-owner="hertzbeat-ui-button"
+                  onClick={() => setContextMessage('Monitor history chart · primary only')}
+                >
+                  Primary only
+                </HzButton>
+                <HzButton
+                  size="sm"
+                  intent="ghost"
+                  data-monitor-history-line-control-owner="hertzbeat-ui-button"
+                  onClick={() => setContextMessage('Monitor history chart · show all')}
+                >
+                  Show all stats
+                </HzButton>
+              </HzActionGroup>
+              <HzActionGroup density="inline">
+                <HzButton
+                  size="sm"
+                  intent="secondary"
+                  data-monitor-history-line-control-owner="hertzbeat-ui-button"
+                  onClick={() => setContextMessage('Monitor history chart · mean')}
+                >
+                  Mean
+                </HzButton>
+                <HzButton
+                  size="sm"
+                  intent="ghost"
+                  data-monitor-history-line-control-owner="hertzbeat-ui-button"
+                  onClick={() => setContextMessage('Monitor history chart · max')}
+                >
+                  Max
+                </HzButton>
+              </HzActionGroup>
+            </HzActionGroup>
+            {historyFullscreenOpen ? (
+              <HzMonitorFullscreenFrame
+                title="Monitor history"
+                kicker="History chart"
+                closeLabel="Exit fullscreen"
+                onClose={() => setHistoryFullscreenOpen(false)}
+                data-monitor-history-fullscreen-owner="hertzbeat-ui-fullscreen-frame"
+              >
+                <HzMonitorHistoryChartCard
+                  cardKey="fullscreen:responseTime"
+                  heading="Monitor history · responseTime"
+                  unit="ms"
+                  selected
+                  option={monitorHistoryEChartsOption}
+                  height={220}
+                  footer="61 samples"
+                />
+              </HzMonitorFullscreenFrame>
+            ) : null}
+            <HzMonitorEvidenceFrame
+              className="border-b-0"
+              data-hz-ui-lab-monitor-evidence-frame="shared"
+              data-monitor-history-chart-frame-owner="hertzbeat-ui-evidence-frame"
+              data-hz-ui-lab-monitor-history-refresh-contract="angular-first-page-reload"
+              data-hz-ui-lab-monitor-history-selection-reset="angular-chart-reload"
+            >
+              <HzMonitorHistoryChartGrid layout="single" data-hz-ui-lab-monitor-history-chart-grid="shared">
+                <HzMonitorHistoryChartCard
+                  cardKey="summary:responseTime"
+                  heading="Monitor history · responseTime"
+                  unit="ms"
+                  selected
+                  option={monitorHistoryEChartsOption}
+                  height={220}
+                  footer="61 samples"
+                  actions={
+                    <HzMonitorMetricFavoriteAction
+                      active
+                      label="Remove favorite"
+                      data-hz-ui-lab-monitor-history-favorite-action="shared"
+                    />
+                  }
+                  zoomActionLabel="Apply as query time"
+                  zoomActionProps={{ 'data-monitor-history-zoom-apply': 'local-to-query-time' } as React.ButtonHTMLAttributes<HTMLButtonElement>}
+                  onZoomAction={() => setContextMessage('Monitor history zoom · responseTime')}
+                  onDataZoomChange={() => {
+                    setTimeFoundationValue(uiLabChartZoomPreviewValue);
+                    setTimeFoundationPreviewSource('chart-datazoom');
+                    setContextMessage('Monitor history native zoom preview');
+                  }}
+                  surfaceClassName="min-h-[336px] border-y-0"
+                />
+              </HzMonitorHistoryChartGrid>
+              <HzMonitorIncrementalLoadFooter
+                visibleCount={6}
+                totalCount={14}
+                hasMore
+                loadMoreLabel="Load more charts"
+                completeLabel="All charts loaded"
+                onLoadMore={() => setContextMessage('History charts incremental load')}
+                data-hz-ui-lab-monitor-history-incremental-load="angular-chart-sentinel"
+                data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"
+                data-monitor-detail-incremental-mode="angular-chart-sentinel"
+                data-monitor-detail-incremental-scope="history"
+              />
+            </HzMonitorEvidenceFrame>
+            <HzMonitorEvidenceFrame
+              variant="media"
+              data-hz-ui-lab-monitor-media-frame="shared"
+              data-monitor-detail-grafana-frame-owner="hertzbeat-ui-evidence-frame"
+              data-hz-ui-lab-monitor-detail-grafana-action-contract="angular-edit-config-delete-dashboard"
+            >
+              <iframe title="grafana-dashboard-preview" src="about:blank" />
+            </HzMonitorEvidenceFrame>
+            <HzActionGroup
+              density="inline"
+              data-hz-ui-lab-monitor-detail-grafana-actions="shared"
+              data-monitor-detail-grafana-actions-owner="hertzbeat-ui-action-group"
+              data-monitor-detail-grafana-actions-contract="angular-dashboard-actions"
+            >
+              <HzButtonLink
+                href="/monitors/42/edit?app=website"
+                size="sm"
+                data-monitor-detail-grafana-config-owner="hertzbeat-ui-button-link"
+                data-monitor-detail-grafana-config-action="monitor-edit-grafana"
+              >
+                Configure dashboard
+              </HzButtonLink>
+              <HzButton
+                intent="danger"
+                size="sm"
+                data-monitor-detail-grafana-delete-owner="hertzbeat-ui-button"
+                data-monitor-detail-grafana-delete-action="delete-dashboard"
+                data-hz-ui-lab-monitor-detail-grafana-delete-teardown="angular-hide-tab"
+              >
+                Delete dashboard
+              </HzButton>
+            </HzActionGroup>
+            <div
+              className="border-b border-[var(--hz-ui-line-soft)] px-0 py-2"
+              data-hz-ui-lab-monitor-history-selectable-rows="shared"
+            >
+              <HzSelectableRows
+                heading="Series samples"
+                rows={[
+                  { key: 'origin', title: 'origin', copy: '128', meta: '14:20' },
+                  { key: 'p95', title: 'p95', copy: '144', meta: '14:25' }
+                ]}
+                selectedKey="origin"
+                selectionAttrName="data-series-selected"
+                onSelect={key => setContextMessage(`Monitor history series · ${key}`)}
+              />
+            </div>
+            <div
+              className="border-b border-[var(--hz-ui-line-soft)] px-0 py-2"
+              data-hz-ui-lab-monitor-history-detail-rows="shared"
+            >
+              <HzDetailRows
+                heading="Selected point"
+                rows={[
+                  { key: 'latest', title: 'Latest value', copy: '128', meta: '14:20' },
+                  { key: 'delta', title: 'Delta', copy: '+8', meta: 'origin' }
+                ]}
+                data-monitor-history-summary-owner="hertzbeat-ui-detail-rows"
+              />
+            </div>
+          </div>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)]" data-hz-ui-lab-monitor-basic-card="shared">
+            <HzMonitorBasicCard
+              heading="Monitoring Basic"
+              editHref="/monitors/640360126405888/edit"
+              editLabel="Edit monitor"
+              data-monitor-basic-owner="hertzbeat-ui-basic-card"
+              className="px-0 py-0"
+              surfaceClassName="rounded-none border-x-0 border-y-0"
+            >
+              <HzMonitorBasicSummary
+                name="mysql-prod-01"
+                statusLabel="available"
+                statusTone="success"
+                facts={[
+                  { label: 'Host', value: '127.0.0.1:3306' },
+                  { label: 'Type', value: 'MYSQL' },
+                  { label: 'Collector', value: 'collector-a' },
+                  { label: 'Updated', value: '14:25:31' }
+                ]}
+                metaRows={[
+                  { label: 'Instance', value: 'mysql-prod-01' },
+                  { label: 'Schedule', value: '60s · static' }
+                ]}
+                labels={[
+                  { label: 'env', value: 'prod' },
+                  { label: 'region', value: 'cn' }
+                ]}
+                annotations={[{ label: 'owner', value: 'sre' }]}
+                labelHeading="Labels"
+                annotationHeading="Annotations"
+              />
+            </HzMonitorBasicCard>
+          </div>
+          <HzMonitorDetailSignalList
+            className="border-b border-[var(--hz-ui-line-soft)]"
+            data-hz-ui-lab-monitor-signal-list="shared"
+            data-monitor-detail-signal-list-owner="hertzbeat-ui-signal-list"
+            data-monitor-detail-realtime-payload-errors="card-local"
+            data-hz-ui-lab-monitor-realtime-payload-errors="card-local"
+            data-hz-ui-lab-monitor-realtime-selection-reset="angular-table-reload"
+          >
+            <HzMonitorMetricCardGrid
+              className="grid min-w-0 lg:grid-cols-1"
+              data-hz-ui-lab-monitor-metric-card="shared"
+              data-monitor-detail-card-grid-rhythm="shared-tight"
+            >
+              <HzMonitorMetricCard
+                title="Realtime metric · basic"
+                columns={[
+                  { key: 'responseTime', title: 'responseTime' },
+                  { key: 'status', title: 'status' },
+                  { key: 'collector', title: 'collector' }
+                ]}
+                rows={[
+                  { key: 'mysql-prod-01', label: 'mysql-prod-01', values: ['118ms', 'available', 'collector-a'] },
+                  { key: 'linux-edge-03', label: 'linux-edge-03', values: ['72ms', 'collecting', 'collector-b'] }
+                ]}
+                selected
+                labelHeader="Resource"
+                actions={
+                  <HzMonitorMetricFavoriteAction
+                    active
+                    label="Remove favorite"
+                    onClick={() => setContextMessage('Realtime metric favorite · remove')}
+                    data-hz-ui-lab-monitor-metric-favorite-action="shared"
+                    data-monitor-detail-signal-row-action="favorite"
+                  />
+                }
+                onSelect={() => setContextMessage('Realtime metric card · basic')}
+                data-monitor-detail-metric-card-owner="hertzbeat-ui-metric-card"
+                data-monitor-detail-signal-card="true"
+                data-monitor-detail-signal-card-chrome="hertzbeat-ui-metric-card"
+                data-monitor-detail-signal-row="basic"
+                data-monitor-detail-signal-row-density="shared-metric-card"
+                data-monitor-detail-signal-selected-style="left-rail"
+                className="px-0 py-0"
+                surfaceClassName="rounded-none border-x-0 border-y-0 bg-transparent"
+              />
+            </HzMonitorMetricCardGrid>
+            <HzMonitorIncrementalLoadFooter
+              visibleCount={10}
+              totalCount={24}
+              hasMore
+              loadMoreLabel="Load more metrics"
+              completeLabel="All metrics loaded"
+              onLoadMore={() => setContextMessage('Realtime metrics incremental load')}
+              data-hz-ui-lab-monitor-incremental-load="angular-sentinel"
+              data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"
+              data-monitor-detail-incremental-mode="angular-sentinel"
+            />
+          </HzMonitorDetailSignalList>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)] px-3 py-2" data-hz-ui-lab-monitor-realtime-toolbar="shared">
+            <HzMonitorRealtimeToolbar
+              compact
+              facts={[{ title: 'Collect time', copy: '14:25:31' }]}
+              expandLabel="Fullscreen"
+              onExpand={() => setContextMessage('Realtime metric fullscreen')}
+              data-monitor-realtime-toolbar-owner="hertzbeat-ui-realtime-toolbar"
+            />
+          </div>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)] px-3 py-2" data-hz-ui-lab-monitor-realtime-inspector="shared">
+            <HzMonitorRealtimeInspector
+              variant="details"
+              label="Active row"
+              value="host=db-1"
+              stats={[
+                { label: 'Fields', value: '2' },
+                { label: 'Labels', value: '1' }
+              ]}
+              rows={[
+                { label: 'labels', value: 'host=db-1', meta: 'payload' },
+                { label: 'usage', value: '72', meta: '%' },
+                { label: 'idle', value: '28', meta: '%' }
+              ]}
+              data-monitor-realtime-inspector-owner="hertzbeat-ui-realtime-inspector"
+            />
+          </div>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)] px-3 py-2" data-hz-ui-lab-monitor-realtime-row-nav="shared">
+            <HzMonitorRealtimeRowNavigator
+              label="Row 1 / 2 · host=db-1"
+              previousLabel="Previous"
+              nextLabel="Next"
+              canPrevious={false}
+              canNext
+              onNext={() => setContextMessage('Realtime row navigation · next')}
+              data-monitor-realtime-row-nav-owner="hertzbeat-ui-row-navigator"
+            />
+          </div>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)] px-3 py-2" data-hz-ui-lab-monitor-row-nav="shared">
+            <HzMonitorRowNavigator
+              label="Series 2 / 4 · mysql-prod-01"
+              previousLabel="Previous series"
+              nextLabel="Next series"
+              canPrevious
+              canNext
+              onPrevious={() => setContextMessage('Monitor row navigation · previous')}
+              onNext={() => setContextMessage('Monitor row navigation · next')}
+              data-monitor-history-row-nav-owner="hertzbeat-ui-row-navigator"
+            />
+          </div>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)]" data-hz-ui-lab-monitor-control-band="shared">
+            <HzMonitorControlBand
+              title="Compare scope"
+              variant="embedded"
+              actions={
+                <>
+                  <HzButton intent="ghost" size="sm" onClick={() => setContextMessage('Compare scope · selected only')}>
+                    Selected only
+                  </HzButton>
+                  <HzButton intent="ghost" size="sm" onClick={() => setContextMessage('Compare scope · all')}>
+                    All
+                  </HzButton>
+                </>
+              }
+            >
+              <HzUnderlineToggle selected selectionAttrName="data-compare-selected">
+                basic.max_connections
+              </HzUnderlineToggle>
+              <HzUnderlineToggle selected={false} selectionAttrName="data-compare-selected">
+                cache.query_cache_hit_rate
+              </HzUnderlineToggle>
+            </HzMonitorControlBand>
+          </div>
+          <div className="grid min-w-0 border-b border-[var(--hz-ui-line-soft)]" data-hz-ui-lab-monitor-favorites="shared">
+            <HzMonitorFavoriteSurface
+              value={favoriteMode}
+              options={[
+                { value: 'realtime', label: 'Realtime favorites' },
+                { value: 'history', label: 'History favorites' }
+              ]}
+              selectorLabel="Favorite scope"
+              message="Added to favorites successfully"
+              onValueChange={value => {
+                setFavoriteMode(value);
+                setContextMessage(`Favorite scope · ${value}`);
+              }}
+              data-monitor-detail-favorite-owner="hertzbeat-ui-favorite-surface"
+              data-monitor-detail-favorite-feedback-copy="angular-favorite-copy"
+              data-hz-ui-lab-monitor-favorite-feedback="angular-copy"
+              data-hz-ui-lab-monitor-favorite-set-semantics="angular-set"
+              data-hz-ui-lab-monitor-favorite-active-fallback="angular-subselector-sticky"
+              data-hz-ui-lab-monitor-favorite-history-reload-reset="angular-chart-reload"
+              data-hz-ui-lab-monitor-favorite-empty-contract="angular-subselector-empty"
+              data-hz-ui-lab-monitor-favorite-history-source="favorite-history-chart-payloads"
+              data-hz-ui-lab-monitor-favorite-history-controls="angular-chart-toolbox"
+              data-hz-ui-lab-monitor-favorite-realtime-incremental-load="angular-favorite-sentinel"
+              data-hz-ui-lab-monitor-favorite-history-incremental-load="angular-favorite-chart-sentinel"
+              data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"
+              data-monitor-detail-incremental-mode="angular-favorite-chart-sentinel"
+              data-monitor-detail-incremental-scope="favorite-history"
+              className="border-x-0 border-y-0"
+            >
+              {favoriteMode === 'realtime' ? (
+                <HzMonitorFavoritePane
+                  kind="realtime"
+                  data-hz-ui-lab-monitor-favorite-pane="shared"
+                  data-hz-monitor-favorite-demo="realtime"
+                >
+                  {['basic', 'availability', 'responseTime'].map(name => (
+                    <button
+                      key={name}
+                      type="button"
+                      className="grid min-h-9 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-3 py-2 text-left hover:bg-[var(--hz-ui-surface-soft)]"
+                      onClick={() => setContextMessage(`Favorite metric · ${name}`)}
+                    >
+                      <span className="min-w-0 truncate text-[12px] font-semibold text-[#dbe4f0]">{name}</span>
+                      <span className="font-mono text-[10px] text-[#727b8c]">realtime</span>
+                    </button>
+                  ))}
+                  <HzMonitorIncrementalLoadFooter
+                    visibleCount={3}
+                    totalCount={7}
+                    hasMore
+                    loadMoreLabel="Load more favorite metrics"
+                    onLoadMore={() => setContextMessage('Favorite realtime incremental load')}
+                    data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"
+                    data-monitor-detail-incremental-mode="angular-favorite-sentinel"
+                    data-monitor-detail-incremental-scope="favorite-realtime"
+                  />
+                </HzMonitorFavoritePane>
+              ) : (
+                <HzMonitorFavoritePane
+                  kind="history"
+                  data-hz-ui-lab-monitor-favorite-pane="shared"
+                  data-hz-monitor-favorite-demo="history"
+                >
+                  {['cpu.usage', 'memory.used'].map(name => (
+                    <button
+                      key={name}
+                      type="button"
+                      className="grid min-h-9 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-3 py-2 text-left hover:bg-[var(--hz-ui-surface-soft)]"
+                      onClick={() => setContextMessage(`Favorite history · ${name}`)}
+                    >
+                      <span className="min-w-0 truncate text-[12px] font-semibold text-[#dbe4f0]">{name}</span>
+                      <span className="font-mono text-[10px] text-[#727b8c]">history</span>
+                    </button>
+                  ))}
+                  <HzMonitorIncrementalLoadFooter
+                    visibleCount={6}
+                    totalCount={11}
+                    hasMore
+                    loadMoreLabel="Load more favorite charts"
+                    completeLabel="All favorite charts loaded"
+                    onLoadMore={() => setContextMessage('Favorite history charts incremental load')}
+                    data-hz-ui-lab-monitor-favorite-history-incremental-load="angular-favorite-chart-sentinel"
+                    data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"
+                    data-monitor-detail-incremental-mode="angular-favorite-chart-sentinel"
+                    data-monitor-detail-incremental-scope="favorite-history"
+                  />
+                </HzMonitorFavoritePane>
+              )}
+            </HzMonitorFavoriteSurface>
+            <HzMonitorFavoriteSurface
+              value="history"
+              options={[
+                { value: 'realtime', label: 'Realtime favorites' },
+                { value: 'history', label: 'History favorites' }
+              ]}
+              selectorLabel="Favorite scope"
+              data-hz-ui-lab-monitor-favorite-empty-teardown="angular-empty-no-sentinel"
+              data-hz-ui-lab-monitor-favorite-empty-contract="angular-subselector-empty"
+              data-monitor-detail-favorite-empty-contract="angular-subselector-empty"
+              className="border-x-0 border-b-0 border-t border-[var(--hz-ui-line-soft)]"
+            >
+              <HzEmptyState
+                title="No history favorites"
+                data-monitor-detail-empty-owner="hertzbeat-ui-empty-state"
+                data-monitor-detail-empty-scope="favorite-history"
+                data-monitor-detail-favorite-empty-teardown="angular-empty-no-sentinel"
+              />
+            </HzMonitorFavoriteSurface>
+          </div>
+          <HzMonitorDetailConsoleShell
+            data-hz-ui-lab-monitor-detail-console-shell="shared"
+            data-hz-ui-lab-monitor-detail-scrape-context="angular-nonstatic-scrape-as-app"
+            data-hz-ui-lab-monitor-detail-history-catalog-load="angular-tab-click-lazy"
+            data-hz-ui-lab-monitor-detail-history-tab-load="angular-click-load-metric-chart"
+            data-hz-ui-lab-monitor-detail-realtime-tab-load="angular-click-load-real-time-metric"
+            data-hz-ui-lab-monitor-detail-favorite-tab-load="angular-click-load-favorite-metrics"
+            data-hz-ui-lab-monitor-detail-grafana-fallback-tab="angular-previous-data-tab"
+            data-monitor-console-layout="angular-workbench"
+            data-monitor-first-viewport-rhythm="angular-tight"
+            data-monitor-detail-history-catalog-load="angular-tab-click-lazy"
+            data-monitor-detail-history-tab-load="angular-click-load-metric-chart"
+            data-monitor-detail-realtime-tab-load="angular-click-load-real-time-metric"
+            data-monitor-detail-favorite-tab-load="angular-click-load-favorite-metrics"
+            data-monitor-detail-grafana-fallback-tab="angular-previous-data-tab"
+          >
+            <HzMonitorBreadcrumb
+              data-hz-ui-lab-monitor-inline-context="shared"
+              data-hz-ui-lab-monitor-breadcrumb="shared"
+              data-monitor-detail-header-mode="breadcrumb-only"
+              data-monitor-detail-scrape-context="angular-nonstatic-scrape-as-app"
+            >
+              <span>Overview</span>
+              <span>/</span>
+              <Link
+                href="/monitors?app=mysql"
+                data-hz-ui-lab-monitor-detail-list-return="angular-app-filter"
+                data-monitor-detail-list-return="angular-app-filter"
+                data-monitor-detail-list-return-target="/monitors?app=mysql"
+              >
+                Monitors
+              </Link>
+              <span>/</span>
+              <span>Monitor detail</span>
+              <HzInlineContextMark
+                component="a"
+                href="/setting/define?app=mysql"
+                placement="breadcrumb"
+                data-monitor-detail-context-mark="breadcrumb"
+                data-monitor-detail-context-mark-owner="hertzbeat-ui-inline-context-mark"
+                data-hz-ui-lab-monitor-app-definition-link="angular-monitor-app"
+                data-monitor-detail-app-definition-link="angular-monitor-app"
+                data-monitor-detail-app-definition-target="/setting/define?app=mysql"
+              >
+                MySQL
+              </HzInlineContextMark>
+            </HzMonitorBreadcrumb>
+            <HzMonitorDetailWorkbenchFrame
+              data-hz-ui-lab-monitor-detail-workbench-frame="shared"
+              data-monitor-workbench-stage="angular-layout"
+              tabs={
+                <HzMonitorDetailTabs
+                  data-hz-ui-lab-monitor-detail-tabs="shared"
+                  data-monitor-detail-tabs-owner="hertzbeat-ui-monitor-detail-tabs"
+                  items={[
+                    {
+                      key: 'realtime',
+                      label: (
+                        <HzMonitorDetailTabLabel
+                          tabKey="realtime"
+                          icon={Activity}
+                          data-hz-ui-lab-monitor-detail-tab-label="shared"
+                        >
+                          Realtime
+                        </HzMonitorDetailTabLabel>
+                      )
+                    },
+                    {
+                      key: 'history',
+                      label: (
+                        <HzMonitorDetailTabLabel
+                          tabKey="history"
+                          icon={BarChart3}
+                          data-hz-ui-lab-monitor-detail-tab-label="shared"
+                        >
+                          History
+                        </HzMonitorDetailTabLabel>
+                      )
+                    },
+                    {
+                      key: 'favorites',
+                      label: (
+                        <HzMonitorDetailTabLabel
+                          tabKey="favorites"
+                          icon={Star}
+                          data-hz-ui-lab-monitor-detail-tab-label="shared"
+                        >
+                          Favorites
+                        </HzMonitorDetailTabLabel>
+                      )
+                    },
+                    {
+                      key: 'grafana',
+                      label: 'Grafana'
+                    }
+                  ]}
+                  selectedKey={monitorDetailTabId}
+                  onSelect={value => {
+                    setMonitorDetailTabId(value);
+                    setContextMessage(`Monitor detail tab · ${value}`);
+                  }}
+                  panelIdPrefix="ui-lab-monitor-detail"
+                  extra={
+                    <HzActionGroup
+                      layout="end-wrap"
+                      density="inline"
+                      data-hz-ui-lab-monitor-detail-tab-extra="angular-refresh-help"
+                      data-monitor-detail-tab-extra-owner="hertzbeat-ui-action-group"
+                      data-monitor-detail-tab-extra-contract="angular-refresh-help"
+                    >
+                      <HzMonitorRefreshToolbar
+                        refreshLabel={
+                          monitorDetailRefreshIntervalId === 'off'
+                            ? t('monitor.detail.close-refresh')
+                            : t('monitor.detail.auto-refresh', { time: monitorDetailRefreshIntervalId.replace(/s$/, '') })
+                        }
+                        refreshActionLabel="Refresh monitor detail"
+                        selectedRefresh={monitorDetailRefreshIntervalId}
+                        refreshOptions={[
+                          { id: '90s', time: 90, current: true },
+                          { id: '10s', time: 10 },
+                          { id: '30s', time: 30 },
+                          { id: '60s', time: 60 },
+                          { id: '300s', time: 300 },
+                          { id: 'off', time: null }
+                        ].map(option => ({
+                          value: option.id,
+                          label:
+                            option.time == null
+                              ? t('monitor.detail.close-refresh')
+                              : option.current
+                                ? t('monitor.detail.auto-refresh', { time: option.time })
+                                : t('monitor.detail.config-refresh', { time: option.time })
+                        }))}
+                        signalLinks={[
+                          {
+                            id: 'metrics',
+                            href: '/ingestion/otlp/metrics?timeRange=last-1h&source=ui-lab',
+                            label: 'Metrics Workbench',
+                            icon: <BarChart3 aria-hidden="true" className="h-3.5 w-3.5" />
+                          },
+                          {
+                            id: 'logs',
+                            href: '/log/manage?timeRange=last-1h&source=ui-lab',
+                            label: 'Logs Workbench',
+                            icon: <FileText aria-hidden="true" className="h-3.5 w-3.5" />
+                          },
+                          {
+                            id: 'traces',
+                            href: '/trace/manage?timeRange=last-1h&source=ui-lab',
+                            label: 'Traces Workbench',
+                            icon: <GitBranch aria-hidden="true" className="h-3.5 w-3.5" />
+                          }
+                        ]}
+                        refreshIcon={<RefreshCw size={13} aria-hidden="true" />}
+                        onRefreshChange={value => {
+                          setMonitorDetailRefreshIntervalId(value);
+                          setContextMessage(`Refresh interval · ${value}`);
+                        }}
+                        onRefresh={() => setContextMessage('Monitor detail refresh')}
+                        data-hz-ui-lab-monitor-detail-refresh-toolbar="shared"
+                        data-hz-ui-lab-monitor-detail-default-refresh="angular-deadline-90"
+                        data-hz-ui-lab-monitor-detail-refresh-select="shared"
+                        data-hz-ui-lab-monitor-detail-refresh-options="angular-config-refresh-copy"
+                        data-hz-ui-lab-monitor-detail-countdown-reset="angular-deadline-reset"
+                        data-monitor-detail-countdown-reset-contract="angular-deadline-reset"
+                        data-monitor-detail-refresh-options-contract="angular-config-refresh-copy"
+                        data-monitor-detail-refresh-copy-contract="angular-deadline-label"
+                        data-monitor-detail-default-refresh-contract={monitorDetailRefreshIntervalId === '90s' ? 'angular-deadline-90' : undefined}
+                        data-monitor-detail-refresh-current-option={monitorDetailRefreshIntervalId === '90s' ? 'angular-default-deadline-current' : 'configured-option'}
+                        data-hz-ui-lab-monitor-detail-grafana-refresh-target="previous-data-tab"
+                        data-hz-ui-lab-monitor-signal-handoff="shared"
+                        data-monitor-refresh-toolbar-owner="hertzbeat-ui-refresh-toolbar"
+                        data-monitor-detail-refresh-target={monitorDetailTabId === 'grafana' ? 'history' : monitorDetailTabId}
+                        data-monitor-detail-grafana-refresh-target={monitorDetailTabId === 'grafana' ? 'history' : undefined}
+                      />
+                      <HzIconLink
+                        href="https://hertzbeat.apache.org/docs/help/http"
+                        label="Help"
+                        target="_blank"
+                        rel="noreferrer"
+                        data-hz-ui-lab-monitor-detail-help-action="angular-docs-help"
+                        data-monitor-detail-help-action="angular-docs-help"
+                        data-monitor-detail-help-owner="hertzbeat-ui-icon-link"
+                        data-monitor-detail-help-target="https://hertzbeat.apache.org/docs/help/http"
+                      >
+                        <HelpCircle aria-hidden="true" className="h-3.5 w-3.5" />
+                      </HzIconLink>
+                    </HzActionGroup>
+                  }
+                />
+              }
+            >
+              <HzMonitorDetailTabPanel
+                id="ui-lab-monitor-detail-panel-realtime"
+                tabId="ui-lab-monitor-detail-tab-realtime"
+                active
+                data-hz-ui-lab-monitor-detail-tab-panel="shared"
+                data-monitor-console-tab-panel-owner="hertzbeat-ui-detail-tab-panel"
+              >
+                <HzMonitorDetailTabSequence data-hz-ui-lab-monitor-detail-tab-sequence="shared">
+                  <HzMonitorDetailStage
+                    title="Monitor detail stage"
+                    header="hidden"
+                    rhythm="stack"
+                    data-hz-ui-lab-monitor-detail-stage="shared"
+                    data-monitor-detail-stage-owner="hertzbeat-ui-detail-stage"
+                  >
+                    <div className="grid min-h-9 grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
+                      <span className="min-w-0 truncate text-[12px] font-semibold text-[#dbe4f0]">Realtime metric rows</span>
+                      <span className="font-mono text-[10px] text-[#727b8c]">shared stage</span>
+                    </div>
+                  </HzMonitorDetailStage>
+                </HzMonitorDetailTabSequence>
+              </HzMonitorDetailTabPanel>
+            </HzMonitorDetailWorkbenchFrame>
+          </HzMonitorDetailConsoleShell>
+          <HzMonitorEditorForm
+            data-hz-ui-lab-monitor-editor-form="shared"
+            data-hz-ui-lab-monitor-editor-default-app="website"
+            data-hz-ui-lab-monitor-editor-app-source="angular-route-context-hidden-field"
+            data-hz-ui-lab-monitor-editor-static-host-position="angular-before-name"
+            data-hz-ui-lab-monitor-editor-field-order="angular-monitor-form-sequence"
+            data-hz-ui-lab-monitor-editor-detect-payload="angular-monitor-collector-params-no-grafana"
+            data-hz-ui-lab-monitor-editor-save-payload="angular-monitor-collector-params-grafana"
+            data-hz-ui-lab-monitor-editor-payload-param-merge="angular-params-advanced-sdparams"
+            data-hz-ui-lab-monitor-editor-payload-host-instance="angular-host-param-as-instance"
+            data-hz-ui-lab-monitor-editor-service-discovery-params="angular-nonstatic-only"
+            data-hz-ui-lab-monitor-editor-scrape-reload="angular-reset-on-user-scrape-change"
+            data-hz-ui-lab-monitor-editor-ssl-port-notice="angular-info-notification"
+            data-hz-ui-lab-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"
+            data-hz-ui-lab-monitor-editor-advanced-visible="angular-visible-param-only"
+            data-hz-ui-lab-monitor-editor-label-selector="angular-app-label-selector"
+            data-hz-ui-lab-monitor-editor-collector-selection="angular-collectors-selection-tags"
+            data-hz-ui-lab-monitor-editor-interval-stepper="angular-min-step-max-by-app"
+            data-hz-ui-lab-monitor-editor-detect-cron-validation="angular-detect-skips-cron-format"
+            data-hz-ui-lab-monitor-editor-cron-required="angular-required-before-detect-save"
+            data-monitor-editor-detect-cron-validation="angular-detect-skips-cron-format"
+            data-monitor-editor-cron-required="angular-required-before-detect-save"
+            onSubmit={event => {
+              event.preventDefault();
+              setContextMessage('Monitor form submit action');
+            }}
+            actionBar={
+              <HzMonitorEditorActionBar
+                data-hz-ui-lab-monitor-editor-action-bar="shared"
+                data-hz-ui-lab-monitor-editor-cancel-return="legacy-list-root"
+                title="Monitor editor actions"
+                status="clean"
+                statusLabel="Ready"
+                summaryVisible={false}
+                actionAlign="center"
+                className="border-x-0"
+                actions={[
+                  {
+                    id: 'detect',
+                    label: 'Detect',
+                    intent: 'ghost',
+                    buttonProps: {
+                      'data-monitor-editor-detect-cron-validation': 'angular-detect-skips-cron-format'
+                    },
+                    onSelect: () => setContextMessage('Monitor detect action')
+                  },
+                  {
+                    id: 'submit',
+                    label: 'OK',
+                    type: 'submit',
+                    intent: 'primary',
+                    buttonProps: {
+                      'data-monitor-editor-save-return': 'angular-app-list',
+                      'data-monitor-editor-save-return-target': '/monitors?app=website',
+                      'data-monitor-editor-save-notification-contract': 'angular-success-before-return'
+                    },
+                    onSelect: () => setContextMessage('Monitor save action')
+                  },
+                  {
+                    id: 'cancel',
+                    label: 'Cancel',
+                    intent: 'ghost',
+                    buttonProps: {
+                      'data-monitor-editor-cancel-return': 'legacy-list-root',
+                      'data-monitor-editor-cancel-return-target': '/monitors'
+                    },
+                    onSelect: () => setContextMessage('Monitor cancel action')
+                  }
+                ]}
+              />
+            }
+          >
+            <HzMonitorEditorHeader
+              data-hz-ui-lab-monitor-editor-header="shared"
+              title="Monitor editor header"
+              className="border-x-0"
+            />
+            <HzMonitorEditorSection
+              data-hz-ui-lab-monitor-editor-section="shared"
+              title="Monitor editor section"
+              copy="Shared section shell for monitor new/edit forms."
+              className="border-x-0"
+            >
+              <HzMonitorEditorFieldGrid
+                data-hz-ui-lab-monitor-editor-field-grid="shared"
+                data-hz-ui-lab-monitor-field-controls="shared"
+              >
+                <HzField label="Scrape">
+                  <HzSelect
+                    value="http_sd"
+                    aria-label="Scrape"
+                    data-monitor-editor-scrape-reload-contract="angular-reset-on-user-scrape-change"
+                    options={[
+                      { value: 'static', label: 'static' },
+                      { value: 'http_sd', label: 'http_sd' }
+                    ]}
+                    onChange={() => setContextMessage('Monitor scrape changed')}
+                  />
+                </HzField>
+                <HzField
+                  label="HTTP SD endpoint"
+                  data-monitor-editor-scrape-param-order="angular-after-scrape-before-name"
+                >
+                  <HzInput value="https://service-discovery.local/targets" readOnly />
+                </HzField>
+                <HzField
+                  label="Host"
+                  data-hz-ui-lab-monitor-host-name-autofill="angular-new-host-change"
+                  data-monitor-editor-static-host-field="angular-before-name"
+                >
+                  <HzInput
+                    value="checkout.example.com"
+                    readOnly
+                    data-monitor-editor-host-name-autofill-contract="angular-new-host-change"
+                  />
+                </HzField>
+                <HzField label="Monitor name">
+                  <HzInput
+                    value="Bright_Probe_23AB"
+                    readOnly
+                    data-monitor-editor-host-name-autofill-target="monitor-name"
+                  />
+                </HzField>
+                <div
+                  className="col-span-full"
+                  data-hz-ui-lab-monitor-editor-advanced-collapse-demo="angular-ghost-collapse-dashed-trigger"
+                  data-hz-ui-lab-monitor-editor-advanced-visible-demo="angular-visible-param-only"
+                  data-monitor-editor-advanced-collapse="angular-ghost-collapse-dashed-trigger"
+                  data-monitor-editor-advanced-collapse-state={monitorEditorAdvancedOpen ? 'expanded' : 'collapsed'}
+                >
+                  <HzButton
+                    type="button"
+                    size="sm"
+                    intent="secondary"
+                    aria-expanded={monitorEditorAdvancedOpen}
+                    data-monitor-editor-advanced-toggle="angular-dashed-collapse-trigger"
+                    data-monitor-editor-advanced-toggle-owner="hertzbeat-ui-button"
+                    onClick={() => setMonitorEditorAdvancedOpen(open => !open)}
+                  >
+                    <SlidersHorizontal aria-hidden="true" className="h-3.5 w-3.5" />
+                    Advanced
+                  </HzButton>
+                </div>
+                <div data-monitor-editor-runtime-order="angular-after-advanced-before-labels" />
+                <HzField
+                  label="Collector"
+                  data-hz-ui-lab-monitor-editor-collector-selection="angular-collectors-selection-tags"
+                  data-monitor-editor-collector-selection="angular-collectors-selection-tags"
+                >
+                  <HzSelect
+                    value="collector-a"
+                    aria-label="Collector"
+                    options={[
+                      { value: '', label: 'Default collector - Public' },
+                      { value: 'collector-a', label: 'collector-a · 10.0.0.12 · Online · Private' }
+                    ]}
+                    data-monitor-editor-collector-selection-owner="hertzbeat-ui-select"
+                    optionDataAttributes={option => ({
+                      'data-monitor-editor-collector-option': option.value || 'system-default',
+                      'data-monitor-editor-collector-option-contract': 'angular-status-ip-mode-tags'
+                    })}
+                    onChange={() => setContextMessage('Monitor collector changed')}
+                  />
+                  <div
+                    className="mt-2 flex min-h-6 flex-wrap items-center gap-1.5"
+                    data-monitor-editor-collector-tags="angular-status-ip-mode-tags"
+                  >
+                    <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="status">Online</span>
+                    <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="name">collector-a</span>
+                    <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="ip">10.0.0.12</span>
+                    <span className="border border-[#263552] bg-[#10203a] px-2 py-0.5 text-[10px] font-semibold text-[#d8e4ff]" data-monitor-editor-collector-tag="mode">Private</span>
+                  </div>
+                </HzField>
+                <HzField
+                  as="div"
+                  label="Interval"
+                  data-hz-ui-lab-monitor-editor-interval-stepper="angular-min-step-max-by-app"
+                  data-monitor-interval-stepper-contract="angular-min-step-max-by-app"
+                >
+                  <HzNumberStepper
+                    name="intervals"
+                    min={10}
+                    max={604800}
+                    step={10}
+                    value="60"
+                    data-monitor-interval-stepper="hertzbeat-ui-number-stepper"
+                    data-monitor-interval-stepper-min="10"
+                    data-monitor-interval-stepper-max="604800"
+                    data-monitor-interval-stepper-step="10"
+                    data-monitor-editor-number-stepper-owner="hertzbeat-ui-number-stepper"
+                    onValueChange={() => setContextMessage('Monitor interval changed')}
+                  />
+                  <span
+                    className="mt-1 inline-flex text-[10px] font-semibold text-[#7d8798]"
+                    data-monitor-interval-stepper-unit="common.time.unit.second"
+                  >
+                    Second
+                  </span>
+                </HzField>
+                <HzField
+                  label="Cron expression"
+                  data-hz-ui-lab-monitor-editor-cron-required="angular-required-before-detect-save"
+                >
+                  <HzInput
+                    value="*/5 * * * *"
+                    readOnly
+                    required
+                    data-monitor-editor-input="cron-expression"
+                    data-monitor-editor-cron-required="angular-required-before-detect-save"
+                  />
+                </HzField>
+                <HzField
+                  as="div"
+                  label="Description"
+                  span="wide"
+                  data-hz-ui-lab-monitor-textarea="shared"
+                  data-hz-ui-lab-monitor-description-textarea-limit="angular-textarea-limit-100"
+                >
+                  <HzTextarea
+                    height="tall"
+                    value="Primary MySQL endpoint for collector-a."
+                    maxCharacterCount={100}
+                    readOnly
+                    data-monitor-description-textarea-limit="angular-textarea-limit-100"
+                  />
+                </HzField>
+                <HzField
+                  as="div"
+                  label="Incident message"
+                  span="wide"
+                  data-hz-ui-lab-status-incident-message-input="angular-textarea"
+                  data-status-incident-message-input-owner="hertzbeat-ui-textarea"
+                  data-status-incident-message-placeholder-contract="angular-state-placeholder"
+                >
+                  <HzTextarea
+                    height="default"
+                    rows={2}
+                    placeholder="We are monitoring this event and observing the processing effect"
+                    value="Monitoring checkout rollback."
+                    readOnly
+                    data-status-incident-message-placeholder="status.incident.message.tip.2"
+                  />
+                </HzField>
+                <HzField
+                  as="div"
+                  label="Incident components"
+                  span="wide"
+                  data-hz-ui-lab-status-incident-component-picker="angular-checkbox-group"
+                  data-status-incident-component-picker-owner="hertzbeat-ui-checkbox"
+                >
+                  <div
+                    className="flex min-h-8 flex-wrap items-center gap-x-4 gap-y-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 py-1.5"
+                    data-status-incident-component-picker="angular-checkbox-group"
+                  >
+                    <HzCheckbox defaultChecked label="checkout-api" data-status-incident-component-option="42" />
+                    <HzCheckbox label="payment-gateway" data-status-incident-component-option="43" />
+                  </div>
+                </HzField>
+                <HzField
+                  as="div"
+                  label="Incident state"
+                  span="wide"
+                  data-hz-ui-lab-status-incident-state-control="angular-radio-buttons"
+                  data-status-incident-state-owner="hertzbeat-ui-radio-button-group"
+                >
+                  <HzRadioButtonGroup
+                    name="ui-lab-status-incident-state"
+                    value="2"
+                    data-status-incident-state-control="angular-radio-buttons"
... 37548 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 20/50: feat(web-next): add release runtime readiness gates

Posted by zh...@apache.org.
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 bf2a36868b279781824338dbf4d8d5ac72a06181
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 02:14:08 2026 +0800

    feat(web-next): add release runtime readiness gates
---
 .github/workflows/frontend-build-test.yml          |   7 +-
 .../docker-compose.yaml                            |   5 +-
 .../nginx/default.conf                             |  20 +++
 web-next/Dockerfile                                |  20 ++-
 web-next/app/api/[...path]/route.ts                |  19 +++
 web-next/next.config.mjs                           |  19 +--
 web-next/package.json                              |   6 +-
 web-next/scripts/release-budget.mjs                | 100 +++++++++++
 web-next/scripts/release-budget.test.ts            |  60 +++++++
 web-next/scripts/release-checklist.mjs             | 111 ++++++++++++
 web-next/scripts/release-checklist.test.ts         |  64 +++++++
 web-next/scripts/release-compose.mjs               |  85 ++++++++++
 web-next/scripts/release-compose.test.ts           |  59 +++++++
 .../scripts/release-readiness-contract.test.ts     | 188 +++++++++++++++++++++
 web-next/scripts/session-security-contract.test.ts |  68 ++++++++
 15 files changed, 809 insertions(+), 22 deletions(-)

diff --git a/.github/workflows/frontend-build-test.yml b/.github/workflows/frontend-build-test.yml
index 8daadcec93..55b57c95de 100644
--- a/.github/workflows/frontend-build-test.yml
+++ b/.github/workflows/frontend-build-test.yml
@@ -22,14 +22,14 @@ name: Frontend CI
 
 on:
   push:
-    branches: [ master, dev, action* ]
+    branches: [ master, dev, 'action*', 'codex/**', 'feature/**', 'release/**' ]
     paths:
       - 'web-app/**'
       - 'web-next/**'
       - 'web-app/src/assets/i18n/**'
       - '.github/workflows/frontend-build-test.yml'
   pull_request:
-    branches: [ master, dev ]
+    branches: [ master, dev, 'codex/**', 'feature/**', 'release/**' ]
     paths:
       - 'web-app/**'
       - 'web-next/**'
@@ -107,3 +107,6 @@ jobs:
     - name: verify:full (blocking)
       working-directory: web-next
       run: npm run verify:full
+    - name: parity:smoke:baseline (blocking)
+      working-directory: web-next
+      run: npm run parity:smoke:baseline
diff --git a/script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/docker-compose.yaml b/script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/docker-compose.yaml
index 5882f9c52e..77adc90084 100644
--- a/script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/docker-compose.yaml
+++ b/script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/docker-compose.yaml
@@ -63,7 +63,7 @@ services:
       - hertzbeat
 
   hertzbeat:
-    image: apache/hertzbeat:1.8.0
+    image: "${HERTZBEAT_SERVER_IMAGE:-apache/hertzbeat}:${HERTZBEAT_RELEASE_VERSION:-1.8.0}"
     container_name: compose-hertzbeat
     hostname: hertzbeat
     restart: always
@@ -88,9 +88,12 @@ services:
       - hertzbeat
 
   web-next:
+    image: "${HERTZBEAT_WEB_NEXT_IMAGE:-apache/hertzbeat-web-next}:${HERTZBEAT_RELEASE_VERSION:-1.8.0}"
     build:
       context: ../../..
       dockerfile: web-next/Dockerfile
+      args:
+        HERTZBEAT_RELEASE_VERSION: ${HERTZBEAT_RELEASE_VERSION:-1.8.0}
     container_name: compose-hertzbeat-web-next
     hostname: web-next
     restart: always
diff --git a/script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/nginx/default.conf b/script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/nginx/default.conf
index cb4f3ed834..a506ba6773 100644
--- a/script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/nginx/default.conf
+++ b/script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/nginx/default.conf
@@ -12,6 +12,14 @@ server {
     proxy_pass http://web-next:4200;
   }
 
+  location ^~ /overview {
+    proxy_pass http://web-next:4200;
+  }
+
+  location ^~ /entities {
+    proxy_pass http://web-next:4200;
+  }
+
   location ^~ /trace/ {
     proxy_pass http://web-next:4200;
   }
@@ -24,6 +32,18 @@ server {
     proxy_pass http://web-next:4200;
   }
 
+  location ^~ /alert {
+    proxy_pass http://web-next:4200;
+  }
+
+  location ^~ /topology {
+    proxy_pass http://web-next:4200;
+  }
+
+  location ^~ /setting {
+    proxy_pass http://web-next:4200;
+  }
+
   location ^~ /ingestion/otlp {
     proxy_pass http://web-next:4200;
   }
diff --git a/web-next/Dockerfile b/web-next/Dockerfile
index af87301a8f..997b258830 100644
--- a/web-next/Dockerfile
+++ b/web-next/Dockerfile
@@ -1,21 +1,31 @@
-FROM node:20-alpine AS deps
+FROM node:22-alpine AS deps
 WORKDIR /app
 COPY web-next/package.json web-next/package-lock.json ./
 RUN npm ci
 
-FROM node:20-alpine AS builder
+FROM node:22-alpine AS builder
 WORKDIR /app
 ENV NEXT_TELEMETRY_DISABLED=1
+ENV NEXT_OUTPUT_FILE_TRACING_ROOT=/app
 COPY --from=deps /app/node_modules ./node_modules
 COPY web-next ./
 RUN npm run build
 
-FROM node:20-alpine AS runner
+FROM node:22-alpine AS runner
 WORKDIR /app
+ARG HERTZBEAT_RELEASE_VERSION=dev
 ENV NODE_ENV=production
 ENV NEXT_TELEMETRY_DISABLED=1
 ENV PORT=4200
 ENV HOSTNAME=0.0.0.0
-COPY --from=builder /app ./
+LABEL org.opencontainers.image.title="Apache HertzBeat Web Next"
+LABEL org.opencontainers.image.version="${HERTZBEAT_RELEASE_VERSION}"
+RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
+COPY --from=builder /app/.next/standalone ./
+COPY --from=builder /app/.next/static ./.next/static
+COPY --from=builder /app/public ./public
+RUN chown -R nextjs:nodejs /app
+USER nextjs
 EXPOSE 4200
-CMD ["npx", "next", "start", "-p", "4200", "-H", "0.0.0.0"]
+HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD wget -q -O - "http://127.0.0.1:${PORT}/overview" > /dev/null || exit 1
+CMD ["node", "server.js"]
diff --git a/web-next/app/api/[...path]/route.ts b/web-next/app/api/[...path]/route.ts
new file mode 100644
index 0000000000..50c09daa1e
--- /dev/null
+++ b/web-next/app/api/[...path]/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest } from 'next/server';
+import { proxyBackendApiRequest } from '@/lib/session-bff';
+
+export const dynamic = 'force-dynamic';
+
+type RouteContext = {
+  params: Promise<{ path?: string[] }>;
+};
+
+async function proxy(request: NextRequest, context: RouteContext) {
+  const params = await context.params;
+  return proxyBackendApiRequest(request, `/${params.path?.join('/') || ''}`);
+}
+
+export const GET = proxy;
+export const POST = proxy;
+export const PUT = proxy;
+export const PATCH = proxy;
+export const DELETE = proxy;
diff --git a/web-next/next.config.mjs b/web-next/next.config.mjs
index 92a0fd4d93..ed10597113 100644
--- a/web-next/next.config.mjs
+++ b/web-next/next.config.mjs
@@ -1,25 +1,18 @@
 import path from 'node:path';
 
+const outputFileTracingRoot = process.env.NEXT_OUTPUT_FILE_TRACING_ROOT
+  ? path.resolve(process.env.NEXT_OUTPUT_FILE_TRACING_ROOT)
+  : path.join(process.cwd(), '..');
+
 /** @type {import('next').NextConfig} */
 const nextConfig = {
   reactStrictMode: true,
   poweredByHeader: false,
   distDir: process.env.NEXT_DIST_DIR || '.next',
-  outputFileTracingRoot: path.join(process.cwd(), '..'),
+  output: 'standalone',
+  outputFileTracingRoot,
   eslint: {
     ignoreDuringBuilds: false
-  },
-  async rewrites() {
-    const backendOrigin = process.env.BACKEND_ORIGIN;
-    if (!backendOrigin) {
-      return [];
-    }
-    return [
-      {
-        source: '/api/:path*',
-        destination: `${backendOrigin}/api/:path*`
-      }
-    ];
   }
 };
 
diff --git a/web-next/package.json b/web-next/package.json
index c8f2969d5e..94a7c4a4ae 100644
--- a/web-next/package.json
+++ b/web-next/package.json
@@ -10,9 +10,13 @@
     "test": "vitest run",
     "parity:runtime": "node ./scripts/parity/runtime.mjs",
     "parity:smoke": "node ./scripts/parity/harness.mjs",
+    "parity:smoke:baseline": "node ./scripts/parity/harness.mjs --milestone 1",
     "route:matrix": "node ./scripts/route-matrix.mjs",
+    "release:budget": "node ./scripts/release-budget.mjs",
+    "release:compose": "node ./scripts/release-compose.mjs",
+    "release:checklist": "node ./scripts/release-checklist.mjs",
     "verify": "npm run lint && npm run test && npm run i18n:report && npm run route:matrix",
-    "verify:full": "npm run build && npm run verify",
+    "verify:full": "npm run build && npm run release:budget && npm run release:compose && npm run release:checklist && npm run verify",
     "i18n:report": "node ./scripts/i18n-report.mjs"
   },
   "dependencies": {
diff --git a/web-next/scripts/release-budget.mjs b/web-next/scripts/release-budget.mjs
new file mode 100644
index 0000000000..8107eea86a
--- /dev/null
+++ b/web-next/scripts/release-budget.mjs
@@ -0,0 +1,100 @@
+import { existsSync, readdirSync, statSync } from 'node:fs';
+import path from 'node:path';
+import process from 'node:process';
+
+export const DEFAULT_RELEASE_BUDGET_BYTES = 8 * 1024 * 1024;
+
+export function parseBudgetBytes(value = process.env.HERTZBEAT_WEB_NEXT_BUNDLE_BUDGET_BYTES) {
+  if (value === undefined || value === null || String(value).trim() === '') {
+    return DEFAULT_RELEASE_BUDGET_BYTES;
+  }
+
+  const parsed = Number.parseInt(String(value), 10);
+  if (!Number.isFinite(parsed) || parsed <= 0) {
+    throw new Error(`Invalid HERTZBEAT_WEB_NEXT_BUNDLE_BUDGET_BYTES value: ${value}`);
+  }
+  return parsed;
+}
+
+export function formatBudgetBytes(bytes) {
+  if (bytes >= 1024 * 1024) {
+    return `${(bytes / 1024 / 1024).toFixed(2)} MiB`;
+  }
+  if (bytes >= 1024) {
+    return `${(bytes / 1024).toFixed(2)} KiB`;
+  }
+  return `${bytes} B`;
+}
+
+function collectFiles(rootDir) {
+  if (!existsSync(rootDir)) {
+    return [];
+  }
+
+  return readdirSync(rootDir, { withFileTypes: true }).flatMap(entry => {
+    const entryPath = path.join(rootDir, entry.name);
+    if (entry.isDirectory()) {
+      return collectFiles(entryPath);
+    }
+    if (entry.isFile()) {
+      return [entryPath];
+    }
+    return [];
+  });
+}
+
+export function collectReleaseJsAssets(distDir = process.env.NEXT_DIST_DIR || '.next') {
+  const staticDir = path.resolve(process.cwd(), distDir, 'static');
+
+  return collectFiles(staticDir)
+    .filter(filePath => filePath.endsWith('.js'))
+    .map(filePath => ({
+      path: path.relative(path.resolve(process.cwd(), distDir), filePath).replaceAll(path.sep, '/'),
+      bytes: statSync(filePath).size
+    }))
+    .sort((left, right) => left.path.localeCompare(right.path));
+}
+
+export function evaluateReleaseBudget(entries, maxBytes = DEFAULT_RELEASE_BUDGET_BYTES) {
+  const normalizedEntries = entries
+    .map(entry => ({
+      path: String(entry.path || ''),
+      bytes: Number(entry.bytes || 0)
+    }))
+    .filter(entry => entry.path && Number.isFinite(entry.bytes) && entry.bytes >= 0);
+  const totalBytes = normalizedEntries.reduce((total, entry) => total + entry.bytes, 0);
+  const largestEntries = [...normalizedEntries].sort((left, right) => right.bytes - left.bytes).slice(0, 5);
+
+  return {
+    totalBytes,
+    maxBytes,
+    passed: totalBytes <= maxBytes,
+    largestEntries
+  };
+}
+
+export function assertReleaseBudget({ distDir = process.env.NEXT_DIST_DIR || '.next', maxBytes = parseBudgetBytes() } = {}) {
+  const entries = collectReleaseJsAssets(distDir);
+  if (entries.length === 0) {
+    throw new Error(`No Next.js release JavaScript assets found under ${distDir}/static. Run npm run build first.`);
+  }
+
+  const result = evaluateReleaseBudget(entries, maxBytes);
+  if (!result.passed) {
+    const largest = result.largestEntries
+      .map(entry => `${entry.path}=${formatBudgetBytes(entry.bytes)}`)
+      .join(', ');
+    throw new Error(
+      `web-next release JavaScript assets exceed budget: ${formatBudgetBytes(result.totalBytes)} > `
+        + `${formatBudgetBytes(result.maxBytes)}. Largest chunks: ${largest}`,
+    );
+  }
+  return result;
+}
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+  const result = assertReleaseBudget();
+  console.log(
+    `web-next release JavaScript assets: ${formatBudgetBytes(result.totalBytes)} / ${formatBudgetBytes(result.maxBytes)}`,
+  );
+}
diff --git a/web-next/scripts/release-budget.test.ts b/web-next/scripts/release-budget.test.ts
new file mode 100644
index 0000000000..5b24cca51a
--- /dev/null
+++ b/web-next/scripts/release-budget.test.ts
@@ -0,0 +1,60 @@
+import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+
+import { describe, expect, it } from 'vitest';
+
+import {
+  collectReleaseJsAssets,
+  evaluateReleaseBudget,
+  formatBudgetBytes,
+  parseBudgetBytes
+} from './release-budget.mjs';
+
+describe('release budget gate', () => {
+  it('collects built Next.js static JavaScript assets without counting non-js files', () => {
+    const currentWorkingDirectory = process.cwd();
+    const tempRoot = mkdtempSync(path.join(tmpdir(), 'hertzbeat-release-budget-'));
+    try {
+      const chunksDir = path.join(tempRoot, '.next/static/chunks/app');
+      mkdirSync(chunksDir, { recursive: true });
+      writeFileSync(path.join(chunksDir, 'overview.js'), 'x'.repeat(13));
+      writeFileSync(path.join(chunksDir, 'overview.css'), 'x'.repeat(99));
+
+      process.chdir(tempRoot);
+
+      expect(collectReleaseJsAssets()).toEqual([
+        {
+          path: 'static/chunks/app/overview.js',
+          bytes: 13
+        }
+      ]);
+    } finally {
+      process.chdir(currentWorkingDirectory);
+      rmSync(tempRoot, { recursive: true, force: true });
+    }
+  });
+
+  it('evaluates total release chunk size against a configurable budget', () => {
+    const result = evaluateReleaseBudget(
+      [
+        { path: 'static/chunks/a.js', bytes: 6 },
+        { path: 'static/chunks/b.js', bytes: 5 }
+      ],
+      10,
+    );
+
+    expect(result).toMatchObject({
+      totalBytes: 11,
+      maxBytes: 10,
+      passed: false
+    });
+    expect(result.largestEntries.map(entry => entry.path)).toEqual(['static/chunks/a.js', 'static/chunks/b.js']);
+  });
+
+  it('parses and formats release budget values for CI diagnostics', () => {
+    expect(parseBudgetBytes('2048')).toBe(2048);
+    expect(formatBudgetBytes(2048)).toBe('2.00 KiB');
+    expect(() => parseBudgetBytes('0')).toThrow('Invalid HERTZBEAT_WEB_NEXT_BUNDLE_BUDGET_BYTES');
+  });
+});
diff --git a/web-next/scripts/release-checklist.mjs b/web-next/scripts/release-checklist.mjs
new file mode 100644
index 0000000000..ed1c6a0e2d
--- /dev/null
+++ b/web-next/scripts/release-checklist.mjs
@@ -0,0 +1,111 @@
+import { readFileSync } from 'node:fs';
+import path from 'node:path';
+import process from 'node:process';
+import { fileURLToPath } from 'node:url';
+
+const scriptDir = path.dirname(fileURLToPath(import.meta.url));
+const webNextRoot = path.resolve(scriptDir, '..');
+const repoRoot = path.resolve(webNextRoot, '..');
+
+export const REQUIRED_WEB_NEXT_INGRESS_PREFIXES = [
+  '/_next/',
+  '/overview',
+  '/entities',
+  '/alert',
+  '/topology',
+  '/setting'
+];
+
+export const RELEASE_CHECKLIST_ITEMS = [
+  {
+    key: 'ci-blocking-gates',
+    label: 'Frontend CI keeps build, budget, compose, checklist, tests, and smoke blocking',
+    verify: files =>
+      files.packageJson.scripts?.['verify:full'] ===
+        'npm run build && npm run release:budget && npm run release:compose && npm run release:checklist && npm run verify' &&
+      files.workflow.includes('npm run verify:full') &&
+      files.workflow.includes('npm run parity:smoke:baseline')
+  },
+  {
+    key: 'standalone-release-image',
+    label: 'web-next release image is standalone, non-root, healthchecked, and version labeled',
+    verify: files =>
+      files.nextConfig.includes("output: 'standalone'") &&
+      files.dockerfile.includes('FROM node:22-alpine') &&
+      files.dockerfile.includes('USER nextjs') &&
+      files.dockerfile.includes('HEALTHCHECK') &&
+      files.dockerfile.includes('org.opencontainers.image.version="${HERTZBEAT_RELEASE_VERSION}"')
+  },
+  {
+    key: 'compose-version-convergence',
+    label: 'Compose release stack shares one overrideable server/web-next release version',
+    verify: files =>
+      files.compose.includes('${HERTZBEAT_RELEASE_VERSION:-1.8.0}') &&
+      files.compose.includes('${HERTZBEAT_SERVER_IMAGE:-apache/hertzbeat}') &&
+      files.compose.includes('${HERTZBEAT_WEB_NEXT_IMAGE:-apache/hertzbeat-web-next}')
+  },
+  {
+    key: 'promotion-rollback-config',
+    label: 'Promotion and rollback tags are validated through docker compose config',
+    verify: files =>
+      files.packageJson.scripts?.['release:compose'] === 'node ./scripts/release-compose.mjs' &&
+      files.releaseCompose.includes('verifyReleaseComposeConfig') &&
+      files.releaseCompose.includes('HERTZBEAT_ROLLBACK_VERSION')
+  },
+  {
+    key: 'bundle-budget',
+    label: 'Production Next build has a JavaScript bundle budget gate',
+    verify: files =>
+      files.packageJson.scripts?.['release:budget'] === 'node ./scripts/release-budget.mjs' &&
+      files.releaseBudget.includes('DEFAULT_RELEASE_BUDGET_BYTES') &&
+      files.releaseBudget.includes('evaluateReleaseBudget')
+  },
+  {
+    key: 'release-ingress-route-ownership',
+    label: 'Gateway routes operator route families to web-next and APIs/fallback to Spring',
+    verify: files =>
+      REQUIRED_WEB_NEXT_INGRESS_PREFIXES.every(prefix => files.gateway.includes(`location ^~ ${prefix}`)) &&
+      files.gateway.includes('proxy_pass http://web-next:4200;') &&
+      files.gateway.includes('location ^~ /api/') &&
+      files.gateway.includes('location /') &&
+      files.gateway.includes('proxy_pass http://hertzbeat:1157;')
+  }
+];
+
+export function readReleaseChecklistFiles(rootDir = repoRoot) {
+  const readRepoFile = relativePath => readFileSync(path.join(rootDir, relativePath), 'utf8');
+  const packageText = readRepoFile('web-next/package.json');
+
+  return {
+    packageJson: JSON.parse(packageText),
+    workflow: readRepoFile('.github/workflows/frontend-build-test.yml'),
+    dockerfile: readRepoFile('web-next/Dockerfile'),
+    nextConfig: readRepoFile('web-next/next.config.mjs'),
+    compose: readRepoFile('script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/docker-compose.yaml'),
+    gateway: readRepoFile('script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/nginx/default.conf'),
+    releaseBudget: readRepoFile('web-next/scripts/release-budget.mjs'),
+    releaseCompose: readRepoFile('web-next/scripts/release-compose.mjs')
+  };
+}
+
+export function evaluateReleaseChecklist(files = readReleaseChecklistFiles()) {
+  return RELEASE_CHECKLIST_ITEMS.map(item => ({
+    key: item.key,
+    label: item.label,
+    passed: Boolean(item.verify(files))
+  }));
+}
+
+export function verifyReleaseChecklist(files = readReleaseChecklistFiles()) {
+  const results = evaluateReleaseChecklist(files);
+  const failures = results.filter(result => !result.passed);
+  if (failures.length > 0) {
+    throw new Error(`Release checklist failed: ${failures.map(result => result.key).join(', ')}`);
+  }
+  return results;
+}
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+  const results = verifyReleaseChecklist();
+  console.log(`release checklist ok: ${results.map(result => result.key).join(', ')}`);
+}
diff --git a/web-next/scripts/release-checklist.test.ts b/web-next/scripts/release-checklist.test.ts
new file mode 100644
index 0000000000..19264688ec
--- /dev/null
+++ b/web-next/scripts/release-checklist.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+  REQUIRED_WEB_NEXT_INGRESS_PREFIXES,
+  evaluateReleaseChecklist,
+  verifyReleaseChecklist
+} from './release-checklist.mjs';
+
+function releaseFiles(overrides = {}) {
+  const gatewayRoutes = REQUIRED_WEB_NEXT_INGRESS_PREFIXES
+    .map(prefix => `location ^~ ${prefix} {\n  proxy_pass http://web-next:4200;\n}`)
+    .join('\n');
+
+  return {
+    packageJson: {
+      scripts: {
+        'verify:full':
+          'npm run build && npm run release:budget && npm run release:compose && npm run release:checklist && npm run verify',
+        'release:budget': 'node ./scripts/release-budget.mjs',
+        'release:compose': 'node ./scripts/release-compose.mjs'
+      }
+    },
+    workflow: 'run: npm run verify:full\nrun: npm run parity:smoke:baseline',
+    dockerfile:
+      'FROM node:22-alpine\nUSER nextjs\nHEALTHCHECK CMD test\nLABEL org.opencontainers.image.version="${HERTZBEAT_RELEASE_VERSION}"',
+    nextConfig: "output: 'standalone'",
+    compose:
+      '${HERTZBEAT_RELEASE_VERSION:-1.8.0}\n${HERTZBEAT_SERVER_IMAGE:-apache/hertzbeat}\n${HERTZBEAT_WEB_NEXT_IMAGE:-apache/hertzbeat-web-next}',
+    gateway: `${gatewayRoutes}\nlocation ^~ /api/ {\n proxy_pass http://hertzbeat:1157;\n}\nlocation / {\n proxy_pass http://hertzbeat:1157;\n}`,
+    releaseBudget: 'DEFAULT_RELEASE_BUDGET_BYTES\nevaluateReleaseBudget',
+    releaseCompose: 'verifyReleaseComposeConfig\nHERTZBEAT_ROLLBACK_VERSION',
+    ...overrides
+  };
+}
+
+describe('release checklist gate', () => {
+  it('passes only when every release-readiness item is present', () => {
+    const results = verifyReleaseChecklist(releaseFiles());
+
+    expect(results.every(result => result.passed)).toBe(true);
+    expect(results.map(result => result.key)).toEqual([
+      'ci-blocking-gates',
+      'standalone-release-image',
+      'compose-version-convergence',
+      'promotion-rollback-config',
+      'bundle-budget',
+      'release-ingress-route-ownership'
+    ]);
+  });
+
+  it('fails with the missing checklist key when release ingress drops a web-next route family', () => {
+    const files = releaseFiles({
+      gateway:
+        'location ^~ /_next/ { proxy_pass http://web-next:4200; }\n'
+        + 'location ^~ /api/ { proxy_pass http://hertzbeat:1157; }\n'
+        + 'location / { proxy_pass http://hertzbeat:1157; }'
+    });
+
+    expect(evaluateReleaseChecklist(files).find(result => result.key === 'release-ingress-route-ownership')?.passed).toBe(
+      false,
+    );
+    expect(() => verifyReleaseChecklist(files)).toThrow('release-ingress-route-ownership');
+  });
+});
diff --git a/web-next/scripts/release-compose.mjs b/web-next/scripts/release-compose.mjs
new file mode 100644
index 0000000000..910f57f9aa
--- /dev/null
+++ b/web-next/scripts/release-compose.mjs
@@ -0,0 +1,85 @@
+import { spawnSync } from 'node:child_process';
+import { readFileSync } from 'node:fs';
+import path from 'node:path';
+import process from 'node:process';
+import { fileURLToPath } from 'node:url';
+
+const scriptDir = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(scriptDir, '../..');
+export const NEXT_OBSERVABILITY_COMPOSE =
+  'script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/docker-compose.yaml';
+
+export function readRootReleaseVersion(rootPom = readFileSync(path.join(repoRoot, 'pom.xml'), 'utf8')) {
+  const version = rootPom.match(/<hzb\.version>\s*([^<]+?)\s*<\/hzb\.version>/)?.[1]?.trim();
+  if (!version) {
+    throw new Error('Unable to resolve <hzb.version> from root pom.xml');
+  }
+  return version;
+}
+
+export function rollbackProbeVersion(releaseVersion) {
+  return process.env.HERTZBEAT_ROLLBACK_VERSION || `${releaseVersion}-rollback-check`;
+}
+
+export function composeConfigEnv(releaseVersion, extraEnv = process.env) {
+  return {
+    ...extraEnv,
+    HERTZBEAT_RELEASE_VERSION: releaseVersion,
+    HERTZBEAT_SERVER_IMAGE: extraEnv.HERTZBEAT_SERVER_IMAGE || 'apache/hertzbeat',
+    HERTZBEAT_WEB_NEXT_IMAGE: extraEnv.HERTZBEAT_WEB_NEXT_IMAGE || 'apache/hertzbeat-web-next'
+  };
+}
+
+export function assertResolvedReleaseImages(configText, releaseVersion) {
+  const expectedServerImage = `image: apache/hertzbeat:${releaseVersion}`;
+  const expectedWebNextImage = `image: apache/hertzbeat-web-next:${releaseVersion}`;
+  const expectedBuildArg = `HERTZBEAT_RELEASE_VERSION: ${releaseVersion}`;
+
+  for (const expected of [expectedServerImage, expectedWebNextImage, expectedBuildArg]) {
+    if (!configText.includes(expected)) {
+      throw new Error(`docker compose config did not resolve ${expected}`);
+    }
+  }
+}
+
+export function runDockerComposeConfig(releaseVersion) {
+  const composePath = path.join(repoRoot, NEXT_OBSERVABILITY_COMPOSE);
+  const command = ['compose', '-f', composePath, 'config'];
+  const result = spawnSync('docker', command, {
+    cwd: repoRoot,
+    env: composeConfigEnv(releaseVersion),
+    encoding: 'utf8'
+  });
+
+  if (result.error || result.status !== 0) {
+    throw new Error(
+      `docker compose config failed for release ${releaseVersion}: ${
+        result.error?.message || result.stderr || `exit ${result.status}`
+      }`,
+    );
+  }
+
+  return result.stdout;
+}
+
+export function verifyReleaseComposeConfig({
+  releaseVersion = process.env.HERTZBEAT_RELEASE_VERSION || readRootReleaseVersion(),
+  rollbackVersion = rollbackProbeVersion(releaseVersion),
+  configRunner = runDockerComposeConfig
+} = {}) {
+  const promotionConfig = configRunner(releaseVersion);
+  assertResolvedReleaseImages(promotionConfig, releaseVersion);
+
+  const rollbackConfig = configRunner(rollbackVersion);
+  assertResolvedReleaseImages(rollbackConfig, rollbackVersion);
+
+  return {
+    releaseVersion,
+    rollbackVersion
+  };
+}
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+  const result = verifyReleaseComposeConfig();
+  console.log(`docker compose promotion/rollback config ok: ${result.releaseVersion} -> ${result.rollbackVersion}`);
+}
diff --git a/web-next/scripts/release-compose.test.ts b/web-next/scripts/release-compose.test.ts
new file mode 100644
index 0000000000..3ce1bee5ec
--- /dev/null
+++ b/web-next/scripts/release-compose.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+  assertResolvedReleaseImages,
+  composeConfigEnv,
+  readRootReleaseVersion,
+  rollbackProbeVersion,
+  verifyReleaseComposeConfig
+} from './release-compose.mjs';
+
+describe('release compose gate', () => {
+  it('resolves the root release version from hzb.version', () => {
+    expect(readRootReleaseVersion('<project><properties><hzb.version>1.8.0</hzb.version></properties></project>')).toBe(
+      '1.8.0',
+    );
+  });
+
+  it('requires server image, web-next image, and web-next build arg to share the release version', () => {
+    expect(() =>
+      assertResolvedReleaseImages(
+        [
+          'image: apache/hertzbeat:1.8.0',
+          'image: apache/hertzbeat-web-next:1.8.0',
+          'HERTZBEAT_RELEASE_VERSION: 1.8.0'
+        ].join('\n'),
+        '1.8.0',
+      ),
+    ).not.toThrow();
+
+    expect(() =>
+      assertResolvedReleaseImages('image: apache/hertzbeat:1.8.0\nimage: apache/hertzbeat-web-next:old', '1.8.0'),
+    ).toThrow('docker compose config did not resolve');
+  });
+
+  it('checks promotion and rollback versions through the compose config runner', () => {
+    const checkedVersions: string[] = [];
+
+    const result = verifyReleaseComposeConfig({
+      releaseVersion: '1.8.0',
+      rollbackVersion: '1.7.9',
+      configRunner: version => {
+        checkedVersions.push(version);
+        return [
+          `image: apache/hertzbeat:${version}`,
+          `image: apache/hertzbeat-web-next:${version}`,
+          `HERTZBEAT_RELEASE_VERSION: ${version}`
+        ].join('\n');
+      }
+    });
+
+    expect(result).toEqual({ releaseVersion: '1.8.0', rollbackVersion: '1.7.9' });
+    expect(checkedVersions).toEqual(['1.8.0', '1.7.9']);
+  });
+
+  it('builds a deterministic rollback probe version when no rollback override is supplied', () => {
+    expect(rollbackProbeVersion('1.8.0')).toBe('1.8.0-rollback-check');
+    expect(composeConfigEnv('2.0.0', {}).HERTZBEAT_RELEASE_VERSION).toBe('2.0.0');
+  });
+});
diff --git a/web-next/scripts/release-readiness-contract.test.ts b/web-next/scripts/release-readiness-contract.test.ts
new file mode 100644
index 0000000000..19828c9080
--- /dev/null
+++ b/web-next/scripts/release-readiness-contract.test.ts
@@ -0,0 +1,188 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+import { describe, expect, it } from 'vitest';
+
+const webNextRoot = resolve(__dirname, '..');
+const repoRoot = resolve(webNextRoot, '..');
+
+function readWorkflow(): string {
+  return readFileSync(resolve(repoRoot, '.github/workflows/frontend-build-test.yml'), 'utf8');
+}
+
+function readDockerfile(): string {
+  return readFileSync(resolve(webNextRoot, 'Dockerfile'), 'utf8');
+}
+
+function readNextObservabilityCompose(): string {
+  return readFileSync(
+    resolve(repoRoot, 'script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/docker-compose.yaml'),
+    'utf8',
+  );
+}
+
+function readNextConfig(): string {
+  return readFileSync(resolve(webNextRoot, 'next.config.mjs'), 'utf8');
+}
+
+function readReleaseBudgetScript(): string {
+  return readFileSync(resolve(webNextRoot, 'scripts/release-budget.mjs'), 'utf8');
+}
+
+function readReleaseComposeScript(): string {
+  return readFileSync(resolve(webNextRoot, 'scripts/release-compose.mjs'), 'utf8');
+}
+
+function readReleaseChecklistScript(): string {
+  return readFileSync(resolve(webNextRoot, 'scripts/release-checklist.mjs'), 'utf8');
+}
+
+function readNextObservabilityGateway(): string {
+  return readFileSync(
+    resolve(repoRoot, 'script/docker-compose/hertzbeat-postgresql-victoria-metrics-next-observability/nginx/default.conf'),
+    'utf8',
+  );
+}
+
+function readRootPom(): string {
+  return readFileSync(resolve(repoRoot, 'pom.xml'), 'utf8');
+}
+
+function readPackageJson(): { scripts?: Record<string, string> } {
+  return JSON.parse(readFileSync(resolve(webNextRoot, 'package.json'), 'utf8'));
+}
+
+function extractBranches(workflow: string, sectionName: 'push' | 'pull_request'): string[] {
+  const nextSection = sectionName === 'push' ? 'pull_request' : 'jobs';
+  const sectionPattern = new RegExp(`\\n\\s+${sectionName}:([\\s\\S]*?)\\n\\s*${nextSection}:`);
+  const section = workflow.match(sectionPattern)?.[1] ?? '';
+  const branches = section.match(/branches:\s*\[([^\]]+)\]/)?.[1] ?? '';
+
+  return branches
+    .split(',')
+    .map((branch) => branch.trim().replace(/^['"]|['"]$/g, ''))
+    .filter(Boolean);
+}
+
+function extractWorkflowNodeMajors(workflow: string): string[] {
+  return [...workflow.matchAll(/node-version:\s*['"]?(\d+)['"]?/g)].map(match => match[1]);
+}
+
+function extractDockerNodeMajors(dockerfile: string): string[] {
+  return [...dockerfile.matchAll(/FROM\s+node:(\d+)-alpine/g)].map(match => match[1]);
+}
+
+function extractXmlTag(xml: string, tagName: string): string {
+  const tagPattern = new RegExp(`<${tagName}>\\s*([^<]+?)\\s*</${tagName}>`);
+  const value = xml.match(tagPattern)?.[1]?.trim();
+  if (!value) {
+    throw new Error(`Missing <${tagName}> in root pom.xml`);
+  }
+  return value;
+}
+
+describe('release-readiness validation baseline', () => {
+  it('keeps frontend CI active for release-readiness working branches', () => {
+    const workflow = readWorkflow();
+
+    expect(extractBranches(workflow, 'push')).toEqual(
+      expect.arrayContaining(['master', 'dev', 'codex/**', 'feature/**', 'release/**']),
+    );
+    expect(extractBranches(workflow, 'pull_request')).toEqual(
+      expect.arrayContaining(['master', 'dev', 'codex/**', 'feature/**', 'release/**']),
+    );
+  });
+
+  it('keeps milestone-one parity smoke as a blocking validation command', () => {
+    const workflow = readWorkflow();
+    const packageJson = readPackageJson();
+
+    expect(packageJson.scripts?.['parity:smoke:baseline']).toContain('--milestone 1');
+    expect(workflow).toContain('npm run parity:smoke:baseline');
+  });
+
+  it('runs a release bundle budget after the production build in full verification', () => {
+    const packageJson = readPackageJson();
+    const releaseBudgetScript = readReleaseBudgetScript();
+
+    expect(packageJson.scripts?.['release:budget']).toBe('node ./scripts/release-budget.mjs');
+    expect(packageJson.scripts?.['verify:full']).toContain('npm run build && npm run release:budget');
+    expect(releaseBudgetScript).toContain('DEFAULT_RELEASE_BUDGET_BYTES');
+    expect(releaseBudgetScript).toContain('evaluateReleaseBudget');
+  });
+
+  it('runs a compose promotion and rollback gate in full verification', () => {
+    const packageJson = readPackageJson();
+    const releaseComposeScript = readReleaseComposeScript();
+
+    expect(packageJson.scripts?.['release:compose']).toBe('node ./scripts/release-compose.mjs');
+    expect(packageJson.scripts?.['verify:full']).toContain('npm run release:budget && npm run release:compose');
+    expect(releaseComposeScript).toContain('verifyReleaseComposeConfig');
+    expect(releaseComposeScript).toContain('HERTZBEAT_ROLLBACK_VERSION');
+    expect(releaseComposeScript).toContain('docker compose');
+  });
+
+  it('runs the release checklist gate and keeps release ingress aligned with web-next route families', () => {
+    const packageJson = readPackageJson();
+    const releaseChecklistScript = readReleaseChecklistScript();
+    const gateway = readNextObservabilityGateway();
+
+    expect(packageJson.scripts?.['release:checklist']).toBe('node ./scripts/release-checklist.mjs');
+    expect(packageJson.scripts?.['verify:full']).toBe(
+      'npm run build && npm run release:budget && npm run release:compose && npm run release:checklist && npm run verify',
+    );
+    expect(releaseChecklistScript).toContain('RELEASE_CHECKLIST_ITEMS');
+    expect(releaseChecklistScript).toContain('verifyReleaseChecklist');
+
+    for (const routePrefix of ['/_next/', '/overview', '/entities', '/alert', '/topology', '/setting']) {
+      expect(gateway).toContain(`location ^~ ${routePrefix}`);
+    }
+    expect(gateway).toContain('proxy_pass http://web-next:4200;');
+    expect(gateway).toContain('location ^~ /api/');
+    expect(gateway).toContain('proxy_pass http://hertzbeat:1157;');
+  });
+
+  it('keeps the web-next release image aligned with CI Node runtime', () => {
+    const workflowNodeMajors = new Set(extractWorkflowNodeMajors(readWorkflow()));
+    const dockerNodeMajors = new Set(extractDockerNodeMajors(readDockerfile()));
+
+    expect(workflowNodeMajors).toContain('22');
+    expect(dockerNodeMajors).toEqual(new Set(['22']));
+  });
+
+  it('packages web-next as a standalone non-root healthchecked release image', () => {
+    const dockerfile = readDockerfile();
+    const nextConfig = readNextConfig();
+
+    expect(nextConfig).toMatch(/output:\s*['"]standalone['"]/);
+    expect(dockerfile).toContain('COPY --from=builder /app/.next/standalone ./');
+    expect(dockerfile).toContain('COPY --from=builder /app/.next/static ./.next/static');
+    expect(dockerfile).toContain('COPY --from=builder /app/public ./public');
+    expect(dockerfile).toContain('USER nextjs');
+    expect(dockerfile).toContain('HEALTHCHECK');
+    expect(dockerfile).toContain('CMD ["node", "server.js"]');
+    expect(dockerfile).not.toContain('npx');
+  });
+
+  it('uses one overrideable release version for the next observability compose stack', () => {
+    const releaseVersion = extractXmlTag(readRootPom(), 'hzb.version');
+    const compose = readNextObservabilityCompose();
+    const releaseVersionExpression = `\${HERTZBEAT_RELEASE_VERSION:-${releaseVersion}}`;
+
+    expect(compose).toContain(
+      `image: "\${HERTZBEAT_SERVER_IMAGE:-apache/hertzbeat}:${releaseVersionExpression}"`,
+    );
+    expect(compose).toContain(
+      `image: "\${HERTZBEAT_WEB_NEXT_IMAGE:-apache/hertzbeat-web-next}:${releaseVersionExpression}"`,
+    );
+    expect(compose).toContain(`HERTZBEAT_RELEASE_VERSION: ${releaseVersionExpression}`);
+    expect(compose).not.toMatch(/image:\s+apache\/hertzbeat:\d/);
+  });
+
+  it('labels the web-next release image with the compose release version', () => {
+    const dockerfile = readDockerfile();
+
+    expect(dockerfile).toContain('ARG HERTZBEAT_RELEASE_VERSION=dev');
+    expect(dockerfile).toContain('org.opencontainers.image.version="${HERTZBEAT_RELEASE_VERSION}"');
+  });
+});
diff --git a/web-next/scripts/session-security-contract.test.ts b/web-next/scripts/session-security-contract.test.ts
new file mode 100644
index 0000000000..5a712f77b9
--- /dev/null
+++ b/web-next/scripts/session-security-contract.test.ts
@@ -0,0 +1,68 @@
+import { existsSync, readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { describe, expect, it } from 'vitest';
+
+const WEB_NEXT_ROOT = resolve(__dirname, '..');
+
+function readWebNext(path: string) {
+  return readFileSync(resolve(WEB_NEXT_ROOT, path), 'utf8');
+}
+
+function existsWebNext(path: string) {
+  return existsSync(resolve(WEB_NEXT_ROOT, path));
+}
+
+describe('session security and token boundary contract', () => {
+  it('keeps ui session tokens behind same-origin BFF cookies instead of browser localStorage', () => {
+    const apiClientSource = readWebNext('lib/api-client.ts');
+    const loginFormSource = readWebNext('components/pages/login-form.tsx');
+    const authGateSource = readWebNext('components/shell/auth-gate.tsx');
+    const appFrameSource = readWebNext('components/shell/app-frame.tsx');
+    const monitorManageSource = readWebNext('app/monitors/monitor-manage-page.tsx');
+
+    expect(apiClientSource).toContain("credentials: 'same-origin'");
+    expect(apiClientSource).not.toContain("localStorage.getItem('Authorization')");
+    expect(apiClientSource).not.toContain("localStorage.getItem('refresh-token')");
+    expect(apiClientSource).not.toContain("Authorization: `Bearer ${token}`");
+
+    expect(loginFormSource).toContain('assertSessionLoginSuccess');
+    expect(loginFormSource).not.toContain('persistLoginTokens(window.localStorage');
+
+    expect(authGateSource).toContain("readClientSessionState()");
+    expect(authGateSource).not.toContain("localStorage.getItem('Authorization')");
+
+    expect(appFrameSource).toContain("clearClientSession()");
+    expect(appFrameSource).not.toContain("localStorage.removeItem('Authorization')");
+    expect(appFrameSource).not.toContain("localStorage.removeItem('refresh-token')");
+
+    expect(monitorManageSource).not.toContain("Authorization: `Bearer ${token}`");
+    expect(monitorManageSource).not.toContain('getAuthorizationToken');
+
+    [
+      'app/api/[...path]/route.ts',
+      'app/api/account/auth/form/route.ts',
+      'app/api/account/auth/refresh/route.ts',
+      'app/api/account/session/route.ts',
+      'lib/session-bff.ts',
+      'lib/session-client.ts'
+    ].forEach(path => {
+      expect(existsWebNext(path), `${path} should exist`).toBe(true);
+    });
+
+    const sessionBffSource = readWebNext('lib/session-bff.ts');
+    const loginRouteSource = readWebNext('app/api/account/auth/form/route.ts');
+    const proxyRouteSource = readWebNext('app/api/[...path]/route.ts');
+    const nextConfigSource = readWebNext('next.config.mjs');
+
+    expect(sessionBffSource).toContain('httpOnly: true');
+    expect(sessionBffSource).toContain("sameSite: 'lax'");
+    expect(sessionBffSource).toContain('HB_UI_ACCESS_COOKIE');
+    expect(sessionBffSource).toContain('HB_UI_REFRESH_COOKIE');
+    expect(sessionBffSource).toContain('HB_UI_SESSION_MARKER_COOKIE');
+    expect(loginRouteSource).toContain('applySessionCookies');
+    expect(loginRouteSource).toContain('sanitizeSessionPayload');
+    expect(proxyRouteSource).toContain('proxyBackendApiRequest');
+    expect(nextConfigSource).not.toContain("source: '/api/:path*'");
+    expect(nextConfigSource).not.toContain('destination: `${backendOrigin}/api/:path*`');
+  });
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 36/50: fix(web-next): tighten topology toolbar alignment

Posted by zh...@apache.org.
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 1d6164c2d5d431e3587dce4fd88060990913be22
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 08:23:09 2026 +0800

    fix(web-next): tighten topology toolbar alignment
---
 web-next/app/topology/page.test.tsx               | 5 +++++
 web-next/app/ui-lab/page.test.tsx                 | 3 +++
 web-next/packages/hertzbeat-ui/src/index.test.tsx | 6 +++++-
 web-next/packages/hertzbeat-ui/src/index.tsx      | 7 +++++--
 4 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 07fde71da5..7ba725afc8 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -636,8 +636,13 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-toolbar-alignment="flush-control-grid"');
     expect(html).toContain('data-hz-topology-toolbar-inset="0px"');
     expect(html).toContain('data-hz-topology-toolbar-control-height="28px"');
+    expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
+    expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
+    expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
     expect(html).not.toContain('overflow-x-auto px-4 py-1');
     expect(html).toContain('overflow-x-auto px-0 py-1');
+    expect(html).toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
+    expect(html).not.toContain('h-7 !pl-2 !pr-1.5');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight-owner="hertzbeat-ui-toolbar-visual-weight"');
     expect(html).toContain('data-hz-topology-toolbar-secondary-visibility="assistive"');
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index 73be41f88f..09959b54d8 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2164,6 +2164,9 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-toolbar-first-viewport-priority="canvas"');
     expect(html).toContain('data-hz-topology-toolbar-row-contract="single-row-overflow"');
     expect(html).toContain('data-hz-topology-toolbar-row-contract-owner="hertzbeat-ui-toolbar-row-contract"');
+    expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
+    expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
+    expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index 3768abb6b1..8747e1a6ad 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -6074,10 +6074,14 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-toolbar-alignment="flush-control-grid"');
     expect(html).toContain('data-hz-topology-toolbar-inset="0px"');
     expect(html).toContain('data-hz-topology-toolbar-control-height="28px"');
+    expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
+    expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
+    expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
     expect(html).toContain('[grid-template-columns:132px_minmax(220px,1fr)_132px_auto]');
     expect(html).not.toContain('overflow-x-auto px-4 py-1');
     expect(html).toContain('overflow-x-auto px-0 py-1');
-    expect(html).toContain('h-7 !pl-2 !pr-1.5');
+    expect(html).toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
+    expect(html).not.toContain('h-7 !pl-2 !pr-1.5');
     expect(html).toContain('data-hz-topology-toolbar-action-policy="scope-controls-only"');
     expect(html).toContain('data-hz-topology-toolbar-canvas-action-policy="in-canvas-g6-toolbar"');
     expect(html).toContain('data-hz-topology-toolbar-chrome="frameless"');
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index c6455ec437..93e6dde149 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -12963,7 +12963,7 @@ export function HzTopologyToolbar({
   const secondaryVisibility = graphFirst ? 'assistive' : 'visible';
   const controlStripLayout = graphFirst ? 'inline-overflow' : 'stacked-grid';
   const chrome = graphFirst && boundary === 'none' ? 'frameless' : 'surface';
-  const selectTriggerClassName = graphFirst ? 'h-7 !pl-2 !pr-1.5' : 'h-8';
+  const selectTriggerClassName = graphFirst ? 'h-7 !gap-1.5 !pl-1 !pr-1.5' : 'h-8';
 
   return (
     <section
@@ -12988,6 +12988,9 @@ export function HzTopologyToolbar({
       data-hz-topology-toolbar-alignment={graphFirst ? 'flush-control-grid' : 'stacked-grid'}
       data-hz-topology-toolbar-inset={graphFirst ? '0px' : '16px'}
       data-hz-topology-toolbar-control-height={graphFirst ? '28px' : '32px'}
+      data-hz-topology-toolbar-select-padding={graphFirst ? 'compact-flush' : 'default'}
+      data-hz-topology-toolbar-row-separator={graphFirst ? 'none' : 'soft'}
+      data-hz-topology-toolbar-control-gap={graphFirst ? '6px' : '8px'}
       data-hz-topology-toolbar-visual-weight={graphFirst ? 'low-interruption' : 'balanced'}
       data-hz-topology-toolbar-visual-weight-owner="hertzbeat-ui-toolbar-visual-weight"
       data-hz-topology-toolbar-secondary-visibility={secondaryVisibility}
@@ -13164,7 +13167,7 @@ export function HzTopologyToolbar({
           className={cn(
             'flex min-w-0 flex-wrap items-center gap-2 border-t border-[var(--hz-ui-line-faint)] pt-2 text-[11px]',
             graphFirst ? '' : 'lg:col-span-4',
-            graphFirst ? 'sr-only' : ''
+            graphFirst ? 'sr-only border-0 pt-0' : ''
           )}
           data-hz-topology-toolbar-state={stateItems.map(item => item.id).join('-')}
           data-hz-topology-toolbar-state-visibility={secondaryVisibility}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 39/50: fix(web-next): dedupe topology header scope summary

Posted by zh...@apache.org.
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 64f7dd69a2693f4f856ab8e58a89d2b60d6430c6
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 08:54:09 2026 +0800

    fix(web-next): dedupe topology header scope summary
---
 web-next/app/topology/page.test.tsx               |  3 +++
 web-next/app/topology/topology-page.tsx           |  2 ++
 web-next/packages/hertzbeat-ui/src/index.test.tsx | 20 ++++++++++++++++++++
 web-next/packages/hertzbeat-ui/src/index.tsx      | 11 ++++++++++-
 4 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index c15758cea4..559e979481 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -537,6 +537,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-primitive="scope-bar"');
     expect(html).toContain('data-hz-topology-scope-bar-boundary="none"');
     expect(html).toContain('data-hz-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"');
+    expect(html).toContain('data-hz-topology-scope-summary-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-scope-summary-deduped-by="topology-toolbar"');
+    expect(html).toContain('data-hz-topology-scope-item-visibility="assistive"');
     expect(html).toContain('data-hz-topology-scope-item="environment"');
     expect(html).toContain('data-hz-topology-scope-item-owner="hertzbeat-ui-scope-item"');
     expect(html).toContain('data-hz-topology-scope-item-value-owner="hertzbeat-ui-scope-item-value"');
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index 62209c2af4..c17a439823 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -1360,6 +1360,8 @@ export default function TopologyPage({
             data-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"
             data-topology-refresh-action="refresh"
             boundary="none"
+            summaryVisibility="assistive"
+            summaryDedupedBy="topology-toolbar"
             items={[
               {
                 id: 'environment',
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index f75ebf9007..1613acd63f 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -5618,6 +5618,26 @@ describe('@hertzbeat/ui', () => {
     expect(html).not.toContain('rounded-[16px]');
     expect(html).not.toContain('rounded-[14px]');
     expect(html).not.toContain('rounded-[12px]');
+
+    const actionOnlyHtml = renderToStaticMarkup(
+      <HzTopologyScopeBar
+        summaryVisibility="assistive"
+        summaryDedupedBy="topology-toolbar"
+        items={[
+          { id: 'environment', label: 'Environment', value: 'prod' },
+          { id: 'time-range', label: 'Time range', value: 'last 1 hour' }
+        ]}
+        actions={[
+          { id: 'refresh', label: 'Refresh topology', emphasis: 'neutral' }
+        ]}
+      />
+    );
+
+    expect(actionOnlyHtml).toContain('data-hz-topology-scope-summary-visibility="assistive"');
+    expect(actionOnlyHtml).toContain('data-hz-topology-scope-summary-deduped-by="topology-toolbar"');
+    expect(actionOnlyHtml).toContain('data-hz-topology-scope-item-visibility="assistive"');
+    expect(actionOnlyHtml).toContain('class="sr-only"');
+    expect(actionOnlyHtml).toContain('data-hz-topology-scope-action="refresh"');
   });
 
   it('renders a compact topology focus trail for focused graph context and filters', () => {
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index 91225227d0..68ccfb32f6 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -10838,6 +10838,8 @@ export type HzTopologyScopeBarProps = React.HTMLAttributes<HTMLElement> & {
   items: HzTopologyScopeBarItem[];
   actions?: HzTopologyScopeBarAction[];
   boundary?: HzTopologyScopeBarBoundary;
+  summaryVisibility?: 'visible' | 'assistive';
+  summaryDedupedBy?: string;
 };
 
 export type HzTopologyNodeProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
@@ -12304,6 +12306,8 @@ export function HzTopologyScopeBar({
   items,
   actions = [],
   boundary = 'none',
+  summaryVisibility = 'visible',
+  summaryDedupedBy,
   className,
   ...props
 }: HzTopologyScopeBarProps) {
@@ -12320,6 +12324,8 @@ export function HzTopologyScopeBar({
       data-hz-topology-scope-bar-density="compact"
       data-hz-topology-scope-bar-boundary={boundary}
       data-hz-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"
+      data-hz-topology-scope-summary-visibility={summaryVisibility}
+      data-hz-topology-scope-summary-deduped-by={summaryDedupedBy}
     >
       {items.map(item => {
         const { id, label, value, className: itemClassName, ...itemProps } = item;
@@ -12328,11 +12334,14 @@ export function HzTopologyScopeBar({
             key={id}
             {...itemProps}
             className={cn(
-              'inline-flex h-7 min-w-0 items-center gap-1 rounded-[3px] border border-[#252832] bg-[#151821] px-2 text-[#9ca3b4]',
+              summaryVisibility === 'assistive'
+                ? 'sr-only'
+                : 'inline-flex h-7 min-w-0 items-center gap-1 rounded-[3px] border border-[#252832] bg-[#151821] px-2 text-[#9ca3b4]',
               itemClassName
             )}
             data-hz-topology-scope-item={id}
             data-hz-topology-scope-item-owner="hertzbeat-ui-scope-item"
+            data-hz-topology-scope-item-visibility={summaryVisibility}
           >
             {label ? (
               <span className="truncate text-[#727b8c]" data-hz-topology-scope-item-label-owner="hertzbeat-ui-scope-item-label">


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 46/50: fix(web-next): clamp compact topology zoom after centering

Posted by zh...@apache.org.
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 7ff8e346850b33fbfbc9cba302c34ef423af7407
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 10:37:07 2026 +0800

    fix(web-next): clamp compact topology zoom after centering
---
 web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx | 11 +++++++++++
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx      |  3 ++-
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
index b4f71a5d5f..7633ecdcdb 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
@@ -402,6 +402,17 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(denseHtml).toContain('data-hz-topology-g6-initial-fit-strategy="overflow-fit"');
   });
 
+  it('keeps the compact center-only path ending with a hard 1x zoom clamp', () => {
+    const source = topologyG6Source;
+    const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={buildHzTopologyG6ScaleFixture(7)} />);
+    const centerOnlyMatch = source.match(/async function centerOnlyG6Viewport[\s\S]*?\n}/);
+
+    expect(html).toContain('data-hz-topology-g6-compact-final-zoom-clamp="true"');
+    expect(centerOnlyMatch?.[0].indexOf('fitCenter')).toBeGreaterThanOrEqual(0);
+    expect(centerOnlyMatch?.[0].indexOf('zoomTo')).toBeGreaterThan(centerOnlyMatch?.[0].indexOf('fitCenter') ?? -1);
+    expect(centerOnlyMatch?.[0]).toContain('HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM');
+  });
+
   it('centers the shared G6 canvas after fit and reset view actions', () => {
     const source = topologyG6Source;
     const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={buildHzTopologyG6ScaleFixture(8)} />);
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
index 86e954234a..20ff1fbd1a 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
@@ -1163,8 +1163,8 @@ async function centerOnlyG6Viewport(
   animation: Record<string, unknown> | boolean
 ) {
   if (!runtimeGraph) return;
-  await runtimeGraph.zoomTo?.(HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM, animation);
   await runtimeGraph.fitCenter?.(animation);
+  await runtimeGraph.zoomTo?.(HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM, animation);
 }
 
 async function withG6AutoFitZoomRange(runtimeGraph: G6GraphRuntime, action: () => Promise<void>) {
@@ -1890,6 +1890,7 @@ export function HzTopologyG6Canvas({
       data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range"
       data-hz-topology-g6-initial-fit-strategy={initialFitStrategy}
       data-hz-topology-g6-initial-fit-strategy-owner="hertzbeat-ui-g6-initial-fit-strategy"
+      data-hz-topology-g6-compact-final-zoom-clamp="true"
       data-hz-topology-g6-operator-zoom-bounds={`${HZ_TOPOLOGY_G6_MIN_ZOOM}-${HZ_TOPOLOGY_G6_MAX_ZOOM}`}
       data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"
       data-hz-topology-g6-fit-mode="overflow-only-center"


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 25/50: fix(collector): include mysql r2dbc runtime

Posted by zh...@apache.org.
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 22d4574d548b868579d4b523cd697b3dd082a0f6
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 09:49:15 2026 +0800

    fix(collector): include mysql r2dbc runtime
    
    Validation:
    
    - ./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests clean compile
    
    - git diff --cached --check
    
    - cached trailing-whitespace scan
---
 hertzbeat-collector/hertzbeat-collector-collector/pom.xml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/hertzbeat-collector/hertzbeat-collector-collector/pom.xml b/hertzbeat-collector/hertzbeat-collector-collector/pom.xml
index 8a108c99de..11b0126495 100644
--- a/hertzbeat-collector/hertzbeat-collector-collector/pom.xml
+++ b/hertzbeat-collector/hertzbeat-collector-collector/pom.xml
@@ -41,6 +41,11 @@
             <artifactId>hertzbeat-collector-basic</artifactId>
             <version>${hertzbeat.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hertzbeat</groupId>
+            <artifactId>hertzbeat-collector-mysql-r2dbc</artifactId>
+            <version>${hertzbeat.version}</version>
+        </dependency>
 
         <!-- collector-kafka -->
         <dependency>


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 10/50: feat(web-next): restore alert workflow parity

Posted by zh...@apache.org.
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 0f6c22d9c7df40890ec83efd303cb36eb52b17fe
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 00:57:14 2026 +0800

    feat(web-next): restore alert workflow parity
---
 web-next/app/alert/alert-center-page.tsx           |  263 ++++
 .../app/alert/alert-modal-feedback.guard.test.ts   |   10 +-
 web-next/app/alert/center/page.tsx                 |    5 +-
 .../alert/group/{page.tsx => alert-group-page.tsx} |  174 ++-
 web-next/app/alert/group/page.test.tsx             |  182 ++-
 web-next/app/alert/group/page.tsx                  |  239 +---
 web-next/app/alert/inhibit/alert-inhibit-page.tsx  |  456 +++++++
 web-next/app/alert/inhibit/page.test.tsx           |  388 +++++-
 web-next/app/alert/inhibit/page.tsx                |  237 +---
 .../app/alert/integration/[source]/page.test.tsx   |   47 +
 web-next/app/alert/integration/[source]/page.tsx   |   51 +-
 .../notice/{page.tsx => alert-notice-page.tsx}     |  680 +++++++---
 web-next/app/alert/notice/page.test.tsx            |  760 ++++++++++-
 web-next/app/alert/notice/page.tsx                 | 1356 +-------------------
 web-next/app/alert/page.test.tsx                   |  352 +++--
 web-next/app/alert/page.tsx                        |  111 +-
 web-next/app/alert/setting/alert-setting-page.tsx  |  502 ++++++++
 web-next/app/alert/setting/page.test.tsx           |  252 +++-
 web-next/app/alert/setting/page.tsx                |  212 +--
 web-next/app/alert/silence/alert-silence-page.tsx  |  443 +++++++
 web-next/app/alert/silence/page.test.tsx           |  397 +++++-
 web-next/app/alert/silence/page.tsx                |  232 +---
 web-next/app/alerts/page.test.ts                   |   68 +-
 web-next/app/alerts/page.tsx                       |   13 +-
 .../components/pages/alert-center-surface.test.tsx |  786 +++++++++++-
 web-next/components/pages/alert-center-surface.tsx |  679 +++++++++-
 .../pages/alert-group-authoring-fields.tsx         |    2 +-
 .../components/pages/alert-group-surface.test.tsx  |  272 ++++
 web-next/components/pages/alert-group-surface.tsx  |  127 +-
 .../pages/alert-inhibit-authoring-fields.tsx       |   10 +-
 .../pages/alert-inhibit-surface.test.tsx           |  440 ++++++-
 .../components/pages/alert-inhibit-surface.tsx     |  277 +++-
 .../components/pages/alert-integration-mermaid.tsx |   15 +-
 .../pages/alert-notice-receiver-fields.test.tsx    |  139 +-
 .../pages/alert-notice-receiver-fields.tsx         |   32 +-
 .../pages/alert-notice-rule-fields.test.tsx        |  239 +++-
 .../components/pages/alert-notice-rule-fields.tsx  |  195 ++-
 .../pages/alert-notice-template-fields.test.tsx    |   70 +-
 .../pages/alert-notice-template-fields.tsx         |   71 +-
 .../components/pages/alert-rule-quick-dialog.tsx   |   50 +-
 .../pages/alert-setting-create-dialog.test.tsx     |   31 +
 .../pages/alert-setting-create-dialog.tsx          |   24 +
 .../pages/alert-setting-surface.test.tsx           |  358 ++++++
 .../components/pages/alert-setting-surface.tsx     |  201 ++-
 .../pages/alert-silence-authoring-fields.test.tsx  |    1 +
 .../pages/alert-silence-authoring-fields.tsx       |   16 +-
 .../pages/alert-silence-surface.test.tsx           |  479 ++++++-
 .../components/pages/alert-silence-surface.tsx     |  249 +++-
 web-next/lib/alert-api-facade.test.ts              |  270 ++++
 web-next/lib/alert-api-facade.ts                   |  168 +++
 web-next/lib/alert-group/controller.test.ts        |   68 +-
 web-next/lib/alert-group/controller.ts             |   66 +
 web-next/lib/alert-group/query-state.test.ts       |   60 +-
 web-next/lib/alert-group/query-state.ts            |   50 +-
 web-next/lib/alert-group/view-model.test.ts        |   53 +-
 web-next/lib/alert-group/view-model.ts             |   50 +-
 web-next/lib/alert-inhibit/controller.test.ts      |  153 ++-
 web-next/lib/alert-inhibit/controller.ts           |  196 ++-
 web-next/lib/alert-inhibit/query-state.test.ts     |  114 +-
 web-next/lib/alert-inhibit/query-state.ts          |   99 +-
 web-next/lib/alert-inhibit/view-model.test.ts      |   51 +-
 web-next/lib/alert-inhibit/view-model.ts           |   19 +-
 web-next/lib/alert-integration/controller.test.ts  |   16 +-
 web-next/lib/alert-integration/controller.ts       |   28 +-
 web-next/lib/alert-integration/view-model.test.ts  |   40 +-
 web-next/lib/alert-integration/view-model.ts       |   80 +-
 web-next/lib/alert-label-options.ts                |    6 +
 web-next/lib/alert-manage/controller.test.ts       |  140 +-
 web-next/lib/alert-manage/controller.ts            |  105 +-
 web-next/lib/alert-manage/query-state.test.ts      |   83 +-
 web-next/lib/alert-manage/query-state.ts           |   71 +-
 web-next/lib/alert-manage/view-model.test.ts       |  296 ++++-
 web-next/lib/alert-manage/view-model.ts            |  300 +++--
 web-next/lib/alert-notice/controller.test.ts       |  138 +-
 web-next/lib/alert-notice/controller.ts            |  143 ++-
 web-next/lib/alert-notice/query-state.test.ts      |   49 +
 web-next/lib/alert-notice/query-state.ts           |   18 +
 web-next/lib/alert-notice/view-model.test.ts       |  101 +-
 web-next/lib/alert-notice/view-model.ts            |  175 ++-
 web-next/lib/alert-setting/controller.test.ts      |   64 +-
 web-next/lib/alert-setting/controller.ts           |   74 +-
 web-next/lib/alert-setting/query-state.test.ts     |   75 ++
 web-next/lib/alert-setting/query-state.ts          |   54 +
 web-next/lib/alert-setting/view-model.test.ts      |   27 +
 web-next/lib/alert-setting/view-model.ts           |   10 +-
 web-next/lib/alert-silence/controller.test.ts      |  164 ++-
 web-next/lib/alert-silence/controller.ts           |  165 ++-
 web-next/lib/alert-silence/query-state.test.ts     |  114 +-
 web-next/lib/alert-silence/query-state.ts          |   99 +-
 web-next/lib/alert-silence/view-model.test.ts      |   85 +-
 web-next/lib/alert-silence/view-model.ts           |   37 +-
 91 files changed, 12759 insertions(+), 3608 deletions(-)

diff --git a/web-next/app/alert/alert-center-page.tsx b/web-next/app/alert/alert-center-page.tsx
new file mode 100644
index 0000000000..f666601e3c
--- /dev/null
+++ b/web-next/app/alert/alert-center-page.tsx
@@ -0,0 +1,263 @@
+'use client';
+
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { AlertCenterSurface, type AlertEntityResponseResult } from '../../components/pages/alert-center-surface';
+import { useI18n } from '../../components/providers/i18n-provider';
+import { ClientWorkbench } from '../../components/workbench/client-workbench';
+import { api } from '../../lib/alert-api-facade';
+import { createAlertInhibitFromFacade, type AlertInhibitFormDraft } from '../../lib/alert-inhibit/controller';
+import {
+  applyAlertClosureOperationFromFacade,
+  buildAlertQueryAfterClosureOperation,
+  clampAlertCenterPageIndexAfterDelete,
+  loadAlertCenterDataFromFacade,
+  type AlertClosureOperationAction,
+  type AlertPageData
+} from '../../lib/alert-manage/controller';
+import {
+  ALERT_CENTER_PAGE_SIZE_OPTIONS,
+  buildAlertListUrl,
+  hasAlertEntityContext,
+  type AlertCenterRouteState,
+  type AlertQueryState
+} from '../../lib/alert-manage/query-state';
+import {
+  buildAlertClosureOperationFailureFeedback,
+  buildAlertClosureOperationFeedback,
+  type AlertRuleDialogMode
+} from '../../lib/alert-manage/view-model';
+import { createAlertSilenceFromFacade, type AlertSilenceFormDraft } from '../../lib/alert-silence/controller';
+import { HEADER_ALERT_EVENT_TYPE, HEADER_ALERT_SSE_URL, parseHeaderSseJson } from '../../lib/shell/header-realtime';
+import type { GroupAlert } from '../../lib/types';
+
+const EMPTY_ALERT_QUERY: AlertQueryState = {
+  search: '',
+  status: '',
+  severity: '',
+  pageIndex: 0,
+  pageSize: ALERT_CENTER_PAGE_SIZE_OPTIONS[0],
+  entityId: '',
+  entityName: '',
+  returnTo: ''
+};
+const EMPTY_ALERT_CENTER_ROUTE_STATE: AlertCenterRouteState = {
+  initialQuery: EMPTY_ALERT_QUERY,
+  cleanUrl: '/alert',
+  shouldCleanUrl: false
+};
+const ALERT_CENTER_SETTLED_CACHE_TTL_MS = 10_000;
+
+function normalizeEntityResponseAction(action: AlertClosureOperationAction): NonNullable<AlertEntityResponseResult>['action'] {
+  return action === 'recover' ? 'resolve' : action;
+}
+
+export function resolveRealtimeGroupId(alert: Pick<GroupAlert, 'id' | 'groupKey'> | null | undefined): number | null {
+  if (!alert) {
+    return null;
+  }
+  const numericId = Number(alert.id);
+  if (Number.isFinite(numericId) && numericId > 0) {
+    return numericId;
+  }
+  const numericGroupKey = Number(alert.groupKey);
+  return Number.isFinite(numericGroupKey) && numericGroupKey > 0 ? numericGroupKey : null;
+}
+
+export default function AlertCenterPage({ initialRouteState }: { initialRouteState?: AlertCenterRouteState } = {}) {
+  const { t } = useI18n();
+  const router = useRouter();
+  const alertCenterRouteState = initialRouteState ?? EMPTY_ALERT_CENTER_ROUTE_STATE;
+  const initialQuery = alertCenterRouteState.initialQuery;
+  const [draft, setDraft] = useState<AlertQueryState>(initialQuery);
+  const [query, setQuery] = useState<AlertQueryState>(initialQuery);
+  const [refreshNonce, setRefreshNonce] = useState(0);
+  const [operationFeedback, setOperationFeedback] = useState<{ tone: 'success' | 'danger'; copy: string } | null>(null);
+  const [selectedGroupIds, setSelectedGroupIds] = useState<number[]>([]);
+  const [entityResponseResult, setEntityResponseResult] = useState<AlertEntityResponseResult>(null);
+  const [realtimeEventCount, setRealtimeEventCount] = useState(0);
+  const [realtimeGroupIds, setRealtimeGroupIds] = useState<number[]>([]);
+  const realtimeHighlightTimers = useRef<number[]>([]);
+  const alertListUrl = useMemo(() => buildAlertListUrl(query), [query]);
+  const alertCenterCacheKey = useMemo(
+    () => ['alert-center', alertListUrl, refreshNonce].join('|'),
+    [alertListUrl, refreshNonce]
+  );
+
+  useEffect(() => {
+    setDraft(initialQuery);
+    setQuery(initialQuery);
+    setSelectedGroupIds([]);
+    setEntityResponseResult(null);
+  }, [initialQuery]);
+
+  useEffect(() => {
+    if (alertCenterRouteState.shouldCleanUrl) {
+      router.replace(alertCenterRouteState.cleanUrl);
+    }
+  }, [alertCenterRouteState.cleanUrl, alertCenterRouteState.shouldCleanUrl, router]);
+
+  useEffect(() => {
+    if (typeof window === 'undefined' || !window.EventSource) {
+      return undefined;
+    }
+
+    const eventSource = new window.EventSource(HEADER_ALERT_SSE_URL);
+    const handleAlertEvent = (event: MessageEvent<string>) => {
+      const alert = parseHeaderSseJson<GroupAlert>(event.data);
+      if (!alert) {
+        return;
+      }
+      const realtimeGroupId = resolveRealtimeGroupId(alert);
+      setRealtimeEventCount(current => current + 1);
+      if (realtimeGroupId) {
+        setRealtimeGroupIds(current => [realtimeGroupId, ...current.filter(groupId => groupId !== realtimeGroupId)].slice(0, 8));
+        const timer = window.setTimeout(() => {
+          setRealtimeGroupIds(current => current.filter(groupId => groupId !== realtimeGroupId));
+        }, 1000);
+        realtimeHighlightTimers.current.push(timer);
+      }
+      setRefreshNonce(current => current + 1);
+    };
+    eventSource.addEventListener(HEADER_ALERT_EVENT_TYPE, handleAlertEvent);
+    eventSource.onerror = () => {
+      eventSource.close();
+    };
+
+    return () => {
+      eventSource.removeEventListener(HEADER_ALERT_EVENT_TYPE, handleAlertEvent);
+      eventSource.close();
+    };
+  }, []);
+
+  useEffect(() => () => {
+    realtimeHighlightTimers.current.forEach(timer => window.clearTimeout(timer));
+    realtimeHighlightTimers.current = [];
+  }, []);
+
+  const load = useCallback(async (): Promise<AlertPageData> => {
+    return loadAlertCenterDataFromFacade(api, query);
+  }, [query]);
+
+  const refreshQuery = useCallback(() => {
+    const nextQuery = { ...draft, pageIndex: 0, pageSize: draft.pageSize ?? query.pageSize ?? ALERT_CENTER_PAGE_SIZE_OPTIONS[0] };
+    setDraft(nextQuery);
+    setQuery(nextQuery);
+    setSelectedGroupIds([]);
+    setRefreshNonce(current => current + 1);
+  }, [draft, query.pageSize]);
+
+  const handlePageIndexChange = useCallback((nextPageIndex: number) => {
+    const normalizedPageIndex = Math.max(0, Math.floor(nextPageIndex));
+    setDraft(current => ({ ...current, pageIndex: normalizedPageIndex }));
+    setQuery(current => ({ ...current, pageIndex: normalizedPageIndex }));
+    setSelectedGroupIds([]);
+  }, []);
+
+  const handlePageSizeChange = useCallback((nextPageSize: number) => {
+    const normalizedPageSize = ALERT_CENTER_PAGE_SIZE_OPTIONS.includes(nextPageSize as (typeof ALERT_CENTER_PAGE_SIZE_OPTIONS)[number])
+      ? nextPageSize
+      : ALERT_CENTER_PAGE_SIZE_OPTIONS[0];
+    setDraft(current => ({ ...current, pageIndex: 0, pageSize: normalizedPageSize }));
+    setQuery(current => ({ ...current, pageIndex: 0, pageSize: normalizedPageSize }));
+    setSelectedGroupIds([]);
+  }, []);
+
+  const handleClosureAction = useCallback(async (action: AlertClosureOperationAction, groupId: number | number[], totalElements = 0) => {
+    setOperationFeedback(null);
+    try {
+      await applyAlertClosureOperationFromFacade(api.alerts, action, groupId);
+      setOperationFeedback({ tone: 'success', copy: buildAlertClosureOperationFeedback(action, t) });
+      const affectedCount = Array.isArray(groupId) ? groupId.length : 1;
+      if (hasAlertEntityContext(query)) {
+        setEntityResponseResult({ action: normalizeEntityResponseAction(action), count: affectedCount });
+      }
+      const buildPostActionQuery = (current: AlertQueryState) => {
+        const nextQuery = buildAlertQueryAfterClosureOperation(current, action);
+        return action === 'close' || action === 'delete'
+          ? clampAlertCenterPageIndexAfterDelete(nextQuery, totalElements, affectedCount)
+          : nextQuery;
+      };
+      setDraft(current => buildPostActionQuery(current));
+      setQuery(current => buildPostActionQuery(current));
+      setSelectedGroupIds([]);
+      setRefreshNonce(current => current + 1);
+    } catch (error) {
+      setOperationFeedback({
+        tone: 'danger',
+        copy: buildAlertClosureOperationFailureFeedback(action, t)
+      });
+    }
+  }, [query, t]);
+
+  const handleRuleQuickCreate = useCallback(async (
+    mode: AlertRuleDialogMode,
+    ruleDraft: AlertSilenceFormDraft | AlertInhibitFormDraft,
+    count: number
+  ) => {
+    setOperationFeedback(null);
+    try {
+      if (mode === 'silence') {
+        await createAlertSilenceFromFacade(api.alertSilences.create, ruleDraft as AlertSilenceFormDraft);
+      } else {
+        await createAlertInhibitFromFacade(api.alertInhibits.create, ruleDraft as AlertInhibitFormDraft);
+      }
+      setOperationFeedback({ tone: 'success', copy: t('common.notify.new-success') });
+      if (hasAlertEntityContext(query) && count > 0) {
+        setEntityResponseResult({ action: mode, count });
+      }
+      setSelectedGroupIds([]);
+      setRefreshNonce(current => current + 1);
+    } catch (error) {
+      setOperationFeedback({
+        tone: 'danger',
+        copy: t('common.notify.new-fail')
+      });
+      throw error;
+    }
+  }, [query, t]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('alert.center.loading')}
+      cacheKey={alertCenterCacheKey}
+      cacheSettledTtlMs={ALERT_CENTER_SETTLED_CACHE_TTL_MS}
+    >
+      {data => (
+        <AlertCenterSurface
+          t={t}
+          data={data}
+          draft={draft}
+          onDraftChange={setDraft}
+          onRefresh={refreshQuery}
+          onClearFilters={() => {
+            const cleared = {
+              ...draft,
+              search: '',
+              severity: '',
+              status: hasAlertEntityContext(draft) ? 'firing' : '',
+              pageIndex: 0,
+              pageSize: draft.pageSize ?? query.pageSize ?? ALERT_CENTER_PAGE_SIZE_OPTIONS[0]
+            };
+            setDraft(cleared);
+            setQuery(cleared);
+            setSelectedGroupIds([]);
+            setEntityResponseResult(null);
+          }}
+          operationFeedback={operationFeedback}
+          entityResponseResult={entityResponseResult}
+          realtimeEventCount={realtimeEventCount}
+          realtimeGroupIds={realtimeGroupIds}
+          pageSizeOptions={[...ALERT_CENTER_PAGE_SIZE_OPTIONS]}
+          onPageIndexChange={handlePageIndexChange}
+          onPageSizeChange={handlePageSizeChange}
+          selectedGroupIds={selectedGroupIds}
+          onSelectedGroupIdsChange={setSelectedGroupIds}
+          onClosureAction={(action, groupId) => void handleClosureAction(action, groupId, data.groupAlerts.totalElements)}
+          onRuleQuickCreate={handleRuleQuickCreate}
+        />
+      )}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/alert/alert-modal-feedback.guard.test.ts b/web-next/app/alert/alert-modal-feedback.guard.test.ts
index 0141904588..8723704464 100644
--- a/web-next/app/alert/alert-modal-feedback.guard.test.ts
+++ b/web-next/app/alert/alert-modal-feedback.guard.test.ts
@@ -3,11 +3,11 @@ import { resolve } from 'node:path';
 import { describe, expect, it } from 'vitest';
 
 const alertPageFiles = [
-  'app/alert/silence/page.tsx',
-  'app/alert/group/page.tsx',
-  'app/alert/inhibit/page.tsx',
-  'app/alert/notice/page.tsx',
-  'app/alert/setting/page.tsx'
+  'app/alert/silence/alert-silence-page.tsx',
+  'app/alert/group/alert-group-page.tsx',
+  'app/alert/inhibit/alert-inhibit-page.tsx',
+  'app/alert/notice/alert-notice-page.tsx',
+  'app/alert/setting/alert-setting-page.tsx'
 ];
 
 describe('alert modal feedback guard', () => {
diff --git a/web-next/app/alert/center/page.tsx b/web-next/app/alert/center/page.tsx
index 0b312bc642..201cbfc2ab 100644
--- a/web-next/app/alert/center/page.tsx
+++ b/web-next/app/alert/center/page.tsx
@@ -1,10 +1,9 @@
 import { redirect } from 'next/navigation';
-import { buildAlertCompatRouteUrl } from '../../../lib/alert-manage/query-state';
-import { createCompatSearchParamReader, type SearchParamsRecord } from '../../../lib/compat/search-params';
+import { buildAlertCompatRouteUrlFromSearchParams, type SearchParamsRecord } from '../../../lib/alert-manage/query-state';
 
 export default async function AlertCenterAliasPage(props: {
   searchParams?: Promise<SearchParamsRecord>;
 }) {
   const resolvedSearchParams = await props?.searchParams;
-  redirect(buildAlertCompatRouteUrl(createCompatSearchParamReader(resolvedSearchParams)));
+  redirect(buildAlertCompatRouteUrlFromSearchParams(resolvedSearchParams));
 }
diff --git a/web-next/app/alert/group/page.tsx b/web-next/app/alert/group/alert-group-page.tsx
similarity index 53%
copy from web-next/app/alert/group/page.tsx
copy to web-next/app/alert/group/alert-group-page.tsx
index 294e4dacbe..352e70d40b 100644
--- a/web-next/app/alert/group/page.tsx
+++ b/web-next/app/alert/group/alert-group-page.tsx
@@ -1,65 +1,89 @@
 'use client';
 
 import React, { useCallback, useMemo, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
 import { ClientWorkbench } from '../../../components/workbench/client-workbench';
 import { useI18n } from '../../../components/providers/i18n-provider';
 import { AlertGroupSurface } from '../../../components/pages/alert-group-surface';
 import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '../../../lib/api-client';
+import { api } from '../../../lib/alert-api-facade';
 import {
-  createAlertGroup,
-  deleteAlertGroup,
-  deleteAlertGroups,
-  loadAlertGroupDetail,
-  updateAlertGroup,
+  createAlertGroupFromFacade,
+  deleteAlertGroupFromFacade,
+  deleteAlertGroupsFromFacade,
+  loadAlertGroupDataFromFacade,
+  loadAlertGroupDetailFromFacade,
+  updateAlertGroupEnabledFromFacade,
+  updateAlertGroupFromFacade,
   type AlertGroupFormDraft
 } from '../../../lib/alert-group/controller';
-import { buildAlertGroupUrl } from '../../../lib/alert-group/query-state';
+import { ALERT_GROUP_PAGE_SIZE_OPTIONS, buildAlertGroupUrl, type AlertGroupRouteState } from '../../../lib/alert-group/query-state';
 import { buildAlertGroupEvidenceContext, buildAlertGroupFormDraft, validateAlertGroupForm } from '../../../lib/alert-group/view-model';
-import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptions } from '../../../lib/alert-label-options';
+import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptionsFromFacade } from '../../../lib/alert-label-options';
 import { formatTime } from '../../../lib/format';
-import { readSignalRouteContext } from '../../../lib/signal-route-context';
-import type { AlertGroupConverge, PageResult } from '../../../lib/types';
+import type { AlertGroupConverge } from '../../../lib/types';
 
 type GroupDeleteRequest = {
   kind: 'single' | 'batch';
   ids: number[];
 };
 
-export default function AlertGroupPage() {
+type AlertGroupErrorContract = 'save' | 'enable' | 'delete';
+
+const ALERT_GROUP_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_ALERT_GROUP_ROUTE_STATE: AlertGroupRouteState = {
+  signal: null,
+  signalContext: {}
+};
+
+export default function AlertGroupPage({ initialRouteState }: { initialRouteState?: AlertGroupRouteState } = {}) {
   const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const signalContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
-  const signal = searchParams.get('signal');
+  const alertGroupRouteState = initialRouteState ?? EMPTY_ALERT_GROUP_ROUTE_STATE;
+  const { signal, signalContext } = alertGroupRouteState;
   const groupEvidenceContext = useMemo(
     () => buildAlertGroupEvidenceContext(signal, signalContext, t),
     [signal, signalContext, t]
   );
   const [search, setSearch] = useState('');
   const [query, setQuery] = useState('');
+  const [pageIndex, setPageIndex] = useState(0);
+  const [pageSize, setPageSize] = useState<number>(ALERT_GROUP_PAGE_SIZE_OPTIONS[0]);
   const [selectedId, setSelectedId] = useState<number | null>(null);
   const [editorOpen, setEditorOpen] = useState(false);
   const [editorLoading, setEditorLoading] = useState(false);
   const [editorSaving, setEditorSaving] = useState(false);
   const [editorMessage, setEditorMessage] = useState<string | null>(null);
   const [editorError, setEditorError] = useState<string | null>(null);
+  const [editorErrorDetail, setEditorErrorDetail] = useState<string | null>(null);
+  const [editorErrorContract, setEditorErrorContract] = useState<AlertGroupErrorContract | null>(null);
   const [draft, setDraft] = useState<AlertGroupFormDraft>(() => buildAlertGroupFormDraft(null, groupEvidenceContext?.draftPatch));
   const [refreshTick, setRefreshTick] = useState(0);
   const [checkedIds, setCheckedIds] = useState<number[]>([]);
   const [deleteRequest, setDeleteRequest] = useState<GroupDeleteRequest | null>(null);
   const [deletePending, setDeletePending] = useState(false);
+  const alertGroupListUrl = useMemo(() => buildAlertGroupUrl({ search: query, pageIndex, pageSize }), [pageIndex, pageSize, query]);
+  const alertGroupCacheKey = useMemo(
+    () => ['alert-group', alertGroupListUrl, refreshTick].join('|'),
+    [alertGroupListUrl, refreshTick]
+  );
 
   const load = useCallback(async () => {
-    const [list, labelOptions] = await Promise.all([
-      apiMessageGet<PageResult<AlertGroupConverge>>(buildAlertGroupUrl(query)),
-      loadAlertLabelOptions(apiMessageGet).catch(() => DEFAULT_ALERT_LABEL_OPTIONS)
-    ]);
-    return { list, labelOptions, refreshTick };
-  }, [query, refreshTick]);
+    const data = await loadAlertGroupDataFromFacade(
+      {
+        list: api.alertGroups.list,
+        labelOptions: () => loadAlertLabelOptionsFromFacade(api.alertLabels.list)
+      },
+      { search: query, pageIndex, pageSize }
+    );
+    return { ...data, refreshTick };
+  }, [pageIndex, pageSize, query, refreshTick]);
 
   return (
-    <ClientWorkbench load={load} loadingCopy={t('alert.group.loading')}>
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('alert.group.loading')}
+      cacheKey={alertGroupCacheKey}
+      cacheSettledTtlMs={ALERT_GROUP_SETTLED_CACHE_TTL_MS}
+    >
       {data => {
         const labelOptions = data.labelOptions ?? DEFAULT_ALERT_LABEL_OPTIONS;
         const selected = data.list.content.find(item => item.id === selectedId) ?? data.list.content[0] ?? null;
@@ -67,6 +91,8 @@ export default function AlertGroupPage() {
         async function handleNew() {
           setDraft(buildAlertGroupFormDraft(null, groupEvidenceContext?.draftPatch));
           setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
           setEditorMessage(null);
           setEditorOpen(true);
         }
@@ -76,13 +102,17 @@ export default function AlertGroupPage() {
           if (!targetId) return;
           setEditorLoading(true);
           setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
           setEditorMessage(null);
           try {
-            const detail = await loadAlertGroupDetail(apiMessageGet, targetId);
+            const detail = await loadAlertGroupDetailFromFacade(api.alertGroups.detail, targetId);
             setDraft(buildAlertGroupFormDraft(detail));
             setEditorOpen(true);
           } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.load-failed'));
+            setEditorError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
           } finally {
             setEditorLoading(false);
           }
@@ -92,22 +122,29 @@ export default function AlertGroupPage() {
           const validationError = validateAlertGroupForm(draft, t);
           if (validationError) {
             setEditorError(validationError);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
             return;
           }
+          const isEdit = Boolean(draft.id);
           setEditorSaving(true);
           setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
           setEditorMessage(null);
           try {
-            if (draft.id) {
-              await updateAlertGroup(apiMessagePut as any, draft);
+            if (isEdit) {
+              await updateAlertGroupFromFacade(api.alertGroups.update, draft);
             } else {
-              await createAlertGroup(apiMessagePost as any, draft);
+              await createAlertGroupFromFacade(api.alertGroups.create, draft);
             }
-            setEditorMessage(t('common.save-success'));
+            setEditorMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditorOpen(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.save-failed'));
+            setEditorError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('save');
           } finally {
             setEditorSaving(false);
           }
@@ -117,15 +154,16 @@ export default function AlertGroupPage() {
           const target = group ?? selected;
           if (!target) return;
           try {
-            await updateAlertGroup(apiMessagePut as any, {
-              ...buildAlertGroupFormDraft(target),
-              enable: !(target.enable ?? true),
-            });
-            setEditorMessage(t('common.save-success'));
+            await updateAlertGroupEnabledFromFacade(api.alertGroups.update, target, !(target.enable ?? true));
+            setEditorMessage(t('common.notify.edit-success'));
             setEditorError(null);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.save-failed'));
+            setEditorError(t('common.notify.edit-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('enable');
           }
         }
 
@@ -141,19 +179,26 @@ export default function AlertGroupPage() {
           setDeletePending(true);
           try {
             if (request.kind === 'batch') {
-              await deleteAlertGroups(apiMessageDelete as any, request.ids);
+              await deleteAlertGroupsFromFacade(api.alertGroups.delete, request.ids);
               setCheckedIds([]);
             } else {
-              await deleteAlertGroup(apiMessageDelete as any, request.ids[0]);
+              await deleteAlertGroupFromFacade(api.alertGroups.delete, request.ids[0]);
             }
+            const nextTotal = Math.max((data.list.totalElements || 0) - request.ids.length, 0);
+            const nextLastPageIndex = Math.max(0, Math.ceil(nextTotal / pageSize) - 1);
+            setPageIndex(value => Math.min(value, nextLastPageIndex));
             setSelectedId(null);
             setEditorOpen(false);
-            setEditorMessage(t('common.delete-success'));
+            setEditorMessage(t('common.notify.delete-success'));
             setEditorError(null);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
             setDeleteRequest(null);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.delete-failed'));
+            setEditorError(t('common.notify.delete-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('delete');
           } finally {
             setDeletePending(false);
           }
@@ -164,10 +209,47 @@ export default function AlertGroupPage() {
         }
 
         async function handleDeleteSelected() {
-          if (checkedIds.length === 0) return;
+          if (checkedIds.length === 0) {
+            setEditorError(t('common.notify.no-select-delete'));
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            setEditorMessage(null);
+            return;
+          }
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
           setDeleteRequest({ kind: 'batch', ids: checkedIds });
         }
 
+        function handleApplyFilter() {
+          setQuery(search);
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handleClearFilter() {
+          setSearch('');
+          setQuery('');
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handlePageIndexChange(nextPageIndex: number) {
+          setPageIndex(nextPageIndex);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handlePageSizeChange(nextPageSize: number) {
+          setPageSize(nextPageSize);
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
         return (
           <>
             <AlertGroupSurface
@@ -181,19 +263,21 @@ export default function AlertGroupPage() {
               editorSaving={editorSaving}
               editorMessage={editorMessage}
               editorError={editorError}
+              editorErrorDetail={editorErrorDetail}
+              editorErrorContract={editorErrorContract}
               evidenceContext={groupEvidenceContext}
               draft={draft}
               formatTime={formatTime}
               labelOptions={labelOptions}
               onSearchChange={setSearch}
-              onApplyFilter={() => setQuery(search)}
-              onClearFilter={() => {
-                setSearch('');
-                setQuery('');
-              }}
+              onApplyFilter={handleApplyFilter}
+              onClearFilter={handleClearFilter}
               onRefresh={handleRefresh}
               onSelect={setSelectedId}
               onCheckedIdsChange={setCheckedIds}
+              pageSizeOptions={[...ALERT_GROUP_PAGE_SIZE_OPTIONS]}
+              onPageIndexChange={handlePageIndexChange}
+              onPageSizeChange={handlePageSizeChange}
               onNew={() => void handleNew()}
               onSave={() => void handleSave()}
               onToggleEnabled={group => void handleToggleEnabled(group)}
diff --git a/web-next/app/alert/group/page.test.tsx b/web-next/app/alert/group/page.test.tsx
index dc1fd1bc7d..b518c31a74 100644
--- a/web-next/app/alert/group/page.test.tsx
+++ b/web-next/app/alert/group/page.test.tsx
@@ -1,10 +1,12 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import type { AlertGroupRouteState } from '../../../lib/alert-group/query-state';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   renderData: {
     list: {
@@ -35,13 +37,6 @@ const mockState = vi.hoisted(() => ({
 
 const apiMessageGet = vi.hoisted(() => vi.fn());
 
-vi.mock('next/navigation', () => ({
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
-}));
-
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock()
@@ -51,13 +46,19 @@ vi.mock('../../../components/providers/i18n-provider', () => ({
 vi.mock('../../../components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return (
+      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
+        {children(mockState.renderData)}
+      </div>
+    );
   }
 }));
 
@@ -66,17 +67,20 @@ vi.mock('../../../components/pages/alert-group-surface', () => ({
     data,
     labelOptions,
     evidenceContext,
-    draft
+    draft,
+    pageSizeOptions
   }: {
     data: { list: { totalElements: number } };
     labelOptions?: { keys: string[] };
     evidenceContext?: { signal: string; groupLabelsText: string; returnHref?: string } | null;
     draft?: { groupLabelsText?: string };
+    pageSizeOptions?: number[];
   }) => (
     <div
       data-alert-group-surface="true"
       data-total={data.list.totalElements}
       data-label-options={labelOptions?.keys?.join('|')}
+      data-page-size-options={pageSizeOptions?.join('|')}
       data-alert-group-evidence-context={evidenceContext ? 'signal-route' : 'none'}
       data-alert-group-evidence-signal={evidenceContext?.signal ?? ''}
       data-alert-group-evidence-return={evidenceContext?.returnHref ?? ''}
@@ -93,19 +97,29 @@ vi.mock('../../../lib/api-client', () => ({
   apiMessagePut: vi.fn()
 }));
 
+const EMPTY_ROUTE_STATE: AlertGroupRouteState = {
+  signal: null,
+  signalContext: {}
+};
+
+async function renderAlertGroupPage(initialRouteState: AlertGroupRouteState = EMPTY_ROUTE_STATE) {
+  const { default: AlertGroupPage } = await import('./alert-group-page');
+  return renderToStaticMarkup(<AlertGroupPage initialRouteState={initialRouteState} />);
+}
+
 describe('alert group page', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams();
     mockState.lastLoad = null;
     apiMessageGet.mockClear().mockResolvedValue(mockState.renderData.list);
   });
 
   it('loads the group workspace through the shared query and surface contracts', async () => {
-    const { default: AlertGroupPage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertGroupPage />);
+    const html = await renderAlertGroupPage();
 
     expect(html).toContain('data-alert-group-surface="true"');
     expect(html).toContain('data-label-options="alertname|service|severity"');
+    expect(html).toContain('data-page-size-options="8|15|25"');
+    expect(html).toContain('data-loading-copy="Loading group rules"');
 
     await mockState.lastLoad?.();
 
@@ -114,23 +128,24 @@ describe('alert group page', () => {
   });
 
   it('preserves three-signal evidence context into new grouping authoring', async () => {
-    mockState.searchParams = new URLSearchParams({
+    const initialRouteState: AlertGroupRouteState = {
       signal: 'metrics',
-      source: 'otlp',
-      entityId: 'service:commerce/checkout',
-      entityName: 'checkout-api',
-      serviceName: 'checkout',
-      serviceNamespace: 'commerce',
-      environment: 'prod',
-      traceId: 'trace-123',
-      spanId: 'span-456',
-      collector: 'edge-collector-a',
-      template: 'java-service',
-      returnTo: '/metrics/manage?entityId=service%3Acommerce%2Fcheckout&returnLabel=%E6%8C%87%E6%A0%87%E5%B7%A5%E4%BD%9C%E5%8F%B0'
-    });
-
-    const { default: AlertGroupPage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertGroupPage />);
+      signalContext: {
+        source: 'otlp',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        collector: 'edge-collector-a',
+        template: 'java-service',
+        returnTo: '/metrics/manage?entityId=service%3Acommerce%2Fcheckout'
+      }
+    };
+
+    const html = await renderAlertGroupPage(initialRouteState);
 
     expect(html).toContain('data-alert-group-evidence-context="signal-route"');
     expect(html).toContain('data-alert-group-evidence-signal="metrics"');
@@ -140,5 +155,112 @@ describe('alert group page', () => {
     expect(html).not.toContain('returnLabel=');
     expect(html).not.toContain('trace_id');
     expect(html).not.toContain('span_id');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+    expect(source).not.toContain('useSearchParams');
+    expect(source).not.toContain('readSignalRouteContext(searchParams)');
+    expect(source).toContain('const alertGroupRouteState = initialRouteState ?? EMPTY_ALERT_GROUP_ROUTE_STATE');
+  });
+
+  it('keeps alert group remounts on a short settled cache window with refresh-tick invalidation', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('ALERT_GROUP_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('const [refreshTick, setRefreshTick] = useState(0)');
+    expect(source).toContain('const [pageIndex, setPageIndex] = useState(0)');
+    expect(source).toContain('const [pageSize, setPageSize] = useState<number>(ALERT_GROUP_PAGE_SIZE_OPTIONS[0])');
+    expect(source).toContain("['alert-group', alertGroupListUrl, refreshTick].join('|')");
+    expect(source).toContain('buildAlertGroupUrl({ search: query, pageIndex, pageSize })');
+    expect(source).toContain('[pageIndex, pageSize, query]');
+    expect(source).toContain('[alertGroupListUrl, refreshTick]');
+    expect(source).toContain('loadAlertGroupDataFromFacade');
+    expect(source).toContain('list: api.alertGroups.list');
+    expect(source).toContain('labelOptions: () => loadAlertLabelOptionsFromFacade(api.alertLabels.list)');
+    expect(source).not.toContain('apiMessageGet<PageResult<AlertGroupConverge>>(alertGroupListUrl)');
+    expect(source).toContain('return { ...data, refreshTick };');
+    expect(source.match(/setRefreshTick\(value => value \+ 1\)/g)?.length).toBeGreaterThanOrEqual(4);
+    expect(source).toContain('cacheKey={alertGroupCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={ALERT_GROUP_SETTLED_CACHE_TTL_MS}');
+    expect(source).toContain('pageSizeOptions={[...ALERT_GROUP_PAGE_SIZE_OPTIONS]}');
+    expect(source).toContain('function handlePageIndexChange(nextPageIndex: number)');
+    expect(source).toContain('function handlePageSizeChange(nextPageSize: number)');
+    expect(source).toContain('setPageIndex(0);');
+  });
+
+  it('keeps Angular no-selection batch delete warning before opening confirm', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('if (checkedIds.length === 0)');
+    expect(source).toContain("setEditorError(t('common.notify.no-select-delete'))");
+    expect(source).toContain('setEditorMessage(null)');
+    expect(source).toContain("setDeleteRequest({ kind: 'batch', ids: checkedIds })");
+  });
+
+  it('uses Angular edit notifications for enable toggle feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('async function handleToggleEnabled');
+    expect(source).toContain('updateAlertGroupEnabledFromFacade(api.alertGroups.update');
+    expect(source).toContain("setEditorMessage(t('common.notify.edit-success'))");
+    expect(source).toContain("setEditorError(t('common.notify.edit-fail'))");
+    expect(source).toContain("setEditorErrorContract('enable')");
+    expect(source).not.toContain("setEditorMessage(t('common.save-success'));\n            setEditorError(null);\n            setRefreshTick(value => value + 1);\n          } catch (error) {\n            setEditorError(error instanceof Error ? error.message : t('common.save-failed'))");
+  });
+
+  it('uses Angular create/edit notifications for editor save feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('const isEdit = Boolean(draft.id)');
+    expect(source).toContain('updateAlertGroupFromFacade(api.alertGroups.update, draft)');
+    expect(source).toContain('createAlertGroupFromFacade(api.alertGroups.create, draft)');
+    expect(source).toContain("setEditorMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'))");
+    expect(source).toContain("setEditorError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+    expect(source).toContain('setEditorErrorDetail(error instanceof Error ? error.message : null)');
+    expect(source).not.toContain("setEditorMessage(t('common.save-success'))");
+    expect(source).not.toContain("t('common.save-failed')");
+  });
+
+  it('uses Angular edit failure notifications when editor detail loading fails', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('async function handleEdit');
+    expect(source).toContain('const detail = await loadAlertGroupDetailFromFacade(api.alertGroups.detail, targetId)');
+    expect(source).toContain("setEditorError(error instanceof Error ? error.message : t('common.notify.edit-fail'))");
+    expect(source).not.toContain('loadAlertGroupDetail(apiMessageGet, targetId)');
+    expect(source).not.toContain("t('common.load-failed')");
+  });
+
+  it('validates Angular required timer fields before saving alert groups', () => {
+    const source = readFileSync(resolve(process.cwd(), 'lib/alert-group/view-model.ts'), 'utf8');
+
+    expect(source).toContain('if (!draft.groupWait.trim())');
+    expect(source).toContain("return t('alert.group.validation.group-wait')");
+    expect(source).toContain('if (!draft.groupInterval.trim())');
+    expect(source).toContain("return t('alert.group.validation.group-interval')");
+    expect(source).toContain('if (!draft.repeatInterval.trim())');
+    expect(source).toContain("return t('alert.group.validation.repeat-interval')");
+  });
+
+  it('validates Angular non-negative timer fields before saving alert groups', () => {
+    const source = readFileSync(resolve(process.cwd(), 'lib/alert-group/view-model.ts'), 'utf8');
+
+    expect(source).toContain('function isNonNegativeTimer(value: string)');
+    expect(source).toContain('Number.parseInt(value, 10)');
+    expect(source).toContain('return Number.isFinite(parsed) && parsed >= 0');
+    expect(source).toContain("return t('alert.group.validation.group-wait-non-negative')");
+    expect(source).toContain("return t('alert.group.validation.group-interval-non-negative')");
+    expect(source).toContain("return t('alert.group.validation.repeat-interval-non-negative')");
+  });
+
+  it('uses Angular delete notifications for confirmed delete feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/group/alert-group-page.tsx'), 'utf8');
+
+    expect(source).toContain('async function handleConfirmedDelete()');
+    expect(source).toContain('deleteAlertGroupsFromFacade(api.alertGroups.delete, request.ids)');
+    expect(source).toContain('deleteAlertGroupFromFacade(api.alertGroups.delete, request.ids[0])');
+    expect(source).toContain("setEditorMessage(t('common.notify.delete-success'))");
+    expect(source).toContain("setEditorError(t('common.notify.delete-fail'))");
+    expect(source).toContain("setEditorErrorContract('delete')");
+    expect(source).not.toContain("setEditorMessage(t('common.delete-success'))");
+    expect(source).not.toContain("t('common.delete-failed')");
   });
 });
diff --git a/web-next/app/alert/group/page.tsx b/web-next/app/alert/group/page.tsx
index 294e4dacbe..9eb1c9c906 100644
--- a/web-next/app/alert/group/page.tsx
+++ b/web-next/app/alert/group/page.tsx
@@ -1,227 +1,14 @@
-'use client';
-
-import React, { useCallback, useMemo, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ClientWorkbench } from '../../../components/workbench/client-workbench';
-import { useI18n } from '../../../components/providers/i18n-provider';
-import { AlertGroupSurface } from '../../../components/pages/alert-group-surface';
-import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '../../../lib/api-client';
-import {
-  createAlertGroup,
-  deleteAlertGroup,
-  deleteAlertGroups,
-  loadAlertGroupDetail,
-  updateAlertGroup,
-  type AlertGroupFormDraft
-} from '../../../lib/alert-group/controller';
-import { buildAlertGroupUrl } from '../../../lib/alert-group/query-state';
-import { buildAlertGroupEvidenceContext, buildAlertGroupFormDraft, validateAlertGroupForm } from '../../../lib/alert-group/view-model';
-import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptions } from '../../../lib/alert-label-options';
-import { formatTime } from '../../../lib/format';
-import { readSignalRouteContext } from '../../../lib/signal-route-context';
-import type { AlertGroupConverge, PageResult } from '../../../lib/types';
-
-type GroupDeleteRequest = {
-  kind: 'single' | 'batch';
-  ids: number[];
-};
-
-export default function AlertGroupPage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const signalContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
-  const signal = searchParams.get('signal');
-  const groupEvidenceContext = useMemo(
-    () => buildAlertGroupEvidenceContext(signal, signalContext, t),
-    [signal, signalContext, t]
-  );
-  const [search, setSearch] = useState('');
-  const [query, setQuery] = useState('');
-  const [selectedId, setSelectedId] = useState<number | null>(null);
-  const [editorOpen, setEditorOpen] = useState(false);
-  const [editorLoading, setEditorLoading] = useState(false);
-  const [editorSaving, setEditorSaving] = useState(false);
-  const [editorMessage, setEditorMessage] = useState<string | null>(null);
-  const [editorError, setEditorError] = useState<string | null>(null);
-  const [draft, setDraft] = useState<AlertGroupFormDraft>(() => buildAlertGroupFormDraft(null, groupEvidenceContext?.draftPatch));
-  const [refreshTick, setRefreshTick] = useState(0);
-  const [checkedIds, setCheckedIds] = useState<number[]>([]);
-  const [deleteRequest, setDeleteRequest] = useState<GroupDeleteRequest | null>(null);
-  const [deletePending, setDeletePending] = useState(false);
-
-  const load = useCallback(async () => {
-    const [list, labelOptions] = await Promise.all([
-      apiMessageGet<PageResult<AlertGroupConverge>>(buildAlertGroupUrl(query)),
-      loadAlertLabelOptions(apiMessageGet).catch(() => DEFAULT_ALERT_LABEL_OPTIONS)
-    ]);
-    return { list, labelOptions, refreshTick };
-  }, [query, refreshTick]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('alert.group.loading')}>
-      {data => {
-        const labelOptions = data.labelOptions ?? DEFAULT_ALERT_LABEL_OPTIONS;
-        const selected = data.list.content.find(item => item.id === selectedId) ?? data.list.content[0] ?? null;
-
-        async function handleNew() {
-          setDraft(buildAlertGroupFormDraft(null, groupEvidenceContext?.draftPatch));
-          setEditorError(null);
-          setEditorMessage(null);
-          setEditorOpen(true);
-        }
-
-        async function handleEdit(groupId?: number) {
-          const targetId = groupId ?? selected?.id;
-          if (!targetId) return;
-          setEditorLoading(true);
-          setEditorError(null);
-          setEditorMessage(null);
-          try {
-            const detail = await loadAlertGroupDetail(apiMessageGet, targetId);
-            setDraft(buildAlertGroupFormDraft(detail));
-            setEditorOpen(true);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.load-failed'));
-          } finally {
-            setEditorLoading(false);
-          }
-        }
-
-        async function handleSave() {
-          const validationError = validateAlertGroupForm(draft, t);
-          if (validationError) {
-            setEditorError(validationError);
-            return;
-          }
-          setEditorSaving(true);
-          setEditorError(null);
-          setEditorMessage(null);
-          try {
-            if (draft.id) {
-              await updateAlertGroup(apiMessagePut as any, draft);
-            } else {
-              await createAlertGroup(apiMessagePost as any, draft);
-            }
-            setEditorMessage(t('common.save-success'));
-            setEditorOpen(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setEditorSaving(false);
-          }
-        }
-
-        async function handleToggleEnabled(group?: AlertGroupConverge | null) {
-          const target = group ?? selected;
-          if (!target) return;
-          try {
-            await updateAlertGroup(apiMessagePut as any, {
-              ...buildAlertGroupFormDraft(target),
-              enable: !(target.enable ?? true),
-            });
-            setEditorMessage(t('common.save-success'));
-            setEditorError(null);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.save-failed'));
-          }
-        }
-
-        async function handleDelete(groupId?: number) {
-          const targetId = groupId ?? selected?.id;
-          if (!targetId) return;
-          setDeleteRequest({ kind: 'single', ids: [targetId] });
-        }
-
-        async function handleConfirmedDelete() {
-          const request = deleteRequest;
-          if (!request || request.ids.length === 0) return;
-          setDeletePending(true);
-          try {
-            if (request.kind === 'batch') {
-              await deleteAlertGroups(apiMessageDelete as any, request.ids);
-              setCheckedIds([]);
-            } else {
-              await deleteAlertGroup(apiMessageDelete as any, request.ids[0]);
-            }
-            setSelectedId(null);
-            setEditorOpen(false);
-            setEditorMessage(t('common.delete-success'));
-            setEditorError(null);
-            setDeleteRequest(null);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.delete-failed'));
-          } finally {
-            setDeletePending(false);
-          }
-        }
-
-        function handleRefresh() {
-          setRefreshTick(value => value + 1);
-        }
-
-        async function handleDeleteSelected() {
-          if (checkedIds.length === 0) return;
-          setDeleteRequest({ kind: 'batch', ids: checkedIds });
-        }
-
-        return (
-          <>
-            <AlertGroupSurface
-              t={t}
-              data={data}
-              search={search}
-              selectedId={selectedId}
-              checkedIds={checkedIds}
-              editorOpen={editorOpen}
-              editorLoading={editorLoading}
-              editorSaving={editorSaving}
-              editorMessage={editorMessage}
-              editorError={editorError}
-              evidenceContext={groupEvidenceContext}
-              draft={draft}
-              formatTime={formatTime}
-              labelOptions={labelOptions}
-              onSearchChange={setSearch}
-              onApplyFilter={() => setQuery(search)}
-              onClearFilter={() => {
-                setSearch('');
-                setQuery('');
-              }}
-              onRefresh={handleRefresh}
-              onSelect={setSelectedId}
-              onCheckedIdsChange={setCheckedIds}
-              onNew={() => void handleNew()}
-              onSave={() => void handleSave()}
-              onToggleEnabled={group => void handleToggleEnabled(group)}
-              onEdit={groupId => void handleEdit(groupId)}
-              onDelete={groupId => void handleDelete(groupId)}
-              onDeleteSelected={() => void handleDeleteSelected()}
-              onCloseEditor={() => setEditorOpen(false)}
-              onDraftChange={setDraft}
-            />
-            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
-              <ColdConfirmDialog
-                open={Boolean(deleteRequest)}
-                title={deleteRequest?.kind === 'batch' ? t('common.confirm.delete-batch') : t('common.confirm.delete')}
-                copy={
-                  deleteRequest?.kind === 'batch'
-                    ? t('alert.group.delete.confirm.batch', { count: deleteRequest.ids.length })
-                    : t('alert.group.delete.confirm.single')
-                }
-                confirmLabel={t('common.button.ok')}
-                cancelLabel={t('common.button.cancel')}
-                pending={deletePending}
-                onCancel={() => setDeleteRequest(null)}
-                onConfirm={() => void handleConfirmedDelete()}
-              />
-            </div>
-          </>
-        );
-      }}
-    </ClientWorkbench>
-  );
+import React from 'react';
+
+import AlertGroupPage from './alert-group-page';
+import { readAlertGroupRouteState, type AlertGroupSearchParams } from '../../../lib/alert-group/query-state';
+
+export default async function AlertGroupRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<AlertGroupSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readAlertGroupRouteState(resolvedSearchParams);
+  return <AlertGroupPage initialRouteState={routeState} />;
 }
diff --git a/web-next/app/alert/inhibit/alert-inhibit-page.tsx b/web-next/app/alert/inhibit/alert-inhibit-page.tsx
new file mode 100644
index 0000000000..cbae8f24f5
--- /dev/null
+++ b/web-next/app/alert/inhibit/alert-inhibit-page.tsx
@@ -0,0 +1,456 @@
+'use client';
+
+import React from 'react';
+import { useCallback, useMemo, useState } from 'react';
+import { ClientWorkbench } from '../../../components/workbench/client-workbench';
+import { useI18n } from '../../../components/providers/i18n-provider';
+import { AlertInhibitSurface } from '../../../components/pages/alert-inhibit-surface';
+import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
+import { api } from '../../../lib/alert-api-facade';
+import {
+  buildAlertInhibitEntityPrefillFromFacade,
+  buildAlertInhibitFormDraft,
+  createAlertInhibitFromFacade,
+  deleteAlertInhibitFromFacade,
+  deleteAlertInhibitsFromFacade,
+  loadAlertInhibitDataFromFacade,
+  loadAlertInhibitDetailFromFacade,
+  loadMatchedAlertInhibitsFromFacade,
+  updateAlertInhibitEnabledFromFacade,
+  updateAlertInhibitFromFacade,
+  type AlertInhibitFormDraft
+} from '../../../lib/alert-inhibit/controller';
+import { ALERT_INHIBIT_PAGE_SIZE_OPTIONS, buildAlertInhibitUrl, type AlertInhibitRouteState } from '../../../lib/alert-inhibit/query-state';
+import { buildAlertInhibitEvidenceContext, validateAlertInhibitForm } from '../../../lib/alert-inhibit/view-model';
+import { clearAlertInhibitEqualLabels, clearAlertInhibitTarget, copyAlertInhibitSourceToTarget, dropSeverityFromAlertInhibitTarget } from '../../../lib/alert-manage/view-model';
+import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptionsFromFacade } from '../../../lib/alert-label-options';
+import { formatTime } from '../../../lib/format';
+import type { AlertInhibit, PageResult } from '../../../lib/types';
+import type { AlertInhibitManagementContext } from '../../../lib/alert-inhibit/query-state';
+
+type InhibitDeleteRequest = {
+  kind: 'single' | 'batch';
+  ids: number[];
+};
+
+const ALERT_INHIBIT_SETTLED_CACHE_TTL_MS = 10_000;
+const ALERT_INHIBIT_LABEL_OPTIONS_TIMEOUT_MS = 2_500;
+const EMPTY_ALERT_INHIBIT_ROUTE_STATE: AlertInhibitRouteState = {
+  returnContext: {
+    search: '',
+    status: '',
+    severity: '',
+    entityId: '',
+    entityName: '',
+    returnTo: ''
+  },
+  signal: null,
+  signalContext: {},
+  managementContext: {
+    entityId: '',
+    entityName: '',
+    returnTo: '',
+    returnLabel: '',
+    matchMode: '',
+    matchingRuleType: '',
+    matchingRuleIds: [],
+    matchedViewEnabled: false
+  }
+};
+
+function filterMatchedInhibitsBySearch(inhibits: AlertInhibit[], search: string): AlertInhibit[] {
+  const keyword = search.trim().toLowerCase();
+  if (!keyword) return inhibits;
+  return inhibits.filter(inhibit => {
+    if (inhibit.name?.toLowerCase().includes(keyword)) {
+      return true;
+    }
+    const sourceHit = Object.entries(inhibit.sourceLabels || {}).some(([key, value]) => `${key}:${value}`.toLowerCase().includes(keyword));
+    const targetHit = Object.entries(inhibit.targetLabels || {}).some(([key, value]) => `${key}:${value}`.toLowerCase().includes(keyword));
+    const equalHit = (inhibit.equalLabels || []).some(label => label.toLowerCase().includes(keyword));
+    return sourceHit || targetHit || equalHit;
+  });
+}
+
+function paginateMatchedInhibits(inhibits: AlertInhibit[], pageIndex: number, pageSize: number): PageResult<AlertInhibit> {
+  const normalizedPageSize = Math.max(1, pageSize);
+  const lastPageIndex = Math.max(0, Math.ceil(inhibits.length / normalizedPageSize) - 1);
+  const normalizedPageIndex = Math.min(Math.max(0, pageIndex), lastPageIndex);
+  const start = normalizedPageIndex * normalizedPageSize;
+  return {
+    content: inhibits.slice(start, start + normalizedPageSize),
+    totalElements: inhibits.length,
+    pageIndex: normalizedPageIndex,
+    pageSize: normalizedPageSize
+  };
+}
+
+function shouldUseMatchedInhibitView(context: AlertInhibitManagementContext, matchedViewEnabled: boolean) {
+  return matchedViewEnabled && context.matchMode === 'entity-noise-controls';
+}
+
+function withTimeoutFallback<T>(promise: Promise<T>, fallback: T, timeoutMs: number): Promise<T> {
+  return new Promise(resolve => {
+    const timer = setTimeout(() => resolve(fallback), timeoutMs);
+    promise.then(
+      value => {
+        clearTimeout(timer);
+        resolve(value);
+      },
+      () => {
+        clearTimeout(timer);
+        resolve(fallback);
+      }
+    );
+  });
+}
+
+export default function AlertInhibitPage({ initialRouteState }: { initialRouteState?: AlertInhibitRouteState } = {}) {
+  const { t } = useI18n();
+  const alertInhibitRouteState = initialRouteState ?? EMPTY_ALERT_INHIBIT_ROUTE_STATE;
+  const { returnContext, signal, signalContext, managementContext } = alertInhibitRouteState;
+  const inhibitEvidenceContext = useMemo(
+    () => buildAlertInhibitEvidenceContext(signal, signalContext, t),
+    [signal, signalContext, t]
+  );
+  const [search, setSearch] = useState('');
+  const [query, setQuery] = useState('');
+  const [pageIndex, setPageIndex] = useState(0);
+  const [pageSize, setPageSize] = useState<number>(ALERT_INHIBIT_PAGE_SIZE_OPTIONS[0]);
+  const [selectedId, setSelectedId] = useState<number | null>(null);
+  const [editorOpen, setEditorOpen] = useState(false);
+  const [editorLoading, setEditorLoading] = useState(false);
+  const [editorSaving, setEditorSaving] = useState(false);
+  const [editorMessage, setEditorMessage] = useState<string | null>(null);
+  const [editorError, setEditorError] = useState<string | null>(null);
+  const [editorErrorDetail, setEditorErrorDetail] = useState<string | null>(null);
+  const [editorErrorContract, setEditorErrorContract] = useState<'save' | 'enable' | null>(null);
+  const [draft, setDraft] = useState<AlertInhibitFormDraft>(() => buildAlertInhibitFormDraft(null, inhibitEvidenceContext?.draftPatch));
+  const [refreshTick, setRefreshTick] = useState(0);
+  const [checkedIds, setCheckedIds] = useState<number[]>([]);
+  const [deleteRequest, setDeleteRequest] = useState<InhibitDeleteRequest | null>(null);
+  const [deletePending, setDeletePending] = useState(false);
+  const [matchedViewEnabled, setMatchedViewEnabled] = useState(managementContext.matchedViewEnabled);
+  const [createdOutsideMatchedViewNotice, setCreatedOutsideMatchedViewNotice] = useState(false);
+  const [entityPrefillSource, setEntityPrefillSource] = useState<'alerts-common-labels' | 'none'>('none');
+  const [entityPrefillWarning, setEntityPrefillWarning] = useState<string | null>(null);
+  const alertInhibitListUrl = useMemo(() => buildAlertInhibitUrl({ search: query, pageIndex, pageSize }), [pageIndex, pageSize, query]);
+  const matchedRuleIdsKey = managementContext.matchingRuleIds.join(',');
+  const useMatchedView = shouldUseMatchedInhibitView(managementContext, matchedViewEnabled);
+  const alertInhibitCacheKey = useMemo(
+    () => ['alert-inhibit', useMatchedView ? `matched:${matchedRuleIdsKey}` : alertInhibitListUrl, refreshTick].join('|'),
+    [alertInhibitListUrl, matchedRuleIdsKey, refreshTick, useMatchedView]
+  );
+
+  const load = useCallback(async () => {
+    const labelOptionsPromise = withTimeoutFallback(
+      loadAlertLabelOptionsFromFacade(api.alertLabels.list),
+      DEFAULT_ALERT_LABEL_OPTIONS,
+      ALERT_INHIBIT_LABEL_OPTIONS_TIMEOUT_MS
+    );
+    if (useMatchedView) {
+      const [matchedResult, labelOptions] = await Promise.all([
+        loadMatchedAlertInhibitsFromFacade(api.alertInhibits.detail, managementContext.matchingRuleIds),
+        labelOptionsPromise
+      ]);
+      const filtered = filterMatchedInhibitsBySearch(matchedResult.matched, query);
+      return {
+        list: paginateMatchedInhibits(filtered, pageIndex, pageSize),
+        labelOptions,
+        refreshTick,
+        missingMatchedRuleCount: matchedResult.missingMatchedRuleCount
+      };
+    }
+
+    const data = await loadAlertInhibitDataFromFacade(
+      {
+        list: api.alertInhibits.list,
+        labelOptions: () => labelOptionsPromise
+      },
+      { search: query, pageIndex, pageSize }
+    );
+    return { ...data, refreshTick, missingMatchedRuleCount: 0 };
+  }, [managementContext.matchingRuleIds, pageIndex, pageSize, query, refreshTick, useMatchedView]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('alert.inhibit.loading')}
+      cacheKey={alertInhibitCacheKey}
+      cacheSettledTtlMs={ALERT_INHIBIT_SETTLED_CACHE_TTL_MS}
+    >
+      {data => {
+        const labelOptions = data.labelOptions ?? DEFAULT_ALERT_LABEL_OPTIONS;
+        const selected = data.list.content.find(item => item.id === selectedId) ?? data.list.content[0] ?? null;
+
+        async function handleNew() {
+          const displayName = managementContext.entityName || managementContext.returnLabel || managementContext.entityId || 'entity';
+          const entityContextDraft = managementContext.entityId || managementContext.entityName || managementContext.returnTo
+            ? { name: `${displayName} inhibit` }
+            : {};
+          const baseDraftPatch = inhibitEvidenceContext?.draftPatch ?? entityContextDraft;
+          setEntityPrefillSource('none');
+          setEntityPrefillWarning(null);
+          if (managementContext.entityId || managementContext.entityName || managementContext.returnTo) {
+            setEditorLoading(true);
+            try {
+              const prefill = await buildAlertInhibitEntityPrefillFromFacade(
+                entityId => api.entities.alerts(entityId, { pageIndex: 0, pageSize: 20, status: 'firing' }),
+                managementContext.entityId,
+                t('entity.noise-controls.authoring.inhibit.prefill-warning'),
+                t('entity.noise-controls.authoring.prefill-warning.no-entity-id')
+              );
+              setEntityPrefillSource(prefill.source);
+              setEntityPrefillWarning(prefill.warning);
+              setDraft(buildAlertInhibitFormDraft(null, { ...baseDraftPatch, ...prefill.draftPatch }));
+            } finally {
+              setEditorLoading(false);
+            }
+          } else {
+            setDraft(buildAlertInhibitFormDraft(null, baseDraftPatch));
+          }
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setEditorMessage(null);
+          setCreatedOutsideMatchedViewNotice(false);
+          setEditorOpen(true);
+        }
+
+        async function handleEdit(inhibitId?: number) {
+          const targetId = inhibitId ?? selected?.id;
+          if (!targetId) return;
+          setEditorLoading(true);
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setEditorMessage(null);
+          try {
+            const detail = await loadAlertInhibitDetailFromFacade(api.alertInhibits.detail, targetId);
+            setDraft(buildAlertInhibitFormDraft(detail));
+            setEditorOpen(true);
+          } catch (error) {
+            setEditorError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+          } finally {
+            setEditorLoading(false);
+          }
+        }
+
+        async function handleSave() {
+          const validationError = validateAlertInhibitForm(draft, t);
+          if (validationError) {
+            setEditorError(validationError);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            return;
+          }
+          setEditorSaving(true);
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setEditorMessage(null);
+          const isEdit = Boolean(draft.id);
+          try {
+            if (isEdit) {
+              await updateAlertInhibitFromFacade(api.alertInhibits.update, draft);
+            } else {
+              await createAlertInhibitFromFacade(api.alertInhibits.create, draft);
+            }
+            setEditorMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
+            setEditorOpen(false);
+            setCreatedOutsideMatchedViewNotice(!isEdit && useMatchedView);
+            setRefreshTick(value => value + 1);
+          } catch (error) {
+            setEditorError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('save');
+          } finally {
+            setEditorSaving(false);
+          }
+        }
+
+        async function handleToggleEnabled(inhibit?: AlertInhibit) {
+          const target = inhibit ?? selected;
+          if (!target) return;
+          try {
+            await updateAlertInhibitEnabledFromFacade(api.alertInhibits.update, target, !(target.enable ?? true));
+            setEditorMessage(t('common.notify.edit-success'));
+            setEditorError(null);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            setRefreshTick(value => value + 1);
+          } catch (error) {
+            setEditorError(t('common.notify.edit-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('enable');
+          }
+        }
+
+        async function handleDelete(inhibitId?: number) {
+          const targetId = inhibitId ?? selected?.id;
+          if (!targetId) return;
+          setDeleteRequest({ kind: 'single', ids: [targetId] });
+        }
+
+        async function handleConfirmedDelete() {
+          const request = deleteRequest;
+          if (!request || request.ids.length === 0) return;
+          setDeletePending(true);
+          try {
+            if (request.kind === 'batch') {
+              await deleteAlertInhibitsFromFacade(api.alertInhibits.delete, request.ids);
+              setCheckedIds([]);
+            } else {
+              await deleteAlertInhibitFromFacade(api.alertInhibits.delete, request.ids[0]);
+            }
+            const nextTotal = Math.max((data.list.totalElements || 0) - request.ids.length, 0);
+            const nextLastPageIndex = Math.max(0, Math.ceil(nextTotal / pageSize) - 1);
+            setPageIndex(value => Math.min(value, nextLastPageIndex));
+            setSelectedId(null);
+            setEditorOpen(false);
+            setEditorMessage(t('common.notify.delete-success'));
+            setEditorError(null);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            setDeleteRequest(null);
+            setRefreshTick(value => value + 1);
+          } catch (error) {
+            setEditorError(t('common.notify.delete-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('delete');
+          } finally {
+            setDeletePending(false);
+          }
+        }
+
+        function handleRefresh() {
+          setRefreshTick(value => value + 1);
+        }
+
+        async function handleDeleteSelected() {
+          if (checkedIds.length === 0) {
+            setEditorError(t('common.notify.no-select-delete'));
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            setEditorMessage(null);
+            return;
+          }
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setDeleteRequest({ kind: 'batch', ids: checkedIds });
+        }
+
+        function handleApplyFilter() {
+          setQuery(search);
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handleClearFilter() {
+          setSearch('');
+          setQuery('');
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handlePageIndexChange(nextPageIndex: number) {
+          setPageIndex(nextPageIndex);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handlePageSizeChange(nextPageSize: number) {
+          setPageSize(nextPageSize);
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        return (
+          <>
+            <AlertInhibitSurface
+              t={t}
+              data={data}
+              search={search}
+              selectedId={selectedId}
+              checkedIds={checkedIds}
+              editorOpen={editorOpen}
+              editorLoading={editorLoading}
+              editorSaving={editorSaving}
+              editorMessage={editorMessage}
+              editorError={editorError}
+              editorErrorDetail={editorErrorDetail}
+              editorErrorContract={editorErrorContract}
+              returnContext={returnContext}
+              managementContext={managementContext}
+              matchedViewEnabled={useMatchedView}
+              missingMatchedRuleCount={data.missingMatchedRuleCount ?? 0}
+              createdOutsideMatchedViewNotice={createdOutsideMatchedViewNotice}
+              entityPrefillSource={entityPrefillSource}
+              entityPrefillWarning={entityPrefillWarning}
+              evidenceContext={inhibitEvidenceContext}
+              draft={draft}
+              labelOptions={labelOptions}
+              formatTime={formatTime}
+              onSearchChange={setSearch}
+              onApplyFilter={handleApplyFilter}
+              onClearFilter={handleClearFilter}
+              onRefresh={handleRefresh}
+              onSelect={setSelectedId}
+              onCheckedIdsChange={setCheckedIds}
+              pageSizeOptions={[...ALERT_INHIBIT_PAGE_SIZE_OPTIONS]}
+              onPageIndexChange={handlePageIndexChange}
+              onPageSizeChange={handlePageSizeChange}
+              onViewAllRules={() => {
+                setMatchedViewEnabled(false);
+                setCreatedOutsideMatchedViewNotice(false);
+                setPageIndex(0);
+                setSelectedId(null);
+                setCheckedIds([]);
+              }}
+              onViewMatchedRules={() => {
+                setMatchedViewEnabled(true);
+                setPageIndex(0);
+                setSelectedId(null);
+                setCheckedIds([]);
+              }}
+              onNew={() => void handleNew()}
+              onEdit={inhibitId => void handleEdit(inhibitId)}
+              onSave={() => void handleSave()}
+              onToggleEnabled={inhibit => void handleToggleEnabled(inhibit)}
+              onDelete={inhibitId => void handleDelete(inhibitId)}
+              onDeleteSelected={() => void handleDeleteSelected()}
+              onCloseEditor={() => setEditorOpen(false)}
+              onDraftChange={setDraft}
+              onCopySourceToTarget={() => setDraft(prev => copyAlertInhibitSourceToTarget(prev))}
+              onDropSeverity={() => setDraft(prev => dropSeverityFromAlertInhibitTarget(prev))}
+              onClearTarget={() => setDraft(prev => clearAlertInhibitTarget(prev))}
+              onClearEqual={() => setDraft(prev => clearAlertInhibitEqualLabels(prev))}
+            />
+            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
+              <ColdConfirmDialog
+                open={Boolean(deleteRequest)}
+                title={deleteRequest?.kind === 'batch' ? t('common.confirm.delete-batch') : t('common.confirm.delete')}
+                copy={
+                  deleteRequest?.kind === 'batch'
+                    ? t('alert.inhibit.delete.confirm.batch', { count: deleteRequest.ids.length })
+                    : t('alert.inhibit.delete.confirm.single')
+                }
+                confirmLabel={t('common.button.ok')}
+                cancelLabel={t('common.button.cancel')}
+                pending={deletePending}
+                onCancel={() => setDeleteRequest(null)}
+                onConfirm={() => void handleConfirmedDelete()}
+              />
+            </div>
+          </>
+        );
+      }}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/alert/inhibit/page.test.tsx b/web-next/app/alert/inhibit/page.test.tsx
index 671620c63a..7865540998 100644
--- a/web-next/app/alert/inhibit/page.test.tsx
+++ b/web-next/app/alert/inhibit/page.test.tsx
@@ -1,10 +1,12 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import type { AlertInhibitRouteState } from '../../../lib/alert-inhibit/query-state';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   renderData: {
     list: {
@@ -28,13 +30,6 @@ const mockState = vi.hoisted(() => ({
 
 const apiMessageGet = vi.hoisted(() => vi.fn());
 
-vi.mock('next/navigation', () => ({
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
-}));
-
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock()
@@ -44,13 +39,19 @@ vi.mock('../../../components/providers/i18n-provider', () => ({
 vi.mock('../../../components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return (
+      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
+        {children(mockState.renderData)}
+      </div>
+    );
   }
 }));
 
@@ -58,18 +59,39 @@ vi.mock('../../../components/pages/alert-inhibit-surface', () => ({
   AlertInhibitSurface: ({
     data,
     returnContext,
+    managementContext,
+    matchedViewEnabled,
+    missingMatchedRuleCount,
+    createdOutsideMatchedViewNotice,
+    entityPrefillSource,
+    entityPrefillWarning,
     evidenceContext,
-    draft
+    draft,
+    pageSizeOptions
   }: {
     data: { list: { totalElements: number } };
     returnContext?: Record<string, string>;
+    managementContext?: { matchMode?: string; matchingRuleIds?: number[] };
+    matchedViewEnabled?: boolean;
+    missingMatchedRuleCount?: number;
+    createdOutsideMatchedViewNotice?: boolean;
+    entityPrefillSource?: string;
+    entityPrefillWarning?: string | null;
     evidenceContext?: { signal: string; sourceLabelsText: string; returnHref?: string } | null;
     draft?: { sourceLabelsText?: string; targetLabelsText?: string; equalLabelsText?: string };
+    pageSizeOptions?: number[];
   }) => (
     <div
       data-alert-inhibit-surface="true"
       data-total={data.list.totalElements}
+      data-page-size-options={pageSizeOptions?.join('|')}
       data-return-context={JSON.stringify(returnContext ?? {})}
+      data-management-context={JSON.stringify(managementContext ?? {})}
+      data-alert-inhibit-match-view={matchedViewEnabled ? 'matched' : 'all'}
+      data-alert-inhibit-missing-rule-count={missingMatchedRuleCount ?? 0}
+      data-alert-inhibit-created-outside-matched={createdOutsideMatchedViewNotice ? 'true' : 'false'}
+      data-alert-inhibit-entity-prefill-source={entityPrefillSource ?? 'none'}
+      data-alert-inhibit-entity-prefill-warning={entityPrefillWarning ?? ''}
       data-alert-inhibit-evidence-context={evidenceContext ? 'signal-route' : 'none'}
       data-alert-inhibit-evidence-signal={evidenceContext?.signal ?? ''}
       data-alert-inhibit-evidence-return={evidenceContext?.returnHref ?? ''}
@@ -88,18 +110,46 @@ vi.mock('../../../lib/api-client', () => ({
   apiMessagePut: vi.fn()
 }));
 
+const EMPTY_ROUTE_STATE: AlertInhibitRouteState = {
+  returnContext: {
+    search: '',
+    status: '',
+    severity: '',
+    entityId: '',
+    entityName: '',
+    returnTo: ''
+  },
+  signal: null,
+  signalContext: {},
+  managementContext: {
+    entityId: '',
+    entityName: '',
+    returnTo: '',
+    returnLabel: '',
+    matchMode: '',
+    matchingRuleType: '',
+    matchingRuleIds: [],
+    matchedViewEnabled: false
+  }
+};
+
+async function renderAlertInhibitPage(initialRouteState: AlertInhibitRouteState = EMPTY_ROUTE_STATE) {
+  const { default: AlertInhibitPage } = await import('./alert-inhibit-page');
+  return renderToStaticMarkup(<AlertInhibitPage initialRouteState={initialRouteState} />);
+}
+
 describe('alert inhibit page', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams();
     mockState.lastLoad = null;
     apiMessageGet.mockClear().mockResolvedValue(mockState.renderData.list);
   });
 
   it('loads the inhibit workbench through the shared query and surface contracts', async () => {
-    const { default: AlertInhibitPage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertInhibitPage />);
+    const html = await renderAlertInhibitPage();
 
     expect(html).toContain('data-alert-inhibit-surface="true"');
+    expect(html).toContain('data-page-size-options="8|15|25"');
+    expect(html).toContain('data-loading-copy="Loading inhibit rules"');
 
     await mockState.lastLoad?.();
 
@@ -109,23 +159,47 @@ describe('alert inhibit page', () => {
   it('passes topology edge return context into the inhibit surface', async () => {
     const returnTo =
       '/topology?viewMode=resource-dependency&sourceKind=database-middleware-connection&edgeId=svc-checkout--res-orders-db&environment=prod&timeRange=last-1h';
-    mockState.searchParams = new URLSearchParams({
-      source: 'topology',
-      viewMode: 'resource-dependency',
-      sourceKind: 'database-middleware-connection',
-      edgeId: 'svc-checkout--res-orders-db',
-      entityId: 'service:commerce/checkout',
-      entityName: 'checkout-api',
-      serviceName: 'checkout-api',
-      serviceNamespace: 'commerce',
-      environment: 'prod',
-      timeRange: 'last-1h',
-      returnTo: `${returnTo}&returnLabel=HertzBeat%20%E4%BC%81%E4%B8%9A%E8%BF%90%E7%BB%B4%E6%8B%93%E6%89%91`,
-      returnLabel: 'HertzBeat 企业运维拓扑'
-    });
+    const initialRouteState: AlertInhibitRouteState = {
+      returnContext: {
+        search: 'checkout-api',
+        status: 'firing',
+        severity: '',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        returnTo,
+        serviceName: 'checkout-api',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'topology',
+        viewMode: 'resource-dependency',
+        sourceKind: 'database-middleware-connection',
+        edgeId: 'svc-checkout--res-orders-db'
+      },
+      signal: null,
+      managementContext: {
+        entityId: '',
+        entityName: '',
+        returnTo: '',
+        returnLabel: '',
+        matchMode: '',
+        matchingRuleType: '',
+        matchingRuleIds: [],
+        matchedViewEnabled: false
+      },
+      signalContext: {
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout-api',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'topology',
+        returnTo
+      }
+    };
 
-    const { default: AlertInhibitPage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertInhibitPage />);
+    const html = await renderAlertInhibitPage(initialRouteState);
 
     expect(html).toContain('&quot;edgeId&quot;:&quot;svc-checkout--res-orders-db&quot;');
     expect(html).toContain('&quot;viewMode&quot;:&quot;resource-dependency&quot;');
@@ -136,23 +210,51 @@ describe('alert inhibit page', () => {
   });
 
   it('preserves three-signal evidence context into new inhibit authoring', async () => {
-    mockState.searchParams = new URLSearchParams({
+    const initialRouteState: AlertInhibitRouteState = {
       signal: 'traces',
-      source: 'otlp',
-      entityId: 'service:commerce/checkout',
-      entityName: 'checkout-api',
-      serviceName: 'checkout',
-      serviceNamespace: 'commerce',
-      environment: 'prod',
-      traceId: 'trace-123',
-      spanId: 'span-456',
-      collector: 'edge-collector-a',
-      template: 'java-service',
-      returnTo: '/trace/manage?traceId=trace-123&returnLabel=%E9%93%BE%E8%B7%AF%E5%B7%A5%E4%BD%9C%E5%8F%B0'
-    });
+      returnContext: {
+        search: 'checkout',
+        status: 'firing',
+        severity: '',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        returnTo: '/trace/manage?traceId=trace-123',
+        serviceName: 'checkout',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        source: 'otlp',
+        signal: 'traces',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        collector: 'edge-collector-a',
+        template: 'java-service'
+      },
+      managementContext: {
+        entityId: '',
+        entityName: '',
+        returnTo: '',
+        returnLabel: '',
+        matchMode: '',
+        matchingRuleType: '',
+        matchingRuleIds: [],
+        matchedViewEnabled: false
+      },
+      signalContext: {
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        source: 'otlp',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        collector: 'edge-collector-a',
+        template: 'java-service',
+        returnTo: '/trace/manage?traceId=trace-123'
+      }
+    };
 
-    const { default: AlertInhibitPage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertInhibitPage />);
+    const html = await renderAlertInhibitPage(initialRouteState);
 
     expect(html).toContain('data-alert-inhibit-evidence-context="signal-route"');
     expect(html).toContain('data-alert-inhibit-evidence-signal="traces"');
@@ -166,5 +268,201 @@ describe('alert inhibit page', () => {
     expect(html).toContain('data-alert-inhibit-draft-target-labels="hertzbeat.signal:traces');
     expect(html).toContain('data-alert-inhibit-draft-equal-labels="hertzbeat.entity.id, service.name, service.namespace, deployment.environment"');
     expect(html).not.toContain('returnLabel=');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+    expect(source).not.toContain('useSearchParams');
+    expect(source).not.toContain('queryStateFromParams(searchParams)');
+    expect(source).not.toContain('readSignalRouteContext(searchParams)');
+    expect(source).toContain('const alertInhibitRouteState = initialRouteState ?? EMPTY_ALERT_INHIBIT_ROUTE_STATE');
+  });
+
+  it('keeps alert inhibit remounts on a short settled cache window with refresh-tick invalidation', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+
+    expect(source).toContain('ALERT_INHIBIT_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('ALERT_INHIBIT_LABEL_OPTIONS_TIMEOUT_MS = 2_500');
+    expect(source).toContain('withTimeoutFallback(');
+    expect(source).toContain('const [refreshTick, setRefreshTick] = useState(0)');
+    expect(source).toContain('const [pageIndex, setPageIndex] = useState(0)');
+    expect(source).toContain('const [pageSize, setPageSize] = useState<number>(ALERT_INHIBIT_PAGE_SIZE_OPTIONS[0])');
+    expect(source).toContain("['alert-inhibit', useMatchedView ? `matched:${matchedRuleIdsKey}` : alertInhibitListUrl, refreshTick].join('|')");
+    expect(source).toContain('buildAlertInhibitUrl({ search: query, pageIndex, pageSize })');
+    expect(source).toContain('[pageIndex, pageSize, query]');
+    expect(source).toContain('[alertInhibitListUrl, matchedRuleIdsKey, refreshTick, useMatchedView]');
+    expect(source).toContain('loadAlertInhibitDataFromFacade');
+    expect(source).toContain('list: api.alertInhibits.list');
+    expect(source).toContain('loadAlertLabelOptionsFromFacade(api.alertLabels.list)');
+    expect(source).toContain('loadMatchedAlertInhibitsFromFacade(api.alertInhibits.detail, managementContext.matchingRuleIds)');
+    expect(source).not.toContain('apiMessageGet<PageResult<AlertInhibit>>(alertInhibitListUrl)');
+    expect(source).toContain('return { ...data, refreshTick, missingMatchedRuleCount: 0 };');
+    expect(source.match(/setRefreshTick\(value => value \+ 1\)/g)?.length).toBeGreaterThanOrEqual(4);
+    expect(source).toContain('cacheKey={alertInhibitCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={ALERT_INHIBIT_SETTLED_CACHE_TTL_MS}');
+    expect(source).toContain('pageSizeOptions={[...ALERT_INHIBIT_PAGE_SIZE_OPTIONS]}');
+    expect(source).toContain('function handlePageIndexChange(nextPageIndex: number)');
+    expect(source).toContain('function handlePageSizeChange(nextPageSize: number)');
+    expect(source).toContain('setPageIndex(0);');
+  });
+
+  it('loads Angular entity-noise-control matched inhibit rules by id', async () => {
+    const initialRouteState: AlertInhibitRouteState = {
+      returnContext: {
+        search: '',
+        status: '',
+        severity: '',
+        entityId: '42',
+        entityName: 'checkout-api',
+        returnTo: '/entities/42?tab=alerts'
+      },
+      signal: null,
+      signalContext: {},
+      managementContext: {
+        entityId: '42',
+        entityName: 'checkout-api',
+        returnTo: '/entities/42?tab=alerts',
+        returnLabel: 'checkout-api',
+        matchMode: 'entity-noise-controls',
+        matchingRuleType: 'inhibit',
+        matchingRuleIds: [11, 12],
+        matchedViewEnabled: true
+      }
+    };
+    apiMessageGet.mockImplementation(async (url: string) => {
+      if (url === '/alert/inhibit/11') {
+        return {
+          id: 11,
+          name: 'checkout inhibit',
+          enable: true,
+          sourceLabels: { service: 'checkout' },
+          targetLabels: { service: 'orders' },
+          equalLabels: ['cluster']
+        };
+      }
+      if (url === '/alert/inhibit/12') {
+        throw new Error('missing');
+      }
+      return mockState.renderData.list;
+    });
+
+    const html = await renderAlertInhibitPage(initialRouteState);
+    const result = (await mockState.lastLoad?.()) as any;
+
+    expect(html).toContain('data-alert-inhibit-match-view="matched"');
+    expect(html).toContain('&quot;matchMode&quot;:&quot;entity-noise-controls&quot;');
+    expect(apiMessageGet).toHaveBeenCalledWith('/alert/inhibit/11');
+    expect(apiMessageGet).toHaveBeenCalledWith('/alert/inhibit/12');
+    expect(apiMessageGet).not.toHaveBeenCalledWith('/alert/inhibits?pageIndex=0&pageSize=8&sort=id&order=desc');
+    expect(result.list.content.map((item: any) => item.id)).toEqual([11]);
+    expect(result.missingMatchedRuleCount).toBe(1);
+  });
+
+  it('keeps Angular no-selection batch delete warning before opening confirm', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+    const surfaceSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-inhibit-surface.tsx'), 'utf8');
+
+    expect(source).toContain('if (checkedIds.length === 0)');
+    expect(source).toContain("setEditorError(t('common.notify.no-select-delete'))");
+    expect(source).toContain('setEditorMessage(null)');
+    expect(source).toContain("setDeleteRequest({ kind: 'batch', ids: checkedIds })");
+    expect(surfaceSource).toContain('data-alert-inhibit-delete-selected="toolbar"');
+    expect(surfaceSource).toContain('data-alert-inhibit-action-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(surfaceSource).not.toContain('disabled={selectedCount === 0}');
+  });
+
+  it('keeps Angular created-outside-matched notice state for matched-view authoring', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+    const surfaceSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-inhibit-surface.tsx'), 'utf8');
+
+    expect(source).toContain('const [createdOutsideMatchedViewNotice, setCreatedOutsideMatchedViewNotice] = useState(false)');
+    expect(source).toContain('setCreatedOutsideMatchedViewNotice(false)');
+    expect(source).toContain('createdOutsideMatchedViewNotice={createdOutsideMatchedViewNotice}');
+    expect(surfaceSource).toContain('data-alert-inhibit-created-outside-matched="angular-authoring-notice"');
+    expect(surfaceSource).toContain('data-alert-inhibit-created-outside-matched-owner="hertzbeat-ui-inline-feedback"');
+    expect(surfaceSource).toContain('data-alert-inhibit-created-outside-matched-action="view-all"');
+  });
+
+  it('keeps Angular entity-alert authoring prefill for entity noise-control inhibits', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+    const surfaceSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-inhibit-surface.tsx'), 'utf8');
+
+    expect(source).toContain('buildAlertInhibitEntityPrefillFromFacade(');
+    expect(source).toContain("entityId => api.entities.alerts(entityId, { pageIndex: 0, pageSize: 20, status: 'firing' })");
+    expect(source).not.toContain("from '../../../lib/api-client'");
+    expect(source).toContain("t('entity.noise-controls.authoring.inhibit.prefill-warning')");
+    expect(source).toContain("t('entity.noise-controls.authoring.prefill-warning.no-entity-id')");
+    expect(source).toContain('entityPrefillSource={entityPrefillSource}');
+    expect(source).toContain('entityPrefillWarning={entityPrefillWarning}');
+    expect(source).toContain("name: `${displayName} inhibit`");
+    expect(surfaceSource).toContain('data-alert-inhibit-entity-prefill={entityPrefillSource ===');
+    expect(surfaceSource).toContain("t('entity.noise-controls.authoring.inhibit.title')");
+    expect(surfaceSource).toContain("t('entity.noise-controls.authoring.inhibit.prefill-success')");
+    expect(surfaceSource).toContain('prefillWarning={entityPrefillWarning}');
+  });
+
+  it('uses Angular delete notifications for confirmed delete feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+
+    expect(source).toContain('async function handleConfirmedDelete()');
+    expect(source).toContain('deleteAlertInhibitsFromFacade(api.alertInhibits.delete, request.ids)');
+    expect(source).toContain('deleteAlertInhibitFromFacade(api.alertInhibits.delete, request.ids[0])');
+    expect(source).toContain("setEditorMessage(t('common.notify.delete-success'))");
+    expect(source).toContain("setEditorError(t('common.notify.delete-fail'))");
+    expect(source).toContain('setEditorErrorDetail(error instanceof Error ? error.message : null)');
+    expect(source).toContain("setEditorErrorContract('delete')");
+    expect(source).not.toContain('apiMessageDelete');
+    expect(source).not.toContain("setEditorMessage(t('common.delete-success'))");
+    expect(source).not.toContain("t('common.delete-failed')");
+  });
+
+  it('uses Angular edit notifications and facade persistence for enable-toggle feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+    const toggleSource = source.slice(
+      source.indexOf('async function handleToggleEnabled(inhibit?: AlertInhibit)'),
+      source.indexOf('async function handleDelete(inhibitId?: number)')
+    );
+
+    expect(toggleSource).toContain('async function handleToggleEnabled(inhibit?: AlertInhibit)');
+    expect(toggleSource).toContain('updateAlertInhibitEnabledFromFacade(api.alertInhibits.update, target, !(target.enable ?? true))');
+    expect(toggleSource).toContain("setEditorMessage(t('common.notify.edit-success'))");
+    expect(toggleSource).toContain("setEditorError(t('common.notify.edit-fail'))");
+    expect(toggleSource).toContain('setEditorErrorDetail(error instanceof Error ? error.message : null)');
+    expect(toggleSource).toContain("setEditorErrorContract('enable')");
+    expect(toggleSource).not.toContain('apiMessagePut');
+    expect(toggleSource).not.toContain("setEditorMessage(t('common.save-success'))");
+    expect(toggleSource).not.toContain("t('common.save-failed')");
+  });
+
+  it('uses Angular create/edit notifications and facade persistence for editor save feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+    const saveSource = source.slice(
+      source.indexOf('async function handleSave()'),
+      source.indexOf('async function handleToggleEnabled(inhibit?: AlertInhibit)')
+    );
+
+    expect(saveSource).toContain('const isEdit = Boolean(draft.id)');
+    expect(saveSource).toContain('updateAlertInhibitFromFacade(api.alertInhibits.update, draft)');
+    expect(saveSource).toContain('createAlertInhibitFromFacade(api.alertInhibits.create, draft)');
+    expect(saveSource).toContain("setEditorMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'))");
+    expect(saveSource).toContain('setCreatedOutsideMatchedViewNotice(!isEdit && useMatchedView)');
+    expect(saveSource).toContain("setEditorError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+    expect(saveSource).toContain('setEditorErrorDetail(error instanceof Error ? error.message : null)');
+    expect(saveSource).toContain("setEditorErrorContract('save')");
+    expect(saveSource).not.toContain('apiMessagePost');
+    expect(saveSource).not.toContain('apiMessagePut');
+    expect(saveSource).not.toContain("setEditorMessage(t('common.save-success'))");
+    expect(saveSource).not.toContain("t('common.save-failed')");
+  });
+
+  it('uses Angular edit failure notification and facade detail loading for edit detail fallback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/inhibit/alert-inhibit-page.tsx'), 'utf8');
+    const editSource = source.slice(
+      source.indexOf('async function handleEdit(inhibitId?: number)'),
+      source.indexOf('async function handleSave()')
+    );
+
+    expect(editSource).toContain('async function handleEdit(inhibitId?: number)');
+    expect(editSource).toContain('loadAlertInhibitDetailFromFacade(api.alertInhibits.detail, targetId)');
+    expect(editSource).toContain("setEditorError(error instanceof Error ? error.message : t('common.notify.edit-fail'))");
+    expect(editSource).not.toContain('apiMessageGet');
+    expect(editSource).not.toContain("t('common.load-failed')");
   });
 });
diff --git a/web-next/app/alert/inhibit/page.tsx b/web-next/app/alert/inhibit/page.tsx
index 1cecc58f9e..b979c457e6 100644
--- a/web-next/app/alert/inhibit/page.tsx
+++ b/web-next/app/alert/inhibit/page.tsx
@@ -1,229 +1,14 @@
-'use client';
-
 import React from 'react';
-import { useCallback, useMemo, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ClientWorkbench } from '../../../components/workbench/client-workbench';
-import { useI18n } from '../../../components/providers/i18n-provider';
-import { AlertInhibitSurface } from '../../../components/pages/alert-inhibit-surface';
-import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '../../../lib/api-client';
-import { buildAlertInhibitFormDraft, createAlertInhibit, deleteAlertInhibit, deleteAlertInhibits, loadAlertInhibitDetail, updateAlertInhibit, type AlertInhibitFormDraft } from '../../../lib/alert-inhibit/controller';
-import { buildAlertInhibitUrl } from '../../../lib/alert-inhibit/query-state';
-import { queryStateFromParams } from '../../../lib/alert-manage/query-state';
-import { buildAlertInhibitEvidenceContext, validateAlertInhibitForm } from '../../../lib/alert-inhibit/view-model';
-import { clearAlertInhibitEqualLabels, clearAlertInhibitTarget, copyAlertInhibitSourceToTarget, dropSeverityFromAlertInhibitTarget } from '../../../lib/alert-manage/view-model';
-import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptions } from '../../../lib/alert-label-options';
-import { formatTime } from '../../../lib/format';
-import { readSignalRouteContext } from '../../../lib/signal-route-context';
-import type { AlertInhibit, PageResult } from '../../../lib/types';
-
-type InhibitDeleteRequest = {
-  kind: 'single' | 'batch';
-  ids: number[];
-};
-
-export default function AlertInhibitPage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const returnContext = useMemo(() => queryStateFromParams(searchParams), [searchParams]);
-  const signalContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
-  const signal = searchParams.get('signal');
-  const inhibitEvidenceContext = useMemo(
-    () => buildAlertInhibitEvidenceContext(signal, signalContext, t),
-    [signal, signalContext, t]
-  );
-  const [search, setSearch] = useState('');
-  const [query, setQuery] = useState('');
-  const [selectedId, setSelectedId] = useState<number | null>(null);
-  const [editorOpen, setEditorOpen] = useState(false);
-  const [editorLoading, setEditorLoading] = useState(false);
-  const [editorSaving, setEditorSaving] = useState(false);
-  const [editorMessage, setEditorMessage] = useState<string | null>(null);
-  const [editorError, setEditorError] = useState<string | null>(null);
-  const [draft, setDraft] = useState<AlertInhibitFormDraft>(() => buildAlertInhibitFormDraft(null, inhibitEvidenceContext?.draftPatch));
-  const [refreshTick, setRefreshTick] = useState(0);
-  const [checkedIds, setCheckedIds] = useState<number[]>([]);
-  const [deleteRequest, setDeleteRequest] = useState<InhibitDeleteRequest | null>(null);
-  const [deletePending, setDeletePending] = useState(false);
-
-  const load = useCallback(async () => {
-    const [list, labelOptions] = await Promise.all([
-      apiMessageGet<PageResult<AlertInhibit>>(buildAlertInhibitUrl(query)),
-      loadAlertLabelOptions(apiMessageGet).catch(() => DEFAULT_ALERT_LABEL_OPTIONS)
-    ]);
-    return { list, labelOptions, refreshTick };
-  }, [query, refreshTick]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('alert.inhibit.loading')}>
-      {data => {
-        const labelOptions = data.labelOptions ?? DEFAULT_ALERT_LABEL_OPTIONS;
-        const selected = data.list.content.find(item => item.id === selectedId) ?? data.list.content[0] ?? null;
-
-        async function handleNew() {
-          setDraft(buildAlertInhibitFormDraft(null, inhibitEvidenceContext?.draftPatch));
-          setEditorError(null);
-          setEditorMessage(null);
-          setEditorOpen(true);
-        }
-
-        async function handleEdit(inhibitId?: number) {
-          const targetId = inhibitId ?? selected?.id;
-          if (!targetId) return;
-          setEditorLoading(true);
-          setEditorError(null);
-          setEditorMessage(null);
-          try {
-            const detail = await loadAlertInhibitDetail(apiMessageGet, targetId);
-            setDraft(buildAlertInhibitFormDraft(detail));
-            setEditorOpen(true);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.load-failed'));
-          } finally {
-            setEditorLoading(false);
-          }
-        }
-
-        async function handleSave() {
-          const validationError = validateAlertInhibitForm(draft, t);
-          if (validationError) {
-            setEditorError(validationError);
-            return;
-          }
-          setEditorSaving(true);
-          setEditorError(null);
-          setEditorMessage(null);
-          try {
-            if (draft.id) {
-              await updateAlertInhibit(apiMessagePut as any, draft);
-            } else {
-              await createAlertInhibit(apiMessagePost as any, draft);
-            }
-            setEditorMessage(t('common.save-success'));
-            setEditorOpen(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setEditorSaving(false);
-          }
-        }
-
-        async function handleToggleEnabled(inhibit?: AlertInhibit) {
-          const target = inhibit ?? selected;
-          if (!target) return;
-          try {
-            await updateAlertInhibit(apiMessagePut as any, {
-              ...buildAlertInhibitFormDraft(target),
-              enable: !(target.enable ?? true),
-            });
-            setEditorMessage(t('common.save-success'));
-            setEditorError(null);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.save-failed'));
-          }
-        }
-
-        async function handleDelete(inhibitId?: number) {
-          const targetId = inhibitId ?? selected?.id;
-          if (!targetId) return;
-          setDeleteRequest({ kind: 'single', ids: [targetId] });
-        }
-
-        async function handleConfirmedDelete() {
-          const request = deleteRequest;
-          if (!request || request.ids.length === 0) return;
-          setDeletePending(true);
-          try {
-            if (request.kind === 'batch') {
-              await deleteAlertInhibits(apiMessageDelete as any, request.ids);
-              setCheckedIds([]);
-            } else {
-              await deleteAlertInhibit(apiMessageDelete as any, request.ids[0]);
-            }
-            setSelectedId(null);
-            setEditorOpen(false);
-            setEditorMessage(t('common.delete-success'));
-            setEditorError(null);
-            setDeleteRequest(null);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.delete-failed'));
-          } finally {
-            setDeletePending(false);
-          }
-        }
-
-        function handleRefresh() {
-          setRefreshTick(value => value + 1);
-        }
-
-        async function handleDeleteSelected() {
-          if (checkedIds.length === 0) return;
-          setDeleteRequest({ kind: 'batch', ids: checkedIds });
-        }
 
-        return (
-          <>
-            <AlertInhibitSurface
-              t={t}
-              data={data}
-              search={search}
-              selectedId={selectedId}
-              checkedIds={checkedIds}
-              editorOpen={editorOpen}
-              editorLoading={editorLoading}
-              editorSaving={editorSaving}
-              editorMessage={editorMessage}
-              editorError={editorError}
-              returnContext={returnContext}
-              evidenceContext={inhibitEvidenceContext}
-              draft={draft}
-              labelOptions={labelOptions}
-              formatTime={formatTime}
-              onSearchChange={setSearch}
-              onApplyFilter={() => setQuery(search)}
-              onClearFilter={() => {
-                setSearch('');
-                setQuery('');
-              }}
-              onRefresh={handleRefresh}
-              onSelect={setSelectedId}
-              onCheckedIdsChange={setCheckedIds}
-              onNew={() => void handleNew()}
-              onEdit={inhibitId => void handleEdit(inhibitId)}
-              onSave={() => void handleSave()}
-              onToggleEnabled={inhibit => void handleToggleEnabled(inhibit)}
-              onDelete={inhibitId => void handleDelete(inhibitId)}
-              onDeleteSelected={() => void handleDeleteSelected()}
-              onCloseEditor={() => setEditorOpen(false)}
-              onDraftChange={setDraft}
-              onCopySourceToTarget={() => setDraft(prev => copyAlertInhibitSourceToTarget(prev))}
-              onDropSeverity={() => setDraft(prev => dropSeverityFromAlertInhibitTarget(prev))}
-              onClearTarget={() => setDraft(prev => clearAlertInhibitTarget(prev))}
-              onClearEqual={() => setDraft(prev => clearAlertInhibitEqualLabels(prev))}
-            />
-            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
-              <ColdConfirmDialog
-                open={Boolean(deleteRequest)}
-                title={deleteRequest?.kind === 'batch' ? t('common.confirm.delete-batch') : t('common.confirm.delete')}
-                copy={
-                  deleteRequest?.kind === 'batch'
-                    ? t('alert.inhibit.delete.confirm.batch', { count: deleteRequest.ids.length })
-                    : t('alert.inhibit.delete.confirm.single')
-                }
-                confirmLabel={t('common.button.ok')}
-                cancelLabel={t('common.button.cancel')}
-                pending={deletePending}
-                onCancel={() => setDeleteRequest(null)}
-                onConfirm={() => void handleConfirmedDelete()}
-              />
-            </div>
-          </>
-        );
-      }}
-    </ClientWorkbench>
-  );
+import AlertInhibitPage from './alert-inhibit-page';
+import { readAlertInhibitRouteState, type AlertInhibitSearchParams } from '../../../lib/alert-inhibit/query-state';
+
+export default async function AlertInhibitRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<AlertInhibitSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readAlertInhibitRouteState(resolvedSearchParams);
+  return <AlertInhibitPage initialRouteState={routeState} />;
 }
diff --git a/web-next/app/alert/integration/[source]/page.test.tsx b/web-next/app/alert/integration/[source]/page.test.tsx
index b720e59e75..76391d1c9b 100644
--- a/web-next/app/alert/integration/[source]/page.test.tsx
+++ b/web-next/app/alert/integration/[source]/page.test.tsx
@@ -4,6 +4,9 @@ import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 
+const redirect = vi.fn();
+const headers = vi.fn();
+
 const mockLoadIntegrationDoc = vi.hoisted(() =>
   vi.fn(
     async () => `# Webhook guide
@@ -35,6 +38,14 @@ vi.mock('next/link', () => ({
   )
 }));
 
+vi.mock('next/navigation', () => ({
+  redirect
+}));
+
+vi.mock('next/headers', () => ({
+  headers
+}));
+
 vi.mock('@/components/workbench/workbench-page', () => ({
   WorkbenchPage: ({ title, subtitle, facts, actions, main, side, tone }: any) => (
     <main data-workbench-page="true" data-tone={tone}>
@@ -76,6 +87,7 @@ vi.mock('@/lib/utils', () => ({
 
 vi.mock('@/lib/alert-integration/controller', () => ({
   fallbackDocCopy: 'No integration guide is available for this provider yet.',
+  getAlertIntegrationFallbackDocCopy: () => '当前告警源暂未提供集成指南。',
   loadIntegrationDoc: mockLoadIntegrationDoc
 }));
 
@@ -84,11 +96,25 @@ vi.mock('@/lib/alert-integration/view-model', () => ({
     { id: 'webhook', name: '默认Webhook', icon: '/assets/logo.svg' },
     { id: 'prometheus', name: 'Prometheus', icon: '/assets/img/integration/prometheus.svg' }
   ],
+  buildAlertIntegrationSourceHref: (source: any) => `/alert/integration/${source.id}`,
   getIntegrationSource: (source: string) =>
     [
       { id: 'webhook', name: '默认Webhook', icon: '/assets/logo.svg' },
       { id: 'prometheus', name: 'Prometheus', icon: '/assets/img/integration/prometheus.svg' }
     ].find(item => item.id === source) ?? { id: 'webhook', name: '默认Webhook', icon: '/assets/logo.svg' },
+  createAlertIntegrationTranslator: () => (key: string) =>
+    ({
+      'alert.integration.kicker': '集成接入',
+      'alert.integration.sources': '集成告警源',
+      'alert.integration.token.manage': '管理令牌'
+    })[key] ?? key,
+  getIntegrationSourceName: (item: any) => item.name ?? item.id,
+  translateAlertIntegration: (key: string) =>
+    ({
+      'alert.integration.kicker': '集成接入',
+      'alert.integration.sources': '集成告警源',
+      'alert.integration.token.manage': '管理令牌'
+    })[key] ?? key,
   buildIntegrationFacts: (source: string, hasDoc: boolean) => [
     { label: '集成接入', value: `alert/integration/${source}` },
     { label: '集成告警源', value: source },
@@ -107,6 +133,7 @@ vi.mock('@/lib/alert-integration/view-model', () => ({
 
 describe('alert integration page', () => {
   it('renders the OTLP cold-matte source rail plus markdown document shell for the selected integration source', async () => {
+    headers.mockResolvedValue(new Headers({ 'accept-language': 'zh-CN,zh;q=0.9' }));
     const source = readFileSync(resolve(process.cwd(), 'app/alert/integration/[source]/page.tsx'), 'utf8');
     const { default: AlertIntegrationPage } = await import('./page');
     const html = renderToStaticMarkup(
@@ -136,6 +163,7 @@ describe('alert integration page', () => {
     expect(html).toContain('管理令牌');
     expect(html).toContain('Webhook guide');
     expect(html).toContain('curl http://localhost:9090/api/v1/rules');
+    expect(mockLoadIntegrationDoc).toHaveBeenLastCalledWith(expect.stringContaining('alert-integration'), 'webhook', 'zh-CN');
     expect(html).not.toContain('# Webhook guide');
     expect(html).not.toContain('```json');
     expect(html).not.toContain('```bash');
@@ -156,6 +184,9 @@ describe('alert integration page', () => {
     expect(html).not.toContain('Navigation');
 
     expect(source).toContain('coldOpsCatalogVisual');
+    expect(source).toContain("from 'next/headers'");
+    expect(source).toContain('createAlertIntegrationTranslator(locale)');
+    expect(source).toContain('loadIntegrationDoc(baseDir, selectedSource.id, locale)');
     expect(source).toContain('data-alert-integration-surface="otlp-cold-source-doc"');
     expect(source).toContain('data-alert-integration-style-baseline={coldOpsVisual.canvasName}');
     expect(source).toContain('data-alert-integration-source-rail="cold-source-list"');
@@ -180,4 +211,20 @@ describe('alert integration page', () => {
     expect(source).not.toContain("from '@/components/workbench/primitives'");
     expect(source).not.toContain("from '@/components/observability/code-pane'");
   });
+
+  it('redirects unknown source params to the canonical default source route', async () => {
+    headers.mockResolvedValue(new Headers({ 'accept-language': 'zh-CN' }));
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: AlertIntegrationPage } = await import('./page');
+
+    await expect(
+      AlertIntegrationPage({
+        params: Promise.resolve({ source: 'unknown-provider' })
+      })
+    ).rejects.toThrow('redirect:/alert/integration/webhook');
+    expect(redirect).toHaveBeenLastCalledWith('/alert/integration/webhook');
+  });
 });
diff --git a/web-next/app/alert/integration/[source]/page.tsx b/web-next/app/alert/integration/[source]/page.tsx
index 367d0ef3f6..0e8924c4e7 100644
--- a/web-next/app/alert/integration/[source]/page.tsx
+++ b/web-next/app/alert/integration/[source]/page.tsx
@@ -1,10 +1,19 @@
 import path from 'node:path';
 import Link from 'next/link';
+import { headers } from 'next/headers';
+import { redirect } from 'next/navigation';
 import React from 'react';
 import { Settings } from 'lucide-react';
 import { Button } from '@/components/ui/button';
-import { fallbackDocCopy, loadIntegrationDoc } from '@/lib/alert-integration/controller';
-import { DATA_SOURCES, getIntegrationSource } from '@/lib/alert-integration/view-model';
+import { getAlertIntegrationFallbackDocCopy, loadIntegrationDoc } from '@/lib/alert-integration/controller';
+import {
+  buildAlertIntegrationSourceHref,
+  createAlertIntegrationTranslator,
+  DATA_SOURCES,
+  getIntegrationSource,
+  getIntegrationSourceName
+} from '@/lib/alert-integration/view-model';
+import { normalizeLocale } from '@/lib/i18n';
 import { cn } from '@/lib/utils';
 import { AlertIntegrationMarkdown } from '../../../../components/pages/alert-integration-markdown';
 import { coldOpsCatalogVisual } from '../../../../lib/cold-ops-visual';
@@ -12,9 +21,18 @@ import { coldOpsCatalogVisual } from '../../../../lib/cold-ops-visual';
 export default async function AlertIntegrationPage({ params }: { params: Promise<{ source: string }> }) {
   const { source } = await params;
   const coldOpsVisual = coldOpsCatalogVisual;
-  const selectedSource = getIntegrationSource(source);
+  const selectedSource = getIntegrationSource(source.trim());
+  if (selectedSource.id !== source) {
+    redirect(buildAlertIntegrationSourceHref(selectedSource));
+  }
+  const requestHeaders = await headers();
+  const locale = normalizeLocale(requestHeaders.get('accept-language'));
+  const t = createAlertIntegrationTranslator(locale);
+  const selectedSourceName = getIntegrationSourceName(selectedSource, t);
+  const sourceRailLabel = t('alert.integration.sources');
   const baseDir = path.join(process.cwd(), '..', 'web-app', 'src', 'assets', 'doc', 'alert-integration');
-  const doc = await loadIntegrationDoc(baseDir, selectedSource.id);
+  const doc = await loadIntegrationDoc(baseDir, selectedSource.id, locale);
+  const fallbackDocCopy = getAlertIntegrationFallbackDocCopy(locale);
 
   return (
     <div
@@ -24,18 +42,20 @@ export default async function AlertIntegrationPage({ params }: { params: Promise
     >
       <header className="grid gap-4 pb-1 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-end">
         <div>
-          <div className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">集成接入</div>
-          <h1 className="mt-1 text-[26px] font-semibold leading-tight text-[#f5f7fb]">{selectedSource.name}</h1>
+          <div className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">
+            {t('alert.integration.kicker')}
+          </div>
+          <h1 className="mt-1 text-[26px] font-semibold leading-tight text-[#f5f7fb]">{selectedSourceName}</h1>
         </div>
         <div
           data-alert-integration-header-actions="cold-source-doc-actions"
           className={cn(coldOpsVisual.button.row, 'mt-0 xl:justify-end')}
         >
-          <span className="hidden text-[11px] font-semibold text-[#7e8494] md:inline-flex">集成告警源</span>
+          <span className="hidden text-[11px] font-semibold text-[#7e8494] md:inline-flex">{sourceRailLabel}</span>
           <Link href="/setting/settings/token">
             <Button size="sm" variant="default" className={cn(coldOpsVisual.button.compact, 'rounded-[3px]')}>
               <Settings className="h-3.5 w-3.5" aria-hidden="true" />
-              管理令牌
+              {t('alert.integration.token.manage')}
             </Button>
           </Link>
         </div>
@@ -52,14 +72,17 @@ export default async function AlertIntegrationPage({ params }: { params: Promise
           data-alert-integration-source-rail="cold-source-list"
           className="border-b border-[#252b34] bg-[#0b0c0e] px-3 py-4 xl:border-b-0 xl:border-r"
         >
-          <h2 className="mb-3 text-[11px] font-semibold uppercase tracking-[0.14em] text-[#7e8494]">集成告警源</h2>
-          <nav className="space-y-1" aria-label="集成告警源">
+          <h2 className="mb-3 text-[11px] font-semibold uppercase tracking-[0.14em] text-[#7e8494]">
+            {sourceRailLabel}
+          </h2>
+          <nav className="space-y-1" aria-label={sourceRailLabel}>
             {DATA_SOURCES.map(item => {
               const selected = item.id === selectedSource.id;
+              const sourceName = getIntegrationSourceName(item, t);
               return (
                 <Link
                   key={item.id}
-                  href={`/alert/integration/${item.id}`}
+                  href={buildAlertIntegrationSourceHref(item)}
                   data-alert-integration-source-item={item.id}
                   data-alert-integration-source-selected={selected ? 'true' : undefined}
                   className={cn(
@@ -71,12 +94,12 @@ export default async function AlertIntegrationPage({ params }: { params: Promise
                 >
                   <img
                     src={item.icon}
-                    alt={item.name}
+                    alt={sourceName}
                     data-alert-integration-source-icon={item.id}
                     className="mr-2.5 h-5 w-5 shrink-0 object-contain"
                     aria-hidden="true"
                   />
-                  <span className="min-w-0 truncate">{item.name}</span>
+                  <span className="min-w-0 truncate">{sourceName}</span>
                 </Link>
               );
             })}
@@ -84,7 +107,7 @@ export default async function AlertIntegrationPage({ params }: { params: Promise
         </aside>
 
         <section data-alert-integration-doc-panel="cold-markdown-doc" className="min-w-0 overflow-y-auto px-6 py-5">
-          <h2 className="mb-4 text-[22px] font-semibold leading-tight text-[#f5f7fb]">{selectedSource.name}</h2>
+          <h2 className="mb-4 text-[22px] font-semibold leading-tight text-[#f5f7fb]">{selectedSourceName}</h2>
           {doc === fallbackDocCopy ? (
             <p data-alert-integration-markdown="rendered" className="text-[13px] leading-7 text-[#a9b0bb]">
               {doc}
diff --git a/web-next/app/alert/notice/page.tsx b/web-next/app/alert/notice/alert-notice-page.tsx
similarity index 66%
copy from web-next/app/alert/notice/page.tsx
copy to web-next/app/alert/notice/alert-notice-page.tsx
index 55115c454e..e47f2499a8 100644
--- a/web-next/app/alert/notice/page.tsx
+++ b/web-next/app/alert/notice/alert-notice-page.tsx
@@ -1,8 +1,8 @@
 'use client';
 
 import React, { useCallback, useMemo, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ArrowLeft, ChevronLeft, ChevronRight, Eye, Inbox, MoreHorizontal, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react';
+import { ArrowLeft, Eye, Inbox, MoreHorizontal, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react';
+import { HzConfirmDialog, HzPaginationBar } from '@hertzbeat/ui';
 import { AlertNoticeConsoleShell, type AlertNoticeConsoleTabKey } from '../../../components/pages/alert-notice-console-shell';
 import { ClientWorkbench } from '../../../components/workbench/client-workbench';
 import { useI18n } from '../../../components/providers/i18n-provider';
@@ -10,7 +10,6 @@ import { AlertNoticeReceiverFields } from '../../../components/pages/alert-notic
 import { AlertNoticeRuleFields } from '../../../components/pages/alert-notice-rule-fields';
 import { AlertNoticeTemplateFields } from '../../../components/pages/alert-notice-template-fields';
 import { Button } from '../../../components/ui/button';
-import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
 import { SearchRow } from '../../../components/ui/search-row';
 import { Select } from '../../../components/ui/select';
 import { OverlayDialog } from '../../../components/workbench/overlay-dialog';
@@ -20,13 +19,13 @@ import {
   AlertSurfaceTableShell,
   AlertSurfaceValuePill
 } from '../../../components/pages/alert-surface-primitives';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '../../../lib/api-client';
-import { buildNoticeRuleDraft, createNoticeReceiver, createNoticeRule, createNoticeTemplate, deleteNoticeReceiver, deleteNoticeRule, deleteNoticeTemplate, loadAlertNoticeData, loadNoticeReceiverDetail, loadNoticeRuleDetail, loadNoticeTemplateDetail, sendNoticeReceiverTest, updateNoticeReceiver, updateNoticeRule, updateNoticeTemplate, type NoticeReceiverDraft, type NoticeRuleDraft, type NoticeTemplateDraft } from '../../../lib/alert-notice/controller';
+import { api } from '../../../lib/alert-api-facade';
+import { buildNoticeListUrl, buildNoticeRuleDisplayNames, buildNoticeRuleDraft, buildNoticeTemplateListUrl, loadAlertNoticeDataFromFacade, type NoticeReceiverDraft, type NoticeRuleDraft, type NoticeTemplateDraft } from '../../../lib/alert-notice/controller';
+import type { AlertNoticeRouteState } from '../../../lib/alert-notice/query-state';
 import { buildAlertNoticeEvidenceContext, buildNoticeReceiverDraft, buildNoticeTemplateDraft, getAlertNoticeProductCopy, validateNoticeReceiverDraft, validateNoticeRuleDraft, validateNoticeTemplateDraft } from '../../../lib/alert-notice/view-model';
-import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptions, type AlertLabelOptions } from '../../../lib/alert-label-options';
+import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptionsFromFacade, type AlertLabelOptions } from '../../../lib/alert-label-options';
 import { coldOpsCatalogVisual } from '../../../lib/cold-ops-visual';
 import { formatTime } from '../../../lib/format';
-import { readSignalRouteContext } from '../../../lib/signal-route-context';
 import type { NoticeReceiver, NoticeRule, NoticeTemplate, PageResult } from '../../../lib/types';
 
 type NoticePageData = {
@@ -34,6 +33,7 @@ type NoticePageData = {
   receiverOptions: PageResult<NoticeReceiver>;
   rules: PageResult<NoticeRule>;
   templates: PageResult<NoticeTemplate>;
+  templateOptions: PageResult<NoticeTemplate>;
   labelOptions: AlertLabelOptions;
 };
 
@@ -58,6 +58,22 @@ const coldCommandButtonClass = `${coldButtonClassName} w-[124px] min-w-[124px]`;
 const coldPrimaryCommandButtonClass = `${coldPrimaryButtonClassName} w-[124px] min-w-[124px]`;
 const coldIconButtonClass =
   'h-8 w-8 min-w-0 rounded-[3px] border-[#2b3039] bg-[#101217] text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white';
+const ALERT_NOTICE_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_ALERT_NOTICE_ROUTE_STATE: AlertNoticeRouteState = {
+  signal: null,
+  signalContext: {}
+};
+
+function isApiMessageBusinessError(error: unknown) {
+  return typeof error === 'object' && error !== null && typeof (error as { code?: unknown }).code === 'number';
+}
+
+function clampNoticePageIndexAfterDelete(pageIndex: number, pageSize: number, totalElements: number, deleteCount = 1) {
+  const safePageSize = Math.max(1, pageSize);
+  const nextTotal = Math.max(0, totalElements - deleteCount);
+  const lastPageIndex = Math.max(0, Math.ceil(nextTotal / safePageSize) - 1);
+  return Math.min(pageIndex, lastPageIndex);
+}
 
 const NOTICE_TYPE_LABEL_KEYS: Record<string, string> = {
   '0': 'alert.notice.type.sms',
@@ -77,57 +93,87 @@ const NOTICE_TYPE_LABEL_KEYS: Record<string, string> = {
   '14': 'alert.notice.type.lark-app'
 };
 
-function getNoticeTypeLabel(type: number | string | null | undefined, t: Translator) {
-  if (type == null) return '-';
+const NOTICE_TEMPLATE_TYPE_LABEL_KEYS: Record<string, string> = {
+  ...NOTICE_TYPE_LABEL_KEYS,
+  '7': 'alert.notice.type.telegram'
+};
+
+function getNoticeTypeLabel(type: number | string | null | undefined, t: Translator, emptyValue: string) {
+  if (type == null) return emptyValue;
   const normalized = String(type).trim();
+  if (!normalized) return emptyValue;
   const key = NOTICE_TYPE_LABEL_KEYS[normalized];
   return key ? t(key) : normalized;
 }
 
-function getReceiverSetting(receiver: NoticeReceiver) {
+function getNoticeTemplateTypeLabel(type: number | string | null | undefined, t: Translator, emptyValue: string) {
+  if (type == null) return emptyValue;
+  const normalized = String(type).trim();
+  if (!normalized) return emptyValue;
+  const key = NOTICE_TEMPLATE_TYPE_LABEL_KEYS[normalized];
+  return key ? t(key) : normalized;
+}
+
+function formatReceiverSettingValue(value: string | null | undefined, emptyValue: string) {
+  return value?.trim() || emptyValue;
+}
+
+function formatReceiverSettingParts(
+  values: Array<string | number | null | undefined>,
+  emptyValue: string
+) {
+  const text = values.map(value => String(value ?? '').trim()).filter(Boolean).join(' / ');
+  return text || emptyValue;
+}
+
+function getReceiverSetting(receiver: NoticeReceiver, emptyValue: string) {
   const type = String(receiver.type ?? '');
   switch (type) {
     case '0':
-      return receiver.phone?.trim() || '-';
+      return formatReceiverSettingValue(receiver.phone, emptyValue);
     case '1':
-      return receiver.email?.trim() || '-';
+      return formatReceiverSettingValue(receiver.email, emptyValue);
     case '2':
-      return receiver.hookUrl?.trim() || '-';
+      return formatReceiverSettingValue(receiver.hookUrl, emptyValue);
     case '3':
     case '4':
-      return receiver.wechatId?.trim() || '-';
+      return formatReceiverSettingValue(receiver.wechatId, emptyValue);
     case '5':
-      return receiver.accessToken?.trim() || '-';
+      return formatReceiverSettingValue(receiver.accessToken, emptyValue);
     case '6':
-      return receiver.wechatId?.trim() || receiver.accessToken?.trim() || '-';
+      return formatReceiverSettingParts([receiver.wechatId, receiver.accessToken], emptyValue);
     case '7':
-      return [receiver.tgBotToken?.trim(), receiver.tgUserId?.trim()].filter(Boolean).join(' / ') || '-';
+      return formatReceiverSettingParts([receiver.tgBotToken, receiver.tgUserId], emptyValue);
     case '8':
-      return receiver.slackWebHookUrl?.trim() || '-';
+      return formatReceiverSettingValue(receiver.slackWebHookUrl, emptyValue);
     case '9':
-      return [receiver.discordChannelId?.trim(), receiver.discordBotToken?.trim()].filter(Boolean).join(' / ') || '-';
+      return formatReceiverSettingParts([receiver.discordChannelId, receiver.discordBotToken], emptyValue);
     case '10':
-      return [receiver.corpId?.trim(), receiver.agentId != null ? String(receiver.agentId).trim() : '', receiver.appSecret?.trim()]
-        .filter(Boolean)
-        .join(' / ') || '-';
+      return formatReceiverSettingParts([receiver.corpId, receiver.agentId, receiver.appSecret], emptyValue);
     case '11':
-      return receiver.smnAk?.trim() || '-';
+      return formatReceiverSettingValue(receiver.smnAk, emptyValue);
     case '12':
-      return receiver.serverChanToken?.trim() || '-';
+      return formatReceiverSettingValue(receiver.serverChanToken, emptyValue);
     case '13':
-      return receiver.gotifyToken?.trim() || '-';
+      return formatReceiverSettingValue(receiver.gotifyToken, emptyValue);
     case '14':
-      return receiver.appId?.trim() || '-';
+      return formatReceiverSettingValue(receiver.appId, emptyValue);
     default:
-      return receiver.email?.trim() || receiver.phone?.trim() || receiver.hookUrl?.trim() || '-';
+      return formatReceiverSettingParts([receiver.email, receiver.phone, receiver.hookUrl], emptyValue);
   }
 }
 
+function formatNoticeRuleReceivers(receiverNames: string[] | null | undefined, fallback: string) {
+  const text = (receiverNames || []).map(name => name.trim()).filter(Boolean).join(',');
+  return text || fallback;
+}
+
 function NoticePagination({
   t,
   pageIndex,
   pageSize,
   totalElements,
+  visibleCount,
   testIdPrefix,
   onPageIndexChange,
   onPageSizeChange
@@ -136,66 +182,65 @@ function NoticePagination({
   pageIndex: number;
   pageSize: number;
   totalElements: number;
+  visibleCount: number;
   testIdPrefix: string;
   onPageIndexChange: (pageIndex: number) => void;
   onPageSizeChange: (pageSize: number) => void;
 }) {
   const totalPages = Math.max(1, Math.ceil(totalElements / Math.max(1, pageSize)));
-  const pageNumbers = Array.from({ length: Math.min(totalPages, 3) }, (_, index) => index);
+  const currentPageIndex = Math.min(Math.max(0, pageIndex), totalPages - 1);
+  const currentPage = currentPageIndex + 1;
+  const pageStart = totalElements === 0 || visibleCount === 0 ? 0 : currentPageIndex * pageSize + 1;
+  const pageEnd = totalElements === 0 ? 0 : Math.min(totalElements, currentPageIndex * pageSize + visibleCount);
+  const paginationSummary = t('alert.notice.pagination.summary', {
+    page: currentPage,
+    totalPages,
+    from: pageStart,
+    to: pageEnd,
+    total: totalElements
+  });
+
+  function handlePageJumpChange(value: string) {
+    const parsed = Number.parseInt(value, 10);
+    if (!Number.isFinite(parsed)) return;
+    onPageIndexChange(Math.min(Math.max(parsed, 1), totalPages) - 1);
+  }
 
   return (
     <div
       data-alert-notice-pagination="cold-dense-pagination"
-      className="flex flex-wrap items-center justify-end gap-2 border-t border-[#252b34] bg-[#0b0c0e] px-3 py-2 text-xs text-[#8f99ab]"
+      data-alert-notice-pagination-owner="hertzbeat-ui-pagination-bar"
     >
-      <span>{`${t('common.total')} ${totalElements}`}</span>
-      <Button
-        data-testid={`${testIdPrefix}-previous`}
-        size="icon"
-        variant="subtle"
-        disabled={pageIndex <= 0}
-        onClick={() => onPageIndexChange(Math.max(0, pageIndex - 1))}
-        title={t('common.previous')}
-      >
-        <ChevronLeft className="h-4 w-4" aria-hidden="true" />
-        <span className="sr-only">{t('common.previous')}</span>
-      </Button>
-      {pageNumbers.map(pageNumber => (
-        <Button
-          key={pageNumber}
-          data-testid={`${testIdPrefix}-page-${pageNumber + 1}`}
-          size="icon"
-          variant={pageNumber === pageIndex ? 'primary' : 'subtle'}
-          onClick={() => onPageIndexChange(pageNumber)}
-          aria-current={pageNumber === pageIndex ? 'page' : undefined}
-        >
-          {pageNumber + 1}
-        </Button>
-      ))}
-      <Button
-        data-testid={`${testIdPrefix}-next`}
-        size="icon"
-        variant="subtle"
-        disabled={pageIndex >= totalPages - 1}
-        onClick={() => onPageIndexChange(Math.min(totalPages - 1, pageIndex + 1))}
-        title={t('common.next')}
-      >
-        <ChevronRight className="h-4 w-4" aria-hidden="true" />
-        <span className="sr-only">{t('common.next')}</span>
-      </Button>
-      <Select
-        data-testid={`${testIdPrefix}-page-size`}
-        value={String(pageSize)}
-        onChange={event => onPageSizeChange(Math.max(1, Number.parseInt(event.target.value, 10) || pageSize))}
-        containerClassName="w-[96px]"
-        className="h-8 min-w-0 text-[#eef2f7]"
-      >
-        {NOTICE_PAGE_SIZE_OPTIONS.map(option => (
-          <option key={option} value={option}>
-            {t('alert.notice.page-size', { count: option })}
-          </option>
-        ))}
-      </Select>
+      <HzPaginationBar
+        summary={paginationSummary}
+        pageSizeLabel={t('alert.notice.pagination.page-size')}
+        pageSizeValue={String(pageSize)}
+        pageSizeOptions={NOTICE_PAGE_SIZE_OPTIONS.map(option => ({ value: String(option), label: String(option) }))}
+        pageJumpLabel={t('alert.notice.pagination.page')}
+        pageJumpValue={String(currentPage)}
+        pageJumpMax={totalPages}
+        previousLabel={t('common.previous-page')}
+        nextLabel={t('common.next-page')}
+        previousDisabled={currentPageIndex <= 0}
+        nextDisabled={currentPage >= totalPages}
+        onPrevious={() => onPageIndexChange(Math.max(0, currentPageIndex - 1))}
+        onNext={() => onPageIndexChange(Math.min(totalPages - 1, currentPageIndex + 1))}
+        onPageSizeChange={value => onPageSizeChange(Math.max(1, Number.parseInt(value, 10) || pageSize))}
+        onPageJumpChange={handlePageJumpChange}
+        pageJumpInputProps={
+          {
+            'data-alert-notice-pagination-page-jump-owner': 'hertzbeat-ui-input',
+            'data-alert-notice-pagination-page-jump-scope': testIdPrefix
+          } as React.ComponentProps<typeof HzPaginationBar>['pageJumpInputProps']
+        }
+        pageSizeSelectProps={
+          {
+            'data-alert-notice-pagination-page-size-owner': 'hertzbeat-ui-select',
+            'data-alert-notice-pagination-page-size-scope': testIdPrefix
+          } as React.ComponentProps<typeof HzPaginationBar>['pageSizeSelectProps']
+        }
+        className="border-x-0"
+      />
     </div>
   );
 }
@@ -249,6 +294,8 @@ function NoticeTableSwitch({
       aria-checked={checked}
       aria-label={label}
       data-alert-notice-rule-table-switch={field}
+      data-alert-notice-rule-table-switch-update="angular-edit-notify"
+      data-alert-notice-rule-table-switch-update-owner="route-action-feedback-contract"
       data-state={checked ? 'checked' : 'unchecked'}
       disabled={pending}
       onClick={() => onChange(!checked)}
@@ -262,11 +309,10 @@ function NoticeTableSwitch({
   );
 }
 
-export default function AlertNoticePage() {
+export default function AlertNoticePage({ initialRouteState }: { initialRouteState?: AlertNoticeRouteState } = {}) {
   const { t, locale } = useI18n();
-  const searchParams = useSearchParams();
-  const signal = searchParams.get('signal');
-  const signalContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
+  const alertNoticeRouteState = initialRouteState ?? EMPTY_ALERT_NOTICE_ROUTE_STATE;
+  const { signal, signalContext } = alertNoticeRouteState;
   const noticeEvidenceContext = useMemo(
     () => buildAlertNoticeEvidenceContext(signal, signalContext, t),
     [signal, signalContext, t]
@@ -279,8 +325,10 @@ export default function AlertNoticePage() {
   const [receiverPageIndex, setReceiverPageIndex] = useState(0);
   const [receiverPageSize, setReceiverPageSize] = useState(8);
   const [savingReceiver, setSavingReceiver] = useState(false);
+  const [testingReceiver, setTestingReceiver] = useState(false);
   const [receiverMessage, setReceiverMessage] = useState<string | null>(null);
   const [receiverError, setReceiverError] = useState<string | null>(null);
+  const [receiverErrorDetail, setReceiverErrorDetail] = useState<string | null>(null);
   const [editingTemplate, setEditingTemplate] = useState(false);
   const [templateDraft, setTemplateDraft] = useState<NoticeTemplateDraft>(() => buildNoticeTemplateDraft(null));
   const [templateReadOnly, setTemplateReadOnly] = useState(false);
@@ -288,6 +336,7 @@ export default function AlertNoticePage() {
   const [savingTemplate, setSavingTemplate] = useState(false);
   const [templateMessage, setTemplateMessage] = useState<string | null>(null);
   const [templateError, setTemplateError] = useState<string | null>(null);
+  const [templateErrorDetail, setTemplateErrorDetail] = useState<string | null>(null);
   const [templateSearchDraft, setTemplateSearchDraft] = useState('');
   const [templateSearch, setTemplateSearch] = useState('');
   const [templatePresetFilter, setTemplatePresetFilter] = useState(true);
@@ -304,34 +353,71 @@ export default function AlertNoticePage() {
   const [savingRule, setSavingRule] = useState(false);
   const [ruleMessage, setRuleMessage] = useState<string | null>(null);
   const [ruleError, setRuleError] = useState<string | null>(null);
+  const [ruleErrorDetail, setRuleErrorDetail] = useState<string | null>(null);
   const [ruleSwitchPending, setRuleSwitchPending] = useState<string | null>(null);
   const [refreshTick, setRefreshTick] = useState(0);
   const [deleteRequest, setDeleteRequest] = useState<NoticeDeleteRequest | null>(null);
   const [deletePending, setDeletePending] = useState(false);
+  const alertNoticeReceiverListUrl = useMemo(() => buildNoticeListUrl('/notice/receivers', { search: receiverSearch, pageIndex: receiverPageIndex, pageSize: receiverPageSize }), [receiverPageIndex, receiverPageSize, receiverSearch]);
+  const alertNoticeRuleListUrl = useMemo(() => buildNoticeListUrl('/notice/rules', { search: ruleSearch, pageIndex: rulePageIndex, pageSize: rulePageSize }), [rulePageIndex, rulePageSize, ruleSearch]);
+  const alertNoticeTemplateListUrl = useMemo(
+    () => buildNoticeTemplateListUrl({ search: templateSearch, preset: templatePresetFilter, pageIndex: templatePageIndex, pageSize: templatePageSize }),
+    [templatePageIndex, templatePageSize, templatePresetFilter, templateSearch]
+  );
+  const alertNoticeLoadQuery = useMemo(
+    () => ({
+      receivers: {
+        search: receiverSearch,
+        pageIndex: receiverPageIndex,
+        pageSize: receiverPageSize
+      },
+      rules: {
+        search: ruleSearch,
+        pageIndex: rulePageIndex,
+        pageSize: rulePageSize
+      },
+      templates: {
+        search: templateSearch,
+        preset: templatePresetFilter,
+        pageIndex: templatePageIndex,
+        pageSize: templatePageSize
+      }
+    }),
+    [receiverPageIndex, receiverPageSize, receiverSearch, rulePageIndex, rulePageSize, ruleSearch, templatePageIndex, templatePageSize, templatePresetFilter, templateSearch]
+  );
+  const alertNoticeCacheKey = useMemo(
+    () => ['alert-notice', alertNoticeReceiverListUrl, alertNoticeRuleListUrl, alertNoticeTemplateListUrl, refreshTick].join('|'),
+    [alertNoticeReceiverListUrl, alertNoticeRuleListUrl, alertNoticeTemplateListUrl, refreshTick]
+  );
 
   const load = useCallback(async (): Promise<NoticePageData> => {
+    void refreshTick;
     const [noticeData, labelOptions] = await Promise.all([
-      loadAlertNoticeData(apiMessageGet, {
-        receivers: {
-          search: receiverSearch,
-          pageIndex: receiverPageIndex,
-          pageSize: receiverPageSize
+      loadAlertNoticeDataFromFacade(
+        {
+          receivers: api.alertNotice.receivers.list,
+          rules: api.alertNotice.rules.list,
+          receiverOptions: api.alertNotice.receivers.options,
+          templates: api.alertNotice.templates.list,
+          templateOptions: api.alertNotice.templates.options
         },
-        rules: {
-          search: ruleSearch,
-          pageIndex: rulePageIndex,
-          pageSize: rulePageSize
-        }
-      }),
-      loadAlertLabelOptions(apiMessageGet).catch(() => DEFAULT_ALERT_LABEL_OPTIONS)
+        alertNoticeLoadQuery
+      ),
+      loadAlertLabelOptionsFromFacade(api.alertLabels.list).catch(() => DEFAULT_ALERT_LABEL_OPTIONS)
     ]);
     return { ...noticeData, labelOptions };
-  }, [receiverPageIndex, receiverPageSize, receiverSearch, rulePageIndex, rulePageSize, ruleSearch]);
+  }, [alertNoticeLoadQuery, refreshTick]);
 
-  const productCopy = getAlertNoticeProductCopy(locale);
+  const productCopy = getAlertNoticeProductCopy(t);
+  const emptyValue = t('common.none');
 
   return (
-    <ClientWorkbench key={refreshTick} load={load} loadingCopy={t('alert.notice.loading')}>
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('alert.notice.loading')}
+      cacheKey={alertNoticeCacheKey}
+      cacheSettledTtlMs={ALERT_NOTICE_SETTLED_CACHE_TTL_MS}
+    >
       {data => {
         const selectedReceiver = data.receivers.content.find(item => item.id === selectedReceiverId) ?? data.receivers.content[0] ?? null;
         const selectedRule = data.rules.content.find(item => item.id === selectedRuleId) ?? data.rules.content[0] ?? null;
@@ -342,32 +428,18 @@ export default function AlertNoticePage() {
           data.templates.content[0] ??
           null;
         const selectedTemplateIsCustom = selectedTemplate ? !selectedTemplate.preset : false;
-        const templateSearchNeedle = templateSearch.trim().toLowerCase();
-        const filteredTemplates = data.templates.content.filter(template => {
-          if (Boolean(template.preset) !== templatePresetFilter) {
-            return false;
-          }
-          if (!templateSearchNeedle) {
-            return true;
-          }
-          return (template.name || '').toLowerCase().includes(templateSearchNeedle);
-        });
-        const templateTotalElements = filteredTemplates.length;
-        const templateMaxPageIndex = Math.max(0, Math.ceil(templateTotalElements / Math.max(1, templatePageSize)) - 1);
-        const normalizedTemplatePageIndex = Math.min(templatePageIndex, templateMaxPageIndex);
-        const templateVisibleRows = filteredTemplates.slice(
-          normalizedTemplatePageIndex * templatePageSize,
-          normalizedTemplatePageIndex * templatePageSize + templatePageSize
-        );
+        const templateTotalElements = data.templates.totalElements ?? data.templates.content.length;
+        const normalizedTemplatePageIndex = data.templates.pageIndex ?? templatePageIndex;
+        const templateVisibleRows = data.templates.content;
         const receiverOptionRows = data.receiverOptions?.content?.length ? data.receiverOptions.content : data.receivers.content;
         const receiverOptions = receiverOptionRows.map(receiver => ({
           value: String(receiver.id),
-          label: `${receiver.name || productCopy.receiverFallback}-${getNoticeTypeLabel(receiver.type, t)}`,
+          label: `${receiver.name || productCopy.receiverFallback}-${getNoticeTypeLabel(receiver.type, t, emptyValue)}`,
           type: receiver.type
         }));
         const templateOptions = [
           { value: '-1', label: t('alert.notice.template.preset.true') },
-          ...data.templates.content
+          ...(data.templateOptions?.content ?? data.templates.content)
             .filter(template => template.id != null)
             .map(template => ({
               value: String(template.id),
@@ -384,44 +456,55 @@ export default function AlertNoticePage() {
           setReceiverDraft(buildNoticeReceiverDraft(null));
           setReceiverMessage(null);
           setReceiverError(null);
+          setReceiverErrorDetail(null);
           setEditingReceiver(true);
         }
 
-        async function handleEditReceiver() {
-          if (!selectedReceiver?.id) return;
+        async function handleEditReceiver(receiver = selectedReceiver) {
+          if (!receiver?.id) return;
           try {
             setSelectedTab('receiver');
-            const detail = await loadNoticeReceiverDetail(apiMessageGet, selectedReceiver.id);
+            setSelectedReceiverId(receiver.id);
+            const detail = await api.alertNotice.receivers.detail(receiver.id);
             setReceiverDraft(buildNoticeReceiverDraft(detail));
             setReceiverMessage(null);
             setReceiverError(null);
+            setReceiverErrorDetail(null);
             setEditingReceiver(true);
           } catch (error) {
-            setReceiverError(error instanceof Error ? error.message : t('common.load-failed'));
+            setReceiverError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
+            setReceiverErrorDetail(null);
           }
         }
 
         async function handleSaveReceiver() {
+          const isEdit = Boolean(receiverDraft.id);
           const validationError = validateNoticeReceiverDraft(receiverDraft, t);
           if (validationError) {
             setReceiverMessage(null);
             setReceiverError(validationError);
+            setReceiverErrorDetail(null);
             return;
           }
           setSavingReceiver(true);
           setReceiverMessage(null);
           setReceiverError(null);
+          setReceiverErrorDetail(null);
           try {
             if (receiverDraft.id) {
-              await updateNoticeReceiver(apiMessagePut as any, receiverDraft);
+              await api.alertNotice.receivers.update(receiverDraft);
             } else {
-              await createNoticeReceiver(apiMessagePost as any, receiverDraft);
+              await api.alertNotice.receivers.create(receiverDraft);
             }
-            setReceiverMessage(t('common.save-success'));
+            setReceiverMessage([t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'), t('alert.notice.receiver.next')].join(' '));
             setEditingReceiver(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setReceiverError(error instanceof Error ? error.message : t('common.save-failed'));
+            setReceiverError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
+            setReceiverErrorDetail(error instanceof Error ? error.message : null);
+            if (!isApiMessageBusinessError(error)) {
+              setEditingReceiver(false);
+            }
           } finally {
             setSavingReceiver(false);
           }
@@ -442,35 +525,44 @@ export default function AlertNoticePage() {
           setDeletePending(true);
           try {
             if (request.kind === 'receiver') {
-              await deleteNoticeReceiver(apiMessageDelete as any, request.id);
+              await api.alertNotice.receivers.delete(request.id);
               setSelectedReceiverId(null);
-              setReceiverMessage(t('common.delete-success'));
+              setReceiverPageIndex(pageIndex => clampNoticePageIndexAfterDelete(pageIndex, receiverPageSize, receiverTotal, 1));
+              setReceiverMessage(t('common.notify.delete-success'));
               setReceiverError(null);
+              setReceiverErrorDetail(null);
               setEditingReceiver(false);
             } else if (request.kind === 'template') {
-              await deleteNoticeTemplate(apiMessageDelete as any, request.id);
+              await api.alertNotice.templates.delete(request.id);
               setSelectedTemplateId(null);
-              setTemplateMessage(t('common.delete-success'));
+              setTemplatePageIndex(pageIndex => clampNoticePageIndexAfterDelete(pageIndex, templatePageSize, templateTotal, 1));
+              setTemplateMessage(t('common.notify.delete-success'));
               setTemplateError(null);
+              setTemplateErrorDetail(null);
               setTemplateReadOnly(false);
               setEditingTemplate(false);
             } else {
-              await deleteNoticeRule(apiMessageDelete as any, request.id);
+              await api.alertNotice.rules.delete(request.id);
               setSelectedRuleId(null);
-              setRuleMessage(t('common.delete-success'));
+              setRulePageIndex(pageIndex => clampNoticePageIndexAfterDelete(pageIndex, rulePageSize, ruleTotal, 1));
+              setRuleMessage(t('common.notify.delete-success'));
               setRuleError(null);
+              setRuleErrorDetail(null);
               setEditingRule(false);
             }
             setDeleteRequest(null);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            const fallback = error instanceof Error ? error.message : t('common.delete-failed');
+            const fallback = error instanceof Error ? error.message : t('common.notify.delete-fail');
             if (request.kind === 'receiver') {
               setReceiverError(fallback);
+              setReceiverErrorDetail(null);
             } else if (request.kind === 'template') {
               setTemplateError(fallback);
+              setTemplateErrorDetail(null);
             } else {
               setRuleError(fallback);
+              setRuleErrorDetail(null);
             }
           } finally {
             setDeletePending(false);
@@ -478,18 +570,21 @@ export default function AlertNoticePage() {
         }
 
         async function handleTestSend() {
-          const validationError = validateNoticeReceiverDraft(receiverDraft, t);
-          if (validationError) {
-            setReceiverMessage(null);
-            setReceiverError(validationError);
-            return;
-          }
+          setTestingReceiver(true);
+          setReceiverMessage(null);
+          setReceiverError(null);
+          setReceiverErrorDetail(null);
           try {
-            await sendNoticeReceiverTest(apiMessagePost as any, receiverDraft);
+            await api.alertNotice.receivers.sendTest(receiverDraft);
             setReceiverMessage(t('alert.notice.send-test.notify.success'));
             setReceiverError(null);
+            setReceiverErrorDetail(null);
           } catch (error) {
+            setReceiverMessage(null);
             setReceiverError(error instanceof Error ? error.message : t('alert.notice.send-test.notify.failed'));
+            setReceiverErrorDetail(null);
+          } finally {
+            setTestingReceiver(false);
           }
         }
 
@@ -499,6 +594,7 @@ export default function AlertNoticePage() {
           setTemplateReadOnly(false);
           setTemplateMessage(null);
           setTemplateError(null);
+          setTemplateErrorDetail(null);
           setEditingTemplate(true);
         }
 
@@ -508,13 +604,15 @@ export default function AlertNoticePage() {
             setSelectedTab('template');
             setSelectedTemplateId(template.id);
             setTemplateReadOnly(false);
-            const detail = await loadNoticeTemplateDetail(apiMessageGet, template.id);
+            const detail = await api.alertNotice.templates.detail(template.id);
             setTemplateDraft(buildNoticeTemplateDraft(detail));
             setTemplateMessage(null);
             setTemplateError(null);
+            setTemplateErrorDetail(null);
             setEditingTemplate(true);
           } catch (error) {
-            setTemplateError(error instanceof Error ? error.message : t('common.load-failed'));
+            setTemplateError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
+            setTemplateErrorDetail(null);
           }
         }
 
@@ -527,39 +625,45 @@ export default function AlertNoticePage() {
           setTemplateDraft(rowDraft);
           setTemplateMessage(null);
           setTemplateError(null);
+          setTemplateErrorDetail(null);
           setEditingTemplate(true);
 
           if (templateId == null) return;
 
           try {
-            const detail = await loadNoticeTemplateDetail(apiMessageGet, templateId);
+            const detail = await api.alertNotice.templates.detail(templateId);
             setTemplateDraft(buildNoticeTemplateDraft(detail));
           } catch (error) {
             setTemplateError(null);
+            setTemplateErrorDetail(null);
           }
         }
 
         async function handleSaveTemplate() {
+          const isEdit = Boolean(templateDraft.id);
           const validationError = validateNoticeTemplateDraft(templateDraft, t);
           if (validationError) {
             setTemplateMessage(null);
             setTemplateError(validationError);
+            setTemplateErrorDetail(null);
             return;
           }
           setSavingTemplate(true);
           setTemplateMessage(null);
           setTemplateError(null);
+          setTemplateErrorDetail(null);
           try {
             if (templateDraft.id) {
-              await updateNoticeTemplate(apiMessagePut as any, templateDraft);
+              await api.alertNotice.templates.update(templateDraft);
             } else {
-              await createNoticeTemplate(apiMessagePost as any, templateDraft);
+              await api.alertNotice.templates.create(templateDraft);
             }
-            setTemplateMessage(t('common.save-success'));
+            setTemplateMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditingTemplate(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setTemplateError(error instanceof Error ? error.message : t('common.save-failed'));
+            setTemplateError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
+            setTemplateErrorDetail(error instanceof Error ? error.message : null);
           } finally {
             setSavingTemplate(false);
           }
@@ -590,6 +694,7 @@ export default function AlertNoticePage() {
           setRuleDraft(buildNoticeRuleDraft(null, noticeEvidenceContext?.ruleDraftPatch));
           setRuleMessage(null);
           setRuleError(null);
+          setRuleErrorDetail(null);
           setEditingRule(true);
         }
 
@@ -599,37 +704,44 @@ export default function AlertNoticePage() {
           try {
             setSelectedTab('rule');
             setSelectedRuleId(ruleId);
-            const detail = await loadNoticeRuleDetail(apiMessageGet, ruleId);
+            const detail = await api.alertNotice.rules.detail(ruleId);
             setRuleDraft(buildNoticeRuleDraft(detail));
             setRuleMessage(null);
             setRuleError(null);
+            setRuleErrorDetail(null);
             setEditingRule(true);
           } catch (error) {
-            setRuleError(error instanceof Error ? error.message : t('common.load-failed'));
+            setRuleError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
+            setRuleErrorDetail(null);
           }
         }
 
         async function handleSaveRule() {
+          const isEdit = Boolean(ruleDraft.id);
           const validationError = validateNoticeRuleDraft(ruleDraft, t);
           if (validationError) {
             setRuleMessage(null);
             setRuleError(validationError);
+            setRuleErrorDetail(null);
             return;
           }
           setSavingRule(true);
           setRuleMessage(null);
           setRuleError(null);
+          setRuleErrorDetail(null);
           try {
+            const displayNames = buildNoticeRuleDisplayNames(ruleDraft, receiverOptions, templateOptions);
             if (ruleDraft.id) {
-              await updateNoticeRule(apiMessagePut as any, ruleDraft);
+              await api.alertNotice.rules.update(ruleDraft, displayNames);
             } else {
-              await createNoticeRule(apiMessagePost as any, ruleDraft);
+              await api.alertNotice.rules.create(ruleDraft, displayNames);
             }
-            setRuleMessage(t('common.save-success'));
+            setRuleMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditingRule(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setRuleError(error instanceof Error ? error.message : t('common.save-failed'));
+            setRuleError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
+            setRuleErrorDetail(error instanceof Error ? error.message : null);
           } finally {
             setSavingRule(false);
           }
@@ -648,12 +760,22 @@ export default function AlertNoticePage() {
           if (!rule.id) return;
           const pendingKey = `${rule.id}:${field}`;
           setRuleSwitchPending(pendingKey);
+          setRuleMessage(null);
           setRuleError(null);
+          setRuleErrorDetail(null);
           try {
-            await updateNoticeRule(apiMessagePut as any, buildNoticeRuleDraft({ ...rule, [field]: checked }));
+            await api.alertNotice.rules.update(
+              buildNoticeRuleDraft({ ...rule, [field]: checked }),
+              {
+                receiverName: rule.receiverName ?? [],
+                templateName: rule.templateId ? rule.templateName ?? null : null
+              }
+            );
+            setRuleMessage(t('common.notify.edit-success'));
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setRuleError(error instanceof Error ? error.message : t('common.save-failed'));
+            setRuleError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
+            setRuleErrorDetail(null);
           } finally {
             setRuleSwitchPending(current => (current === pendingKey ? null : current));
           }
@@ -689,7 +811,15 @@ export default function AlertNoticePage() {
               className={coldToolbarClass}
             >
               <div className="flex flex-wrap items-center gap-2">
-                <Button className={coldIconButtonClass} size="icon" variant="default" onClick={() => setRefreshTick(value => value + 1)} title={t('common.refresh')}>
+                <Button
+                  data-alert-notice-receiver-sync="angular-load-table"
+                  data-alert-notice-receiver-sync-owner="route-refresh-contract"
+                  className={coldIconButtonClass}
+                  size="icon"
+                  variant="default"
+                  onClick={() => setRefreshTick(value => value + 1)}
+                  title={t('common.refresh')}
+                >
                   <RefreshCw className="h-4 w-4" aria-hidden="true" />
                   <span className="sr-only">{t('common.refresh')}</span>
                 </Button>
@@ -713,6 +843,8 @@ export default function AlertNoticePage() {
               </div>
                 <SearchRow
                   data-alert-notice-receiver-search="shared-compact"
+                  data-alert-notice-receiver-search-submit="angular-enter-and-clear"
+                  data-alert-notice-receiver-search-submit-owner="cold-search-row"
                 className="mb-0"
                 value={receiverSearchDraft}
                 placeholder={t('alert.notice.receiver.people.name')}
@@ -745,24 +877,20 @@ export default function AlertNoticePage() {
                       <td className={`px-3 py-3 text-center font-medium ${coldTextPrimaryClass}`}>{receiver.name || t('alert.notice.receiver.people')}</td>
                       <td className="px-3 py-3 text-center">
                         <div className="flex justify-center">
-                          <AlertSurfaceValuePill>{getNoticeTypeLabel(receiver.type, t)}</AlertSurfaceValuePill>
+                          <AlertSurfaceValuePill>{getNoticeTypeLabel(receiver.type, t, emptyValue)}</AlertSurfaceValuePill>
                         </div>
                       </td>
-                      <td className="max-w-[340px] truncate px-3 py-3 text-center" title={getReceiverSetting(receiver)}>{getReceiverSetting(receiver)}</td>
+                      <td className="max-w-[340px] truncate px-3 py-3 text-center" title={getReceiverSetting(receiver, emptyValue)}>{getReceiverSetting(receiver, emptyValue)}</td>
                       <td className="px-3 py-3 text-center">{formatTime(receiver.gmtUpdate || receiver.gmtCreate || null)}</td>
                       <td className="px-3 py-3 text-center" onClick={event => event.stopPropagation()}>
                         <div className="flex justify-center gap-2">
                           <Button
+                            data-alert-notice-receiver-edit-detail="angular-detail-fetch"
+                            data-alert-notice-receiver-edit-detail-owner="route-detail-fetch-contract"
                             className={coldIconButtonClass}
                             size="icon"
                             variant="default"
-                            onClick={() => {
-                              setSelectedReceiverId(receiver.id);
-                              setReceiverDraft(buildNoticeReceiverDraft(receiver));
-                              setReceiverMessage(null);
-                              setReceiverError(null);
-                              setEditingReceiver(true);
-                            }}
+                            onClick={() => void handleEditReceiver(receiver)}
                             title={t('alert.notice.receiver.edit')}
                           >
                             <Pencil className="h-4 w-4" aria-hidden="true" />
@@ -793,6 +921,7 @@ export default function AlertNoticePage() {
                 pageIndex={receiverPageIndex}
                 pageSize={receiverPageSize}
                 totalElements={data.receivers.totalElements || 0}
+                visibleCount={data.receivers.content.length}
                 testIdPrefix="notice-receiver"
                 onPageIndexChange={setReceiverPageIndex}
                 onPageSizeChange={nextPageSize => {
@@ -801,22 +930,46 @@ export default function AlertNoticePage() {
                 }}
               />
             ) : null}
-            {receiverMessage ? <div className="px-3 py-2 text-sm text-emerald-300">{receiverMessage}</div> : null}
+            {!editingReceiver && receiverMessage ? <div className="px-3 py-2 text-sm text-emerald-300">{receiverMessage}</div> : null}
             {!editingReceiver && receiverError ? (
-              <div role="alert" className="px-3 py-2 text-sm text-rose-300">{receiverError}</div>
+              <div
+                role="alert"
+                data-alert-notice-receiver-save-failure={receiverErrorDetail ? 'angular-notify-title-detail' : undefined}
+                data-alert-notice-receiver-save-failure-owner={receiverErrorDetail ? 'route-action-feedback-contract' : undefined}
+                data-alert-notice-receiver-save-failure-title={receiverErrorDetail ? receiverError : undefined}
+                data-alert-notice-receiver-save-failure-detail={receiverErrorDetail ?? undefined}
+                className="px-3 py-2 text-sm text-rose-300"
+              >
+                <span>{receiverError}</span>
+                {receiverErrorDetail ? <span className="mt-1 block text-rose-200">{receiverErrorDetail}</span> : null}
+              </div>
             ) : null}
           </div>
         );
 
         const rulePanel = (
-          <div data-alert-notice-rule-panel="true">
+          <div
+            data-alert-notice-rule-panel="true"
+            data-alert-notice-rule-template-display="angular-template-id-fallback"
+            data-alert-notice-rule-template-display-owner="route-table-contract"
+            data-alert-notice-rule-receiver-display="angular-array-interpolation"
+            data-alert-notice-rule-receiver-display-owner="route-table-contract"
+          >
             <div
               data-alert-notice-rule-toolbar="cold-query-toolbar"
               data-alert-notice-rule-toolbar-layout="compact-inline-actions-query"
               className={coldToolbarClass}
             >
               <div className="flex flex-wrap items-center gap-2">
-                  <Button className={coldIconButtonClass} size="icon" variant="default" onClick={() => setRefreshTick(value => value + 1)} title={t('common.refresh')}>
+                <Button
+                  data-alert-notice-rule-sync="angular-load-table"
+                  data-alert-notice-rule-sync-owner="route-refresh-contract"
+                  className={coldIconButtonClass}
+                  size="icon"
+                  variant="default"
+                  onClick={() => setRefreshTick(value => value + 1)}
+                  title={t('common.refresh')}
+                >
                   <RefreshCw className="h-4 w-4" aria-hidden="true" />
                   <span className="sr-only">{t('common.refresh')}</span>
                 </Button>
@@ -831,6 +984,8 @@ export default function AlertNoticePage() {
               </div>
                 <SearchRow
                   data-alert-notice-rule-search="shared-compact"
+                  data-alert-notice-rule-search-submit="angular-enter-and-clear"
+                  data-alert-notice-rule-search-submit-owner="cold-search-row"
                 className="mb-0"
                 value={ruleSearchDraft}
                 placeholder={t('alert.notice.rule.name')}
@@ -863,8 +1018,8 @@ export default function AlertNoticePage() {
                       onClick={() => setSelectedRuleId(rule.id)}
                     >
                       <td className={`px-3 py-3 text-center font-medium ${coldTextPrimaryClass}`}>{rule.name || t('alert.notice.rule')}</td>
-                      <td className="px-3 py-3 text-center">{rule.receiverName?.join(', ') || '-'}</td>
-                      <td className="px-3 py-3 text-center">{rule.templateName || t('alert.notice.template.preset.true')}</td>
+                      <td className="px-3 py-3 text-center">{formatNoticeRuleReceivers(rule.receiverName, productCopy.ruleNoReceiver)}</td>
+                      <td className="px-3 py-3 text-center">{rule.templateId ? rule.templateName || t('alert.notice.template.preset.true') : t('alert.notice.template.preset.true')}</td>
                       <td className="px-3 py-3 text-center" onClick={event => event.stopPropagation()}>
                         <NoticeTableSwitch
                           field="filter-all"
@@ -890,6 +1045,8 @@ export default function AlertNoticePage() {
                             className={coldIconButtonClass}
                             size="icon"
                             variant="default"
+                            data-alert-notice-rule-edit-detail="angular-detail-fetch"
+                            data-alert-notice-rule-edit-detail-owner="route-detail-fetch-contract"
                             onClick={() => void handleEditRule(rule)}
                             title={t('alert.notice.rule.edit')}
                           >
@@ -915,6 +1072,7 @@ export default function AlertNoticePage() {
                 pageIndex={rulePageIndex}
                 pageSize={rulePageSize}
                 totalElements={data.rules.totalElements || 0}
+                visibleCount={data.rules.content.length}
                 testIdPrefix="notice-rule"
                 onPageIndexChange={setRulePageIndex}
                 onPageSizeChange={nextPageSize => {
@@ -931,14 +1089,28 @@ export default function AlertNoticePage() {
         );
 
         const templatePanel = (
-          <div data-alert-notice-template-panel="true">
+          <div
+            data-alert-notice-template-panel="true"
+            data-alert-notice-template-query-owner="backend-paginated"
+            data-alert-notice-template-query-url={alertNoticeTemplateListUrl}
+            data-alert-notice-template-telegram-label="angular-template-telegram"
+            data-alert-notice-template-telegram-label-owner="route-i18n-contract"
+          >
             <div
               data-alert-notice-template-toolbar="cold-query-toolbar"
               data-alert-notice-template-toolbar-layout="compact-inline-actions-query"
               className={coldToolbarClass}
             >
               <div className="flex flex-wrap items-center gap-2">
-                <Button className={coldIconButtonClass} size="icon" variant="default" onClick={() => setRefreshTick(value => value + 1)} title={t('common.refresh')}>
+                <Button
+                  data-alert-notice-template-sync="angular-load-table"
+                  data-alert-notice-template-sync-owner="route-refresh-contract"
+                  className={coldIconButtonClass}
+                  size="icon"
+                  variant="default"
+                  onClick={() => setRefreshTick(value => value + 1)}
+                  title={t('common.refresh')}
+                >
                   <RefreshCw className="h-4 w-4" aria-hidden="true" />
                   <span className="sr-only">{t('common.refresh')}</span>
                 </Button>
@@ -954,6 +1126,7 @@ export default function AlertNoticePage() {
               <div className="flex min-w-0 flex-wrap items-center gap-2">
                 <Select
                   data-alert-notice-template-preset-filter="cold-select"
+                  data-alert-notice-template-preset-query="server-param"
                   aria-label={t('monitor.status')}
                   value={templatePresetFilter ? 'true' : 'false'}
                   onChange={event => {
@@ -968,6 +1141,8 @@ export default function AlertNoticePage() {
                 </Select>
                 <SearchRow
                   data-alert-notice-template-search="shared-compact"
+                  data-alert-notice-template-search-submit="angular-enter-and-clear"
+                  data-alert-notice-template-search-submit-owner="cold-search-row"
                   className="mb-0"
                   value={templateSearchDraft}
                   placeholder={t('alert.notice.template.name')}
@@ -1001,7 +1176,7 @@ export default function AlertNoticePage() {
                       <td className={`px-3 py-3 text-center font-medium ${coldTextPrimaryClass}`}>{template.name || t('alert.notice.template')}</td>
                       <td className="px-3 py-3 text-center">
                         <div className="flex justify-center">
-                          <AlertSurfaceValuePill>{getNoticeTypeLabel(template.type, t)}</AlertSurfaceValuePill>
+                          <AlertSurfaceValuePill>{getNoticeTemplateTypeLabel(template.type, t, emptyValue)}</AlertSurfaceValuePill>
                         </div>
                       </td>
                       <td className="px-3 py-3 text-center">
@@ -1027,6 +1202,8 @@ export default function AlertNoticePage() {
                           ) : (
                             <>
                               <Button
+                                data-alert-notice-template-edit-detail="angular-detail-fetch"
+                                data-alert-notice-template-edit-detail-owner="route-detail-fetch-contract"
                                 className={coldIconButtonClass}
                                 size="icon"
                                 variant="default"
@@ -1057,6 +1234,7 @@ export default function AlertNoticePage() {
                 pageIndex={normalizedTemplatePageIndex}
                 pageSize={templatePageSize}
                 totalElements={templateTotalElements}
+                visibleCount={templateVisibleRows.length}
                 testIdPrefix="notice-template"
                 onPageIndexChange={setTemplatePageIndex}
                 onPageSizeChange={nextPageSize => {
@@ -1084,7 +1262,18 @@ export default function AlertNoticePage() {
                 <Button data-testid="notice-receiver-cancel" className={coldButtonClassName} size="sm" variant="default" onClick={() => setEditingReceiver(false)}>
                   {t('common.cancel')}
                 </Button>
-                <Button data-testid="notice-receiver-test" className={coldButtonClassName} size="sm" variant="default" onClick={() => void handleTestSend()}>
+                <Button
+                  data-testid="notice-receiver-test"
+                  data-alert-notice-receiver-test-loading={testingReceiver ? 'true' : 'false'}
+                  data-alert-notice-receiver-test-validation="angular-backend-owned"
+                  data-alert-notice-receiver-test-validation-owner="route-mutation-contract"
+                  aria-busy={testingReceiver}
+                  className={coldButtonClassName}
+                  size="sm"
+                  variant="default"
+                  onClick={() => void handleTestSend()}
+                  disabled={testingReceiver || savingReceiver}
+                >
                   {t('alert.notice.send-test')}
                 </Button>
                 <Button data-testid="notice-receiver-save" className={coldPrimaryButtonClassName} size="sm" variant="default" onClick={() => void handleSaveReceiver()} disabled={savingReceiver}>
@@ -1094,13 +1283,37 @@ export default function AlertNoticePage() {
             }
           >
             <div data-alert-notice-receiver-editor-dialog="cold-modal-editor" className="space-y-3">
+              <div
+                data-alert-notice-save-feedback="angular-new-edit-notify"
+                data-alert-notice-save-feedback-owner="route-action-feedback-contract"
+                data-alert-notice-receiver-success-next="angular-policy-next"
+                data-alert-notice-receiver-success-next-owner="route-action-feedback-contract"
+                data-alert-notice-receiver-save-failure-close="angular-transport-error-close"
+                data-alert-notice-receiver-save-failure-close-owner="route-action-feedback-contract"
+                className="hidden"
+                aria-hidden="true"
+              />
               {editingReceiver && receiverError ? (
                 <div
                   role="alert"
                   data-alert-notice-receiver-validation="cold-validation-feedback"
+                  data-alert-notice-receiver-save-failure={receiverErrorDetail ? 'angular-notify-title-detail' : undefined}
+                  data-alert-notice-receiver-save-failure-owner={receiverErrorDetail ? 'route-action-feedback-contract' : undefined}
+                  data-alert-notice-receiver-save-failure-title={receiverErrorDetail ? receiverError : undefined}
+                  data-alert-notice-receiver-save-failure-detail={receiverErrorDetail ?? undefined}
                   className="rounded-[3px] border border-[#6f3141] bg-[#1b1014] px-3 py-2 text-[12px] font-semibold leading-5 text-[#ffb4c1]"
                 >
-                  {receiverError}
+                  <span>{receiverError}</span>
+                  {receiverErrorDetail ? <span className="mt-1 block font-medium text-[#ffccd5]">{receiverErrorDetail}</span> : null}
+                </div>
+              ) : null}
+              {editingReceiver && receiverMessage ? (
+                <div
+                  role="status"
+                  data-alert-notice-receiver-test-feedback="cold-test-feedback"
+                  className="rounded-[3px] border border-[#24563d] bg-[#0d1a14] px-3 py-2 text-[12px] font-semibold leading-5 text-[#9be8bd]"
+                >
+                  {receiverMessage}
                 </div>
               ) : null}
               <AlertNoticeReceiverFields
@@ -1135,20 +1348,44 @@ export default function AlertNoticePage() {
                 <Button className={coldButtonClassName} size="sm" variant="default" onClick={() => setEditingRule(false)}>
                   {t('common.cancel')}
                 </Button>
-                <Button className={coldPrimaryButtonClassName} size="sm" variant="default" onClick={() => void handleSaveRule()} disabled={savingRule}>
+                <Button
+                  className={coldPrimaryButtonClassName}
+                  size="sm"
+                  variant="default"
+                  data-alert-notice-rule-save-loading="angular-nz-ok-loading"
+                  data-alert-notice-rule-save-loading-owner="route-modal-ok-contract"
+                  data-alert-notice-rule-save-loading-state={savingRule ? 'true' : 'false'}
+                  aria-busy={savingRule}
+                  onClick={() => void handleSaveRule()}
+                  disabled={savingRule}
+                >
                   {savingRule ? t('common.saving') : t('common.save')}
                 </Button>
               </div>
             }
           >
-            <div data-alert-notice-rule-editor-dialog="cold-modal-editor" className="space-y-3">
+            <div
+              data-alert-notice-rule-editor-dialog="cold-modal-editor"
+              data-alert-notice-rule-display-names="angular-save-payload"
+              data-alert-notice-rule-display-names-owner="route-payload-contract"
+              data-alert-notice-rule-edit-display-names="angular-detail-options"
+              data-alert-notice-rule-edit-display-names-owner="route-payload-contract"
+              data-alert-notice-save-feedback="angular-new-edit-notify"
+              data-alert-notice-save-feedback-owner="route-action-feedback-contract"
+              className="space-y-3"
+            >
               {editingRule && ruleError ? (
                 <div
                   role="alert"
                   data-alert-notice-rule-validation="cold-validation-feedback"
+                  data-alert-notice-rule-save-failure={ruleErrorDetail ? 'angular-notify-title-detail' : undefined}
+                  data-alert-notice-rule-save-failure-owner={ruleErrorDetail ? 'route-action-feedback-contract' : undefined}
+                  data-alert-notice-rule-save-failure-title={ruleErrorDetail ? ruleError : undefined}
+                  data-alert-notice-rule-save-failure-detail={ruleErrorDetail ?? undefined}
                   className="rounded-[3px] border border-[#6f3141] bg-[#1b1014] px-3 py-2 text-[12px] font-semibold leading-5 text-[#ffb4c1]"
                 >
-                  {ruleError}
+                  <span>{ruleError}</span>
+                  {ruleErrorDetail ? <span className="mt-1 block font-medium text-[#ffccd5]">{ruleErrorDetail}</span> : null}
                 </div>
               ) : null}
               <AlertNoticeRuleFields
@@ -1183,12 +1420,14 @@ export default function AlertNoticePage() {
                   className={coldButtonClassName}
                   size="sm"
                   variant="default"
+                  data-alert-notice-template-viewer-return={templateReadOnly ? 'angular-cancel-return' : undefined}
+                  data-alert-notice-template-viewer-return-owner={templateReadOnly ? 'route-modal-footer-contract' : undefined}
                   onClick={() => {
                     setEditingTemplate(false);
                     setTemplateReadOnly(false);
                   }}
                 >
-                  {t('common.cancel')}
+                  {templateReadOnly ? t('common.button.return') : t('common.cancel')}
                 </Button>
                 {!templateReadOnly ? (
                   <Button className={coldPrimaryButtonClassName} size="sm" variant="default" onClick={() => void handleSaveTemplate()} disabled={savingTemplate}>
@@ -1199,19 +1438,34 @@ export default function AlertNoticePage() {
             }
           >
             {templateReadOnly ? (
-              <div data-alert-notice-template-viewer-dialog="cold-modal-viewer" className="space-y-3">
+              <div
+                data-alert-notice-template-viewer-dialog="cold-modal-viewer"
+                data-alert-notice-template-viewer-ok="none"
+                data-alert-notice-template-viewer-ok-owner="route-modal-footer-contract"
+                className="space-y-3"
+              >
                 <AlertNoticeTemplateFields t={t} draft={templateDraft} readOnly={templateReadOnly} onDraftChange={setTemplateDraft} />
               </div>
             ) : (
-              <div data-alert-notice-template-editor-dialog="cold-modal-editor" className="space-y-3">
+              <div
+                data-alert-notice-template-editor-dialog="cold-modal-editor"
+                data-alert-notice-save-feedback="angular-new-edit-notify"
+                data-alert-notice-save-feedback-owner="route-action-feedback-contract"
+                className="space-y-3"
+              >
                 {editingTemplate && templateError ? (
                   <div
-                    role="alert"
-                    data-alert-notice-template-validation="cold-validation-feedback"
-                    className="rounded-[3px] border border-[#6f3141] bg-[#1b1014] px-3 py-2 text-[12px] font-semibold leading-5 text-[#ffb4c1]"
-                  >
-                    {templateError}
-                  </div>
+                  role="alert"
+                  data-alert-notice-template-validation="cold-validation-feedback"
+                  data-alert-notice-template-save-failure={templateErrorDetail ? 'angular-notify-title-detail' : undefined}
+                  data-alert-notice-template-save-failure-owner={templateErrorDetail ? 'route-action-feedback-contract' : undefined}
+                  data-alert-notice-template-save-failure-title={templateErrorDetail ? templateError : undefined}
+                  data-alert-notice-template-save-failure-detail={templateErrorDetail ?? undefined}
+                  className="rounded-[3px] border border-[#6f3141] bg-[#1b1014] px-3 py-2 text-[12px] font-semibold leading-5 text-[#ffb4c1]"
+                >
+                  <span>{templateError}</span>
+                  {templateErrorDetail ? <span className="mt-1 block font-medium text-[#ffccd5]">{templateErrorDetail}</span> : null}
+                </div>
                 ) : null}
                 <AlertNoticeTemplateFields t={t} draft={templateDraft} readOnly={templateReadOnly} onDraftChange={setTemplateDraft} />
               </div>
@@ -1224,6 +1478,8 @@ export default function AlertNoticePage() {
             <div
               data-alert-notice-surface="otlp-cold-notice-console"
               data-alert-notice-style-baseline={coldNoticeVisual.canvasName}
+              data-alert-notice-edit-load-feedback="angular-edit-fail"
+              data-alert-notice-edit-load-feedback-owner="route-action-feedback-contract"
               className={coldNoticeVisual.canvas.root}
               style={coldNoticeVisual.canvas.backgroundStyle}
             >
@@ -1304,8 +1560,11 @@ export default function AlertNoticePage() {
                           </a>
                         ) : null}
                       </div>
-                      <div className="mt-3 rounded-[3px] border border-[#222a34] bg-[#080a0e] px-3 py-2 font-mono text-[11px] leading-5 text-[#9aa5b5]">
-                        {noticeEvidenceContext.labelsText || '-'}
+                      <div
+                        data-alert-notice-evidence-labels="generated-labels"
+                        className="mt-3 rounded-[3px] border border-[#222a34] bg-[#080a0e] px-3 py-2 font-mono text-[11px] leading-5 text-[#9aa5b5]"
+                      >
+                        {noticeEvidenceContext.labelsText || emptyValue}
                       </div>
                       <div className="mt-3 grid gap-2 md:grid-cols-3 xl:grid-cols-5">
                         {noticeEvidenceContext.rows.map(row => (
@@ -1324,23 +1583,46 @@ export default function AlertNoticePage() {
             {receiverEditorDialog}
             {ruleEditorDialog}
             {templateEditorDialog}
-            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
-              <ColdConfirmDialog
+            <div
+              data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}
+              data-alert-notice-delete-confirm="angular-modal-confirm"
+              data-alert-notice-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+              data-alert-notice-delete-page-clamp="angular-update-page-index"
+              data-alert-notice-delete-page-clamp-owner="route-state-contract"
+              data-alert-notice-delete-feedback="angular-delete-notify"
+              data-alert-notice-delete-feedback-owner="route-action-feedback-contract"
+            >
+              <HzConfirmDialog
                 open={Boolean(deleteRequest)}
+                tone="critical"
+                kicker={t('common.confirm.operation')}
                 title={t('common.confirm.delete')}
-                copy={
-                  deleteRequest?.kind === 'receiver'
-                    ? t('alert.notice.delete.confirm.receiver')
-                    : deleteRequest?.kind === 'template'
-                      ? t('alert.notice.delete.confirm.template')
-                      : t('alert.notice.delete.confirm.rule')
-                }
                 confirmLabel={t('common.button.ok')}
                 cancelLabel={t('common.button.cancel')}
-                pending={deletePending}
-                onCancel={() => setDeleteRequest(null)}
+                confirmDisabled={deletePending}
+                onClose={deletePending ? undefined : () => setDeleteRequest(null)}
                 onConfirm={() => void handleConfirmedDelete()}
-              />
+                data-alert-notice-delete-confirm-dialog="angular-modal-confirm"
+                confirmButtonProps={
+                  {
+                    'data-alert-notice-delete-confirm-ok': 'angular-modal-confirm'
+                  } as React.ComponentProps<typeof HzConfirmDialog>['confirmButtonProps']
+                }
+                cancelButtonProps={
+                  {
+                    disabled: deletePending,
+                    'data-alert-notice-delete-confirm-cancel': 'angular-modal-confirm'
+                  } as React.ComponentProps<typeof HzConfirmDialog>['cancelButtonProps']
+                }
+              >
+                <p data-alert-notice-delete-confirm-copy="angular-modal-confirm">
+                  {deleteRequest?.kind === 'receiver'
+                    ? t('alert.notice.delete.confirm.receiver')
+                    : deleteRequest?.kind === 'template'
+                      ? t('alert.notice.delete.confirm.template')
+                      : t('alert.notice.delete.confirm.rule')}
+                </p>
+              </HzConfirmDialog>
             </div>
           </>
         )
diff --git a/web-next/app/alert/notice/page.test.tsx b/web-next/app/alert/notice/page.test.tsx
index aa77184840..51dba30aaa 100644
--- a/web-next/app/alert/notice/page.test.tsx
+++ b/web-next/app/alert/notice/page.test.tsx
@@ -4,10 +4,10 @@ import Link from 'next/link';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { AlertNoticeRouteState } from '../../../lib/alert-notice/query-state';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   renderData: {
     receivers: {
@@ -81,6 +81,29 @@ const mockState = vi.hoisted(() => ({
       totalElements: 4,
       pageIndex: 0,
       pageSize: 8
+    },
+    templateOptions: {
+      content: [
+        {
+          id: 9,
+          name: 'WebhookTemplate',
+          preset: true,
+          type: 2,
+          content: 'Preset template body',
+          gmtUpdate: 1712730000000
+        },
+        {
+          id: 10,
+          name: 'CustomTemplate',
+          preset: false,
+          type: 2,
+          content: 'Custom template body',
+          gmtUpdate: 1712730000000
+        }
+      ],
+      totalElements: 2,
+      pageIndex: 0,
+      pageSize: 2
     }
   }
 }));
@@ -95,13 +118,6 @@ vi.mock('next/link', () => ({
   )
 }));
 
-vi.mock('next/navigation', () => ({
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
-}));
-
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
@@ -123,13 +139,15 @@ vi.mock('../../../components/providers/i18n-provider', () => ({
 vi.mock('../../../components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     load,
+    loadingCopy,
     children
   }: {
     load: () => Promise<unknown>;
+    loadingCopy?: string;
     children: (data: any) => React.ReactNode;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return <div data-client-workbench="true" data-loading-copy={loadingCopy}>{children(mockState.renderData)}</div>;
   }
 }));
 
@@ -209,6 +227,29 @@ vi.mock('../../../components/pages/alert-notice-console-shell', () => ({
 }));
 
 vi.mock('../../../lib/alert-notice/controller', () => ({
+  buildNoticeListUrl: (path: '/notice/receivers' | '/notice/rules', query: { search?: string; pageIndex?: number; pageSize?: number } = {}) => {
+    const params = new URLSearchParams({
+      pageIndex: String(query.pageIndex ?? 0),
+      pageSize: String(query.pageSize ?? 8)
+    });
+    const search = query.search?.trim();
+    if (search) {
+      params.set('name', search);
+    }
+    return `${path}?${params.toString()}`;
+  },
+  buildNoticeTemplateListUrl: (query: { search?: string; pageIndex?: number; pageSize?: number; preset?: boolean } = {}) => {
+    const params = new URLSearchParams({
+      pageIndex: String(query.pageIndex ?? 0),
+      pageSize: String(query.pageSize ?? 8)
+    });
+    const search = query.search?.trim();
+    if (search) {
+      params.set('name', search);
+    }
+    params.set('preset', String(query.preset ?? true));
+    return `/notice/templates?${params.toString()}`;
+  },
   buildNoticeRuleDraft: () => ({
     name: '',
     receiverIdsText: '',
@@ -220,6 +261,10 @@ vi.mock('../../../lib/alert-notice/controller', () => ({
     periodStart: '09:00',
     periodEnd: '18:00'
   }),
+  buildNoticeRuleDisplayNames: vi.fn(() => ({
+    receiverName: ['Receiver page 0-邮箱'],
+    templateName: 'WebhookTemplate'
+  })),
   createNoticeReceiver: vi.fn(),
   createNoticeRule: vi.fn(),
   createNoticeTemplate: vi.fn(),
@@ -227,6 +272,7 @@ vi.mock('../../../lib/alert-notice/controller', () => ({
   deleteNoticeRule: vi.fn(),
   deleteNoticeTemplate: vi.fn(),
   loadAlertNoticeData: mockLoadAlertNoticeData,
+  loadAlertNoticeDataFromFacade: mockLoadAlertNoticeData,
   loadNoticeReceiverDetail: vi.fn(),
   loadNoticeRuleDetail: vi.fn(),
   loadNoticeTemplateDetail: vi.fn(),
@@ -285,6 +331,10 @@ vi.mock('../../../lib/alert-label-options', () => ({
   loadAlertLabelOptions: vi.fn(async () => ({
     keys: ['alertname', 'severity', 'service'],
     valuesByKey: { severity: ['critical', 'warning'], service: ['checkout'] }
+  })),
+  loadAlertLabelOptionsFromFacade: vi.fn(async () => ({
+    keys: ['alertname', 'severity', 'service'],
+    valuesByKey: { severity: ['critical', 'warning'], service: ['checkout'] }
   }))
 }));
 
@@ -292,17 +342,25 @@ vi.mock('../../../lib/format', () => ({
   formatTime: () => '2026-04-10 18:00:00'
 }));
 
+const EMPTY_ROUTE_STATE: AlertNoticeRouteState = {
+  signal: null,
+  signalContext: {}
+};
+
+async function renderAlertNoticePage(initialRouteState: AlertNoticeRouteState = EMPTY_ROUTE_STATE) {
+  const { default: AlertNoticePage } = await import('./alert-notice-page');
+  return renderToStaticMarkup(<AlertNoticePage initialRouteState={initialRouteState} />);
+}
+
 describe('alert notice page', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams();
     mockState.lastLoad = null;
     mockLoadAlertNoticeData.mockClear().mockResolvedValue(mockState.renderData);
   });
 
   it('renders the OTLP cold notice tab shell with the receiver console selected by default', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
-    const { default: AlertNoticePage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertNoticePage />);
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const html = await renderAlertNoticePage();
 
     expect(html).toContain('消息通知');
     expect(html).not.toContain('工作区:alert/notice');
@@ -310,6 +368,7 @@ describe('alert notice page', () => {
     expect(html).not.toContain('规则:17');
     expect(html).not.toContain('模板:4');
     expect(html).toContain('data-alert-notice-console="true"');
+    expect(html).toContain('data-loading-copy="正在加载通知中心"');
     expect(html).toContain('data-selected-tab="receiver"');
     expect(html).toContain('data-tab="receiver"');
     expect(html).toContain('data-tab="rule"');
@@ -325,6 +384,10 @@ describe('alert notice page', () => {
     expect(html).toContain('data-alert-notice-receiver-toolbar="cold-query-toolbar"');
     expect(html).toContain('data-alert-notice-receiver-toolbar-layout="compact-inline-actions-query"');
     expect(html).toContain('data-alert-notice-receiver-search="shared-compact"');
+    expect(html).toContain('data-alert-notice-receiver-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-alert-notice-receiver-search-submit-owner="cold-search-row"');
+    expect(html).toContain('data-alert-notice-receiver-sync="angular-load-table"');
+    expect(html).toContain('data-alert-notice-receiver-sync-owner="route-refresh-contract"');
     expect(html).toContain('data-cold-search-row-owner="cold-search-row"');
     expect(html).toContain('data-cold-search-layout="compact-detached-button"');
     expect(html).toContain('data-cold-search-input="fixed-width-direct"');
@@ -333,6 +396,16 @@ describe('alert notice page', () => {
     expect(html).not.toContain('data-cold-search-input-shell');
     expect(html).toContain('data-cold-search-action="submit"');
     expect(html).toContain('data-alert-notice-receiver-table-shell="cold-dense-table"');
+    expect(html).toContain('data-alert-notice-pagination="cold-dense-pagination"');
+    expect(html).toContain('data-alert-notice-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-hz-ui="pagination-bar"');
+    expect(html).toContain('data-hz-pagination-page-size="select-menu"');
+    expect(html).toContain('data-hz-pagination-page-jump="number-input"');
+    expect(html).toContain('data-alert-notice-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-alert-notice-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('第 1 / 3 页 · 1-1 / 17');
+    expect(html).toContain('每页条数');
+    expect(html).toContain('页码');
     expect(html).toContain('data-alert-notice-receiver-row="7"');
     expect(html).toContain('新增接收对象');
     expect(html).toContain('接收对象名称');
@@ -369,16 +442,38 @@ describe('alert notice page', () => {
     expect(source).toContain('coldButtonClassName');
     expect(source).toContain('coldPrimaryButtonClassName');
     expect(source).toContain('coldCommandButtonClass');
+    expect(source).toContain("from '@hertzbeat/ui'");
+    expect(source).toContain('HzConfirmDialog');
+    expect(source).toContain('HzPaginationBar');
     expect(source).toContain("from '../../../components/ui/search-row'");
     expect(source).toContain("from '../../../lib/alert-label-options'");
-    expect(source).toContain('loadAlertLabelOptions(apiMessageGet)');
+    expect(source).toContain('loadAlertLabelOptionsFromFacade(api.alertLabels.list)');
+    expect(source).toContain('loadAlertNoticeDataFromFacade');
+    expect(source).toContain('receivers: api.alertNotice.receivers.list');
+    expect(source).toContain('rules: api.alertNotice.rules.list');
+    expect(source).toContain('receiverOptions: api.alertNotice.receivers.options');
+    expect(source).toContain('templates: api.alertNotice.templates.list');
+    expect(source).toContain('templateOptions: api.alertNotice.templates.options');
+    expect(source).not.toContain('loadAlertNoticeData(apiMessageGet, alertNoticeLoadQuery)');
+    expect(source).not.toContain('loadAlertLabelOptions(apiMessageGet)');
+    expect(source).not.toContain("from '../../../lib/api-client'");
     expect(source).toContain('labelOptions={labelOptions}');
     expect(source).toContain('data-alert-notice-receiver-toolbar="cold-query-toolbar"');
     expect(source).toContain('data-alert-notice-rule-toolbar="cold-query-toolbar"');
     expect(source).toContain('data-alert-notice-template-toolbar="cold-query-toolbar"');
+    expect(source).toContain('data-alert-notice-receiver-sync="angular-load-table"');
+    expect(source).toContain('data-alert-notice-receiver-sync-owner="route-refresh-contract"');
+    expect(source).toContain('data-alert-notice-rule-sync="angular-load-table"');
+    expect(source).toContain('data-alert-notice-rule-sync-owner="route-refresh-contract"');
+    expect(source).toContain('data-alert-notice-template-sync="angular-load-table"');
+    expect(source).toContain('data-alert-notice-template-sync-owner="route-refresh-contract"');
     expect(source).toContain('data-alert-notice-receiver-search="shared-compact"');
+    expect(source).toContain('data-alert-notice-receiver-search-submit="angular-enter-and-clear"');
+    expect(source).toContain('data-alert-notice-receiver-search-submit-owner="cold-search-row"');
     expect(source).toContain('data-alert-notice-rule-search="shared-compact"');
     expect(source).toContain('data-alert-notice-template-search="shared-compact"');
+    expect(source).toContain('data-alert-notice-template-search-submit="angular-enter-and-clear"');
+    expect(source).toContain('data-alert-notice-template-search-submit-owner="cold-search-row"');
     expect(source).not.toContain('data-alert-notice-receiver-toolbar="cold-table-toolbar"');
     expect(source).not.toContain('data-alert-notice-rule-toolbar="cold-table-toolbar"');
     expect(source).not.toContain('data-alert-notice-template-toolbar="cold-table-toolbar"');
@@ -404,23 +499,153 @@ describe('alert notice page', () => {
     expect(source).not.toContain('NoticeListToolbar');
     expect(source).not.toContain('buildNoticeFacts');
     expect(source).not.toContain("from '../../../components/workbench/primitives'");
-  });
+  }, 30_000);
+
+  it('renders missing receiver table settings with the localized empty fallback', async () => {
+    const originalRenderData = mockState.renderData;
+    mockState.renderData = {
+      ...mockState.renderData,
+      receivers: {
+        ...mockState.renderData.receivers,
+        content: [
+          {
+            id: 8,
+            name: 'Receiver missing setting',
+            email: ' ',
+            type: 1,
+            gmtUpdate: 1712730000000
+          }
+        ],
+        totalElements: 1
+      }
+    };
+
+    try {
+      const html = await renderAlertNoticePage();
+
+      expect(html).toContain('Receiver missing setting');
+      expect(html).toContain('title="无"');
+      expect(html).not.toContain('title="-"');
+    } finally {
+      mockState.renderData = originalRenderData;
+    }
+  }, 30_000);
+
+  it('renders missing receiver and template type badges with the localized empty fallback', async () => {
+    const originalRenderData = mockState.renderData;
+    mockState.renderData = {
+      ...mockState.renderData,
+      receivers: {
+        ...mockState.renderData.receivers,
+        content: [
+          {
+            id: 9,
+            name: 'Receiver missing type',
+            email: 'ops@example.com',
+            type: ' ',
+            gmtUpdate: 1712730000000
+          }
+        ],
+        totalElements: 1
+      },
+      templates: {
+        ...mockState.renderData.templates,
+        content: [
+          {
+            id: 11,
+            name: 'Template missing type',
+            type: null,
+            preset: true,
+            content: 'Template body',
+            gmtUpdate: 1712730000000
+          }
+        ],
+        totalElements: 1
+      }
+    };
+
+    try {
+      const html = await renderAlertNoticePage();
+      const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+
+      expect(html).toContain('Receiver missing type');
+      expect(html).toContain('>无</span>');
+      expect(html).not.toContain('>-</span>');
+      expect(source).toContain('getNoticeTemplateTypeLabel(template.type, t, emptyValue)');
+    } finally {
+      mockState.renderData = originalRenderData;
+    }
+  }, 30_000);
+
+  it('keeps template Telegram type labels separate from receiver Telegram bot copy', async () => {
+    const originalRenderData = mockState.renderData;
+    mockState.renderData = {
+      ...mockState.renderData,
+      templates: {
+        ...mockState.renderData.templates,
+        content: [
+          {
+            id: 17,
+            name: 'TelegramTemplate',
+            type: 7,
+            preset: true,
+            content: 'Telegram template body',
+            gmtUpdate: 1712730000000
+          }
+        ],
+        totalElements: 1
+      }
+    };
+
+    try {
+      const html = await renderAlertNoticePage();
+      const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+
+      expect(html).toContain('data-selected-tab="receiver"');
+      expect(source).toContain('const NOTICE_TEMPLATE_TYPE_LABEL_KEYS');
+      expect(source).toContain("'7': 'alert.notice.type.telegram'");
+      expect(source).toContain("'7': 'alert.notice.type.telegram-bot'");
+      expect(source).toContain('getNoticeTemplateTypeLabel(template.type, t, emptyValue)');
+      expect(source).toContain('data-alert-notice-template-telegram-label="angular-template-telegram"');
+      expect(source).toContain('data-alert-notice-template-telegram-label-owner="route-i18n-contract"');
+      expect(source).toContain('<AlertSurfaceValuePill>{getNoticeTemplateTypeLabel(template.type, t, emptyValue)}</AlertSurfaceValuePill>');
+      expect(source).not.toContain('<AlertSurfaceValuePill>{getNoticeTypeLabel(template.type, t, emptyValue)}</AlertSurfaceValuePill>');
+    } finally {
+      mockState.renderData = originalRenderData;
+    }
+  }, 30_000);
 
   it('keeps the rule tab on the OTLP cold table contract when the console switches tabs', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
 
     expect(source).toContain('data-alert-notice-rule-panel="true"');
+    expect(source).toContain('data-alert-notice-rule-template-display="angular-template-id-fallback"');
+    expect(source).toContain('data-alert-notice-rule-template-display-owner="route-table-contract"');
+    expect(source).toContain('data-alert-notice-rule-receiver-display="angular-array-interpolation"');
+    expect(source).toContain('data-alert-notice-rule-receiver-display-owner="route-table-contract"');
     expect(source).toContain('data-alert-notice-rule-toolbar="cold-query-toolbar"');
     expect(source).toContain('data-alert-notice-rule-toolbar-layout="compact-inline-actions-query"');
+    expect(source).toContain('data-alert-notice-rule-search-submit="angular-enter-and-clear"');
+    expect(source).toContain('data-alert-notice-rule-search-submit-owner="cold-search-row"');
+    expect(source).toContain('onSearch={commitRuleSearch}');
+    expect(source).toContain('onClear={ruleSearchDraft || ruleSearch ? resetRuleSearch : undefined}');
+    expect(source).toContain('setRulePageIndex(0);');
     expect(source).toContain('data-alert-notice-rule-table-shell="cold-dense-table"');
     expect(source).toContain('function NoticeTableSwitch');
     expect(source).toContain('role="switch"');
     expect(source).toContain('data-alert-notice-rule-table-switch={field}');
+    expect(source).toContain('data-alert-notice-rule-table-switch-update="angular-edit-notify"');
+    expect(source).toContain('data-alert-notice-rule-table-switch-update-owner="route-action-feedback-contract"');
     expect(source).toContain('handleToggleRuleSwitch');
-    expect(source).toContain("updateNoticeRule(apiMessagePut as any, buildNoticeRuleDraft({ ...rule, [field]: checked }));");
+    expect(source).toContain('receiverName: rule.receiverName ?? []');
+    expect(source).toContain('templateName: rule.templateId ? rule.templateName ?? null : null');
+    expect(source).toContain("{rule.templateId ? rule.templateName || t('alert.notice.template.preset.true') : t('alert.notice.template.preset.true')}");
+    expect(source).toContain("setRuleMessage(t('common.notify.edit-success'))");
+    expect(source).toContain("t('common.notify.edit-fail')");
+    expect(source).not.toContain("t('common.save-failed')");
     expect(source).toContain('async function handleEditRule(rule = selectedRule)');
     expect(source).toContain('const ruleId = rule?.id;');
-    expect(source).toContain('const detail = await loadNoticeRuleDetail(apiMessageGet, ruleId);');
+    expect(source).toContain('const detail = await api.alertNotice.rules.detail(ruleId);');
     expect(source).toContain('setSelectedRuleId(ruleId);');
     expect(source).toContain('setRuleDraft(buildNoticeRuleDraft(detail));');
     expect(source).toContain('onClick={() => void handleEditRule(rule)}');
@@ -431,25 +656,150 @@ describe('alert notice page', () => {
     expect(source).toContain("t('alert.notice.template.name')");
     expect(source).toContain("t('alert.notice.rule.all')");
     expect(source).toContain("t('common.enable')");
+    expect(source).toContain("filter(Boolean).join(',')");
+    expect(source).not.toContain("filter(Boolean).join(', ')");
     expect(source).not.toContain('accent-[var(--ops-primary)]');
     expect(source).not.toContain('<input type="checkbox"');
     expect(source).not.toContain('data-alert-notice-rule-table-checkbox');
     expect(source).not.toContain('setRuleDraft(buildNoticeRuleDraft(rule));');
     expect(source).not.toContain('buildNoticeRuleRows');
-  });
+  }, 30_000);
+
+  it('renders notice rule receiver arrays with Angular interpolation spacing', async () => {
+    const originalRenderData = mockState.renderData;
+    mockState.renderData = {
+      ...mockState.renderData,
+      rules: {
+        ...mockState.renderData.rules,
+        content: [
+          {
+            id: 8,
+            name: 'Multi receiver rule',
+            enable: true,
+            filterAll: true,
+            receiverName: ['ops-email', 'pager-webhook'],
+            templateId: 9,
+            templateName: 'Default',
+            gmtUpdate: 1712730000000
+          }
+        ],
+        totalElements: 1
+      }
+    };
+
+    try {
+      const html = await renderAlertNoticePage({
+        signal: 'metrics',
+        signalContext: {}
+      });
+
+      expect(html).toContain('data-alert-notice-rule-receiver-display="angular-array-interpolation"');
+      expect(html).toContain('ops-email,pager-webhook');
+      expect(html).not.toContain('ops-email, pager-webhook');
+    } finally {
+      mockState.renderData = originalRenderData;
+    }
+  }, 30_000);
+
+  it('renders missing rule receiver cells with the localized no-receiver fallback', async () => {
+    const originalRenderData = mockState.renderData;
+    mockState.renderData = {
+      ...mockState.renderData,
+      rules: {
+        ...mockState.renderData.rules,
+        content: [
+          {
+            id: 6,
+            name: 'Rule without receivers',
+            enable: true,
+            filterAll: false,
+            receiverName: [' ', ''],
+            templateId: null,
+            templateName: '',
+            gmtUpdate: 1712730000000
+          }
+        ],
+        totalElements: 1
+      }
+    };
+
+    try {
+      const html = await renderAlertNoticePage({
+        signal: 'metrics',
+        signalContext: {}
+      });
+
+      expect(html).toContain('data-selected-tab="rule"');
+      expect(html).toContain('Rule without receivers');
+      expect(html).toContain('未绑定接收对象');
+      expect(html).toContain('系统内置模版');
+      expect(html).not.toContain('<td class="px-3 py-3 text-center">-</td>');
+    } finally {
+      mockState.renderData = originalRenderData;
+    }
+  }, 30_000);
+
+  it('renders preset template fallback from templateId instead of stale templateName', async () => {
+    const originalRenderData = mockState.renderData;
+    mockState.renderData = {
+      ...mockState.renderData,
+      rules: {
+        ...mockState.renderData.rules,
+        content: [
+          {
+            id: 7,
+            name: 'Preset template rule',
+            enable: true,
+            filterAll: true,
+            receiverName: ['ops-email'],
+            templateId: null,
+            templateName: 'Stale template',
+            gmtUpdate: 1712730000000
+          }
+        ],
+        totalElements: 1
+      }
+    };
+
+    try {
+      const html = await renderAlertNoticePage({
+        signal: 'metrics',
+        signalContext: {}
+      });
+
+      expect(html).toContain('data-alert-notice-rule-template-display="angular-template-id-fallback"');
+      expect(html).toContain('Preset template rule');
+      expect(html).toContain('系统内置模版');
+      expect(html).not.toContain('Stale template');
+    } finally {
+      mockState.renderData = originalRenderData;
+    }
+  }, 30_000);
 
   it('keeps the template tab on the OTLP cold table contract when the console switches tabs', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
 
     expect(source).toContain('data-alert-notice-template-panel="true"');
+    expect(source).toContain('data-alert-notice-template-query-owner="backend-paginated"');
+    expect(source).toContain('data-alert-notice-template-query-url={alertNoticeTemplateListUrl}');
+    expect(source).toContain('data-alert-notice-template-preset-query="server-param"');
     expect(source).toContain('data-alert-notice-template-toolbar="cold-query-toolbar"');
     expect(source).toContain('data-alert-notice-template-toolbar-layout="compact-inline-actions-query"');
     expect(source).toContain('data-alert-notice-template-preset-filter="cold-select"');
+    expect(source).toContain('onSearch={commitTemplateSearch}');
+    expect(source).toContain('onClear={templateSearchDraft || templateSearch ? resetTemplateSearch : undefined}');
+    expect(source).toContain('setTemplatePageIndex(0);');
     expect(source).toContain('data-alert-notice-template-table-shell="cold-dense-table"');
     expect(source).toContain('data-alert-notice-pagination="cold-dense-pagination"');
+    expect(source).toContain('data-alert-notice-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(source).toContain('data-alert-notice-pagination-page-jump-owner');
+    expect(source).toContain('data-alert-notice-pagination-page-size-owner');
     expect(source).toContain('testIdPrefix="notice-template"');
-    expect(source).toContain('ChevronLeft');
-    expect(source).toContain('ChevronRight');
+    expect(source).not.toContain('ChevronLeft');
+    expect(source).not.toContain('ChevronRight');
+    expect(source).toContain("t('alert.notice.pagination.summary'");
+    expect(source).toContain("t('alert.notice.pagination.page-size')");
+    expect(source).toContain("t('alert.notice.pagination.page')");
     expect(source).toContain("t('alert.notice.template.name')");
     expect(source).toContain("t('alert.notice.template.type')");
     expect(source).toContain("t('alert.notice.template.preset')");
@@ -458,10 +808,10 @@ describe('alert notice page', () => {
     expect(source).toContain("t('common.edit-time')");
     expect(source).toContain("t('common.edit')");
     expect(source).not.toContain('buildNoticeTemplateRows');
-  });
+  }, 30_000);
 
   it('keeps all notice authoring flows in cold modal dialogs instead of inline cards', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
 
     expect(source).toContain("from '../../../components/workbench/overlay-dialog'");
     expect(source).toContain('<OverlayDialog');
@@ -478,7 +828,7 @@ describe('alert notice page', () => {
   });
 
   it('wires notice policy and template modals to Angular-parity selectors and template viewing', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
 
     expect(source).toContain('const receiverOptionRows = data.receiverOptions?.content?.length ? data.receiverOptions.content : data.receivers.content;');
     expect(source).toContain('const receiverOptions = receiverOptionRows.map');
@@ -492,7 +842,7 @@ describe('alert notice page', () => {
     expect(source).toContain('handleViewTemplate');
     expect(source).toContain('const rowDraft = buildNoticeTemplateDraft(template);');
     expect(source).toContain('setTemplateDraft(rowDraft);');
-    expect(source).toContain('loadNoticeTemplateDetail(apiMessageGet, template.id)');
+    expect(source).toContain('api.alertNotice.templates.detail(template.id)');
     expect(source).toContain('data-alert-notice-template-view-trigger="cold-modal-viewer-trigger"');
     expect(source).toContain('data-alert-notice-template-viewer-dialog="cold-modal-viewer"');
     expect(source).toContain('readOnly={templateReadOnly}');
@@ -503,8 +853,119 @@ describe('alert notice page', () => {
     expect(source).not.toContain('async function handleViewTemplate(template: NoticeTemplate) {\n          if (!template.id) return;');
   });
 
+  it('keeps Angular notice-rule receiver/template names in the save payload', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const controllerSource = readFileSync(resolve(process.cwd(), 'lib/alert-notice/controller.ts'), 'utf8');
+
+    expect(source).toContain('buildNoticeRuleDisplayNames(ruleDraft, receiverOptions, templateOptions)');
+    expect(source).toContain('await api.alertNotice.rules.update(ruleDraft, displayNames)');
+    expect(source).toContain('await api.alertNotice.rules.create(ruleDraft, displayNames)');
+    expect(source).toContain('data-alert-notice-rule-display-names="angular-save-payload"');
+    expect(source).toContain('data-alert-notice-rule-display-names-owner="route-payload-contract"');
+    expect(source).toContain('data-alert-notice-rule-edit-display-names="angular-detail-options"');
+    expect(source).toContain('data-alert-notice-rule-edit-display-names-owner="route-payload-contract"');
+    expect(controllerSource).toContain('receiverName: string[]');
+    expect(controllerSource).toContain('templateName: string | null');
+    expect(controllerSource).toContain('receiverOptions');
+    expect(controllerSource).toContain('templateOptions.find');
+    expect(controllerSource).toContain('draft.receiverName');
+    expect(controllerSource).toContain('draft.templateName');
+    expect(controllerSource).toContain('...(displayNames ? displayNames : {})');
+  });
+
+  it('keeps Angular notice-rule row edit loading the full detail before opening the modal', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleEditRuleSource = source.slice(source.indexOf('async function handleEditRule('), source.indexOf('async function handleSaveRule()'));
+
+    expect(source).toContain('data-alert-notice-rule-edit-detail="angular-detail-fetch"');
+    expect(source).toContain('data-alert-notice-rule-edit-detail-owner="route-detail-fetch-contract"');
+    expect(source).toContain('onClick={() => void handleEditRule(rule)}');
+    expect(handleEditRuleSource).toContain('const detail = await api.alertNotice.rules.detail(ruleId)');
+    expect(handleEditRuleSource).toContain('setRuleDraft(buildNoticeRuleDraft(detail))');
+    expect(handleEditRuleSource).not.toContain('buildNoticeRuleDraft(rule)');
+  });
+
+  it('keeps Angular notice-rule modal OK loading wired to the save request', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleSaveRuleSource = source.slice(source.indexOf('async function handleSaveRule()'), source.indexOf('async function handleDeleteRule()'));
+
+    expect(source).toContain('data-alert-notice-rule-save-loading="angular-nz-ok-loading"');
+    expect(source).toContain('data-alert-notice-rule-save-loading-owner="route-modal-ok-contract"');
+    expect(source).toContain("data-alert-notice-rule-save-loading-state={savingRule ? 'true' : 'false'}");
+    expect(source).toContain('aria-busy={savingRule}');
+    expect(source).toContain('disabled={savingRule}');
+    expect(source).toContain("{savingRule ? t('common.saving') : t('common.save')}");
+    expect(handleSaveRuleSource).toContain('setSavingRule(true);');
+    expect(handleSaveRuleSource).toContain('setSavingRule(false);');
+  });
+
+  it('keeps Angular notice-rule save failure title separate from backend detail', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleSaveRuleSource = source.slice(source.indexOf('async function handleSaveRule()'), source.indexOf('async function handleDeleteRule()'));
+
+    expect(source).toContain('const [ruleErrorDetail, setRuleErrorDetail] = useState<string | null>(null);');
+    expect(source).toContain('data-alert-notice-rule-save-failure={ruleErrorDetail ? \'angular-notify-title-detail\' : undefined}');
+    expect(source).toContain('data-alert-notice-rule-save-failure-owner={ruleErrorDetail ? \'route-action-feedback-contract\' : undefined}');
+    expect(source).toContain('data-alert-notice-rule-save-failure-title={ruleErrorDetail ? ruleError : undefined}');
+    expect(source).toContain('data-alert-notice-rule-save-failure-detail={ruleErrorDetail ?? undefined}');
+    expect(handleSaveRuleSource).toContain("setRuleError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+    expect(handleSaveRuleSource).toContain('setRuleErrorDetail(error instanceof Error ? error.message : null)');
+    expect(handleSaveRuleSource).not.toContain("setRuleError(error instanceof Error ? error.message : t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+  });
+
+  it('keeps Angular notice-template save failure title separate from backend detail', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleSaveTemplateSource = source.slice(source.indexOf('async function handleSaveTemplate()'), source.indexOf('async function handleDeleteTemplate()'));
+
+    expect(source).toContain('const [templateErrorDetail, setTemplateErrorDetail] = useState<string | null>(null);');
+    expect(source).toContain('data-alert-notice-template-save-failure={templateErrorDetail ? \'angular-notify-title-detail\' : undefined}');
+    expect(source).toContain('data-alert-notice-template-save-failure-owner={templateErrorDetail ? \'route-action-feedback-contract\' : undefined}');
+    expect(source).toContain('data-alert-notice-template-save-failure-title={templateErrorDetail ? templateError : undefined}');
+    expect(source).toContain('data-alert-notice-template-save-failure-detail={templateErrorDetail ?? undefined}');
+    expect(handleSaveTemplateSource).toContain("setTemplateError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+    expect(handleSaveTemplateSource).toContain('setTemplateErrorDetail(error instanceof Error ? error.message : null)');
+    expect(handleSaveTemplateSource).not.toContain("setTemplateError(error instanceof Error ? error.message : t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+  });
+
+  it('keeps new notice templates on Angular required type selection before save', () => {
+    const viewModelSource = readFileSync(resolve(process.cwd(), 'lib/alert-notice/view-model.ts'), 'utf8');
+    const fieldsSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-template-fields.tsx'), 'utf8');
+    const routeSource = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+
+    expect(viewModelSource).toContain("type: source?.type == null ? '' : String(source.type)");
+    expect(viewModelSource).toContain("return t('alert.notice.template.validation.type')");
+    expect(fieldsSource).toContain("const typeValue = draft.type || '';");
+    expect(fieldsSource).toContain("t('alert.notice.receiver.type.placeholder')");
+    expect(fieldsSource).toContain('data-alert-notice-template-type-required="angular-required-select"');
+    expect(fieldsSource).toContain('data-alert-notice-template-type-required-owner="route-validation-contract"');
+    expect(routeSource).toContain('<AlertNoticeTemplateFields t={t} draft={templateDraft} readOnly={templateReadOnly} onDraftChange={setTemplateDraft} />');
+  });
+
+  it('keeps notice-rule single Boolean switches unframed inside editor rows', () => {
+    const fieldsSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-rule-fields.tsx'), 'utf8');
+
+    expect(fieldsSource).toContain('data-alert-notice-rule-single-switch-frame="none"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-single-switch-frame-owner="route-form-contract"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-template-type-filter="angular-selected-receiver-type"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-template-type-filter-owner="route-form-contract"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-template-active-type="angular-switch-receiver"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-template-active-type-owner="route-form-contract"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-optional-period-time="angular-form-validity"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-optional-period-time-owner="route-validation-contract"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-period-limit-state="angular-independent-isLimit"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-period-limit-state-owner="route-form-contract"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-edit-option-seeding="angular-detail-options"');
+    expect(fieldsSource).toContain('data-alert-notice-rule-edit-option-seeding-owner="route-form-contract"');
+    expect(fieldsSource).toContain('export function AlertNoticeRuleSwitch');
+    expect(fieldsSource).toContain('aria-label={label}');
+    expect(fieldsSource).toContain('hover:border-[#5f7df6]');
+    expect(fieldsSource).toContain('data-alert-notice-rule-switch-label={row}');
+    expect(fieldsSource).not.toContain('hover:text-white');
+    expect(fieldsSource).not.toContain('inline-flex h-8 items-center gap-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-2');
+  });
+
   it('keeps notice editor validation feedback inside the active cold editor dialogs', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
 
     expect(source).toContain('data-alert-notice-receiver-validation="cold-validation-feedback"');
     expect(source).toContain('data-alert-notice-rule-validation="cold-validation-feedback"');
@@ -518,35 +979,200 @@ describe('alert notice page', () => {
     expect(source).toContain('!editingTemplate && templateError');
   });
 
+  it('keeps Angular create and edit notification keys for all notice save dialogs', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleSaveReceiverSource = source.slice(source.indexOf('async function handleSaveReceiver()'), source.indexOf('async function handleDeleteReceiver()'));
+    const handleSaveTemplateSource = source.slice(source.indexOf('async function handleSaveTemplate()'), source.indexOf('async function handleDeleteTemplate()'));
+    const handleSaveRuleSource = source.slice(source.indexOf('async function handleSaveRule()'), source.indexOf('async function handleDeleteRule()'));
+    const saveSources = [handleSaveReceiverSource, handleSaveTemplateSource, handleSaveRuleSource];
+
+    expect(source).toContain('data-alert-notice-save-feedback="angular-new-edit-notify"');
+    expect(source).toContain('data-alert-notice-save-feedback-owner="route-action-feedback-contract"');
+    for (const saveSource of saveSources) {
+      expect(saveSource).toContain('const isEdit = Boolean(');
+      expect(saveSource).toContain("isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'");
+      expect(saveSource).toContain("isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'");
+      expect(saveSource).not.toContain('common.save-success');
+      expect(saveSource).not.toContain('common.save-failed');
+    }
+  });
+
+  it('keeps Angular receiver save next-step copy on create and edit success', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleSaveReceiverSource = source.slice(source.indexOf('async function handleSaveReceiver()'), source.indexOf('async function handleDeleteReceiver()'));
+    const handleSaveTemplateSource = source.slice(source.indexOf('async function handleSaveTemplate()'), source.indexOf('async function handleDeleteTemplate()'));
+    const handleSaveRuleSource = source.slice(source.indexOf('async function handleSaveRule()'), source.indexOf('async function handleDeleteRule()'));
+
+    expect(source).toContain('data-alert-notice-receiver-success-next="angular-policy-next"');
+    expect(source).toContain('data-alert-notice-receiver-success-next-owner="route-action-feedback-contract"');
+    expect(handleSaveReceiverSource).toContain("t('alert.notice.receiver.next')");
+    expect(handleSaveReceiverSource).toContain("setReceiverMessage([t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'), t('alert.notice.receiver.next')].join(' '))");
+    expect(handleSaveTemplateSource).not.toContain('alert.notice.receiver.next');
+    expect(handleSaveRuleSource).not.toContain('alert.notice.receiver.next');
+  });
+
+  it('keeps Angular receiver save transport-error close behavior distinct from business failures', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleSaveReceiverSource = source.slice(source.indexOf('async function handleSaveReceiver()'), source.indexOf('async function handleDeleteReceiver()'));
+    const handleSaveTemplateSource = source.slice(source.indexOf('async function handleSaveTemplate()'), source.indexOf('async function handleDeleteTemplate()'));
+
+    expect(source).toContain('function isApiMessageBusinessError(error: unknown)');
+    expect(source).toContain('data-alert-notice-receiver-save-failure-close="angular-transport-error-close"');
+    expect(source).toContain('data-alert-notice-receiver-save-failure-close-owner="route-action-feedback-contract"');
+    expect(handleSaveReceiverSource).toContain('if (!isApiMessageBusinessError(error))');
+    expect(handleSaveReceiverSource).toContain('setEditingReceiver(false);');
+    expect(handleSaveTemplateSource).not.toContain('isApiMessageBusinessError');
+  });
+
+  it('keeps Angular receiver save failure title separate from backend detail while preserving transport close', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleSaveReceiverSource = source.slice(source.indexOf('async function handleSaveReceiver()'), source.indexOf('async function handleDeleteReceiver()'));
+
+    expect(source).toContain('const [receiverErrorDetail, setReceiverErrorDetail] = useState<string | null>(null);');
+    expect(source).toContain('data-alert-notice-receiver-save-failure={receiverErrorDetail ? \'angular-notify-title-detail\' : undefined}');
+    expect(source).toContain('data-alert-notice-receiver-save-failure-owner={receiverErrorDetail ? \'route-action-feedback-contract\' : undefined}');
+    expect(source).toContain('data-alert-notice-receiver-save-failure-title={receiverErrorDetail ? receiverError : undefined}');
+    expect(source).toContain('data-alert-notice-receiver-save-failure-detail={receiverErrorDetail ?? undefined}');
+    expect(handleSaveReceiverSource).toContain("setReceiverError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+    expect(handleSaveReceiverSource).toContain('setReceiverErrorDetail(error instanceof Error ? error.message : null)');
+    expect(handleSaveReceiverSource).toContain('if (!isApiMessageBusinessError(error))');
+    expect(handleSaveReceiverSource).not.toContain("setReceiverError(error instanceof Error ? error.message : t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+  });
+
+  it('keeps Angular edit-fail fallback keys for all notice detail loads', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleEditReceiverSource = source.slice(source.indexOf('async function handleEditReceiver('), source.indexOf('async function handleSaveReceiver()'));
+    const handleEditTemplateSource = source.slice(source.indexOf('async function handleEditTemplate('), source.indexOf('async function handleViewTemplate('));
+    const handleEditRuleSource = source.slice(source.indexOf('async function handleEditRule('), source.indexOf('async function handleSaveRule()'));
+    const editSources = [handleEditReceiverSource, handleEditTemplateSource, handleEditRuleSource];
+
+    expect(source).toContain('data-alert-notice-edit-load-feedback="angular-edit-fail"');
+    expect(source).toContain('data-alert-notice-edit-load-feedback-owner="route-action-feedback-contract"');
+    for (const editSource of editSources) {
+      expect(editSource).toContain("t('common.notify.edit-fail')");
+      expect(editSource).not.toContain('common.load-failed');
+    }
+  });
+
+  it('keeps Angular receiver row edit loading the full detail before opening the modal', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleEditReceiverSource = source.slice(source.indexOf('async function handleEditReceiver('), source.indexOf('async function handleSaveReceiver()'));
+
+    expect(source).toContain('data-alert-notice-receiver-edit-detail="angular-detail-fetch"');
+    expect(source).toContain('data-alert-notice-receiver-edit-detail-owner="route-detail-fetch-contract"');
+    expect(source).toContain('onClick={() => void handleEditReceiver(receiver)}');
+    expect(handleEditReceiverSource).toContain('async function handleEditReceiver(receiver = selectedReceiver)');
+    expect(handleEditReceiverSource).toContain('await api.alertNotice.receivers.detail(receiver.id)');
+    expect(handleEditReceiverSource).toContain('setReceiverDraft(buildNoticeReceiverDraft(detail))');
+    expect(handleEditReceiverSource).not.toContain('buildNoticeReceiverDraft(receiver)');
+  });
+
+  it('keeps Angular custom template row edit loading the full detail before opening the modal', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleEditTemplateSource = source.slice(source.indexOf('async function handleEditTemplate('), source.indexOf('async function handleViewTemplate('));
+
+    expect(source).toContain('data-alert-notice-template-edit-detail="angular-detail-fetch"');
+    expect(source).toContain('data-alert-notice-template-edit-detail-owner="route-detail-fetch-contract"');
+    expect(source).toContain('onClick={() => void handleEditTemplate(template)}');
+    expect(handleEditTemplateSource).toContain('const detail = await api.alertNotice.templates.detail(template.id)');
+    expect(handleEditTemplateSource).toContain('setTemplateDraft(buildNoticeTemplateDraft(detail))');
+    expect(handleEditTemplateSource).not.toContain('buildNoticeTemplateDraft(template)');
+  });
+
+  it('keeps Angular preset template viewer footer using return and no OK action', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const templateDialogStart = source.indexOf('const templateEditorDialog = (');
+    const templateEditorDialogSource = source.slice(templateDialogStart, source.indexOf('        return (', templateDialogStart));
+
+    expect(source).toContain('data-alert-notice-template-viewer-return={templateReadOnly ? \'angular-cancel-return\' : undefined}');
+    expect(source).toContain('data-alert-notice-template-viewer-return-owner={templateReadOnly ? \'route-modal-footer-contract\' : undefined}');
+    expect(source).toContain('data-alert-notice-template-viewer-ok="none"');
+    expect(source).toContain('data-alert-notice-template-viewer-ok-owner="route-modal-footer-contract"');
+    expect(templateEditorDialogSource).toContain("templateReadOnly ? t('common.button.return') : t('common.cancel')");
+    expect(templateEditorDialogSource).toContain('{!templateReadOnly ? (');
+  });
+
+  it('keeps Angular delete page-index clamping for all notice tabs', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const handleConfirmedDeleteSource = source.slice(source.indexOf('async function handleConfirmedDelete()'), source.indexOf('async function handleTestSend()'));
+
+    expect(source).toContain('function clampNoticePageIndexAfterDelete');
+    expect(source).toContain('Math.ceil(nextTotal / safePageSize) - 1');
+    expect(source).toContain('setReceiverPageIndex(pageIndex => clampNoticePageIndexAfterDelete(pageIndex, receiverPageSize, receiverTotal, 1))');
+    expect(source).toContain('setRulePageIndex(pageIndex => clampNoticePageIndexAfterDelete(pageIndex, rulePageSize, ruleTotal, 1))');
+    expect(source).toContain('setTemplatePageIndex(pageIndex => clampNoticePageIndexAfterDelete(pageIndex, templatePageSize, templateTotal, 1))');
+    expect(source).toContain('data-alert-notice-delete-page-clamp="angular-update-page-index"');
+    expect(source).toContain('data-alert-notice-delete-page-clamp-owner="route-state-contract"');
+    expect(source).toContain('data-alert-notice-delete-confirm="angular-modal-confirm"');
+    expect(source).toContain('data-alert-notice-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-alert-notice-delete-confirm-dialog="angular-modal-confirm"');
+    expect(source).toContain('data-alert-notice-delete-confirm-ok');
+    expect(source).toContain('data-alert-notice-delete-confirm-cancel');
+    expect(source).toContain('data-alert-notice-delete-feedback="angular-delete-notify"');
+    expect(source).toContain('data-alert-notice-delete-feedback-owner="route-action-feedback-contract"');
+    expect(source).not.toContain("from '../../../components/ui/cold-confirm-dialog'");
+    expect(handleConfirmedDeleteSource.match(/common.notify.delete-success/g)?.length).toBe(3);
+    expect(handleConfirmedDeleteSource).toContain("t('common.notify.delete-fail')");
+    expect(handleConfirmedDeleteSource).not.toContain('common.delete-success');
+    expect(handleConfirmedDeleteSource).not.toContain('common.delete-failed');
+  });
+
+  it('keeps the old Angular receiver test-send loading guard inside the editor dialog', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const receiverFieldsSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-notice-receiver-fields.tsx'), 'utf8');
+    const handleTestSendSource = source.slice(source.indexOf('async function handleTestSend()'), source.indexOf('async function handleNewTemplate()'));
+
+    expect(source).toContain('const [testingReceiver, setTestingReceiver] = useState(false)');
+    expect(source).toContain('setTestingReceiver(true);');
+    expect(source).toContain('setTestingReceiver(false);');
+    expect(source).toContain('buildNoticeTemplateListUrl');
+    expect(source).toContain('alertNoticeTemplateListUrl');
+    expect(source).toContain('data.templateOptions?.content ?? data.templates.content');
+    expect(source).not.toContain('const filteredTemplates = data.templates.content.filter');
+    expect(source).toContain('data-alert-notice-receiver-test-loading={testingReceiver ? \'true\' : \'false\'}');
+    expect(source).toContain('data-alert-notice-receiver-test-validation="angular-backend-owned"');
+    expect(source).toContain('data-alert-notice-receiver-test-validation-owner="route-mutation-contract"');
+    expect(receiverFieldsSource).toContain('data-alert-notice-receiver-default-type="angular-email"');
+    expect(receiverFieldsSource).toContain('data-alert-notice-receiver-default-type-owner="route-form-contract"');
+    expect(source).toContain('aria-busy={testingReceiver}');
+    expect(source).toContain('disabled={testingReceiver || savingReceiver}');
+    expect(source).toContain('data-alert-notice-receiver-test-feedback="cold-test-feedback"');
+    expect(source).toContain('editingReceiver && receiverMessage');
+    expect(source).toContain('!editingReceiver && receiverMessage');
+    expect(handleTestSendSource).toContain('await api.alertNotice.receivers.sendTest(receiverDraft)');
+    expect(handleTestSendSource).not.toContain('validateNoticeReceiverDraft(receiverDraft, t)');
+  });
+
   it('loads alert notice data through the default receiver and rule query contract', async () => {
-    const { default: AlertNoticePage } = await import('./page');
-    renderToStaticMarkup(<AlertNoticePage />);
+    await renderAlertNoticePage();
 
     await mockState.lastLoad?.();
 
     expect(mockLoadAlertNoticeData).toHaveBeenCalledWith(expect.anything(), {
       receivers: { search: '', pageIndex: 0, pageSize: 8 },
-      rules: { search: '', pageIndex: 0, pageSize: 8 }
+      rules: { search: '', pageIndex: 0, pageSize: 8 },
+      templates: { search: '', preset: true, pageIndex: 0, pageSize: 8 }
     });
-  });
+  }, 30_000);
 
   it('opens notice policy context when routed from a three-signal alert investigation', async () => {
-    mockState.searchParams = new URLSearchParams({
+    const initialRouteState: AlertNoticeRouteState = {
       signal: 'logs',
-      entityId: '7',
-      entityName: 'Checkout API',
-      serviceName: 'checkout',
-      environment: 'prod',
-      timeRange: 'last-1h',
-      source: 'otlp',
-      traceId: 'trace-123',
-      spanId: 'span-456',
-      returnTo: '/log/manage?traceId=trace-123&returnLabel=Logs'
-    });
+      signalContext: {
+        entityId: '7',
+        entityName: 'Checkout API',
+        serviceName: 'checkout',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'otlp',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        returnTo: '/log/manage?traceId=trace-123'
+      }
+    };
 
-    const { default: AlertNoticePage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertNoticePage />);
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
+    const html = await renderAlertNoticePage(initialRouteState);
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
 
     expect(html).toContain('data-selected-tab="rule"');
     expect(html).toContain('data-alert-notice-evidence-context="signal-route"');
@@ -559,13 +1185,31 @@ describe('alert notice page', () => {
     expect(html).toContain('新建通知策略时会按当前实体、服务、环境和链路标签做匹配');
     expect(html).toContain('data-alert-notice-evidence-return="true"');
     expect(html).toContain('href="/log/manage?traceId=trace-123"');
-    expect(source).toContain('useSearchParams');
-    expect(source).toContain('readSignalRouteContext');
+    expect(source).not.toContain('useSearchParams');
+    expect(source).not.toContain('readSignalRouteContext(searchParams)');
+    expect(source).toContain('const alertNoticeRouteState = initialRouteState ?? EMPTY_ALERT_NOTICE_ROUTE_STATE');
     expect(source).toContain('buildAlertNoticeEvidenceContext');
     expect(source).toContain('data-alert-notice-rule-editor-return="evidence-context"');
     expect(source).toContain('noticeEvidenceContext?.returnHref');
     expect(source).toContain('buildNoticeRuleDraft(null, noticeEvidenceContext?.ruleDraftPatch)');
-  });
+  }, 30_000);
+
+  it('renders missing evidence labels with the localized empty fallback', async () => {
+    const html = await renderAlertNoticePage({
+      signal: null,
+      signalContext: {
+        returnTo: '/alert?source=logs'
+      }
+    });
+
+    expect(html).toContain('data-selected-tab="rule"');
+    expect(html).toContain('data-alert-notice-evidence-context="signal-route"');
+    expect(html).toContain('data-alert-notice-prefill-labels=""');
+    expect(html).toContain('data-alert-notice-evidence-labels="generated-labels"');
+    expect(html).toContain('>无</div>');
+    expect(html).not.toContain('data-alert-notice-prefill-labels="-"');
+    expect(html).not.toContain('>-</div>');
+  }, 30_000);
 
   it('keeps the receiver empty state close to the OTLP cold table baseline', async () => {
     const previousData = mockState.renderData;
@@ -580,8 +1224,7 @@ describe('alert notice page', () => {
     };
 
     try {
-      const { default: AlertNoticePage } = await import('./page');
-      const html = renderToStaticMarkup(<AlertNoticePage />);
+      const html = await renderAlertNoticePage();
 
       expect(html).toContain('data-alert-notice-receiver-empty-state="cold-empty-state"');
       expect(html).toContain('data-alert-notice-receiver-empty-icon="cold-empty-icon"');
@@ -590,5 +1233,22 @@ describe('alert notice page', () => {
     } finally {
       mockState.renderData = previousData;
     }
+  }, 30_000);
+
+  it('keeps alert notice remounts on a short settled cache window with refresh-tick invalidation', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+
+    expect(source).toContain('ALERT_NOTICE_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('const [refreshTick, setRefreshTick] = useState(0)');
+    expect(source).toContain("['alert-notice', alertNoticeReceiverListUrl, alertNoticeRuleListUrl, alertNoticeTemplateListUrl, refreshTick].join('|')");
+    expect(source).toContain('[alertNoticeReceiverListUrl, alertNoticeRuleListUrl, alertNoticeTemplateListUrl, refreshTick]');
+    expect(source).toContain('void refreshTick');
+    expect(source).toContain('[alertNoticeLoadQuery, refreshTick]');
+    expect(source.match(/setRefreshTick\(value => value \+ 1\)/g)?.length).toBeGreaterThanOrEqual(6);
+    expect(source).toContain('data-alert-notice-receiver-sync="angular-load-table"');
+    expect(source).toContain('data-alert-notice-rule-sync="angular-load-table"');
+    expect(source).toContain('data-alert-notice-template-sync="angular-load-table"');
+    expect(source).toContain('cacheKey={alertNoticeCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={ALERT_NOTICE_SETTLED_CACHE_TTL_MS}');
   });
 });
diff --git a/web-next/app/alert/notice/page.tsx b/web-next/app/alert/notice/page.tsx
index 55115c454e..c5e5e1e1a6 100644
--- a/web-next/app/alert/notice/page.tsx
+++ b/web-next/app/alert/notice/page.tsx
@@ -1,1350 +1,14 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback, useMemo, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ArrowLeft, ChevronLeft, ChevronRight, Eye, Inbox, MoreHorizontal, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react';
-import { AlertNoticeConsoleShell, type AlertNoticeConsoleTabKey } from '../../../components/pages/alert-notice-console-shell';
-import { ClientWorkbench } from '../../../components/workbench/client-workbench';
-import { useI18n } from '../../../components/providers/i18n-provider';
-import { AlertNoticeReceiverFields } from '../../../components/pages/alert-notice-receiver-fields';
-import { AlertNoticeRuleFields } from '../../../components/pages/alert-notice-rule-fields';
-import { AlertNoticeTemplateFields } from '../../../components/pages/alert-notice-template-fields';
-import { Button } from '../../../components/ui/button';
-import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
-import { SearchRow } from '../../../components/ui/search-row';
-import { Select } from '../../../components/ui/select';
-import { OverlayDialog } from '../../../components/workbench/overlay-dialog';
-import {
-  AlertSurfaceTable,
-  AlertSurfaceTableHead,
-  AlertSurfaceTableShell,
-  AlertSurfaceValuePill
-} from '../../../components/pages/alert-surface-primitives';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '../../../lib/api-client';
-import { buildNoticeRuleDraft, createNoticeReceiver, createNoticeRule, createNoticeTemplate, deleteNoticeReceiver, deleteNoticeRule, deleteNoticeTemplate, loadAlertNoticeData, loadNoticeReceiverDetail, loadNoticeRuleDetail, loadNoticeTemplateDetail, sendNoticeReceiverTest, updateNoticeReceiver, updateNoticeRule, updateNoticeTemplate, type NoticeReceiverDraft, type NoticeRuleDraft, type NoticeTemplateDraft } from '../../../lib/alert-notice/controller';
-import { buildAlertNoticeEvidenceContext, buildNoticeReceiverDraft, buildNoticeTemplateDraft, getAlertNoticeProductCopy, validateNoticeReceiverDraft, validateNoticeRuleDraft, validateNoticeTemplateDraft } from '../../../lib/alert-notice/view-model';
-import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptions, type AlertLabelOptions } from '../../../lib/alert-label-options';
-import { coldOpsCatalogVisual } from '../../../lib/cold-ops-visual';
-import { formatTime } from '../../../lib/format';
-import { readSignalRouteContext } from '../../../lib/signal-route-context';
-import type { NoticeReceiver, NoticeRule, NoticeTemplate, PageResult } from '../../../lib/types';
+import AlertNoticePage from './alert-notice-page';
+import { readAlertNoticeRouteState, type AlertNoticeSearchParams } from '../../../lib/alert-notice/query-state';
 
-type NoticePageData = {
-  receivers: PageResult<NoticeReceiver>;
-  receiverOptions: PageResult<NoticeReceiver>;
-  rules: PageResult<NoticeRule>;
-  templates: PageResult<NoticeTemplate>;
-  labelOptions: AlertLabelOptions;
-};
-
-type NoticeDeleteRequest = {
-  kind: 'receiver' | 'rule' | 'template';
-  id: number;
-};
-
-const NOTICE_PAGE_SIZE_OPTIONS = [8, 15, 25];
-type Translator = ReturnType<typeof useI18n>['t'];
-const coldNoticeVisual = coldOpsCatalogVisual;
-const coldToolbarClass = 'flex flex-wrap items-center gap-2 border-b border-[#252b34] bg-[#0b0c0e] px-3 py-2';
-const coldSelectClass =
-  'h-8 w-[132px] rounded-[3px] border border-[#2b3039] bg-[#101217] px-2 text-[12px] font-semibold text-[#eef2f7] outline-none focus:border-[#4e74f8]';
-const coldTableShellClass = 'rounded-[4px] border border-[#252b34] bg-[#0b0c0e]';
-const coldTextPrimaryClass = 'text-[#f2f5f8]';
-const coldButtonClassName =
-  'h-8 min-w-[104px] rounded-[3px] border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white';
-const coldPrimaryButtonClassName =
-  'h-8 min-w-[104px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-white';
-const coldCommandButtonClass = `${coldButtonClassName} w-[124px] min-w-[124px]`;
-const coldPrimaryCommandButtonClass = `${coldPrimaryButtonClassName} w-[124px] min-w-[124px]`;
-const coldIconButtonClass =
-  'h-8 w-8 min-w-0 rounded-[3px] border-[#2b3039] bg-[#101217] text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white';
-
-const NOTICE_TYPE_LABEL_KEYS: Record<string, string> = {
-  '0': 'alert.notice.type.sms',
-  '1': 'alert.notice.type.email',
-  '2': 'alert.notice.type.url',
-  '3': 'alert.notice.type.wechat',
-  '4': 'alert.notice.type.WeCom-robot',
-  '5': 'alert.notice.type.ding',
-  '6': 'alert.notice.type.fei-shu',
-  '7': 'alert.notice.type.telegram-bot',
-  '8': 'alert.notice.type.slack',
-  '9': 'alert.notice.type.discord',
-  '10': 'alert.notice.type.WeComApp',
-  '11': 'alert.notice.type.smn',
-  '12': 'alert.notice.type.serverchan',
-  '13': 'alert.notice.type.gotify',
-  '14': 'alert.notice.type.lark-app'
-};
-
-function getNoticeTypeLabel(type: number | string | null | undefined, t: Translator) {
-  if (type == null) return '-';
-  const normalized = String(type).trim();
-  const key = NOTICE_TYPE_LABEL_KEYS[normalized];
-  return key ? t(key) : normalized;
-}
-
-function getReceiverSetting(receiver: NoticeReceiver) {
-  const type = String(receiver.type ?? '');
-  switch (type) {
-    case '0':
-      return receiver.phone?.trim() || '-';
-    case '1':
-      return receiver.email?.trim() || '-';
-    case '2':
-      return receiver.hookUrl?.trim() || '-';
-    case '3':
-    case '4':
-      return receiver.wechatId?.trim() || '-';
-    case '5':
-      return receiver.accessToken?.trim() || '-';
-    case '6':
-      return receiver.wechatId?.trim() || receiver.accessToken?.trim() || '-';
-    case '7':
-      return [receiver.tgBotToken?.trim(), receiver.tgUserId?.trim()].filter(Boolean).join(' / ') || '-';
-    case '8':
-      return receiver.slackWebHookUrl?.trim() || '-';
-    case '9':
-      return [receiver.discordChannelId?.trim(), receiver.discordBotToken?.trim()].filter(Boolean).join(' / ') || '-';
-    case '10':
-      return [receiver.corpId?.trim(), receiver.agentId != null ? String(receiver.agentId).trim() : '', receiver.appSecret?.trim()]
-        .filter(Boolean)
-        .join(' / ') || '-';
-    case '11':
-      return receiver.smnAk?.trim() || '-';
-    case '12':
-      return receiver.serverChanToken?.trim() || '-';
-    case '13':
-      return receiver.gotifyToken?.trim() || '-';
-    case '14':
-      return receiver.appId?.trim() || '-';
-    default:
-      return receiver.email?.trim() || receiver.phone?.trim() || receiver.hookUrl?.trim() || '-';
-  }
-}
-
-function NoticePagination({
-  t,
-  pageIndex,
-  pageSize,
-  totalElements,
-  testIdPrefix,
-  onPageIndexChange,
-  onPageSizeChange
+export default async function AlertNoticeRoutePage({
+  searchParams
 }: {
-  t: Translator;
-  pageIndex: number;
-  pageSize: number;
-  totalElements: number;
-  testIdPrefix: string;
-  onPageIndexChange: (pageIndex: number) => void;
-  onPageSizeChange: (pageSize: number) => void;
-}) {
-  const totalPages = Math.max(1, Math.ceil(totalElements / Math.max(1, pageSize)));
-  const pageNumbers = Array.from({ length: Math.min(totalPages, 3) }, (_, index) => index);
-
-  return (
-    <div
-      data-alert-notice-pagination="cold-dense-pagination"
-      className="flex flex-wrap items-center justify-end gap-2 border-t border-[#252b34] bg-[#0b0c0e] px-3 py-2 text-xs text-[#8f99ab]"
-    >
-      <span>{`${t('common.total')} ${totalElements}`}</span>
-      <Button
-        data-testid={`${testIdPrefix}-previous`}
-        size="icon"
-        variant="subtle"
-        disabled={pageIndex <= 0}
-        onClick={() => onPageIndexChange(Math.max(0, pageIndex - 1))}
-        title={t('common.previous')}
-      >
-        <ChevronLeft className="h-4 w-4" aria-hidden="true" />
-        <span className="sr-only">{t('common.previous')}</span>
-      </Button>
-      {pageNumbers.map(pageNumber => (
-        <Button
-          key={pageNumber}
-          data-testid={`${testIdPrefix}-page-${pageNumber + 1}`}
-          size="icon"
-          variant={pageNumber === pageIndex ? 'primary' : 'subtle'}
-          onClick={() => onPageIndexChange(pageNumber)}
-          aria-current={pageNumber === pageIndex ? 'page' : undefined}
-        >
-          {pageNumber + 1}
-        </Button>
-      ))}
-      <Button
-        data-testid={`${testIdPrefix}-next`}
-        size="icon"
-        variant="subtle"
-        disabled={pageIndex >= totalPages - 1}
-        onClick={() => onPageIndexChange(Math.min(totalPages - 1, pageIndex + 1))}
-        title={t('common.next')}
-      >
-        <ChevronRight className="h-4 w-4" aria-hidden="true" />
-        <span className="sr-only">{t('common.next')}</span>
-      </Button>
-      <Select
-        data-testid={`${testIdPrefix}-page-size`}
-        value={String(pageSize)}
-        onChange={event => onPageSizeChange(Math.max(1, Number.parseInt(event.target.value, 10) || pageSize))}
-        containerClassName="w-[96px]"
-        className="h-8 min-w-0 text-[#eef2f7]"
-      >
-        {NOTICE_PAGE_SIZE_OPTIONS.map(option => (
-          <option key={option} value={option}>
-            {t('alert.notice.page-size', { count: option })}
-          </option>
-        ))}
-      </Select>
-    </div>
-  );
-}
-
-function NoticeTableEmptyRow({
-  t,
-  colSpan,
-  prefix
-}: {
-  t: Translator;
-  colSpan: number;
-  prefix: 'receiver' | 'rule' | 'template';
-}) {
-  return (
-    <tr data-alert-notice-empty-row="true" data-alert-notice-receiver-empty-state={prefix === 'receiver' ? 'cold-empty-state' : undefined} className="bg-[#0b0c0e]">
-      <td
-        colSpan={colSpan}
-        className="h-[360px] px-3 pt-[54px] text-center align-top text-[#8f99ab]"
-      >
-        <div className="inline-flex flex-col items-center gap-2.5">
-          <span
-            data-alert-notice-receiver-empty-icon={prefix === 'receiver' ? 'cold-empty-icon' : undefined}
-            className="inline-flex h-10 w-12 items-center justify-center rounded-[4px] border border-[#2b3039] bg-[#101217] text-[#7e8494]"
-          >
-            <Inbox className="h-7 w-7" aria-hidden="true" />
-          </span>
-          <div className="text-[14px]">{t('common.no-data')}</div>
-        </div>
-      </td>
-    </tr>
-  );
-}
-
-function NoticeTableSwitch({
-  checked,
-  field,
-  label,
-  pending,
-  onChange
-}: {
-  checked: boolean;
-  field: 'filter-all' | 'enable';
-  label: string;
-  pending?: boolean;
-  onChange: (checked: boolean) => void;
-}) {
-  return (
-    <button
-      type="button"
-      role="switch"
-      aria-checked={checked}
-      aria-label={label}
-      data-alert-notice-rule-table-switch={field}
-      data-state={checked ? 'checked' : 'unchecked'}
-      disabled={pending}
-      onClick={() => onChange(!checked)}
-      className="inline-flex h-6 w-11 items-center rounded-[3px] border border-[#394150] bg-[#0d0f14] p-0.5 transition hover:border-[#4e74f8] focus-visible:border-[#4e74f8] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[rgba(78,116,248,0.14)] disabled:cursor-not-allowed disabled:opacity-55 data-[state=checked]:border-[#4e74f8] data-[state=checked]:bg-[#182238]"
-    >
-      <span
-        aria-hidden="true"
-        className={`h-4 w-4 rounded-[2px] bg-[#dbe4f0] shadow transition ${checked ? 'translate-x-[19px]' : 'translate-x-0'}`}
-      />
-    </button>
-  );
-}
-
-export default function AlertNoticePage() {
-  const { t, locale } = useI18n();
-  const searchParams = useSearchParams();
-  const signal = searchParams.get('signal');
-  const signalContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
-  const noticeEvidenceContext = useMemo(
-    () => buildAlertNoticeEvidenceContext(signal, signalContext, t),
-    [signal, signalContext, t]
-  );
-  const [editingReceiver, setEditingReceiver] = useState(false);
-  const [receiverDraft, setReceiverDraft] = useState<NoticeReceiverDraft>(() => buildNoticeReceiverDraft(null));
-  const [selectedReceiverId, setSelectedReceiverId] = useState<number | null>(null);
-  const [receiverSearchDraft, setReceiverSearchDraft] = useState('');
-  const [receiverSearch, setReceiverSearch] = useState('');
-  const [receiverPageIndex, setReceiverPageIndex] = useState(0);
-  const [receiverPageSize, setReceiverPageSize] = useState(8);
-  const [savingReceiver, setSavingReceiver] = useState(false);
-  const [receiverMessage, setReceiverMessage] = useState<string | null>(null);
-  const [receiverError, setReceiverError] = useState<string | null>(null);
-  const [editingTemplate, setEditingTemplate] = useState(false);
-  const [templateDraft, setTemplateDraft] = useState<NoticeTemplateDraft>(() => buildNoticeTemplateDraft(null));
-  const [templateReadOnly, setTemplateReadOnly] = useState(false);
-  const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
-  const [savingTemplate, setSavingTemplate] = useState(false);
-  const [templateMessage, setTemplateMessage] = useState<string | null>(null);
-  const [templateError, setTemplateError] = useState<string | null>(null);
-  const [templateSearchDraft, setTemplateSearchDraft] = useState('');
-  const [templateSearch, setTemplateSearch] = useState('');
-  const [templatePresetFilter, setTemplatePresetFilter] = useState(true);
-  const [templatePageIndex, setTemplatePageIndex] = useState(0);
-  const [templatePageSize, setTemplatePageSize] = useState(8);
-  const [selectedTab, setSelectedTab] = useState<AlertNoticeConsoleTabKey>(() => noticeEvidenceContext ? 'rule' : 'receiver');
-  const [editingRule, setEditingRule] = useState(false);
-  const [ruleDraft, setRuleDraft] = useState<NoticeRuleDraft>(() => buildNoticeRuleDraft(null));
-  const [selectedRuleId, setSelectedRuleId] = useState<number | null>(null);
-  const [ruleSearchDraft, setRuleSearchDraft] = useState('');
-  const [ruleSearch, setRuleSearch] = useState('');
-  const [rulePageIndex, setRulePageIndex] = useState(0);
-  const [rulePageSize, setRulePageSize] = useState(8);
-  const [savingRule, setSavingRule] = useState(false);
-  const [ruleMessage, setRuleMessage] = useState<string | null>(null);
-  const [ruleError, setRuleError] = useState<string | null>(null);
-  const [ruleSwitchPending, setRuleSwitchPending] = useState<string | null>(null);
-  const [refreshTick, setRefreshTick] = useState(0);
-  const [deleteRequest, setDeleteRequest] = useState<NoticeDeleteRequest | null>(null);
-  const [deletePending, setDeletePending] = useState(false);
-
-  const load = useCallback(async (): Promise<NoticePageData> => {
-    const [noticeData, labelOptions] = await Promise.all([
-      loadAlertNoticeData(apiMessageGet, {
-        receivers: {
-          search: receiverSearch,
-          pageIndex: receiverPageIndex,
-          pageSize: receiverPageSize
-        },
-        rules: {
-          search: ruleSearch,
-          pageIndex: rulePageIndex,
-          pageSize: rulePageSize
-        }
-      }),
-      loadAlertLabelOptions(apiMessageGet).catch(() => DEFAULT_ALERT_LABEL_OPTIONS)
-    ]);
-    return { ...noticeData, labelOptions };
-  }, [receiverPageIndex, receiverPageSize, receiverSearch, rulePageIndex, rulePageSize, ruleSearch]);
-
-  const productCopy = getAlertNoticeProductCopy(locale);
-
-  return (
-    <ClientWorkbench key={refreshTick} load={load} loadingCopy={t('alert.notice.loading')}>
-      {data => {
-        const selectedReceiver = data.receivers.content.find(item => item.id === selectedReceiverId) ?? data.receivers.content[0] ?? null;
-        const selectedRule = data.rules.content.find(item => item.id === selectedRuleId) ?? data.rules.content[0] ?? null;
-        const labelOptions = data.labelOptions ?? DEFAULT_ALERT_LABEL_OPTIONS;
-        const selectedTemplate =
-          data.templates.content.find(item => item.id === selectedTemplateId) ??
-          data.templates.content.find(item => !item.preset) ??
-          data.templates.content[0] ??
-          null;
-        const selectedTemplateIsCustom = selectedTemplate ? !selectedTemplate.preset : false;
-        const templateSearchNeedle = templateSearch.trim().toLowerCase();
-        const filteredTemplates = data.templates.content.filter(template => {
-          if (Boolean(template.preset) !== templatePresetFilter) {
-            return false;
-          }
-          if (!templateSearchNeedle) {
-            return true;
-          }
-          return (template.name || '').toLowerCase().includes(templateSearchNeedle);
-        });
-        const templateTotalElements = filteredTemplates.length;
-        const templateMaxPageIndex = Math.max(0, Math.ceil(templateTotalElements / Math.max(1, templatePageSize)) - 1);
-        const normalizedTemplatePageIndex = Math.min(templatePageIndex, templateMaxPageIndex);
-        const templateVisibleRows = filteredTemplates.slice(
-          normalizedTemplatePageIndex * templatePageSize,
-          normalizedTemplatePageIndex * templatePageSize + templatePageSize
-        );
-        const receiverOptionRows = data.receiverOptions?.content?.length ? data.receiverOptions.content : data.receivers.content;
-        const receiverOptions = receiverOptionRows.map(receiver => ({
-          value: String(receiver.id),
-          label: `${receiver.name || productCopy.receiverFallback}-${getNoticeTypeLabel(receiver.type, t)}`,
-          type: receiver.type
-        }));
-        const templateOptions = [
-          { value: '-1', label: t('alert.notice.template.preset.true') },
-          ...data.templates.content
-            .filter(template => template.id != null)
-            .map(template => ({
-              value: String(template.id),
-              label: template.name || productCopy.templateFallback,
-              type: template.type
-            }))
-        ];
-        const receiverTotal = data.receivers.totalElements ?? data.receivers.content.length;
-        const ruleTotal = data.rules.totalElements ?? data.rules.content.length;
-        const templateTotal = data.templates.totalElements ?? data.templates.content.length;
-
-        async function handleNewReceiver() {
-          setSelectedTab('receiver');
-          setReceiverDraft(buildNoticeReceiverDraft(null));
-          setReceiverMessage(null);
-          setReceiverError(null);
-          setEditingReceiver(true);
-        }
-
-        async function handleEditReceiver() {
-          if (!selectedReceiver?.id) return;
-          try {
-            setSelectedTab('receiver');
-            const detail = await loadNoticeReceiverDetail(apiMessageGet, selectedReceiver.id);
-            setReceiverDraft(buildNoticeReceiverDraft(detail));
-            setReceiverMessage(null);
-            setReceiverError(null);
-            setEditingReceiver(true);
-          } catch (error) {
-            setReceiverError(error instanceof Error ? error.message : t('common.load-failed'));
-          }
-        }
-
-        async function handleSaveReceiver() {
-          const validationError = validateNoticeReceiverDraft(receiverDraft, t);
-          if (validationError) {
-            setReceiverMessage(null);
-            setReceiverError(validationError);
-            return;
-          }
-          setSavingReceiver(true);
-          setReceiverMessage(null);
-          setReceiverError(null);
-          try {
-            if (receiverDraft.id) {
-              await updateNoticeReceiver(apiMessagePut as any, receiverDraft);
-            } else {
-              await createNoticeReceiver(apiMessagePost as any, receiverDraft);
-            }
-            setReceiverMessage(t('common.save-success'));
-            setEditingReceiver(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setReceiverError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setSavingReceiver(false);
-          }
-        }
-
-        async function handleDeleteReceiver() {
-          if (!selectedReceiver?.id) return;
-          await handleDeleteReceiverById(selectedReceiver.id);
-        }
-
-        async function handleDeleteReceiverById(receiverId: number) {
-          setDeleteRequest({ kind: 'receiver', id: receiverId });
-        }
-
-        async function handleConfirmedDelete() {
-          const request = deleteRequest;
-          if (!request) return;
-          setDeletePending(true);
-          try {
-            if (request.kind === 'receiver') {
-              await deleteNoticeReceiver(apiMessageDelete as any, request.id);
-              setSelectedReceiverId(null);
-              setReceiverMessage(t('common.delete-success'));
-              setReceiverError(null);
-              setEditingReceiver(false);
-            } else if (request.kind === 'template') {
-              await deleteNoticeTemplate(apiMessageDelete as any, request.id);
-              setSelectedTemplateId(null);
-              setTemplateMessage(t('common.delete-success'));
-              setTemplateError(null);
-              setTemplateReadOnly(false);
-              setEditingTemplate(false);
-            } else {
-              await deleteNoticeRule(apiMessageDelete as any, request.id);
-              setSelectedRuleId(null);
-              setRuleMessage(t('common.delete-success'));
-              setRuleError(null);
-              setEditingRule(false);
-            }
-            setDeleteRequest(null);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            const fallback = error instanceof Error ? error.message : t('common.delete-failed');
-            if (request.kind === 'receiver') {
-              setReceiverError(fallback);
-            } else if (request.kind === 'template') {
-              setTemplateError(fallback);
-            } else {
-              setRuleError(fallback);
-            }
-          } finally {
-            setDeletePending(false);
-          }
-        }
-
-        async function handleTestSend() {
-          const validationError = validateNoticeReceiverDraft(receiverDraft, t);
-          if (validationError) {
-            setReceiverMessage(null);
-            setReceiverError(validationError);
-            return;
-          }
-          try {
-            await sendNoticeReceiverTest(apiMessagePost as any, receiverDraft);
-            setReceiverMessage(t('alert.notice.send-test.notify.success'));
-            setReceiverError(null);
-          } catch (error) {
-            setReceiverError(error instanceof Error ? error.message : t('alert.notice.send-test.notify.failed'));
-          }
-        }
-
-        async function handleNewTemplate() {
-          setSelectedTab('template');
-          setTemplateDraft(buildNoticeTemplateDraft(null));
-          setTemplateReadOnly(false);
-          setTemplateMessage(null);
-          setTemplateError(null);
-          setEditingTemplate(true);
-        }
-
-        async function handleEditTemplate(template = selectedTemplate) {
-          if (!template?.id || template.preset) return;
-          try {
-            setSelectedTab('template');
-            setSelectedTemplateId(template.id);
-            setTemplateReadOnly(false);
-            const detail = await loadNoticeTemplateDetail(apiMessageGet, template.id);
-            setTemplateDraft(buildNoticeTemplateDraft(detail));
-            setTemplateMessage(null);
-            setTemplateError(null);
-            setEditingTemplate(true);
-          } catch (error) {
-            setTemplateError(error instanceof Error ? error.message : t('common.load-failed'));
-          }
-        }
-
-        async function handleViewTemplate(template: NoticeTemplate) {
-          const templateId = typeof template.id === 'number' && Number.isFinite(template.id) ? template.id : null;
-          const rowDraft = buildNoticeTemplateDraft(template);
-          setSelectedTab('template');
-          setSelectedTemplateId(templateId);
-          setTemplateReadOnly(true);
-          setTemplateDraft(rowDraft);
-          setTemplateMessage(null);
-          setTemplateError(null);
-          setEditingTemplate(true);
-
-          if (templateId == null) return;
-
-          try {
-            const detail = await loadNoticeTemplateDetail(apiMessageGet, templateId);
-            setTemplateDraft(buildNoticeTemplateDraft(detail));
-          } catch (error) {
-            setTemplateError(null);
-          }
-        }
-
-        async function handleSaveTemplate() {
-          const validationError = validateNoticeTemplateDraft(templateDraft, t);
-          if (validationError) {
-            setTemplateMessage(null);
-            setTemplateError(validationError);
-            return;
-          }
-          setSavingTemplate(true);
-          setTemplateMessage(null);
-          setTemplateError(null);
-          try {
-            if (templateDraft.id) {
-              await updateNoticeTemplate(apiMessagePut as any, templateDraft);
-            } else {
-              await createNoticeTemplate(apiMessagePost as any, templateDraft);
-            }
-            setTemplateMessage(t('common.save-success'));
-            setEditingTemplate(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setTemplateError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setSavingTemplate(false);
-          }
-        }
-
-        async function handleDeleteTemplate() {
-          if (!selectedTemplate?.id || selectedTemplate.preset) return;
-          await handleDeleteTemplateById(selectedTemplate.id);
-        }
-
-        async function handleDeleteTemplateById(templateId: number) {
-          setDeleteRequest({ kind: 'template', id: templateId });
-        }
-
-        function commitTemplateSearch() {
-          setTemplateSearch(templateSearchDraft);
-          setTemplatePageIndex(0);
-        }
-
-        function resetTemplateSearch() {
-          setTemplateSearchDraft('');
-          setTemplateSearch('');
-          setTemplatePageIndex(0);
-        }
-
-        async function handleNewRule() {
-          setSelectedTab('rule');
-          setRuleDraft(buildNoticeRuleDraft(null, noticeEvidenceContext?.ruleDraftPatch));
-          setRuleMessage(null);
-          setRuleError(null);
-          setEditingRule(true);
-        }
-
-        async function handleEditRule(rule = selectedRule) {
-          const ruleId = rule?.id;
-          if (!ruleId) return;
-          try {
-            setSelectedTab('rule');
-            setSelectedRuleId(ruleId);
-            const detail = await loadNoticeRuleDetail(apiMessageGet, ruleId);
-            setRuleDraft(buildNoticeRuleDraft(detail));
-            setRuleMessage(null);
-            setRuleError(null);
-            setEditingRule(true);
-          } catch (error) {
-            setRuleError(error instanceof Error ? error.message : t('common.load-failed'));
-          }
-        }
-
-        async function handleSaveRule() {
-          const validationError = validateNoticeRuleDraft(ruleDraft, t);
-          if (validationError) {
-            setRuleMessage(null);
-            setRuleError(validationError);
-            return;
-          }
-          setSavingRule(true);
-          setRuleMessage(null);
-          setRuleError(null);
-          try {
-            if (ruleDraft.id) {
-              await updateNoticeRule(apiMessagePut as any, ruleDraft);
-            } else {
-              await createNoticeRule(apiMessagePost as any, ruleDraft);
-            }
-            setRuleMessage(t('common.save-success'));
-            setEditingRule(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setRuleError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setSavingRule(false);
-          }
-        }
-
-        async function handleDeleteRule() {
-          if (!selectedRule?.id) return;
-          await handleDeleteRuleById(selectedRule.id);
-        }
-
-        async function handleDeleteRuleById(ruleId: number) {
-          setDeleteRequest({ kind: 'rule', id: ruleId });
-        }
-
-        async function handleToggleRuleSwitch(rule: NoticeRule, field: 'filterAll' | 'enable', checked: boolean) {
-          if (!rule.id) return;
-          const pendingKey = `${rule.id}:${field}`;
-          setRuleSwitchPending(pendingKey);
-          setRuleError(null);
-          try {
-            await updateNoticeRule(apiMessagePut as any, buildNoticeRuleDraft({ ...rule, [field]: checked }));
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setRuleError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setRuleSwitchPending(current => (current === pendingKey ? null : current));
-          }
-        }
-
-        function commitReceiverSearch() {
-          setReceiverSearch(receiverSearchDraft.trim());
-          setReceiverPageIndex(0);
-        }
-
-        function resetReceiverSearch() {
-          setReceiverSearchDraft('');
-          setReceiverSearch('');
-          setReceiverPageIndex(0);
-        }
-
-        function commitRuleSearch() {
-          setRuleSearch(ruleSearchDraft.trim());
-          setRulePageIndex(0);
-        }
-
-        function resetRuleSearch() {
-          setRuleSearchDraft('');
-          setRuleSearch('');
-          setRulePageIndex(0);
-        }
-
-        const receiverPanel = (
-          <div data-alert-notice-receiver-panel="true">
-            <div
-              data-alert-notice-receiver-toolbar="cold-query-toolbar"
-              data-alert-notice-receiver-toolbar-layout="compact-inline-actions-query"
-              className={coldToolbarClass}
-            >
-              <div className="flex flex-wrap items-center gap-2">
-                <Button className={coldIconButtonClass} size="icon" variant="default" onClick={() => setRefreshTick(value => value + 1)} title={t('common.refresh')}>
-                  <RefreshCw className="h-4 w-4" aria-hidden="true" />
-                  <span className="sr-only">{t('common.refresh')}</span>
-                </Button>
-                <Button data-testid="notice-receiver-new" className={coldPrimaryCommandButtonClass} size="sm" variant="default" onClick={() => void handleNewReceiver()}>
-                  <Plus className="h-4 w-4" aria-hidden="true" />
-                  {t('alert.notice.receiver.new')}
-                </Button>
-                <Button
-                  data-alert-notice-receiver-bulk-menu="cold-more"
-                  data-testid="notice-receiver-delete"
-                  className={coldIconButtonClass}
-                  size="icon"
-                  variant="default"
-                  onClick={() => void handleDeleteReceiver()}
-                  disabled={!selectedReceiver}
-                  title={t('common.button.delete')}
-                >
-                  <MoreHorizontal className="h-4 w-4" aria-hidden="true" />
-                  <span className="sr-only">{t('common.button.delete')}</span>
-                </Button>
-              </div>
-                <SearchRow
-                  data-alert-notice-receiver-search="shared-compact"
-                className="mb-0"
-                value={receiverSearchDraft}
-                placeholder={t('alert.notice.receiver.people.name')}
-                searchLabel={t('common.search')}
-                clearLabel={t('common.clear')}
-                onValueChange={setReceiverSearchDraft}
-                onSearch={commitReceiverSearch}
-                onClear={receiverSearchDraft || receiverSearch ? resetReceiverSearch : undefined}
-              />
-            </div>
-            <AlertSurfaceTableShell data-alert-notice-receiver-table-shell="cold-dense-table" className={coldTableShellClass}>
-              <AlertSurfaceTable className="min-w-[1240px] text-center">
-                <AlertSurfaceTableHead className="text-center text-[13px] normal-case tracking-normal">
-                  <tr>
-                    <th className="w-[15%] px-3 py-3 text-center">{t('alert.notice.receiver.people')}</th>
-                    <th className="w-[15%] px-3 py-3 text-center">{t('alert.notice.receiver.type')}</th>
-                    <th className="w-[25%] px-3 py-3 text-center">{t('alert.notice.receiver.setting')}</th>
-                    <th className="w-[15%] px-3 py-3 text-center">{t('common.edit-time')}</th>
-                    <th className="w-[15%] px-3 py-3 text-center">{t('common.edit')}</th>
-                  </tr>
-                </AlertSurfaceTableHead>
-                <tbody>
-                  {data.receivers.content.length > 0 ? data.receivers.content.map(receiver => (
-                    <tr
-                      key={receiver.id}
-                      data-alert-notice-receiver-row={String(receiver.id)}
-                      className="border-t border-[#252b34] hover:bg-[#101217]"
-                      onClick={() => setSelectedReceiverId(receiver.id)}
-                    >
-                      <td className={`px-3 py-3 text-center font-medium ${coldTextPrimaryClass}`}>{receiver.name || t('alert.notice.receiver.people')}</td>
-                      <td className="px-3 py-3 text-center">
-                        <div className="flex justify-center">
-                          <AlertSurfaceValuePill>{getNoticeTypeLabel(receiver.type, t)}</AlertSurfaceValuePill>
-                        </div>
-                      </td>
-                      <td className="max-w-[340px] truncate px-3 py-3 text-center" title={getReceiverSetting(receiver)}>{getReceiverSetting(receiver)}</td>
-                      <td className="px-3 py-3 text-center">{formatTime(receiver.gmtUpdate || receiver.gmtCreate || null)}</td>
-                      <td className="px-3 py-3 text-center" onClick={event => event.stopPropagation()}>
-                        <div className="flex justify-center gap-2">
-                          <Button
-                            className={coldIconButtonClass}
-                            size="icon"
-                            variant="default"
-                            onClick={() => {
-                              setSelectedReceiverId(receiver.id);
-                              setReceiverDraft(buildNoticeReceiverDraft(receiver));
-                              setReceiverMessage(null);
-                              setReceiverError(null);
-                              setEditingReceiver(true);
-                            }}
-                            title={t('alert.notice.receiver.edit')}
-                          >
-                            <Pencil className="h-4 w-4" aria-hidden="true" />
-                            <span className="sr-only">{t('alert.notice.receiver.edit')}</span>
-                          </Button>
-                          <Button
-                            className={coldIconButtonClass}
-                            size="icon"
-                            variant="default"
-                            onClick={() => void handleDeleteReceiverById(receiver.id)}
-                            title={t('alert.notice.receiver.delete')}
-                          >
-                            <Trash2 className="h-4 w-4" aria-hidden="true" />
-                            <span className="sr-only">{t('alert.notice.receiver.delete')}</span>
-                          </Button>
-                        </div>
-                      </td>
-                    </tr>
-                  )) : (
-                    <NoticeTableEmptyRow t={t} colSpan={5} prefix="receiver" />
-                  )}
-                </tbody>
-              </AlertSurfaceTable>
-            </AlertSurfaceTableShell>
-            {(data.receivers.totalElements || 0) > 0 ? (
-              <NoticePagination
-                t={t}
-                pageIndex={receiverPageIndex}
-                pageSize={receiverPageSize}
-                totalElements={data.receivers.totalElements || 0}
-                testIdPrefix="notice-receiver"
-                onPageIndexChange={setReceiverPageIndex}
-                onPageSizeChange={nextPageSize => {
-                  setReceiverPageSize(nextPageSize);
-                  setReceiverPageIndex(0);
-                }}
-              />
-            ) : null}
-            {receiverMessage ? <div className="px-3 py-2 text-sm text-emerald-300">{receiverMessage}</div> : null}
-            {!editingReceiver && receiverError ? (
-              <div role="alert" className="px-3 py-2 text-sm text-rose-300">{receiverError}</div>
-            ) : null}
-          </div>
-        );
-
-        const rulePanel = (
-          <div data-alert-notice-rule-panel="true">
-            <div
-              data-alert-notice-rule-toolbar="cold-query-toolbar"
-              data-alert-notice-rule-toolbar-layout="compact-inline-actions-query"
-              className={coldToolbarClass}
-            >
-              <div className="flex flex-wrap items-center gap-2">
-                  <Button className={coldIconButtonClass} size="icon" variant="default" onClick={() => setRefreshTick(value => value + 1)} title={t('common.refresh')}>
-                  <RefreshCw className="h-4 w-4" aria-hidden="true" />
-                  <span className="sr-only">{t('common.refresh')}</span>
-                </Button>
-                <Button className={coldPrimaryCommandButtonClass} variant="default" size="sm" onClick={() => void handleNewRule()}>
-                  <Plus className="h-4 w-4" aria-hidden="true" />
-                  {t('alert.notice.rule.new')}
-                </Button>
-                <Button className={coldIconButtonClass} variant="default" size="icon" onClick={() => void handleDeleteRule()} disabled={!selectedRule} title={t('common.button.delete')}>
-                  <MoreHorizontal className="h-4 w-4" aria-hidden="true" />
-                  <span className="sr-only">{t('common.button.delete')}</span>
-                </Button>
-              </div>
-                <SearchRow
-                  data-alert-notice-rule-search="shared-compact"
-                className="mb-0"
-                value={ruleSearchDraft}
-                placeholder={t('alert.notice.rule.name')}
-                searchLabel={t('common.search')}
-                clearLabel={t('common.clear')}
-                onValueChange={setRuleSearchDraft}
-                onSearch={commitRuleSearch}
-                onClear={ruleSearchDraft || ruleSearch ? resetRuleSearch : undefined}
-              />
-            </div>
-            <AlertSurfaceTableShell data-alert-notice-rule-table-shell="cold-dense-table" className={coldTableShellClass}>
-              <AlertSurfaceTable className="min-w-[1240px] text-center">
-                <AlertSurfaceTableHead className="text-center text-[13px] normal-case tracking-normal">
-                  <tr>
-                    <th className="px-3 py-3 text-center">{t('alert.notice.rule.name')}</th>
-                    <th className="px-3 py-3 text-center">{t('alert.notice.receiver.people')}</th>
-                    <th className="px-3 py-3 text-center">{t('alert.notice.template.name')}</th>
-                    <th className="px-3 py-3 text-center">{t('alert.notice.rule.all')}</th>
-                    <th className="px-3 py-3 text-center">{t('common.enable')}</th>
-                    <th className="px-3 py-3 text-center">{t('common.edit-time')}</th>
-                    <th className="px-3 py-3 text-center">{t('common.edit')}</th>
-                  </tr>
-                </AlertSurfaceTableHead>
-                <tbody>
-                  {data.rules.content.length > 0 ? data.rules.content.map(rule => (
-                    <tr
-                      key={rule.id}
-                      data-alert-notice-rule-row={String(rule.id)}
-                      className="border-t border-[#252b34] hover:bg-[#101217]"
-                      onClick={() => setSelectedRuleId(rule.id)}
-                    >
-                      <td className={`px-3 py-3 text-center font-medium ${coldTextPrimaryClass}`}>{rule.name || t('alert.notice.rule')}</td>
-                      <td className="px-3 py-3 text-center">{rule.receiverName?.join(', ') || '-'}</td>
-                      <td className="px-3 py-3 text-center">{rule.templateName || t('alert.notice.template.preset.true')}</td>
-                      <td className="px-3 py-3 text-center" onClick={event => event.stopPropagation()}>
-                        <NoticeTableSwitch
-                          field="filter-all"
-                          checked={Boolean(rule.filterAll)}
-                          pending={ruleSwitchPending === `${rule.id}:filterAll`}
-                          label={t('alert.notice.rule.all')}
-                          onChange={checked => void handleToggleRuleSwitch(rule, 'filterAll', checked)}
-                        />
-                      </td>
-                      <td className="px-3 py-3 text-center" onClick={event => event.stopPropagation()}>
-                        <NoticeTableSwitch
-                          field="enable"
-                          checked={Boolean(rule.enable)}
-                          pending={ruleSwitchPending === `${rule.id}:enable`}
-                          label={t('common.enable')}
-                          onChange={checked => void handleToggleRuleSwitch(rule, 'enable', checked)}
-                        />
-                      </td>
-                      <td className="px-3 py-3 text-center">{formatTime(rule.gmtUpdate || rule.gmtCreate || null)}</td>
-                      <td className="px-3 py-3 text-center" onClick={event => event.stopPropagation()}>
-                        <div className="flex justify-center gap-2">
-                          <Button
-                            className={coldIconButtonClass}
-                            size="icon"
-                            variant="default"
-                            onClick={() => void handleEditRule(rule)}
-                            title={t('alert.notice.rule.edit')}
-                          >
-                            <Pencil className="h-4 w-4" aria-hidden="true" />
-                            <span className="sr-only">{t('alert.notice.rule.edit')}</span>
-                          </Button>
-                          <Button className={coldIconButtonClass} size="icon" variant="default" onClick={() => void handleDeleteRuleById(rule.id)} title={t('alert.notice.rule.delete')}>
-                            <Trash2 className="h-4 w-4" aria-hidden="true" />
-                            <span className="sr-only">{t('alert.notice.rule.delete')}</span>
-                          </Button>
-                        </div>
-                      </td>
-                    </tr>
-                  )) : (
-                    <NoticeTableEmptyRow t={t} colSpan={7} prefix="rule" />
-                  )}
-                </tbody>
-              </AlertSurfaceTable>
-            </AlertSurfaceTableShell>
-            {(data.rules.totalElements || 0) > 0 ? (
-              <NoticePagination
-                t={t}
-                pageIndex={rulePageIndex}
-                pageSize={rulePageSize}
-                totalElements={data.rules.totalElements || 0}
-                testIdPrefix="notice-rule"
-                onPageIndexChange={setRulePageIndex}
-                onPageSizeChange={nextPageSize => {
-                  setRulePageSize(nextPageSize);
-                  setRulePageIndex(0);
-                }}
-              />
-            ) : null}
-            {ruleMessage ? <div className="px-3 py-2 text-sm text-emerald-300">{ruleMessage}</div> : null}
-            {!editingRule && ruleError ? (
-              <div role="alert" className="px-3 py-2 text-sm text-rose-300">{ruleError}</div>
-            ) : null}
-          </div>
-        );
-
-        const templatePanel = (
-          <div data-alert-notice-template-panel="true">
-            <div
-              data-alert-notice-template-toolbar="cold-query-toolbar"
-              data-alert-notice-template-toolbar-layout="compact-inline-actions-query"
-              className={coldToolbarClass}
-            >
-              <div className="flex flex-wrap items-center gap-2">
-                <Button className={coldIconButtonClass} size="icon" variant="default" onClick={() => setRefreshTick(value => value + 1)} title={t('common.refresh')}>
-                  <RefreshCw className="h-4 w-4" aria-hidden="true" />
-                  <span className="sr-only">{t('common.refresh')}</span>
-                </Button>
-                <Button className={coldPrimaryCommandButtonClass} variant="default" size="sm" onClick={() => void handleNewTemplate()}>
-                  <Plus className="h-4 w-4" aria-hidden="true" />
-                  {t('alert.notice.template.new')}
-                </Button>
-                <Button className={coldIconButtonClass} variant="default" size="icon" onClick={() => void handleDeleteTemplate()} disabled={!selectedTemplateIsCustom} title={t('common.button.delete')}>
-                  <MoreHorizontal className="h-4 w-4" aria-hidden="true" />
-                  <span className="sr-only">{t('common.button.delete')}</span>
-                </Button>
-              </div>
-              <div className="flex min-w-0 flex-wrap items-center gap-2">
-                <Select
-                  data-alert-notice-template-preset-filter="cold-select"
-                  aria-label={t('monitor.status')}
-                  value={templatePresetFilter ? 'true' : 'false'}
-                  onChange={event => {
-                    setTemplatePresetFilter(event.target.value === 'true');
-                    setTemplatePageIndex(0);
-                  }}
-                  containerClassName="w-[132px]"
-                  className={coldSelectClass}
-                >
-                  <option value="true">{t('alert.notice.template.preset.true')}</option>
-                  <option value="false">{t('alert.notice.template.preset.false')}</option>
-                </Select>
-                <SearchRow
-                  data-alert-notice-template-search="shared-compact"
-                  className="mb-0"
-                  value={templateSearchDraft}
-                  placeholder={t('alert.notice.template.name')}
-                  searchLabel={t('common.search')}
-                  clearLabel={t('common.clear')}
-                  onValueChange={setTemplateSearchDraft}
-                  onSearch={commitTemplateSearch}
-                  onClear={templateSearchDraft || templateSearch ? resetTemplateSearch : undefined}
-                />
-              </div>
-            </div>
-            <AlertSurfaceTableShell data-alert-notice-template-table-shell="cold-dense-table" className={coldTableShellClass}>
-              <AlertSurfaceTable className="min-w-[1240px] text-center">
-                <AlertSurfaceTableHead className="text-center text-[13px] normal-case tracking-normal">
-                  <tr>
-                    <th className="px-3 py-3 text-center">{t('alert.notice.template.name')}</th>
-                    <th className="px-3 py-3 text-center">{t('alert.notice.template.type')}</th>
-                    <th className="px-3 py-3 text-center">{t('alert.notice.template.preset')}</th>
-                    <th className="px-3 py-3 text-center">{t('common.edit-time')}</th>
-                    <th className="px-3 py-3 text-center">{t('common.edit')}</th>
-                  </tr>
-                </AlertSurfaceTableHead>
-                <tbody>
-                  {templateVisibleRows.length > 0 ? templateVisibleRows.map((template, index) => (
-                    <tr
-                      key={String(template.id ?? template.name ?? index)}
-                      data-alert-notice-template-row={String(template.id ?? template.name ?? index)}
-                      className="border-t border-[#252b34] hover:bg-[#101217]"
-                      onClick={() => setSelectedTemplateId(typeof template.id === 'number' ? template.id : null)}
-                    >
-                      <td className={`px-3 py-3 text-center font-medium ${coldTextPrimaryClass}`}>{template.name || t('alert.notice.template')}</td>
-                      <td className="px-3 py-3 text-center">
-                        <div className="flex justify-center">
-                          <AlertSurfaceValuePill>{getNoticeTypeLabel(template.type, t)}</AlertSurfaceValuePill>
-                        </div>
-                      </td>
-                      <td className="px-3 py-3 text-center">
-                        <div className="flex justify-center">
-                          <AlertSurfaceValuePill>{template.preset ? t('alert.notice.template.preset.true') : t('alert.notice.template.preset.false')}</AlertSurfaceValuePill>
-                        </div>
-                      </td>
-                      <td className="px-3 py-3 text-center">{formatTime(template.gmtUpdate || template.gmtCreate || null)}</td>
-                      <td className="px-3 py-3 text-center" onClick={event => event.stopPropagation()}>
-                        <div className="flex justify-center gap-2">
-                          {template.preset ? (
-                            <Button
-                              data-alert-notice-template-view-trigger="cold-modal-viewer-trigger"
-                              className={coldIconButtonClass}
-                              size="icon"
-                              variant="default"
-                              onClick={() => void handleViewTemplate(template)}
-                              title={t('alert.notice.template.show')}
-                            >
-                              <Eye className="h-4 w-4" aria-hidden="true" />
-                              <span className="sr-only">{t('alert.notice.template.show')}</span>
-                            </Button>
-                          ) : (
-                            <>
-                              <Button
-                                className={coldIconButtonClass}
-                                size="icon"
-                                variant="default"
-                                onClick={() => void handleEditTemplate(template)}
-                                title={t('alert.notice.template.edit')}
-                              >
-                                <Pencil className="h-4 w-4" aria-hidden="true" />
-                                <span className="sr-only">{t('alert.notice.template.edit')}</span>
-                              </Button>
-                              <Button className={coldIconButtonClass} size="icon" variant="default" onClick={() => void handleDeleteTemplateById(template.id)} title={t('alert.notice.template.delete')}>
-                                <Trash2 className="h-4 w-4" aria-hidden="true" />
-                                <span className="sr-only">{t('alert.notice.template.delete')}</span>
-                              </Button>
-                            </>
-                          )}
-                        </div>
-                      </td>
-                    </tr>
-                  )) : (
-                    <NoticeTableEmptyRow t={t} colSpan={5} prefix="template" />
-                  )}
-                </tbody>
-              </AlertSurfaceTable>
-            </AlertSurfaceTableShell>
-            {templateTotalElements > 0 ? (
-              <NoticePagination
-                t={t}
-                pageIndex={normalizedTemplatePageIndex}
-                pageSize={templatePageSize}
-                totalElements={templateTotalElements}
-                testIdPrefix="notice-template"
-                onPageIndexChange={setTemplatePageIndex}
-                onPageSizeChange={nextPageSize => {
-                  setTemplatePageSize(nextPageSize);
-                  setTemplatePageIndex(0);
-                }}
-              />
-            ) : null}
-            {templateMessage ? <div className="px-3 py-2 text-sm text-emerald-300">{templateMessage}</div> : null}
-            {!editingTemplate && templateError ? (
-              <div role="alert" className="px-3 py-2 text-sm text-rose-300">{templateError}</div>
-            ) : null}
-          </div>
-        );
-
-        const receiverEditorDialog = (
-          <OverlayDialog
-            open={editingReceiver}
-            title={receiverDraft.id ? t('alert.notice.receiver.edit') : t('alert.notice.receiver.new')}
-            kicker={t('menu.alert.dispatch')}
-            onClose={() => setEditingReceiver(false)}
-            maxWidthClassName="max-w-4xl"
-            footer={
-              <div className="flex flex-wrap justify-end gap-2">
-                <Button data-testid="notice-receiver-cancel" className={coldButtonClassName} size="sm" variant="default" onClick={() => setEditingReceiver(false)}>
-                  {t('common.cancel')}
-                </Button>
-                <Button data-testid="notice-receiver-test" className={coldButtonClassName} size="sm" variant="default" onClick={() => void handleTestSend()}>
-                  {t('alert.notice.send-test')}
-                </Button>
-                <Button data-testid="notice-receiver-save" className={coldPrimaryButtonClassName} size="sm" variant="default" onClick={() => void handleSaveReceiver()} disabled={savingReceiver}>
-                  {savingReceiver ? t('common.saving') : t('common.save')}
-                </Button>
-              </div>
-            }
-          >
-            <div data-alert-notice-receiver-editor-dialog="cold-modal-editor" className="space-y-3">
-              {editingReceiver && receiverError ? (
-                <div
-                  role="alert"
-                  data-alert-notice-receiver-validation="cold-validation-feedback"
-                  className="rounded-[3px] border border-[#6f3141] bg-[#1b1014] px-3 py-2 text-[12px] font-semibold leading-5 text-[#ffb4c1]"
-                >
-                  {receiverError}
-                </div>
-              ) : null}
-              <AlertNoticeReceiverFields
-                t={t}
-                draft={receiverDraft}
-                productCopy={productCopy}
-                onDraftChange={setReceiverDraft}
-              />
-            </div>
-          </OverlayDialog>
-        );
-
-        const ruleEditorDialog = (
-          <OverlayDialog
-            open={editingRule}
-            title={ruleDraft.id ? t('alert.notice.rule.edit') : t('alert.notice.rule.new')}
-            kicker={t('menu.alert.dispatch')}
-            onClose={() => setEditingRule(false)}
-            maxWidthClassName="max-w-4xl"
-            footer={
-              <div className="flex flex-wrap justify-end gap-2">
-                {noticeEvidenceContext?.returnHref ? (
-                  <a
-                    data-alert-notice-rule-editor-return="evidence-context"
-                    href={noticeEvidenceContext.returnHref}
-                    className="inline-flex h-8 items-center gap-1 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white"
-                  >
-                    <ArrowLeft className="h-3.5 w-3.5" aria-hidden="true" />
-                    {t('alert.rule.evidence.return')}
-                  </a>
-                ) : null}
-                <Button className={coldButtonClassName} size="sm" variant="default" onClick={() => setEditingRule(false)}>
-                  {t('common.cancel')}
-                </Button>
-                <Button className={coldPrimaryButtonClassName} size="sm" variant="default" onClick={() => void handleSaveRule()} disabled={savingRule}>
-                  {savingRule ? t('common.saving') : t('common.save')}
-                </Button>
-              </div>
-            }
-          >
-            <div data-alert-notice-rule-editor-dialog="cold-modal-editor" className="space-y-3">
-              {editingRule && ruleError ? (
-                <div
-                  role="alert"
-                  data-alert-notice-rule-validation="cold-validation-feedback"
-                  className="rounded-[3px] border border-[#6f3141] bg-[#1b1014] px-3 py-2 text-[12px] font-semibold leading-5 text-[#ffb4c1]"
-                >
-                  {ruleError}
-                </div>
-              ) : null}
-              <AlertNoticeRuleFields
-                t={t}
-                draft={ruleDraft}
-                receiverIdsPlaceholder={productCopy.receiverIdsPlaceholder}
-                templateIdPlaceholder={productCopy.templateIdPlaceholder}
-                labelsPlaceholder={productCopy.labelsPlaceholder}
-                daysPlaceholder={productCopy.daysPlaceholder}
-                receiverOptions={receiverOptions}
-                templateOptions={templateOptions}
-                labelOptions={labelOptions}
-                onDraftChange={setRuleDraft}
-              />
-            </div>
-          </OverlayDialog>
-        );
-
-        const templateEditorDialog = (
-          <OverlayDialog
-            open={editingTemplate}
-            title={templateReadOnly ? t('alert.notice.template.content') : templateDraft.id ? t('alert.notice.template.edit') : t('alert.notice.template.new')}
-            kicker={t('menu.alert.dispatch')}
-            onClose={() => {
-              setEditingTemplate(false);
-              setTemplateReadOnly(false);
-            }}
-            maxWidthClassName="max-w-4xl"
-            footer={
-              <div className="flex flex-wrap justify-end gap-2">
-                <Button
-                  className={coldButtonClassName}
-                  size="sm"
-                  variant="default"
-                  onClick={() => {
-                    setEditingTemplate(false);
-                    setTemplateReadOnly(false);
-                  }}
-                >
-                  {t('common.cancel')}
-                </Button>
-                {!templateReadOnly ? (
-                  <Button className={coldPrimaryButtonClassName} size="sm" variant="default" onClick={() => void handleSaveTemplate()} disabled={savingTemplate}>
-                    {savingTemplate ? t('common.saving') : t('common.save')}
-                  </Button>
-                ) : null}
-              </div>
-            }
-          >
-            {templateReadOnly ? (
-              <div data-alert-notice-template-viewer-dialog="cold-modal-viewer" className="space-y-3">
-                <AlertNoticeTemplateFields t={t} draft={templateDraft} readOnly={templateReadOnly} onDraftChange={setTemplateDraft} />
-              </div>
-            ) : (
-              <div data-alert-notice-template-editor-dialog="cold-modal-editor" className="space-y-3">
-                {editingTemplate && templateError ? (
-                  <div
-                    role="alert"
-                    data-alert-notice-template-validation="cold-validation-feedback"
-                    className="rounded-[3px] border border-[#6f3141] bg-[#1b1014] px-3 py-2 text-[12px] font-semibold leading-5 text-[#ffb4c1]"
-                  >
-                    {templateError}
-                  </div>
-                ) : null}
-                <AlertNoticeTemplateFields t={t} draft={templateDraft} readOnly={templateReadOnly} onDraftChange={setTemplateDraft} />
-              </div>
-            )}
-          </OverlayDialog>
-        );
-
-        return (
-          <>
-            <div
-              data-alert-notice-surface="otlp-cold-notice-console"
-              data-alert-notice-style-baseline={coldNoticeVisual.canvasName}
-              className={coldNoticeVisual.canvas.root}
-              style={coldNoticeVisual.canvas.backgroundStyle}
-            >
-              <section className={coldNoticeVisual.layout.pageSection}>
-                <div className="mx-auto max-w-[1480px]">
-                  <div className="mb-5">
-                    <section
-                      data-alert-notice-header="cold-compact-header"
-                      data-alert-notice-admin-layout="full-width-admin-list"
-                      className={coldNoticeVisual.panel.hero}
-                    >
-                      <div className="flex flex-wrap items-start justify-between gap-5">
-                        <div className="min-w-[260px] flex-1">
-                          <h1 className="text-[30px] font-semibold leading-tight text-[#f5f7fb]">{t('menu.alert.dispatch')}</h1>
-                          <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">
-                            {t('alert.notice.copy')}
-                          </p>
-                          <div data-alert-notice-command-bar="standard-equal-buttons" className={coldNoticeVisual.button.row}>
-                            <Button size="sm" className={coldCommandButtonClass} variant="default" onClick={() => setRefreshTick(value => value + 1)}>
-                              {t('common.refresh')}
-                            </Button>
-                            <Button size="sm" className={coldCommandButtonClass} variant="default" onClick={() => void handleNewReceiver()}>
-                              {t('alert.notice.receiver.new')}
-                            </Button>
-                            <Button size="sm" className={coldPrimaryCommandButtonClass} variant="default" onClick={() => void handleNewRule()}>
-                              {t('alert.notice.rule.new')}
-                            </Button>
-                          </div>
-                        </div>
-                        <div
-                          data-alert-notice-inline-metrics="cold-inline-counts"
-                          className="flex min-w-[280px] flex-wrap justify-end gap-2"
-                        >
-                          {[
-                            { label: t('alert.notice.receiver'), value: receiverTotal },
-                            { label: t('alert.notice.rule'), value: ruleTotal },
-                            { label: t('alert.notice.template'), value: templateTotal }
-                          ].map(item => (
-                            <div
-                              key={item.label}
-                              className="grid h-8 min-w-[112px] grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-[3px] border border-[#2b3039] bg-[#101217] px-2.5"
-                            >
-                              <span className="truncate text-[12px] font-semibold text-[#8f99ab]">{item.label}</span>
-                              <span className="text-[13px] font-semibold text-[#f2f5f8]">{item.value}</span>
-                            </div>
-                          ))}
-                        </div>
-                      </div>
-                    </section>
-                  </div>
-                  <AlertNoticeConsoleShell
-                    t={t}
-                    selectedTab={selectedTab}
-                    onSelectTab={setSelectedTab}
-                    receiverContent={receiverPanel}
-                    ruleContent={rulePanel}
-                    templateContent={templatePanel}
-                  />
-                  {noticeEvidenceContext ? (
-                    <section
-                      data-alert-notice-evidence-context="signal-route"
-                      data-alert-notice-evidence-signal={noticeEvidenceContext.signal}
-                      data-alert-notice-prefill-labels={noticeEvidenceContext.labelsText}
-                      className="mt-5 rounded-[4px] border border-[#27303c] bg-[#0b0f15] px-4 py-3 shadow-[0_18px_48px_rgba(0,0,0,0.24)]"
-                    >
-                      <div className="flex flex-wrap items-start justify-between gap-3">
-                        <div className="min-w-0">
-                          <p className="text-[12px] font-semibold text-[#eef2f7]">{noticeEvidenceContext.title}</p>
-                          <p className="mt-1 max-w-[820px] text-[12px] leading-5 text-[#9099a7]">{noticeEvidenceContext.copy}</p>
-                        </div>
-                        {noticeEvidenceContext.returnHref ? (
-                          <a
-                            data-alert-notice-evidence-return="true"
-                            href={noticeEvidenceContext.returnHref}
-                            className="inline-flex h-8 items-center rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white"
-                          >
-                            {t('alert.rule.evidence.return')}
-                          </a>
-                        ) : null}
-                      </div>
-                      <div className="mt-3 rounded-[3px] border border-[#222a34] bg-[#080a0e] px-3 py-2 font-mono text-[11px] leading-5 text-[#9aa5b5]">
-                        {noticeEvidenceContext.labelsText || '-'}
-                      </div>
-                      <div className="mt-3 grid gap-2 md:grid-cols-3 xl:grid-cols-5">
-                        {noticeEvidenceContext.rows.map(row => (
-                          <div key={`${row.label}-${row.value}`} className="min-w-0 rounded-[3px] border border-[#222a34] bg-[#101217] px-3 py-2">
-                            <p className="text-[11px] font-semibold text-[#788292]">{row.label}</p>
-                            <p className="mt-1 truncate text-[13px] font-semibold text-[#eef2f7]" title={row.value}>{row.value}</p>
-                            <p className="mt-1 truncate text-[11px] text-[#778091]" title={row.meta}>{row.meta}</p>
-                          </div>
-                        ))}
-                      </div>
-                    </section>
-                  ) : null}
-                </div>
-              </section>
-            </div>
-            {receiverEditorDialog}
-            {ruleEditorDialog}
-            {templateEditorDialog}
-            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
-              <ColdConfirmDialog
-                open={Boolean(deleteRequest)}
-                title={t('common.confirm.delete')}
-                copy={
-                  deleteRequest?.kind === 'receiver'
-                    ? t('alert.notice.delete.confirm.receiver')
-                    : deleteRequest?.kind === 'template'
-                      ? t('alert.notice.delete.confirm.template')
-                      : t('alert.notice.delete.confirm.rule')
-                }
-                confirmLabel={t('common.button.ok')}
-                cancelLabel={t('common.button.cancel')}
-                pending={deletePending}
-                onCancel={() => setDeleteRequest(null)}
-                onConfirm={() => void handleConfirmedDelete()}
-              />
-            </div>
-          </>
-        )
-      }}
-    </ClientWorkbench>
-  );
+  searchParams?: Promise<AlertNoticeSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readAlertNoticeRouteState(resolvedSearchParams);
+  return <AlertNoticePage initialRouteState={routeState} />;
 }
diff --git a/web-next/app/alert/page.test.tsx b/web-next/app/alert/page.test.tsx
index bc5d5b02f3..c82bc703eb 100644
--- a/web-next/app/alert/page.test.tsx
+++ b/web-next/app/alert/page.test.tsx
@@ -4,11 +4,16 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
+import {
+  readAlertCenterRouteState,
+  type AlertCenterRouteState,
+  type AlertCenterSearchParams
+} from '../../lib/alert-manage/query-state';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   replace: vi.fn(),
   lastLoad: null as null | (() => Promise<unknown>),
+  lastSurfaceProps: null as null | Record<string, any>,
   renderData: {
     summary: {
       total: 3,
@@ -27,18 +32,28 @@ const mockState = vi.hoisted(() => ({
   }
 }));
 
-const loadAlertCenterData = vi.hoisted(() => vi.fn(async () => mockState.renderData));
-const applyAlertClosureOperation = vi.hoisted(() => vi.fn(async () => undefined));
+const loadAlertCenterDataFromFacade = vi.hoisted(() => vi.fn(async () => mockState.renderData));
+const applyAlertClosureOperationFromFacade = vi.hoisted(() => vi.fn(async () => undefined));
+const alertFacade = vi.hoisted(() => ({
+  summary: vi.fn(async () => undefined),
+  groupAlerts: vi.fn(async () => undefined),
+  groupStatus: vi.fn(async () => undefined),
+  groupClose: vi.fn(async () => undefined)
+}));
+const entityFacade = vi.hoisted(() => ({
+  detail: vi.fn(async () => undefined)
+}));
+const alertSilenceFacade = vi.hoisted(() => ({
+  create: vi.fn(async () => undefined)
+}));
+const alertInhibitFacade = vi.hoisted(() => ({
+  create: vi.fn(async () => undefined)
+}));
 
 vi.mock('next/navigation', () => ({
   useRouter: () => ({
     replace: mockState.replace
-  }),
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key),
-      has: (key: string) => mockState.searchParams.has(key)
-    }) as { get(name: string): string | null; has(name: string): boolean }
+  })
 }));
 
 vi.mock('../../components/providers/i18n-provider', () => ({
@@ -50,59 +65,111 @@ vi.mock('../../components/providers/i18n-provider', () => ({
 vi.mock('../../components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return <div data-client-workbench="true" data-loading-copy={loadingCopy}>{children(mockState.renderData)}</div>;
   }
 }));
 
 vi.mock('../../components/pages/alert-center-surface', () => ({
-  AlertCenterSurface: ({ draft, data }: { draft: Record<string, string>; data: { groupAlerts: { totalElements: number } } }) => (
-    <div
-      data-alert-center-surface="otlp-cold-center-console"
-      data-alert-center-style-baseline="hertzbeat-cold-matte"
-      data-draft={JSON.stringify(draft)}
-    />
-  )
+  AlertCenterSurface: (props: any) => {
+    mockState.lastSurfaceProps = props;
+    return (
+      <div
+        data-alert-center-surface="otlp-cold-center-console"
+        data-alert-center-style-baseline="hertzbeat-cold-matte"
+        data-draft={JSON.stringify(props.draft)}
+        data-rule-create={typeof props.onRuleQuickCreate}
+        data-realtime-event-count={props.realtimeEventCount}
+        data-realtime-group-ids={(props.realtimeGroupIds || []).join(',')}
+      />
+    );
+  }
 }));
 
 vi.mock('../../lib/api-client', () => ({
   apiMessageDelete: vi.fn(),
   apiMessageGet: vi.fn(),
+  apiMessagePost: vi.fn(),
   apiMessagePut: vi.fn()
 }));
 
+vi.mock('../../lib/alert-api-facade', () => ({
+  api: {
+    alerts: alertFacade,
+    entities: entityFacade,
+    alertSilences: alertSilenceFacade,
+    alertInhibits: alertInhibitFacade
+  }
+}));
+
 vi.mock('../../lib/alert-manage/controller', () => ({
-  applyAlertClosureOperation,
-  loadAlertCenterData
+  applyAlertClosureOperationFromFacade,
+  loadAlertCenterDataFromFacade
 }));
 
+function buildAlertCenterRouteState(searchParams: AlertCenterSearchParams = {}): AlertCenterRouteState {
+  return readAlertCenterRouteState(searchParams);
+}
+
+async function renderAlertCenterPage(initialRouteState: AlertCenterRouteState = buildAlertCenterRouteState()) {
+  const { default: AlertCenterPage } = await import('./alert-center-page');
+  return renderToStaticMarkup(<AlertCenterPage initialRouteState={initialRouteState} />);
+}
+
 describe('alert center page', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams('search=checkout&status=acknowledged&severity=warning');
     mockState.replace.mockClear();
     mockState.lastLoad = null;
-    loadAlertCenterData.mockClear().mockResolvedValue(mockState.renderData);
-    applyAlertClosureOperation.mockClear().mockResolvedValue(undefined);
+    mockState.lastSurfaceProps = null;
+    loadAlertCenterDataFromFacade.mockClear().mockResolvedValue(mockState.renderData);
+    applyAlertClosureOperationFromFacade.mockClear().mockResolvedValue(undefined);
+    alertFacade.summary.mockClear().mockResolvedValue(undefined);
+    alertFacade.groupAlerts.mockClear().mockResolvedValue(undefined);
+    alertFacade.groupStatus.mockClear().mockResolvedValue(undefined);
+    alertFacade.groupClose.mockClear().mockResolvedValue(undefined);
+    entityFacade.detail.mockClear().mockResolvedValue(undefined);
+    alertSilenceFacade.create.mockClear().mockResolvedValue(undefined);
+    alertInhibitFacade.create.mockClear().mockResolvedValue(undefined);
+  });
+
+  it('keeps alert center remounts on a short settled cache window while refresh and closure operations invalidate it', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/alert-center-page.tsx'), 'utf8');
+
+    expect(source).toContain('ALERT_CENTER_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['alert-center', alertListUrl, refreshNonce].join('|')");
+    expect(source).toContain('const refreshQuery = useCallback(');
+    expect(source).toContain('setRefreshNonce(current => current + 1)');
+    expect(source).toContain('onRefresh={refreshQuery}');
+    expect(source).toContain('cacheSettledTtlMs={ALERT_CENTER_SETTLED_CACHE_TTL_MS}');
   });
 
   it('loads the alert center through the shared query/controller contract', async () => {
-    const { default: AlertCenterPage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertCenterPage />);
+    const html = await renderAlertCenterPage(
+      buildAlertCenterRouteState({ search: 'checkout', status: 'acknowledged', severity: 'warning' })
+    );
 
     expect(html).toContain('data-alert-center-surface="otlp-cold-center-console"');
     expect(html).toContain('data-alert-center-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-loading-copy="Loading alert center"');
 
     await mockState.lastLoad?.();
 
-    expect(loadAlertCenterData).toHaveBeenCalledWith(expect.any(Function), {
+    expect(loadAlertCenterDataFromFacade).toHaveBeenCalledWith(expect.objectContaining({
+      alerts: alertFacade,
+      entities: entityFacade
+    }), {
       search: 'checkout',
       status: 'acknowledged',
       severity: 'warning',
+      pageIndex: 0,
+      pageSize: 8,
       entityId: '',
       entityName: '',
       returnTo: ''
@@ -110,19 +177,26 @@ describe('alert center page', () => {
   });
 
   it('keeps machine entity context in shared query state and drops display labels', async () => {
-    mockState.searchParams = new URLSearchParams(
-      'entityId=42&entityName=Checkout%20API&returnTo=%2Fentities%2F42%3FreturnLabel%3DCheckout&returnLabel=Checkout'
+    await renderAlertCenterPage(
+      buildAlertCenterRouteState({
+        entityId: '42',
+        entityName: 'Checkout API',
+        returnTo: '/entities/42?returnLabel=Checkout',
+        returnLabel: 'Checkout'
+      })
     );
 
-    const { default: AlertCenterPage } = await import('./page');
-    renderToStaticMarkup(<AlertCenterPage />);
-
     await mockState.lastLoad?.();
 
-    expect(loadAlertCenterData).toHaveBeenCalledWith(expect.any(Function), {
+    expect(loadAlertCenterDataFromFacade).toHaveBeenCalledWith(expect.objectContaining({
+      alerts: alertFacade,
+      entities: entityFacade
+    }), {
       search: '',
       status: 'firing',
       severity: '',
+      pageIndex: 0,
+      pageSize: 8,
       entityId: '42',
       entityName: 'Checkout API',
       returnTo: '/entities/42'
@@ -132,30 +206,34 @@ describe('alert center page', () => {
   it('keeps topology edge context when opened from the topology alert-impact action', async () => {
     const returnTo =
       '/topology?viewMode=resource-dependency&sourceKind=database-middleware-connection&edgeId=svc-checkout--res-orders-db&environment=prod&timeRange=last-1h';
-    mockState.searchParams = new URLSearchParams({
-      source: 'topology',
-      viewMode: 'resource-dependency',
-      sourceKind: 'database-middleware-connection',
-      edgeId: 'svc-checkout--res-orders-db',
-      entityId: 'service:commerce/checkout',
-      entityName: 'checkout-api',
-      serviceName: 'checkout-api',
-      serviceNamespace: 'commerce',
-      environment: 'prod',
-      timeRange: 'last-1h',
-      returnTo: `${returnTo}&returnLabel=HertzBeat%20%E4%BC%81%E4%B8%9A%E8%BF%90%E7%BB%B4%E6%8B%93%E6%89%91`,
-      returnLabel: 'HertzBeat 企业运维拓扑'
-    });
-
-    const { default: AlertCenterPage } = await import('./page');
-    renderToStaticMarkup(<AlertCenterPage />);
+    await renderAlertCenterPage(
+      buildAlertCenterRouteState({
+        source: 'topology',
+        viewMode: 'resource-dependency',
+        sourceKind: 'database-middleware-connection',
+        edgeId: 'svc-checkout--res-orders-db',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout-api',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        returnTo: `${returnTo}&returnLabel=HertzBeat%20%E4%BC%81%E4%B8%9A%E8%BF%90%E7%BB%B4%E6%8B%93%E6%89%91`,
+        returnLabel: 'HertzBeat 企业运维拓扑'
+      })
+    );
 
     await mockState.lastLoad?.();
 
-    expect(loadAlertCenterData).toHaveBeenCalledWith(expect.any(Function), {
+    expect(loadAlertCenterDataFromFacade).toHaveBeenCalledWith(expect.objectContaining({
+      alerts: alertFacade,
+      entities: entityFacade
+    }), {
       search: 'checkout-api',
       status: 'firing',
       severity: '',
+      pageIndex: 0,
+      pageSize: 8,
       entityId: 'service:commerce/checkout',
       entityName: 'checkout-api',
       serviceName: 'checkout-api',
@@ -171,33 +249,37 @@ describe('alert center page', () => {
   });
 
   it('keeps three-signal evidence context when opened from OTLP workbenches', async () => {
-    mockState.searchParams = new URLSearchParams({
-      signal: 'logs',
-      search: 'checkout',
-      status: 'firing',
-      entityId: '42',
-      entityName: 'Checkout API',
-      serviceName: 'checkout',
-      serviceNamespace: 'payments',
-      environment: 'prod',
-      timeRange: 'last-1h',
-      source: 'otlp',
-      traceId: 'trace-123',
-      spanId: 'span-456',
-      collector: 'collector-a',
-      template: 'spring-boot',
-      returnTo: '/log/manage?traceId=trace-123&returnLabel=Logs'
-    });
-
-    const { default: AlertCenterPage } = await import('./page');
-    renderToStaticMarkup(<AlertCenterPage />);
+    await renderAlertCenterPage(
+      buildAlertCenterRouteState({
+        signal: 'logs',
+        search: 'checkout',
+        status: 'firing',
+        entityId: '42',
+        entityName: 'Checkout API',
+        serviceName: 'checkout',
+        serviceNamespace: 'payments',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'otlp',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        collector: 'collector-a',
+        template: 'spring-boot',
+        returnTo: '/log/manage?traceId=trace-123&returnLabel=Logs'
+      })
+    );
 
     await mockState.lastLoad?.();
 
-    expect(loadAlertCenterData).toHaveBeenCalledWith(expect.any(Function), {
+    expect(loadAlertCenterDataFromFacade).toHaveBeenCalledWith(expect.objectContaining({
+      alerts: alertFacade,
+      entities: entityFacade
+    }), {
       search: 'checkout',
       status: 'firing',
       severity: '',
+      pageIndex: 0,
+      pageSize: 8,
       entityId: '42',
       entityName: 'Checkout API',
       serviceName: 'checkout',
@@ -215,39 +297,139 @@ describe('alert center page', () => {
   });
 
   it('canonicalizes dirty alert entry urls without display labels after hydration', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/alert-center-page.tsx'), 'utf8');
 
-    expect(source).toContain("searchParams.has('returnLabel')");
-    expect(source).toContain("searchParams.get('returnTo')?.includes('returnLabel')");
-    expect(source).toContain('router.replace(buildAlertCompatRouteUrl(searchParams))');
+    expect(source).toContain('if (alertCenterRouteState.shouldCleanUrl)');
+    expect(source).toContain('router.replace(alertCenterRouteState.cleanUrl)');
+    expect(source).not.toContain("searchParams.has('returnLabel')");
+    expect(source).not.toContain("searchParams.get('returnTo')?.includes('returnLabel')");
   });
 
   it('wires evidence-closure direct operations to the alert mutation controller', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/alert-center-page.tsx'), 'utf8');
 
-    expect(source).toContain('apiMessagePut');
-    expect(source).toContain('apiMessageDelete');
-    expect(source).toContain('applyAlertClosureOperation');
-    expect(source).toContain('onClosureAction={(action, groupId) => void handleClosureAction(action, groupId)}');
+    expect(source).toContain('api.alerts');
+    expect(source).toContain('applyAlertClosureOperationFromFacade');
+    expect(source).not.toContain('apiMessagePut');
+    expect(source).not.toContain('apiMessageDelete');
+    expect(source).toContain('onClosureAction={(action, groupId) => void handleClosureAction(action, groupId, data.groupAlerts.totalElements)}');
   });
 
   it('surfaces success and failure feedback after alert closure operations', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/alert-center-page.tsx'), 'utf8');
 
     expect(source).toContain('operationFeedback');
     expect(source).toContain('setOperationFeedback');
     expect(source).toContain('buildAlertClosureOperationFeedback(action, t)');
-    expect(source).toContain('alert.center.operation.failed');
+    expect(source).toContain('buildAlertClosureOperationFailureFeedback(action, t)');
     expect(source).toContain('operationFeedback={operationFeedback}');
   });
 
+  it('subscribes the alert center list to Angular ALERT_EVENT SSE refreshes', async () => {
+    const html = await renderAlertCenterPage();
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/alert-center-page.tsx'), 'utf8');
+
+    expect(html).toContain('data-realtime-event-count="0"');
+    expect(source).toContain('HEADER_ALERT_SSE_URL');
+    expect(source).toContain('HEADER_ALERT_EVENT_TYPE');
+    expect(source).toContain('parseHeaderSseJson<GroupAlert>(event.data)');
+    expect(source).toContain('new window.EventSource(HEADER_ALERT_SSE_URL)');
+    expect(source).toContain('eventSource.addEventListener(HEADER_ALERT_EVENT_TYPE, handleAlertEvent)');
+    expect(source).toContain('resolveRealtimeGroupId(alert)');
+    expect(source).toContain('setRealtimeEventCount(current => current + 1)');
+    expect(source).toContain('setRealtimeGroupIds(current => [realtimeGroupId, ...current.filter(groupId => groupId !== realtimeGroupId)].slice(0, 8))');
+    expect(source).toContain('realtimeEventCount={realtimeEventCount}');
+    expect(source).toContain('realtimeGroupIds={realtimeGroupIds}');
+    expect(source).toContain('eventSource.onerror = () =>');
+  });
+
+  it('normalizes realtime alert group ids for the Angular new-alert highlight contract', async () => {
+    const { resolveRealtimeGroupId } = await import('./alert-center-page');
+
+    expect(resolveRealtimeGroupId({ id: 42 })).toBe(42);
+    expect(resolveRealtimeGroupId({ id: 0, groupKey: '43' })).toBe(43);
+    expect(resolveRealtimeGroupId({ id: 0, groupKey: 'service:checkout' })).toBeNull();
+    expect(resolveRealtimeGroupId(null)).toBeNull();
+  });
+
   it('refreshes alert evidence with the post-closure query instead of dropping signal context', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/alert-center-page.tsx'), 'utf8');
 
     expect(source).toContain('buildAlertQueryAfterClosureOperation');
+    expect(source).toContain('clampAlertCenterPageIndexAfterDelete');
     expect(source).toContain('buildAlertClosureOperationFeedback');
     expect(source).toContain('buildAlertClosureOperationFeedback(action, t)');
-    expect(source).toContain('setDraft(current => buildAlertQueryAfterClosureOperation(current, action))');
-    expect(source).toContain('setQuery(current => buildAlertQueryAfterClosureOperation(current, action))');
+    expect(source).toContain("return action === 'close' || action === 'delete'");
+    expect(source).toContain('clampAlertCenterPageIndexAfterDelete(nextQuery, totalElements, affectedCount)');
+    expect(source).toContain('setDraft(current => buildPostActionQuery(current))');
+    expect(source).toContain('setQuery(current => buildPostActionQuery(current))');
+    expect(source).toContain('handleClosureAction(action, groupId, data.groupAlerts.totalElements)');
+  });
+
+  it('wires alert-center quick rule dialogs to create APIs and refreshes the workbench', async () => {
+    const html = await renderAlertCenterPage(
+      buildAlertCenterRouteState({ entityId: '42', entityName: 'Checkout API', returnTo: '/entities/42' })
+    );
+    const onRuleQuickCreate = mockState.lastSurfaceProps?.onRuleQuickCreate;
+
+    expect(html).toContain('data-rule-create="function"');
+    expect(onRuleQuickCreate).toEqual(expect.any(Function));
+
+    await onRuleQuickCreate('silence', {
+      name: 'Checkout API silence',
+      enable: true,
+      matchAll: false,
+      type: '0',
+      labelsText: 'service:checkout',
+      daysText: '1,2,3',
+      periodStart: '2026-05-25T10:00',
+      periodEnd: '2026-05-25T16:00'
+    });
+
+    expect(alertSilenceFacade.create).toHaveBeenCalledWith(expect.objectContaining({
+      name: 'Checkout API silence',
+      enable: true,
+      matchAll: false,
+      type: 0,
+      labels: { service: 'checkout' }
+    }));
+
+    await onRuleQuickCreate('inhibit', {
+      name: 'Checkout API inhibit',
+      enable: true,
+      sourceLabelsText: 'service:checkout',
+      targetLabelsText: 'service:checkout',
+      equalLabelsText: 'service'
+    });
+
+    expect(alertInhibitFacade.create).toHaveBeenCalledWith(expect.objectContaining({
+      name: 'Checkout API inhibit',
+      enable: true,
+      sourceLabels: { service: 'checkout' },
+      targetLabels: { service: 'checkout' },
+      equalLabels: ['service']
+    }));
+
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/alert-center-page.tsx'), 'utf8');
+    expect(source).toContain('createAlertSilenceFromFacade(api.alertSilences.create');
+    expect(source).toContain('createAlertInhibitFromFacade(api.alertInhibits.create');
+    expect(source).not.toContain('apiMessagePost');
+    expect(source).toContain('onRuleQuickCreate={handleRuleQuickCreate}');
+    expect(source).toContain("setOperationFeedback({ tone: 'success', copy: t('common.notify.new-success') })");
+    expect(source).toContain("copy: t('common.notify.new-fail')");
+    expect(source).not.toContain("setOperationFeedback({ tone: 'success', copy: t('common.save-success') })");
+    expect(source).toContain('setSelectedGroupIds([])');
+    expect(source).toContain('setRefreshNonce(current => current + 1)');
+  });
+
+  it('remembers entity response result state for return links after alert operations', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/alert-center-page.tsx'), 'utf8');
+
+    expect(source).toContain('normalizeEntityResponseAction');
+    expect(source).toContain('useState<AlertEntityResponseResult>(null)');
+    expect(source).toContain('setEntityResponseResult({ action: normalizeEntityResponseAction(action), count: affectedCount })');
+    expect(source).toContain('setEntityResponseResult({ action: mode, count })');
+    expect(source).toContain('entityResponseResult={entityResponseResult}');
+    expect(source).toContain('setEntityResponseResult(null)');
   });
 });
diff --git a/web-next/app/alert/page.tsx b/web-next/app/alert/page.tsx
index 798cb6f89f..d557c69e10 100644
--- a/web-next/app/alert/page.tsx
+++ b/web-next/app/alert/page.tsx
@@ -1,99 +1,14 @@
-'use client';
-
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { AlertCenterSurface } from '../../components/pages/alert-center-surface';
-import { useI18n } from '../../components/providers/i18n-provider';
-import { ClientWorkbench } from '../../components/workbench/client-workbench';
-import { apiMessageDelete, apiMessageGet, apiMessagePut } from '../../lib/api-client';
-import {
-  applyAlertClosureOperation,
-  buildAlertQueryAfterClosureOperation,
-  loadAlertCenterData,
-  type AlertClosureOperationAction,
-  type AlertPageData
-} from '../../lib/alert-manage/controller';
-import {
-  buildAlertCompatRouteUrl,
-  hasAlertEntityContext,
-  queryStateFromParams,
-  type AlertQueryState
-} from '../../lib/alert-manage/query-state';
-import { buildAlertClosureOperationFeedback } from '../../lib/alert-manage/view-model';
-
-const EMPTY_ALERT_QUERY: AlertQueryState = {
-  search: '',
-  status: '',
-  severity: '',
-  entityId: '',
-  entityName: '',
-  returnTo: ''
-};
-
-export default function AlertCenterPage() {
-  const { t } = useI18n();
-  const router = useRouter();
-  const searchParams = useSearchParams();
-  const initialQuery = useMemo(() => queryStateFromParams(searchParams), [searchParams]);
-  const [draft, setDraft] = useState<AlertQueryState>(initialQuery);
-  const [query, setQuery] = useState<AlertQueryState>(initialQuery);
-  const [operationFeedback, setOperationFeedback] = useState<{ tone: 'success' | 'danger'; copy: string } | null>(null);
-
-  useEffect(() => {
-    setDraft(initialQuery);
-    setQuery(initialQuery);
-  }, [initialQuery]);
-
-  useEffect(() => {
-    if (searchParams.has('returnLabel') || searchParams.get('returnTo')?.includes('returnLabel')) {
-      router.replace(buildAlertCompatRouteUrl(searchParams));
-    }
-  }, [router, searchParams]);
-
-  const load = useCallback(async (): Promise<AlertPageData> => {
-    return loadAlertCenterData(apiMessageGet, query);
-  }, [query]);
-
-  const handleClosureAction = useCallback(async (action: AlertClosureOperationAction, groupId: number) => {
-    setOperationFeedback(null);
-    try {
-      await applyAlertClosureOperation(apiMessagePut as any, apiMessageDelete as any, action, groupId);
-      setOperationFeedback({ tone: 'success', copy: buildAlertClosureOperationFeedback(action, t) });
-      setDraft(current => buildAlertQueryAfterClosureOperation(current, action));
-      setQuery(current => buildAlertQueryAfterClosureOperation(current, action));
-    } catch (error) {
-      setOperationFeedback({
-        tone: 'danger',
-        copy: t('alert.center.operation.failed', {
-          reason: error instanceof Error ? error.message : t('common.failed')
-        })
-      });
-    }
-  }, [t]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('alert.center.loading')}>
-      {data => (
-        <AlertCenterSurface
-          t={t}
-          data={data}
-          draft={draft}
-          onDraftChange={setDraft}
-          onRefresh={() => setQuery({ ...draft })}
-          onClearFilters={() => {
-            const cleared = {
-              ...draft,
-              search: '',
-              severity: '',
-              status: hasAlertEntityContext(draft) ? 'firing' : ''
-            };
-            setDraft(cleared);
-            setQuery(cleared);
-          }}
-          operationFeedback={operationFeedback}
-          onClosureAction={(action, groupId) => void handleClosureAction(action, groupId)}
-        />
-      )}
-    </ClientWorkbench>
-  );
+import React from 'react';
+
+import AlertCenterPage from './alert-center-page';
+import { readAlertCenterRouteState, type AlertCenterSearchParams } from '../../lib/alert-manage/query-state';
+
+export default async function AlertCenterRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<AlertCenterSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readAlertCenterRouteState(resolvedSearchParams);
+  return <AlertCenterPage initialRouteState={routeState} />;
 }
diff --git a/web-next/app/alert/setting/alert-setting-page.tsx b/web-next/app/alert/setting/alert-setting-page.tsx
new file mode 100644
index 0000000000..9f98ef86a4
--- /dev/null
+++ b/web-next/app/alert/setting/alert-setting-page.tsx
@@ -0,0 +1,502 @@
+'use client';
+
+import React, { useCallback, useMemo, useRef, useState } from 'react';
+import {
+  HzExportTypeDialog,
+  HzFileInput,
+  type HzExportTypeDialogType,
+  type HzStatusTone
+} from '@hertzbeat/ui';
+import { ClientWorkbench } from '../../../components/workbench/client-workbench';
+import { AlertSettingSurface } from '../../../components/pages/alert-setting-surface';
+import {
+  AlertSettingCreateDialog,
+  buildAlertSettingDraftFromDefine,
+  createDefaultAlertSettingDraft,
+  type AlertSettingCreateKind,
+  type AlertSettingCreateMode,
+  type AlertSettingCreatePayload
+} from '../../../components/pages/alert-setting-create-dialog';
+import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
+import { useI18n } from '../../../components/providers/i18n-provider';
+import { getCurrentLocale } from '../../../lib/api-client';
+import { api } from '../../../lib/alert-api-facade';
+import {
+  buildAlertDefineExportUrl,
+  buildAlertDefineImportUrl,
+  deleteAlertDefineFromFacade,
+  deleteAlertDefinesFromFacade,
+  createAlertDefineFromFacade,
+  loadAlertDefineDetailFromFacade,
+  loadAlertSettingDataFromFacade,
+  updateAlertDefineEnabledFromFacade,
+  updateAlertDefineFromFacade
+} from '../../../lib/alert-setting/controller';
+import { buildAlertSettingEvidenceContext } from '../../../lib/alert-setting/view-model';
+import {
+  buildAlertSettingAppEntries,
+  buildDefineListUrl,
+  type AlertSettingRouteState
+} from '../../../lib/alert-setting/query-state';
+import { formatTime } from '../../../lib/format';
+
+type SettingDeleteRequest = {
+  kind: 'single' | 'batch';
+  ids: number[];
+};
+
+type AlertSettingActionFeedback = {
+  tone: HzStatusTone;
+  title: string;
+  description?: string;
+  contract?: 'delete' | 'enable' | 'export-fail' | 'no-select-delete' | 'no-select-export' | 'import-success' | 'import-fail';
+};
+
+type AlertSettingSaveFeedback = {
+  tone: HzStatusTone;
+  title: string;
+  description?: string;
+  contract: 'create' | 'edit';
+};
+
+const ALERT_SETTING_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_ALERT_SETTING_ROUTE_STATE: AlertSettingRouteState = {
+  signal: null,
+  signalContext: {}
+};
+
+function resolveDownloadFilename(contentDisposition: string | null, fallbackName: string) {
+  const match = contentDisposition?.match(/filename\*?=(?:UTF-8'')?("?)([^";]+)\1/i);
+  if (!match?.[2]) return fallbackName;
+  try {
+    return decodeURIComponent(match[2]);
+  } catch {
+    return match[2];
+  }
+}
+
+export default function AlertSettingPage({ initialRouteState }: { initialRouteState?: AlertSettingRouteState } = {}) {
+  const { t } = useI18n();
+  const alertSettingRouteState = initialRouteState ?? EMPTY_ALERT_SETTING_ROUTE_STATE;
+  const { signal, signalContext } = alertSettingRouteState;
+  const evidenceContext = useMemo(
+    () => buildAlertSettingEvidenceContext(signal, signalContext, t),
+    [signal, signalContext, t]
+  );
+  const [search, setSearch] = useState('');
+  const [query, setQuery] = useState('');
+  const [pageIndex, setPageIndex] = useState(0);
+  const [pageSize, setPageSize] = useState(8);
+  const [refreshKey, setRefreshKey] = useState(0);
+  const [checkedIds, setCheckedIds] = useState<number[]>([]);
+  const [createMode, setCreateMode] = useState<AlertSettingCreateMode | 'closed'>('closed');
+  const [createDraft, setCreateDraft] = useState(() => createDefaultAlertSettingDraft('realtime'));
+  const [creating, setCreating] = useState(false);
+  const [deleteRequest, setDeleteRequest] = useState<SettingDeleteRequest | null>(null);
+  const [deletePending, setDeletePending] = useState(false);
+  const [exportDialogOpen, setExportDialogOpen] = useState(false);
+  const [pendingExportType, setPendingExportType] = useState<HzExportTypeDialogType | null>(null);
+  const [pendingActionId, setPendingActionId] = useState<string | null>(null);
+  const [actionFeedback, setActionFeedback] = useState<AlertSettingActionFeedback | null>(null);
+  const [saveFeedback, setSaveFeedback] = useState<AlertSettingSaveFeedback | null>(null);
+  const importInputRef = useRef<HTMLInputElement | null>(null);
+  const alertSettingListUrl = useMemo(() => buildDefineListUrl(query, pageIndex, pageSize), [query, pageIndex, pageSize]);
+  const alertSettingCacheKey = useMemo(
+    () => ['alert-setting', alertSettingListUrl, refreshKey].join('|'),
+    [alertSettingListUrl, refreshKey]
+  );
+
+  const load = useCallback(async () => {
+    void refreshKey;
+    const appMap = await api.alertSettings.appDefines(getCurrentLocale()).catch(() => null);
+    const appEntries = buildAlertSettingAppEntries(appMap);
+    return loadAlertSettingDataFromFacade(
+      {
+        list: api.alertSettings.list,
+        datasourceStatus: api.alertSettings.datasourceStatus
+      },
+      query,
+      pageIndex,
+      pageSize,
+      appEntries
+    );
+  }, [query, pageIndex, pageSize, refreshKey]);
+
+  function openTypeSelection() {
+    setSaveFeedback(null);
+    setCreateDraft(createDefaultAlertSettingDraft('realtime', { labelsText: evidenceContext?.labelsText || '' }));
+    setCreateMode('type');
+  }
+
+  function closeCreateFlow() {
+    setCreateMode('closed');
+    setCreating(false);
+    setSaveFeedback(null);
+  }
+
+  function selectCreateType(kind: AlertSettingCreateKind) {
+    setSaveFeedback(null);
+    setCreateDraft(current => createDefaultAlertSettingDraft(kind, current));
+    setCreateMode('authoring');
+  }
+
+  async function submitCreate(payload: AlertSettingCreatePayload) {
+    const isEdit = typeof payload.id === 'number';
+    setCreating(true);
+    setSaveFeedback(null);
+    try {
+      if (isEdit) {
+        await updateAlertDefineFromFacade(api.alertSettings.update, payload);
+      } else {
+        await createAlertDefineFromFacade(api.alertSettings.create, payload);
+      }
+      closeCreateFlow();
+      setCheckedIds([]);
+      setRefreshKey(value => value + 1);
+    } catch (error) {
+      setSaveFeedback({
+        tone: 'critical',
+        title: t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'),
+        description: error instanceof Error ? error.message : undefined,
+        contract: isEdit ? 'edit' : 'create'
+      });
+    } finally {
+      setCreating(false);
+    }
+  }
+
+  function requestSingleDelete(defineId: number) {
+    setDeleteRequest({ kind: 'single', ids: [defineId] });
+  }
+
+  function requestBatchDelete() {
+    if (checkedIds.length === 0) {
+      setActionFeedback({
+        tone: 'warning',
+        title: t('common.notify.no-select-delete'),
+        contract: 'no-select-delete'
+      });
+      return;
+    }
+    setActionFeedback(null);
+    setDeleteRequest({ kind: 'batch', ids: checkedIds });
+  }
+
+  async function confirmDelete() {
+    const request = deleteRequest;
+    if (!request || request.ids.length === 0) return;
+    setDeletePending(true);
+    setActionFeedback(null);
+    try {
+      if (request.kind === 'batch') {
+        await deleteAlertDefinesFromFacade(api.alertSettings.delete, request.ids);
+      } else {
+        await deleteAlertDefineFromFacade(api.alertSettings.delete, request.ids[0]);
+      }
+      setCheckedIds([]);
+      setDeleteRequest(null);
+      setRefreshKey(value => value + 1);
+    } catch (error) {
+      setActionFeedback({
+        tone: 'critical',
+        title: t('common.notify.delete-fail'),
+        description: error instanceof Error ? error.message : undefined,
+        contract: 'delete'
+      });
+    } finally {
+      setDeletePending(false);
+    }
+  }
+
+  async function downloadAlertDefineExport(type: HzExportTypeDialogType) {
+    const locale = getCurrentLocale();
+    const response = await fetch(`/api${buildAlertDefineExportUrl(checkedIds, type)}`, {
+      headers: {
+        ...(locale ? { 'Accept-Language': locale } : {})
+      },
+      credentials: 'same-origin',
+      cache: 'no-store'
+    });
+    if (!response.ok) {
+      throw new Error(t('common.notify.export-fail-status', { status: response.status }));
+    }
+    const contentType = response.headers.get('Content-Type') || '';
+    if (contentType.includes('application/json')) {
+      throw new Error('');
+    }
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const anchor = document.createElement('a');
+    anchor.href = url;
+    anchor.download = resolveDownloadFilename(response.headers.get('Content-Disposition'), type === 'JSON' ? 'alert-defines.json' : 'alert-defines.xlsx');
+    anchor.click();
+    window.URL.revokeObjectURL(url);
+  }
+
+  async function uploadAlertDefineImport(file: File) {
+    const locale = getCurrentLocale();
+    const formData = new FormData();
+    formData.append('file', file);
+    const response = await fetch(`/api${buildAlertDefineImportUrl()}`, {
+      method: 'POST',
+      headers: {
+        ...(locale ? { 'Accept-Language': locale } : {})
+      },
+      credentials: 'same-origin',
+      body: formData
+    });
+    if (!response.ok) {
+      throw new Error(t('common.notify.import-fail-status', { status: response.status }));
+    }
+    const payload = (await response.json()) as { code: number; msg?: string };
+    if (payload.code !== 0) {
+      throw new Error(payload.msg || t('common.notify.import-fail'));
+    }
+  }
+
+  async function runSettingAction(
+    actionId: string,
+    pendingTitle: string,
+    task: () => Promise<string>,
+    fallbackTitle: string,
+    contracts: { success?: AlertSettingActionFeedback['contract']; failure?: AlertSettingActionFeedback['contract'] } = {}
+  ) {
+    setPendingActionId(actionId);
+    setActionFeedback({ tone: 'info', title: pendingTitle });
+    try {
+      const successTitle = await task();
+      setActionFeedback({ tone: 'success', title: successTitle, contract: contracts.success });
+      return true;
+    } catch (error) {
+      setActionFeedback({
+        tone: 'critical',
+        title: fallbackTitle,
+        description: error instanceof Error ? error.message : t('common.failed'),
+        contract: contracts.failure
+      });
+      return false;
+    } finally {
+      setPendingActionId(current => (current === actionId ? null : current));
+    }
+  }
+
+  function openExportDialog() {
+    if (checkedIds.length === 0) {
+      setActionFeedback({
+        tone: 'warning',
+        title: t('common.notify.no-select-export'),
+        contract: 'no-select-export'
+      });
+      return;
+    }
+    setActionFeedback(null);
+    setExportDialogOpen(true);
+  }
+
+  async function handleExportDialogSelect(type: HzExportTypeDialogType) {
+    if (checkedIds.length === 0) {
+      setActionFeedback({
+        tone: 'warning',
+        title: t('common.notify.no-select-export'),
+        contract: 'no-select-export'
+      });
+      setExportDialogOpen(false);
+      return;
+    }
+    setPendingActionId('export');
+    setPendingExportType(type);
+    setActionFeedback(null);
+    try {
+      await downloadAlertDefineExport(type);
+      setExportDialogOpen(false);
+    } catch (error) {
+      setActionFeedback({
+        tone: 'critical',
+        title: t('common.notify.export-fail'),
+        description: error instanceof Error && error.message ? error.message : undefined,
+        contract: 'export-fail'
+      });
+    } finally {
+      setPendingActionId(current => (current === 'export' ? null : current));
+      setPendingExportType(null);
+    }
+  }
+
+  function handleImportClick() {
+    importInputRef.current?.click();
+  }
+
+  async function handleImportChange(event: React.ChangeEvent<HTMLInputElement>) {
+    const file = event.target.files?.[0];
+    event.target.value = '';
+    if (!file) return;
+    await runSettingAction(
+      'import',
+      t('common.notify.import-submitted', { taskName: file.name }),
+      async () => {
+        await uploadAlertDefineImport(file);
+        setCheckedIds([]);
+        setRefreshKey(value => value + 1);
+        return t('common.notify.import-success');
+      },
+      t('common.notify.import-fail'),
+      { success: 'import-success', failure: 'import-fail' }
+    );
+  }
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('alert.setting.loading')}
+      cacheKey={alertSettingCacheKey}
+      cacheSettledTtlMs={ALERT_SETTING_SETTLED_CACHE_TTL_MS}
+    >
+      {data => {
+        async function handleToggleEnabled(defineId: number, enabled: boolean) {
+          const target = data.list.content.find(item => item.id === defineId);
+          if (!target) return;
+          setActionFeedback(null);
+          try {
+            await updateAlertDefineEnabledFromFacade(api.alertSettings.update, target, enabled);
+            setRefreshKey(value => value + 1);
+          } catch (error) {
+            setActionFeedback({
+              tone: 'critical',
+              title: t('common.notify.edit-fail'),
+              description: error instanceof Error ? error.message : undefined,
+              contract: 'enable'
+            });
+          }
+        }
+
+        async function handleEdit(defineId: number) {
+          const define = await loadAlertDefineDetailFromFacade(api.alertSettings.detail, defineId);
+          setSaveFeedback(null);
+          setCreateDraft(buildAlertSettingDraftFromDefine(define));
+          setCreateMode('authoring');
+        }
+
+        return (
+          <>
+            <AlertSettingSurface
+              t={t}
+              data={data}
+              search={search}
+              checkedIds={checkedIds}
+              evidenceContext={evidenceContext}
+              formatTime={formatTime}
+              onSearchChange={setSearch}
+              onApplyFilter={() => {
+                setQuery(search);
+                setPageIndex(0);
+                setCheckedIds([]);
+              }}
+              onClearFilter={() => {
+                setSearch('');
+                setQuery('');
+                setPageIndex(0);
+                setCheckedIds([]);
+              }}
+              onRefresh={() => {
+                setRefreshKey(value => value + 1);
+                setCheckedIds([]);
+              }}
+              onNew={openTypeSelection}
+              onDeleteSelected={requestBatchDelete}
+              onExport={openExportDialog}
+              onImport={handleImportClick}
+              onToggleEnabled={(defineId, enabled) => void handleToggleEnabled(defineId, enabled)}
+              onEdit={defineId => void handleEdit(defineId)}
+              onDelete={requestSingleDelete}
+              onCheckedIdsChange={setCheckedIds}
+              onPageIndexChange={nextPageIndex => {
+                setPageIndex(nextPageIndex);
+                setCheckedIds([]);
+              }}
+              onPageSizeChange={nextPageSize => {
+                setPageIndex(0);
+                setPageSize(nextPageSize);
+                setCheckedIds([]);
+              }}
+              pendingActionId={pendingActionId}
+              actionFeedback={actionFeedback}
+            />
+            <HzFileInput
+              ref={importInputRef}
+              aria-label={t('alert.setting.import.input')}
+              data-alert-setting-import-input-owner="hertzbeat-ui-file-input"
+              data-alert-setting-import-file-input="true"
+              onChange={event => void handleImportChange(event)}
+            />
+            <AlertSettingCreateDialog
+              t={t}
+              open={createMode !== 'closed'}
+              mode={createMode === 'closed' ? 'type' : createMode}
+              intent={typeof createDraft.id === 'number' ? 'edit' : 'create'}
+              datasourceStatus={data.datasourceStatus}
+              draft={createDraft}
+              submitting={creating}
+              saveFeedback={saveFeedback}
+              evidenceReturnHref={evidenceContext?.returnHref}
+              onClose={closeCreateFlow}
+              onSelectType={selectCreateType}
+              onDraftChange={setCreateDraft}
+              onBackToType={() => setCreateMode('type')}
+              onSubmit={submitCreate}
+            />
+            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
+              <ColdConfirmDialog
+                open={Boolean(deleteRequest)}
+                title={deleteRequest?.kind === 'batch' ? t('common.confirm.delete-batch') : t('common.confirm.delete')}
+                copy={
+                  deleteRequest?.kind === 'batch'
+                    ? t('alert.setting.delete.confirm.batch', { count: deleteRequest.ids.length })
+                    : t('alert.setting.delete.confirm.single')
+                }
+                confirmLabel={t('common.button.ok')}
+                cancelLabel={t('common.button.cancel')}
+                pending={deletePending}
+                onCancel={() => setDeleteRequest(null)}
+                onConfirm={() => void confirmDelete()}
+              />
+            </div>
+            <HzExportTypeDialog
+              open={exportDialogOpen}
+              title={t('alert.export.switch-type')}
+              description={t('alert.setting.export.selected', { count: checkedIds.length })}
+              scope="selected"
+              selectedCount={checkedIds.length}
+              closeLabel={t('common.button.cancel')}
+              onClose={() => setExportDialogOpen(false)}
+              onSelect={type => void handleExportDialogSelect(type)}
+              jsonBusy={pendingExportType === 'JSON'}
+              excelBusy={pendingExportType === 'EXCEL'}
+              jsonDescription={t('alert.export.use-type', { type: 'JSON' })}
+              excelDescription={t('alert.export.use-type', { type: 'EXCEL' })}
+              data-alert-setting-export-type-dialog-owner="hertzbeat-ui-export-type-dialog"
+              data-alert-setting-export-type-dialog={exportDialogOpen ? 'open' : 'closed'}
+              data-alert-setting-export-success-contract="angular-download-closes-dialog-no-toast"
+              data-alert-setting-export-success-owner="route-action-feedback-contract"
+              data-alert-setting-export-loading-contract="angular-selected-type-only"
+              data-alert-setting-export-loading-owner="route-action-feedback-contract"
+              jsonButtonProps={
+                {
+                  'data-alert-setting-export-type-option-owner': 'hertzbeat-ui-export-type-dialog',
+                  'data-alert-setting-export-type-option': 'json',
+                  'data-alert-setting-export-loading': 'json-selected-only'
+                } as React.ComponentProps<typeof HzExportTypeDialog>['jsonButtonProps']
+              }
+              excelButtonProps={
+                {
+                  'data-alert-setting-export-type-option-owner': 'hertzbeat-ui-export-type-dialog',
+                  'data-alert-setting-export-type-option': 'excel',
+                  'data-alert-setting-export-loading': 'excel-selected-only'
+                } as React.ComponentProps<typeof HzExportTypeDialog>['excelButtonProps']
+              }
+            />
+          </>
+        );
+      }}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/alert/setting/page.test.tsx b/web-next/app/alert/setting/page.test.tsx
index f869c7359a..5aa3ccf9ca 100644
--- a/web-next/app/alert/setting/page.test.tsx
+++ b/web-next/app/alert/setting/page.test.tsx
@@ -4,14 +4,16 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import type { AlertSettingRouteState } from '../../../lib/alert-setting/query-state';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   lastOnNew: null as null | (() => void),
   lastOnSubmit: null as null | ((payload: unknown) => Promise<void>),
   lastOnToggleEnabled: null as null | ((defineId: number, enabled: boolean) => void),
   lastOnEdit: null as null | ((defineId: number) => Promise<void> | void),
+  lastOnExport: null as null | (() => void),
+  lastOnImport: null as null | (() => void),
   push: vi.fn(),
   renderData: {
     list: {
@@ -44,16 +46,6 @@ const apiMessageGet = vi.hoisted(() => vi.fn());
 const apiMessageDelete = vi.hoisted(() => vi.fn());
 const apiMessagePut = vi.hoisted(() => vi.fn());
 
-vi.mock('next/navigation', () => ({
-  useRouter: () => ({
-    push: mockState.push
-  }),
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
-}));
-
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock()
@@ -63,13 +55,15 @@ vi.mock('../../../components/providers/i18n-provider', () => ({
 vi.mock('../../../components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return <div data-client-workbench="true" data-loading-copy={loadingCopy}>{children(mockState.renderData)}</div>;
   }
 }));
 
@@ -79,6 +73,8 @@ vi.mock('../../../components/pages/alert-setting-surface', () => ({
     search,
     evidenceContext,
     onNew,
+    onExport,
+    onImport,
     onToggleEnabled,
     onEdit
   }: {
@@ -86,10 +82,14 @@ vi.mock('../../../components/pages/alert-setting-surface', () => ({
     search: string;
     evidenceContext?: { signal: string; labelsText: string } | null;
     onNew: () => void;
+    onExport: () => void;
+    onImport: () => void;
     onToggleEnabled: (defineId: number, enabled: boolean) => void;
     onEdit: (defineId: number) => Promise<void> | void;
   }) => {
     mockState.lastOnNew = onNew;
+    mockState.lastOnExport = onExport;
+    mockState.lastOnImport = onImport;
     mockState.lastOnToggleEnabled = onToggleEnabled;
     mockState.lastOnEdit = onEdit;
     return (
@@ -145,17 +145,24 @@ vi.mock('../../../lib/api-client', () => ({
   apiMessageGet,
   apiMessageDelete,
   apiMessagePut,
-  apiMessagePost: vi.fn()
+  apiMessagePost: vi.fn(),
+  getCurrentLocale: () => null
 }));
 
+async function renderAlertSettingPage(initialRouteState?: AlertSettingRouteState) {
+  const { default: AlertSettingPage } = await import('./alert-setting-page');
+  return renderToStaticMarkup(<AlertSettingPage initialRouteState={initialRouteState} />);
+}
+
 describe('alert setting page', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams();
     mockState.lastLoad = null;
     mockState.lastOnNew = null;
     mockState.lastOnSubmit = null;
     mockState.lastOnToggleEnabled = null;
     mockState.lastOnEdit = null;
+    mockState.lastOnExport = null;
+    mockState.lastOnImport = null;
     mockState.push.mockReset();
     apiGet.mockReset().mockResolvedValue(mockState.renderData.datasourceStatus);
     apiMessageGet.mockReset().mockResolvedValue(mockState.renderData.list);
@@ -164,23 +171,51 @@ describe('alert setting page', () => {
   });
 
   it('loads the alert-define console through the shared route and surface contracts', async () => {
-    const { default: AlertSettingPage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertSettingPage />);
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+    const html = await renderAlertSettingPage();
 
     expect(html).toContain('data-alert-setting-surface="true"');
     expect(html).toContain('data-total="2"');
     expect(html).toContain('data-search=""');
+    expect(html).toContain('data-loading-copy="Loading alert settings"');
 
     await mockState.lastLoad?.();
 
+    expect(apiMessageGet).toHaveBeenCalledWith('/apps/defines?lang=en_US');
     expect(apiMessageGet).toHaveBeenCalledWith('/alert/defines?pageIndex=0&pageSize=8&sort=id&order=desc');
     expect(apiGet).toHaveBeenCalledWith('/alert/define/datasource/status');
+    expect(source).toContain('loadAlertSettingDataFromFacade');
+    expect(source).toContain('buildAlertSettingAppEntries');
+    expect(source).toContain('api.alertSettings.appDefines');
+    expect(source).toContain('api.alertSettings.list');
+    expect(source).toContain('api.alertSettings.datasourceStatus');
+    expect(source).not.toContain('loadAlertSettingData(apiGet, apiMessageGet');
+  }, 30_000);
+
+  it('maps alert setting search through Angular app-define translations before list reads', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain('const appMap = await api.alertSettings.appDefines(getCurrentLocale()).catch(() => null)');
+    expect(source).toContain('const appEntries = buildAlertSettingAppEntries(appMap)');
+    expect(source).toContain('pageSize,');
+    expect(source).toContain('appEntries');
+  });
+
+  it('keeps alert setting list pagination on the Angular server-side page index and page size contract', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain('const [pageIndex, setPageIndex] = useState(0)');
+    expect(source).toContain('const [pageSize, setPageSize] = useState(8)');
+    expect(source).toContain('buildDefineListUrl(query, pageIndex, pageSize)');
+    expect(source).toContain('setPageIndex(0);');
+    expect(source).toContain('onPageIndexChange={nextPageIndex =>');
+    expect(source).toContain('onPageSizeChange={nextPageSize =>');
+    expect(source).toContain('setPageSize(nextPageSize)');
   });
 
   it('opens the local threshold create flow instead of routing to monitor define', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/page.tsx'), 'utf8');
-    const { default: AlertSettingPage } = await import('./page');
-    renderToStaticMarkup(<AlertSettingPage />);
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+    await renderAlertSettingPage();
 
     expect(mockState.lastOnNew).toBeTypeOf('function');
 
@@ -189,16 +224,20 @@ describe('alert setting page', () => {
     expect(mockState.push).not.toHaveBeenCalled();
     expect(source).not.toContain("router.push('/setting/define')");
     expect(source).toContain('AlertSettingCreateDialog');
-    expect(source).toContain('apiMessagePost');
+    expect(source).toContain('createAlertDefineFromFacade');
+    expect(source).toContain('updateAlertDefineFromFacade');
+    expect(source).toContain('api.alertSettings.create');
+    expect(source).toContain('api.alertSettings.update');
   });
 
   it('uses the shared cold delete confirmation instead of leaving threshold delete actions as no-ops', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
 
     expect(source).toContain('ColdConfirmDialog');
-    expect(source).toContain('apiMessageDelete');
-    expect(source).toContain('deleteAlertDefine');
-    expect(source).toContain('deleteAlertDefines');
+    expect(source).toContain('deleteAlertDefineFromFacade');
+    expect(source).toContain('deleteAlertDefinesFromFacade');
+    expect(source).toContain('api.alertSettings.delete');
+    expect(source).not.toContain('apiMessageDelete');
     expect(source).toContain('data-alert-delete-confirm');
     expect(source).not.toContain('onDeleteSelected={() => {}}');
     expect(source).not.toContain('onDelete={() => {}}');
@@ -208,10 +247,108 @@ describe('alert setting page', () => {
     expect(source).not.toContain('alert(');
   });
 
+  it('maps threshold delete failures to the Angular notify title plus backend detail', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain("title: t('common.notify.delete-fail')");
+    expect(source).toContain('description: error instanceof Error ? error.message : undefined');
+    expect(source).toContain("contract: 'delete'");
+    expect(source).not.toContain("t('common.delete-failed')");
+  });
+
+  it('maps threshold save failures to the Angular create/edit notify title plus backend detail', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain("title: t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail')");
+    expect(source).toContain('description: error instanceof Error ? error.message : undefined');
+    expect(source).toContain("contract: isEdit ? 'edit' : 'create'");
+    expect(source).toContain('saveFeedback={saveFeedback}');
+    expect(source).not.toContain("t('common.save-failed')");
+  });
+
+  it('maps threshold enable toggle failures to the Angular edit notify title plus backend detail', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain('updateAlertDefineEnabledFromFacade');
+    expect(source).toContain("title: t('common.notify.edit-fail')");
+    expect(source).toContain('description: error instanceof Error ? error.message : undefined');
+    expect(source).toContain("contract: 'enable'");
+    expect(source).not.toContain("t('common.enable-failed')");
+  });
+
+  it('keeps threshold batch delete clickable and warns when no rows are selected', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain("title: t('common.notify.no-select-delete')");
+    expect(source).toContain("contract: 'no-select-delete'");
+    expect(source).not.toContain('if (checkedIds.length === 0) return;');
+  });
+
+  it('keeps threshold export warning on the Angular common no-select key', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain("title: t('common.notify.no-select-export')");
+    expect(source).toContain("contract: 'no-select-export'");
+    expect(source).not.toContain("t('alert.setting.notify.no-select-export')");
+  });
+
+  it('wires Angular-compatible threshold import and export actions instead of placeholders', async () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+    await renderAlertSettingPage();
+
+    expect(mockState.lastOnExport).toBeTypeOf('function');
+    expect(mockState.lastOnImport).toBeTypeOf('function');
+    expect(source).toContain('buildAlertDefineExportUrl');
+    expect(source).toContain('buildAlertDefineImportUrl');
+    expect(source).toContain('HzExportTypeDialog');
+    expect(source).toContain('HzFileInput');
+    expect(source).toContain('data-alert-setting-import-file-input="true"');
+    expect(source).toContain('data-alert-setting-import-input-owner="hertzbeat-ui-file-input"');
+    expect(source).toContain('data-alert-setting-export-type-dialog-owner="hertzbeat-ui-export-type-dialog"');
+    expect(source).toContain('data-alert-setting-export-type-option-owner');
+    expect(source).toContain('fetch(`/api${buildAlertDefineExportUrl(checkedIds, type)}`');
+    expect(source).toContain('fetch(`/api${buildAlertDefineImportUrl()}`');
+    expect(source).not.toContain('onExport={() => {}}');
+    expect(source).not.toContain('onImport={() => {}}');
+  });
+
+  it('maps threshold import results to Angular success and failure notifications', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain("return t('common.notify.import-success')");
+    expect(source).toContain("t('common.notify.import-fail')");
+    expect(source).toContain("success: 'import-success'");
+    expect(source).toContain("failure: 'import-fail'");
+    expect(source).not.toContain("return t('common.notify.import-success-detail'");
+  });
+
+  it('keeps threshold export success silent while mapping failures to the Angular notify title', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain("title: t('common.notify.export-fail')");
+    expect(source).toContain("contract: 'export-fail'");
+    expect(source).toContain('data-alert-setting-export-success-contract="angular-download-closes-dialog-no-toast"');
+    expect(source).toContain('setActionFeedback(null);');
+    expect(source).toContain("throw new Error('')");
+    expect(source).not.toContain("return t('common.notify.export-success')");
+  });
+
+  it('keeps threshold export loading scoped to the selected Angular export type', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain('const [pendingExportType, setPendingExportType]');
+    expect(source).toContain('setPendingExportType(type)');
+    expect(source).toContain("jsonBusy={pendingExportType === 'JSON'}");
+    expect(source).toContain("excelBusy={pendingExportType === 'EXCEL'}");
+    expect(source).toContain('data-alert-setting-export-loading-contract="angular-selected-type-only"');
+    expect(source).toContain("setPendingExportType(null)");
+    expect(source).not.toContain("jsonBusy={pendingActionId === 'export'}");
+    expect(source).not.toContain("excelBusy={pendingActionId === 'export'}");
+  });
+
   it('persists threshold enable toggles instead of leaving the enable checkbox as a no-op', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/page.tsx'), 'utf8');
-    const { default: AlertSettingPage } = await import('./page');
-    renderToStaticMarkup(<AlertSettingPage />);
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+    await renderAlertSettingPage();
 
     expect(mockState.lastOnToggleEnabled).toBeTypeOf('function');
 
@@ -226,13 +363,14 @@ describe('alert setting page', () => {
       })
     );
     expect(source).toContain('updateAlertDefineEnabled');
+    expect(source).toContain('api.alertSettings.update');
+    expect(source).not.toContain('apiMessagePut');
     expect(source).not.toContain('onToggleEnabled={() => {}}');
   });
 
   it('loads threshold detail into the shared authoring dialog instead of leaving edit as a no-op', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/page.tsx'), 'utf8');
-    const { default: AlertSettingPage } = await import('./page');
-    renderToStaticMarkup(<AlertSettingPage />);
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+    await renderAlertSettingPage();
 
     apiMessageGet.mockResolvedValueOnce({
       id: 7,
@@ -253,38 +391,58 @@ describe('alert setting page', () => {
     await mockState.lastOnEdit?.(7);
 
     expect(apiMessageGet).toHaveBeenCalledWith('/alert/define/7');
-    expect(source).toContain('loadAlertDefineDetail');
+    expect(source).toContain('loadAlertDefineDetailFromFacade');
+    expect(source).toContain('api.alertSettings.detail');
+    expect(source).not.toContain('apiMessageGet as any');
     expect(source).toContain('buildAlertSettingDraftFromDefine');
     expect(source).toContain("setCreateMode('authoring')");
     expect(source).not.toContain('onEdit={() => {}}');
   });
 
   it('keeps three-signal route context on the alert-rule workspace entry', async () => {
-    mockState.searchParams = new URLSearchParams({
+    const initialRouteState: AlertSettingRouteState = {
       signal: 'logs',
-      entityId: '7',
-      entityName: 'Checkout API',
-      serviceName: 'checkout',
-      environment: 'prod',
-      timeRange: 'last-1h',
-      source: 'otlp',
-      traceId: 'trace-123',
-      spanId: 'span-456',
-      returnTo: '/log/manage?traceId=trace-123&returnLabel=Logs'
-    });
+      signalContext: {
+        signal: 'logs',
+        entityId: '7',
+        entityName: 'Checkout API',
+        serviceName: 'checkout',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'otlp',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        returnTo: '/log/manage?traceId=trace-123'
+      }
+    };
 
-    const { default: AlertSettingPage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertSettingPage />);
-    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/page.tsx'), 'utf8');
+    const html = await renderAlertSettingPage(initialRouteState);
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
 
     expect(html).toContain('data-evidence-signal="logs"');
     expect(html).toContain('hertzbeat.signal:logs');
     expect(html).toContain('hertzbeat.entity.id:7');
     expect(html).toContain('service.name:checkout');
     expect(html).toContain('trace_id:trace-123');
-    expect(source).toContain('readSignalRouteContext');
+    expect(source).not.toContain('useSearchParams');
+    expect(source).not.toContain('readSignalRouteContext');
     expect(source).toContain('buildAlertSettingEvidenceContext');
     expect(source).toContain('evidenceContext={evidenceContext}');
     expect(source).toContain('evidenceReturnHref={evidenceContext?.returnHref}');
+    expect(source).toContain('const alertSettingRouteState = initialRouteState ?? EMPTY_ALERT_SETTING_ROUTE_STATE');
+  });
+
+  it('keeps alert setting remounts on a short settled cache window with refresh-key invalidation', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain('ALERT_SETTING_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('const [refreshKey, setRefreshKey] = useState(0)');
+    expect(source).toContain("['alert-setting', alertSettingListUrl, refreshKey].join('|')");
+    expect(source).toContain('[alertSettingListUrl, refreshKey]');
+    expect(source).toContain('void refreshKey');
+    expect(source).toContain('[query, pageIndex, pageSize, refreshKey]');
+    expect(source.match(/setRefreshKey\(value => value \+ 1\)/g)?.length).toBeGreaterThanOrEqual(4);
+    expect(source).toContain('cacheKey={alertSettingCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={ALERT_SETTING_SETTLED_CACHE_TTL_MS}');
   });
 });
diff --git a/web-next/app/alert/setting/page.tsx b/web-next/app/alert/setting/page.tsx
index 2a7ba746bc..33802a5980 100644
--- a/web-next/app/alert/setting/page.tsx
+++ b/web-next/app/alert/setting/page.tsx
@@ -1,200 +1,14 @@
-'use client';
-
-import React, { useCallback, useMemo, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ClientWorkbench } from '../../../components/workbench/client-workbench';
-import { AlertSettingSurface } from '../../../components/pages/alert-setting-surface';
-import {
-  AlertSettingCreateDialog,
-  buildAlertSettingDraftFromDefine,
-  createDefaultAlertSettingDraft,
-  type AlertSettingCreateKind,
-  type AlertSettingCreateMode,
-  type AlertSettingCreatePayload
-} from '../../../components/pages/alert-setting-create-dialog';
-import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
-import { useI18n } from '../../../components/providers/i18n-provider';
-import { apiGet, apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '../../../lib/api-client';
-import {
-  deleteAlertDefine,
-  deleteAlertDefines,
-  loadAlertDefineDetail,
-  loadAlertSettingData,
-  updateAlertDefineEnabled
-} from '../../../lib/alert-setting/controller';
-import { buildAlertSettingEvidenceContext } from '../../../lib/alert-setting/view-model';
-import { formatTime } from '../../../lib/format';
-import { readSignalRouteContext } from '../../../lib/signal-route-context';
-
-type SettingDeleteRequest = {
-  kind: 'single' | 'batch';
-  ids: number[];
-};
-
-export default function AlertSettingPage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const signal = searchParams.get('signal');
-  const signalContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
-  const evidenceContext = useMemo(
-    () => buildAlertSettingEvidenceContext(signal, signalContext, t),
-    [signal, signalContext, t]
-  );
-  const [search, setSearch] = useState('');
-  const [query, setQuery] = useState('');
-  const [refreshKey, setRefreshKey] = useState(0);
-  const [checkedIds, setCheckedIds] = useState<number[]>([]);
-  const [createMode, setCreateMode] = useState<AlertSettingCreateMode | 'closed'>('closed');
-  const [createDraft, setCreateDraft] = useState(() => createDefaultAlertSettingDraft('realtime'));
-  const [creating, setCreating] = useState(false);
-  const [deleteRequest, setDeleteRequest] = useState<SettingDeleteRequest | null>(null);
-  const [deletePending, setDeletePending] = useState(false);
-
-  const load = useCallback(async () => {
-    void refreshKey;
-    return loadAlertSettingData(apiGet, apiMessageGet, query);
-  }, [query, refreshKey]);
-
-  function openTypeSelection() {
-    setCreateDraft(createDefaultAlertSettingDraft('realtime', { labelsText: evidenceContext?.labelsText || '' }));
-    setCreateMode('type');
-  }
-
-  function closeCreateFlow() {
-    setCreateMode('closed');
-    setCreating(false);
-  }
-
-  function selectCreateType(kind: AlertSettingCreateKind) {
-    setCreateDraft(current => createDefaultAlertSettingDraft(kind, current));
-    setCreateMode('authoring');
-  }
-
-  async function submitCreate(payload: AlertSettingCreatePayload) {
-    setCreating(true);
-    try {
-      if (typeof payload.id === 'number') {
-        await apiMessagePut('/alert/define', payload);
-      } else {
-        await apiMessagePost('/alert/define', payload);
-      }
-      closeCreateFlow();
-      setCheckedIds([]);
-      setRefreshKey(value => value + 1);
-    } finally {
-      setCreating(false);
-    }
-  }
-
-  function requestSingleDelete(defineId: number) {
-    setDeleteRequest({ kind: 'single', ids: [defineId] });
-  }
-
-  function requestBatchDelete() {
-    if (checkedIds.length === 0) return;
-    setDeleteRequest({ kind: 'batch', ids: checkedIds });
-  }
-
-  async function confirmDelete() {
-    const request = deleteRequest;
-    if (!request || request.ids.length === 0) return;
-    setDeletePending(true);
-    try {
-      if (request.kind === 'batch') {
-        await deleteAlertDefines(apiMessageDelete as any, request.ids);
-      } else {
-        await deleteAlertDefine(apiMessageDelete as any, request.ids[0]);
-      }
-      setCheckedIds([]);
-      setDeleteRequest(null);
-      setRefreshKey(value => value + 1);
-    } finally {
-      setDeletePending(false);
-    }
-  }
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('alert.setting.loading')}>
-      {data => {
-        async function handleToggleEnabled(defineId: number, enabled: boolean) {
-          const target = data.list.content.find(item => item.id === defineId);
-          if (!target) return;
-          await updateAlertDefineEnabled(apiMessagePut as any, target, enabled);
-          setRefreshKey(value => value + 1);
-        }
-
-        async function handleEdit(defineId: number) {
-          const define = await loadAlertDefineDetail(apiMessageGet as any, defineId);
-          setCreateDraft(buildAlertSettingDraftFromDefine(define));
-          setCreateMode('authoring');
-        }
-
-        return (
-          <>
-            <AlertSettingSurface
-              t={t}
-              data={data}
-              search={search}
-              checkedIds={checkedIds}
-              evidenceContext={evidenceContext}
-              formatTime={formatTime}
-              onSearchChange={setSearch}
-              onApplyFilter={() => {
-                setQuery(search);
-                setCheckedIds([]);
-              }}
-              onClearFilter={() => {
-                setSearch('');
-                setQuery('');
-                setCheckedIds([]);
-              }}
-              onRefresh={() => {
-                setRefreshKey(value => value + 1);
-                setCheckedIds([]);
-              }}
-              onNew={openTypeSelection}
-              onDeleteSelected={requestBatchDelete}
-              onExport={() => {}}
-              onImport={() => {}}
-              onToggleEnabled={(defineId, enabled) => void handleToggleEnabled(defineId, enabled)}
-              onEdit={defineId => void handleEdit(defineId)}
-              onDelete={requestSingleDelete}
-              onCheckedIdsChange={setCheckedIds}
-            />
-            <AlertSettingCreateDialog
-              t={t}
-              open={createMode !== 'closed'}
-              mode={createMode === 'closed' ? 'type' : createMode}
-              intent={typeof createDraft.id === 'number' ? 'edit' : 'create'}
-              datasourceStatus={data.datasourceStatus}
-              draft={createDraft}
-              submitting={creating}
-              evidenceReturnHref={evidenceContext?.returnHref}
-              onClose={closeCreateFlow}
-              onSelectType={selectCreateType}
-              onDraftChange={setCreateDraft}
-              onBackToType={() => setCreateMode('type')}
-              onSubmit={submitCreate}
-            />
-            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
-              <ColdConfirmDialog
-                open={Boolean(deleteRequest)}
-                title={deleteRequest?.kind === 'batch' ? t('common.confirm.delete-batch') : t('common.confirm.delete')}
-                copy={
-                  deleteRequest?.kind === 'batch'
-                    ? t('alert.setting.delete.confirm.batch', { count: deleteRequest.ids.length })
-                    : t('alert.setting.delete.confirm.single')
-                }
-                confirmLabel={t('common.button.ok')}
-                cancelLabel={t('common.button.cancel')}
-                pending={deletePending}
-                onCancel={() => setDeleteRequest(null)}
-                onConfirm={() => void confirmDelete()}
-              />
-            </div>
-          </>
-        );
-      }}
-    </ClientWorkbench>
-  );
+import React from 'react';
+
+import AlertSettingPage from './alert-setting-page';
+import { readAlertSettingRouteState, type AlertSettingSearchParams } from '../../../lib/alert-setting/query-state';
+
+export default async function AlertSettingRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<AlertSettingSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readAlertSettingRouteState(resolvedSearchParams);
+  return <AlertSettingPage initialRouteState={routeState} />;
 }
diff --git a/web-next/app/alert/silence/alert-silence-page.tsx b/web-next/app/alert/silence/alert-silence-page.tsx
new file mode 100644
index 0000000000..86230b8149
--- /dev/null
+++ b/web-next/app/alert/silence/alert-silence-page.tsx
@@ -0,0 +1,443 @@
+'use client';
+
+import React from 'react';
+import { useCallback, useMemo, useState } from 'react';
+import { ClientWorkbench } from '../../../components/workbench/client-workbench';
+import { useI18n } from '../../../components/providers/i18n-provider';
+import { AlertSilenceSurface } from '../../../components/pages/alert-silence-surface';
+import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
+import { api } from '../../../lib/alert-api-facade';
+import {
+  buildAlertSilenceEntityPrefillFromFacade,
+  buildAlertSilenceFormDraft,
+  createAlertSilenceFromFacade,
+  deleteAlertSilenceFromFacade,
+  deleteAlertSilencesFromFacade,
+  loadAlertSilenceDataFromFacade,
+  loadAlertSilenceDetailFromFacade,
+  loadMatchedAlertSilencesFromFacade,
+  updateAlertSilenceEnabledFromFacade,
+  updateAlertSilenceFromFacade,
+  type AlertSilenceFormDraft
+} from '../../../lib/alert-silence/controller';
+import { ALERT_SILENCE_PAGE_SIZE_OPTIONS, buildAlertSilenceUrl, type AlertSilenceRouteState } from '../../../lib/alert-silence/query-state';
+import { buildAlertSilenceEvidenceContext, validateAlertSilenceForm } from '../../../lib/alert-silence/view-model';
+import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptionsFromFacade } from '../../../lib/alert-label-options';
+import { formatTime } from '../../../lib/format';
+import type { AlertSilence, PageResult } from '../../../lib/types';
+import type { AlertSilenceManagementContext } from '../../../lib/alert-silence/query-state';
+
+type SilenceDeleteRequest = {
+  kind: 'single' | 'batch';
+  ids: number[];
+};
+
+const ALERT_SILENCE_SETTLED_CACHE_TTL_MS = 10_000;
+const ALERT_SILENCE_LABEL_OPTIONS_TIMEOUT_MS = 2_500;
+const EMPTY_ALERT_SILENCE_ROUTE_STATE: AlertSilenceRouteState = {
+    returnContext: {
+      search: '',
+      status: '',
+      severity: '',
+      entityId: '',
+      entityName: '',
+      returnTo: ''
+    },
+    signal: null,
+    signalContext: {},
+    managementContext: {
+      entityId: '',
+      entityName: '',
+      returnTo: '',
+      returnLabel: '',
+      matchMode: '',
+      matchingRuleType: '',
+      matchingRuleIds: [],
+      matchedViewEnabled: false
+    }
+  };
+
+function filterMatchedSilencesBySearch(silences: AlertSilence[], search: string): AlertSilence[] {
+  const keyword = search.trim().toLowerCase();
+  if (!keyword) return silences;
+  return silences.filter(silence => {
+    if (silence.name?.toLowerCase().includes(keyword)) {
+      return true;
+    }
+    return Object.entries(silence.labels || {}).some(([key, value]) => `${key}:${value}`.toLowerCase().includes(keyword));
+  });
+}
+
+function paginateMatchedSilences(silences: AlertSilence[], pageIndex: number, pageSize: number): PageResult<AlertSilence> {
+  const normalizedPageSize = Math.max(1, pageSize);
+  const lastPageIndex = Math.max(0, Math.ceil(silences.length / normalizedPageSize) - 1);
+  const normalizedPageIndex = Math.min(Math.max(0, pageIndex), lastPageIndex);
+  const start = normalizedPageIndex * normalizedPageSize;
+  return {
+    content: silences.slice(start, start + normalizedPageSize),
+    totalElements: silences.length,
+    pageIndex: normalizedPageIndex,
+    pageSize: normalizedPageSize
+  };
+}
+
+function shouldUseMatchedSilenceView(context: AlertSilenceManagementContext, matchedViewEnabled: boolean) {
+  return matchedViewEnabled && context.matchMode === 'entity-noise-controls';
+}
+
+function withTimeoutFallback<T>(promise: Promise<T>, fallback: T, timeoutMs: number): Promise<T> {
+  return new Promise(resolve => {
+    const timer = setTimeout(() => resolve(fallback), timeoutMs);
+    promise.then(
+      value => {
+        clearTimeout(timer);
+        resolve(value);
+      },
+      () => {
+        clearTimeout(timer);
+        resolve(fallback);
+      }
+    );
+  });
+}
+
+export default function AlertSilencePage({ initialRouteState }: { initialRouteState?: AlertSilenceRouteState } = {}) {
+  const { t } = useI18n();
+  const alertSilenceRouteState = initialRouteState ?? EMPTY_ALERT_SILENCE_ROUTE_STATE;
+  const { returnContext, signal, signalContext, managementContext } = alertSilenceRouteState;
+  const silenceEvidenceContext = useMemo(
+    () => buildAlertSilenceEvidenceContext(signal, signalContext, t),
+    [signal, signalContext, t]
+  );
+  const [search, setSearch] = useState('');
+  const [query, setQuery] = useState('');
+  const [pageIndex, setPageIndex] = useState(0);
+  const [pageSize, setPageSize] = useState<number>(ALERT_SILENCE_PAGE_SIZE_OPTIONS[0]);
+  const [selectedId, setSelectedId] = useState<number | null>(null);
+  const [editorOpen, setEditorOpen] = useState(false);
+  const [editorLoading, setEditorLoading] = useState(false);
+  const [editorSaving, setEditorSaving] = useState(false);
+  const [editorMessage, setEditorMessage] = useState<string | null>(null);
+  const [editorError, setEditorError] = useState<string | null>(null);
+  const [editorErrorDetail, setEditorErrorDetail] = useState<string | null>(null);
+  const [editorErrorContract, setEditorErrorContract] = useState<'save' | 'enable' | 'delete' | null>(null);
+  const [draft, setDraft] = useState<AlertSilenceFormDraft>(() => buildAlertSilenceFormDraft(null, silenceEvidenceContext?.draftPatch));
+  const [refreshTick, setRefreshTick] = useState(0);
+  const [checkedIds, setCheckedIds] = useState<number[]>([]);
+  const [deleteRequest, setDeleteRequest] = useState<SilenceDeleteRequest | null>(null);
+  const [deletePending, setDeletePending] = useState(false);
+  const [matchedViewEnabled, setMatchedViewEnabled] = useState(managementContext.matchedViewEnabled);
+  const [createdOutsideMatchedViewNotice, setCreatedOutsideMatchedViewNotice] = useState(false);
+  const [entityPrefillSource, setEntityPrefillSource] = useState<'alerts-common-labels' | 'none'>('none');
+  const [entityPrefillWarning, setEntityPrefillWarning] = useState<string | null>(null);
+  const alertSilenceListUrl = useMemo(() => buildAlertSilenceUrl({ search: query, pageIndex, pageSize }), [pageIndex, pageSize, query]);
+  const matchedRuleIdsKey = managementContext.matchingRuleIds.join(',');
+  const useMatchedView = shouldUseMatchedSilenceView(managementContext, matchedViewEnabled);
+  const alertSilenceCacheKey = useMemo(
+    () => ['alert-silence', useMatchedView ? `matched:${matchedRuleIdsKey}` : alertSilenceListUrl, refreshTick].join('|'),
+    [alertSilenceListUrl, matchedRuleIdsKey, refreshTick, useMatchedView]
+  );
+
+  const load = useCallback(async () => {
+    const labelOptionsPromise = withTimeoutFallback(
+      loadAlertLabelOptionsFromFacade(api.alertLabels.list),
+      DEFAULT_ALERT_LABEL_OPTIONS,
+      ALERT_SILENCE_LABEL_OPTIONS_TIMEOUT_MS
+    );
+    if (useMatchedView) {
+      const [matchedResult, labelOptions] = await Promise.all([
+        loadMatchedAlertSilencesFromFacade(api.alertSilences.detail, managementContext.matchingRuleIds),
+        labelOptionsPromise
+      ]);
+      const filtered = filterMatchedSilencesBySearch(matchedResult.matched, query);
+      return {
+        list: paginateMatchedSilences(filtered, pageIndex, pageSize),
+        labelOptions,
+        refreshTick,
+        missingMatchedRuleCount: matchedResult.missingMatchedRuleCount
+      };
+    }
+
+    const data = await loadAlertSilenceDataFromFacade(
+      {
+        list: api.alertSilences.list,
+        labelOptions: () => labelOptionsPromise
+      },
+      { search: query, pageIndex, pageSize }
+    );
+    return { ...data, refreshTick, missingMatchedRuleCount: 0 };
+  }, [managementContext.matchingRuleIds, pageIndex, pageSize, query, refreshTick, useMatchedView]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('alert.silence.loading')}
+      cacheKey={alertSilenceCacheKey}
+      cacheSettledTtlMs={ALERT_SILENCE_SETTLED_CACHE_TTL_MS}
+    >
+      {data => {
+        const labelOptions = data.labelOptions ?? DEFAULT_ALERT_LABEL_OPTIONS;
+        const selected = data.list.content.find(item => item.id === selectedId) ?? data.list.content[0] ?? null;
+
+        async function handleNew() {
+          const displayName = managementContext.entityName || managementContext.returnLabel || managementContext.entityId || 'entity';
+          const entityContextDraft = managementContext.entityId || managementContext.entityName || managementContext.returnTo
+            ? { name: `${displayName} silence` }
+            : {};
+          const baseDraftPatch = silenceEvidenceContext?.draftPatch ?? entityContextDraft;
+          setEntityPrefillSource('none');
+          setEntityPrefillWarning(null);
+          if (managementContext.entityId || managementContext.entityName || managementContext.returnTo) {
+            setEditorLoading(true);
+            try {
+              const prefill = await buildAlertSilenceEntityPrefillFromFacade(
+                entityId => api.entities.alerts(entityId, { pageIndex: 0, pageSize: 20, status: 'firing' }),
+                managementContext.entityId,
+                t('entity.noise-controls.authoring.silence.prefill-warning'),
+                t('entity.noise-controls.authoring.prefill-warning.no-entity-id')
+              );
+              setEntityPrefillSource(prefill.source);
+              setEntityPrefillWarning(prefill.warning);
+              setDraft(buildAlertSilenceFormDraft(null, { ...baseDraftPatch, ...prefill.draftPatch }));
+            } finally {
+              setEditorLoading(false);
+            }
+          } else {
+            setDraft(buildAlertSilenceFormDraft(null, baseDraftPatch));
+          }
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setEditorMessage(null);
+          setCreatedOutsideMatchedViewNotice(false);
+          setEditorOpen(true);
+        }
+
+        async function handleEdit(silenceId?: number) {
+          const targetId = silenceId ?? selected?.id;
+          if (!targetId) return;
+          setEditorLoading(true);
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setEditorMessage(null);
+          try {
+            const detail = await loadAlertSilenceDetailFromFacade(api.alertSilences.detail, targetId);
+            setDraft(buildAlertSilenceFormDraft(detail));
+            setEditorOpen(true);
+          } catch (error) {
+            setEditorError(error instanceof Error ? error.message : t('common.notify.edit-fail'));
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+          } finally {
+            setEditorLoading(false);
+          }
+        }
+
+        async function handleSave() {
+          const validationError = validateAlertSilenceForm(draft, t);
+          if (validationError) {
+            setEditorError(validationError);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            return;
+          }
+          setEditorSaving(true);
+          setEditorError(null);
+          setEditorErrorDetail(null);
+          setEditorErrorContract(null);
+          setEditorMessage(null);
+          const isEdit = Boolean(draft.id);
+          try {
+            if (isEdit) {
+              await updateAlertSilenceFromFacade(api.alertSilences.update, draft);
+            } else {
+              await createAlertSilenceFromFacade(api.alertSilences.create, draft);
+            }
+            setEditorMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
+            setEditorOpen(false);
+            setCreatedOutsideMatchedViewNotice(!isEdit && useMatchedView);
+            setRefreshTick(value => value + 1);
+          } catch (error) {
+            setEditorError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('save');
+          } finally {
+            setEditorSaving(false);
+          }
+        }
+
+        async function handleToggleEnabled(silence?: AlertSilence) {
+          const target = silence ?? selected;
+          if (!target) return;
+          try {
+            await updateAlertSilenceEnabledFromFacade(api.alertSilences.update, target, !(target.enable ?? true));
+            setEditorMessage(t('common.notify.edit-success'));
+            setEditorError(null);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            setRefreshTick(value => value + 1);
+          } catch (error) {
+            setEditorError(t('common.notify.edit-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('enable');
+          }
+        }
+
+        async function handleDelete(silenceId?: number) {
+          const targetId = silenceId ?? selected?.id;
+          if (!targetId) return;
+          setDeleteRequest({ kind: 'single', ids: [targetId] });
+        }
+
+        async function handleConfirmedDelete() {
+          const request = deleteRequest;
+          if (!request || request.ids.length === 0) return;
+          setDeletePending(true);
+          try {
+            if (request.kind === 'batch') {
+              await deleteAlertSilencesFromFacade(api.alertSilences.delete, request.ids);
+              setCheckedIds([]);
+            } else {
+              await deleteAlertSilenceFromFacade(api.alertSilences.delete, request.ids[0]);
+            }
+            const nextTotal = Math.max((data.list.totalElements || 0) - request.ids.length, 0);
+            const nextLastPageIndex = Math.max(0, Math.ceil(nextTotal / pageSize) - 1);
+            setPageIndex(value => Math.min(value, nextLastPageIndex));
+            setSelectedId(null);
+            setEditorOpen(false);
+            setEditorMessage(t('common.notify.delete-success'));
+            setEditorError(null);
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            setDeleteRequest(null);
+            setRefreshTick(value => value + 1);
+          } catch (error) {
+            setEditorError(t('common.notify.delete-fail'));
+            setEditorErrorDetail(error instanceof Error ? error.message : null);
+            setEditorErrorContract('delete');
+          } finally {
+            setDeletePending(false);
+          }
+        }
+
+        function handleRefresh() {
+          setRefreshTick(value => value + 1);
+        }
+
+        async function handleDeleteSelected() {
+          if (checkedIds.length === 0) {
+            setEditorError(t('common.notify.no-select-delete'));
+            setEditorErrorDetail(null);
+            setEditorErrorContract(null);
+            setEditorMessage(null);
+            return;
+          }
+          setDeleteRequest({ kind: 'batch', ids: checkedIds });
+        }
+
+        function handleApplyFilter() {
+          setQuery(search);
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handleClearFilter() {
+          setSearch('');
+          setQuery('');
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handlePageIndexChange(nextPageIndex: number) {
+          setPageIndex(nextPageIndex);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        function handlePageSizeChange(nextPageSize: number) {
+          setPageSize(nextPageSize);
+          setPageIndex(0);
+          setSelectedId(null);
+          setCheckedIds([]);
+        }
+
+        return (
+          <>
+            <AlertSilenceSurface
+              t={t}
+              data={data}
+              search={search}
+              selectedId={selectedId}
+              checkedIds={checkedIds}
+              editorOpen={editorOpen}
+              editorLoading={editorLoading}
+              editorSaving={editorSaving}
+              editorMessage={editorMessage}
+              editorError={editorError}
+              editorErrorDetail={editorErrorDetail}
+              editorErrorContract={editorErrorContract}
+              returnContext={returnContext}
+              managementContext={managementContext}
+              matchedViewEnabled={useMatchedView}
+              missingMatchedRuleCount={data.missingMatchedRuleCount ?? 0}
+              createdOutsideMatchedViewNotice={createdOutsideMatchedViewNotice}
+              entityPrefillSource={entityPrefillSource}
+              entityPrefillWarning={entityPrefillWarning}
+              evidenceContext={silenceEvidenceContext}
+              draft={draft}
+              labelOptions={labelOptions}
+              formatTime={formatTime}
+              onSearchChange={setSearch}
+              onApplyFilter={handleApplyFilter}
+              onClearFilter={handleClearFilter}
+              onRefresh={handleRefresh}
+              onSelect={setSelectedId}
+              onCheckedIdsChange={setCheckedIds}
+              pageSizeOptions={[...ALERT_SILENCE_PAGE_SIZE_OPTIONS]}
+              onPageIndexChange={handlePageIndexChange}
+              onPageSizeChange={handlePageSizeChange}
+              onViewAllRules={() => {
+                setMatchedViewEnabled(false);
+                setCreatedOutsideMatchedViewNotice(false);
+                setPageIndex(0);
+                setCheckedIds([]);
+              }}
+              onViewMatchedRules={() => {
+                setMatchedViewEnabled(true);
+                setPageIndex(0);
+                setCheckedIds([]);
+              }}
+              onNew={() => void handleNew()}
+              onEdit={silenceId => void handleEdit(silenceId)}
+              onSave={() => void handleSave()}
+              onToggleEnabled={silence => void handleToggleEnabled(silence)}
+              onDelete={silenceId => void handleDelete(silenceId)}
+              onDeleteSelected={() => void handleDeleteSelected()}
+              onCloseEditor={() => setEditorOpen(false)}
+              onDraftChange={setDraft}
+            />
+            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
+              <ColdConfirmDialog
+                open={Boolean(deleteRequest)}
+                title={deleteRequest?.kind === 'batch' ? t('common.confirm.delete-batch') : t('common.confirm.delete')}
+                copy={
+                  deleteRequest?.kind === 'batch'
+                    ? t('alert.silence.delete.confirm.batch', { count: deleteRequest.ids.length })
+                    : t('alert.silence.delete.confirm.single')
+                }
+                confirmLabel={t('common.button.ok')}
+                cancelLabel={t('common.button.cancel')}
+                pending={deletePending}
+                onCancel={() => setDeleteRequest(null)}
+                onConfirm={() => void handleConfirmedDelete()}
+              />
+            </div>
+          </>
+        );
+      }}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/alert/silence/page.test.tsx b/web-next/app/alert/silence/page.test.tsx
index 987c45049a..739d1ea4fd 100644
--- a/web-next/app/alert/silence/page.test.tsx
+++ b/web-next/app/alert/silence/page.test.tsx
@@ -1,10 +1,12 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
+import type { AlertSilenceRouteState } from '../../../lib/alert-silence/query-state';
 
 const mockState = vi.hoisted(() => ({
-  searchParams: new URLSearchParams(),
   lastLoad: null as null | (() => Promise<unknown>),
   renderData: {
     list: {
@@ -29,13 +31,6 @@ const mockState = vi.hoisted(() => ({
 
 const apiMessageGet = vi.hoisted(() => vi.fn());
 
-vi.mock('next/navigation', () => ({
-  useSearchParams: () =>
-    ({
-      get: (key: string) => mockState.searchParams.get(key)
-    }) as { get(name: string): string | null }
-}));
-
 vi.mock('../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock()
@@ -43,9 +38,21 @@ vi.mock('../../../components/providers/i18n-provider', () => ({
 }));
 
 vi.mock('../../../components/workbench/client-workbench', () => ({
-  ClientWorkbench: ({ children, load }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown> }) => {
+  ClientWorkbench: ({
+    children,
+    load,
+    loadingCopy
+  }: {
+    children: (data: any) => React.ReactNode;
+    load: () => Promise<unknown>;
+    loadingCopy?: string;
+  }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return (
+      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
+        {children(mockState.renderData)}
+      </div>
+    );
   }
 }));
 
@@ -53,18 +60,40 @@ vi.mock('../../../components/pages/alert-silence-surface', () => ({
   AlertSilenceSurface: ({
     data,
     returnContext,
+    managementContext,
+    matchedViewEnabled,
+    missingMatchedRuleCount,
+    createdOutsideMatchedViewNotice,
+    entityPrefillSource,
+    entityPrefillWarning,
     evidenceContext,
-    draft
+    draft,
+    pageSizeOptions
   }: {
     data: { list: { totalElements: number } };
     returnContext?: Record<string, string>;
+    managementContext?: { matchMode?: string; matchingRuleIds?: number[] };
+    matchedViewEnabled?: boolean;
+    missingMatchedRuleCount?: number;
+    createdOutsideMatchedViewNotice?: boolean;
+    entityPrefillSource?: string;
+    entityPrefillWarning?: string | null;
     evidenceContext?: { signal: string; labelsText: string; returnHref?: string } | null;
     draft?: { labelsText?: string };
+    pageSizeOptions?: number[];
   }) => (
     <div
       data-alert-silence-surface="true"
       data-total={data.list.totalElements}
+      data-page-size-options={pageSizeOptions?.join('|')}
       data-return-context={JSON.stringify(returnContext ?? {})}
+      data-alert-silence-match-mode={managementContext?.matchMode ?? ''}
+      data-alert-silence-match-view={matchedViewEnabled ? 'matched' : 'all'}
+      data-alert-silence-created-outside-matched={createdOutsideMatchedViewNotice ? 'true' : 'false'}
+      data-alert-silence-matching-rule-ids={managementContext?.matchingRuleIds?.join(',') ?? ''}
+      data-alert-silence-missing-rule-count={missingMatchedRuleCount ?? 0}
+      data-alert-silence-entity-prefill-source={entityPrefillSource ?? 'none'}
+      data-alert-silence-entity-prefill-warning={entityPrefillWarning ?? ''}
       data-alert-silence-evidence-context={evidenceContext ? 'signal-route' : 'none'}
       data-alert-silence-evidence-signal={evidenceContext?.signal ?? ''}
       data-alert-silence-evidence-return={evidenceContext?.returnHref ?? ''}
@@ -81,44 +110,96 @@ vi.mock('../../../lib/api-client', () => ({
   apiMessagePut: vi.fn()
 }));
 
+const EMPTY_ROUTE_STATE: AlertSilenceRouteState = {
+  returnContext: {
+    search: '',
+    status: '',
+    severity: '',
+    entityId: '',
+    entityName: '',
+    returnTo: ''
+  },
+  signal: null,
+  signalContext: {},
+  managementContext: {
+    entityId: '',
+    entityName: '',
+    returnTo: '',
+    returnLabel: '',
+    matchMode: '',
+    matchingRuleType: '',
+    matchingRuleIds: [],
+    matchedViewEnabled: false
+  }
+};
+
+async function renderAlertSilencePage(initialRouteState: AlertSilenceRouteState = EMPTY_ROUTE_STATE) {
+  const { default: AlertSilencePage } = await import('./alert-silence-page');
+  return renderToStaticMarkup(<AlertSilencePage initialRouteState={initialRouteState} />);
+}
+
 describe('alert silence page', () => {
   beforeEach(() => {
-    mockState.searchParams = new URLSearchParams();
     mockState.lastLoad = null;
     apiMessageGet.mockClear().mockResolvedValue(mockState.renderData.list);
   });
 
   it('loads the silence workbench through the shared query and surface contracts', async () => {
-    const { default: AlertSilencePage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertSilencePage />);
+    const html = await renderAlertSilencePage();
 
     expect(html).toContain('data-alert-silence-surface="true"');
+    expect(html).toContain('data-page-size-options="8|15|25"');
+    expect(html).toContain('data-loading-copy="Loading silence rules"');
 
     await mockState.lastLoad?.();
 
     expect(apiMessageGet).toHaveBeenCalledWith('/alert/silences?pageIndex=0&pageSize=8&sort=id&order=desc');
-  });
+  }, 30_000);
 
   it('passes topology edge return context into the silence surface', async () => {
     const returnTo =
       '/topology?viewMode=resource-dependency&sourceKind=database-middleware-connection&edgeId=svc-checkout--res-orders-db&environment=prod&timeRange=last-1h';
-    mockState.searchParams = new URLSearchParams({
-      source: 'topology',
-      viewMode: 'resource-dependency',
-      sourceKind: 'database-middleware-connection',
-      edgeId: 'svc-checkout--res-orders-db',
-      entityId: 'service:commerce/checkout',
-      entityName: 'checkout-api',
-      serviceName: 'checkout-api',
-      serviceNamespace: 'commerce',
-      environment: 'prod',
-      timeRange: 'last-1h',
-      returnTo: `${returnTo}&returnLabel=HertzBeat%20%E4%BC%81%E4%B8%9A%E8%BF%90%E7%BB%B4%E6%8B%93%E6%89%91`,
-      returnLabel: 'HertzBeat 企业运维拓扑'
-    });
+    const initialRouteState: AlertSilenceRouteState = {
+      returnContext: {
+        search: 'checkout-api',
+        status: 'firing',
+        severity: '',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        returnTo,
+        serviceName: 'checkout-api',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'topology',
+        viewMode: 'resource-dependency',
+        sourceKind: 'database-middleware-connection',
+        edgeId: 'svc-checkout--res-orders-db'
+      },
+      signal: null,
+      signalContext: {
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout-api',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'topology',
+        returnTo
+      },
+      managementContext: {
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        returnTo,
+        returnLabel: '',
+        matchMode: '',
+        matchingRuleType: '',
+        matchingRuleIds: [],
+        matchedViewEnabled: false
+      }
+    };
 
-    const { default: AlertSilencePage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertSilencePage />);
+    const html = await renderAlertSilencePage(initialRouteState);
 
     expect(html).toContain('&quot;edgeId&quot;:&quot;svc-checkout--res-orders-db&quot;');
     expect(html).toContain('&quot;viewMode&quot;:&quot;resource-dependency&quot;');
@@ -126,26 +207,54 @@ describe('alert silence page', () => {
     expect(html).toContain('&quot;returnTo&quot;:&quot;/topology?viewMode=resource-dependency&amp;sourceKind=database-middleware-connection&amp;edgeId=svc-checkout--res-orders-db');
     expect(html).not.toContain('returnLabel=');
     expect(html).not.toContain('HertzBeat 企业运维拓扑');
-  });
+  }, 30_000);
 
   it('preserves three-signal evidence context into new silence authoring', async () => {
-    mockState.searchParams = new URLSearchParams({
+    const initialRouteState: AlertSilenceRouteState = {
       signal: 'logs',
-      source: 'otlp',
-      entityId: 'service:commerce/checkout',
-      entityName: 'checkout-api',
-      serviceName: 'checkout',
-      serviceNamespace: 'commerce',
-      environment: 'prod',
-      traceId: 'trace-123',
-      spanId: 'span-456',
-      collector: 'edge-collector-a',
-      template: 'java-service',
-      returnTo: '/log/manage?view=list&traceId=trace-123&returnLabel=%E6%97%A5%E5%BF%97%E5%B7%A5%E4%BD%9C%E5%8F%B0'
-    });
+      returnContext: {
+        search: 'checkout',
+        status: 'firing',
+        severity: '',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        returnTo: '/log/manage?view=list&traceId=trace-123',
+        serviceName: 'checkout',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        source: 'otlp',
+        signal: 'logs',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        collector: 'edge-collector-a',
+        template: 'java-service'
+      },
+      signalContext: {
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        source: 'otlp',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        collector: 'edge-collector-a',
+        template: 'java-service',
+        returnTo: '/log/manage?view=list&traceId=trace-123'
+      },
+      managementContext: {
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        returnTo: '/log/manage?view=list&traceId=trace-123',
+        returnLabel: '',
+        matchMode: '',
+        matchingRuleType: '',
+        matchingRuleIds: [],
+        matchedViewEnabled: false
+      }
+    };
 
-    const { default: AlertSilencePage } = await import('./page');
-    const html = renderToStaticMarkup(<AlertSilencePage />);
+    const html = await renderAlertSilencePage(initialRouteState);
 
     expect(html).toContain('data-alert-silence-evidence-context="signal-route"');
     expect(html).toContain('data-alert-silence-evidence-signal="logs"');
@@ -157,5 +266,199 @@ describe('alert silence page', () => {
     expect(html).toContain('hertzbeat.collector:edge-collector-a');
     expect(html).toContain('data-alert-silence-draft-labels="hertzbeat.signal:logs');
     expect(html).not.toContain('returnLabel=');
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+    expect(source).not.toContain('useSearchParams');
+    expect(source).not.toContain('queryStateFromParams(searchParams)');
+    expect(source).not.toContain('readSignalRouteContext(searchParams)');
+    expect(source).toContain('const alertSilenceRouteState = initialRouteState ?? EMPTY_ALERT_SILENCE_ROUTE_STATE');
+  }, 30_000);
+
+  it('loads Angular entity-noise-control matched silence rules by id', async () => {
+    const returnTo = '/entities/42?tab=alerts';
+    const initialRouteState: AlertSilenceRouteState = {
+      returnContext: {
+        search: 'checkout',
+        status: 'firing',
+        severity: '',
+        entityId: '42',
+        entityName: 'checkout-api',
+        returnTo
+      },
+      signal: null,
+      signalContext: {
+        entityId: '42',
+        entityName: 'checkout-api',
+        returnTo
+      },
+      managementContext: {
+        entityId: '42',
+        entityName: 'checkout-api',
+        returnTo,
+        returnLabel: 'checkout-api',
+        matchMode: 'entity-noise-controls',
+        matchingRuleType: 'silence',
+        matchingRuleIds: [11, 12],
+        matchedViewEnabled: true
+      }
+    };
+    apiMessageGet.mockImplementation(async (url: string) => {
+      if (url === '/alert/silence/11') {
+        return { id: 11, name: 'checkout silence', enable: true, matchAll: false, labels: { service: 'checkout' }, type: 0 };
+      }
+      if (url === '/alert/silence/12') {
+        throw new Error('missing');
+      }
+      return mockState.renderData.list;
+    });
+
+    const html = await renderAlertSilencePage(initialRouteState);
+    const loaded = await mockState.lastLoad?.() as any;
+
+    expect(html).toContain('data-alert-silence-match-mode="entity-noise-controls"');
+    expect(html).toContain('data-alert-silence-match-view="matched"');
+    expect(html).toContain('data-alert-silence-matching-rule-ids="11,12"');
+    expect(apiMessageGet).toHaveBeenCalledWith('/alert/silence/11');
+    expect(apiMessageGet).toHaveBeenCalledWith('/alert/silence/12');
+    expect(apiMessageGet).not.toHaveBeenCalledWith('/alert/silences?pageIndex=0&pageSize=8&sort=id&order=desc');
+    expect(loaded.list.content.map((item: any) => item.id)).toEqual([11]);
+    expect(loaded.missingMatchedRuleCount).toBe(1);
+  }, 30_000);
+
+  it('keeps alert silence remounts on a short settled cache window with refresh-tick invalidation', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+
+    expect(source).toContain('ALERT_SILENCE_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('ALERT_SILENCE_LABEL_OPTIONS_TIMEOUT_MS = 2_500');
+    expect(source).toContain('withTimeoutFallback(');
+    expect(source).toContain('const [refreshTick, setRefreshTick] = useState(0)');
+    expect(source).toContain('const [pageIndex, setPageIndex] = useState(0)');
+    expect(source).toContain('const [pageSize, setPageSize] = useState<number>(ALERT_SILENCE_PAGE_SIZE_OPTIONS[0])');
+    expect(source).toContain("['alert-silence', useMatchedView ? `matched:${matchedRuleIdsKey}` : alertSilenceListUrl, refreshTick].join('|')");
+    expect(source).toContain('buildAlertSilenceUrl({ search: query, pageIndex, pageSize })');
+    expect(source).toContain('[pageIndex, pageSize, query]');
+    expect(source).toContain('[alertSilenceListUrl, matchedRuleIdsKey, refreshTick, useMatchedView]');
+    expect(source).toContain('loadAlertSilenceDataFromFacade');
+    expect(source).toContain('list: api.alertSilences.list');
+    expect(source).toContain('loadAlertLabelOptionsFromFacade(api.alertLabels.list)');
+    expect(source).toContain('loadMatchedAlertSilencesFromFacade(api.alertSilences.detail, managementContext.matchingRuleIds)');
+    expect(source).not.toContain('apiMessageGet<PageResult<AlertSilence>>(alertSilenceListUrl)');
+    expect(source).toContain('return { ...data, refreshTick, missingMatchedRuleCount: 0 };');
+    expect(source.match(/setRefreshTick\(value => value \+ 1\)/g)?.length).toBeGreaterThanOrEqual(4);
+    expect(source).toContain('cacheKey={alertSilenceCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={ALERT_SILENCE_SETTLED_CACHE_TTL_MS}');
+    expect(source).toContain('pageSizeOptions={[...ALERT_SILENCE_PAGE_SIZE_OPTIONS]}');
+    expect(source).toContain('function handlePageIndexChange(nextPageIndex: number)');
+    expect(source).toContain('function handlePageSizeChange(nextPageSize: number)');
+    expect(source).toContain('setPageIndex(0);');
+  });
+
+  it('keeps Angular no-selection batch delete warning before opening confirm', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+    const surfaceSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-silence-surface.tsx'), 'utf8');
+
+    expect(source).toContain('if (checkedIds.length === 0)');
+    expect(source).toContain("setEditorError(t('common.notify.no-select-delete'))");
+    expect(source).toContain('setEditorMessage(null)');
+    expect(source).toContain("setDeleteRequest({ kind: 'batch', ids: checkedIds })");
+    expect(surfaceSource).toContain('data-alert-silence-delete-selected="toolbar"');
+    expect(surfaceSource).toContain('data-alert-silence-action-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(surfaceSource).not.toContain('disabled={selectedCount === 0}');
+  });
+
+  it('uses Angular delete notifications for confirmed delete feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+
+    expect(source).toContain('async function handleConfirmedDelete()');
+    expect(source).toContain('deleteAlertSilencesFromFacade(api.alertSilences.delete, request.ids)');
+    expect(source).toContain('deleteAlertSilenceFromFacade(api.alertSilences.delete, request.ids[0])');
+    expect(source).toContain("setEditorMessage(t('common.notify.delete-success'))");
+    expect(source).toContain("setEditorError(t('common.notify.delete-fail'))");
+    expect(source).toContain('setEditorErrorDetail(error instanceof Error ? error.message : null)');
+    expect(source).toContain("setEditorErrorContract('delete')");
+    expect(source).not.toContain('apiMessageDelete');
+    expect(source).not.toContain("setEditorMessage(t('common.delete-success'))");
+    expect(source).not.toContain("t('common.delete-failed')");
+  });
+
+  it('uses Angular edit notifications for enable-toggle feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+    const toggleSource = source.slice(
+      source.indexOf('async function handleToggleEnabled(silence?: AlertSilence)'),
+      source.indexOf('async function handleDelete(silenceId?: number)')
+    );
+
+    expect(toggleSource).toContain('async function handleToggleEnabled(silence?: AlertSilence)');
+    expect(toggleSource).toContain('updateAlertSilenceEnabledFromFacade(api.alertSilences.update, target, !(target.enable ?? true))');
+    expect(toggleSource).toContain("setEditorMessage(t('common.notify.edit-success'))");
+    expect(toggleSource).toContain("setEditorError(t('common.notify.edit-fail'))");
+    expect(toggleSource).toContain('setEditorErrorDetail(error instanceof Error ? error.message : null)');
+    expect(toggleSource).toContain("setEditorErrorContract('enable')");
+    expect(toggleSource).not.toContain('apiMessagePut');
+    expect(toggleSource).not.toContain("setEditorMessage(t('common.save-success'))");
+    expect(toggleSource).not.toContain("t('common.save-failed')");
+  });
+
+  it('uses Angular create/edit notifications for editor save feedback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+    const saveSource = source.slice(
+      source.indexOf('async function handleSave()'),
+      source.indexOf('async function handleToggleEnabled(silence?: AlertSilence)')
+    );
+
+    expect(saveSource).toContain('const isEdit = Boolean(draft.id)');
+    expect(saveSource).toContain('updateAlertSilenceFromFacade(api.alertSilences.update, draft)');
+    expect(saveSource).toContain('createAlertSilenceFromFacade(api.alertSilences.create, draft)');
+    expect(saveSource).toContain("setEditorMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'))");
+    expect(saveSource).toContain('setCreatedOutsideMatchedViewNotice(!isEdit && useMatchedView)');
+    expect(saveSource).toContain("setEditorError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+    expect(saveSource).toContain('setEditorErrorDetail(error instanceof Error ? error.message : null)');
+    expect(saveSource).toContain("setEditorErrorContract('save')");
+    expect(saveSource).not.toContain('apiMessagePost');
+    expect(saveSource).not.toContain('apiMessagePut');
+    expect(saveSource).not.toContain("setEditorMessage(t('common.save-success'))");
+    expect(saveSource).not.toContain("t('common.save-failed')");
+  });
+
+  it('keeps Angular created-outside-matched notice state for matched-view authoring', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+    const surfaceSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-silence-surface.tsx'), 'utf8');
+
+    expect(source).toContain('const [createdOutsideMatchedViewNotice, setCreatedOutsideMatchedViewNotice] = useState(false)');
+    expect(source).toContain('setCreatedOutsideMatchedViewNotice(false)');
+    expect(source).toContain('createdOutsideMatchedViewNotice={createdOutsideMatchedViewNotice}');
+    expect(surfaceSource).toContain('data-alert-silence-created-outside-matched="angular-authoring-notice"');
+    expect(surfaceSource).toContain('data-alert-silence-created-outside-matched-owner="hertzbeat-ui-inline-feedback"');
+    expect(surfaceSource).toContain('data-alert-silence-created-outside-matched-action="view-all"');
+  });
+
+  it('keeps Angular entity alert common-label prefill for new silence authoring', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+    const surfaceSource = readFileSync(resolve(process.cwd(), 'components/pages/alert-silence-surface.tsx'), 'utf8');
+
+    expect(source).toContain('buildAlertSilenceEntityPrefillFromFacade(');
+    expect(source).toContain("entityId => api.entities.alerts(entityId, { pageIndex: 0, pageSize: 20, status: 'firing' })");
+    expect(source).not.toContain("from '../../../lib/api-client'");
+    expect(source).toContain("t('entity.noise-controls.authoring.silence.prefill-warning')");
+    expect(source).toContain("t('entity.noise-controls.authoring.prefill-warning.no-entity-id')");
+    expect(source).toContain('entityPrefillSource={entityPrefillSource}');
+    expect(source).toContain('entityPrefillWarning={entityPrefillWarning}');
+    expect(surfaceSource).toContain('data-alert-silence-entity-prefill=');
+    expect(surfaceSource).toContain("t('entity.noise-controls.authoring.silence.title')");
+    expect(surfaceSource).toContain("t('entity.noise-controls.authoring.silence.prefill-success')");
+    expect(surfaceSource).toContain('prefillWarning={entityPrefillWarning}');
+  });
+
+  it('uses Angular edit failure notification for edit detail load fallback', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/silence/alert-silence-page.tsx'), 'utf8');
+    const editSource = source.slice(
+      source.indexOf('async function handleEdit(silenceId?: number)'),
+      source.indexOf('async function handleSave()')
+    );
+
+    expect(editSource).toContain('async function handleEdit(silenceId?: number)');
+    expect(editSource).toContain('loadAlertSilenceDetailFromFacade(api.alertSilences.detail, targetId)');
+    expect(editSource).toContain("setEditorError(error instanceof Error ? error.message : t('common.notify.edit-fail'))");
+    expect(editSource).not.toContain('apiMessageGet');
+    expect(editSource).not.toContain("t('common.load-failed')");
   });
 });
diff --git a/web-next/app/alert/silence/page.tsx b/web-next/app/alert/silence/page.tsx
index 939dabba19..1759084477 100644
--- a/web-next/app/alert/silence/page.tsx
+++ b/web-next/app/alert/silence/page.tsx
@@ -1,224 +1,14 @@
-'use client';
-
 import React from 'react';
-import { useCallback, useMemo, useState } from 'react';
-import { useSearchParams } from 'next/navigation';
-import { ClientWorkbench } from '../../../components/workbench/client-workbench';
-import { useI18n } from '../../../components/providers/i18n-provider';
-import { AlertSilenceSurface } from '../../../components/pages/alert-silence-surface';
-import { ColdConfirmDialog } from '../../../components/ui/cold-confirm-dialog';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '../../../lib/api-client';
-import { buildAlertSilenceFormDraft, createAlertSilence, deleteAlertSilence, deleteAlertSilences, loadAlertSilenceDetail, updateAlertSilence, type AlertSilenceFormDraft } from '../../../lib/alert-silence/controller';
-import { buildAlertSilenceUrl } from '../../../lib/alert-silence/query-state';
-import { queryStateFromParams } from '../../../lib/alert-manage/query-state';
-import { buildAlertSilenceEvidenceContext, validateAlertSilenceForm } from '../../../lib/alert-silence/view-model';
-import { DEFAULT_ALERT_LABEL_OPTIONS, loadAlertLabelOptions } from '../../../lib/alert-label-options';
-import { formatTime } from '../../../lib/format';
-import { readSignalRouteContext } from '../../../lib/signal-route-context';
-import type { AlertSilence, PageResult } from '../../../lib/types';
-
-type SilenceDeleteRequest = {
-  kind: 'single' | 'batch';
-  ids: number[];
-};
-
-export default function AlertSilencePage() {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const returnContext = useMemo(() => queryStateFromParams(searchParams), [searchParams]);
-  const signalContext = useMemo(() => readSignalRouteContext(searchParams), [searchParams]);
-  const signal = searchParams.get('signal');
-  const silenceEvidenceContext = useMemo(
-    () => buildAlertSilenceEvidenceContext(signal, signalContext, t),
-    [signal, signalContext, t]
-  );
-  const [search, setSearch] = useState('');
-  const [query, setQuery] = useState('');
-  const [selectedId, setSelectedId] = useState<number | null>(null);
-  const [editorOpen, setEditorOpen] = useState(false);
-  const [editorLoading, setEditorLoading] = useState(false);
-  const [editorSaving, setEditorSaving] = useState(false);
-  const [editorMessage, setEditorMessage] = useState<string | null>(null);
-  const [editorError, setEditorError] = useState<string | null>(null);
-  const [draft, setDraft] = useState<AlertSilenceFormDraft>(() => buildAlertSilenceFormDraft(null, silenceEvidenceContext?.draftPatch));
-  const [refreshTick, setRefreshTick] = useState(0);
-  const [checkedIds, setCheckedIds] = useState<number[]>([]);
-  const [deleteRequest, setDeleteRequest] = useState<SilenceDeleteRequest | null>(null);
-  const [deletePending, setDeletePending] = useState(false);
-
-  const load = useCallback(async () => {
-    const [list, labelOptions] = await Promise.all([
-      apiMessageGet<PageResult<AlertSilence>>(buildAlertSilenceUrl(query)),
-      loadAlertLabelOptions(apiMessageGet).catch(() => DEFAULT_ALERT_LABEL_OPTIONS)
-    ]);
-    return { list, labelOptions, refreshTick };
-  }, [query, refreshTick]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('alert.silence.loading')}>
-      {data => {
-        const labelOptions = data.labelOptions ?? DEFAULT_ALERT_LABEL_OPTIONS;
-        const selected = data.list.content.find(item => item.id === selectedId) ?? data.list.content[0] ?? null;
-
-        async function handleNew() {
-          setDraft(buildAlertSilenceFormDraft(null, silenceEvidenceContext?.draftPatch));
-          setEditorError(null);
-          setEditorMessage(null);
-          setEditorOpen(true);
-        }
-
-        async function handleEdit(silenceId?: number) {
-          const targetId = silenceId ?? selected?.id;
-          if (!targetId) return;
-          setEditorLoading(true);
-          setEditorError(null);
-          setEditorMessage(null);
-          try {
-            const detail = await loadAlertSilenceDetail(apiMessageGet, targetId);
-            setDraft(buildAlertSilenceFormDraft(detail));
-            setEditorOpen(true);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.load-failed'));
-          } finally {
-            setEditorLoading(false);
-          }
-        }
-
-        async function handleSave() {
-          const validationError = validateAlertSilenceForm(draft, t);
-          if (validationError) {
-            setEditorError(validationError);
-            return;
-          }
-          setEditorSaving(true);
-          setEditorError(null);
-          setEditorMessage(null);
-          try {
-            if (draft.id) {
-              await updateAlertSilence(apiMessagePut as any, draft);
-            } else {
-              await createAlertSilence(apiMessagePost as any, draft);
-            }
-            setEditorMessage(t('common.save-success'));
-            setEditorOpen(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setEditorSaving(false);
-          }
-        }
-
-        async function handleToggleEnabled(silence?: AlertSilence) {
-          const target = silence ?? selected;
-          if (!target) return;
-          try {
-            await updateAlertSilence(apiMessagePut as any, {
-              ...buildAlertSilenceFormDraft(target),
-              enable: !(target.enable ?? true),
-            });
-            setEditorMessage(t('common.save-success'));
-            setEditorError(null);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.save-failed'));
-          }
-        }
-
-        async function handleDelete(silenceId?: number) {
-          const targetId = silenceId ?? selected?.id;
-          if (!targetId) return;
-          setDeleteRequest({ kind: 'single', ids: [targetId] });
-        }
-
-        async function handleConfirmedDelete() {
-          const request = deleteRequest;
-          if (!request || request.ids.length === 0) return;
-          setDeletePending(true);
-          try {
-            if (request.kind === 'batch') {
-              await deleteAlertSilences(apiMessageDelete as any, request.ids);
-              setCheckedIds([]);
-            } else {
-              await deleteAlertSilence(apiMessageDelete as any, request.ids[0]);
-            }
-            setSelectedId(null);
-            setEditorOpen(false);
-            setEditorMessage(t('common.delete-success'));
-            setEditorError(null);
-            setDeleteRequest(null);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setEditorError(error instanceof Error ? error.message : t('common.delete-failed'));
-          } finally {
-            setDeletePending(false);
-          }
-        }
-
-        function handleRefresh() {
-          setRefreshTick(value => value + 1);
-        }
-
-        async function handleDeleteSelected() {
-          if (checkedIds.length === 0) return;
-          setDeleteRequest({ kind: 'batch', ids: checkedIds });
-        }
 
-        return (
-          <>
-            <AlertSilenceSurface
-              t={t}
-              data={data}
-              search={search}
-              selectedId={selectedId}
-              checkedIds={checkedIds}
-              editorOpen={editorOpen}
-              editorLoading={editorLoading}
-              editorSaving={editorSaving}
-              editorMessage={editorMessage}
-              editorError={editorError}
-              returnContext={returnContext}
-              evidenceContext={silenceEvidenceContext}
-              draft={draft}
-              labelOptions={labelOptions}
-              formatTime={formatTime}
-              onSearchChange={setSearch}
-              onApplyFilter={() => setQuery(search)}
-              onClearFilter={() => {
-                setSearch('');
-                setQuery('');
-              }}
-              onRefresh={handleRefresh}
-              onSelect={setSelectedId}
-              onCheckedIdsChange={setCheckedIds}
-              onNew={() => void handleNew()}
-              onEdit={silenceId => void handleEdit(silenceId)}
-              onSave={() => void handleSave()}
-              onToggleEnabled={silence => void handleToggleEnabled(silence)}
-              onDelete={silenceId => void handleDelete(silenceId)}
-              onDeleteSelected={() => void handleDeleteSelected()}
-              onCloseEditor={() => setEditorOpen(false)}
-              onDraftChange={setDraft}
-            />
-            <div data-alert-delete-confirm={deleteRequest ? 'open' : 'closed'}>
-              <ColdConfirmDialog
-                open={Boolean(deleteRequest)}
-                title={deleteRequest?.kind === 'batch' ? t('common.confirm.delete-batch') : t('common.confirm.delete')}
-                copy={
-                  deleteRequest?.kind === 'batch'
-                    ? t('alert.silence.delete.confirm.batch', { count: deleteRequest.ids.length })
-                    : t('alert.silence.delete.confirm.single')
-                }
-                confirmLabel={t('common.button.ok')}
-                cancelLabel={t('common.button.cancel')}
-                pending={deletePending}
-                onCancel={() => setDeleteRequest(null)}
-                onConfirm={() => void handleConfirmedDelete()}
-              />
-            </div>
-          </>
-        );
-      }}
-    </ClientWorkbench>
-  );
+import AlertSilencePage from './alert-silence-page';
+import { readAlertSilenceRouteState, type AlertSilenceSearchParams } from '../../../lib/alert-silence/query-state';
+
+export default async function AlertSilenceRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<AlertSilenceSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const routeState = readAlertSilenceRouteState(resolvedSearchParams);
+  return <AlertSilencePage initialRouteState={routeState} />;
 }
diff --git a/web-next/app/alerts/page.test.ts b/web-next/app/alerts/page.test.ts
index 11daabb81b..57d520da93 100644
--- a/web-next/app/alerts/page.test.ts
+++ b/web-next/app/alerts/page.test.ts
@@ -1,34 +1,48 @@
-import React from 'react';
-import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 
-const alertCenterPage = vi.fn(() => (
-  React.createElement(
-    'main',
-    {
-      'data-alert-center-surface': 'otlp-cold-center-console',
-      'data-alert-center-style-baseline': 'hertzbeat-cold-matte'
-    },
-    React.createElement('h1', null, '告警中心'),
-    React.createElement('p', null, '集中查看并处理当前告警'),
-    React.createElement('button', { type: 'button' }, '刷新')
-  )
-));
-
-vi.mock('../alert/page', () => ({
-  default: alertCenterPage
+const redirect = vi.fn();
+
+vi.mock('next/navigation', () => ({
+  redirect
 }));
 
 describe('alerts alias route', () => {
-  it('renders the shared alert center surface directly for live parity captures', async () => {
+  it('redirects alerts compatibility traffic to the main alert workbench', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
     const { default: AlertsAliasPage } = await import('./page');
-    const html = renderToStaticMarkup(React.createElement(AlertsAliasPage));
-
-    expect(html).toContain('data-alert-center-surface="otlp-cold-center-console"');
-    expect(html).toContain('data-alert-center-style-baseline="hertzbeat-cold-matte"');
-    expect(html).toContain('告警中心');
-    expect(html).toContain('集中查看并处理当前告警');
-    expect(html).toContain('刷新');
-    expect(alertCenterPage).toHaveBeenCalled();
-  });
+
+    await expect(AlertsAliasPage({ searchParams: Promise.resolve({}) })).rejects.toThrow('redirect:/alert');
+    expect(redirect).toHaveBeenCalledWith('/alert');
+  }, 20000);
+
+  it('preserves alert filters and machine context while stripping display-only labels', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: AlertsAliasPage } = await import('./page');
+
+    await expect(
+      AlertsAliasPage({
+        searchParams: Promise.resolve({
+          content: ' checkout ',
+          status: 'ACKNOWLEDGED',
+          severity: 'Warning',
+          entityId: '42',
+          entityName: 'Checkout API',
+          returnTo: '/entities/42?returnLabel=Checkout',
+          returnLabel: 'Checkout',
+          signal: 'logs'
+        })
+      })
+    ).rejects.toThrow(
+      'redirect:/alert?search=checkout&status=acknowledged&severity=warning&entityId=42&entityName=Checkout+API&returnTo=%2Fentities%2F42&signal=logs'
+    );
+    expect(redirect).toHaveBeenLastCalledWith(
+      '/alert?search=checkout&status=acknowledged&severity=warning&entityId=42&entityName=Checkout+API&returnTo=%2Fentities%2F42&signal=logs'
+    );
+  }, 20000);
 });
diff --git a/web-next/app/alerts/page.tsx b/web-next/app/alerts/page.tsx
index 825b4839af..d5f331ddf7 100644
--- a/web-next/app/alerts/page.tsx
+++ b/web-next/app/alerts/page.tsx
@@ -1,6 +1,11 @@
-import React from 'react';
-import AlertCenterPage from '../alert/page';
+import { redirect } from 'next/navigation';
+import { buildAlertCompatRouteUrlFromSearchParams, type SearchParamsRecord } from '../../lib/alert-manage/query-state';
 
-export default function AlertsAliasPage() {
-  return <AlertCenterPage />;
+export default async function AlertsAliasPage({
+  searchParams
+}: {
+  searchParams?: Promise<SearchParamsRecord>;
+}) {
+  const resolvedSearchParams = await searchParams;
+  redirect(buildAlertCompatRouteUrlFromSearchParams(resolvedSearchParams));
 }
diff --git a/web-next/components/pages/alert-center-surface.test.tsx b/web-next/components/pages/alert-center-surface.test.tsx
index c227a6d87c..dddde328bb 100644
--- a/web-next/components/pages/alert-center-surface.test.tsx
+++ b/web-next/components/pages/alert-center-surface.test.tsx
@@ -99,6 +99,7 @@ describe('AlertCenterSurface', () => {
             fingerprint: 'fp-1',
             creator: 'ops',
             labels: { service: 'checkout', severity: 'critical', alertname: 'HighCPU' },
+            annotations: { summary: 'CPU has been high for two intervals' },
             status: 'firing',
             triggerTimes: 2,
             gmtUpdate: 1713200000000,
@@ -146,8 +147,19 @@ describe('AlertCenterSurface', () => {
 
     expect(html).toContain('data-alert-center-surface="otlp-cold-center-console"');
     expect(html).toContain('data-alert-center-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-alert-center-sse-contract="angular-alert-event-refresh"');
+    expect(html).toContain('data-alert-center-sse-event-count="0"');
+    expect(html).toContain('data-alert-center-sse-highlight="angular-new-alert"');
+    expect(html).toContain('data-alert-center-sse-highlight-ids=""');
     expect(html).toContain('data-alert-center-header="cold-compact-header"');
     expect(html).toContain('data-alert-center-command-row="standard-equal-buttons"');
+    expect(html).toContain('data-alert-center-facts-strip="angular-platform-facts-strip"');
+    expect(html).toContain('data-alert-center-facts-strip-owner="hertzbeat-ui-stat-strip"');
+    expect(html).toContain('data-alert-center-fact="total"');
+    expect(html).toContain('data-alert-center-fact="firing"');
+    expect(html).toContain('data-alert-center-fact="acknowledged"');
+    expect(html).toContain('data-alert-center-fact="resolved"');
+    expect(html).toContain('data-alert-center-fact-owner="hertzbeat-ui-stat-cell"');
     expect(html).toContain('data-alert-center-admin-layout="full-width-admin-list"');
     expect(html).toContain('data-alert-center-toolbar="cold-query-toolbar"');
     expect(html).toContain('data-alert-center-query-toolbar="single-query-form"');
@@ -269,6 +281,64 @@ describe('AlertCenterSurface', () => {
     expect(source).not.toContain('min-h-[596px]');
   });
 
+  it('renders Angular-style alert center pagination through the shared HzPaginationBar owner', () => {
+    const html = renderSurface({
+      data: {
+        summary,
+        groupAlerts: {
+          ...groupAlerts,
+          totalElements: 31,
+          pageIndex: 1,
+          pageSize: 8
+        }
+      } as any,
+      draft: { search: 'checkout', status: 'firing', severity: 'critical', pageIndex: 1, pageSize: 8 }
+    }, zh);
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-center-surface.tsx'), 'utf8');
+
+    expect(html).toContain('data-alert-center-pagination="cold-dense-pagination"');
+    expect(html).toContain('data-alert-center-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-hz-ui="pagination-bar"');
+    expect(html).toContain('data-hz-pagination-page-size="select-menu"');
+    expect(html).toContain('data-hz-pagination-page-jump="number-input"');
+    expect(html).toContain('data-alert-center-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-alert-center-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('第 2 / 4 页 · 9-9 / 31');
+    expect(html).toContain('每页条数');
+    expect(html).toContain('页码');
+    expect(source).toContain('HzPaginationBar');
+    expect(source).toContain('data-alert-center-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(source).toContain('onPageIndexChange?.(Math.max(currentPageIndex - 1, 0))');
+    expect(source).toContain('onPageSizeChange?.(Number.parseInt(value, 10))');
+  });
+
+  it('marks SSE-refreshed alert groups with the Angular new-alert highlight contract', () => {
+    const html = renderSurface({
+      data: {
+        summary,
+        groupAlerts: {
+          ...groupAlerts,
+          totalElements: 2,
+          content: [
+            groupAlerts.content[0],
+            { ...groupAlerts.content[0], id: 8, groupLabels: { service: 'cart', severity: 'warning' } }
+          ]
+        }
+      } as any,
+      realtimeEventCount: 2,
+      realtimeGroupIds: [7]
+    }, zh);
+
+    expect(html).toContain('data-alert-center-sse-event-count="2"');
+    expect(html).toContain('data-alert-center-sse-highlight-ids="7"');
+    expect(html).toContain('data-alert-group-card="7"');
+    expect(html).toContain('data-alert-group-realtime-state="new"');
+    expect(html).toContain('data-alert-group-realtime-owner="angular-new-alert"');
+    expect(html).toContain('border-l-[3px]');
+    expect(html).toContain('data-alert-group-card="8"');
+    expect(html).not.toContain('data-alert-group-card="8" data-alert-group-realtime-state="new"');
+  });
+
   it('hides the clear-filters action when no filters are active', () => {
     const html = renderSurface({}, t);
 
@@ -277,6 +347,478 @@ describe('AlertCenterSurface', () => {
     expect(html).not.toContain('data-alert-center-clear-filters="true"');
   });
 
+  it('fires alert center pagination callbacks without replacing the query toolbar contract', async () => {
+    const onPageIndexChange = vi.fn();
+    const onPageSizeChange = vi.fn();
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <AlertCenterSurface
+          t={zh}
+          data={{
+            summary,
+            groupAlerts: {
+              ...groupAlerts,
+              totalElements: 31,
+              pageIndex: 1,
+              pageSize: 8
+            }
+          } as any}
+          draft={{ search: 'checkout', status: 'firing', severity: 'critical', pageIndex: 1, pageSize: 8 }}
+          onDraftChange={vi.fn()}
+          onRefresh={vi.fn()}
+          onClearFilters={vi.fn()}
+          onPageIndexChange={onPageIndexChange}
+          onPageSizeChange={onPageSizeChange}
+        />
+      );
+      await Promise.resolve();
+    });
+
+    const pageJump = interactionContainer.querySelector(
+      'input[data-alert-center-pagination-page-jump-owner="hertzbeat-ui-input"]'
+    ) as HTMLInputElement | null;
+    const pageSize = interactionContainer.querySelector('[data-alert-center-pagination-page-size-owner="hertzbeat-ui-select"]');
+    const next = interactionContainer.querySelector('button[data-hz-pagination-action="next"]') as HTMLButtonElement | null;
+    const toolbar = interactionContainer.querySelector('[data-alert-center-query-toolbar="single-query-form"]');
+
+    expect(pageJump?.getAttribute('data-hz-pagination-action')).toBe('page-jump');
+    expect(pageSize).not.toBeNull();
+    expect(toolbar).not.toBeNull();
+
+    await act(async () => {
+      pageJump?.dispatchEvent(new Event('change', { bubbles: true }));
+      pageJump!.value = '3';
+      pageJump?.dispatchEvent(new Event('input', { bubbles: true }));
+      next?.click();
+    });
+
+    expect(onPageIndexChange).toHaveBeenCalledWith(2);
+    expect(onPageSizeChange).not.toHaveBeenCalled();
+  });
+
+  it('renders the entity-context batch selection toolbar with shared UI owners', () => {
+    const html = renderSurface({
+      data: {
+        summary,
+        groupAlerts: {
+          content: [
+            groupAlerts.content[0],
+            { ...groupAlerts.content[0], id: 8, status: 'acknowledged', groupLabels: { service: 'cart', severity: 'warning' } },
+            { ...groupAlerts.content[0], id: 9, status: 'resolved', groupLabels: { service: 'payment', severity: 'info' } }
+          ],
+          totalElements: 3,
+          pageIndex: 0,
+          pageSize: 8
+        }
+      } as any,
+      draft: {
+        search: '',
+        status: 'firing',
+        severity: '',
+        entityId: '42',
+        entityName: 'Checkout API',
+        returnTo: '/entities/42'
+      },
+      selectedGroupIds: [7, 8]
+    }, zh);
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-center-surface.tsx'), 'utf8');
+
+    expect(html).toContain('data-alert-center-entity-batch="angular-selected-groups"');
+    expect(html).toContain('data-alert-center-entity-batch-owner="hertzbeat-ui-batch-toolbar"');
+    expect(html).toContain('data-alert-center-delete-page-clamp="angular-update-page-index"');
+    expect(html).toContain('data-alert-center-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-alert-center-rule-create-feedback="angular-new-notify"');
+    expect(html).toContain('data-alert-center-rule-create-feedback-owner="route-state-contract"');
+    expect(html).toContain('data-alert-center-rule-selection-count="angular-group-count"');
+    expect(html).toContain('data-alert-center-rule-selection-count-owner="route-state-contract"');
+    expect(html).toContain('data-alert-center-inhibit-defaults="angular-drop-severity-equal-allowlist"');
+    expect(html).toContain('data-alert-center-inhibit-defaults-owner="route-state-contract"');
+    expect(html).toContain('data-alert-center-batch-confirm="angular-status-confirm"');
+    expect(html).toContain('data-alert-center-batch-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-alert-center-row-delete-confirm="angular-single-delete-confirm"');
+    expect(html).toContain('data-alert-center-row-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-alert-center-batch-select-page="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-alert-center-batch-toolbar="selected-entity-alerts"');
+    expect(html).toContain('data-hz-ui="batch-toolbar"');
+    expect(html).toContain('data-hz-ui="checkbox"');
+    expect(html).toContain('data-alert-center-group-select="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-alert-center-batch-action="acknowledge-selected"');
+    expect(html).toContain('data-alert-center-batch-action="unacknowledge-selected"');
+    expect(html).toContain('data-alert-center-batch-action="resolve-selected"');
+    expect(html).toContain('data-alert-center-batch-action="reopen-selected"');
+    expect(html).toContain('data-alert-center-batch-action="silence-selected"');
+    expect(html).toContain('data-alert-center-batch-action="inhibit-selected"');
+    expect(html).toContain('data-alert-center-batch-dialog-source="selected-groups"');
+    expect(html).toContain('data-hz-batch-selection-count="2"');
+    expect(html).toContain('条告警组已选择');
+    expect(html).toContain('确认 1 条');
+    expect(html).toContain('取消确认 1 条');
+    expect(html).toContain('恢复 1 条');
+    expect(html).toContain('创建静默 2 条');
+    expect(html).toContain('创建抑制 2 条');
+    expect(source).toContain('HzBatchToolbar');
+    expect(source).toContain('HzCheckbox');
+    expect(source).toContain('SELECTED_BATCH_DIALOG_GROUP_KEY');
+    expect(source).toContain('buildSelectedBatchRuleGroup');
+    expect(source).toContain('data-alert-center-entity-batch-owner="hertzbeat-ui-batch-toolbar"');
+  });
+
+  it('prefills selected alert silence and inhibit dialogs from shared group labels', () => {
+    const data = {
+      summary,
+      groupAlerts: {
+        content: [
+          groupAlerts.content[0],
+          { ...groupAlerts.content[0], id: 8, groupLabels: { service: 'checkout', severity: 'warning' } }
+        ],
+        totalElements: 2,
+        pageIndex: 0,
+        pageSize: 8
+      }
+    } as any;
+    const silenceHtml = renderSurface({
+      data,
+      draft: {
+        search: '',
+        status: 'firing',
+        severity: '',
+        entityId: '42',
+        entityName: 'Checkout API',
+        returnTo: '/entities/42'
+      },
+      selectedGroupIds: [7, 8],
+      initialDialogState: { groupKey: '__selected_batch__', mode: 'silence' },
+      onRuleQuickCreate: vi.fn(async () => undefined)
+    }, zh);
+    const inhibitHtml = renderSurface({
+      data,
+      draft: {
+        search: '',
+        status: 'firing',
+        severity: '',
+        entityId: '42',
+        entityName: 'Checkout API',
+        returnTo: '/entities/42'
+      },
+      selectedGroupIds: [7, 8],
+      initialDialogState: { groupKey: '__selected_batch__', mode: 'inhibit' },
+      onRuleQuickCreate: vi.fn(async () => undefined)
+    }, zh);
+
+    expect(silenceHtml).toContain('data-alert-rule-dialog="silence"');
+    expect(silenceHtml).toContain('data-alert-rule-dialog-submit="silence"');
+    expect(silenceHtml).toContain('data-alert-rule-dialog-submit-owner="alert-center-quick-dialog"');
+    expect(silenceHtml).toContain('将基于 2 条告警的共用标签预填静默规则');
+    expect(silenceHtml).toContain('service:checkout');
+    expect(silenceHtml).toContain('name="silence_labels" value="service:checkout"');
+    expect(inhibitHtml).toContain('data-alert-rule-dialog="inhibit"');
+    expect(inhibitHtml).toContain('data-alert-rule-dialog-submit="inhibit"');
+    expect(inhibitHtml).toContain('data-alert-rule-dialog-submit-owner="alert-center-quick-dialog"');
+    expect(inhibitHtml).toContain('将基于 2 条告警的共用标签预填抑制规则');
+    expect(inhibitHtml).toContain('service:checkout');
+    expect(inhibitHtml).toContain('name="inhibit_source_labels" value="service:checkout"');
+    expect(inhibitHtml).toContain('name="inhibit_target_labels" value="service:checkout"');
+    expect(inhibitHtml).toContain('name="inhibit_equal_labels" value="service"');
+  });
+
+  it('updates selected entity alert groups and fires status-scoped batch operations', async () => {
+    const onSelectedGroupIdsChange = vi.fn();
+    const onClosureAction = vi.fn();
+    const onRuleQuickCreate = vi.fn(async () => undefined);
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <AlertCenterSurface
+          t={zh}
+          data={{
+            summary,
+            groupAlerts: {
+              content: [
+                groupAlerts.content[0],
+                { ...groupAlerts.content[0], id: 8, status: 'acknowledged', groupLabels: { service: 'cart', severity: 'warning' } },
+                { ...groupAlerts.content[0], id: 9, status: 'resolved', groupLabels: { service: 'payment', severity: 'info' } }
+              ],
+              totalElements: 3,
+              pageIndex: 0,
+              pageSize: 8
+            }
+          } as any}
+          draft={{
+            search: '',
+            status: 'firing',
+            severity: '',
+            entityId: '42',
+            entityName: 'Checkout API',
+            returnTo: '/entities/42'
+          }}
+          selectedGroupIds={[7, 8, 9]}
+          onSelectedGroupIdsChange={onSelectedGroupIdsChange}
+          onDraftChange={vi.fn()}
+          onRefresh={vi.fn()}
+          onClearFilters={vi.fn()}
+          onClosureAction={onClosureAction}
+          onRuleQuickCreate={onRuleQuickCreate}
+        />
+      );
+      await Promise.resolve();
+    });
+
+    const selectPage = interactionContainer.querySelector(
+      'input[data-alert-center-batch-select-page="hertzbeat-ui-checkbox"]'
+    ) as HTMLInputElement | null;
+    const selectGroup = interactionContainer.querySelector(
+      'input[data-alert-center-group-select-id="7"]'
+    ) as HTMLInputElement | null;
+    const acknowledge = interactionContainer.querySelector(
+      'button[data-alert-center-batch-action="acknowledge-selected"]'
+    ) as HTMLButtonElement | null;
+    const unacknowledge = interactionContainer.querySelector(
+      'button[data-alert-center-batch-action="unacknowledge-selected"]'
+    ) as HTMLButtonElement | null;
+    const resolve = interactionContainer.querySelector(
+      'button[data-alert-center-batch-action="resolve-selected"]'
+    ) as HTMLButtonElement | null;
+    const reopen = interactionContainer.querySelector('button[data-alert-center-batch-action="reopen-selected"]') as HTMLButtonElement | null;
+    const silence = interactionContainer.querySelector('button[data-alert-center-batch-action="silence-selected"]') as HTMLButtonElement | null;
+    const inhibit = interactionContainer.querySelector('button[data-alert-center-batch-action="inhibit-selected"]') as HTMLButtonElement | null;
+
+    expect(selectPage).not.toBeNull();
+    expect(selectGroup).not.toBeNull();
+    expect(acknowledge).not.toBeNull();
+    expect(unacknowledge).not.toBeNull();
+    expect(resolve).not.toBeNull();
+    expect(reopen).not.toBeNull();
+    expect(silence).not.toBeNull();
+    expect(inhibit).not.toBeNull();
+
+    await act(async () => {
+      selectPage?.click();
+      selectGroup?.click();
+      acknowledge?.click();
+    });
+
+    expect(onClosureAction).not.toHaveBeenCalled();
+    expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm="angular-status-confirm"]')).not.toBeNull();
+    expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-action="acknowledge"]')).not.toBeNull();
+    expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-count="1"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('确认选中的告警?');
+
+    await act(async () => {
+      (interactionContainer.querySelector(
+        'button[data-alert-center-batch-status-confirm-ok="angular-status-confirm"]'
+      ) as HTMLButtonElement | null)?.click();
+    });
+
+    await act(async () => {
+      unacknowledge?.click();
+    });
+    expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-action="unacknowledge"]')).not.toBeNull();
+
+    await act(async () => {
+      (interactionContainer.querySelector(
+        'button[data-alert-center-batch-status-confirm-ok="angular-status-confirm"]'
+      ) as HTMLButtonElement | null)?.click();
+    });
+
+    await act(async () => {
+      resolve?.click();
+    });
+    expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-action="resolve"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('请确认是否批量标记已处理!');
+
+    await act(async () => {
+      (interactionContainer.querySelector(
+        'button[data-alert-center-batch-status-confirm-ok="angular-status-confirm"]'
+      ) as HTMLButtonElement | null)?.click();
+    });
+
+    await act(async () => {
+      reopen?.click();
+    });
+    expect(interactionContainer.querySelector('[data-alert-center-batch-status-confirm-action="reopen"]')).not.toBeNull();
+    expect(interactionContainer.textContent).toContain('请确认是否批量标记未处理!');
+
+    await act(async () => {
+      (interactionContainer.querySelector(
+        'button[data-alert-center-batch-status-confirm-ok="angular-status-confirm"]'
+      ) as HTMLButtonElement | null)?.click();
+    });
+
+    await act(async () => {
+      silence?.click();
+    });
+
+    expect(onSelectedGroupIdsChange).toHaveBeenNthCalledWith(1, []);
+    expect(onSelectedGroupIdsChange).toHaveBeenNthCalledWith(2, [8, 9]);
+    expect(onClosureAction).toHaveBeenNthCalledWith(1, 'acknowledge', [7]);
+    expect(onClosureAction).toHaveBeenNthCalledWith(2, 'unacknowledge', [8]);
+    expect(onClosureAction).toHaveBeenNthCalledWith(3, 'resolve', [7]);
+    expect(onClosureAction).toHaveBeenNthCalledWith(4, 'reopen', [9]);
+    expect(interactionContainer.querySelector('[data-alert-rule-dialog="silence"]')).not.toBeNull();
+    expect(interactionContainer.querySelector('[data-alert-rule-dialog-submit="silence"]')).not.toBeNull();
+  });
+
+  it('submits selected alert quick-rule drafts through the alert center create callback', async () => {
+    const onRuleQuickCreate = vi.fn(async () => undefined);
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <AlertCenterSurface
+          t={zh}
+          data={{
+            summary,
+            groupAlerts: {
+              content: [
+                groupAlerts.content[0],
+                { ...groupAlerts.content[0], id: 8, groupLabels: { service: 'checkout', severity: 'warning' } }
+              ],
+              totalElements: 2,
+              pageIndex: 0,
+              pageSize: 8
+            }
+          } as any}
+          draft={{
+            search: '',
+            status: 'firing',
+            severity: '',
+            entityId: '42',
+            entityName: 'Checkout API',
+            returnTo: '/entities/42'
+          }}
+          selectedGroupIds={[7, 8]}
+          onSelectedGroupIdsChange={vi.fn()}
+          onDraftChange={vi.fn()}
+          onRefresh={vi.fn()}
+          onClearFilters={vi.fn()}
+          onClosureAction={vi.fn()}
+          onRuleQuickCreate={onRuleQuickCreate}
+        />
+      );
+      await Promise.resolve();
+    });
+
+    const silence = interactionContainer.querySelector(
+      'button[data-alert-center-batch-action="silence-selected"]'
+    ) as HTMLButtonElement | null;
+
+    await act(async () => {
+      silence?.click();
+      await Promise.resolve();
+    });
+
+    const silenceSubmit = interactionContainer.querySelector(
+      'button[data-alert-rule-dialog-submit="silence"]'
+    ) as HTMLButtonElement | null;
+    const silenceName = interactionContainer.querySelector('input[name="silence_name"]') as HTMLInputElement | null;
+
+    expect(silenceSubmit).not.toBeNull();
+    expect(silenceName?.value).toBe('Checkout API silence');
+
+    await act(async () => {
+      silenceSubmit?.click();
+      await Promise.resolve();
+    });
+
+    expect(onRuleQuickCreate).toHaveBeenCalledWith(
+      'silence',
+      expect.objectContaining({
+        name: 'Checkout API silence',
+        labelsText: 'service:checkout'
+      }),
+      2
+    );
+    expect(interactionContainer.querySelector('[data-alert-rule-dialog="silence"]')).toBeNull();
+
+    const inhibit = interactionContainer.querySelector(
+      'button[data-alert-center-batch-action="inhibit-selected"]'
+    ) as HTMLButtonElement | null;
+
+    await act(async () => {
+      inhibit?.click();
+      await Promise.resolve();
+    });
+
+    const inhibitSubmit = interactionContainer.querySelector(
+      'button[data-alert-rule-dialog-submit="inhibit"]'
+    ) as HTMLButtonElement | null;
+    const inhibitName = interactionContainer.querySelector('input[name="inhibit_name"]') as HTMLInputElement | null;
+
+    expect(inhibitSubmit).not.toBeNull();
+    expect(inhibitName?.value).toBe('Checkout API inhibit');
+
+    await act(async () => {
+      inhibitSubmit?.click();
+      await Promise.resolve();
+    });
+
+    expect(onRuleQuickCreate).toHaveBeenLastCalledWith(
+      'inhibit',
+      expect.objectContaining({
+        name: 'Checkout API inhibit',
+        sourceLabelsText: 'service:checkout',
+        targetLabelsText: 'service:checkout',
+        equalLabelsText: 'service'
+      }),
+      2
+    );
+  });
+
+  it('blocks invalid quick-rule drafts before invoking the create callback', async () => {
+    const onRuleQuickCreate = vi.fn(async () => undefined);
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(
+        <AlertCenterSurface
+          t={zh}
+          data={{ summary, groupAlerts } as any}
+          draft={{
+            search: '',
+            status: 'firing',
+            severity: '',
+            entityId: '42',
+            entityName: 'Checkout API',
+            returnTo: '/entities/42'
+          }}
+          initialDialogState={{ groupKey: '7', mode: 'silence' }}
+          onDraftChange={vi.fn()}
+          onRefresh={vi.fn()}
+          onClearFilters={vi.fn()}
+          onClosureAction={vi.fn()}
+          onRuleQuickCreate={onRuleQuickCreate}
+        />
+      );
+      await Promise.resolve();
+    });
+
+    const nameInput = interactionContainer.querySelector('input[name="silence_name"]') as HTMLInputElement | null;
+    const submit = interactionContainer.querySelector('button[data-alert-rule-dialog-submit="silence"]') as HTMLButtonElement | null;
+    const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
+
+    await act(async () => {
+      valueSetter?.call(nameInput, '');
+      nameInput?.dispatchEvent(new Event('input', { bubbles: true }));
+      submit?.click();
+      await Promise.resolve();
+    });
+
+    expect(onRuleQuickCreate).not.toHaveBeenCalled();
+    expect(interactionContainer.querySelector('[data-alert-rule-dialog="silence"]')).not.toBeNull();
+    expect(interactionContainer.querySelector('[data-alert-rule-submit-error="true"]')?.textContent).toContain('规则名称为必填项');
+  });
+
   it('renders cold entity context and noise-control summary shells', () => {
     const html = renderSurface({
       data: {
@@ -311,6 +853,61 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('Open matching inhibit rules');
   });
 
+  it('marks create-capable noise-control action labels when possible suppression has no matching rules', () => {
+    const html = renderSurface({
+      data: {
+        summary,
+        groupAlerts,
+        noiseControlSummary: {
+          activeSilenceCount: 0,
+          matchingInhibitCount: 0,
+          possibleAlertSuppression: true,
+          activeSilences: [],
+          matchingInhibits: []
+        }
+      } as any,
+      draft: {
+        search: '',
+        status: 'firing',
+        severity: '',
+        entityId: '42',
+        entityName: 'Checkout API',
+        returnTo: '/entities/42',
+        returnLabel: 'Checkout'
+      }
+    }, t);
+
+    expect(html).toContain('data-alert-noise-control-action="silence"');
+    expect(html).toContain('data-alert-noise-control-action="inhibit"');
+    expect(html).toContain('data-alert-noise-control-action-label="angular-view-or-create"');
+    expect(html).toContain('View or create silence rules');
+    expect(html).toContain('View or create inhibit rules');
+  });
+
+  it('appends Angular response-result params to the entity return link after operations', () => {
+    const html = renderSurface({
+      draft: {
+        search: '',
+        status: 'firing',
+        severity: '',
+        entityId: '42',
+        entityName: 'Checkout API',
+        returnTo: '/entities/42?tab=alerts#noise'
+      },
+      entityResponseResult: { action: 'silence', count: 2 }
+    }, zh);
+    const source = readFileSync(resolve(process.cwd(), 'components/pages/alert-center-surface.tsx'), 'utf8');
+
+    expect(html).toContain('data-alert-entity-context="cold-context-panel"');
+    expect(html).toContain(
+      'href="/entities/42?tab=alerts&amp;responseResultKind=alerts&amp;responseResultAction=silence&amp;responseResultCount=2#noise"'
+    );
+    expect(source).toContain('buildEntityReturnHrefWithResponseResult');
+    expect(source).toContain("params.set('responseResultKind', 'alerts')");
+    expect(source).toContain("params.set('responseResultAction', result.action)");
+    expect(source).toContain("params.set('responseResultCount', String(result.count))");
+  });
+
   it('renders topology alert-impact context with the selected edge and return path', () => {
     const returnTo =
       '/topology?viewMode=resource-dependency&sourceKind=database-middleware-connection&edgeId=svc-checkout--res-orders-db&environment=prod&timeRange=last-1h';
@@ -337,6 +934,7 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-alert-topology-context="impact-filter-panel"');
     expect(html).toContain('拓扑影响面');
     expect(html).toContain('checkout-api');
+    expect(html).toContain('资源依赖');
     expect(html).toContain('resource-dependency');
     expect(html).toContain('数据库 / 中间件连接');
     expect(html).toContain('svc-checkout--res-orders-db');
@@ -346,6 +944,56 @@ describe('AlertCenterSurface', () => {
     expect(html).not.toContain('HertzBeat 企业运维拓扑');
   });
 
+  it('renders unknown topology source kind as a localized fallback while preserving the route slug', () => {
+    const html = renderSurface({
+      draft: {
+        search: 'checkout-api',
+        status: 'firing',
+        severity: '',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout-api',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'topology',
+        viewMode: 'alert-impact',
+        sourceKind: 'custom-edge-source',
+        returnTo: '/topology?sourceKind=custom-edge-source'
+      }
+    }, zh);
+
+    expect(html).toContain('未知关系来源 custom-edge-source');
+    expect(html).toContain('data-alert-topology-source-kind="custom-edge-source"');
+    expect(html).toContain('href="/topology?sourceKind=custom-edge-source"');
+    expect(html).not.toContain('>custom-edge-source</span>');
+  });
+
+  it('renders unknown topology view mode as a localized fallback while preserving the route slug', () => {
+    const html = renderSurface({
+      draft: {
+        search: 'checkout-api',
+        status: 'firing',
+        severity: '',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout-api',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        source: 'topology',
+        viewMode: 'custom-impact-view',
+        sourceKind: 'alert-impact',
+        returnTo: '/topology?viewMode=custom-impact-view'
+      }
+    }, zh);
+
+    expect(html).toContain('未知拓扑视图 custom-impact-view');
+    expect(html).toContain('data-alert-topology-view-mode="custom-impact-view"');
+    expect(html).toContain('href="/topology?viewMode=custom-impact-view"');
+    expect(html).not.toContain('>custom-impact-view</span>');
+  });
+
   it('renders the OTLP alert evidence closure panel with signal evidence and operation entries', () => {
     const html = renderSurface({
       draft: {
@@ -441,11 +1089,11 @@ describe('AlertCenterSurface', () => {
     expect(html).toContain('data-alert-evidence-context-row="time"');
     expect(html).toContain('时间范围');
     expect(html).toContain('last-45m');
-    expect(html).toContain('2024/04/16 00:53:20 → 2024/04/16 01:38:20 · 刷新 30s · 已暂停 · Asia/Shanghai');
+    expect(html).toContain('2024/04/16 00:53:20 → 2024/04/16 01:38:20 · 已暂停 · Asia/Shanghai');
     expect(html).toContain('data-alert-evidence-context-row="monitor"');
     expect(html).toContain('监控实例');
     expect(html).toContain('checkout-http');
-    expect(html).toContain('website · example.com:443 · monitorId 632051474676992');
+    expect(html).toContain('website · example.com:443 · 监控 ID 632051474676992');
     expect(html).toContain('data-alert-evidence-context-row="source"');
     expect(html).toContain('采集来源');
     expect(html).toContain('传统监控');
@@ -620,6 +1268,111 @@ describe('AlertCenterSurface', () => {
     expect(onClosureAction).toHaveBeenNthCalledWith(2, 'recover', 7);
   });
 
+  it('keeps resolve available on acknowledged entity alert cards like Angular', async () => {
+    const onClosureAction = vi.fn();
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+    const acknowledgedGroupAlerts = {
+      ...groupAlerts,
+      content: [
+        {
+          ...groupAlerts.content[0],
+          id: 8,
+          status: 'acknowledged',
+          groupLabels: { service: 'checkout' },
+          commonLabels: { service: 'checkout' },
+          alerts: [
+            {
+              ...groupAlerts.content[0].alerts[0],
+              id: 801,
+              status: 'acknowledged',
+              content: 'checkout latency acknowledged'
+            }
+          ]
+        }
+      ]
+    };
+
+    await act(async () => {
+      interactionRoot?.render(
+        <AlertCenterSurface
+          t={zh}
+          data={{ summary, groupAlerts: acknowledgedGroupAlerts } as any}
+          draft={{ search: '', status: 'acknowledged', severity: '', entityId: '42', entityName: 'Checkout API' }}
... 11505 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 33/50: feat(web-next): expose source-backed topology node legend

Posted by zh...@apache.org.
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 0e284d69c30ef4b59e15067c3f9086ecedd8d2f9
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 21:58:31 2026 +0800

    feat(web-next): expose source-backed topology node legend
---
 web-next/app/topology/page.test.tsx               | 54 ++++++++++++++---------
 web-next/app/topology/topology-page.tsx           | 23 +++++++++-
 web-next/app/ui-lab/page.test.tsx                 | 42 +++++++++++-------
 web-next/app/ui-lab/page.tsx                      | 24 +++++++++-
 web-next/lib/i18n-runtime-messages.ts             | 22 ++++++++-
 web-next/packages/hertzbeat-ui/src/index.test.tsx | 34 ++++++++++----
 web-next/packages/hertzbeat-ui/src/index.tsx      | 30 +++++++++++--
 web-next/test/i18n-test-helper.ts                 | 22 ++++++++-
 8 files changed, 193 insertions(+), 58 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 3b9495c28f..5f5f377db7 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -947,31 +947,41 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
     expect(html).toContain('data-hz-topology-legend-title-owner="hertzbeat-ui-legend-title"');
     expect(html).toContain('data-hz-topology-legend-summary-owner="hertzbeat-ui-legend-summary"');
-    expect(html).toContain('data-hz-topology-legend-section="status"');
-    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
-    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
-    expect(html).toContain('data-hz-topology-legend-section="interaction"');
-    expect(html).not.toContain('data-hz-topology-legend-section="source-kind"');
-    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
-    expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
-    expect(html).toContain('data-hz-topology-legend-item="warning-node"');
-    expect(html).toContain('data-hz-topology-legend-item="critical-node"');
+	    expect(html).toContain('data-hz-topology-legend-section="status"');
+	    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
+	    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
+	    expect(html).toContain('data-hz-topology-legend-section="node-type"');
+	    expect(html).toContain('data-hz-topology-legend-section="interaction"');
+	    expect(html).not.toContain('data-hz-topology-legend-section="source-kind"');
+	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
+	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
+	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="blocks"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
+	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+	    expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
+	    expect(html).toContain('data-hz-topology-legend-item="warning-node"');
+	    expect(html).toContain('data-hz-topology-legend-item="critical-node"');
     expect(html).toContain('data-hz-topology-legend-item="selected-node"');
     expect(html).toContain('data-hz-topology-legend-item="directional-edge"');
     expect(html).toContain('data-hz-topology-legend-item="dimmed-edge"');
-    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
-    expect(html).toContain('data-hz-topology-legend-color="#f59e0b"');
-    expect(html).toContain('data-hz-topology-legend-color="#ef4444"');
-    expect(html).toContain('data-hz-topology-legend-swatch-shape="line"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
-    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
-    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
-    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
-    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
-    expect(html).toContain('data-hz-topology-legend-swatch-owner="hertzbeat-ui-legend-swatch"');
-    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
+	    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
+	    expect(html).toContain('data-hz-topology-legend-color="#f59e0b"');
+	    expect(html).toContain('data-hz-topology-legend-color="#ef4444"');
+	    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+	    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
+	    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
+	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
+	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
+	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
+	    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
+	    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
     expect(html).toContain('data-hz-topology-legend-item-value-owner="hertzbeat-ui-legend-item-value"');
     expect(html).not.toContain('data-hz-topology-legend-pattern="dashed"');
     expect(html).toContain('data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"');
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index 5dd9ec494b..f40c4c4d33 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -42,6 +42,7 @@ import {
 } from '@hertzbeat/ui';
 import {
   HzTopologyG6Canvas,
+  HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6RenderWindow,
   type HzTopologyG6GraphInput,
@@ -1122,8 +1123,28 @@ export default function TopologyPage({
     : topologyEmptyStateKind === 'degraded'
       ? t('topology.degraded.api.source')
       : 'API';
+  const topologyLegendNodeTypeItems = React.useMemo(
+    () =>
+      HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => icon.kind !== 'unknown').map(icon => ({
+        id: `node-type-${icon.kind}`,
+        label: t(`topology.legend.node-type.${icon.kind}`),
+        value: icon.iconName,
+        iconSrc: icon.iconSrc,
+        iconAlt: '',
+        iconLibrary: icon.iconLibrary,
+        iconName: icon.iconName,
+        iconSource: icon.iconSource,
+        visualSource: 'lucide-react' as const
+      })),
+    [t]
+  );
   const topologyLegendSections = React.useMemo(
     () => [
+      {
+        id: 'node-type',
+        label: t('topology.legend.node-type'),
+        items: topologyLegendNodeTypeItems
+      },
       {
         id: 'status',
         label: t('topology.legend.status'),
@@ -1181,7 +1202,7 @@ export default function TopologyPage({
         ]
       }
     ],
-    [t]
+    [t, topologyLegendNodeTypeItems]
   );
   const topologyEdgeIds = React.useMemo(
     () => new Set(map.edges.map(edge => edge.id)),
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index 0297d90fcf..6b600075e3 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2688,23 +2688,31 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-primitive="legend"');
     expect(html).toContain('data-hz-topology-legend-boundary="flush"');
     expect(html).toContain('data-hz-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"');
-    expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
-    expect(html).toContain('data-hz-topology-legend-section="status"');
-    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
-    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
-    expect(html).toContain('data-hz-topology-legend-section="interaction"');
-    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
-    expect(html).toContain('data-hz-topology-legend-swatch-shape="line"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
-    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
-    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
-    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
-    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
-    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
-    expect(html).toContain('data-hz-topology-legend-swatch-owner="hertzbeat-ui-legend-swatch"');
-    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
+	    expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
+	    expect(html).toContain('data-hz-topology-legend-section="status"');
+	    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
+	    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
+	    expect(html).toContain('data-hz-topology-legend-section="node-type"');
+	    expect(html).toContain('data-hz-topology-legend-section="interaction"');
+	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
+	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
+	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
+	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
+	    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+	    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
+	    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
+	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
+	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
+	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
+	    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
+	    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
+	    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
     expect(html).not.toContain('data-hz-topology-legend-pattern="dashed"');
     expect(html).toContain('data-hz-ui-lab-topology-detail-drawer="shared"');
     expect(html).toContain('data-hz-ui="topology-detail-drawer"');
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
index f49f175e4c..6d4aa16b02 100644
--- a/web-next/app/ui-lab/page.tsx
+++ b/web-next/app/ui-lab/page.tsx
@@ -204,6 +204,7 @@ import {
 } from '@hertzbeat/ui';
 import {
   HzTopologyG6Canvas,
+  HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6RenderWindow,
   buildHzTopologyG6ScaleFixture,
@@ -494,6 +495,17 @@ const topologyG6LargeGraphStrategy500 = buildHzTopologyG6LargeGraphStrategy(topo
 const topologyG6ScaleLabRenderWindow500 = buildHzTopologyG6RenderWindow(topologyG6ScaleLabGraph500, topologyG6LargeGraphStrategy500, {
   priorityNodeIds: ['scale-svc-420']
 });
+const topologyG6NodeTypeLegendItems = HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => icon.kind !== 'unknown').map(icon => ({
+  id: `node-type-${icon.kind}`,
+  label: icon.label,
+  value: icon.iconName,
+  iconSrc: icon.iconSrc,
+  iconAlt: '',
+  iconLibrary: icon.iconLibrary,
+  iconName: icon.iconName,
+  iconSource: icon.iconSource,
+  visualSource: 'lucide-react' as const
+}));
 
 const monitorRows = [
   { name: 'mysql-prod-01', app: 'MySQL', collector: 'collector-a', signal: 'metrics', status: '可用', latency: '38 ms', tone: 'success' as const },
@@ -7000,6 +7012,11 @@ export default function HertzBeatUiLabPage() {
                   boundary="flush"
                   density="canvas-dock"
                   sections={[
+                        {
+                          id: 'node-type',
+                          label: 'Node type',
+                          items: topologyG6NodeTypeLegendItems
+                        },
                         {
                           id: 'status',
                           label: 'Status',
@@ -7262,9 +7279,14 @@ export default function HertzBeatUiLabPage() {
                   <HzTopologyLegend
                     data-hz-ui-lab-topology-legend="shared"
                     title="Topology legend"
-                    summaryLabel="2 groups"
+                    summaryLabel="3 groups"
                     boundary="flush"
                     sections={[
+                      {
+                        id: 'node-type',
+                        label: 'Node type',
+                        items: topologyG6NodeTypeLegendItems
+                      },
                       {
                         id: 'status',
                         label: 'Status',
diff --git a/web-next/lib/i18n-runtime-messages.ts b/web-next/lib/i18n-runtime-messages.ts
index 84396b91be..74fe659faf 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -984,7 +984,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.alert-impact.open': 'Open alert impact',
     'topology.alert-impact.copy': 'Keep the selected edge, entity, and signal scope.',
     'topology.legend.title': 'Legend',
-    'topology.legend.summary': '2 groups',
+    'topology.legend.summary': '3 groups',
     'topology.legend.health': 'Health',
     'topology.legend.health.healthy': 'Healthy',
     'topology.legend.health.healthy-value': 'good',
@@ -1016,6 +1016,15 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.legend.confidence': 'Confidence',
     'topology.legend.confidence.live': 'Current evidence',
     'topology.legend.confidence.stale': 'Low confidence / stale',
+    'topology.legend.node-type': 'Node type',
+    'topology.legend.node-type.application': 'Application',
+    'topology.legend.node-type.service': 'Service',
+    'topology.legend.node-type.database': 'Database',
+    'topology.legend.node-type.middleware': 'Middleware',
+    'topology.legend.node-type.k8s-workload': 'Workload',
+    'topology.legend.node-type.monitor': 'Monitor',
+    'topology.legend.node-type.resource': 'Resource',
+    'topology.legend.node-type.alert': 'Alert',
     'topology.timeline.title': 'Impact timeline',
     'topology.timeline.copy': 'Recent entity, relationship, and monitor evidence that changed this topology view.',
     'topology.timeline.detail.fallback': 'Topology evidence changed',
@@ -4492,7 +4501,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.alert-impact.open': '打开告警影响面',
     'topology.alert-impact.copy': '保留当前边、实体和三信号范围。',
     'topology.legend.title': '图例',
-    'topology.legend.summary': '2 组',
+    'topology.legend.summary': '3 组',
     'topology.legend.health': '健康状态',
     'topology.legend.health.healthy': '健康',
     'topology.legend.health.healthy-value': '正常',
@@ -4524,6 +4533,15 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.legend.confidence': '可信度',
     'topology.legend.confidence.live': '当前窗口证据',
     'topology.legend.confidence.stale': '低可信 / 过期',
+    'topology.legend.node-type': '节点类型',
+    'topology.legend.node-type.application': '应用',
+    'topology.legend.node-type.service': '服务',
+    'topology.legend.node-type.database': '数据库',
+    'topology.legend.node-type.middleware': '中间件',
+    'topology.legend.node-type.k8s-workload': '工作负载',
+    'topology.legend.node-type.monitor': '监控',
+    'topology.legend.node-type.resource': '资源',
+    'topology.legend.node-type.alert': '告警',
     'topology.timeline.title': '影响时间线',
     'topology.timeline.copy': '最近改变当前拓扑视图的实体、关系和监控对象证据。',
     'topology.timeline.detail.fallback': '拓扑证据已变更',
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index a36e11afc9..e045fa1fc7 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -5202,11 +5202,20 @@ describe('@hertzbeat/ui', () => {
           {
             id: 'status',
             label: 'Status',
-            items: [
-              { id: 'healthy-node', label: 'Healthy node', color: '#22c55e', visualSource: 'hertzbeat-status-token' },
-              { id: 'critical-node', label: 'Critical node', color: '#ef4444', visualSource: 'hertzbeat-status-token' }
-            ]
-          },
+	            items: [
+	              {
+	                id: 'service-node',
+	                label: 'Service node',
+	                iconSrc: 'data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%3E%3C/svg%3E',
+	                iconLibrary: 'lucide-react',
+	                iconName: 'blocks',
+	                iconSource: 'entity-type-catalog',
+	                visualSource: 'lucide-react'
+	              },
+	              { id: 'healthy-node', label: 'Healthy node', color: '#22c55e', visualSource: 'hertzbeat-status-token' },
+	              { id: 'critical-node', label: 'Critical node', color: '#ef4444', visualSource: 'hertzbeat-status-token' }
+	            ]
+	          },
           {
             id: 'interaction',
             label: 'Interaction',
@@ -5227,10 +5236,17 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-legend-summary-visibility="hidden"');
     expect(html).toContain('data-hz-topology-legend-section="status"');
     expect(html).toContain('data-hz-topology-legend-section="interaction"');
-    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
-    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+	    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
+	    expect(html).toContain('data-hz-topology-legend-source-label="lucide-react"');
+	    expect(html).toContain('data-hz-topology-legend-icon-owner="hertzbeat-ui-legend-source-icon"');
+	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="blocks"');
+	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
+	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
     expect(html).toContain('data-hz-topology-legend-source-label="status token"');
     expect(html).toContain('data-hz-topology-legend-source-label="interaction token"');
     expect(html).toContain('data-hz-topology-legend-source-label="edge token"');
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index b3f3fd412a..f9cfcc8d04 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -10493,7 +10493,12 @@ export type HzTopologyLegendItem = {
   color?: string;
   fill?: string;
   swatch?: 'line';
-  visualSource?: 'hertzbeat-status-token' | 'hertzbeat-interaction-token' | 'hertzbeat-edge-token';
+  visualSource?: 'hertzbeat-status-token' | 'hertzbeat-interaction-token' | 'hertzbeat-edge-token' | 'lucide-react';
+  iconSrc?: string;
+  iconAlt?: string;
+  iconLibrary?: 'lucide-react';
+  iconName?: string;
+  iconSource?: 'entity-type-catalog';
 };
 
 export type HzTopologyLegendSection = {
@@ -10523,7 +10528,8 @@ const topologyLegendBoundaryClassName: Record<HzTopologyLegendBoundary, string>
 const topologyLegendVisualSourceLabel: Record<NonNullable<HzTopologyLegendItem['visualSource']>, string> = {
   'hertzbeat-status-token': 'status token',
   'hertzbeat-interaction-token': 'interaction token',
-  'hertzbeat-edge-token': 'edge token'
+  'hertzbeat-edge-token': 'edge token',
+  'lucide-react': 'lucide-react'
 };
 
 export type HzTopologyHoverTooltipKind = 'node' | 'edge';
@@ -11288,8 +11294,8 @@ export function HzTopologyLegend({
                   <div
                     key={item.id}
                     className={cn(
-                      'grid min-w-0 items-center text-[11px]',
-                      isCanvasDock ? 'min-h-4 grid-cols-[auto] gap-1' : 'min-h-5 grid-cols-[minmax(0,1fr)_auto] gap-2'
+                      'flex min-w-0 items-center text-[11px]',
+                      isCanvasDock ? 'min-h-4 gap-1' : 'min-h-5 gap-2'
                     )}
                     data-hz-topology-legend-item={item.id}
                     data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"
@@ -11302,6 +11308,22 @@ export function HzTopologyLegend({
                     data-hz-topology-legend-source-label={sourceLabel}
                     data-hz-topology-legend-no-handdrawn-icon="true"
                   >
+                    {item.iconSrc ? (
+                      <span
+                        aria-label={item.iconAlt}
+                        role={item.iconAlt ? 'img' : undefined}
+                        className={cn(
+                          'h-3.5 w-3.5 shrink-0 bg-contain bg-center bg-no-repeat opacity-80',
+                          isCanvasDock ? 'mr-1 inline-block' : 'mr-1.5 inline-block'
+                        )}
+                        style={{ backgroundImage: `url("${item.iconSrc}")` }}
+                        data-hz-topology-legend-icon-owner="hertzbeat-ui-legend-source-icon"
+                        data-hz-topology-legend-icon-library={item.iconLibrary}
+                        data-hz-topology-legend-icon-name={item.iconName}
+                        data-hz-topology-legend-icon-source={item.iconSource}
+                        data-hz-topology-legend-icon-no-handdrawn="true"
+                      />
+                    ) : null}
                     <span
                       className="min-w-0 truncate text-[#cbd3df]"
                       data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"
diff --git a/web-next/test/i18n-test-helper.ts b/web-next/test/i18n-test-helper.ts
index ed498db025..1f955f4fdd 100644
--- a/web-next/test/i18n-test-helper.ts
+++ b/web-next/test/i18n-test-helper.ts
@@ -585,7 +585,7 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.alert-impact.open': 'Open alert impact',
     'topology.alert-impact.copy': 'Keep the selected edge, entity, and signal scope.',
     'topology.legend.title': 'Legend',
-    'topology.legend.summary': '2 groups',
+    'topology.legend.summary': '3 groups',
     'topology.legend.health': 'Health',
     'topology.legend.health.healthy': 'Healthy',
     'topology.legend.health.healthy-value': 'good',
@@ -617,6 +617,15 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.legend.confidence': 'Confidence',
     'topology.legend.confidence.live': 'Current evidence',
     'topology.legend.confidence.stale': 'Low confidence / stale',
+    'topology.legend.node-type': 'Node type',
+    'topology.legend.node-type.application': 'Application',
+    'topology.legend.node-type.service': 'Service',
+    'topology.legend.node-type.database': 'Database',
+    'topology.legend.node-type.middleware': 'Middleware',
+    'topology.legend.node-type.k8s-workload': 'Workload',
+    'topology.legend.node-type.monitor': 'Monitor',
+    'topology.legend.node-type.resource': 'Resource',
+    'topology.legend.node-type.alert': 'Alert',
     'topology.timeline.title': 'Impact timeline',
     'topology.timeline.copy': 'Recent entity, relationship, and monitor evidence that changed this topology view.',
     'topology.timeline.detail.fallback': 'Topology evidence changed',
@@ -3366,7 +3375,7 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.alert-impact.open': '打开告警影响面',
     'topology.alert-impact.copy': '保留当前边、实体和三信号范围。',
     'topology.legend.title': '图例',
-    'topology.legend.summary': '2 组',
+    'topology.legend.summary': '3 组',
     'topology.legend.health': '健康状态',
     'topology.legend.health.healthy': '健康',
     'topology.legend.health.healthy-value': '正常',
@@ -3398,6 +3407,15 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.legend.confidence': '可信度',
     'topology.legend.confidence.live': '当前窗口证据',
     'topology.legend.confidence.stale': '低可信 / 过期',
+    'topology.legend.node-type': '节点类型',
+    'topology.legend.node-type.application': '应用',
+    'topology.legend.node-type.service': '服务',
+    'topology.legend.node-type.database': '数据库',
+    'topology.legend.node-type.middleware': '中间件',
+    'topology.legend.node-type.k8s-workload': '工作负载',
+    'topology.legend.node-type.monitor': '监控',
+    'topology.legend.node-type.resource': '资源',
+    'topology.legend.node-type.alert': '告警',
     'topology.timeline.title': '影响时间线',
     'topology.timeline.copy': '最近改变当前拓扑视图的实体、关系和监控对象证据。',
     'topology.timeline.detail.fallback': '拓扑证据已变更',


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 34/50: fix(web-next): scope topology legend to visible graph types

Posted by zh...@apache.org.
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 72ddbb80203eacec94cee63662773b5694ee23c4
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 22:08:23 2026 +0800

    fix(web-next): scope topology legend to visible graph types
---
 web-next/app/topology/page.test.tsx               |  2 ++
 web-next/app/topology/topology-page.tsx           | 24 ++++++++++++++++++-----
 web-next/app/ui-lab/page.test.tsx                 |  5 +++++
 web-next/app/ui-lab/page.tsx                      |  8 +++++++-
 web-next/packages/hertzbeat-ui/src/index.test.tsx | 23 ++++++++++++++++++++++
 web-next/packages/hertzbeat-ui/src/index.tsx      |  5 +++--
 6 files changed, 59 insertions(+), 8 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 5f5f377db7..33ffd36fbd 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -956,6 +956,8 @@ describe('topology page', () => {
 	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
 	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
 	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
+	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
 	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
 	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
 	    expect(html).toContain('data-hz-topology-legend-icon-name="blocks"');
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index f40c4c4d33..ea04862bef 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -45,6 +45,7 @@ import {
   HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6RenderWindow,
+  getHzTopologyG6NodeIcon,
   type HzTopologyG6GraphInput,
   type HzTopologyG6HoverAnchor
 } from '@hertzbeat/ui/topology-g6';
@@ -72,6 +73,13 @@ function findEdge(edges: TopologyServiceEdge[], id: string) {
   return edges.find(edge => edge.id === id);
 }
 
+function resolveTopologyG6EntityType(node: Pick<TopologyServiceNode, 'entityType' | 'id' | 'label' | 'source'>) {
+  const directIcon = getHzTopologyG6NodeIcon(node.entityType);
+  if (directIcon.kind !== 'unknown') return directIcon.kind;
+  const inferredIcon = getHzTopologyG6NodeIcon(`${node.label} ${node.source} ${node.id}`);
+  return inferredIcon.kind === 'unknown' ? node.entityType : inferredIcon.kind;
+}
+
 function formatTimeRange(timeRange: string, t: (key: string) => string) {
   if (timeRange === 'last-1h') return t('topology.time.last-1h');
   return timeRange;
@@ -738,7 +746,7 @@ export default function TopologyPage({
     nodes: topologyCanvasNodes.map(node => ({
       id: node.id,
       label: node.label,
-      entityType: node.entityType,
+      entityType: resolveTopologyG6EntityType(node),
       health: node.health,
       tone: node.tone,
       focus: node.focus,
@@ -1124,8 +1132,13 @@ export default function TopologyPage({
       ? t('topology.degraded.api.source')
       : 'API';
   const topologyLegendNodeTypeItems = React.useMemo(
-    () =>
-      HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => icon.kind !== 'unknown').map(icon => ({
+    () => {
+      const visibleNodeKinds = new Set(
+        topologyG6Graph.nodes
+          .map(node => getHzTopologyG6NodeIcon(node.entityType).kind)
+          .filter(kind => kind !== 'unknown')
+      );
+      return HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => visibleNodeKinds.has(icon.kind)).map(icon => ({
         id: `node-type-${icon.kind}`,
         label: t(`topology.legend.node-type.${icon.kind}`),
         value: icon.iconName,
@@ -1135,8 +1148,9 @@ export default function TopologyPage({
         iconName: icon.iconName,
         iconSource: icon.iconSource,
         visualSource: 'lucide-react' as const
-      })),
-    [t]
+      }));
+    },
+    [topologyG6Graph.nodes, t]
   );
   const topologyLegendSections = React.useMemo(
     () => [
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index 6b600075e3..73be41f88f 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2697,6 +2697,11 @@ describe('HertzBeat UI lab page', () => {
 	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
 	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
 	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+	    expect(html).toContain('data-hz-topology-legend-item="node-type-middleware"');
+	    expect(html).toContain('data-hz-topology-legend-item="node-type-monitor"');
+	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-application"');
+	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
+	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
 	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
 	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
 	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
index 6d4aa16b02..0693307083 100644
--- a/web-next/app/ui-lab/page.tsx
+++ b/web-next/app/ui-lab/page.tsx
@@ -209,6 +209,7 @@ import {
   buildHzTopologyG6RenderWindow,
   buildHzTopologyG6ScaleFixture,
   buildHzTopologyG6ScaleProfile,
+  getHzTopologyG6NodeIcon,
   type HzTopologyG6GraphInput
 } from '@hertzbeat/ui/topology-g6';
 import { AlertNoticeRuleSwitch } from '../../components/pages/alert-notice-rule-fields';
@@ -495,7 +496,12 @@ const topologyG6LargeGraphStrategy500 = buildHzTopologyG6LargeGraphStrategy(topo
 const topologyG6ScaleLabRenderWindow500 = buildHzTopologyG6RenderWindow(topologyG6ScaleLabGraph500, topologyG6LargeGraphStrategy500, {
   priorityNodeIds: ['scale-svc-420']
 });
-const topologyG6NodeTypeLegendItems = HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => icon.kind !== 'unknown').map(icon => ({
+const topologyG6LabVisibleNodeKinds = new Set(
+  topologyG6LabGraph.nodes.map(node => getHzTopologyG6NodeIcon(node.entityType).kind).filter(kind => kind !== 'unknown')
+);
+const topologyG6NodeTypeLegendItems = HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon =>
+  topologyG6LabVisibleNodeKinds.has(icon.kind)
+).map(icon => ({
   id: `node-type-${icon.kind}`,
   label: icon.label,
   value: icon.iconName,
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index e045fa1fc7..fa75a423af 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -5262,6 +5262,29 @@ describe('@hertzbeat/ui', () => {
     expect(html).not.toContain('2 groups');
   });
 
+  it('omits empty topology legend sections so graph-scoped legends do not advertise absent node types', () => {
+    const html = renderToStaticMarkup(
+      <HzTopologyLegend
+        title="Legend"
+        boundary="flush"
+        density="canvas-dock"
+        sections={[
+          { id: 'node-type', label: 'Node type', items: [] },
+          {
+            id: 'status',
+            label: 'Status',
+            items: [{ id: 'healthy-node', label: 'Healthy', value: 'healthy', visualSource: 'hertzbeat-status-token' }]
+          }
+        ]}
+      />
+    );
+
+    expect(html).not.toContain('data-hz-topology-legend-section="node-type"');
+    expect(html).not.toContain('Node type');
+    expect(html).toContain('data-hz-topology-legend-section="status"');
+    expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
+  });
+
   it('renders a topology detail drawer for edge evidence and cross-signal handoffs', () => {
     const html = renderToStaticMarkup(
       <HzTopologyDetailDrawer
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index f9cfcc8d04..9d2cf77344 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -11204,6 +11204,7 @@ export function HzTopologyLegend({
   ...props
 }: HzTopologyLegendProps) {
   const isCanvasDock = density === 'canvas-dock';
+  const visibleSections = sections.filter(section => section.items.length > 0);
 
   return (
     <section
@@ -11247,7 +11248,7 @@ export function HzTopologyLegend({
             className="font-mono text-[10px] uppercase tracking-[0.08em] text-[#727b8c]"
             data-hz-topology-legend-summary-owner="hertzbeat-ui-legend-summary"
           >
-            {summaryLabel ?? `${sections.length} groups`}
+            {summaryLabel ?? `${visibleSections.length} groups`}
           </span>
         ) : null}
       </header>
@@ -11257,7 +11258,7 @@ export function HzTopologyLegend({
           isCanvasDock ? 'flex flex-wrap gap-x-3 gap-y-1 px-2 py-1.5' : 'grid divide-y divide-[var(--hz-ui-line-faint)]'
         )}
       >
-        {sections.map(section => (
+        {visibleSections.map(section => (
           <div
             key={section.id}
             className={cn(


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 29/50: fix(web-next): align alert setting import lifecycle

Posted by zh...@apache.org.
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 43b8d665f307af5f86490d7c850533fe7266733d
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 15:52:23 2026 +0800

    fix(web-next): align alert setting import lifecycle
---
 web-next/app/alert/setting/alert-setting-page.tsx |  7 +++++++
 web-next/app/alert/setting/page.test.tsx          | 13 +++++++++++++
 web-next/app/ui-lab/page.test.tsx                 |  6 ++++++
 web-next/app/ui-lab/page.tsx                      |  7 +++++++
 4 files changed, 33 insertions(+)

diff --git a/web-next/app/alert/setting/alert-setting-page.tsx b/web-next/app/alert/setting/alert-setting-page.tsx
index 9f98ef86a4..cc144b59b8 100644
--- a/web-next/app/alert/setting/alert-setting-page.tsx
+++ b/web-next/app/alert/setting/alert-setting-page.tsx
@@ -323,6 +323,7 @@ export default function AlertSettingPage({ initialRouteState }: { initialRouteSt
   }
 
   function handleImportClick() {
+    if (pendingActionId) return;
     importInputRef.current?.click();
   }
 
@@ -330,6 +331,7 @@ export default function AlertSettingPage({ initialRouteState }: { initialRouteSt
     const file = event.target.files?.[0];
     event.target.value = '';
     if (!file) return;
+    if (pendingActionId) return;
     await runSettingAction(
       'import',
       t('common.notify.import-submitted', { taskName: file.name }),
@@ -424,8 +426,13 @@ export default function AlertSettingPage({ initialRouteState }: { initialRouteSt
             <HzFileInput
               ref={importInputRef}
               aria-label={t('alert.setting.import.input')}
+              multiple={false}
               data-alert-setting-import-input-owner="hertzbeat-ui-file-input"
               data-alert-setting-import-file-input="true"
+              data-alert-setting-import-upload-contract="angular-nz-upload-limit-one-no-list"
+              data-alert-setting-import-show-list="false"
+              data-alert-setting-import-refresh-contract="angular-success-refresh"
+              data-alert-setting-import-failure-refresh-contract="angular-failure-no-refresh"
               onChange={event => void handleImportChange(event)}
             />
             <AlertSettingCreateDialog
diff --git a/web-next/app/alert/setting/page.test.tsx b/web-next/app/alert/setting/page.test.tsx
index 5aa3ccf9ca..f4974b2d04 100644
--- a/web-next/app/alert/setting/page.test.tsx
+++ b/web-next/app/alert/setting/page.test.tsx
@@ -322,6 +322,19 @@ describe('alert setting page', () => {
     expect(source).not.toContain("return t('common.notify.import-success-detail'");
   });
 
+  it('keeps threshold import upload lifecycle on the Angular single-file reload contract', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
+
+    expect(source).toContain('if (pendingActionId) return;');
+    expect(source).toContain('multiple={false}');
+    expect(source).toContain('data-alert-setting-import-upload-contract="angular-nz-upload-limit-one-no-list"');
+    expect(source).toContain('data-alert-setting-import-show-list="false"');
+    expect(source).toContain('data-alert-setting-import-refresh-contract="angular-success-refresh"');
+    expect(source).toContain('data-alert-setting-import-failure-refresh-contract="angular-failure-no-refresh"');
+    expect(source).toContain('setRefreshKey(value => value + 1)');
+    expect(source).not.toContain('multiple={true}');
+  });
+
   it('keeps threshold export success silent while mapping failures to the Angular notify title', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/alert/setting/alert-setting-page.tsx'), 'utf8');
 
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index 27435228ff..0297d90fcf 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2963,6 +2963,9 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-alert-setting-import-trigger-owner="hertzbeat-ui-batch-toolbar"');
     expect(html).toContain('data-alert-setting-delete-trigger-owner="hertzbeat-ui-batch-toolbar"');
     expect(html).toContain('data-hz-ui-lab-alert-setting-import-feedback="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-import-lifecycle="angular-success-refresh-failure-stays"');
+    expect(html).toContain('data-alert-setting-import-refresh-contract="angular-success-refresh"');
+    expect(html).toContain('data-alert-setting-import-failure-refresh-contract="angular-failure-no-refresh"');
     expect(html).toContain('data-hz-ui-lab-alert-center-entity-batch="shared"');
     expect(html).toContain('data-alert-center-entity-batch-owner="hertzbeat-ui-batch-toolbar"');
     expect(html).toContain('data-alert-center-batch-toolbar="selected-entity-alerts"');
@@ -3052,7 +3055,10 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-alert-setting-save-feedback-title="common.notify.edit-fail"');
     expect(html).toContain('data-alert-setting-save-feedback-detail="backend-message"');
     expect(html).toContain('data-hz-ui-lab-alert-setting-import-input="shared"');
+    expect(html).toContain('data-hz-ui-lab-alert-setting-import-upload="angular-nz-upload-limit-one-no-list"');
     expect(html).toContain('data-alert-setting-import-input-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-alert-setting-import-upload-contract="angular-nz-upload-limit-one-no-list"');
+    expect(html).toContain('data-alert-setting-import-show-list="false"');
     expect(html).toContain('Import [alert-defines.json] submitted');
     expect(html).toContain('data-hz-ui-lab-alert-setting-import-success="angular-import-success-notification"');
     expect(html).toContain('data-alert-setting-import-success-owner="hertzbeat-ui-inline-feedback"');
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
index fc01c95888..f49f175e4c 100644
--- a/web-next/app/ui-lab/page.tsx
+++ b/web-next/app/ui-lab/page.tsx
@@ -8216,6 +8216,9 @@ export default function HertzBeatUiLabPage() {
             meta="Preparing threshold import"
             variant="embedded"
             data-hz-ui-lab-alert-setting-import-feedback="shared"
+            data-hz-ui-lab-alert-setting-import-lifecycle="angular-success-refresh-failure-stays"
+            data-alert-setting-import-refresh-contract="angular-success-refresh"
+            data-alert-setting-import-failure-refresh-contract="angular-failure-no-refresh"
             data-alert-setting-action-feedback-owner="hertzbeat-ui-inline-feedback"
           />
           <HzInlineFeedback
@@ -8311,8 +8314,12 @@ export default function HertzBeatUiLabPage() {
           />
           <HzFileInput
             aria-label="Import threshold rules file"
+            multiple={false}
             data-hz-ui-lab-alert-setting-import-input="shared"
+            data-hz-ui-lab-alert-setting-import-upload="angular-nz-upload-limit-one-no-list"
             data-alert-setting-import-input-owner="hertzbeat-ui-file-input"
+            data-alert-setting-import-upload-contract="angular-nz-upload-limit-one-no-list"
+            data-alert-setting-import-show-list="false"
           />
           <HzExportTypeDialog
             open={alertSettingExportDialogOpen}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 01/50: feat: add web-next observability migration

Posted by zh...@apache.org.
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 f6ca255355d91db7c6ed632808a8a3fbd8a0c132
Author: Logic <zq...@dromara.org>
AuthorDate: Mon May 4 00:40:12 2026 +0800

    feat: add web-next observability migration
---
 .github/workflows/frontend-build-test.yml          |   65 +-
 .gitignore                                         |   12 +
 hertzbeat-alerter/pom.xml                          |    5 +
 .../apache/hertzbeat/alert/AlerterWorkerPool.java  |  120 +-
 .../alert/calculate/AlarmCacheManager.java         |   10 +
 .../periodic/LogPeriodicAlertCalculator.java       |    7 +-
 .../periodic/PeriodicAlertRuleScheduler.java       |  181 +-
 .../periodic/TracePeriodicAlertCalculator.java     |  233 +
 .../calculate/realtime/window/AlarmEvaluator.java  |  106 +-
 .../alert/controller/AlertDefineController.java    |    2 +-
 .../alert/controller/AlertsController.java         |   23 +-
 .../alert/notice/AlertNoticeDispatch.java          |   34 +-
 .../hertzbeat/alert/reduce/AlarmCommonReduce.java  |   71 +-
 .../hertzbeat/alert/reduce/AlarmGroupReduce.java   |  233 +-
 .../hertzbeat/alert/reduce/AlarmInhibitReduce.java |  174 +-
 .../hertzbeat/alert/reduce/AlarmSilenceReduce.java |   44 +-
 .../hertzbeat/alert/service/AlertService.java      |   10 +-
 .../hertzbeat/alert/service/DataSourceService.java |   13 +-
 .../alert/service/impl/AlertDefineServiceImpl.java |   19 +-
 .../alert/service/impl/AlertServiceImpl.java       |   98 +-
 .../alert/service/impl/DataSourceServiceImpl.java  |   32 +-
 .../hertzbeat/alert/AlerterWorkerPoolTest.java     |  195 +-
 .../periodic/LogPeriodicAlertCalculatorTest.java   |   39 +-
 .../periodic/PeriodicAlertRuleSchedulerTest.java   |  257 +
 .../periodic/TracePeriodicAlertCalculatorTest.java |  229 +
 .../realtime/window/AlarmEvaluatorTest.java        |  175 +-
 .../controller/AlertDefineControllerTest.java      |   23 +
 .../alert/controller/AlertsControllerTest.java     |   30 +-
 .../alert/notice/AlertNoticeDispatchTest.java      |   45 +-
 .../alert/reduce/AlarmCommonReduceTest.java        |   89 +-
 .../alert/reduce/AlarmGroupReduceTest.java         |  134 +-
 .../alert/reduce/AlarmInhibitReduceTest.java       |  206 +-
 .../alert/reduce/AlarmSilenceReduceTest.java       |  104 +-
 .../alert/service/AlertDefineServiceTest.java      |   39 +
 .../hertzbeat/alert/service/AlertServiceTest.java  |   81 +-
 .../alert/service/DataSourceServiceTest.java       |   67 +-
 hertzbeat-common-core/pom.xml                      |    1 +
 .../hertzbeat/common/concurrent/AdmissionMode.java |   25 +-
 .../common/concurrent/BackgroundTaskExecutor.java  |   48 +-
 .../common/concurrent/ManagedExecutor.java         |   22 +-
 .../common/concurrent/ManagedExecutors.java        |  428 +
 .../common/config/BaseKafkaProperties.java         |    0
 .../common/config/VirtualThreadProperties.java     |  201 +
 .../common/constants/CommonConstants.java          |   34 +-
 .../common/constants/ConfigConstants.java          |    4 +-
 .../hertzbeat/common/entity/dto/sms/SmsConfig.java |    6 +-
 .../apache/hertzbeat/common/entity/job/Job.java    |   63 +-
 .../hertzbeat/common/entity/job/Metrics.java       |    0
 .../common/entity/job/RuntimeParamDefine.java      |   60 +-
 .../hertzbeat/common/entity/job/SshTunnel.java     |    4 +-
 .../common/entity/job/protocol/HttpProtocol.java   |   34 +-
 .../common/entity/job/protocol/JdbcProtocol.java   |   22 +-
 .../common/entity/job/protocol/KafkaProtocol.java  |   20 +-
 .../entity/job/protocol/MongodbProtocol.java       |   15 +-
 .../common/entity/job/protocol/MqttProtocol.java   |   48 +-
 .../entity/job/protocol/NebulaGraphProtocol.java   |   18 +-
 .../common/entity/job/protocol/NginxProtocol.java  |   17 +-
 .../common/entity/job/protocol/RedisProtocol.java  |   20 +-
 .../entity/job/protocol/RocketmqProtocol.java      |   17 +-
 .../common/entity/job/protocol/SshProtocol.java    |   30 +-
 .../hertzbeat/common/entity/log/LogEntry.java      |   14 +-
 .../common/entity/plugin/PluginConfig.java         |    9 +-
 .../common/entity/push/PushMetricsDto.java         |    0
 .../dto/binding/OtlpEntityBindingSummaryDto.java   |   69 +
 .../dto/binding/TelemetryBindingResult.java        |   29 +-
 .../dto/binding/TelemetryIdentitySnapshot.java     |   35 +-
 .../observability/dto/evidence/LogEvidence.java    |   66 +
 .../observability/dto/evidence/MetricEvidence.java |   72 +
 .../observability/dto/evidence/TraceEvidence.java  |   69 +
 .../dto/handoff/EntityResponseHandoffInfo.java     |   49 +-
 .../dto/handoff/EntityResponseHandoffsInfo.java    |   26 +-
 .../dto/ingestion/OtlpIngestionGuideDto.java       |   55 +-
 .../dto/ingestion/OtlpIngestionOverviewDto.java    |  100 +
 .../observability/dto/log/EntityLogQueryHint.java  |   36 +-
 .../dto/log/EntityLogSummaryInfo.java              |   28 +-
 .../dto/metrics/MetricCorrelationHint.java         |   34 +-
 .../dto/metrics/OtlpMetricsConsoleDto.java         |   86 +
 .../dto/trace/EntityTraceQueryHintDto.java         |   40 +-
 .../dto/trace/EntityTraceSummaryDto.java           |   22 +-
 .../observability/dto/trace/TraceDetailDto.java    |   51 +-
 .../observability/dto/trace/TraceListItemDto.java  |   33 +-
 .../observability/dto/trace/TraceOverviewDto.java  |   16 +-
 .../observability/dto/trace/TraceSpanEventDto.java |   17 +-
 .../observability/dto/trace/TraceSpanLinkDto.java  |   19 +-
 .../observability/dto/trace/TraceSpanNodeDto.java  |   68 +-
 .../observability/model/CodeNavigationHint.java    |   18 +-
 .../model/EntityCanonicalIdentityRegistry.java     |   77 +
 .../common/support/ResourceBundleUtf8Control.java  |    0
 .../exception/AlertExpressionException.java        |    2 +-
 .../exception/CommonDataQueueUnknownException.java |    2 +-
 .../common/support/exception/CommonException.java  |    0
 .../exception/ExpressionVisitorException.java      |    2 +-
 .../common/support/exception/IgnoreException.java  |    0
 .../support/exception/SendMessageException.java    |    0
 .../common/support/valid/SqlSecurityException.java |    0
 .../hertzbeat/common/util/ResourceBundleUtil.java  |    0
 .../hertzbeat/common/util/TimePeriodUtil.java      |    9 +
 .../common/concurrent/ManagedExecutorsTest.java    |  184 +
 .../entity/job/protocol/HttpProtocolTest.java      |  240 +
 .../entity/job/protocol/JdbcProtocolTest.java      |  185 +
 .../entity/job/protocol/KafkaProtocolTest.java     |  178 +
 .../entity/job/protocol/MongodbProtocolTest.java   |  168 +
 .../entity/job/protocol/MqttProtocolTest.java      |  322 +
 .../job/protocol/NebulaGraphProtocolTest.java      |  188 +
 .../entity/job/protocol/NginxProtocolTest.java     |  197 +
 .../entity/job/protocol/RedisProtocolTest.java     |  217 +
 .../entity/job/protocol/RocketmqProtocolTest.java  |  148 +
 .../entity/job/protocol/SshProtocolTest.java       |  243 +
 .../dto/EvidenceDtoMigrationTest.java              |   85 +
 .../dto/OtlpWorkspaceDtoMigrationTest.java         |  124 +
 .../observability/dto/TraceDtoMigrationTest.java   |   82 +
 .../model/EntityCanonicalIdentityRegistryTest.java |   36 +
 hertzbeat-common-spring/pom.xml                    |    1 +
 .../hertzbeat/common/config/CommonConfig.java      |    8 +-
 .../SmsConfigBinding.java}                         |   18 +-
 .../config/VirtualThreadPropertiesBinding.java     |  177 +
 .../common/entity/alerter/AlertDefine.java         |    7 +-
 .../hertzbeat/common/entity/job/SshTunnel.java     |   87 -
 .../hertzbeat/common/entity/manager/AuthToken.java |   94 +
 .../EntityCatalogContact.java}                     |   29 +-
 .../common/entity/manager/EntityCatalogLink.java   |   30 +-
 .../entity/manager/EntityDefinitionActivity.java   |   99 +
 .../entity/manager/EntityGovernanceState.java      |  114 +
 .../EntityIdentity.java}                           |   98 +-
 .../EntityMonitorBind.java}                        |   97 +-
 .../common/entity/manager/EntityOwnerRef.java      |   14 +-
 .../common/entity/manager/EntityRelation.java      |  126 +
 ...EntityCatalogContactListAttributeConverter.java |   33 +-
 ...sonEntityCatalogLinkListAttributeConverter.java |   33 +-
 .../JsonEntityOwnerRefListAttributeConverter.java  |   87 +
 .../entity/manager/JsonNodeAttributeConverter.java |   32 +-
 .../common/entity/manager/ObserveEntity.java       |  229 +
 .../observability/TelemetryIntakeSignalEvent.java  |   56 +-
 .../dto/entity/EntityAlertSummaryInfo.java         |   21 +-
 .../dto/entity/EntityEvidenceSummaryInfo.java      |   20 +-
 .../dto/entity/EntityMonitorSummaryInfo.java       |   18 +-
 .../dto/entity/EntityNextActionInfo.java           |   22 +-
 .../entity/EntityObservabilityDetailBundle.java    |   57 +
 .../dto/entity/EntityOpsSummaryInfo.java           |   24 +-
 .../observability/dto/entity/EntityStatusInfo.java |   25 +-
 .../dto/entity/EntityStatusPageSummaryInfo.java    |   20 +-
 .../dto/entity/EntityTriageRecommendation.java     |   22 +-
 .../dto/entity/EntityUnifiedEvidenceSummary.java   |   25 +-
 .../observability/dto/entity/MonitorInfo.java      |  134 +
 .../dto/handoff/EntityResponseHandoffsRequest.java |   79 +
 .../gateway/EntityObservabilityGateway.java        |  134 +
 .../gateway/ObservabilityAccessTokenGateway.java   |   41 +-
 .../gateway/ObservabilitySignalIntakeGateway.java  |   74 +
 .../ObservabilityWorkspaceQueryGateway.java        |   52 +
 .../gateway/TelemetryEvidenceGateway.java          |   83 +
 .../observability/model/ObservedEntityContext.java |   45 +
 .../hertzbeat/common/support/CommonThreadPool.java |   73 +-
 .../apache/hertzbeat/common/util/IpDomainUtil.java |  169 -
 .../common/config/SmsConfigBindingTest.java        |   50 +
 .../common/config/VirtualThreadPropertiesTest.java |  114 +
 ...onEntityOwnerRefListAttributeConverterTest.java |   65 +
 .../EntityObservabilityDtoMigrationTest.java       |   64 +
 .../model/ObservedEntityContextTest.java           |   55 +
 .../common/support/CommonThreadPoolTest.java       |  124 +-
 .../hertzbeat-collector-basic-e2e/pom.xml          |    4 +-
 .../hertzbeat-collector-common-e2e/pom.xml         |    6 +-
 .../hertzbeat-collector-kafka-e2e/pom.xml          |    4 +-
 .../pom.xml                                        |   86 +-
 .../mysql/AbstractMysqlR2dbcCollectE2eTest.java    |  233 +
 .../MysqlR2dbcCollectCompatibilityE2eTest.java     |   61 +
 .../collect/mysql/MysqlR2dbcCollectE2eTest.java    |   47 +-
 .../pom.xml                                        |    8 +-
 .../alert/LogPeriodicAlertE2eTest.java             |    4 +-
 .../alert/LogRealTimeAlertE2eTest.java             |    2 +-
 .../ingestion/LogIngestionE2eTest.java             |    4 +-
 .../storage/GreptimeLogStorageE2eTest.java         |    2 +-
 .../GreptimeOtlpLogPipelineProofE2eTest.java       |  218 +
 .../storage/GreptimeTraceApmFlowProofE2eTest.java  |  286 +
 .../src/test/resources/sureness.yml                |    4 +-
 .../src/test/resources/vector.yml                  |    0
 hertzbeat-e2e/pom.xml                              |    7 +-
 hertzbeat-manager/pom.xml                          |   23 +-
 .../component/sd/ServiceDiscoveryWorker.java       |    2 +-
 .../manager/component/status/CalculateStatus.java  |  440 +-
 .../component/validator/ParamValidator.java        |   10 +-
 .../component/validator/ParamValidatorManager.java |    6 +-
 .../validator/impl/ArrayParamValidator.java        |    6 +-
 .../validator/impl/BooleanParamValidator.java      |    6 +-
 .../validator/impl/HostParamValidatorAdapter.java  |    6 +-
 .../validator/impl/JsonParamValidator.java         |    6 +-
 .../validator/impl/NumberParamValidator.java       |    6 +-
 .../validator/impl/OptionParamValidator.java       |   10 +-
 .../validator/impl/PasswordParamValidator.java     |    6 +-
 .../validator/impl/TextParamValidator.java         |    6 +-
 .../config/ApiTokenValidationConfiguration.java    |   51 +-
 .../manager/config/ApiTokenValidationFilter.java   |  168 +
 .../manager/controller/AppController.java          |    4 +-
 .../manager/controller/AuthTokenController.java    |   62 +-
 .../manager/controller/CollectorController.java    |    4 +-
 .../manager/controller/EntityController.java       |  335 +
 .../manager/controller/MonitorsController.java     |   14 +-
 .../manager/controller/PluginController.java       |    2 +-
 .../manager/controller/StatusPageController.java   |   41 +-
 .../controller/StatusPagePublicController.java     |   12 +-
 .../dao/{CollectorDao.java => AuthTokenDao.java}   |   40 +-
 .../apache/hertzbeat/manager/dao/CollectorDao.java |    9 +-
 .../EntityDefinitionActivityDao.java}              |   28 +-
 ...ectorDao.java => EntityGovernanceStateDao.java} |   34 +-
 .../hertzbeat/manager/dao/EntityIdentityDao.java   |   49 +
 ...CollectorDao.java => EntityMonitorBindDao.java} |   43 +-
 .../hertzbeat/manager/dao/EntityRelationDao.java   |   45 +
 .../apache/hertzbeat/manager/dao/MonitorDao.java   |    2 +
 .../hertzbeat/manager/dao/ObserveEntityDao.java    |   31 +-
 .../ManagerObservabilityWorkspaceQueryGateway.java |  109 +
 .../hertzbeat/manager/pojo/dto/CollectorInfo.java  |   82 +
 .../manager/pojo}/dto/CollectorSummary.java        |   13 +-
 .../manager/pojo/dto/ComponentStatus.java          |   63 +-
 .../pojo/dto/EntityCatalogSuggestionsInfo.java     |   53 +
 .../manager/pojo/dto/EntityDefinition.java         |  227 +
 ...rsVO.java => EntityDefinitionActivityInfo.java} |   31 +-
 ...DefineDto.java => EntityDefinitionRequest.java} |   19 +-
 ... => EntityDefinitionWorkspaceActivityInfo.java} |   25 +-
 ...va => EntityDefinitionWorkspaceResumeInfo.java} |   36 +-
 ... => EntityDefinitionWorkspaceTemplateInfo.java} |   25 +-
 .../manager/pojo/dto/EntityDetailDto.java          |  124 +
 ... => EntityDiscoveryGovernanceActivityInfo.java} |   32 +-
 ...=> EntityDiscoveryGovernanceEntityRefInfo.java} |   10 +-
 ...va => EntityDiscoveryGovernancePresetInfo.java} |   29 +-
 .../hertzbeat/manager/pojo/dto/EntityDto.java      |   99 +
 .../hertzbeat/manager/pojo/dto/EntityInfo.java     |  222 +
 ...Dto.java => EntityMonitorBindingCandidate.java} |   20 +-
 ...{ParamDefineDto.java => EntitySummaryInfo.java} |   38 +-
 .../hertzbeat/manager/pojo/dto/MonitorDto.java     |   94 +-
 .../hertzbeat/manager/pojo/dto/MonitorParam.java   |   81 +
 .../hertzbeat/manager/pojo/dto/ParamDefineDto.java |    3 +-
 .../manager/pojo/dto/ParamDefineInfo.java          |  144 +
 .../manager/pojo/dto/PluginParametersVO.java       |    5 +-
 .../hertzbeat/manager/pojo}/dto/PluginUpload.java  |    5 +-
 .../manager/pojo/dto/StatusPageComponentInfo.java  |   95 +
 .../manager/pojo/dto/StatusPageHistoryInfo.java    |   94 +
 .../pojo/dto/StatusPageIncidentContentInfo.java    |   84 +
 .../manager/pojo/dto/StatusPageIncidentInfo.java   |  115 +
 .../manager/pojo/dto/StatusPageOrgInfo.java        |   99 +
 .../manager/scheduler/CollectorJobScheduler.java   |    4 +-
 .../manager/scheduler/ManagerWorkerPool.java       |   74 +-
 .../hertzbeat/manager/scheduler/SchedulerInit.java |   18 +-
 .../manager/scheduler/netty/ManageServer.java      |  145 +-
 .../CollectCyclicDataResponseProcessor.java        |    9 +-
 ...yclicServiceDiscoveryDataResponseProcessor.java |    9 +-
 .../hertzbeat/manager/service/AccountService.java  |   32 +-
 .../hertzbeat/manager/service/AppService.java      |   10 +-
 .../manager/service/CollectorService.java          |    6 +-
 .../manager/service/ObserveEntityService.java      |  105 +
 .../hertzbeat/manager/service/PluginService.java   |    2 +-
 .../manager/service/StatusPageService.java         |   26 +-
 .../manager/service/impl/AccountServiceImpl.java   |  231 +-
 .../manager/service/impl/AppServiceImpl.java       |   47 +-
 .../manager/service/impl/CollectorServiceImpl.java |   14 +-
 .../manager/service/impl/MonitorServiceImpl.java   |   30 +-
 .../service/impl/ObserveEntityServiceImpl.java     | 3765 ++++++++
 .../manager/service/impl/PluginServiceImpl.java    |   10 +-
 .../service/impl/StatusPageServiceImpl.java        |   66 +-
 .../src/main/resources/define/app-lmstudio.yml     |  202 +
 .../src/main/resources/define/app-ollama.yml       |  239 +
 .../component/status/CalculateStatusTest.java      |  148 +
 .../validator/ParamValidatorManagerTest.java       |   12 +-
 .../validator/impl/ArrayParamValidatorTest.java    |   32 +-
 .../validator/impl/BooleanParamValidatorTest.java  |   20 +-
 .../impl/HostParamValidatorAdapterTest.java        |   24 +-
 .../validator/impl/JsonParamValidatorTest.java     |   12 +-
 .../validator/impl/NumberParamValidatorTest.java   |   20 +-
 .../validator/impl/OptionParamValidatorTest.java   |   20 +-
 .../validator/impl/PasswordParamValidatorTest.java |   12 +-
 .../validator/impl/TextParamValidatorTest.java     |   12 +-
 .../config/ApiTokenValidationFilterTest.java       |  263 +
 .../manager/controller/AppControllerTest.java      |    6 +-
 .../controller/AuthTokenControllerTest.java        |  226 +
 .../controller/MetricsFavoriteControllerTest.java  |    4 +-
 .../manager/controller/MonitorsControllerTest.java |   29 +-
 .../controller/StatusPageControllerTest.java       |   45 +-
 .../controller/StatusPagePublicControllerTest.java |    4 +-
 ...agerObservabilityWorkspaceQueryGatewayTest.java |  124 +
 .../manager/scheduler/ManagerWorkerPoolTest.java   |  108 +
 .../manager/scheduler/netty/ManageServerTest.java  |  153 +
 .../manager/service/AccountServiceTest.java        |  428 +-
 .../hertzbeat/manager/service/AppServiceTest.java  |   11 +-
 .../manager/service/MonitorServiceTest.java        |  106 +-
 .../manager/service/ObserveEntityServiceTest.java  | 3351 +++++++
 .../manager/service/PluginServiceTest.java         |   36 +-
 .../manager/service/StatusPageServiceTest.java     |   29 +-
 hertzbeat-manager/src/test/resources/sureness.yml  |    5 +-
 .../pom.xml                                        |   86 +-
 .../observability/config/LogAutoConfiguration.java |   15 +-
 .../observability/config/OpenTelemetryConfig.java  |  140 +
 .../OpenTelemetryLogbackAppenderInstaller.java     |   54 +
 .../ingestion/adapter/LogProtocolAdapter.java      |   33 +-
 .../ingestion/adapter/OtlpLogProtocolAdapter.java  |  446 +
 .../ingestion/config/OtlpGrpcServerConfig.java     |  156 +
 .../controller/OtlpIngestionController.java        |   80 +
 .../ingestion/controller/OtlpLogController.java    |  195 +
 .../controller/OtlpSignalIngestionController.java  |   64 +
 .../ingestion/enricher/OtlpCorrelationContext.java |   20 +-
 .../enricher/OtlpCorrelationEnricher.java          |  359 +
 .../forwarder/GreptimeApmFlowInitializer.java      |  127 +
 .../forwarder/GreptimeLogPipelineInitializer.java  |  178 +
 .../ingestion/forwarder/GreptimeOtlpForwarder.java |  278 +
 .../ingestion/grpc/OtlpGrpcLogsService.java        |   46 +
 .../ingestion/grpc/OtlpGrpcMetricsService.java     |   47 +
 .../ingestion/grpc/OtlpGrpcTraceService.java       |   46 +
 .../service/OtlpGrpcIngestionService.java          |   46 +
 .../service/OtlpIngestionWorkspaceService.java     |   40 +
 .../service/impl/OtlpGrpcIngestionServiceImpl.java |  962 ++
 .../impl/OtlpIngestionWorkspaceServiceImpl.java    | 1631 ++++
 .../logs/controller/LogIngestionController.java    |   82 +
 .../logs/controller/LogManagerController.java      |   65 +
 .../logs/controller/LogQueryController.java        |  158 +
 .../logs/controller/LogSseController.java          |   61 +
 .../logs/service/LogManagementService.java         |   13 +-
 .../logs/service/LogQueryService.java              |   44 +
 .../observability/logs/service/LogSseService.java  |   17 +-
 .../service/impl/LogManagementServiceImpl.java     |   54 +
 .../logs/service/impl/LogQueryServiceImpl.java     |  360 +
 .../logs/service/impl/LogSseServiceImpl.java       |   44 +
 .../logs/sse/LogSseFilterCriteria.java             |  116 +
 .../observability/logs/sse/LogSseManager.java      |  209 +
 .../impl/EntityObservabilityGatewayImpl.java       | 1146 +++
 .../service/impl/TelemetryIntakeServiceImpl.java   | 1768 ++++
 .../traces/controller/TraceQueryController.java    |   97 +
 .../traces/service/EntityTraceQueryService.java    |   61 +
 .../service/impl/EntityTraceQueryServiceImpl.java  |  868 ++
 .../src/main/resources/META-INF/spring.factories   |   17 +
 .../greptime/flows/hertzbeat_apm_red_1m.sql        |   37 +
 .../greptime/pipelines/hertzbeat_otlp_log_v1.yaml  |   63 +
 .../config/LogAutoConfigurationTest.java           |   32 +-
 .../config/OpenTelemetryConfigTest.java            |   48 +
 .../adapter/OtlpLogProtocolAdapterTest.java        |  424 +
 .../ingestion/config/OtlpGrpcServerConfigTest.java |  159 +
 .../controller/OtlpIngestionControllerTest.java    |  184 +
 .../controller/OtlpLogControllerTest.java          |  243 +
 .../OtlpSignalIngestionControllerTest.java         |   82 +
 .../enricher/OtlpCorrelationEnricherTest.java      |  345 +
 .../forwarder/GreptimeApmFlowInitializerTest.java  |  164 +
 .../GreptimeLogPipelineInitializerTest.java        |  171 +
 .../forwarder/GreptimeOtlpForwarderTest.java       |  214 +
 .../impl/OtlpGrpcIngestionServiceImplTest.java     | 1047 ++
 .../OtlpIngestionWorkspaceServiceImplTest.java     | 1405 +++
 .../controller/LogIngestionControllerTest.java     |  117 +
 .../LogManagerControllerContextTest.java           |   43 +
 .../logs/controller/LogManagerControllerTest.java  |  103 +
 .../controller/LogQueryControllerContextTest.java  |   43 +
 .../logs/controller/LogQueryControllerTest.java    |  488 +
 .../logs/controller/LogSseControllerTest.java      |  123 +
 .../logs/sse/LogSseFilterCriteriaTest.java         |  227 +
 .../observability/logs/sse/LogSseManagerTest.java  |  224 +
 .../impl/EntityObservabilityGatewayImplTest.java   |  735 ++
 .../TelemetryIntakeServiceImplContextTest.java     |   44 +
 .../impl/TelemetryIntakeServiceImplTest.java       |  619 ++
 .../controller/TraceQueryControllerTest.java       |  120 +
 .../impl/EntityTraceQueryServiceImplTest.java      |  321 +
 hertzbeat-startup/pom.xml                          |   33 +
 .../org/apache/hertzbeat/startup/AsyncConfig.java  |   41 +
 .../HertzBeatLogCorrelationTurboFilter.java        |   47 +
 .../src/main/resources/application-local.yml       |   40 +
 .../src/main/resources/application-test.yml        |   36 +
 .../src/main/resources/application.yml             |   52 +
 .../h2/V200__create_entity_foundation.sql          |  177 +
 .../mysql/V200__create_entity_foundation.sql       |  160 +
 .../postgresql/V200__create_entity_foundation.sql  |  177 +
 .../src/main/resources/logback-spring.xml          |    7 +-
 hertzbeat-startup/src/main/resources/sureness.yml  |    8 +-
 .../apache/hertzbeat/startup/AsyncConfigTest.java  |   88 +
 .../startup/ReactorNettyCompatibilityTest.java     |   54 +
 .../StartupMysqlR2dbcCompatibilityTest.java        |  116 +
 .../HertzBeatLogCorrelationTurboFilterTest.java    |  110 +
 .../hertzbeat/warehouse/WarehouseWorkerPool.java   |   72 +-
 .../controller/MetricsDataController.java          |   11 +-
 .../warehouse/db/GreptimeSqlQueryExecutor.java     |   14 +-
 .../warehouse/db/PromqlQueryExecutor.java          |    6 +-
 .../GreptimeTtlApplicationReadyListener.java       |   90 +
 .../WareHouseApplicationReadyListener.java         |   15 +-
 .../repository/DelegatingLogQueryRepository.java   |   78 +
 .../repository/GreptimeTraceQueryRepository.java   |  169 +
 .../warehouse/repository/LogQueryRepository.java   |   62 +
 .../repository/MetricQueryRepository.java          |   61 +
 .../repository/PromqlMetricQueryRepository.java    |   79 +
 .../warehouse/repository/TraceQueryRepository.java |   58 +
 .../warehouse/service/MetricsDataService.java      |   22 +-
 .../service/impl/MetricsDataServiceImpl.java       |   67 +-
 .../warehouse/store/DataStorageDispatch.java       |   51 +-
 .../store/history/tsdb/HistoryDataReader.java      |  128 +-
 .../store/history/tsdb/doris/DorisDataStorage.java |    6 +-
 .../tsdb/duckdb/DuckdbDatabaseDataStorage.java     |  350 +-
 .../tsdb/greptime/GreptimeDbDataStorage.java       |  589 +-
 .../history/tsdb/greptime/GreptimeProperties.java  |    2 +-
 .../tsdb/vm/VictoriaMetricsClusterDataStorage.java |   91 +-
 .../tsdb/vm/VictoriaMetricsDataStorage.java        |   91 +-
 .../warehouse/WarehouseWorkerPoolTest.java         |   70 +-
 .../controller/MetricsDataControllerTest.java      |   12 +-
 .../db/GreptimePromqlQueryExecutorTest.java        |  105 +
 .../warehouse/db/GreptimeSqlQueryExecutorTest.java |   33 +
 .../GreptimeTtlApplicationReadyListenerTest.java   |   76 +
 ...eHouseApplicationReadyListenerContextTest.java} |   37 +-
 .../DelegatingLogQueryRepositoryTest.java          |  114 +
 .../GreptimeTraceQueryRepositoryTest.java          |  176 +
 .../PromqlMetricQueryRepositoryTest.java           |   71 +
 .../warehouse/service/MetricsDataServiceTest.java  |   26 +
 .../impl/MetricsDataServiceImplContextTest.java    |   43 +
 .../store/DataStorageDispatchContextTest.java      |   53 +
 .../history/tsdb/doris/DorisDataStorageTest.java   |    4 +-
 .../tsdb/duckdb/DuckdbDatabaseDataStorageTest.java |  224 +
 .../tsdb/greptime/GreptimeDbDataStorageTest.java   |  239 +-
 .../tsdb/vm/VictoriaMetricsDataStorageTest.java    |   66 +
 home/scripts/verify-roadmap-docs.mjs               |   99 +
 home/sidebars.json                                 |   38 +-
 .../docker-compose.hertzbeat.override.yml          |    9 +
 .../otel-demo/otelcol-config-extras.hertzbeat.yml  |   20 +
 .../trigger-recommendation-correlation.sh          |   47 +
 script/dev/run-official-otel-demo.sh               |  475 +
 script/dev/seed-otlp-three-signal-demo.sh          |  442 +
 script/dev/seed-trace-rich-demo.sh                 |  212 +
 script/dev/seed-verify-alert-notice.py             |  399 +
 script/dev/smoke-entity-alert-feedback.py          |  255 +
 script/dev/smoke-entity-ops-workspace.py           |  102 +
 script/dev/start-mixed-frontend.sh                 |   46 +
 script/dev/start-workspace-backend.sh              |   52 +
 script/dev/status-public-demo.py                   |  410 +
 .../docker-compose.yaml                            |    4 +-
 .../conf/sql/.gitkeep                              |    1 +
 .../dbdata/pgdata/.gitkeep                         |    1 +
 .../dbdata/tsdbdata/.gitkeep                       |    1 +
 .../docker-compose.yaml                            |   86 +-
 .../logs/hertzbeat/.gitkeep                        |    1 +
 .../nginx/default.conf                             |   38 +
 web-app/proxy.conf.json                            |  154 +-
 web-app/src/app/app.component.ts                   |    2 +-
 web-app/src/app/app.module.ts                      |   19 +-
 web-app/src/app/core/i18n/i18n.service.spec.ts     |    2 +-
 .../ops-workspace/ops-workspace.facade.spec.ts     |   70 +
 .../app/core/ops-workspace/ops-workspace.facade.ts |  174 +
 .../app/core/ops-workspace/ops-workspace.types.ts  |   75 +
 .../src/app/core/startup/startup.service.spec.ts   |  121 +
 web-app/src/app/core/startup/startup.service.ts    |  128 +-
 .../src/app/layout/basic/basic-layout.util.spec.ts |    7 +
 web-app/src/app/layout/basic/basic-layout.util.ts  |    3 +
 web-app/src/app/layout/basic/basic.component.less  |  516 +-
 .../src/app/layout/basic/basic.component.spec.ts   |  147 +
 web-app/src/app/layout/basic/basic.component.ts    |  121 +-
 .../setting-drawer-i18n.directive.spec.ts          |   12 +-
 .../directives/setting-drawer-i18n.directive.ts    |    2 +-
 .../layout/basic/widgets/chat-input.component.ts   |    9 +-
 .../layout/basic/widgets/fullscreen.component.ts   |    2 +-
 .../src/app/layout/basic/widgets/i18n.component.ts |    2 +-
 .../basic/widgets/main-nav.component.spec.ts       |  126 +
 .../app/layout/basic/widgets/main-nav.component.ts |  160 +
 .../app/layout/basic/widgets/notify.component.ts   |    2 +-
 .../basic/widgets/setup-guide.component.less       |  317 +
 .../basic/widgets/setup-guide.component.spec.ts    |   57 +
 .../layout/basic/widgets/setup-guide.component.ts  |  430 +
 .../src/app/layout/basic/widgets/user.component.ts |    6 +-
 web-app/src/app/layout/blank/blank.component.ts    |    2 +-
 web-app/src/app/layout/layout.module.ts            |   18 +-
 .../app/layout/passport/passport.component.less    |    2 +-
 .../src/app/layout/passport/passport.component.ts  |    2 +-
 web-app/src/app/pojo/AlertDefine.ts                |    2 +-
 web-app/src/app/pojo/Entity.ts                     |  105 +
 .../EntityCatalogSuggestions.ts}                   |   20 +-
 .../EntityDefinition.ts}                           |   20 +-
 .../EntityDefinitionWorkspaceActivity.ts}          |   34 +-
 .../EntityDefinitionWorkspaceResume.ts}            |   21 +-
 .../EntityDefinitionWorkspaceTemplate.ts}          |   27 +-
 web-app/src/app/pojo/EntityDetail.ts               |  325 +
 web-app/src/app/pojo/EntityDiscoveryGovernance.ts  |   65 +
 .../EntityIdentity.ts}                             |   18 +-
 .../EntityMonitorBind.ts}                          |   18 +-
 .../EntityMonitorBindingCandidate.ts}              |   17 +-
 .../EntityRelation.ts}                             |   22 +-
 .../EntityStatus.ts}                               |   18 +-
 .../timezone.pipe.ts => pojo/EntitySummary.ts}     |   36 +-
 web-app/src/app/pojo/LogEntry.ts                   |    2 +
 web-app/src/app/pojo/OtlpIngestion.ts              |  159 +
 web-app/src/app/pojo/Trace.ts                      |   94 +
 .../alert/alert-center/alert-center.component.html |  584 +-
 .../alert/alert-center/alert-center.component.less |  684 +-
 .../alert-center/alert-center.component.spec.ts    |  209 +-
 .../alert/alert-center/alert-center.component.ts   |  910 +-
 .../alert-group-converge.component.html            |   91 +-
 .../alert-group/alert-group-converge.component.ts  |    2 +-
 .../alert-inhibit/alert-inhibit.component.html     |   91 +-
 .../alert-inhibit/alert-inhibit.component.less     |  149 +-
 .../alert/alert-inhibit/alert-inhibit.component.ts |  336 +-
 .../alert-integration.component.html               |   92 +-
 .../alert-integration.component.less               |  164 +-
 .../alert-integration.component.spec.ts            |   41 +-
 .../alert-integration.component.ts                 |   44 +-
 .../alert-notice-receiver.component.ts             |    2 +-
 .../alert-notice-rule.component.ts                 |    2 +-
 .../alert-notice-template.component.less           |    2 +-
 .../alert-notice-template.component.ts             |    2 +-
 .../alert/alert-notice/alert-notice.component.html |   35 +-
 .../alert/alert-notice/alert-notice.component.ts   |    2 +-
 .../src/app/routes/alert/alert-routing.module.ts   |    4 +-
 .../alert-setting/alert-setting.component.html     |  218 +-
 .../alert-setting/alert-setting.component.less     |    3 +-
 .../alert-setting/alert-setting.component.spec.ts  |   91 +-
 .../alert/alert-setting/alert-setting.component.ts |  102 +-
 .../alert-silence/alert-silence.component.html     |   77 +-
 .../alert-silence/alert-silence.component.less     |  139 +
 .../alert/alert-silence/alert-silence.component.ts |  308 +-
 web-app/src/app/routes/alert/alert.module.ts       |    7 +-
 .../app/routes/bulletin/bulletin.component.html    |  282 -
 .../app/routes/bulletin/bulletin.component.less    |   17 -
 .../src/app/routes/bulletin/bulletin.component.ts  |  675 --
 .../routes/dashboard/dashboard-routing.module.ts   |   12 +
 .../app/routes/dashboard/dashboard.component.html  |  695 +-
 .../app/routes/dashboard/dashboard.component.less  |  932 +-
 .../routes/dashboard/dashboard.component.spec.ts   |  344 +
 .../app/routes/dashboard/dashboard.component.ts    | 1103 +--
 .../src/app/routes/dashboard/dashboard.module.ts   |   30 +
 .../entity/entity-definition-activity.util.ts      |   54 +
 .../entity/entity-definition-preview.util.ts       |  162 +
 .../entity/entity-definition-request.util.ts       |   86 +
 .../routes/entity/entity-definition-resume.util.ts |   86 +
 .../entity/entity-definition-template.util.ts      |   64 +
 .../entity-definition-workspace.component.html     |  783 ++
 .../entity-definition-workspace.component.less     |  730 ++
 .../entity-definition-workspace.component.spec.ts  |  193 +
 .../entity-definition-workspace.component.ts       | 3988 ++++++++
 .../entity-detail/entity-detail.component.html     | 1814 ++++
 .../entity-detail/entity-detail.component.less     | 2778 ++++++
 .../entity-detail/entity-detail.component.spec.ts  |  650 ++
 .../entity-detail/entity-detail.component.ts       | 6476 +++++++++++++
 .../entity/entity-discovery-activity.util.ts       |   65 +
 .../routes/entity/entity-discovery-preset.util.ts  |  115 +
 .../entity-discovery.component.html                | 1030 ++
 .../entity-discovery.component.less                |  680 ++
 .../entity-discovery.component.spec.ts             |  265 +
 .../entity-discovery/entity-discovery.component.ts | 3573 +++++++
 .../entity-editor/entity-editor.component.html     | 2140 +++++
 .../entity-editor/entity-editor.component.less     | 1738 ++++
 .../entity-editor/entity-editor.component.spec.ts  |  136 +
 .../entity-editor/entity-editor.component.ts       | 4844 ++++++++++
 .../routes/entity/entity-governance-recipe.util.ts |  207 +
 .../entity/entity-list/entity-list.component.html  |  688 ++
 .../entity/entity-list/entity-list.component.less  | 2463 +++++
 .../entity-list/entity-list.component.spec.ts      |  193 +
 .../entity/entity-list/entity-list.component.ts    | 3065 ++++++
 .../app/routes/entity/entity-log-handoff.util.ts   |  103 +
 .../app/routes/entity/entity-otel-identity.util.ts |  305 +
 .../routes/entity/entity-rail-label.util.spec.ts   |    9 +
 .../app/routes/entity/entity-rail-label.util.ts    |   10 +
 .../src/app/routes/entity/entity-routing.module.ts |   44 +
 .../monitor.module.ts => entity/entity.module.ts}  |   60 +-
 .../exception.component.spec.ts}                   |   22 +-
 .../app/routes/exception/exception.component.ts    |    9 +-
 .../src/app/routes/exception/exception.module.ts   |    3 +-
 .../ingestion-routing.module.ts}                   |   14 +-
 .../ingestion.module.ts}                           |   17 +-
 .../otlp-center/otlp-center.component.html         |  225 +
 .../otlp-center/otlp-center.component.less         |  652 ++
 .../otlp-center/otlp-center.component.spec.ts      |  303 +
 .../ingestion/otlp-center/otlp-center.component.ts |  596 ++
 .../otlp-metrics-console.component.html            |  160 +
 .../otlp-metrics-console.component.less            |  262 +
 .../otlp-metrics-console.component.spec.ts         |  177 +
 .../otlp-metrics-console.component.ts              |  411 +
 .../log-integration/log-integration.component.html |   85 -
 .../log-integration/log-integration.component.less |   84 -
 .../log-integration.component.spec.ts              |   41 -
 .../log-integration/log-integration.component.ts   |  152 -
 .../log/log-manage/log-manage.component.html       |  366 -
 .../log/log-manage/log-manage.component.less       |   82 -
 .../log/log-manage/log-manage.component.spec.ts    |   43 -
 .../routes/log/log-manage/log-manage.component.ts  |  571 --
 .../log/log-stream/log-stream.component.html       |  256 -
 .../log/log-stream/log-stream.component.less       |  437 -
 .../log/log-stream/log-stream.component.spec.ts    |   41 -
 .../routes/log/log-stream/log-stream.component.ts  |  457 -
 web-app/src/app/routes/log/log.module.ts           |   52 -
 .../monitor-data-chart.component.html              |   55 +-
 .../monitor-data-chart.component.less              |   95 +
 .../monitor-data-chart.component.spec.ts           |   88 +-
 .../monitor-data-chart.component.ts                |  101 +-
 .../monitor-data-chart.worker.ts                   |  335 +-
 .../monitor-data-table.component.html              |  307 +-
 .../monitor-data-table.component.less              |  322 +-
 .../monitor-data-table.component.spec.ts           |  177 +-
 .../monitor-data-table.component.ts                |  152 +-
 .../monitor-detail/monitor-detail.component.html   |  214 +-
 .../monitor-detail/monitor-detail.component.less   |  344 +-
 .../monitor-detail.component.spec.ts               |  600 +-
 .../monitor-detail/monitor-detail.component.ts     |  132 +-
 .../monitor-edit/monitor-edit.component.html       |   71 +-
 .../monitor-edit/monitor-edit.component.spec.ts    |  299 +-
 .../monitor/monitor-edit/monitor-edit.component.ts |  119 +-
 .../monitor-form/monitor-form.component.html       |  582 +-
 .../monitor-form/monitor-form.component.less       |  310 +-
 .../monitor-form/monitor-form.component.spec.ts    |   84 +-
 .../monitor/monitor-form/monitor-form.component.ts |   44 +-
 .../monitor-list/monitor-list.component.html       |  144 +-
 .../monitor-list/monitor-list.component.less       |  489 +-
 .../monitor-list/monitor-list.component.spec.ts    |  305 +-
 .../monitor/monitor-list/monitor-list.component.ts |  932 +-
 .../monitor/monitor-new/monitor-new.component.html |   71 +-
 .../monitor-new/monitor-new.component.spec.ts      |  233 +-
 .../monitor/monitor-new/monitor-new.component.ts   |  100 +-
 web-app/src/app/routes/monitor/monitor.module.ts   |    8 +-
 .../monitor-route-state-panel.component.html       |   29 +
 .../monitor-route-state-panel.component.less       |   95 +
 .../monitor-route-state-panel.component.spec.ts    |   68 +
 .../monitor-route-state-panel.component.ts}        |   25 +-
 .../shared/monitor-route-state.type.ts}            |    9 +-
 .../ops-placeholder-page.component.less            |   38 +
 .../ops-placeholder-page.component.ts              |   95 +
 .../app/routes/passport/lock/lock.component.html   |   36 +-
 .../src/app/routes/passport/lock/lock.component.ts |    2 +-
 .../app/routes/passport/login/login.component.html |  122 +-
 .../app/routes/passport/login/login.component.less |    2 +-
 .../routes/passport/login/login.component.spec.ts  |   93 +
 .../app/routes/passport/login/login.component.ts   |    6 +-
 .../app/routes/passport/passport-routing.module.ts |   23 +
 web-app/src/app/routes/passport/passport.module.ts |   14 +
 .../src/app/routes/routes-routing.module.spec.ts   |   56 +
 web-app/src/app/routes/routes-routing.module.ts    |   75 +-
 web-app/src/app/routes/routes.module.ts            |   50 +-
 .../setting/collector/collector.component.html     |  349 +-
 .../setting/collector/collector.component.ts       |    2 +-
 .../routes/setting/define/define.component.html    |  217 +-
 .../routes/setting/define/define.component.less    |   38 +
 .../routes/setting/define/define.component.spec.ts |   44 +-
 .../app/routes/setting/define/define.component.ts  |    6 +-
 .../app/routes/setting/label/label.component.html  |  137 +-
 .../app/routes/setting/label/label.component.less  |    2 +-
 .../app/routes/setting/label/label.component.ts    |    2 +-
 .../routes/setting/plugins/plugin.component.html   |  220 +-
 .../app/routes/setting/plugins/plugin.component.ts |    2 +-
 .../app/routes/setting/setting-routing.module.ts   |    7 +
 web-app/src/app/routes/setting/setting.module.ts   |    6 +-
 .../message-server/message-server.component.ts     |    2 +-
 .../object-store/object-store.component.ts         |    2 +-
 .../setting/settings/settings.component.html       |   30 +-
 .../setting/settings/settings.component.less       |   44 +-
 .../setting/settings/settings.component.spec.ts    |   41 +-
 .../routes/setting/settings/settings.component.ts  |   15 +-
 .../system-config/system-config.component.html     |    4 +-
 .../system-config/system-config.component.spec.ts  |   38 +-
 .../system-config/system-config.component.ts       |    4 +-
 .../token-management.component.html                |  169 +
 .../token-management.component.less                |  149 +
 .../token-management.component.spec.ts             |   64 +
 .../token-management/token-management.component.ts |  201 +
 .../routes/setting/status/status.component.html    |   32 +-
 .../routes/setting/status/status.component.spec.ts |    2 +
 .../app/routes/setting/status/status.component.ts  |    2 +-
 .../status-public/status-public.component.html     |  255 -
 .../status-public/status-public.component.less     |   75 -
 .../status-public/status-public.component.spec.ts  |   41 -
 .../status-public/status-public.component.ts       |  201 -
 web-app/src/app/service/alert.service.ts           |   14 +
 web-app/src/app/service/auth.service.ts            |   19 +-
 web-app/src/app/service/bulletin-define.service.ts |   65 -
 web-app/src/app/service/entity.service.ts          |  293 +
 web-app/src/app/service/log.service.ts             |   24 +-
 .../app/service/notice-receiver.service.spec.ts    |   12 +-
 .../src/app/service/otlp-ingestion.service.spec.ts |   65 +
 web-app/src/app/service/otlp-ingestion.service.ts  |   66 +
 .../src/app/service/status-page-public.service.ts  |   74 -
 web-app/src/app/service/theme.service.spec.ts      |   37 +
 web-app/src/app/service/theme.service.ts           |   22 +-
 .../activity-timeline.component.html               |   19 +
 .../activity-timeline.component.less               |   77 +
 .../activity-timeline.component.ts                 |   24 +
 .../shared/components/ai-chat/ai-chat.module.ts    |    2 +
 .../shared/components/ai-chat/chat.component.html  |    5 +-
 .../shared/components/ai-chat/chat.component.less  |   24 +-
 .../shared/components/ai-chat/chat.component.ts    |   10 +-
 .../code-block/code-block.component.html           |   26 +
 .../code-block/code-block.component.less           |  240 +
 .../components/code-block/code-block.component.ts  |  193 +
 .../configurable-field.component.ts                |    2 +-
 .../form-field/form-field.component.html           |    8 +-
 .../form-field/form-field.component.spec.ts        |   70 +-
 .../components/form-field/form-field.component.ts  |    4 +-
 .../help-message-show.component.less               |  113 +-
 .../help-message-show.component.ts                 |    2 +-
 .../label-selector/label-selector.component.ts     |    2 +-
 .../monitor-select-list.component.ts               |    2 +-
 .../monitor-select-menu.component.less             |    2 +-
 .../monitor-select-menu.component.ts               |    2 +-
 .../multi-func-input.component.html                |    5 +
 .../multi-func-input/multi-func-input.component.ts |   10 +-
 .../ops-context-bar/ops-context-bar.component.html |   81 +
 .../ops-context-bar/ops-context-bar.component.less |  202 +
 .../ops-context-bar.component.spec.ts              |  113 +
 .../ops-context-bar/ops-context-bar.component.ts   |  129 +
 .../page-shell/page-shell.component.html           |   48 +
 .../page-shell/page-shell.component.less           |  134 +
 .../page-shell/page-shell.component.spec.ts        |   92 +
 .../components/page-shell/page-shell.component.ts  |   44 +
 .../platform-chart-card.component.html             |   12 +
 .../platform-chart-card.component.less             |   27 +
 .../platform-chart-card.component.spec.ts          |   21 +
 .../platform-chart-card.component.ts               |   17 +
 .../platform-column-header-row.component.html      |    1 +
 .../platform-column-header-row.component.less      |    8 +
 .../platform-column-header-row.component.spec.ts   |   27 +
 .../platform-column-header-row.component.ts        |   14 +
 .../platform-context-chip-bar.component.html       |   12 +
 .../platform-context-chip-bar.component.less       |   94 +
 .../platform-context-chip-bar.component.spec.ts    |   44 +
 .../platform-context-chip-bar.component.ts         |   20 +
 .../platform-copyright-footer.component.html       |   10 +
 .../platform-copyright-footer.component.less       |   33 +
 .../platform-copyright-footer.component.spec.ts    |   24 +
 .../platform-copyright-footer.component.ts         |   16 +
 .../platform-drawer-action-links.component.html    |   15 +
 .../platform-drawer-action-links.component.less    |   16 +
 .../platform-drawer-action-links.component.spec.ts |   29 +
 .../platform-drawer-action-links.component.ts      |   27 +
 .../platform-drawer-attribute-list.component.html  |    6 +
 .../platform-drawer-attribute-list.component.less  |   58 +
 ...latform-drawer-attribute-list.component.spec.ts |   35 +
 .../platform-drawer-attribute-list.component.ts    |   19 +
 .../platform-drawer-callout-card.component.html    |   18 +
 .../platform-drawer-callout-card.component.less    |   74 +
 .../platform-drawer-callout-card.component.spec.ts |   44 +
 .../platform-drawer-callout-card.component.ts      |   18 +
 .../platform-drawer-code-preview.component.html    |    9 +
 .../platform-drawer-code-preview.component.less    |   11 +
 .../platform-drawer-code-preview.component.spec.ts |   41 +
 .../platform-drawer-code-preview.component.ts      |   17 +
 .../platform-drawer-facts.component.html           |    9 +
 .../platform-drawer-facts.component.less           |   42 +
 .../platform-drawer-facts.component.spec.ts        |   28 +
 .../platform-drawer-facts.component.ts             |   21 +
 .../platform-drawer-pill-row.component.html        |   20 +
 .../platform-drawer-pill-row.component.less        |   37 +
 .../platform-drawer-pill-row.component.spec.ts     |   49 +
 .../platform-drawer-pill-row.component.ts          |   27 +
 .../platform-drawer-preview-list.component.html    |    6 +
 .../platform-drawer-preview-list.component.less    |   38 +
 .../platform-drawer-preview-list.component.spec.ts |   30 +
 .../platform-drawer-preview-list.component.ts      |   18 +
 .../platform-drawer-section-list.component.html    |   33 +
 .../platform-drawer-section-list.component.less    |   55 +
 .../platform-drawer-section-list.component.spec.ts |   66 +
 .../platform-drawer-section-list.component.ts      |   37 +
 .../platform-drawer-section.component.html         |   12 +
 .../platform-drawer-section.component.less         |   40 +
 .../platform-drawer-section.component.spec.ts      |   31 +
 .../platform-drawer-section.component.ts           |   13 +
 .../platform-drawer-shell.component.html           |   29 +
 .../platform-drawer-shell.component.less           |   62 +
 .../platform-drawer-shell.component.spec.ts        |   64 +
 .../platform-drawer-shell.component.ts             |   27 +
 .../platform-drawer-toolbar.component.html         |   16 +
 .../platform-drawer-toolbar.component.less         |   38 +
 .../platform-drawer-toolbar.component.spec.ts      |   39 +
 .../platform-drawer-toolbar.component.ts           |   19 +
 .../platform-facts-strip.component.html            |   13 +
 .../platform-facts-strip.component.less            |   78 +
 .../platform-facts-strip.component.spec.ts         |   30 +
 .../platform-facts-strip.component.ts              |   20 +
 .../platform-key-value-grid.component.html         |    6 +
 .../platform-key-value-grid.component.less         |   85 +
 .../platform-key-value-grid.component.spec.ts      |   31 +
 .../platform-key-value-grid.component.ts           |   19 +
 .../platform-panel-header.component.html           |   11 +
 .../platform-panel-header.component.less           |   62 +
 .../platform-panel-header.component.spec.ts        |   35 +
 .../platform-panel-header.component.ts             |   15 +
 .../platform-rail-nav.component.html               |   36 +
 .../platform-rail-nav.component.less               |  124 +
 .../platform-rail-nav.component.spec.ts            |   55 +
 .../platform-rail-nav.component.ts                 |   53 +
 .../platform-section-header.component.html         |   12 +
 .../platform-section-header.component.less         |   51 +
 .../platform-section-header.component.spec.ts      |   35 +
 .../platform-section-header.component.ts           |   16 +
 .../platform-stage-insight-list.component.html     |    6 +
 .../platform-stage-insight-list.component.less     |   47 +
 .../platform-stage-insight-list.component.spec.ts  |   33 +
 .../platform-stage-insight-list.component.ts       |   18 +
 .../platform-stage-meta-header.component.html      |   25 +
 .../platform-stage-meta-header.component.less      |  102 +
 .../platform-stage-meta-header.component.spec.ts   |   44 +
 .../platform-stage-meta-header.component.ts        |   22 +
 .../platform-stage-section.component.html          |   21 +
 .../platform-stage-section.component.less          |   67 +
 .../platform-stage-section.component.spec.ts       |   39 +
 .../platform-stage-section.component.ts            |   21 +
 .../platform-summary-metric-grid.component.html    |   29 +
 .../platform-summary-metric-grid.component.less    |  128 +
 .../platform-summary-metric-grid.component.spec.ts |   43 +
 .../platform-summary-metric-grid.component.ts      |   32 +
 .../platform-support-action-bar.component.html     |   15 +
 .../platform-support-action-bar.component.less     |   26 +
 .../platform-support-action-bar.component.spec.ts  |   36 +
 .../platform-support-action-bar.component.ts       |   28 +
 .../platform-support-link-list.component.html      |   15 +
 .../platform-support-link-list.component.less      |   37 +
 .../platform-support-link-list.component.spec.ts   |   46 +
 .../platform-support-link-list.component.ts        |   29 +
 .../platform-support-panel.component.html          |   13 +
 .../platform-support-panel.component.less          |   56 +
 .../platform-support-panel.component.spec.ts       |   35 +
 .../platform-support-panel.component.ts            |   16 +
 ...platform-trace-waterfall-preview.component.html |   69 +
 ...platform-trace-waterfall-preview.component.less |  301 +
 ...tform-trace-waterfall-preview.component.spec.ts |   66 +
 .../platform-trace-waterfall-preview.component.ts  |  181 +
 .../right-drawer/right-drawer.component.html       |   48 +
 .../right-drawer/right-drawer.component.less       |  101 +
 .../right-drawer/right-drawer.component.spec.ts    |   70 +
 .../right-drawer/right-drawer.component.ts         |   67 +
 .../components/sql-editor/sql-editor.component.ts  |    2 +-
 .../components/sql-editor/sql-editor.module.ts     |   13 +
 .../summary-card/summary-card.component.html       |   11 +
 .../summary-card/summary-card.component.less       |   61 +
 .../summary-card/summary-card.component.ts         |   19 +
 .../shared/components/toolbar/toolbar.component.ts |    2 +-
 .../workspace-guidance-panel.component.html        |   40 +
 .../workspace-guidance-panel.component.less        |   26 +
 .../workspace-guidance-panel.component.spec.ts     |   61 +
 .../workspace-guidance-panel.component.ts          |   90 +
 .../workspace-shell/workspace-shell.component.html |   84 +
 .../workspace-shell/workspace-shell.component.less |  156 +
 .../workspace-shell.component.spec.ts              |  142 +
 .../workspace-shell/workspace-shell.component.ts   |   96 +
 .../directives/markdown-mermaid.directive.spec.ts  |   46 +
 .../directives/markdown-mermaid.directive.ts       |  152 +
 .../app/shared/json-schema/json-schema.module.ts   |    4 +-
 .../src/app/shared/json-schema/test/test.widget.ts |    2 +-
 .../observability-console-context.spec.ts          |   91 +
 .../observability/observability-console-context.ts |  139 +
 .../observability/observability-theme.spec.ts      |  111 +
 .../shared/observability/observability-theme.ts    |  528 ++
 .../src/app/shared/pipe/elapsed-time.pipe.spec.ts  |   22 +-
 web-app/src/app/shared/pipe/elapsed-time.pipe.ts   |   20 +-
 web-app/src/app/shared/pipe/i18n-else.pipe.spec.ts |   12 +-
 web-app/src/app/shared/pipe/i18n-else.pipe.ts      |    2 +-
 web-app/src/app/shared/pipe/timezone.pipe.ts       |    2 +-
 .../app/shared/services/ai-chat-modal.service.ts   |   12 +-
 web-app/src/app/shared/shared-delon.module.ts      |    3 +-
 web-app/src/app/shared/shared.module.ts            |  110 +-
 .../src/app/shared/utils/code-navigation.util.ts   |   76 +
 web-app/src/assets/app-data.json                   |  270 +-
 web-app/src/assets/dev/trace-demo-bootstrap.html   |   70 +
 web-app/src/assets/i18n/en-US.json                 | 2414 ++++-
 web-app/src/assets/i18n/ja-JP.json                 | 1245 ++-
 web-app/src/assets/i18n/pt-BR.json                 | 1224 ++-
 web-app/src/assets/i18n/zh-CN.json                 | 2475 ++++-
 web-app/src/assets/i18n/zh-TW.json                 | 1231 ++-
 web-app/src/environments/environment.ts            |    2 +-
 web-app/src/styles.less                            |  435 +-
 web-app/src/styles/entity-workspace.less           |  280 +
 web-app/src/styles/index.less                      |  230 +-
 web-app/src/styles/observability-workbench.less    |  113 +
 web-app/src/styles/platform-drawer.less            |  119 +
 web-app/src/styles/platform-shell.less             |   82 +
 web-app/src/styles/theme-vars.less                 |   64 +
 web-app/src/styles/theme.less                      |  454 +-
 web-app/src/test.ts                                |  119 +-
 web-app/src/typings.d.ts                           |    2 +
 web-next/.dockerignore                             |    5 +
 web-next/.eslintrc.json                            |    3 +
 web-next/.gitignore                                |   11 +
 web-next/Dockerfile                                |   21 +
 web-next/app/actions/page.test.tsx                 |   90 +
 web-next/app/actions/page.tsx                      |  187 +
 web-next/app/alert-family.chrome.test.ts           |   34 +
 .../app/alert/alert-label-options.guard.test.ts    |   20 +
 .../app/alert/alert-modal-feedback.guard.test.ts   |   26 +
 web-next/app/alert/center/page.test.ts             |   67 +
 web-next/app/alert/center/page.tsx                 |   10 +
 web-next/app/alert/group/page.test.tsx             |  144 +
 web-next/app/alert/group/page.tsx                  |  227 +
 web-next/app/alert/inhibit/page.test.tsx           |  170 +
 web-next/app/alert/inhibit/page.tsx                |  229 +
 .../app/alert/integration/[source]/page.test.tsx   |  183 +
 web-next/app/alert/integration/[source]/page.tsx   |   99 +
 web-next/app/alert/notice/page.test.tsx            |  594 ++
 web-next/app/alert/notice/page.tsx                 | 1350 +++
 web-next/app/alert/page.test.tsx                   |  253 +
 web-next/app/alert/page.tsx                        |   99 +
 web-next/app/alert/setting/page.test.tsx           |  290 +
 web-next/app/alert/setting/page.tsx                |  200 +
 web-next/app/alert/silence/page.test.tsx           |  161 +
 web-next/app/alert/silence/page.tsx                |  224 +
 web-next/app/alerts/page.test.ts                   |   34 +
 web-next/app/alerts/page.tsx                       |    6 +
 web-next/app/bulletin/page.test.tsx                |   83 +
 web-next/app/bulletin/page.tsx                     |   23 +
 .../app/compatibility-entrypoints.chrome.test.ts   |   52 +
 web-next/app/dashboard/page.test.ts                |   34 +
 web-next/app/dashboard/page.tsx                    |    6 +
 web-next/app/entities/[...rest]/page.test.ts       |   20 +
 web-next/app/entities/[...rest]/page.tsx           |    5 +
 .../entities/[entityId]/definition/page.test.tsx   |   88 +
 .../app/entities/[entityId]/definition/page.tsx    |   32 +
 .../app/entities/[entityId]/edit/page.test.tsx     |  157 +
 web-next/app/entities/[entityId]/edit/page.tsx     |   28 +
 web-next/app/entities/[entityId]/page.test.tsx     |  212 +
 web-next/app/entities/[entityId]/page.tsx          |   69 +
 web-next/app/entities/discovery/page.test.tsx      |   71 +
 web-next/app/entities/discovery/page.tsx           |   19 +
 web-next/app/entities/import/page.test.tsx         |   64 +
 web-next/app/entities/import/page.tsx              |   19 +
 web-next/app/entities/new/page.test.tsx            |  140 +
 web-next/app/entities/new/page.tsx                 |   32 +
 web-next/app/entities/not-found.test.ts            |   20 +
 web-next/app/entities/not-found.tsx                |    5 +
 web-next/app/entities/page.test.tsx                |  260 +
 web-next/app/entities/page.tsx                     |   73 +
 web-next/app/entity-detail-family.chrome.test.ts   |   49 +
 web-next/app/entity-editor-family.chrome.test.ts   |   98 +
 web-next/app/events/page.test.ts                   |   47 +
 web-next/app/events/page.tsx                       |    6 +
 web-next/app/exception/[type]/page.test.tsx        |   25 +
 web-next/app/exception/[type]/page.tsx             |    8 +
 web-next/app/explorer/page.test.tsx                |   65 +
 web-next/app/explorer/page.tsx                     |  226 +
 web-next/app/globals.css                           |  546 ++
 web-next/app/globals.test.ts                       |   27 +
 web-next/app/hb-i18n/[lang]/route.test.ts          |   54 +
 web-next/app/hb-i18n/[lang]/route.ts               |   35 +
 web-next/app/incidents/page.test.tsx               |   47 +
 web-next/app/incidents/page.tsx                    |   95 +
 web-next/app/ingestion/otlp/metrics/page.test.tsx  | 1258 +++
 web-next/app/ingestion/otlp/metrics/page.tsx       |  909 ++
 .../app/ingestion/otlp/metrics/route-state.test.ts |   91 +
 web-next/app/ingestion/otlp/metrics/route-state.ts |   88 +
 web-next/app/ingestion/otlp/page.test.tsx          |  514 +
 web-next/app/ingestion/otlp/page.tsx               |  692 ++
 web-next/app/layout.tsx                            |   26 +
 web-next/app/loading.test.tsx                      |   19 +
 web-next/app/loading.tsx                           |   28 +
 .../app/log/integration/[source]/page.test.tsx     |   76 +
 web-next/app/log/integration/[source]/page.tsx     |   16 +
 web-next/app/log/integration/page.test.tsx         |   52 +
 web-next/app/log/integration/page.tsx              |   11 +
 web-next/app/log/manage/log-manage-page.tsx        | 1559 +++
 web-next/app/log/manage/page.test.tsx              | 1035 ++
 web-next/app/log/manage/page.tsx                   |    8 +
 web-next/app/log/manage/route-state.test.ts        |   81 +
 web-next/app/log/manage/route-state.ts             |   30 +
 web-next/app/log/stream/page.test.tsx              |   31 +
 web-next/app/log/stream/page.tsx                   |   12 +
 web-next/app/login/page.test.ts                    |   35 +
 web-next/app/login/page.tsx                        |   11 +
 .../app/milestone4-auth-settings.chrome.test.ts    |   46 +
 web-next/app/milestone5-final-sweep.chrome.test.ts |  108 +
 .../milestone5-shared-route-owners.chrome.test.ts  |   94 +
 web-next/app/monitor-family.chrome.test.ts         |   93 +
 web-next/app/monitor-route-ownership.test.ts       |   61 +
 web-next/app/monitors/[...rest]/page.test.ts       |   20 +
 web-next/app/monitors/[...rest]/page.tsx           |    5 +
 .../app/monitors/[monitorId]/edit/page.test.tsx    |  198 +
 web-next/app/monitors/[monitorId]/edit/page.tsx    |   40 +
 web-next/app/monitors/[monitorId]/page.test.tsx    |   40 +
 web-next/app/monitors/[monitorId]/page.tsx         |  739 ++
 web-next/app/monitors/new/page.test.tsx            |  198 +
 web-next/app/monitors/new/page.tsx                 |   38 +
 web-next/app/monitors/not-found.test.ts            |   20 +
 web-next/app/monitors/not-found.tsx                |    5 +
 web-next/app/monitors/page.test.tsx                |  514 +
 web-next/app/monitors/page.tsx                     |  754 ++
 web-next/app/operator-family.chrome.test.ts        |   36 +
 web-next/app/overview/page.test.tsx                |  563 ++
 web-next/app/overview/page.tsx                     |  417 +
 web-next/app/page.test.ts                          |   20 +
 web-next/app/page.tsx                              |    5 +
 web-next/app/passport/lock/page.test.tsx           |   76 +
 web-next/app/passport/lock/page.tsx                |   71 +
 web-next/app/passport/login/page.test.tsx          |   17 +
 web-next/app/passport/login/page.tsx               |   10 +
 web-next/app/setting/collector/page.test.tsx       |  134 +
 web-next/app/setting/collector/page.tsx            |   43 +
 web-next/app/setting/define/page.test.tsx          |  133 +
 web-next/app/setting/define/page.tsx               |  152 +
 web-next/app/setting/labels/page.test.tsx          |  140 +
 web-next/app/setting/labels/page.tsx               |   70 +
 web-next/app/setting/page.test.ts                  |   39 +
 web-next/app/setting/page.tsx                      |    9 +
 web-next/app/setting/plugins/page.test.tsx         |  132 +
 web-next/app/setting/plugins/page.tsx              |   69 +
 web-next/app/setting/settings/config/page.test.tsx |  141 +
 web-next/app/setting/settings/config/page.tsx      |  152 +
 web-next/app/setting/settings/layout.tsx           |   28 +
 web-next/app/setting/settings/mcp-server/page.tsx  |    9 +
 .../setting/settings/object-store/page.test.tsx    |  165 +
 .../app/setting/settings/object-store/page.tsx     |  172 +
 web-next/app/setting/settings/page.test.ts         |   39 +
 web-next/app/setting/settings/page.tsx             |    9 +
 web-next/app/setting/settings/server/page.test.tsx |  259 +
 web-next/app/setting/settings/server/page.tsx      |  460 +
 web-next/app/setting/settings/token/page.test.tsx  |  203 +
 web-next/app/setting/settings/token/page.tsx       |  192 +
 web-next/app/setting/status/page.test.tsx          |  200 +
 web-next/app/setting/status/page.tsx               |  277 +
 web-next/app/settings-family.chrome.test.ts        |   75 +
 web-next/app/status/page.test.tsx                  |  144 +
 web-next/app/status/page.tsx                       |  135 +
 web-next/app/status/public/page.test.ts            |   40 +
 web-next/app/status/public/page.tsx                |    9 +
 web-next/app/three-signal-family.chrome.test.ts    |   96 +
 web-next/app/topology/page.test.tsx                |  274 +
 web-next/app/topology/page.tsx                     |  399 +
 .../app/trace/manage/page.route-context.test.ts    |   12 +
 web-next/app/trace/manage/page.test.tsx            |  497 +
 web-next/app/trace/manage/page.tsx                 |  990 ++
 web-next/app/trace/manage/route-state.test.ts      |   72 +
 web-next/app/trace/manage/route-state.ts           |   16 +
 web-next/components.json                           |   16 +
 .../log-manage/log-dialog.chrome.test.ts           |   42 +
 .../log-manage/log-related-trace-dialog.test.tsx   |  116 +
 .../log-manage/log-related-trace-dialog.tsx        |  286 +
 .../log-manage/log-stream-detail-dialog.test.tsx   |   71 +
 .../log-manage/log-stream-detail-dialog.tsx        |  151 +
 .../monitor-detail/history-line-chart.test.tsx     |   77 +
 .../monitor-detail/history-line-chart.tsx          |  234 +
 .../monitor-detail/monitor-detail-console.test.tsx |  457 +
 .../monitor-detail/monitor-detail-console.tsx      |  307 +
 .../monitor-detail-sections.test.tsx               |  522 +
 .../monitor-detail/monitor-detail-sections.tsx     |  544 ++
 .../monitor-history-chart-grid.test.tsx            |  160 +
 .../monitor-detail/monitor-history-chart-grid.tsx  |  261 +
 .../monitor-detail/monitor-history-panel.test.tsx  |  563 ++
 .../monitor-detail/monitor-history-panel.tsx       |  584 ++
 .../monitor-detail/monitor-metric-table.test.tsx   |   57 +
 .../monitor-detail/monitor-metric-table.tsx        |   84 +
 .../monitor-panel-primitives.test.tsx              |   43 +
 .../monitor-detail/monitor-panel-primitives.tsx    |   18 +
 .../monitor-detail/monitor-realtime-panel.test.tsx |  194 +
 .../monitor-detail/monitor-realtime-panel.tsx      |  341 +
 .../monitor-detail/monitor-summary-card.test.tsx   |   67 +
 .../monitor-detail/monitor-summary-card.tsx        |   84 +
 web-next/components/observability.ts               |    1 +
 .../components/observability/chip-toggle.test.tsx  |   33 +
 web-next/components/observability/chip-toggle.tsx  |   46 +
 .../components/observability/code-pane.test.tsx    |   16 +
 web-next/components/observability/code-pane.tsx    |   25 +
 .../components/observability/context-chip-bar.tsx  |   62 +
 .../observability/control-button.test.tsx          |   56 +
 .../components/observability/control-button.tsx    |   60 +
 .../components/observability/data-table.test.tsx   |   50 +
 web-next/components/observability/data-table.tsx   |   96 +
 .../components/observability/detail-rows.test.tsx  |   23 +
 web-next/components/observability/detail-rows.tsx  |   78 +
 .../components/observability/disclosure.test.tsx   |   18 +
 web-next/components/observability/disclosure.tsx   |   20 +
 .../components/observability/drawer-section.tsx    |  105 +
 .../observability/echarts-panel.test.tsx           |   18 +
 .../components/observability/echarts-panel.tsx     |  141 +
 .../components/observability/editor-rows.test.tsx  |   34 +
 web-next/components/observability/editor-rows.tsx  |   20 +
 web-next/components/observability/facts-strip.tsx  |   54 +
 web-next/components/observability/index.ts         |   35 +
 .../observability/observability.chrome.test.ts     |  171 +
 .../components/observability/owner-layer.test.tsx  |  108 +
 .../observability/ownership-primitives.tsx         |  184 +
 .../components/observability/page-header.test.tsx  |   69 +
 web-next/components/observability/page-header.tsx  |  127 +
 .../components/observability/panel-shell.test.tsx  |   42 +
 web-next/components/observability/panel-shell.tsx  |   86 +
 web-next/components/observability/rail-nav.tsx     |   58 +
 .../components/observability/rail-shell.test.tsx   |   46 +
 web-next/components/observability/rail-shell.tsx   |   42 +
 .../components/observability/search-input.test.tsx |   20 +
 web-next/components/observability/search-input.tsx |   44 +
 .../observability/selectable-card-grid.test.tsx    |   56 +
 .../observability/selectable-card-grid.tsx         |  114 +
 .../observability/selectable-evidence-list.tsx     |   53 +
 .../observability/selectable-rows.test.tsx         |   62 +
 .../components/observability/selectable-rows.tsx   |  116 +
 .../components/observability/signal-bars.test.tsx  |   25 +
 web-next/components/observability/signal-bars.tsx  |   36 +
 .../components/observability/stage-section.tsx     |   88 +
 web-next/components/observability/stat-grid.tsx    |   70 +
 .../components/observability/status-badge.test.tsx |   28 +
 web-next/components/observability/status-badge.tsx |   26 +
 .../components/observability/status-state.test.tsx |   31 +
 web-next/components/observability/status-state.tsx |   28 +
 .../observability/summary-metric-grid.tsx          |   45 +
 .../components/observability/support-panel.tsx     |   86 +
 .../components/observability/tab-strip.test.tsx    |   75 +
 web-next/components/observability/tab-strip.tsx    |  193 +
 .../observability/time-range-control.test.tsx      |  386 +
 .../observability/time-range-control.tsx           |  510 +
 .../components/observability/timeline.test.tsx     |   30 +
 web-next/components/observability/timeline.tsx     |   50 +
 web-next/components/observability/toolbar.test.tsx |   34 +
 web-next/components/observability/toolbar.tsx      |   88 +
 web-next/components/observability/virtual-list.tsx |   50 +
 .../components/observability/waterfall.test.tsx    |  270 +
 web-next/components/observability/waterfall.tsx    |  364 +
 .../components/observability/workspace-shell.tsx   |  143 +
 .../observability/workspace-tab-strip.tsx          |   93 +
 .../components/overview/overview-console.test.tsx  |  196 +
 web-next/components/overview/overview-console.tsx  |  682 ++
 .../components/overview/overview-detail-dialog.tsx |   87 +
 .../overview/overview-problem-focus-dialog.tsx     |   65 +
 .../components/overview/overview.chrome.test.ts    |   21 +
 .../pages/alert-authoring-primitives.test.tsx      |   48 +
 .../pages/alert-authoring-primitives.tsx           |  139 +
 .../pages/alert-authoring.chrome.test.ts           |   69 +
 .../components/pages/alert-center-surface.test.tsx |  731 ++
 web-next/components/pages/alert-center-surface.tsx |  571 ++
 .../components/pages/alert-center.chrome.test.ts   |   64 +
 .../pages/alert-group-authoring-fields.test.tsx    |   81 +
 .../pages/alert-group-authoring-fields.tsx         |  105 +
 .../components/pages/alert-group-surface.test.tsx  |  379 +
 web-next/components/pages/alert-group-surface.tsx  |  348 +
 .../pages/alert-inhibit-authoring-fields.test.tsx  |  104 +
 .../pages/alert-inhibit-authoring-fields.tsx       |  177 +
 .../pages/alert-inhibit-surface.test.tsx           |  480 +
 .../components/pages/alert-inhibit-surface.tsx     |  430 +
 .../pages/alert-integration-markdown.test.tsx      |  106 +
 .../pages/alert-integration-markdown.tsx           |  303 +
 .../components/pages/alert-integration-mermaid.tsx |  111 +
 .../pages/alert-notice-console-shell.test.tsx      |   47 +
 .../pages/alert-notice-console-shell.tsx           |   88 +
 .../pages/alert-notice-receiver-fields.test.tsx    |   91 +
 .../pages/alert-notice-receiver-fields.tsx         |  332 +
 .../pages/alert-notice-rule-fields.test.tsx        |  175 +
 .../components/pages/alert-notice-rule-fields.tsx  |  363 +
 .../pages/alert-notice-template-fields.test.tsx    |  120 +
 .../pages/alert-notice-template-fields.tsx         |  164 +
 .../components/pages/alert-notice.chrome.test.ts   |   75 +
 .../pages/alert-rule-quick-dialog.test.tsx         |  100 +
 .../components/pages/alert-rule-quick-dialog.tsx   |  127 +
 .../pages/alert-setting-create-dialog.test.tsx     |  279 +
 .../pages/alert-setting-create-dialog.tsx          |  455 +
 .../pages/alert-setting-surface.test.tsx           |  242 +
 .../components/pages/alert-setting-surface.tsx     |  291 +
 .../pages/alert-silence-authoring-fields.test.tsx  |  137 +
 .../pages/alert-silence-authoring-fields.tsx       |  225 +
 .../pages/alert-silence-surface.test.tsx           |  447 +
 .../components/pages/alert-silence-surface.tsx     |  391 +
 .../pages/alert-surface-primitives.test.tsx        |   56 +
 .../components/pages/alert-surface-primitives.tsx  |  181 +
 .../components/pages/alert-surface.chrome.test.ts  |   89 +
 .../pages/bulletin-center-surface.test.tsx         |  110 +
 .../components/pages/bulletin-center-surface.tsx   |  407 +
 .../pages/bulletin-manage-dialog.test.tsx          |   91 +
 .../components/pages/bulletin-manage-dialog.tsx    |   87 +
 .../components/pages/bulletin-metrics-table.tsx    |  124 +
 .../pages/bulletin-monitor-detail.chrome.test.ts   |   83 +
 web-next/components/pages/client-workbench.tsx     |    3 +
 .../pages/collector-manage-surface.test.tsx        |  183 +
 .../components/pages/collector-manage-surface.tsx  |  269 +
 .../entity-definition-workspace-surface.test.tsx   |  337 +
 .../pages/entity-definition-workspace-surface.tsx  |  821 ++
 .../pages/entity-detail-surface.test.tsx           |  248 +
 .../components/pages/entity-detail-surface.tsx     |  514 +
 .../pages/entity-discovery-surface.test.tsx        |  212 +
 .../components/pages/entity-discovery-surface.tsx  |  320 +
 .../pages/entity-editor-surface.test.tsx           |  504 +
 .../components/pages/entity-editor-surface.tsx     | 1157 +++
 .../pages/entity-import-surface.test.tsx           |   27 +
 .../components/pages/entity-import-surface.tsx     |   14 +
 .../components/pages/entity-list-surface.test.tsx  |  142 +
 web-next/components/pages/entity-list-surface.tsx  |  306 +
 .../pages/exception-center-surface.test.tsx        |   90 +
 .../components/pages/exception-center-surface.tsx  |  175 +
 .../pages/explorer-bulletin.chrome.test.ts         |   71 +
 .../components/pages/label-manage-surface.test.tsx |  232 +
 web-next/components/pages/label-manage-surface.tsx |  273 +
 .../pages/log-integration-redirect-shell.test.tsx  |   52 +
 .../pages/log-integration-redirect-shell.tsx       |   50 +
 .../components/pages/login-form.submit.test.tsx    |  171 +
 web-next/components/pages/login-form.test.tsx      |  158 +
 web-next/components/pages/login-form.tsx           |  167 +
 .../pages/monitor-editor-surface.test.tsx          |  543 ++
 .../components/pages/monitor-editor-surface.tsx    |  508 +
 .../components/pages/ops-surface-page.test.tsx     |  129 +
 web-next/components/pages/ops-surface-page.tsx     |  116 +
 web-next/components/pages/passport-shell.test.tsx  |  143 +
 web-next/components/pages/passport-shell.tsx       |  178 +
 .../pages/plugin-manage-surface.test.tsx           |  278 +
 .../components/pages/plugin-manage-surface.tsx     |  343 +
 .../components/pages/public-status-shell.test.tsx  |  207 +
 web-next/components/pages/public-status-shell.tsx  |  582 ++
 .../pages/setting-define-surface.test.tsx          |  322 +
 .../components/pages/setting-define-surface.tsx    |  348 +
 .../pages/settings-surface-page.test.tsx           |   74 +
 .../components/pages/settings-surface-page.tsx     |   51 +
 .../pages/status-setting-surface.test.tsx          |  176 +
 .../components/pages/status-setting-surface.tsx    |  641 ++
 .../pages/three-signal-desk-shell.test.tsx         |   46 +
 .../components/pages/three-signal-desk-shell.tsx   |   21 +
 .../components/parity/angular-parity-kit.test.tsx  |  156 +
 web-next/components/parity/angular-parity-kit.tsx  |  320 +
 .../components/providers/i18n-provider.test.ts     |  236 +
 web-next/components/providers/i18n-provider.tsx    |  253 +
 .../settings/settings-console-shell.test.tsx       |   78 +
 .../components/settings/settings-console-shell.tsx |  103 +
 .../settings/settings-dialog-form.test.tsx         |   55 +
 .../components/settings/settings-dialog-form.tsx   |  117 +
 .../components/settings/settings-form.test.tsx     |   64 +
 web-next/components/settings/settings-form.tsx     |   93 +
 .../settings/settings-summary-list.test.tsx        |   49 +
 .../components/settings/settings-summary-list.tsx  |   66 +
 web-next/components/shared-owner.chrome.test.ts    |   51 +
 .../components/shell/app-frame.chrome.test.tsx     |  336 +
 web-next/components/shell/app-frame.tsx            |  882 ++
 web-next/components/shell/app-sidebar.test.tsx     |   64 +
 web-next/components/shell/app-sidebar.tsx          |   57 +
 web-next/components/shell/auth-gate.tsx            |   25 +
 .../components/shell/auth-recovery.chrome.test.ts  |   29 +
 .../components/shell/locale-option-list.test.tsx   |   41 +
 web-next/components/shell/locale-option-list.tsx   |   51 +
 .../shell/platform-copyright-footer.test.tsx       |   14 +
 .../components/shell/platform-copyright-footer.tsx |   40 +
 web-next/components/ui/badge.tsx                   |   26 +
 web-next/components/ui/button.test.tsx             |   25 +
 web-next/components/ui/button.tsx                  |   41 +
 web-next/components/ui/card.tsx                    |   30 +
 web-next/components/ui/checkbox.test.tsx           |   21 +
 web-next/components/ui/checkbox.tsx                |   43 +
 web-next/components/ui/cold-code-editor.test.tsx   |   78 +
 web-next/components/ui/cold-code-editor.tsx        |  147 +
 web-next/components/ui/cold-confirm-dialog.tsx     |   56 +
 web-next/components/ui/date-time-range.test.tsx    |  121 +
 web-next/components/ui/date-time-range.tsx         |  407 +
 web-next/components/ui/file-input.test.tsx         |   15 +
 .../components/ui/file-input.tsx                   |   40 +-
 .../components/ui/hidden-input.tsx                 |   30 +-
 web-next/components/ui/input.test.tsx              |   15 +
 web-next/components/ui/input.tsx                   |   19 +
 web-next/components/ui/label-record-input.test.tsx |   51 +
 web-next/components/ui/label-record-input.tsx      |  376 +
 web-next/components/ui/number-stepper.test.tsx     |   18 +
 web-next/components/ui/number-stepper.tsx          |  115 +
 web-next/components/ui/search-row.test.tsx         |   57 +
 web-next/components/ui/search-row.tsx              |  100 +
 web-next/components/ui/segmented-control.tsx       |   47 +
 web-next/components/ui/select.test.tsx             |   75 +
 web-next/components/ui/select.tsx                  |  271 +
 web-next/components/ui/separator.tsx               |    6 +
 web-next/components/ui/tag-input.test.tsx          |   41 +
 web-next/components/ui/tag-input.tsx               |  188 +
 web-next/components/ui/textarea.test.tsx           |   20 +
 web-next/components/ui/textarea.tsx                |   25 +
 web-next/components/ui/weekday-picker.test.tsx     |   27 +
 web-next/components/ui/weekday-picker.tsx          |   62 +
 web-next/components/workbench/action-toolbar.tsx   |   26 +
 .../components/workbench/client-workbench.test.tsx |   54 +
 web-next/components/workbench/client-workbench.tsx |  105 +
 web-next/components/workbench/code-pane.tsx        |    3 +
 .../components/workbench/overlay-dialog.test.tsx   |   97 +
 web-next/components/workbench/overlay-dialog.tsx   |   81 +
 web-next/components/workbench/primitives.test.tsx  |  372 +
 web-next/components/workbench/primitives.tsx       |  542 ++
 .../workbench/selectable-evidence-list.test.tsx    |   34 +
 .../workbench/selectable-evidence-list.tsx         |    4 +
 web-next/components/workbench/toolbar.test.tsx     |   31 +
 web-next/components/workbench/toolbar.tsx          |   58 +
 .../components/workbench/workbench-page.test.tsx   |  134 +
 web-next/components/workbench/workbench-page.tsx   |   87 +
 .../components/workbench/workspace-shell.test.tsx  |  102 +
 web-next/components/workbench/workspace-shell.tsx  |    3 +
 .../workbench/workspace-tab-strip.test.tsx         |   57 +
 .../components/workbench/workspace-tab-strip.tsx   |    3 +
 web-next/lib/actions-surface/model.test.ts         |  137 +
 web-next/lib/actions-surface/model.ts              |  360 +
 web-next/lib/actions-surface/view-model.test.ts    |  106 +
 web-next/lib/actions-surface/view-model.ts         |  129 +
 web-next/lib/alert-group/controller.test.ts        |   74 +
 web-next/lib/alert-group/controller.ts             |   70 +
 web-next/lib/alert-group/query-state.test.ts       |   12 +
 web-next/lib/alert-group/query-state.ts            |    5 +
 web-next/lib/alert-group/view-model.test.ts        |  183 +
 web-next/lib/alert-group/view-model.ts             |  159 +
 web-next/lib/alert-inhibit/controller.test.ts      |  116 +
 web-next/lib/alert-inhibit/controller.ts           |  100 +
 web-next/lib/alert-inhibit/query-state.test.ts     |   12 +
 web-next/lib/alert-inhibit/query-state.ts          |    5 +
 web-next/lib/alert-inhibit/view-model.test.ts      |  161 +
 web-next/lib/alert-inhibit/view-model.ts           |  171 +
 web-next/lib/alert-integration/controller.test.ts  |   28 +
 web-next/lib/alert-integration/controller.ts       |   16 +
 web-next/lib/alert-integration/view-model.test.ts  |   35 +
 web-next/lib/alert-integration/view-model.ts       |   41 +
 web-next/lib/alert-label-options.ts                |   43 +
 web-next/lib/alert-manage/controller.test.ts       |  133 +
 web-next/lib/alert-manage/controller.ts            |  117 +
 web-next/lib/alert-manage/display-mapping.test.ts  |   24 +
 web-next/lib/alert-manage/display-mapping.ts       |   16 +
 web-next/lib/alert-manage/query-state.test.ts      |  286 +
 web-next/lib/alert-manage/query-state.ts           |  166 +
 web-next/lib/alert-manage/view-model.test.ts       |  777 ++
 web-next/lib/alert-manage/view-model.ts            | 1067 +++
 web-next/lib/alert-notice/controller.test.ts       |  242 +
 web-next/lib/alert-notice/controller.ts            |  433 +
 web-next/lib/alert-notice/view-model.test.ts       |  323 +
 web-next/lib/alert-notice/view-model.ts            |  666 ++
 web-next/lib/alert-rule-evidence-copy.ts           |   34 +
 web-next/lib/alert-setting/controller.test.ts      |   89 +
 web-next/lib/alert-setting/controller.ts           |   55 +
 web-next/lib/alert-setting/view-model.test.ts      |  141 +
 web-next/lib/alert-setting/view-model.ts           |  122 +
 web-next/lib/alert-silence/controller.test.ts      |  177 +
 web-next/lib/alert-silence/controller.ts           |  157 +
 web-next/lib/alert-silence/query-state.test.ts     |   12 +
 web-next/lib/alert-silence/query-state.ts          |    5 +
 web-next/lib/alert-silence/view-model.test.ts      |  187 +
 web-next/lib/alert-silence/view-model.ts           |  152 +
 web-next/lib/api-client.ts                         |  153 +
 web-next/lib/app-frame-state.test.ts               |   31 +
 web-next/lib/app-frame-state.ts                    |   14 +
 web-next/lib/bulletin-center/controller.test.ts    |   66 +
 web-next/lib/bulletin-center/controller.ts         |   62 +
 web-next/lib/bulletin-center/query-state.test.ts   |   20 +
 web-next/lib/bulletin-center/query-state.ts        |   16 +
 web-next/lib/bulletin-center/view-model.test.ts    |  164 +
 web-next/lib/bulletin-center/view-model.ts         |  201 +
 web-next/lib/client-workbench-state.test.ts        |   39 +
 web-next/lib/client-workbench-state.ts             |   17 +
 web-next/lib/code-navigation.test.ts               |   26 +
 web-next/lib/code-navigation.ts                    |   66 +
 web-next/lib/cold-ops-visual.test.ts               |   95 +
 web-next/lib/cold-ops-visual.ts                    |  119 +
 web-next/lib/collector-health-evidence.test.ts     |   38 +
 web-next/lib/collector-health-evidence.ts          |   85 +
 web-next/lib/collector-manage/controller.test.ts   |   11 +
 web-next/lib/collector-manage/controller.ts        |    9 +
 web-next/lib/collector-manage/query-state.test.ts  |   12 +
 web-next/lib/collector-manage/query-state.ts       |    9 +
 web-next/lib/collector-manage/view-model.test.ts   |  129 +
 web-next/lib/collector-manage/view-model.ts        |  135 +
 web-next/lib/compat/search-params.test.ts          |   46 +
 web-next/lib/compat/search-params.ts               |   51 +
 web-next/lib/dashboard/navigation.test.ts          |   36 +
 web-next/lib/dashboard/navigation.ts               |    5 +
 web-next/lib/entity-contract.test.ts               |  193 +
 web-next/lib/entity-contract.ts                    |  259 +
 web-next/lib/entity-definition/controller.test.ts  |   75 +
 web-next/lib/entity-definition/controller.ts       |  101 +
 web-next/lib/entity-definition/view-model.test.ts  |   41 +
 web-next/lib/entity-definition/view-model.ts       |   29 +
 web-next/lib/entity-detail/controller.test.ts      |   27 +
 web-next/lib/entity-detail/controller.ts           |   95 +
 web-next/lib/entity-detail/view-model.test.ts      |  388 +
 web-next/lib/entity-detail/view-model.ts           |  573 ++
 web-next/lib/entity-discovery/controller.test.ts   |   86 +
 web-next/lib/entity-discovery/controller.ts        |   65 +
 web-next/lib/entity-discovery/search-state.test.ts |   18 +
 web-next/lib/entity-discovery/search-state.ts      |   20 +
 web-next/lib/entity-discovery/view-model.test.ts   |  293 +
 web-next/lib/entity-discovery/view-model.ts        |  581 ++
 .../lib/entity-editor/collection-state.test.ts     |   43 +
 web-next/lib/entity-editor/collection-state.ts     |   31 +
 web-next/lib/entity-editor/controller.test.ts      |  180 +
 web-next/lib/entity-editor/controller.ts           |  500 +
 web-next/lib/entity-editor/draft-utils.test.ts     |   40 +
 web-next/lib/entity-editor/draft-utils.ts          |   33 +
 web-next/lib/entity-editor/editor-state.test.ts    |   37 +
 web-next/lib/entity-editor/editor-state.ts         |   26 +
 web-next/lib/entity-editor/initial-state.test.ts   |   52 +
 web-next/lib/entity-editor/initial-state.ts        |   48 +
 web-next/lib/entity-editor/view-model.test.ts      |  225 +
 web-next/lib/entity-editor/view-model.ts           |  245 +
 web-next/lib/entity-health-affordance.test.ts      |   26 +
 web-next/lib/entity-health-affordance.ts           |  126 +
 web-next/lib/entity-import/controller.test.ts      |   63 +
 web-next/lib/entity-import/controller.ts           |   61 +
 web-next/lib/entity-import/view-model.test.ts      |  277 +
 web-next/lib/entity-import/view-model.ts           |  279 +
 web-next/lib/entity-manage/controller.test.ts      |   26 +
 web-next/lib/entity-manage/controller.ts           |   28 +
 web-next/lib/entity-manage/display-mapping.test.ts |   15 +
 web-next/lib/entity-manage/display-mapping.ts      |   53 +
 web-next/lib/entity-manage/query-state.test.ts     |   23 +
 web-next/lib/entity-manage/query-state.ts          |   33 +
 web-next/lib/entity-manage/view-model.test.ts      |  124 +
 web-next/lib/entity-manage/view-model.ts           |   94 +
 web-next/lib/exception-center/view-model.test.ts   |   97 +
 web-next/lib/exception-center/view-model.ts        |  162 +
 web-next/lib/explorer-surface/view-model.test.ts   |   56 +
 web-next/lib/explorer-surface/view-model.ts        |   82 +
 web-next/lib/format.ts                             |   33 +
 web-next/lib/hertzbeat-product-language.test.ts    |   58 +
 web-next/lib/hertzbeat-product-language.ts         |   81 +
 web-next/lib/i18n-runtime-messages.ts              | 2109 +++++
 web-next/lib/i18n.test.ts                          |   26 +
 web-next/lib/i18n.ts                               |   39 +
 web-next/lib/incidents-surface/model.test.ts       |   37 +
 web-next/lib/incidents-surface/model.ts            |  139 +
 web-next/lib/incidents-surface/view-model.test.ts  |   66 +
 web-next/lib/incidents-surface/view-model.ts       |  101 +
 web-next/lib/label-manage/controller.test.ts       |   58 +
 web-next/lib/label-manage/controller.ts            |   52 +
 web-next/lib/label-manage/display-mapping.test.ts  |   11 +
 web-next/lib/label-manage/display-mapping.ts       |    6 +
 web-next/lib/label-manage/query-state.test.ts      |   12 +
 web-next/lib/label-manage/query-state.ts           |   11 +
 web-next/lib/label-manage/view-model.test.ts       |   62 +
 web-next/lib/label-manage/view-model.ts            |   49 +
 web-next/lib/log-integration/navigation.ts         |   36 +
 web-next/lib/log-manage/compat-search-params.ts    |   29 +
 web-next/lib/log-manage/display-mapping.test.ts    |   17 +
 web-next/lib/log-manage/display-mapping.ts         |   15 +
 web-next/lib/log-manage/query-state.test.ts        |  208 +
 web-next/lib/log-manage/query-state.ts             |  191 +
 web-next/lib/log-manage/stream-buffer.test.ts      |   59 +
 web-next/lib/log-manage/stream-buffer.ts           |   44 +
 web-next/lib/log-manage/stream-focus.test.ts       |   44 +
 web-next/lib/log-manage/stream-focus.ts            |   24 +
 web-next/lib/log-manage/stream-navigation.test.ts  |   52 +
 web-next/lib/log-manage/stream-navigation.ts       |   31 +
 web-next/lib/log-manage/stream-notices.test.ts     |   16 +
 web-next/lib/log-manage/stream-notices.ts          |    7 +
 web-next/lib/log-manage/stream-reconnect.test.ts   |   24 +
 web-next/lib/log-manage/stream-reconnect.ts        |   30 +
 web-next/lib/log-manage/stream-scroll.test.ts      |  175 +
 web-next/lib/log-manage/stream-scroll.ts           |   98 +
 web-next/lib/log-manage/stream-selection.test.ts   |   61 +
 web-next/lib/log-manage/stream-selection.ts        |   30 +
 web-next/lib/log-manage/stream-shortcuts.test.ts   |   31 +
 web-next/lib/log-manage/stream-shortcuts.ts        |   64 +
 web-next/lib/log-manage/stream-viewport.test.ts    |  231 +
 web-next/lib/log-manage/stream-viewport.ts         |  187 +
 web-next/lib/log-manage/view-model.test.ts         |  448 +
 web-next/lib/log-manage/view-model.ts              |  396 +
 web-next/lib/monitor-detail/controller.test.ts     |  159 +
 web-next/lib/monitor-detail/controller.ts          |  183 +
 .../lib/monitor-detail/detail-route-state.test.ts  |  180 +
 web-next/lib/monitor-detail/detail-route-state.ts  |  162 +
 web-next/lib/monitor-detail/history-chart.test.ts  |  305 +
 web-next/lib/monitor-detail/history-chart.ts       |  431 +
 web-next/lib/monitor-detail/view-model.test.ts     |  550 ++
 web-next/lib/monitor-detail/view-model.ts          |  837 ++
 web-next/lib/monitor-editor/controller.test.ts     |  195 +
 web-next/lib/monitor-editor/controller.ts          |  368 +
 web-next/lib/monitor-editor/localized-text.test.ts |   19 +
 web-next/lib/monitor-editor/localized-text.ts      |    7 +
 web-next/lib/monitor-editor/navigation.test.ts     |   29 +
 web-next/lib/monitor-editor/navigation.ts          |   10 +
 web-next/lib/monitor-manage/controller.test.ts     |   81 +
 web-next/lib/monitor-manage/controller.ts          |   87 +
 .../lib/monitor-manage/display-mapping.test.ts     |   20 +
 web-next/lib/monitor-manage/display-mapping.ts     |   16 +
 web-next/lib/monitor-manage/navigation.test.ts     |   99 +
 web-next/lib/monitor-manage/navigation.ts          |  102 +
 web-next/lib/monitor-manage/query-state.test.ts    |  118 +
 web-next/lib/monitor-manage/query-state.ts         |   88 +
 web-next/lib/monitor-manage/view-model.test.ts     |  111 +
 web-next/lib/monitor-manage/view-model.ts          |   88 +
 web-next/lib/nav.test.ts                           |  128 +
 web-next/lib/nav.ts                                |  698 ++
 web-next/lib/object-store/controller.test.ts       |   22 +
 web-next/lib/object-store/controller.ts            |   13 +
 web-next/lib/object-store/view-model.test.ts       |   97 +
 web-next/lib/object-store/view-model.ts            |   78 +
 web-next/lib/ops-surface/view-model.test.ts        |   23 +
 web-next/lib/ops-surface/view-model.ts             |   17 +
 web-next/lib/otlp-center/controller.test.ts        |   49 +
 web-next/lib/otlp-center/controller.ts             |   60 +
 web-next/lib/otlp-center/view-model.test.ts        |  172 +
 web-next/lib/otlp-center/view-model.ts             |  219 +
 web-next/lib/otlp-metrics/controller.test.ts       |  121 +
 web-next/lib/otlp-metrics/controller.ts            |   77 +
 .../otlp-metrics/three-signal-demo-seed.test.ts    |   93 +
 web-next/lib/otlp-metrics/view-model.test.ts       |  687 ++
 web-next/lib/otlp-metrics/view-model.ts            |  526 +
 web-next/lib/overview/navigation.test.ts           |   61 +
 web-next/lib/overview/navigation.ts                |   60 +
 web-next/lib/overview/view-model.test.ts           |  107 +
 web-next/lib/overview/view-model.ts                |  472 +
 web-next/lib/parity/route-manifest.json            | 2539 +++++
 web-next/lib/parity/route-manifest.test.ts         | 1832 ++++
 web-next/lib/parity/route-manifest.ts              |  154 +
 web-next/lib/parity/route-parity-spec.test.ts      |  144 +
 web-next/lib/parity/route-parity-spec.ts           |  530 ++
 web-next/lib/passport-lock/view-model.test.ts      |   21 +
 web-next/lib/passport-lock/view-model.ts           |   16 +
 web-next/lib/passport-login/controller.test.ts     |  106 +
 web-next/lib/passport-login/controller.ts          |  102 +
 web-next/lib/passport-login/view-model.test.ts     |   39 +
 web-next/lib/passport-login/view-model.ts          |   41 +
 web-next/lib/plugin-manage/controller.test.ts      |   45 +
 web-next/lib/plugin-manage/controller.ts           |   50 +
 web-next/lib/plugin-manage/query-state.test.ts     |   12 +
 web-next/lib/plugin-manage/query-state.ts          |    9 +
 web-next/lib/plugin-manage/view-model.test.ts      |   68 +
 web-next/lib/plugin-manage/view-model.ts           |   43 +
 web-next/lib/route-labels.ts                       |   25 +
 web-next/lib/setting-config/controller.test.ts     |   66 +
 web-next/lib/setting-config/controller.ts          |   41 +
 web-next/lib/setting-config/view-model.test.ts     |   95 +
 web-next/lib/setting-config/view-model.ts          |   95 +
 web-next/lib/setting-define/controller.test.ts     |   32 +
 web-next/lib/setting-define/controller.ts          |   47 +
 web-next/lib/setting-define/query-state.test.ts    |   16 +
 web-next/lib/setting-define/query-state.ts         |   12 +
 web-next/lib/setting-define/view-model.test.ts     |   73 +
 web-next/lib/setting-define/view-model.ts          |   85 +
 web-next/lib/setting-server/controller.test.ts     |   31 +
 web-next/lib/setting-server/controller.ts          |   21 +
 web-next/lib/setting-server/view-model.test.ts     |  154 +
 web-next/lib/setting-server/view-model.ts          |  292 +
 .../lib/setting-settings-layout/view-model.test.ts |   16 +
 .../lib/setting-settings-layout/view-model.tsx     |   32 +
 web-next/lib/setting-status/controller.test.ts     |   89 +
 web-next/lib/setting-status/controller.ts          |   82 +
 web-next/lib/setting-status/view-model.test.ts     |  297 +
 web-next/lib/setting-status/view-model.ts          |  297 +
 web-next/lib/setting-token/controller.test.ts      |   29 +
 web-next/lib/setting-token/controller.ts           |   24 +
 web-next/lib/setting-token/view-model.test.ts      |   87 +
 web-next/lib/setting-token/view-model.ts           |   57 +
 web-next/lib/settings-surface/view-model.test.ts   |  156 +
 web-next/lib/settings-surface/view-model.ts        |  192 +
 web-next/lib/shell/sidebar.test.ts                 |   46 +
 web-next/lib/shell/sidebar.ts                      |   75 +
 web-next/lib/signal-route-context.test.ts          |  245 +
 web-next/lib/signal-route-context.ts               |  369 +
 web-next/lib/status-center/controller.test.ts      |  212 +
 web-next/lib/status-center/controller.ts           |  227 +
 web-next/lib/status-center/display.ts              |  350 +
 web-next/lib/status-center/view-model.test.ts      |  255 +
 web-next/lib/status-center/view-model.ts           |  216 +
 web-next/lib/time-context.test.ts                  |  164 +
 web-next/lib/time-context.ts                       |  272 +
 web-next/lib/topology-surface/view-model.test.ts   |  530 ++
 web-next/lib/topology-surface/view-model.ts        |  924 ++
 web-next/lib/trace-manage/controller.test.ts       |   28 +
 web-next/lib/trace-manage/controller.ts            |   20 +
 web-next/lib/trace-manage/display-mapping.test.ts  |   18 +
 web-next/lib/trace-manage/display-mapping.ts       |   17 +
 web-next/lib/trace-manage/query-state.test.ts      |   75 +
 web-next/lib/trace-manage/query-state.ts           |   69 +
 web-next/lib/trace-manage/span-derivation.test.ts  |  114 +
 web-next/lib/trace-manage/span-derivation.ts       |   73 +
 web-next/lib/trace-manage/view-model.test.ts       |  629 ++
 web-next/lib/trace-manage/view-model.ts            |  511 +
 web-next/lib/types.ts                              | 1001 ++
 web-next/lib/utils.ts                              |    6 +
 web-next/lib/workbench-load-cache.test.ts          |   48 +
 web-next/lib/workbench-load-cache.ts               |   21 +
 web-next/lib/workbench-theme.test.ts               |   67 +
 web-next/lib/workbench-theme.ts                    |   57 +
 web-next/lib/workspace-navigation.test.ts          |  161 +
 web-next/lib/workspace-navigation.ts               |  255 +
 web-next/next.config.mjs                           |   26 +
 web-next/package-lock.json                         | 9995 ++++++++++++++++++++
 web-next/package.json                              |   56 +
 web-next/postcss.config.js                         |    6 +
 web-next/public/assets/bg.png                      |  Bin 0 -> 154582 bytes
 web-next/public/assets/brand.svg                   |    6 +
 web-next/public/assets/brand_white.svg             |    6 +
 .../public/assets/img/integration/alibabacloud.svg |   15 +
 .../public/assets/img/integration/huaweicloud.svg  |    1 +
 .../public/assets/img/integration/prometheus.svg   |    1 +
 .../public/assets/img/integration/skywalking.svg   |    6 +
 web-next/public/assets/img/integration/tencent.svg |    1 +
 .../public/assets/img/integration/uptime-kuma.svg  |    9 +
 .../public/assets/img/integration/volcengine.svg   |   33 +
 web-next/public/assets/img/integration/zabbix.svg  |   35 +
 web-next/public/assets/logo.svg                    |    4 +
 web-next/public/assets/logo_white.svg              |    4 +
 web-next/scripts/bulletin-browser-smoke.spec.ts    |   27 +
 web-next/scripts/dashboard-smoke-lib.mjs           |   38 +
 web-next/scripts/dashboard-smoke-lib.test.ts       |   66 +
 web-next/scripts/dashboard-smoke.mjs               |   38 +
 .../scripts/entity-editor-browser-smoke.spec.ts    |  235 +
 web-next/scripts/i18n-report.mjs                   |   75 +
 web-next/scripts/i18n-report.test.ts               |   51 +
 web-next/scripts/log-manage-browser-smoke.spec.ts  |  457 +
 web-next/scripts/log-manage-smoke-lib.mjs          |  110 +
 web-next/scripts/log-manage-smoke-lib.test.ts      |  147 +
 web-next/scripts/log-manage-smoke.mjs              |   38 +
 .../scripts/monitor-route-browser-smoke.spec.ts    |  435 +
 web-next/scripts/monitor-route-smoke-lib.mjs       |   99 +
 web-next/scripts/monitor-route-smoke-lib.test.ts   |   93 +
 web-next/scripts/monitor-route-smoke.mjs           |  159 +
 .../parity/angular-reference-server-lib.mjs        |  384 +
 .../parity/angular-reference-server-lib.test.ts    |  521 +
 .../scripts/parity/angular-reference-server.mjs    |  161 +
 web-next/scripts/parity/harness-lib.mjs            |  794 ++
 web-next/scripts/parity/harness-lib.test.ts        |  855 ++
 web-next/scripts/parity/harness-plan.mjs           |  104 +
 web-next/scripts/parity/harness-plan.test.ts       |  274 +
 web-next/scripts/parity/harness-targets.mjs        |   48 +
 web-next/scripts/parity/harness-targets.test.ts    |   44 +
 web-next/scripts/parity/harness.mjs                |  746 ++
 web-next/scripts/parity/owner-contracts.test.ts    |   46 +
 web-next/scripts/parity/runtime-lib.mjs            |   65 +
 web-next/scripts/parity/runtime-lib.test.ts        |   34 +
 web-next/scripts/parity/runtime.mjs                |  264 +
 web-next/scripts/parity/seed-state-lib.mjs         |  201 +
 web-next/scripts/parity/seed-state-lib.test.ts     |  239 +
 .../scripts/passport-login-browser-smoke.spec.ts   |   61 +
 web-next/scripts/passport-login-smoke-lib.mjs      |   23 +
 web-next/scripts/passport-login-smoke-lib.test.ts  |   22 +
 web-next/scripts/passport-login-smoke.mjs          |   82 +
 web-next/scripts/release-shell-smoke.mjs           |  150 +
 web-next/scripts/release-shell-smoke.test.ts       |   36 +
 web-next/scripts/release-shell.mjs                 |  100 +
 web-next/scripts/return-label-smoke-guard.test.ts  |   20 +
 web-next/scripts/route-matrix.mjs                  |  162 +
 web-next/scripts/status-public-smoke-lib.mjs       |   16 +
 web-next/scripts/status-public-smoke-lib.test.ts   |   41 +
 web-next/scripts/status-public-smoke.mjs           |  122 +
 web-next/scripts/system-config-smoke-lib.mjs       |  194 +
 web-next/scripts/system-config-smoke-lib.test.ts   |  140 +
 web-next/scripts/system-config-smoke.mjs           |   71 +
 .../three-signal-workbench-browser-smoke.spec.ts   |  211 +
 .../three-signal-workbench-browser-smoke.test.ts   |   19 +
 .../scripts/three-signal-workbench-smoke-lib.mjs   |  110 +
 .../three-signal-workbench-smoke-lib.test.ts       |  123 +
 .../scripts/time-context-browser-smoke.spec.ts     |  848 ++
 .../scripts/trace-manage-browser-smoke.spec.ts     |  292 +
 web-next/scripts/trace-manage-smoke-lib.mjs        |   82 +
 web-next/scripts/trace-manage-smoke-lib.test.ts    |  105 +
 web-next/scripts/trace-manage-smoke.mjs            |   38 +
 web-next/tailwind.config.ts                        |   54 +
 web-next/test/i18n-test-helper.ts                  | 1798 ++++
 web-next/tsconfig.json                             |   43 +
 web-next/vitest.config.ts                          |   20 +
 1615 files changed, 256451 insertions(+), 11912 deletions(-)

diff --git a/.github/workflows/frontend-build-test.yml b/.github/workflows/frontend-build-test.yml
index 6c28cf999d..8daadcec93 100644
--- a/.github/workflows/frontend-build-test.yml
+++ b/.github/workflows/frontend-build-test.yml
@@ -18,19 +18,42 @@
 # This workflow will build a Java project with Maven
 # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
 
-name: Frontend CI 
+name: Frontend CI
 
 on:
   push:
     branches: [ master, dev, action* ]
     paths:
       - 'web-app/**'
+      - 'web-next/**'
+      - 'web-app/src/assets/i18n/**'
+      - '.github/workflows/frontend-build-test.yml'
   pull_request:
     branches: [ master, dev ]
     paths:
       - 'web-app/**'
+      - 'web-next/**'
+      - 'web-app/src/assets/i18n/**'
+      - '.github/workflows/frontend-build-test.yml'
 
 jobs:
+  web-next-scope:
+    runs-on: ubuntu-latest
+    outputs:
+      web_next: ${{ steps.web_next_scope.outputs.web_next }}
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+    - id: web_next_scope
+      uses: dorny/paths-filter@v3
+      with:
+        filters: |
+          web_next:
+            - 'web-next/**'
+            - 'web-app/src/assets/i18n/**'
+            - '.github/workflows/frontend-build-test.yml'
+
   build:
     runs-on: ubuntu-latest
     steps:
@@ -44,3 +67,43 @@ jobs:
     - name: EsLint Test
       working-directory: web-app
       run: pnpm lint:ts
+
+  web-next-contract:
+    needs: web-next-scope
+    if: ${{ needs.web-next-scope.outputs.web_next == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - uses: actions/setup-node@v4
+      with:
+        node-version: 22
+        cache: npm
+        cache-dependency-path: web-next/package-lock.json
+    - name: Install
+      working-directory: web-next
+      run: npm ci
+    - name: Contract Test
+      working-directory: web-next
+      run: npm test
+
+  web-next-standards:
+    needs:
+    - web-next-scope
+    - web-next-contract
+    if: ${{ needs.web-next-scope.outputs.web_next == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+    - uses: actions/setup-node@v4
+      with:
+        node-version: 22
+        cache: npm
+        cache-dependency-path: web-next/package-lock.json
+    - name: Install
+      working-directory: web-next
+      run: npm ci
+    - name: verify:full (blocking)
+      working-directory: web-next
+      run: npm run verify:full
diff --git a/.gitignore b/.gitignore
index 0c6673035e..8e3b8815b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,6 +46,14 @@ nbdist/
 node_modules
 .docusaurus
 
+# codex / browser / screenshot noise
+.codex-artifacts/
+.playwright-mcp/
+web-next-*.png
+
+# local temp runtime data
+.tmp/
+
 # h2 data
 *.mv.db
 *.trace.db
@@ -56,3 +64,7 @@ application-mysql.yml
 application-pg.yml
 /package.json
 /yarn.lock
+.omx/
+
+# legacy frontend build output
+/web-app/dist/
diff --git a/hertzbeat-alerter/pom.xml b/hertzbeat-alerter/pom.xml
index 7af87836e9..c3a3890d2c 100644
--- a/hertzbeat-alerter/pom.xml
+++ b/hertzbeat-alerter/pom.xml
@@ -40,6 +40,11 @@
           <artifactId>hertzbeat-common-core</artifactId>
           <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hertzbeat</groupId>
+            <artifactId>hertzbeat-common-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <!-- plugin -->
         <dependency>
             <groupId>org.apache.hertzbeat</groupId>
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java
index 23426ddcf0..b8abee9edf 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java
@@ -18,12 +18,20 @@
 package org.apache.hertzbeat.alert;
 
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
+import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 /**
@@ -31,16 +39,25 @@ import org.springframework.stereotype.Component;
  */
 @Component
 @Slf4j
-public class AlerterWorkerPool {
+public class AlerterWorkerPool implements DisposableBean {
 
     private ThreadPoolExecutor workerExecutor;
-    private ThreadPoolExecutor notifyExecutor;
-    private ThreadPoolExecutor logWorkerExecutor;
+    private ManagedExecutor notifyExecutor;
+    private ManagedExecutor logWorkerExecutor;
+    private Map<Byte, Semaphore> notifyChannelPermits;
+    private int notifyMaxConcurrentPerChannel;
 
     public AlerterWorkerPool() {
+        this(VirtualThreadProperties.defaults());
+    }
+
+    @Autowired
+    public AlerterWorkerPool(VirtualThreadProperties virtualThreadProperties) {
+        VirtualThreadProperties properties =
+                virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
         initWorkExecutor();
-        initNotifyExecutor();
-        initLogWorkerExecutor();
+        initNotifyExecutor(properties);
+        initLogWorkerExecutor(properties);
     }
 
     private void initWorkExecutor() {
@@ -61,16 +78,32 @@ public class AlerterWorkerPool {
                 new ThreadPoolExecutor.AbortPolicy());
     }
 
-    private void initNotifyExecutor() {
+    private void initNotifyExecutor(VirtualThreadProperties properties) {
+        Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
+            log.error("Alerter notifyExecutor has uncaughtException.");
+            log.error(throwable.getMessage(), throwable);
+        };
+        if (properties.enabled()) {
+            VirtualThreadProperties.AlerterProperties alerterProperties = properties.alerter();
+            VirtualThreadProperties.PoolProperties notifyProperties = alerterProperties.notifyPool();
+            notifyMaxConcurrentPerChannel = Math.max(1, alerterProperties.notifyMaxConcurrentPerChannel());
+            notifyChannelPermits = new ConcurrentHashMap<>(8);
+            notifyExecutor = ManagedExecutors.newVirtualExecutor("notify-worker", "notify-worker-",
+                    notifyProperties.mode(), notifyProperties.maxConcurrentJobs(), handler);
+            return;
+        }
+        notifyMaxConcurrentPerChannel = 0;
+        notifyChannelPermits = null;
+        notifyExecutor = ManagedExecutors.wrap("notify-worker", createLegacyNotifyExecutor(handler));
+    }
+
+    private ThreadPoolExecutor createLegacyNotifyExecutor(Thread.UncaughtExceptionHandler handler) {
         ThreadFactory threadFactory = new ThreadFactoryBuilder()
-                .setUncaughtExceptionHandler((thread, throwable) -> {
-                    log.error("Alerter notifyExecutor has uncaughtException.");
-                    log.error(throwable.getMessage(), throwable);
-                })
+                .setUncaughtExceptionHandler(handler)
                 .setDaemon(true)
                 .setNameFormat("notify-worker-%d")
                 .build();
-        notifyExecutor = new ThreadPoolExecutor(6,
+        return new ThreadPoolExecutor(6,
                 6,
                 10,
                 TimeUnit.SECONDS,
@@ -79,16 +112,27 @@ public class AlerterWorkerPool {
                 new ThreadPoolExecutor.AbortPolicy());
     }
 
-    private void initLogWorkerExecutor() {
+    private void initLogWorkerExecutor(VirtualThreadProperties properties) {
+        Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
+            log.error("Alerter logWorkerExecutor has uncaughtException.");
+            log.error(throwable.getMessage(), throwable);
+        };
+        if (properties.enabled()) {
+            VirtualThreadProperties.QueueProperties logWorkerProperties = properties.alerter().logWorker();
+            logWorkerExecutor = ManagedExecutors.newQueuedVirtualExecutor("alerter-log-worker", "log-worker-",
+                    logWorkerProperties.maxConcurrentJobs(), logWorkerProperties.queueCapacity(), handler);
+            return;
+        }
+        logWorkerExecutor = ManagedExecutors.wrap("alerter-log-worker", createLegacyLogWorkerExecutor(handler));
+    }
+
+    private ThreadPoolExecutor createLegacyLogWorkerExecutor(Thread.UncaughtExceptionHandler handler) {
         ThreadFactory threadFactory = new ThreadFactoryBuilder()
-                .setUncaughtExceptionHandler((thread, throwable) -> {
-                    log.error("Alerter logWorkerExecutor has uncaughtException.");
-                    log.error(throwable.getMessage(), throwable);
-                })
+                .setUncaughtExceptionHandler(handler)
                 .setDaemon(true)
                 .setNameFormat("log-worker-%d")
                 .build();
-        logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
+        return new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
                 new LinkedBlockingQueue<>(1000),
                 threadFactory,
                 new ThreadPoolExecutor.AbortPolicy());
@@ -113,6 +157,41 @@ public class AlerterWorkerPool {
         notifyExecutor.execute(runnable);
     }
 
+    /**
+     * Executes the given runnable task using the notify executor with per-channel concurrency control.
+     *
+     * @param channelType notification channel type
+     * @param runnable the task to be executed
+     * @throws RejectedExecutionException if the task cannot be accepted for execution
+     */
+    public void executeNotify(byte channelType, Runnable runnable) throws RejectedExecutionException {
+        if (notifyChannelPermits == null) {
+            notifyExecutor.execute(runnable);
+            return;
+        }
+        Semaphore semaphore = notifyChannelPermits.computeIfAbsent(channelType,
+                key -> new Semaphore(notifyMaxConcurrentPerChannel));
+        if (!semaphore.tryAcquire()) {
+            throw new RejectedExecutionException(
+                    "notify-worker rejected task because channel concurrency limit was reached for type " + channelType);
+        }
+        boolean submitted = false;
+        try {
+            notifyExecutor.execute(() -> {
+                try {
+                    runnable.run();
+                } finally {
+                    semaphore.release();
+                }
+            });
+            submitted = true;
+        } finally {
+            if (!submitted) {
+                semaphore.release();
+            }
+        }
+    }
+
     /**
      * Executes the given runnable task using the logWorkerExecutor.
      *
@@ -122,4 +201,11 @@ public class AlerterWorkerPool {
     public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
         logWorkerExecutor.execute(runnable);
     }
+
+    @Override
+    public void destroy() {
+        workerExecutor.shutdownNow();
+        notifyExecutor.close();
+        logWorkerExecutor.close();
+    }
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManager.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManager.java
index edf4fef6b8..1558bd3bb5 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManager.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManager.java
@@ -27,6 +27,8 @@ import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.springframework.stereotype.Component;
 
 import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -78,10 +80,18 @@ public class AlarmCacheManager {
         this.pendingAlertMap.remove(String.valueOf(defineId), fingerPrint);
     }
 
+    public Map<String, SingleAlert> getPendingAlerts(Long defineId) {
+        return new HashMap<>(this.pendingAlertMap.row(String.valueOf(defineId)));
+    }
+
     public void putFiring(Long defineId, String fingerPrint, SingleAlert alert) {
         this.firingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
     }
 
+    public Map<String, SingleAlert> getFiringAlerts(Long defineId) {
+        return new HashMap<>(this.firingAlertMap.row(String.valueOf(defineId)));
+    }
+
     public void putFiring(String fingerPrint, SingleAlert alert) {
         this.firingAlertMap.put(getCustomKey(fingerPrint), fingerPrint, alert);
     }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculator.java
index 6e4e038727..cb7f1e3726 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculator.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculator.java
@@ -40,7 +40,7 @@ import java.util.Map;
 @Slf4j
 @Component
 public class LogPeriodicAlertCalculator {
-    
+
     private static final String ROWS = "__rows__";
 
     private final DataSourceService dataSourceService;
@@ -67,7 +67,8 @@ public class LogPeriodicAlertCalculator {
     private void doCalculate(AlertDefine define) {
         try {
             // Log-based queries are SQL queries with log-specific expressions
-            List<Map<String, Object>> results = dataSourceService.query(define.getDatasource(), define.getExpr());
+            List<Map<String, Object>> results = dataSourceService.query(define.getDatasource(), define.getExpr(),
+                    CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
             if (CollectionUtils.isEmpty(results)) {
                 return;
             }
@@ -229,4 +230,4 @@ public class LogPeriodicAlertCalculator {
         return annotations;
     }
 
-}
\ No newline at end of file
+}
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleScheduler.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleScheduler.java
index 978fb3d234..5182fb64ab 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleScheduler.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleScheduler.java
@@ -19,18 +19,26 @@ package org.apache.hertzbeat.alert.calculate.periodic;
 
 import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
 import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
+import static org.apache.hertzbeat.common.constants.CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.alert.dao.AlertDefineDao;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.stereotype.Component;
 import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
@@ -40,18 +48,32 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
  */
 @Slf4j
 @Component
-public class PeriodicAlertRuleScheduler implements CommandLineRunner {
+public class PeriodicAlertRuleScheduler implements CommandLineRunner, DisposableBean {
 
     private final MetricsPeriodicAlertCalculator metricsCalculator;
     private final LogPeriodicAlertCalculator logCalculator;
+    private final TracePeriodicAlertCalculator traceCalculator;
     private final AlertDefineDao alertDefineDao;
     private final ScheduledExecutorService scheduledExecutor;
-    private final Map<Long, ScheduledFuture<?>> scheduledFutures;
+    private final ExecutorService periodicExecutor;
+    private final Semaphore periodicPermits;
+    private final boolean virtualThreadsEnabled;
+    private final Map<Long, ScheduledTaskState> scheduledTasks;
 
-    public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) {
+    @Autowired
+    public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator,
+                                      LogPeriodicAlertCalculator logCalculator,
+                                      TracePeriodicAlertCalculator traceCalculator,
+                                      AlertDefineDao alertDefineDao,
+                                      VirtualThreadProperties virtualThreadProperties) {
         this.metricsCalculator = metricsCalculator;
         this.logCalculator = logCalculator;
+        this.traceCalculator = traceCalculator;
         this.alertDefineDao = alertDefineDao;
+        Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
+            log.error("Scheduled periodic alert threshold has uncaughtException.");
+            log.error(throwable.getMessage(), throwable);
+        };
         ThreadFactory threadFactory = new ThreadFactoryBuilder()
                 .setUncaughtExceptionHandler((thread, throwable) -> {
                     log.error("Scheduled periodic alert threshold has uncaughtException.");
@@ -61,17 +83,27 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
                 .setNameFormat("periodic-alert-threshold-worker-%d")
                 .build();
         this.scheduledExecutor = Executors.newScheduledThreadPool(10, threadFactory);
-        this.scheduledFutures = new ConcurrentHashMap<>();
+        VirtualThreadProperties properties =
+                virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
+        this.virtualThreadsEnabled = properties.enabled();
+        int maxConcurrentPeriodicTasks = Math.max(1, properties.alerter().periodicMaxConcurrentJobs());
+        this.periodicExecutor = virtualThreadsEnabled
+                ? Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
+                .name("periodic-alert-task-", 0)
+                .uncaughtExceptionHandler(handler)
+                .factory())
+                : null;
+        this.periodicPermits = virtualThreadsEnabled ? new Semaphore(maxConcurrentPeriodicTasks) : null;
+        this.scheduledTasks = new ConcurrentHashMap<>();
     }
 
     public void cancelSchedule(Long ruleId) {
         if (ruleId == null) {
             return;
         }
-        ScheduledFuture<?> future = scheduledFutures.get(ruleId);
-        if (future != null) {
-            future.cancel(true);
-            scheduledFutures.remove(ruleId);
+        ScheduledTaskState state = scheduledTasks.remove(ruleId);
+        if (state != null) {
+            state.cancel();
         }
     }
 
@@ -81,16 +113,13 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
             return;
         }
         cancelSchedule(rule.getId());
-        if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
-                || rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
-            ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
-                if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
-                    metricsCalculator.calculate(rule);
-                } else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
-                    logCalculator.calculate(rule);
-                }
-            }, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
-            scheduledFutures.put(rule.getId(), future);
+        if (isPeriodicRule(rule.getType())) {
+            ScheduledTaskState state = new ScheduledTaskState(rule);
+            ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(
+                    virtualThreadsEnabled ? state::trigger : () -> executeRule(rule),
+                    0, rule.getPeriod(), TimeUnit.SECONDS);
+            state.setScheduledFuture(future);
+            scheduledTasks.put(rule.getId(), state);
         }
     }
 
@@ -99,11 +128,125 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
         log.info("Starting periodic alert rule scheduler...");
         List<AlertDefine> metricsPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
         List<AlertDefine> logPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
-        List<AlertDefine> periodicRules = new ArrayList<>(metricsPeriodicRules.size() + logPeriodicRules.size());
+        List<AlertDefine> tracePeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC);
+        List<AlertDefine> periodicRules = new ArrayList<>(metricsPeriodicRules.size() + logPeriodicRules.size()
+                + tracePeriodicRules.size());
         periodicRules.addAll(metricsPeriodicRules);
         periodicRules.addAll(logPeriodicRules);
+        periodicRules.addAll(tracePeriodicRules);
         for (AlertDefine rule : periodicRules) {
             updateSchedule(rule);
         }
     }
+
+    @Override
+    public void destroy() {
+        scheduledTasks.values().forEach(ScheduledTaskState::cancel);
+        scheduledTasks.clear();
+        scheduledExecutor.shutdownNow();
+        if (periodicExecutor != null) {
+            periodicExecutor.shutdownNow();
+        }
+    }
+
+    private void executeRule(AlertDefine rule) {
+        if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
+            metricsCalculator.calculate(rule);
+        } else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
+            logCalculator.calculate(rule);
+        } else if (rule.getType().equals(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC)) {
+            traceCalculator.calculate(rule);
+        }
+    }
+
+    private boolean isPeriodicRule(String type) {
+        return METRIC_ALERT_THRESHOLD_TYPE_PERIODIC.equals(type)
+                || LOG_ALERT_THRESHOLD_TYPE_PERIODIC.equals(type)
+                || TRACE_ALERT_THRESHOLD_TYPE_PERIODIC.equals(type);
+    }
+
+    private final class ScheduledTaskState {
+
+        private final AlertDefine rule;
+        private ScheduledFuture<?> scheduledFuture;
+        private Future<?> runningFuture;
+        private boolean running;
+        private boolean pending;
+        private boolean cancelled;
+
+        private ScheduledTaskState(AlertDefine rule) {
+            this.rule = rule;
+        }
+
+        private synchronized void setScheduledFuture(ScheduledFuture<?> scheduledFuture) {
+            this.scheduledFuture = scheduledFuture;
+        }
+
+        private synchronized void trigger() {
+            if (cancelled) {
+                return;
+            }
+            if (running) {
+                pending = true;
+                return;
+            }
+            running = true;
+            submitLocked();
+        }
+
+        private synchronized void cancel() {
+            cancelled = true;
+            pending = false;
+            ScheduledFuture<?> periodicFuture = scheduledFuture;
+            Future<?> currentFuture = runningFuture;
+            if (periodicFuture != null) {
+                periodicFuture.cancel(true);
+            }
+            if (currentFuture != null) {
+                currentFuture.cancel(true);
+            }
+        }
+
+        private void submitLocked() {
+            try {
+                runningFuture = periodicExecutor.submit(() -> {
+                    boolean permitAcquired = false;
+                    try {
+                        periodicPermits.acquire();
+                        permitAcquired = true;
+                        if (!Thread.currentThread().isInterrupted()) {
+                            executeRule(rule);
+                        }
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                    } catch (Exception e) {
+                        log.error("Periodic alert rule {} execution error: {}", rule.getName(), e.getMessage(), e);
+                    } finally {
+                        if (permitAcquired) {
+                            periodicPermits.release();
+                        }
+                        onComplete();
+                    }
+                });
+            } catch (RuntimeException e) {
+                running = false;
+                throw e;
+            }
+        }
+
+        private synchronized void onComplete() {
+            runningFuture = null;
+            if (cancelled) {
+                running = false;
+                pending = false;
+                return;
+            }
+            if (!pending) {
+                running = false;
+                return;
+            }
+            pending = false;
+            submitLocked();
+        }
+    }
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/TracePeriodicAlertCalculator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/TracePeriodicAlertCalculator.java
new file mode 100644
index 0000000000..e54857970d
--- /dev/null
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/TracePeriodicAlertCalculator.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.calculate.periodic;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
+import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
+import org.apache.hertzbeat.alert.service.DataSourceService;
+import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
+import org.apache.hertzbeat.alert.util.AlertUtil;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.springframework.stereotype.Component;
+
+/**
+ * Periodic trace alert calculator.
+ */
+@Slf4j
+@Component
+public class TracePeriodicAlertCalculator {
+
+    private static final String VALUE = "__value__";
+    private static final String TIMESTAMP = "__timestamp__";
+
+    private final DataSourceService dataSourceService;
+    private final AlarmCommonReduce alarmCommonReduce;
+    private final AlarmCacheManager alarmCacheManager;
+
+    public TracePeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce,
+                                        AlarmCacheManager alarmCacheManager) {
+        this.dataSourceService = dataSourceService;
+        this.alarmCommonReduce = alarmCommonReduce;
+        this.alarmCacheManager = alarmCacheManager;
+    }
+
+    /**
+     * Calculate one periodic trace alert definition.
+     *
+     * @param define alert define
+     */
+    public void calculate(AlertDefine define) {
+        if (define == null || !define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
+            log.error("Trace periodic define {} is disabled or expression is empty",
+                    define == null ? null : define.getName());
+            return;
+        }
+
+        List<Map<String, Object>> results;
+        try {
+            results = dataSourceService.query(define.getDatasource(), define.getExpr(),
+                    CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC);
+        } catch (Exception ex) {
+            log.error("Calculate trace periodic define {} query failed: {}", define.getName(), ex.getMessage());
+            return;
+        }
+
+        if (containsMissingValueColumn(results)) {
+            log.warn("Trace periodic define {} query result must include {} column.", define.getName(), VALUE);
+            return;
+        }
+
+        if (CollectionUtils.isEmpty(results)) {
+            recoverAll(define);
+            return;
+        }
+
+        Set<String> matchedFingerprints = new HashSet<>();
+        long currentTimeMillis = System.currentTimeMillis();
+        for (Map<String, Object> result : results) {
+            Map<String, String> fingerprints = createFingerprints(define, result);
+            String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
+            matchedFingerprints.add(fingerprint);
+            if (result.get(VALUE) == null) {
+                recoverFingerprint(define.getId(), fingerprint);
+                continue;
+            }
+            Map<String, Object> fieldValueMap = createFieldValueMap(define, result, fingerprints);
+            afterThresholdRuleMatch(currentTimeMillis, fingerprint, fingerprints, fieldValueMap, define);
+        }
+        recoverUnmatched(define, matchedFingerprints);
+    }
+
+    private boolean containsMissingValueColumn(List<Map<String, Object>> results) {
+        if (CollectionUtils.isEmpty(results)) {
+            return false;
+        }
+        return results.stream().anyMatch(row -> row == null || !row.containsKey(VALUE));
+    }
+
+    private void afterThresholdRuleMatch(long currentTimeMillis, String fingerprint,
+                                         Map<String, String> fingerprints,
+                                         Map<String, Object> fieldValueMap,
+                                         AlertDefine define) {
+        Long defineId = define.getId();
+        SingleAlert firingAlert = alarmCacheManager.getFiring(defineId, fingerprint);
+        if (firingAlert != null) {
+            firingAlert.setActiveAt(currentTimeMillis);
+            return;
+        }
+
+        SingleAlert pendingAlert = alarmCacheManager.getPending(defineId, fingerprint);
+        int requiredTimes = define.getTimes() == null ? 1 : Math.max(1, define.getTimes());
+        if (pendingAlert == null) {
+            SingleAlert newAlert = SingleAlert.builder()
+                    .labels(fingerprints)
+                    .annotations(createAlertAnnotations(define, fieldValueMap))
+                    .content(AlertTemplateUtil.render(StringUtils.defaultString(define.getTemplate()), fieldValueMap))
+                    .status(CommonConstants.ALERT_STATUS_PENDING)
+                    .triggerTimes(1)
+                    .startAt(currentTimeMillis)
+                    .activeAt(currentTimeMillis)
+                    .build();
+            if (requiredTimes <= 1) {
+                newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
+                alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
+                alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
+            } else {
+                alarmCacheManager.putPending(defineId, fingerprint, newAlert);
+            }
+            return;
+        }
+
+        pendingAlert.setTriggerTimes(pendingAlert.getTriggerTimes() + 1);
+        pendingAlert.setActiveAt(currentTimeMillis);
+        if (pendingAlert.getTriggerTimes() >= requiredTimes) {
+            alarmCacheManager.removePending(defineId, fingerprint);
+            pendingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
+            alarmCacheManager.putFiring(defineId, fingerprint, pendingAlert);
+            alarmCommonReduce.reduceAndSendAlarm(pendingAlert.clone());
+        }
+    }
+
+    private Map<String, String> createFingerprints(AlertDefine define, Map<String, Object> result) {
+        Map<String, String> fingerprints = new HashMap<>(8);
+        if (define.getLabels() != null) {
+            fingerprints.putAll(define.getLabels());
+        }
+        fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
+        fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
+        for (Map.Entry<String, Object> entry : result.entrySet()) {
+            if (entry.getValue() != null && !VALUE.equals(entry.getKey()) && !TIMESTAMP.equals(entry.getKey())) {
+                fingerprints.put(entry.getKey(), entry.getValue().toString());
+            }
+        }
+        return fingerprints;
+    }
+
+    private Map<String, Object> createFieldValueMap(AlertDefine define, Map<String, Object> result,
+                                                   Map<String, String> fingerprints) {
+        Map<String, Object> fieldValueMap = new HashMap<>(8);
+        fieldValueMap.putAll(fingerprints);
+        if (define.getLabels() != null) {
+            fieldValueMap.putAll(define.getLabels());
+        }
+        for (Map.Entry<String, Object> entry : result.entrySet()) {
+            if (entry.getValue() != null) {
+                fieldValueMap.put(entry.getKey(), entry.getValue());
+            }
+        }
+        return fieldValueMap;
+    }
+
+    private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
+        if (define.getAnnotations() == null) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> annotations = new HashMap<>(define.getAnnotations().size());
+        for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
+            annotations.put(entry.getKey(), AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
+        }
+        return annotations;
+    }
+
+    private void recoverAll(AlertDefine define) {
+        recoverCached(define.getId(), alarmCacheManager.getFiringAlerts(define.getId()).keySet(), true);
+        recoverCached(define.getId(), alarmCacheManager.getPendingAlerts(define.getId()).keySet(), false);
+    }
+
+    private void recoverUnmatched(AlertDefine define, Set<String> matchedFingerprints) {
+        Set<String> firingFingerprints = new HashSet<>(alarmCacheManager.getFiringAlerts(define.getId()).keySet());
+        firingFingerprints.removeAll(matchedFingerprints);
+        recoverCached(define.getId(), firingFingerprints, true);
+
+        Set<String> pendingFingerprints = new HashSet<>(alarmCacheManager.getPendingAlerts(define.getId()).keySet());
+        pendingFingerprints.removeAll(matchedFingerprints);
+        recoverCached(define.getId(), pendingFingerprints, false);
+    }
+
+    private void recoverCached(Long defineId, Set<String> fingerprints, boolean notifyResolved) {
+        for (String fingerprint : fingerprints) {
+            if (notifyResolved) {
+                recoverFingerprint(defineId, fingerprint);
+            } else {
+                alarmCacheManager.removePending(defineId, fingerprint);
+            }
+        }
+    }
+
+    private void recoverFingerprint(Long defineId, String fingerprint) {
+        SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
+        if (firingAlert != null) {
+            firingAlert.setTriggerTimes(1);
+            firingAlert.setEndAt(System.currentTimeMillis());
+            firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
+            alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
+        }
+        alarmCacheManager.removePending(defineId, fingerprint);
+    }
+}
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluator.java
index 1e245631d4..f80f6f8e6e 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluator.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluator.java
@@ -20,6 +20,11 @@ package org.apache.hertzbeat.alert.calculate.realtime.window;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
+import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
+import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
@@ -32,10 +37,6 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Alarm Evaluator - Final alarm logic trigger
@@ -47,42 +48,54 @@ import java.util.concurrent.TimeUnit;
  */
 @Component
 @Slf4j
-public class AlarmEvaluator {
+public class AlarmEvaluator implements DisposableBean {
 
     private static final String WINDOW_START_TIME = "window_start_time";
     private static final String WINDOW_END_TIME = "window_end_time";
     private static final String MATCHING_LOGS_COUNT = "matching_logs_count";
-    
+
     private final AlarmCommonReduce alarmCommonReduce;
-    private ThreadPoolExecutor workerExecutor;
-    
+    private final ManagedExecutor workerExecutor;
+
     public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) {
+        this(alarmCommonReduce, VirtualThreadProperties.defaults());
+    }
+
+    @Autowired
+    public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce, VirtualThreadProperties virtualThreadProperties) {
         this.alarmCommonReduce = alarmCommonReduce;
-        initAlarmEvaluator();
+        VirtualThreadProperties properties =
+                virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
+        this.workerExecutor = initAlarmEvaluator(properties);
     }
 
-    public void initAlarmEvaluator() {
-        ThreadFactory threadFactory = new ThreadFactoryBuilder()
-                .setUncaughtExceptionHandler((thread, throwable) -> {
-                    log.error("alerter-reduce-worker has uncaughtException.");
-                    log.error(throwable.getMessage(), throwable);
-                })
-                .setDaemon(true)
-                .setNameFormat("alerter-reduce-worker-%d")
-                .build();
-        workerExecutor = new ThreadPoolExecutor(2,
+    public ManagedExecutor initAlarmEvaluator(VirtualThreadProperties properties) {
+        Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
+            log.error("alerter-reduce-worker has uncaughtException.");
+            log.error(throwable.getMessage(), throwable);
+        };
+        if (properties.enabled()) {
+            VirtualThreadProperties.QueueProperties queueProperties = properties.alerter().windowEvaluator();
+            return ManagedExecutors.newQueuedVirtualExecutor("alerter-window-evaluator", "alerter-window-evaluator-",
+                    queueProperties.maxConcurrentJobs(), queueProperties.queueCapacity(), handler);
+        }
+        return ManagedExecutors.wrap("alerter-window-evaluator", new java.util.concurrent.ThreadPoolExecutor(2,
                 10,
                 10,
-                TimeUnit.SECONDS,
-                new LinkedBlockingQueue<>(),
-                threadFactory,
-                new ThreadPoolExecutor.AbortPolicy());
+                java.util.concurrent.TimeUnit.SECONDS,
+                new java.util.concurrent.LinkedBlockingQueue<>(),
+                new ThreadFactoryBuilder()
+                        .setUncaughtExceptionHandler(handler)
+                        .setDaemon(true)
+                        .setNameFormat("alerter-reduce-worker-%d")
+                        .build(),
+                new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()));
     }
 
     public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) {
         workerExecutor.execute(processWindowData(windowData));
     }
-    
+
     private Runnable processWindowData(WindowAggregator.WindowData windowData) {
         return () -> {
             AlertDefine alertDefine = windowData.getAlertDefine();
@@ -122,7 +135,7 @@ public class AlarmEvaluator {
             }
         };
     }
-    
+
     private void generateIndividualAlert(MatchingLogEvent matchingLog, long currentTime) {
         AlertDefine define = matchingLog.getAlertDefine();
         LogEntry logEntry = matchingLog.getLogEntry();
@@ -147,21 +160,21 @@ public class AlarmEvaluator {
                 .startAt(currentTime)
                 .activeAt(currentTime)
                 .build();
-        
+
         alarmCommonReduce.reduceAndSendAlarm(alert.clone());
-        
+
         log.debug("Generated individual alert for define: {}", define.getName());
     }
-    
-    private void generateGroupAlert(WindowAggregator.WindowData windowData, 
+
+    private void generateGroupAlert(WindowAggregator.WindowData windowData,
                                    List<MatchingLogEvent> matchingLogs, long currentTime) {
 
         List<SingleAlert> alerts = new ArrayList<>(matchingLogs.size());
         AlertDefine define = windowData.getAlertDefine();
-        
+
         // Create fingerprints for group alert
         Map<String, String> commonFingerPrints = createCommonFingerprints(define);
-        
+
         // Add window information to fingerprints
         commonFingerPrints.put(WINDOW_START_TIME, String.valueOf(windowData.getStartTime()));
         commonFingerPrints.put(WINDOW_END_TIME, String.valueOf(windowData.getEndTime()));
@@ -192,11 +205,11 @@ public class AlarmEvaluator {
             alerts.add(alert.clone());
         }
         alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
-        
-        log.debug("Generated group alert for define: {} with {} matching logs", 
+
+        log.debug("Generated group alert for define: {} with {} matching logs",
                  define.getName(), matchingLogs.size());
     }
-    
+
     private String getAlertMode(AlertDefine alertDefine) {
         String mode = null;
         if (alertDefine.getLabels() != null) {
@@ -208,40 +221,40 @@ public class AlarmEvaluator {
             return mode;
         }
     }
-    
+
     private Map<String, String> createCommonFingerprints(AlertDefine define) {
         Map<String, String> fingerprints = new HashMap<>(8);
         fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
         fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
-        
+
         if (define.getLabels() != null) {
             fingerprints.putAll(define.getLabels());
         }
-        
+
         return fingerprints;
     }
 
     private Map<String, Object> createFieldValueMap(LogEntry logEntry, AlertDefine define) {
         Map<String, Object> fieldValueMap = new HashMap<>(8);
         fieldValueMap.put("log", logEntry);
-        
+
         if (define.getLabels() != null) {
             fieldValueMap.putAll(define.getLabels());
         }
-        
+
         return fieldValueMap;
     }
-    
+
     private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
         Map<String, String> annotations = new HashMap<>(8);
-        
+
         if (define.getAnnotations() != null) {
             for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
-                annotations.put(entry.getKey(), 
+                annotations.put(entry.getKey(),
                                AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
             }
         }
-        
+
         return annotations;
     }
 
@@ -314,4 +327,9 @@ public class AlarmEvaluator {
             }
         }
     }
-}
\ No newline at end of file
+
+    @Override
+    public void destroy() {
+        workerExecutor.close();
+    }
+}
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java
index 04b9de2e47..13c13ce3e3 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java
@@ -107,7 +107,7 @@ public class AlertDefineController {
             description = "If the expression is formal, then the result of the query will be returned, otherwise it will respond with an error")
     public ResponseEntity<Message<List<Map<String, Object>>>> getDefinePreview(
             @Parameter(description = "Data Source Type", example = "promql") @PathVariable("datasource") String datasource,
-            @Parameter(description = "alert threshold type:realtime,periodic") @RequestParam String type,
+            @Parameter(description = "alert threshold type, e.g. periodic_metric, periodic_log, periodic_trace") @RequestParam String type,
             @Parameter(description = "alert threshold expression") @RequestParam String expr) {
         try {
             return ResponseEntity.ok(Message.successWithData(alertDefineService.getDefinePreview(datasource, type, expr)));
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java
index d3b49575ca..8d668b800e 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java
@@ -68,11 +68,12 @@ public class AlertsController {
     public ResponseEntity<Message<Page<GroupAlert>>> getGroupAlerts(
             @Parameter(description = "Alarm Status", example = "resolved") @RequestParam(required = false) String status,
             @Parameter(description = "Alarm content fuzzy query", example = "linux") @RequestParam(required = false) String search,
+            @Parameter(description = "Alarm severity", example = "critical") @RequestParam(required = false) String severity,
             @Parameter(description = "Sort field, default id", example = "name") @RequestParam(defaultValue = "gmtUpdate") String sort,
             @Parameter(description = "Sort Type", example = "desc") @RequestParam(defaultValue = "desc") String order,
             @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex,
             @Parameter(description = "Number of list pagination", example = "8") @RequestParam(defaultValue = "8") int pageSize) {
-        Page<GroupAlert> alertPage = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
+        Page<GroupAlert> alertPage = alertService.getGroupAlerts(status, search, severity, sort, order, pageIndex, pageSize);
         return ResponseEntity.ok(Message.success(alertPage));
     }
 
@@ -88,9 +89,10 @@ public class AlertsController {
     }
 
     @PutMapping(path = "/group/status/{status}")
-    @Operation(summary = "Batch modify alarm status, set firing or resolved", description = "Batch modify alarm status, set firing or resolved")
+    @Operation(summary = "Batch modify alarm status, set firing, acknowledged or resolved",
+            description = "Batch modify alarm status, set firing, acknowledged or resolved")
     public ResponseEntity<Message<Void>> applyAlertDefinesStatus(
-            @Parameter(description = "Alarm status value", example = "resolved") @PathVariable String status,
+            @Parameter(description = "Alarm status value", example = "acknowledged") @PathVariable String status,
             @Parameter(description = "Alarm List IDS", example = "6565463543") @RequestParam(required = false) List<Long> ids) {
         if (ids != null && status != null && !ids.isEmpty()) {
             alertService.editGroupAlertStatus(status, ids);
@@ -99,6 +101,19 @@ public class AlertsController {
         return ResponseEntity.ok(message);
     }
 
+    @PutMapping(path = "/status/{status}")
+    @Operation(summary = "Batch modify single alert status, set firing, acknowledged or resolved",
+            description = "Batch modify single alert status, set firing, acknowledged or resolved")
+    public ResponseEntity<Message<Void>> applySingleAlertStatus(
+            @Parameter(description = "Alarm status value", example = "acknowledged") @PathVariable String status,
+            @Parameter(description = "Alarm List IDS", example = "6565463543") @RequestParam(required = false) List<Long> ids) {
+        if (ids != null && status != null && !ids.isEmpty()) {
+            alertService.editSingleAlertStatus(status, ids);
+        }
+        Message<Void> message = Message.success();
+        return ResponseEntity.ok(message);
+    }
+
     @GetMapping(path = "/summary")
     @Operation(summary = "Get alarm statistics", description = "Get alarm statistics information")
     public ResponseEntity<Message<AlertSummary>> getAlertsSummary() {
@@ -106,5 +121,5 @@ public class AlertsController {
         Message<AlertSummary> message = Message.success(alertSummary);
         return ResponseEntity.ok(message);
     }
-    
+
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java
index 6d9207216b..b963badfb4 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java
@@ -21,6 +21,7 @@ import com.google.common.collect.Maps;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.concurrent.RejectedExecutionException;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.alert.AlerterWorkerPool;
 import org.apache.hertzbeat.alert.config.AlertSseManager;
@@ -104,7 +105,7 @@ public class AlertNoticeDispatch {
     private Optional<List<NoticeRule>> matchNoticeRulesByAlert(GroupAlert alert) {
         return Optional.ofNullable(noticeConfigService.getReceiverFilterRule(alert));
     }
-    
+
     public void dispatchAlarm(GroupAlert groupAlert) {
         if (groupAlert != null) {
             // Determining alarm type storage
@@ -121,14 +122,27 @@ public class AlertNoticeDispatch {
     }
 
     private void sendNotify(GroupAlert alert) {
-        matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> workerPool.executeNotify(() -> rule.getReceiverId()
-                .forEach(receiverId -> {
-                    try {
-                        sendNoticeMsg(getOneReceiverById(receiverId),
-                                getOneTemplateById(rule.getTemplateId()), alert);
-                    } catch (AlertNoticeException e) {
-                        log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
-                    }
-                }))));
+        matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> {
+            NoticeTemplate noticeTemplate = getOneTemplateById(rule.getTemplateId());
+            rule.getReceiverId().forEach(receiverId -> {
+                NoticeReceiver receiver = getOneReceiverById(receiverId);
+                if (receiver == null || receiver.getType() == null) {
+                    log.warn("DispatchTask skip invalid receiver, receiverId: {}, alertId: {}", receiverId, alert.getId());
+                    return;
+                }
+                try {
+                    workerPool.executeNotify(receiver.getType(), () -> {
+                        try {
+                            sendNoticeMsg(receiver, noticeTemplate, alert);
+                        } catch (AlertNoticeException e) {
+                            log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
+                        }
+                    });
+                } catch (RejectedExecutionException e) {
+                    log.warn("DispatchTask rejected notify task, receiverId: {}, type: {}, message: {}",
+                            receiverId, receiver.getType(), e.getMessage());
+                }
+            });
+        }));
     }
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java
index 0ef336921a..64807ed8b3 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java
@@ -18,16 +18,16 @@
 package org.apache.hertzbeat.alert.reduce;
 
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
-
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
+import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 /**
@@ -35,33 +35,45 @@ import org.springframework.stereotype.Service;
  */
 @Service
 @Slf4j
-public class AlarmCommonReduce {
-    
+public class AlarmCommonReduce implements DisposableBean {
+
     private final AlarmGroupReduce alarmGroupReduce;
-    
-    private ThreadPoolExecutor workerExecutor;
+
+    private final ManagedExecutor workerExecutor;
 
     public AlarmCommonReduce(AlarmGroupReduce alarmGroupReduce) {
-        initWorkExecutor();
+        this(alarmGroupReduce, VirtualThreadProperties.defaults());
+    }
+
+    @Autowired
+    public AlarmCommonReduce(AlarmGroupReduce alarmGroupReduce, VirtualThreadProperties virtualThreadProperties) {
         this.alarmGroupReduce = alarmGroupReduce;
+        VirtualThreadProperties properties =
+                virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
+        this.workerExecutor = initWorkExecutor(properties);
     }
 
-    private void initWorkExecutor() {
-        ThreadFactory threadFactory = new ThreadFactoryBuilder()
-                .setUncaughtExceptionHandler((thread, throwable) -> {
-                    log.error("alerter-reduce-worker has uncaughtException.");
-                    log.error(throwable.getMessage(), throwable);
-                })
-                .setDaemon(true)
-                .setNameFormat("alerter-reduce-worker-%d")
-                .build();
-        workerExecutor = new ThreadPoolExecutor(2,
+    private ManagedExecutor initWorkExecutor(VirtualThreadProperties properties) {
+        Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
+            log.error("alerter-reduce-worker has uncaughtException.");
+            log.error(throwable.getMessage(), throwable);
+        };
+        if (properties.enabled()) {
+            VirtualThreadProperties.QueueProperties queueProperties = properties.alerter().reduce();
+            return ManagedExecutors.newQueuedVirtualExecutor("alerter-reduce-worker", "alerter-reduce-worker-",
+                    queueProperties.maxConcurrentJobs(), queueProperties.queueCapacity(), handler);
+        }
+        return ManagedExecutors.wrap("alerter-reduce-worker", new java.util.concurrent.ThreadPoolExecutor(2,
                 2,
                 10,
-                TimeUnit.SECONDS,
-                new LinkedBlockingQueue<>(),
-                threadFactory,
-                new ThreadPoolExecutor.AbortPolicy());
+                java.util.concurrent.TimeUnit.SECONDS,
+                new java.util.concurrent.LinkedBlockingQueue<>(),
+                new ThreadFactoryBuilder()
+                        .setUncaughtExceptionHandler(handler)
+                        .setDaemon(true)
+                        .setNameFormat("alerter-reduce-worker-%d")
+                        .build(),
+                new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()));
     }
 
 
@@ -84,7 +96,7 @@ public class AlarmCommonReduce {
             }
         });
     }
-    
+
     Runnable reduceAlarmTask(SingleAlert alert) {
         return () -> {
             try {
@@ -103,10 +115,6 @@ public class AlarmCommonReduce {
      * Fingerprint is based on labels excluding timestamp related fields
      */
     private String generateAlertFingerprint(Map<String, String> labels) {
-        // Remove timestamp related fields
-        labels.remove("timestamp");
-        labels.remove("start_at");
-        labels.remove("active_at");
         return labels.entrySet().stream()
                 .filter(e -> !"timestamp".equals(e.getKey())
                         && !"starts_at".equals(e.getKey()) && !"actives_at".equals(e.getKey())
@@ -116,4 +124,9 @@ public class AlarmCommonReduce {
                 .map(e -> e.getKey() + ":" + e.getValue())
                 .collect(Collectors.joining(","));
     }
+
+    @Override
+    public void destroy() {
+        workerExecutor.close();
+    }
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java
index 94c6cb873e..693658b805 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java
@@ -26,17 +26,22 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
 import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
 import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 /**
@@ -45,18 +50,18 @@ import org.springframework.stereotype.Component;
  */
 @Component
 @Slf4j
-public class AlarmGroupReduce {
+public class AlarmGroupReduce implements DisposableBean {
 
     /**
      * Default initial group wait time 30s
      */
     private static final long DEFAULT_GROUP_WAIT = 30 * 1000L;
-    
+
     /**
      * Default group send interval 5min
      */
-    private static final long DEFAULT_GROUP_INTERVAL = 5 * 60 * 1000L;  
-    
+    private static final long DEFAULT_GROUP_INTERVAL = 5 * 60 * 1000L;
+
     /**
      * Default repeat interval 4h
      */
@@ -71,16 +76,16 @@ public class AlarmGroupReduce {
      * Check interval for group send ms
      */
     private static final long CHECK_INTERVAL = 1000L;
-    
+
     private final AlarmInhibitReduce alarmInhibitReduce;
-    
+
     /**
      * Group define rules
      * key: rule name
      * value: group rule configuration
      */
     private final Map<String, AlertGroupConverge> groupDefines;
-    
+
     /**
      * Alert cache grouped by labels
      * key: groupDefineKey:groupKey
@@ -88,16 +93,60 @@ public class AlarmGroupReduce {
      */
     private final Map<String, GroupAlertCache> groupCacheMap;
 
+    private final ScheduledExecutorService scheduledExecutor;
+
+    private final ExecutorService workerExecutor;
+
+    private final ScheduledDispatchTask checkTask;
+
     public AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao) {
+        this(alarmInhibitReduce, alertGroupConvergeDao, VirtualThreadProperties.defaults(), true);
+    }
+
+    @Autowired
+    public AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao,
+                            VirtualThreadProperties virtualThreadProperties) {
+        this(alarmInhibitReduce, alertGroupConvergeDao, virtualThreadProperties, true);
+    }
+
+    AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao,
+                     VirtualThreadProperties virtualThreadProperties, boolean autoStart) {
         this.alarmInhibitReduce = alarmInhibitReduce;
         this.groupDefines = new ConcurrentHashMap<>(8);
         this.groupCacheMap = new ConcurrentHashMap<>(8);
+        VirtualThreadProperties properties =
+                virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
+        this.scheduledExecutor = createScheduler();
+        this.workerExecutor = createVirtualExecutor(properties);
+        this.checkTask = new ScheduledDispatchTask(workerExecutor, this::runCheckAndSendGroups);
         List<AlertGroupConverge> groupConverges = alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue();
         refreshGroupDefines(groupConverges);
-        startCheckAndSendGroups();
+        if (autoStart) {
+            startCheckAndSendGroups();
+        }
     }
 
     private void startCheckAndSendGroups() {
+        scheduledExecutor.scheduleAtFixedRate(this::dispatchCheckAndSendGroups, 10000, CHECK_INTERVAL,
+                TimeUnit.MILLISECONDS);
+    }
+
+    void dispatchCheckAndSendGroups() {
+        checkTask.dispatch();
+    }
+
+    void beforeCheckAndSendGroupsRun() {
+    }
+
+    @Override
+    public void destroy() {
+        scheduledExecutor.shutdownNow();
+        if (workerExecutor != null) {
+            workerExecutor.shutdownNow();
+        }
+    }
+
+    private ScheduledExecutorService createScheduler() {
         ThreadFactory threadFactory = new ThreadFactoryBuilder()
                 .setUncaughtExceptionHandler((thread, throwable) -> {
                     log.error("Check alarm groups calculate has uncaughtException.");
@@ -106,21 +155,36 @@ public class AlarmGroupReduce {
                 .setDaemon(true)
                 .setNameFormat("alarm-group-calculate-%d")
                 .build();
-        ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
-        scheduledExecutor.scheduleAtFixedRate(() -> {
-            try {
-                long now = System.currentTimeMillis();
-                groupCacheMap.forEach((groupKey, cache) -> {
-                    if (shouldSendGroup(cache, now)) {
-                        sendGroupAlert(cache);
-                        cache.setLastSendTime(now);
-                        cache.getAlertFingerprints().clear();
-                    }
-                });
-            } catch (Exception e) {
-                log.error("Check alarm groups calculate has exception.: {}", e.getMessage(), e);
-            }
-        }, 10000, CHECK_INTERVAL, java.util.concurrent.TimeUnit.MILLISECONDS);
+        return Executors.newSingleThreadScheduledExecutor(threadFactory);
+    }
+
+    private ExecutorService createVirtualExecutor(VirtualThreadProperties properties) {
+        if (!properties.enabled()) {
+            return null;
+        }
+        return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
+                .name("alarm-group-calculate-vt-", 0)
+                .uncaughtExceptionHandler((thread, throwable) -> {
+                    log.error("Check alarm groups calculate worker has uncaughtException.");
+                    log.error(throwable.getMessage(), throwable);
+                })
+                .factory());
+    }
+
+    private void runCheckAndSendGroups() {
+        beforeCheckAndSendGroupsRun();
+        try {
+            long now = System.currentTimeMillis();
+            groupCacheMap.forEach((groupKey, cache) -> {
+                if (shouldSendGroup(cache, now)) {
+                    sendGroupAlert(cache);
+                    cache.setLastSendTime(now);
+                    cache.getAlertFingerprints().clear();
+                }
+            });
+        } catch (Exception e) {
+            log.error("Check alarm groups calculate has exception.: {}", e.getMessage(), e);
+        }
     }
 
     /**
@@ -131,7 +195,7 @@ public class AlarmGroupReduce {
         this.groupDefines.clear();
         groupDefines.forEach(define -> this.groupDefines.put(define.getName(), define));
     }
-    
+
     /**
      * Process single alert and group by defined rules
      */
@@ -141,20 +205,20 @@ public class AlarmGroupReduce {
             sendSingleAlert(alert);
             return;
         }
-        
+
         // Process each group define rule
         boolean matched = false;
         for (Map.Entry<String, AlertGroupConverge> define : groupDefines.entrySet()) {
             String defineName = define.getKey();
             AlertGroupConverge ruleConfig = define.getValue();
-            
+
             // Check if alert has all required group labels
             if (hasRequiredLabels(labels, ruleConfig.getGroupLabels())) {
                 matched = true;
                 processAlertByGroupDefine(alert, defineName, ruleConfig);
             }
         }
-        
+
         if (!matched) {
             sendSingleAlert(alert);
         }
@@ -176,17 +240,17 @@ public class AlarmGroupReduce {
     private boolean hasRequiredLabels(Map<String, String> labels, List<String> requiredLabels) {
         return requiredLabels.stream().allMatch(labels::containsKey);
     }
-    
+
     private void processAlertByGroupDefine(SingleAlert alert, String defineName, AlertGroupConverge ruleConfig) {
         // Extract group labels based on define
         Map<String, String> extractedLabels = new HashMap<>();
         for (String labelKey : ruleConfig.getGroupLabels()) {
             extractedLabels.put(labelKey, alert.getLabels().get(labelKey));
         }
-        
-        // Generate group key 
+
+        // Generate group key
         String groupKey = generateGroupKey(extractedLabels);
-        
+
         // Get or create group cache
         GroupAlertCache cache = groupCacheMap.computeIfAbsent(groupKey, k -> {
             GroupAlertCache newCache = new GroupAlertCache();
@@ -206,39 +270,39 @@ public class AlarmGroupReduce {
             cache.getAlertFingerprints().put(fingerprint, alert);
             return;
         }
-        
+
         // Add new alert
         cache.getAlertFingerprints().put(fingerprint, alert);
-        
+
         if (shouldSendGroupImmediately(cache)) {
             sendGroupAlert(cache);
             cache.setLastSendTime(System.currentTimeMillis());
             cache.getAlertFingerprints().clear();
         }
     }
-    
+
     private void sendGroupAlert(GroupAlertCache cache) {
         if (cache.getAlertFingerprints().isEmpty()) {
             return;
         }
-        
+
         long now = System.currentTimeMillis();
         String status = determineGroupStatus(cache.getAlertFingerprints().values());
-        
+
         // For firing alerts, check repeat interval
         if (CommonConstants.ALERT_STATUS_FIRING.equals(status)) {
             AlertGroupConverge ruleConfig = groupDefines.get(cache.getGroupDefineName());
             long repeatInterval = ruleConfig.getRepeatInterval() != null
                     ? ruleConfig.getRepeatInterval() * MS_PER_SECOND : DEFAULT_REPEAT_INTERVAL;
-            
+
             // Skip if within repeat interval
-            if (cache.getLastRepeatTime() > 0 
+            if (cache.getLastRepeatTime() > 0
                 && now - cache.getLastRepeatTime() < repeatInterval) {
                 return;
             }
             cache.setLastRepeatTime(now);
         }
-        
+
         GroupAlert groupAlert = GroupAlert.builder()
                 .groupKey(cache.getGroupKey())
                 .groupLabels(cache.getGroupLabels())
@@ -250,28 +314,28 @@ public class AlarmGroupReduce {
 
         alarmInhibitReduce.inhibitAlarm(groupAlert);
     }
-    
+
     private boolean shouldSendGroup(GroupAlertCache cache, long now) {
         AlertGroupConverge ruleConfig = groupDefines.get(cache.getGroupDefineName());
         long groupWait = ruleConfig != null ? ruleConfig.getGroupWait() * MS_PER_SECOND : DEFAULT_GROUP_WAIT;
         long groupInterval = ruleConfig != null ? ruleConfig.getGroupInterval() * MS_PER_SECOND : DEFAULT_GROUP_INTERVAL;
-        
+
         // First wait time reached
-        if (cache.getLastSendTime() == 0 
+        if (cache.getLastSendTime() == 0
             && now - cache.getCreateTime() >= groupWait) {
             return true;
         }
         // Group interval time reached
-        return cache.getLastSendTime() > 0 
+        return cache.getLastSendTime() > 0
             && now - cache.getLastSendTime() >= groupInterval;
     }
-    
+
     private boolean shouldSendGroupImmediately(GroupAlertCache cache) {
         // Check if all alerts are resolved
         return cache.getAlertFingerprints().values().stream()
                 .allMatch(alert -> CommonConstants.ALERT_STATUS_RESOLVED.equals(alert.getStatus()));
     }
-    
+
     private void sendSingleAlert(SingleAlert alert) {
         // Wrap single alert as group alert
         String groupKey = generateGroupKey(alert.getLabels());
@@ -286,14 +350,14 @@ public class AlarmGroupReduce {
 
         alarmInhibitReduce.inhibitAlarm(groupAlert);
     }
-    
+
     private String generateGroupKey(Map<String, String> labels) {
         return labels.entrySet().stream()
                 .sorted(Map.Entry.comparingByKey())
                 .map(e -> e.getKey() + ":" + e.getValue())
                 .collect(Collectors.joining(","));
     }
-    
+
     private Map<String, String> extractCommonLabels(Collection<SingleAlert> alerts) {
         // Extract common labels from all alerts
         if (alerts.isEmpty()) {
@@ -301,13 +365,13 @@ public class AlarmGroupReduce {
         }
         Map<String, String> common = new HashMap<>(alerts.stream().findFirst().get().getLabels());
         alerts.forEach(alert -> {
-            common.keySet().removeIf(key -> 
-                !alert.getLabels().containsKey(key) 
+            common.keySet().removeIf(key ->
+                !alert.getLabels().containsKey(key)
                 || !common.get(key).equals(alert.getLabels().get(key)));
         });
         return common;
     }
-    
+
     private Map<String, String> extractCommonAnnotations(Collection<SingleAlert> alerts) {
         // Extract common annotations from all alerts
         if (alerts.isEmpty()) {
@@ -315,20 +379,20 @@ public class AlarmGroupReduce {
         }
         Map<String, String> common = new HashMap<>(alerts.stream().findFirst().get().getAnnotations());
         alerts.forEach(alert -> {
-            common.keySet().removeIf(key -> 
-                !alert.getAnnotations().containsKey(key) 
+            common.keySet().removeIf(key ->
+                !alert.getAnnotations().containsKey(key)
                 || !common.get(key).equals(alert.getAnnotations().get(key)));
         });
         return common;
     }
-    
+
     private String determineGroupStatus(Collection<SingleAlert> alerts) {
         // If any alert is firing, group is firing
         return alerts.stream()
-                .anyMatch(alert -> CommonConstants.ALERT_STATUS_FIRING.equals(alert.getStatus())) 
+                .anyMatch(alert -> CommonConstants.ALERT_STATUS_FIRING.equals(alert.getStatus()))
                 ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED;
     }
-    
+
     @Data
     private static class GroupAlertCache {
         private String groupDefineName;
@@ -339,4 +403,63 @@ public class AlarmGroupReduce {
         private long lastSendTime;
         private long lastRepeatTime;
     }
+
+    private static final class ScheduledDispatchTask {
+
+        private final ExecutorService executor;
+
+        private final Runnable task;
+
+        private boolean running;
+
+        private int pendingRuns;
+
+        private ScheduledDispatchTask(ExecutorService executor, Runnable task) {
+            this.executor = executor;
+            this.task = task;
+        }
+
+        private void dispatch() {
+            boolean shouldSchedule;
+            synchronized (this) {
+                pendingRuns++;
+                shouldSchedule = !running;
+                if (shouldSchedule) {
+                    running = true;
+                }
+            }
+            if (shouldSchedule) {
+                scheduleRun();
+            }
+        }
+
+        private void scheduleRun() {
+            if (executor != null) {
+                executor.execute(this::runOnce);
+            } else {
+                runOnce();
+            }
+        }
+
+        private void runOnce() {
+            try {
+                task.run();
+            } finally {
+                scheduleNextIfNeeded();
+            }
+        }
+
+        private void scheduleNextIfNeeded() {
+            boolean shouldSchedule;
+            synchronized (this) {
+                pendingRuns = Math.max(0, pendingRuns - 1);
+                shouldSchedule = pendingRuns > 0;
+                if (!shouldSchedule) {
+                    running = false;
+                    return;
+                }
+            }
+            scheduleRun();
+        }
+    }
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduce.java
index 08cdf7b3b7..2880a16a2f 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduce.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduce.java
@@ -18,25 +18,28 @@
 package org.apache.hertzbeat.alert.reduce;
 
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.alert.AlerterProperties;
 import org.apache.hertzbeat.alert.dao.AlertInhibitDao;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
 import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.entity.alerter.AlertInhibit;
 import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.Collections;
-import java.util.stream.Collectors;
-
 import lombok.Data;
 import lombok.AllArgsConstructor;
 
@@ -46,8 +49,8 @@ import lombok.AllArgsConstructor;
  */
 @Component
 @Slf4j
-public class AlarmInhibitReduce {
-    
+public class AlarmInhibitReduce implements DisposableBean {
+
     /**
      * Interval for checking and cleaning up expired source alerts
      */
@@ -62,7 +65,7 @@ public class AlarmInhibitReduce {
      * rule cache
      */
     private final Map<Long, AlertInhibit> inhibitRules;
-    
+
     /**
      * Cache for source alerts
      * key: ruleId
@@ -73,22 +76,69 @@ public class AlarmInhibitReduce {
     /**
      * Default TTL for source alerts (4 hours)
      */
-    private static long SOURCE_ALERT_TTL = 4 * 60 * 60 * 1000L;
-    
+    private final long sourceAlertTtl;
+
+    private final ScheduledExecutorService cleanupScheduler;
+
+    private final ExecutorService cleanupExecutor;
+
+    private final ScheduledDispatchTask cleanupTask;
+
     public AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao
             , AlerterProperties alerterProperties) {
+        this(alarmSilenceReduce, alertInhibitDao, alerterProperties, VirtualThreadProperties.defaults(), true);
+    }
+
+    @Autowired
+    public AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao,
+                              AlerterProperties alerterProperties, VirtualThreadProperties virtualThreadProperties) {
+        this(alarmSilenceReduce, alertInhibitDao, alerterProperties, virtualThreadProperties, true);
+    }
+
+    AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao,
+                       AlerterProperties alerterProperties, VirtualThreadProperties virtualThreadProperties,
+                       boolean autoStart) {
         this.alarmSilenceReduce = alarmSilenceReduce;
+        VirtualThreadProperties properties =
+                virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
         if (alerterProperties.getInhibit() != null && alerterProperties.getInhibit().getTtl() > 0) {
-            SOURCE_ALERT_TTL = alerterProperties.getInhibit().getTtl();   
+            this.sourceAlertTtl = alerterProperties.getInhibit().getTtl();
+        } else {
+            this.sourceAlertTtl = 4 * 60 * 60 * 1000L;
         }
         inhibitRules = new ConcurrentHashMap<>(8);
         sourceAlertCache = new ConcurrentHashMap<>(8);
+        this.cleanupScheduler = createCleanupScheduler();
+        this.cleanupExecutor = createCleanupExecutor(properties);
+        this.cleanupTask = new ScheduledDispatchTask(cleanupExecutor, this::runCleanupCache);
         List<AlertInhibit> inhibits = alertInhibitDao.findAlertInhibitsByEnableIsTrue();
         refreshInhibitRules(inhibits);
-        startScheduledCleanupCache();
+        if (autoStart) {
+            startScheduledCleanupCache();
+        }
     }
 
     private void startScheduledCleanupCache() {
+        cleanupScheduler.scheduleAtFixedRate(this::dispatchCleanupCache, CHECK_INTERVAL, CHECK_INTERVAL,
+                TimeUnit.MILLISECONDS);
+    }
+
+    void dispatchCleanupCache() {
+        cleanupTask.dispatch();
+    }
+
+    void beforeCleanupCacheRun() {
+    }
+
+    @Override
+    public void destroy() {
+        cleanupScheduler.shutdownNow();
+        if (cleanupExecutor != null) {
+            cleanupExecutor.shutdownNow();
+        }
+    }
+
+    private ScheduledExecutorService createCleanupScheduler() {
         ThreadFactory threadFactory = new ThreadFactoryBuilder()
                 .setUncaughtExceptionHandler((thread, throwable) -> {
                     log.error("Scheduled clean up inhibit cache has uncaughtException.");
@@ -97,17 +147,30 @@ public class AlarmInhibitReduce {
                 .setDaemon(true)
                 .setNameFormat("inhibit-clean-up-%d")
                 .build();
-        ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
-        // Scheduled cleanup of all expired source alerts
-        scheduledExecutor.scheduleAtFixedRate(() -> {
-            try {
-                sourceAlertCache.values().forEach(this::cleanupExpiredEntries);
-                // Remove empty rule caches
-                sourceAlertCache.entrySet().removeIf(entry -> entry.getValue().isEmpty());
-            } catch (Exception e) {
-                log.error("Error during scheduled cleanup", e);
-            }
-        }, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.MILLISECONDS);
+        return Executors.newSingleThreadScheduledExecutor(threadFactory);
+    }
+
+    private ExecutorService createCleanupExecutor(VirtualThreadProperties properties) {
+        if (!properties.enabled()) {
+            return null;
+        }
+        return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
+                .name("inhibit-clean-up-vt-", 0)
+                .uncaughtExceptionHandler((thread, throwable) -> {
+                    log.error("Scheduled clean up inhibit cache worker has uncaughtException.");
+                    log.error(throwable.getMessage(), throwable);
+                })
+                .factory());
+    }
+
+    private void runCleanupCache() {
+        beforeCleanupCacheRun();
+        try {
+            sourceAlertCache.values().forEach(this::cleanupExpiredEntries);
+            sourceAlertCache.entrySet().removeIf(entry -> entry.getValue().isEmpty());
+        } catch (Exception e) {
+            log.error("Error during scheduled cleanup", e);
+        }
     }
 
     /**
@@ -260,11 +323,11 @@ public class AlarmInhibitReduce {
                 rule.getId(),
                 k -> new ConcurrentHashMap<>()
         );
-        
+
         SourceAlertEntry entry = new SourceAlertEntry(
                 alert,
                 System.currentTimeMillis(),
-                System.currentTimeMillis() + SOURCE_ALERT_TTL
+                System.currentTimeMillis() + sourceAlertTtl
         );
         ruleCache.put(alert.getFingerprint(), entry);
         cleanupExpiredEntries(ruleCache);
@@ -315,4 +378,63 @@ public class AlarmInhibitReduce {
         private final long createTime;
         private final long expiryTime;
     }
+
+    private static final class ScheduledDispatchTask {
+
+        private final ExecutorService executor;
+
+        private final Runnable task;
+
+        private boolean running;
+
+        private int pendingRuns;
+
+        private ScheduledDispatchTask(ExecutorService executor, Runnable task) {
+            this.executor = executor;
+            this.task = task;
+        }
+
+        private void dispatch() {
+            boolean shouldSchedule;
+            synchronized (this) {
+                pendingRuns++;
+                shouldSchedule = !running;
+                if (shouldSchedule) {
+                    running = true;
+                }
+            }
+            if (shouldSchedule) {
+                scheduleRun();
+            }
+        }
+
+        private void scheduleRun() {
+            if (executor != null) {
+                executor.execute(this::runOnce);
+            } else {
+                runOnce();
+            }
+        }
+
+        private void runOnce() {
+            try {
+                task.run();
+            } finally {
+                scheduleNextIfNeeded();
+            }
+        }
+
+        private void scheduleNextIfNeeded() {
+            boolean shouldSchedule;
+            synchronized (this) {
+                pendingRuns = Math.max(0, pendingRuns - 1);
+                shouldSchedule = pendingRuns > 0;
+                if (!shouldSchedule) {
+                    running = false;
+                    return;
+                }
+            }
+            scheduleRun();
+        }
+    }
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java
index 4fdf27746b..d903cdf7f2 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java
@@ -18,6 +18,7 @@
 package org.apache.hertzbeat.alert.reduce;
 
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -51,7 +52,7 @@ public class AlarmSilenceReduce {
             alertSilenceList = alertSilenceDao.findAlertSilencesByEnableTrue();
             CacheFactory.setAlertSilenceCache(alertSilenceList);
         }
-        
+
         // Check each silence rule
         for (AlertSilence alertSilence : alertSilenceList) {
             // Check if alert matches silence rule
@@ -59,10 +60,10 @@ public class AlarmSilenceReduce {
             if (!match && groupAlert.getGroupLabels() != null) {
                 Map<String, String> labels = alertSilence.getLabels();
                 Map<String, String> alertLabels = groupAlert.getGroupLabels();
-                match = labels.entrySet().stream().anyMatch(item -> 
+                match = labels.entrySet().stream().anyMatch(item ->
                     alertLabels.containsKey(item.getKey()) && item.getValue().equals(alertLabels.get(item.getKey())));
             }
-            
+
             if (match) {
                 LocalDateTime now = LocalDateTime.now();
                 if (alertSilence.getType() == 0) {
@@ -75,7 +76,7 @@ public class AlarmSilenceReduce {
                 } else if (alertSilence.getType() == 1) {
                     // Cyclic silence rule
                     int currentDayOfWeek = now.getDayOfWeek().getValue();
-                    if (alertSilence.getDays() != null && alertSilence.getDays().contains((byte) currentDayOfWeek) 
+                    if (alertSilence.getDays() != null && alertSilence.getDays().contains((byte) currentDayOfWeek)
                             && !checkAndSave(now, alertSilence)) {
                         // Alert is silenced
                         return;
@@ -83,7 +84,7 @@ public class AlarmSilenceReduce {
                 }
             }
         }
-        
+
         // No matching silence rule, forward the alert
         dispatcherAlarm.dispatchAlarm(groupAlert);
     }
@@ -95,10 +96,35 @@ public class AlarmSilenceReduce {
      * @return true if alert should not be silenced, false if alert should be silenced
      */
     private boolean checkAndSave(LocalDateTime now, AlertSilence alertSilence) {
-        boolean startMatch = alertSilence.getPeriodStart() == null 
-                || now.isAfter(alertSilence.getPeriodStart().toLocalDateTime());
-        boolean endMatch = alertSilence.getPeriodEnd() == null 
-                || now.isBefore(alertSilence.getPeriodEnd().toLocalDateTime());
+        boolean startMatch;
+        boolean endMatch;
+        if (alertSilence.getType() == 1) {
+            LocalTime nowTime = now.toLocalTime();
+            LocalTime startTime = alertSilence.getPeriodStart() == null ? null : alertSilence.getPeriodStart().toLocalTime();
+            LocalTime endTime = alertSilence.getPeriodEnd() == null ? null : alertSilence.getPeriodEnd().toLocalTime();
+            if (startTime == null && endTime == null) {
+                startMatch = true;
+                endMatch = true;
+            } else if (startTime == null) {
+                startMatch = true;
+                endMatch = !nowTime.isAfter(endTime);
+            } else if (endTime == null) {
+                startMatch = !nowTime.isBefore(startTime);
+                endMatch = true;
+            } else if (!startTime.isAfter(endTime)) {
+                startMatch = !nowTime.isBefore(startTime);
+                endMatch = !nowTime.isAfter(endTime);
+            } else {
+                // Cross-midnight window, e.g. 23:00-02:00.
+                startMatch = !nowTime.isBefore(startTime) || !nowTime.isAfter(endTime);
+                endMatch = true;
+            }
+        } else {
+            startMatch = alertSilence.getPeriodStart() == null
+                    || now.isAfter(alertSilence.getPeriodStart().toLocalDateTime());
+            endMatch = alertSilence.getPeriodEnd() == null
+                    || now.isBefore(alertSilence.getPeriodEnd().toLocalDateTime());
+        }
 
         if (startMatch && endMatch) {
             int time = Optional.ofNullable(alertSilence.getTimes()).orElse(0);
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java
index ea5c9bb9a2..afe3d6184b 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java
@@ -40,7 +40,7 @@ public interface AlertService {
      * @return single alerts
      */
     Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
-    
+
     /**
      * Dynamic conditional query
      * @param status        Alarm Status
@@ -48,9 +48,9 @@ public interface AlertService {
      * @param order         Sort Type
      * @param pageIndex     List current page
      * @param pageSize      Number of list pagination
-     * @return search result    
+     * @return search result
      */
-    Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
+    Page<GroupAlert> getGroupAlerts(String status, String search, String severity, String sort, String order, int pageIndex, int pageSize);
 
     /**
      * delete the group alarm according to the alarm ID
@@ -77,10 +77,10 @@ public interface AlertService {
      * @param ids  Alarm ID List to be modified
      */
     void editSingleAlertStatus(String status, List<Long> ids);
-    
+
     /**
      * Get alarm statistics information
-     * @return Alarm statistics information 
+     * @return Alarm statistics information
      */
     AlertSummary getAlertsSummary();
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java
index a3dc79bf02..b1b0f5f0fc 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java
@@ -24,7 +24,7 @@ import java.util.Map;
  * datasource service
  */
 public interface DataSourceService {
-    
+
     /**
      * execute query expr calculate
      * @param datasource datasource
@@ -41,9 +41,18 @@ public interface DataSourceService {
      */
     List<Map<String, Object>> query(String datasource, String expr);
 
+    /**
+     * query result set from db under an alert-type-specific SQL scope
+     * @param datasource sql or promql
+     * @param expr query expr
+     * @param alertType alert rule type
+     * @return result
+     */
+    List<Map<String, Object>> query(String datasource, String expr, String alertType);
+
     /**
      * Get available datasource executors status
      * @return map containing available executors by type (promql, sql)
      */
     Map<String, Object> getAvailableExecutors();
-} 
+}
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java
index f08234ab30..888f15616b 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java
@@ -36,6 +36,7 @@ import org.apache.hertzbeat.common.constants.ExportFileConstants;
 import org.apache.hertzbeat.common.constants.SignConstants;
 import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
 import org.apache.hertzbeat.common.entity.manager.Label;
+import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
 import org.apache.hertzbeat.common.util.FileUtil;
 import org.apache.hertzbeat.common.util.JexlExpressionRunner;
 import org.apache.hertzbeat.common.util.JsonUtil;
@@ -279,13 +280,28 @@ public class AlertDefineServiceImpl implements AlertDefineService {
                 return dataSourceService.calculate(datasource, expr);
             case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC:
                 // todo support alert expr preview
-                return dataSourceService.query(datasource, expr);
+                return dataSourceService.query(datasource, expr, type);
+            case CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC:
+                List<Map<String, Object>> tracePreview = dataSourceService.query(datasource, expr, type);
+                validateTracePreview(tracePreview);
+                return tracePreview;
             default:
                 log.error("Get define preview unsupported type: {}", type);
                 return Collections.emptyList();
         }
     }
 
+    private void validateTracePreview(List<Map<String, Object>> tracePreview) {
+        if (tracePreview == null || tracePreview.isEmpty()) {
+            return;
+        }
+        boolean hasMissingValueColumn = tracePreview.stream()
+                .anyMatch(row -> !row.containsKey("__value__"));
+        if (hasMissingValueColumn) {
+            throw new AlertExpressionException("Trace alert preview SQL must return __value__ column");
+        }
+    }
+
     @Override
     public List<AlertDefine> getAlertDefinesByType(String type) {
         if (!StringUtils.hasText(type)) {
@@ -297,6 +313,7 @@ public class AlertDefineServiceImpl implements AlertDefineService {
             case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC:
             case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME:
             case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC:
+            case CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC:
                 // Valid type, proceed with query
                 break;
             default:
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java
index 80bb95258b..bfe18c8202 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java
@@ -20,9 +20,12 @@ package org.apache.hertzbeat.alert.service.impl;
 import jakarta.persistence.criteria.Predicate;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.alert.dao.GroupAlertDao;
 import org.apache.hertzbeat.alert.dao.SingleAlertDao;
@@ -34,11 +37,13 @@ import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.domain.Specification;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
 
 /**
  * Realization of Alarm Information Service
@@ -47,13 +52,13 @@ import org.springframework.transaction.annotation.Transactional;
 @Transactional(rollbackFor = Exception.class)
 @Slf4j
 public class AlertServiceImpl implements AlertService {
-    
+
     @Autowired
     private GroupAlertDao groupAlertDao;
-    
+
     @Autowired
     private SingleAlertDao singleAlertDao;
-    
+
     @Autowired
     private AlarmCommonReduce alarmCommonReduce;
 
@@ -94,7 +99,7 @@ public class AlertServiceImpl implements AlertService {
     }
 
     @Override
-    public Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
+    public Page<GroupAlert> getGroupAlerts(String status, String search, String severity, String sort, String order, int pageIndex, int pageSize) {
         Specification<GroupAlert> specification = (root, query, criteriaBuilder) -> {
             List<Predicate> andList = new ArrayList<>();
             if (status != null) {
@@ -126,12 +131,19 @@ public class AlertServiceImpl implements AlertService {
         };
         Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort));
         PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp);
-        Page<GroupAlert> groupAlertPage = groupAlertDao.findAll(specification, pageRequest);
-        for (GroupAlert groupAlert : groupAlertPage.getContent()) {
-            List<String> firingAlerts = groupAlert.getAlertFingerprints();
-            List<SingleAlert> singleAlerts = singleAlertDao.findSingleAlertsByFingerprintIn(firingAlerts);
-            groupAlert.setAlerts(singleAlerts);
+        String normalizedSeverity = normalizeSeverity(severity);
+        if (normalizedSeverity != null) {
+            List<GroupAlert> groupAlerts = groupAlertDao.findAll(specification, sortExp);
+            List<GroupAlert> filteredAlerts = groupAlerts.stream()
+                    .peek(this::hydrateGroupAlerts)
+                    .filter(groupAlert -> groupAlert.getAlerts().stream().anyMatch(alert -> matchesSeverity(alert, normalizedSeverity)))
+                    .toList();
+            int start = Math.min((int) pageRequest.getOffset(), filteredAlerts.size());
+            int end = Math.min(start + pageRequest.getPageSize(), filteredAlerts.size());
+            return new PageImpl<>(filteredAlerts.subList(start, end), pageRequest, filteredAlerts.size());
         }
+        Page<GroupAlert> groupAlertPage = groupAlertDao.findAll(specification, pageRequest);
+        groupAlertPage.getContent().forEach(this::hydrateGroupAlerts);
         return groupAlertPage;
     }
 
@@ -152,14 +164,49 @@ public class AlertServiceImpl implements AlertService {
 
     @Override
     public void editGroupAlertStatus(String status, List<Long> ids) {
-        groupAlertDao.updateGroupAlertsStatus(status, ids);
+        if (!StringUtils.hasText(status) || ids == null || ids.isEmpty()) {
+            return;
+        }
+        List<GroupAlert> groupAlerts = groupAlertDao.findAllById(ids);
+        if (groupAlerts.isEmpty()) {
+            return;
+        }
+        long now = Instant.now().toEpochMilli();
+        List<String> fingerprints = groupAlerts.stream()
+                .map(GroupAlert::getAlertFingerprints)
+                .filter(Objects::nonNull)
+                .flatMap(List::stream)
+                .distinct()
+                .toList();
+        List<SingleAlert> singleAlerts = fingerprints.isEmpty()
+                ? List.of()
+                : singleAlertDao.findSingleAlertsByFingerprintIn(fingerprints);
+        for (GroupAlert groupAlert : groupAlerts) {
+            groupAlert.setStatus(status);
+        }
+        for (SingleAlert singleAlert : singleAlerts) {
+            singleAlert.setStatus(status);
+            if (CommonConstants.ALERT_STATUS_RESOLVED.equals(status)) {
+                singleAlert.setActiveAt(null);
+                singleAlert.setEndAt(now);
+            } else {
+                singleAlert.setEndAt(null);
+                if (singleAlert.getActiveAt() == null) {
+                    singleAlert.setActiveAt(now);
+                }
+            }
+        }
+        groupAlertDao.saveAll(groupAlerts);
+        if (!singleAlerts.isEmpty()) {
+            singleAlertDao.saveAll(singleAlerts);
+        }
     }
 
     @Override
     public void editSingleAlertStatus(String status, List<Long> ids) {
         singleAlertDao.updateSingleAlertsStatus(status, ids);
     }
-    
+
 
     @Override
     public AlertSummary getAlertsSummary() {
@@ -184,7 +231,7 @@ public class AlertServiceImpl implements AlertService {
             alertSummary.setPriorityEmergencyNum(emergencyNum);
             alertSummary.setPriorityWarningNum(warningNum);
         }
-        
+
         long total = singleAlertDao.count();
         alertSummary.setTotal(total);
         long resolved = total - firingAlerts.size();
@@ -203,4 +250,31 @@ public class AlertServiceImpl implements AlertService {
         }
         return alertSummary;
     }
+
+    private void hydrateGroupAlerts(GroupAlert groupAlert) {
+        List<String> firingAlerts = groupAlert.getAlertFingerprints();
+        List<SingleAlert> singleAlerts = singleAlertDao.findSingleAlertsByFingerprintIn(firingAlerts);
+        singleAlerts.sort(Comparator.comparing(SingleAlert::getGmtUpdate, Comparator.nullsLast(Comparator.reverseOrder()))
+                .thenComparing(SingleAlert::getId, Comparator.nullsLast(Comparator.reverseOrder())));
+        groupAlert.setAlerts(singleAlerts);
+    }
+
+    private String normalizeSeverity(String severity) {
+        if (!StringUtils.hasText(severity)) {
+            return null;
+        }
+        return severity.trim().toLowerCase();
+    }
+
+    private boolean matchesSeverity(SingleAlert alert, String severity) {
+        if (alert == null || severity == null) {
+            return false;
+        }
+        String labelSeverity = alert.getLabels() == null ? null : alert.getLabels().get("severity");
+        if (StringUtils.hasText(labelSeverity) && severity.equalsIgnoreCase(labelSeverity.trim())) {
+            return true;
+        }
+        String annotationSeverity = alert.getAnnotations() == null ? null : alert.getAnnotations().get("severity");
+        return StringUtils.hasText(annotationSeverity) && severity.equalsIgnoreCase(annotationSeverity.trim());
+    }
 }
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java
index 136777c085..8e13a49789 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java
@@ -30,6 +30,7 @@ import org.apache.hertzbeat.alert.expr.AlertExpressionEvalVisitor;
 import org.apache.hertzbeat.alert.expr.AlertExpressionLexer;
 import org.apache.hertzbeat.alert.expr.AlertExpressionParser;
 import org.apache.hertzbeat.alert.service.DataSourceService;
+import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
 import org.apache.hertzbeat.common.support.valid.SqlSecurityException;
 import org.apache.hertzbeat.common.support.valid.SqlSecurityValidator;
@@ -54,16 +55,20 @@ import java.util.concurrent.TimeUnit;
 public class DataSourceServiceImpl implements DataSourceService {
 
     /**
-     * Default allowed tables for SQL queries
+     * Default allowed tables for SQL queries.
      */
-    private static final List<String> DEFAULT_ALLOWED_TABLES = List.of(WarehouseConstants.LOG_TABLE_NAME);
+    private static final List<String> LOG_ALLOWED_TABLES = List.of(WarehouseConstants.LOG_TABLE_NAME);
+
+    private static final List<String> TRACE_ALLOWED_TABLES = List.of("hertzbeat_apm_red_1m", "hzb_traces");
 
     protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
 
     @Setter
     private List<QueryExecutor> executors;
 
-    private final SqlSecurityValidator sqlSecurityValidator;
+    private final SqlSecurityValidator logSqlSecurityValidator;
+
+    private final SqlSecurityValidator traceSqlSecurityValidator;
 
     @Getter
     private final Cache<String, ParseTree> expressionCache = Caffeine.newBuilder()
@@ -74,7 +79,8 @@ public class DataSourceServiceImpl implements DataSourceService {
 
     public DataSourceServiceImpl(@Autowired(required = false) List<QueryExecutor> executors) {
         this.executors = executors != null ? executors : Collections.emptyList();
-        this.sqlSecurityValidator = new SqlSecurityValidator(DEFAULT_ALLOWED_TABLES);
+        this.logSqlSecurityValidator = new SqlSecurityValidator(LOG_ALLOWED_TABLES);
+        this.traceSqlSecurityValidator = new SqlSecurityValidator(TRACE_ALLOWED_TABLES);
     }
 
     @Override
@@ -105,6 +111,11 @@ public class DataSourceServiceImpl implements DataSourceService {
 
     @Override
     public List<Map<String, Object>> query(String datasource, String expr) {
+        return query(datasource, expr, null);
+    }
+
+    @Override
+    public List<Map<String, Object>> query(String datasource, String expr, String alertType) {
         if (!StringUtils.hasText(expr)) {
             throw new IllegalArgumentException("Empty expression");
         }
@@ -121,7 +132,7 @@ public class DataSourceServiceImpl implements DataSourceService {
 
         // SQL security validation for SQL-based datasources
         if (isSqlDatasource(datasource)) {
-            validateSqlSecurity(expr);
+            validateSqlSecurity(expr, alertType);
         }
 
         try {
@@ -142,15 +153,22 @@ public class DataSourceServiceImpl implements DataSourceService {
     /**
      * Validate SQL statement for security
      */
-    private void validateSqlSecurity(String sql) {
+    private void validateSqlSecurity(String sql, String alertType) {
         try {
-            sqlSecurityValidator.validate(sql);
+            sqlSecurityValidator(alertType).validate(sql);
         } catch (SqlSecurityException e) {
             log.warn("SQL security validation failed: {}", e.getMessage());
             throw new AlertExpressionException("SQL security validation failed: " + e.getMessage());
         }
     }
 
+    private SqlSecurityValidator sqlSecurityValidator(String alertType) {
+        if (CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC.equals(alertType)) {
+            return traceSqlSecurityValidator;
+        }
+        return logSqlSecurityValidator;
+    }
+
     private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
         CommonTokenStream tokens = createTokenStream(expr);
         AlertExpressionParser parser = new AlertExpressionParser(tokens);
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java
index 444b85d79d..ba83880959 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java
@@ -18,9 +18,18 @@
 package org.apache.hertzbeat.alert;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
-import org.junit.jupiter.api.BeforeEach;
+import org.apache.hertzbeat.common.concurrent.AdmissionMode;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -28,60 +37,178 @@ import org.junit.jupiter.api.Test;
  */
 class AlerterWorkerPoolTest {
 
-    private static final int NUMBER_OF_THREADS = 10;
+    private static final int NUMBER_OF_TASKS = 10;
+
     private AlerterWorkerPool pool;
-    private AtomicInteger counter;
-    private CountDownLatch latch;
 
-    @BeforeEach
-    void setUp() {
-        pool = new AlerterWorkerPool();
-        counter = new AtomicInteger();
-        latch = new CountDownLatch(NUMBER_OF_THREADS);
+    @AfterEach
+    void tearDown() {
+        if (pool != null) {
+            pool.destroy();
+        }
     }
 
     @Test
     void executeJob() throws InterruptedException {
-        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
+        pool = new AlerterWorkerPool();
+        AtomicInteger counter = new AtomicInteger();
+        CountDownLatch latch = new CountDownLatch(NUMBER_OF_TASKS);
+
+        for (int i = 0; i < NUMBER_OF_TASKS; i++) {
             pool.executeJob(() -> {
                 counter.incrementAndGet();
                 latch.countDown();
             });
         }
-        latch.await();
 
-        assertEquals(NUMBER_OF_THREADS, counter.get());
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertEquals(NUMBER_OF_TASKS, counter.get());
     }
 
     @Test
-    void executeNotify() throws InterruptedException {
-        counter = new AtomicInteger();
-        latch = new CountDownLatch(NUMBER_OF_THREADS);
-        
-        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
-            pool.executeNotify(() -> {
-                counter.incrementAndGet();
-                latch.countDown();
-            });
+    void executeNotifyRunsOnVirtualThread() throws Exception {
+        pool = new AlerterWorkerPool();
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicBoolean virtualThread = new AtomicBoolean(false);
+
+        pool.executeNotify((byte) 1, () -> {
+            virtualThread.set(Thread.currentThread().isVirtual());
+            latch.countDown();
+        });
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertTrue(virtualThread.get());
+    }
+
+    @Test
+    void executeNotifyRejectsWhenGlobalConcurrencyLimitReached() throws Exception {
+        VirtualThreadProperties properties = new VirtualThreadProperties(
+                true,
+                VirtualThreadProperties.PoolProperties.collectorDefaults(),
+                VirtualThreadProperties.PoolProperties.commonDefaults(),
+                VirtualThreadProperties.PoolProperties.managerDefaults(),
+                new VirtualThreadProperties.AlerterProperties(
+                        new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 1),
+                        10,
+                        VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
+                        VirtualThreadProperties.QueueProperties.reduceDefaults(),
+                        VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
+                        8),
+                VirtualThreadProperties.PoolProperties.warehouseDefaults(),
+                VirtualThreadProperties.AsyncProperties.defaults());
+        pool = new AlerterWorkerPool(properties);
+
+        CountDownLatch started = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        pool.executeNotify((byte) 1, () -> {
+            started.countDown();
+            try {
+                release.await(5, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        });
+        assertTrue(started.await(5, TimeUnit.SECONDS));
+
+        try {
+            assertThrows(RejectedExecutionException.class, () -> pool.executeNotify((byte) 2, () -> {
+            }));
+        } finally {
+            release.countDown();
         }
-        latch.await();
+    }
+
+    @Test
+    void executeNotifyRejectsWhenChannelLimitReached() throws Exception {
+        VirtualThreadProperties properties = new VirtualThreadProperties(
+                true,
+                VirtualThreadProperties.PoolProperties.collectorDefaults(),
+                VirtualThreadProperties.PoolProperties.commonDefaults(),
+                VirtualThreadProperties.PoolProperties.managerDefaults(),
+                new VirtualThreadProperties.AlerterProperties(
+                        new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 8),
+                        10,
+                        VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
+                        VirtualThreadProperties.QueueProperties.reduceDefaults(),
+                        VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
+                        1),
+                VirtualThreadProperties.PoolProperties.warehouseDefaults(),
+                VirtualThreadProperties.AsyncProperties.defaults());
+        pool = new AlerterWorkerPool(properties);
+
+        CountDownLatch started = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        pool.executeNotify((byte) 1, () -> {
+            started.countDown();
+            try {
+                release.await(5, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        });
+        assertTrue(started.await(5, TimeUnit.SECONDS));
 
-        assertEquals(NUMBER_OF_THREADS, counter.get());
+        try {
+            assertThrows(RejectedExecutionException.class, () -> pool.executeNotify((byte) 1, () -> {
+            }));
+        } finally {
+            release.countDown();
+        }
     }
 
     @Test
     void executeLogJob() throws InterruptedException {
-        counter = new AtomicInteger();
-        latch = new CountDownLatch(NUMBER_OF_THREADS);
-        
-        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
-            pool.executeLogJob(() -> {
-                counter.incrementAndGet();
-                latch.countDown();
-            });
-        }
-        latch.await();
+        pool = new AlerterWorkerPool();
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicBoolean virtualThread = new AtomicBoolean(false);
+
+        pool.executeLogJob(() -> {
+            virtualThread.set(Thread.currentThread().isVirtual());
+            latch.countDown();
+        });
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertTrue(virtualThread.get());
+    }
 
-        assertEquals(NUMBER_OF_THREADS, counter.get());
+    @Test
+    void executeLogJobRejectsWhenQueueCapacityReached() throws InterruptedException {
+        VirtualThreadProperties properties = new VirtualThreadProperties(
+                true,
+                VirtualThreadProperties.PoolProperties.collectorDefaults(),
+                VirtualThreadProperties.PoolProperties.commonDefaults(),
+                VirtualThreadProperties.PoolProperties.managerDefaults(),
+                new VirtualThreadProperties.AlerterProperties(
+                        VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(),
+                        10,
+                        new VirtualThreadProperties.QueueProperties(1, 1),
+                        VirtualThreadProperties.QueueProperties.reduceDefaults(),
+                        VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
+                        4),
+                VirtualThreadProperties.PoolProperties.warehouseDefaults(),
+                VirtualThreadProperties.AsyncProperties.defaults());
+        pool = new AlerterWorkerPool(properties);
+
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirst = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        pool.executeLogJob(() -> {
+            firstStarted.countDown();
+            try {
+                releaseFirst.await(5, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        });
+        assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
+
+        pool.executeLogJob(secondStarted::countDown);
+        assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
+        try {
+            assertThrows(RejectedExecutionException.class, () -> pool.executeLogJob(() -> {
+            }));
+        } finally {
+            releaseFirst.countDown();
+        }
+        assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
     }
 }
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculatorTest.java
index 39a9fcd71c..2c867bde77 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculatorTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculatorTest.java
@@ -41,6 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertAll;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.verify;
@@ -71,7 +72,8 @@ class LogPeriodicAlertCalculatorTest {
                 .id(1L)
                 .name("log_error_alert")
                 .type("periodic_log")
-                .expr("SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?")
+                .expr("SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' "
+                        + "AND `timestamp` >= NOW() - INTERVAL '5 minutes'")
                 .labels(Map.of(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING, "team", "backend", CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP))
                 .annotations(Map.of("summary", "Error logs detected", "description", "Multiple error logs found"))
                 .template("Found ${severity_text} log: ${body} from ${service_name}")
@@ -84,7 +86,8 @@ class LogPeriodicAlertCalculatorTest {
                 .id(1L)
                 .name("log_error_alert")
                 .type("periodic_log")
-                .expr("SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?")
+                .expr("SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' "
+                        + "AND `timestamp` >= NOW() - INTERVAL '5 minutes'")
                 .labels(Map.of(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING, "team", "backend",
                         CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL))
                 .annotations(Map.of("summary", "Error logs detected", "description", "Multiple error logs found"))
@@ -99,52 +102,54 @@ class LogPeriodicAlertCalculatorTest {
     void testCalculateWithEnabledRuleAndValidExpression() {
         // Given
         List<Map<String, Object>> queryResults = createSampleLogResults(3);
-        when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults);
+        when(dataSourceService.query(anyString(), anyString(), anyString())).thenReturn(queryResults);
 
         // When
         calculator.calculate(groupAlertDefine);
 
         // Then
-        verify(dataSourceService).query("sql", "SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?");
+        verify(dataSourceService).query("sql", "SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' "
+                + "AND `timestamp` >= NOW() - INTERVAL '5 minutes'", LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
         verify(alarmCommonReduce).reduceAndSendAlarmGroup(any(), any());
     }
 
     @Test
     void testCalculateWithEmptyQueryResults() {
         // Given
-        when(dataSourceService.query(anyString(), anyString())).thenReturn(new ArrayList<>());
+        when(dataSourceService.query(anyString(), anyString(), anyString())).thenReturn(new ArrayList<>());
 
         // When
         calculator.calculate(groupAlertDefine);
 
         // Then
-        verify(dataSourceService).query(anyString(), anyString());
+        verify(dataSourceService).query(anyString(), anyString(), anyString());
         verifyNoInteractions(alarmCommonReduce);
     }
 
     @Test
     void testCalculateWithNullQueryResults() {
         // Given
-        when(dataSourceService.query(anyString(), anyString())).thenReturn(null);
+        when(dataSourceService.query(anyString(), anyString(), anyString())).thenReturn(null);
 
         // When
         calculator.calculate(groupAlertDefine);
 
         // Then
-        verify(dataSourceService).query(anyString(), anyString());
+        verify(dataSourceService).query(anyString(), anyString(), anyString());
         verifyNoInteractions(alarmCommonReduce);
     }
 
     @Test
     void testCalculateWithQueryException() {
         // Given
-        when(dataSourceService.query(anyString(), anyString())).thenThrow(new RuntimeException("Database connection failed"));
+        when(dataSourceService.query(anyString(), anyString(), anyString()))
+                .thenThrow(new RuntimeException("Database connection failed"));
 
         // When
         calculator.calculate(groupAlertDefine);
 
         // Then
-        verify(dataSourceService).query(anyString(), anyString());
+        verify(dataSourceService).query(anyString(), anyString(), anyString());
         verifyNoInteractions(alarmCommonReduce);
     }
 
@@ -153,7 +158,7 @@ class LogPeriodicAlertCalculatorTest {
         // Given
         Map<String, Object> logContext = createSingleLogResult("Error in payment service", "ERROR");
         List<Map<String, Object>> queryResults = List.of(logContext);
-        when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults);
+        when(dataSourceService.query(anyString(), anyString(), anyString())).thenReturn(queryResults);
 
         try (MockedStatic<org.apache.hertzbeat.alert.util.AlertTemplateUtil> mockedTemplateUtil =
              Mockito.mockStatic(org.apache.hertzbeat.alert.util.AlertTemplateUtil.class)) {
@@ -187,11 +192,11 @@ class LogPeriodicAlertCalculatorTest {
     void testGroupAlertGeneration() {
         // Given
         List<Map<String, Object>> queryResults = createSampleLogResults(2);
-        when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults);
+        when(dataSourceService.query(anyString(), anyString(), anyString())).thenReturn(queryResults);
 
-        try (MockedStatic<org.apache.hertzbeat.alert.util.AlertTemplateUtil> mockedTemplateUtil = 
+        try (MockedStatic<org.apache.hertzbeat.alert.util.AlertTemplateUtil> mockedTemplateUtil =
              Mockito.mockStatic(org.apache.hertzbeat.alert.util.AlertTemplateUtil.class)) {
-            
+
             mockedTemplateUtil.when(() -> org.apache.hertzbeat.alert.util.AlertTemplateUtil.render(anyString(), any()))
                              .thenReturn("Rendered alert content");
 
@@ -240,13 +245,13 @@ class LogPeriodicAlertCalculatorTest {
      */
     private Map<String, Object> createSingleLogResult(String message, String severityText) {
         Map<String, Object> logEntry = new HashMap<>();
-        
+
         // Only essential fields for testing
-        logEntry.put("time_unix_nano", System.nanoTime());
+        logEntry.put("timestamp", System.currentTimeMillis());
         logEntry.put("severity_text", severityText);
         logEntry.put("body", message);
         logEntry.put("service_name", "payment-service");
-        
+
         return logEntry;
     }
 }
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleSchedulerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleSchedulerTest.java
new file mode 100644
index 0000000000..9d104ab064
--- /dev/null
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleSchedulerTest.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.calculate.periodic;
+
+import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
+import static org.apache.hertzbeat.common.constants.CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hertzbeat.alert.dao.AlertDefineDao;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
+import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Tests for {@link PeriodicAlertRuleScheduler}.
+ */
+@ExtendWith(MockitoExtension.class)
+class PeriodicAlertRuleSchedulerTest {
+
+    @Mock
+    private MetricsPeriodicAlertCalculator metricsCalculator;
+
+    @Mock
+    private LogPeriodicAlertCalculator logCalculator;
+
+    @Mock
+    private TracePeriodicAlertCalculator traceCalculator;
+
+    @Mock
+    private AlertDefineDao alertDefineDao;
+
+    private PeriodicAlertRuleScheduler scheduler;
+
+    @BeforeEach
+    void setUp() {
+        scheduler = new PeriodicAlertRuleScheduler(metricsCalculator, logCalculator, traceCalculator, alertDefineDao,
+                VirtualThreadProperties.defaults());
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (scheduler != null) {
+            scheduler.destroy();
+        }
+    }
+
+    @Test
+    void updateScheduleRunsPeriodicCalculationOnVirtualThread() throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicBoolean virtualThread = new AtomicBoolean(false);
+        doAnswer(invocation -> {
+            virtualThread.set(Thread.currentThread().isVirtual());
+            latch.countDown();
+            return null;
+        }).when(metricsCalculator).calculate(any(AlertDefine.class));
+
+        AlertDefine rule = metricRule(1L);
+        scheduler.updateSchedule(rule);
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertTrue(virtualThread.get());
+    }
+
+    @Test
+    void updateScheduleKeepsSingleInFlightExecutionPerRule() throws InterruptedException {
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirst = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        AtomicInteger concurrent = new AtomicInteger();
+        AtomicInteger maxConcurrent = new AtomicInteger();
+        AtomicInteger invocations = new AtomicInteger();
+        doAnswer(invocation -> {
+            int active = concurrent.incrementAndGet();
+            maxConcurrent.updateAndGet(current -> Math.max(current, active));
+            int count = invocations.incrementAndGet();
+            try {
+                if (count == 1) {
+                    firstStarted.countDown();
+                    releaseFirst.await(5, TimeUnit.SECONDS);
+                } else if (count == 2) {
+                    secondStarted.countDown();
+                }
+            } finally {
+                concurrent.decrementAndGet();
+            }
+            return null;
+        }).when(metricsCalculator).calculate(any(AlertDefine.class));
+
+        scheduler.updateSchedule(metricRule(2L));
+
+        assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
+        Thread.sleep(1200L);
+        assertEquals(1, maxConcurrent.get());
+
+        releaseFirst.countDown();
+        assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
+        assertEquals(1, maxConcurrent.get());
+    }
+
+    @Test
+    void cancelScheduleInterruptsRunningVirtualTask() throws InterruptedException {
+        CountDownLatch started = new CountDownLatch(1);
+        CountDownLatch interrupted = new CountDownLatch(1);
+        doAnswer(invocation -> {
+            started.countDown();
+            try {
+                Thread.sleep(5000L);
+            } catch (InterruptedException e) {
+                interrupted.countDown();
+                Thread.currentThread().interrupt();
+            }
+            return null;
+        }).when(metricsCalculator).calculate(any(AlertDefine.class));
+
+        AlertDefine rule = metricRule(3L);
+        scheduler.updateSchedule(rule);
+
+        assertTrue(started.await(5, TimeUnit.SECONDS));
+        scheduler.cancelSchedule(rule.getId());
+        assertTrue(interrupted.await(5, TimeUnit.SECONDS));
+    }
+
+    @Test
+    void updateScheduleHonorsConfiguredGlobalPeriodicConcurrencyLimit() throws InterruptedException {
+        scheduler.destroy();
+        scheduler = new PeriodicAlertRuleScheduler(metricsCalculator, logCalculator, traceCalculator, alertDefineDao,
+                periodicProperties(1));
+
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirst = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        AtomicInteger concurrent = new AtomicInteger();
+        AtomicInteger maxConcurrent = new AtomicInteger();
+        doAnswer(invocation -> {
+            int active = concurrent.incrementAndGet();
+            maxConcurrent.updateAndGet(current -> Math.max(current, active));
+            AlertDefine rule = invocation.getArgument(0);
+            try {
+                if (rule.getId().equals(4L)) {
+                    firstStarted.countDown();
+                    releaseFirst.await(5, TimeUnit.SECONDS);
+                } else if (rule.getId().equals(5L)) {
+                    secondStarted.countDown();
+                }
+            } finally {
+                concurrent.decrementAndGet();
+            }
+            return null;
+        }).when(metricsCalculator).calculate(any(AlertDefine.class));
+
+        scheduler.updateSchedule(metricRule(4L));
+        assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
+
+        scheduler.updateSchedule(metricRule(5L));
+        Thread.sleep(200L);
+        assertEquals(1, maxConcurrent.get());
+        assertEquals(1L, secondStarted.getCount());
+
+        releaseFirst.countDown();
+        assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
+        assertEquals(1, maxConcurrent.get());
+    }
+
+    @Test
+    void runLoadsPeriodicTraceRulesAtStartup() throws Exception {
+        when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(java.util.List.of());
+        when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(
+                org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(java.util.List.of());
+        when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(java.util.List.of(traceRule(6L)));
+
+        scheduler.run();
+
+        verify(alertDefineDao).findAlertDefinesByTypeAndEnableTrue(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC);
+    }
+
+    @Test
+    void updateScheduleRunsPeriodicTraceCalculation() throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+        doAnswer(invocation -> {
+            latch.countDown();
+            return null;
+        }).when(traceCalculator).calculate(any(AlertDefine.class));
+
+        scheduler.updateSchedule(traceRule(7L));
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+    }
+
+    private AlertDefine metricRule(Long id) {
+        return AlertDefine.builder()
+                .id(id)
+                .name("periodic-rule-" + id)
+                .type(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
+                .period(1)
+                .enable(true)
+                .build();
+    }
+
+    private AlertDefine traceRule(Long id) {
+        return AlertDefine.builder()
+                .id(id)
+                .name("periodic-trace-rule-" + id)
+                .type(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC)
+                .period(1)
+                .enable(true)
+                .build();
+    }
+
+    private VirtualThreadProperties periodicProperties(int maxConcurrentJobs) {
+        return new VirtualThreadProperties(
+                true,
+                VirtualThreadProperties.PoolProperties.collectorDefaults(),
+                VirtualThreadProperties.PoolProperties.commonDefaults(),
+                VirtualThreadProperties.PoolProperties.managerDefaults(),
+                new VirtualThreadProperties.AlerterProperties(
+                        VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(),
+                        maxConcurrentJobs,
+                        VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
+                        VirtualThreadProperties.QueueProperties.reduceDefaults(),
+                        VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
+                        4),
+                VirtualThreadProperties.PoolProperties.warehouseDefaults(),
+                VirtualThreadProperties.AsyncProperties.defaults());
+    }
+}
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/TracePeriodicAlertCalculatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/TracePeriodicAlertCalculatorTest.java
new file mode 100644
index 0000000000..f8d1eb66ea
--- /dev/null
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/TracePeriodicAlertCalculatorTest.java
@@ -0,0 +1,229 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.calculate.periodic;
+
+import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_STATUS_FIRING;
+import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_STATUS_PENDING;
+import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_STATUS_RESOLVED;
+import static org.apache.hertzbeat.common.constants.CommonConstants.LABEL_ALERT_NAME;
+import static org.apache.hertzbeat.common.constants.CommonConstants.LABEL_DEFINE_ID;
+import static org.apache.hertzbeat.common.constants.CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
+import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
+import org.apache.hertzbeat.alert.service.DataSourceService;
+import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Tests for trace-derived periodic APM alert calculation.
+ */
+@ExtendWith(MockitoExtension.class)
+class TracePeriodicAlertCalculatorTest {
+
+    private static final String EXPR = "SELECT service_name, operation, span_kind, 0.2 AS __value__ "
+            + "FROM hertzbeat_apm_red_1m";
+
+    @Mock
+    private DataSourceService dataSourceService;
+
+    @Mock
+    private AlarmCommonReduce alarmCommonReduce;
+
+    @Mock
+    private AlarmCacheManager alarmCacheManager;
+
+    @InjectMocks
+    private TracePeriodicAlertCalculator calculator;
+
+    private AlertDefine rule;
+
+    @BeforeEach
+    void setUp() {
+        rule = AlertDefine.builder()
+                .id(42L)
+                .name("apm_error_rate")
+                .type(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC)
+                .datasource("sql")
+                .expr(EXPR)
+                .times(1)
+                .labels(Map.of("team", "checkout", "priority", "P1"))
+                .annotations(Map.of("summary", "${service_name} ${operation} value ${__value__}"))
+                .template("Trace RED alert ${service_name} ${operation}: ${__value__}")
+                .enable(true)
+                .period(300)
+                .build();
+    }
+
+    @Test
+    void breachRowFiresAlertWithTraceLabelsAndTemplateContext() {
+        when(dataSourceService.query("sql", EXPR, TRACE_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(List.of(traceBreachRow()));
+        when(alarmCacheManager.getPending(eq(rule.getId()), anyString())).thenReturn(null);
+        when(alarmCacheManager.getFiring(eq(rule.getId()), anyString())).thenReturn(null);
+
+        calculator.calculate(rule);
+
+        ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
+        verify(alarmCacheManager).putFiring(eq(rule.getId()), anyString(), alertCaptor.capture());
+        verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.getValue().clone());
+
+        SingleAlert alert = alertCaptor.getValue();
+        assertAll(
+                () -> assertEquals(ALERT_STATUS_FIRING, alert.getStatus()),
+                () -> assertEquals("Trace RED alert checkout GET /checkout: 0.2", alert.getContent()),
+                () -> assertEquals("42", alert.getLabels().get(LABEL_DEFINE_ID)),
+                () -> assertEquals("apm_error_rate", alert.getLabels().get(LABEL_ALERT_NAME)),
+                () -> assertEquals("checkout", alert.getLabels().get("team")),
+                () -> assertEquals("checkout", alert.getLabels().get("service_name")),
+                () -> assertEquals("GET /checkout", alert.getLabels().get("operation")),
+                () -> assertEquals("SPAN_KIND_SERVER", alert.getLabels().get("span_kind")),
+                () -> assertFalse(alert.getLabels().containsKey("__value__")),
+                () -> assertFalse(alert.getLabels().containsKey("__timestamp__")),
+                () -> assertEquals("checkout GET /checkout value 0.2", alert.getAnnotations().get("summary"))
+        );
+    }
+
+    @Test
+    void timesGreaterThanOneRequiresConsecutiveBreachesBeforeFiring() {
+        rule.setTimes(2);
+        SingleAlert pending = SingleAlert.builder()
+                .status(ALERT_STATUS_PENDING)
+                .triggerTimes(1)
+                .labels(Map.of(LABEL_DEFINE_ID, "42", LABEL_ALERT_NAME, "apm_error_rate"))
+                .startAt(System.currentTimeMillis() - 1_000)
+                .activeAt(System.currentTimeMillis() - 1_000)
+                .build();
+        when(dataSourceService.query("sql", EXPR, TRACE_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(List.of(traceBreachRow()));
+        when(alarmCacheManager.getPending(eq(rule.getId()), anyString())).thenReturn(null, pending);
+        when(alarmCacheManager.getFiring(eq(rule.getId()), anyString())).thenReturn(null);
+
+        calculator.calculate(rule);
+        verify(alarmCacheManager).putPending(eq(rule.getId()), anyString(), org.mockito.ArgumentMatchers.any());
+        verify(alarmCommonReduce, never()).reduceAndSendAlarm(org.mockito.ArgumentMatchers.any());
+
+        calculator.calculate(rule);
+        verify(alarmCacheManager).removePending(eq(rule.getId()), anyString());
+        verify(alarmCacheManager).putFiring(eq(rule.getId()), anyString(), eq(pending));
+        verify(alarmCommonReduce).reduceAndSendAlarm(org.mockito.ArgumentMatchers.any());
+        assertEquals(ALERT_STATUS_FIRING, pending.getStatus());
+        assertEquals(2, pending.getTriggerTimes());
+    }
+
+    @Test
+    void emptyResultRecoversAllCachedAlertsForDefine() {
+        SingleAlert firing = cachedAlert(ALERT_STATUS_FIRING);
+        SingleAlert pending = cachedAlert(ALERT_STATUS_PENDING);
+        when(dataSourceService.query("sql", EXPR, TRACE_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(List.of());
+        when(alarmCacheManager.getFiringAlerts(rule.getId())).thenReturn(Map.of("firing-fp", firing));
+        when(alarmCacheManager.getPendingAlerts(rule.getId())).thenReturn(Map.of("pending-fp", pending));
+        when(alarmCacheManager.removeFiring(rule.getId(), "firing-fp")).thenReturn(firing);
+
+        calculator.calculate(rule);
+
+        ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
+        verify(alarmCacheManager).removeFiring(rule.getId(), "firing-fp");
+        verify(alarmCacheManager).removePending(rule.getId(), "pending-fp");
+        verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture());
+        assertEquals(ALERT_STATUS_RESOLVED, alertCaptor.getValue().getStatus());
+        assertNotNull(alertCaptor.getValue().getEndAt());
+    }
+
+    @Test
+    void unmatchedCachedFingerprintsAreRecoveredWhenOtherGroupsStillBreach() {
+        SingleAlert staleFiring = cachedAlert(ALERT_STATUS_FIRING);
+        when(dataSourceService.query("sql", EXPR, TRACE_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(List.of(traceBreachRow()));
+        when(alarmCacheManager.getPending(eq(rule.getId()), anyString())).thenReturn(null);
+        when(alarmCacheManager.getFiring(eq(rule.getId()), anyString())).thenReturn(null);
+        when(alarmCacheManager.getFiringAlerts(rule.getId())).thenReturn(Map.of("stale-fp", staleFiring));
+        when(alarmCacheManager.removeFiring(rule.getId(), "stale-fp")).thenReturn(staleFiring);
+
+        calculator.calculate(rule);
+
+        verify(alarmCacheManager).removeFiring(rule.getId(), "stale-fp");
+    }
+
+    @Test
+    void queryFailureDoesNotRecoverOrTriggerAlerts() {
+        when(dataSourceService.query("sql", EXPR, TRACE_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenThrow(new RuntimeException("greptime unavailable"));
+
+        calculator.calculate(rule);
+
+        verifyNoInteractions(alarmCacheManager);
+        verifyNoInteractions(alarmCommonReduce);
+    }
+
+    @Test
+    void missingValueColumnIsEvaluationFailureWithoutRecovery() {
+        when(dataSourceService.query("sql", EXPR, TRACE_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(List.of(Map.of("service_name", "checkout")));
+
+        calculator.calculate(rule);
+
+        verifyNoInteractions(alarmCacheManager);
+        verifyNoInteractions(alarmCommonReduce);
+    }
+
+    private Map<String, Object> traceBreachRow() {
+        Map<String, Object> row = new LinkedHashMap<>();
+        row.put("service_name", "checkout");
+        row.put("operation", "GET /checkout");
+        row.put("span_kind", "SPAN_KIND_SERVER");
+        row.put("workspace_id", "workspace-a");
+        row.put("entity_id", "entity-a");
+        row.put("__value__", 0.2D);
+        row.put("__timestamp__", 1_725_000_000_000L);
+        row.put("calls_total", 100L);
+        return row;
+    }
+
+    private SingleAlert cachedAlert(String status) {
+        return SingleAlert.builder()
+                .status(status)
+                .triggerTimes(1)
+                .labels(Map.of(LABEL_DEFINE_ID, "42", LABEL_ALERT_NAME, "apm_error_rate",
+                        "service_name", "checkout"))
+                .startAt(System.currentTimeMillis() - 60_000)
+                .activeAt(System.currentTimeMillis() - 30_000)
+                .build();
+    }
+}
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluatorTest.java
index e27cb833f3..2279ff0d14 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluatorTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluatorTest.java
@@ -19,9 +19,11 @@ package org.apache.hertzbeat.alert.calculate.realtime.window;
 
 import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
 import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
 import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.entity.log.LogEntry;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,14 +34,20 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.junit.jupiter.api.Assertions.assertAll;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -92,20 +100,27 @@ class AlarmEvaluatorTest {
                 .build();
 
         // Create window data
-        WindowAggregator.WindowKey windowKey = new WindowAggregator.WindowKey(1L, 
+        WindowAggregator.WindowKey windowKey = new WindowAggregator.WindowKey(1L,
             System.currentTimeMillis() - 60000, System.currentTimeMillis());
         windowData = new WindowAggregator.WindowData(windowKey, alertDefine);
         windowData.addMatchingLog(matchingEvent);
     }
 
+    @AfterEach
+    void tearDown() {
+        if (alarmEvaluator != null) {
+            alarmEvaluator.destroy();
+        }
+    }
+
     @Test
     void testProcessWindowDataWithIndividualMode() throws InterruptedException {
         // Given - alert define with individual mode
         alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
-        
+
         // When
         alarmEvaluator.sendAndProcessWindowData(windowData);
-        
+
         // Wait for async processing
         Thread.sleep(2000);
 
@@ -118,10 +133,10 @@ class AlarmEvaluatorTest {
     void testProcessWindowDataWithGroupMode() throws InterruptedException {
         // Given - alert define with group mode
         alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP));
-        
+
         // When
         alarmEvaluator.sendAndProcessWindowData(windowData);
-        
+
         // Wait for async processing
         Thread.sleep(2000);
 
@@ -133,13 +148,13 @@ class AlarmEvaluatorTest {
     @Test
     void testProcessWindowDataWithEmptyLogs() throws InterruptedException {
         // Given - window data with no matching logs
-        WindowAggregator.WindowKey emptyWindowKey = new WindowAggregator.WindowKey(1L, 
+        WindowAggregator.WindowKey emptyWindowKey = new WindowAggregator.WindowKey(1L,
             System.currentTimeMillis() - 60000, System.currentTimeMillis());
         WindowAggregator.WindowData emptyWindowData = new WindowAggregator.WindowData(emptyWindowKey, alertDefine);
-        
+
         // When
         alarmEvaluator.sendAndProcessWindowData(emptyWindowData);
-        
+
         // Wait for async processing
         Thread.sleep(2000);
 
@@ -152,10 +167,10 @@ class AlarmEvaluatorTest {
     void testProcessWindowDataWithInsufficientTimes() throws InterruptedException {
         // Given - alert define requiring 3 times but only 1 matching log
         alertDefine.setTimes(3);
-        
+
         // When
         alarmEvaluator.sendAndProcessWindowData(windowData);
-        
+
         // Wait for async processing
         Thread.sleep(2000);
 
@@ -169,17 +184,17 @@ class AlarmEvaluatorTest {
         // Given
         alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL, CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL));
         alertDefine.setAnnotations(Map.of("summary", "Test summary", "description", "Test description"));
-        
+
         // When
         alarmEvaluator.sendAndProcessWindowData(windowData);
-        
+
         // Wait for async processing
         Thread.sleep(2000);
 
         // Then
         ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
         verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture());
-        
+
         SingleAlert capturedAlert = alertCaptor.getValue();
         assertAll(
             () -> assertNotNull(capturedAlert),
@@ -191,7 +206,7 @@ class AlarmEvaluatorTest {
             () -> assertTrue(capturedAlert.getStartAt() > 0),
             () -> assertTrue(capturedAlert.getActiveAt() > 0)
         );
-        
+
         // Verify labels contain alert define info and log entry data
         Map<String, String> labels = capturedAlert.getLabels();
         assertTrue(labels.containsKey(CommonConstants.LABEL_ALERT_SEVERITY));
@@ -203,24 +218,24 @@ class AlarmEvaluatorTest {
     void testGroupAlertGeneration() throws InterruptedException {
         // Given
         alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP, CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING));
-        
+
         // Add more matching logs to test group functionality
         MatchingLogEvent secondEvent = MatchingLogEvent.builder()
                 .logEntry(LogEntry.builder()
                     .timeUnixNano((System.currentTimeMillis() + 1000) * 1_000_000L)
-                    .severityText("ERROR") 
+                    .severityText("ERROR")
                     .body("Second error message")
                     .build())
                 .alertDefine(alertDefine)
                 .eventTimestamp(System.currentTimeMillis() + 1000)
                 .workerTimestamp(System.currentTimeMillis() + 1000)
                 .build();
-        
+
         windowData.addMatchingLog(secondEvent);
-        
+
         // When
         alarmEvaluator.sendAndProcessWindowData(windowData);
-        
+
         // Wait for async processing
         Thread.sleep(2000);
 
@@ -230,10 +245,10 @@ class AlarmEvaluatorTest {
         @SuppressWarnings("unchecked")
         ArgumentCaptor<List<SingleAlert>> alertsCaptor = ArgumentCaptor.forClass(List.class);
         verify(alarmCommonReduce).reduceAndSendAlarmGroup(labelsCaptor.capture(), alertsCaptor.capture());
-        
+
         Map<String, String> groupLabels = labelsCaptor.getValue();
         List<SingleAlert> alerts = alertsCaptor.getValue();
-        
+
         assertAll(
             () -> assertNotNull(groupLabels),
             () -> assertNotNull(alerts),
@@ -244,7 +259,7 @@ class AlarmEvaluatorTest {
             () -> assertEquals(CommonConstants.ALERT_MODE_GROUP, groupLabels.get(CommonConstants.ALERT_MODE_LABEL)),
             () -> assertEquals("2", groupLabels.get("matching_logs_count"))
         );
-        
+
         // Verify each alert in the group
         for (SingleAlert alert : alerts) {
             assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus());
@@ -281,7 +296,7 @@ class AlarmEvaluatorTest {
         detailedWindowData.addMatchingLog(detailedEvent);
 
         alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
-        
+
         // When
         alarmEvaluator.sendAndProcessWindowData(detailedWindowData);
         Thread.sleep(2000);
@@ -289,10 +304,10 @@ class AlarmEvaluatorTest {
         // Then
         ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
         verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture());
-        
+
         SingleAlert alert = alertCaptor.getValue();
         Map<String, String> labels = alert.getLabels();
-        
+
         // Verify log entry fields are mapped to labels
         assertAll(
             () -> assertEquals("17", labels.get("severityNumber")),
@@ -314,7 +329,7 @@ class AlarmEvaluatorTest {
         // Given - multiple matching logs in the same window
         alertDefine.setTimes(2); // Require at least 2 occurrences
         alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP));
-        
+
         // Add second matching log
         MatchingLogEvent secondEvent = MatchingLogEvent.builder()
                 .logEntry(LogEntry.builder()
@@ -325,22 +340,122 @@ class AlarmEvaluatorTest {
                 .eventTimestamp(System.currentTimeMillis() + 5000)
                 .workerTimestamp(System.currentTimeMillis() + 5000)
                 .build();
-        
+
         windowData.addMatchingLog(secondEvent);
-        
+
         // When
         alarmEvaluator.sendAndProcessWindowData(windowData);
         Thread.sleep(2000);
 
         // Then - should trigger group alert since threshold is met
         verify(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList());
-        
+
         @SuppressWarnings("unchecked")
         ArgumentCaptor<List<SingleAlert>> alertsCaptor = ArgumentCaptor.forClass(List.class);
         verify(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), alertsCaptor.capture());
-        
+
         List<SingleAlert> alerts = alertsCaptor.getValue();
         assertEquals(2, alerts.size());
         assertEquals(2, alerts.get(0).getTriggerTimes()); // Each alert should have trigger times = total count
     }
+
+    @Test
+    void testSendAndProcessWindowDataRunsOnVirtualThread() throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicBoolean virtualThread = new AtomicBoolean(false);
+        doAnswer(invocation -> {
+            virtualThread.set(Thread.currentThread().isVirtual());
+            latch.countDown();
+            return null;
+        }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class));
+
+        alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
+        alarmEvaluator.sendAndProcessWindowData(windowData);
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertTrue(virtualThread.get());
+    }
+
+    @Test
+    void testSendAndProcessWindowDataQueuesWhenConcurrencyLimitReached() throws Exception {
+        VirtualThreadProperties properties = new VirtualThreadProperties(
+                true,
+                VirtualThreadProperties.PoolProperties.collectorDefaults(),
+                VirtualThreadProperties.PoolProperties.commonDefaults(),
+                VirtualThreadProperties.PoolProperties.managerDefaults(),
+                new VirtualThreadProperties.AlerterProperties(
+                        VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(),
+                        10,
+                        VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
+                        VirtualThreadProperties.QueueProperties.reduceDefaults(),
+                        new VirtualThreadProperties.QueueProperties(1, 0),
+                        4),
+                VirtualThreadProperties.PoolProperties.warehouseDefaults(),
+                VirtualThreadProperties.AsyncProperties.defaults());
+        alarmEvaluator.destroy();
+        alarmEvaluator = new AlarmEvaluator(alarmCommonReduce, properties);
+
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirst = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        AtomicInteger invocationOrder = new AtomicInteger();
+        doAnswer(invocation -> {
+            int order = invocationOrder.incrementAndGet();
+            if (order == 1) {
+                firstStarted.countDown();
+                try {
+                    releaseFirst.await(5, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            } else if (order == 2) {
+                secondStarted.countDown();
+            }
+            return null;
+        }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class));
+
+        WindowAggregator.WindowData firstWindow = cloneWindowDataWithBody("first");
+        WindowAggregator.WindowData secondWindow = cloneWindowDataWithBody("second");
+        firstWindow.getAlertDefine().setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
+        secondWindow.getAlertDefine().setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
+
+        alarmEvaluator.sendAndProcessWindowData(firstWindow);
+        assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
+
+        alarmEvaluator.sendAndProcessWindowData(secondWindow);
+        assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
+
+        releaseFirst.countDown();
+        assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
+    }
+
+    private WindowAggregator.WindowData cloneWindowDataWithBody(String body) {
+        LogEntry logEntry = LogEntry.builder()
+                .timeUnixNano(System.currentTimeMillis() * 1_000_000L)
+                .severityText("ERROR")
+                .body(body)
+                .build();
+        AlertDefine define = AlertDefine.builder()
+                .id(alertDefine.getId())
+                .name(alertDefine.getName())
+                .type(alertDefine.getType())
+                .expr(alertDefine.getExpr())
+                .times(alertDefine.getTimes())
+                .template(alertDefine.getTemplate())
+                .labels(alertDefine.getLabels())
+                .annotations(alertDefine.getAnnotations())
+                .enable(alertDefine.isEnable())
+                .build();
+        MatchingLogEvent event = MatchingLogEvent.builder()
+                .logEntry(logEntry)
+                .alertDefine(define)
+                .eventTimestamp(System.currentTimeMillis())
+                .workerTimestamp(System.currentTimeMillis())
+                .build();
+        WindowAggregator.WindowData clonedWindowData = new WindowAggregator.WindowData(
+                new WindowAggregator.WindowKey(define.getId(), System.currentTimeMillis() - 60000, System.currentTimeMillis()),
+                define);
+        clonedWindowData.addMatchingLog(event);
+        return clonedWindowData;
+    }
 }
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java
index 14471e0fef..d05e5e1a14 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java
@@ -18,6 +18,7 @@
 package org.apache.hertzbeat.alert.controller;
 
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
@@ -138,6 +139,28 @@ class AlertDefineControllerTest {
 
     }
 
+    @Test
+    void testGetPeriodicTraceDefinePreview() throws Exception {
+        List<Map<String, Object>> previewData = new ArrayList<>();
+        Map<String, Object> row = new HashMap<>();
+        row.put("__value__", 0.12D);
+        row.put("service_name", "checkout");
+        previewData.add(row);
+
+        Mockito.when(alertDefineService.getDefinePreview(eq("sql"), eq("periodic_trace"), anyString()))
+                .thenReturn(previewData);
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/define/preview/{datasource}", "sql")
+                        .param("type", "periodic_trace")
+                        .param("expr", "SELECT service_name, 0.12 AS __value__ FROM hertzbeat_apm_red_1m"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(0))
+                .andExpect(jsonPath("$.data[0].__value__").value(0.12D))
+                .andExpect(jsonPath("$.data[0].service_name").value("checkout"));
+
+        Mockito.verify(alertDefineService).getDefinePreview(eq("sql"), eq("periodic_trace"), anyString());
+    }
+
     @Test
     void modifyAlertDefine() throws Exception {
         mockMvc.perform(MockMvcRequestBuilders.put("/api/alert/define")
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java
index 318f328179..8f66759350 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java
@@ -73,6 +73,7 @@ class AlertsControllerTest {
         String orderType = "desc";
         String status = "firing";
         String content = "test";
+        String severity = "critical";
         int pageIndex = 0;
         int pageSize = 10;
 
@@ -81,13 +82,14 @@ class AlertsControllerTest {
                 PageRequest.of(pageIndex, pageSize, Sort.by(sortField).descending()),
                 ids.size()
         );
-        Mockito.when(alertService.getGroupAlerts(status, content, sortField, orderType, pageIndex, pageSize))
+        Mockito.when(alertService.getGroupAlerts(status, content, severity, sortField, orderType, pageIndex, pageSize))
                 .thenReturn(alertPage);
 
         mockMvc.perform(MockMvcRequestBuilders
                         .get("/api/alerts/group")
                         .param("status", status)
                         .param("search", content)
+                        .param("severity", severity)
                         .param("sort", sortField)
                         .param("order", orderType)
                         .param("pageIndex", String.valueOf(pageIndex))
@@ -125,6 +127,32 @@ class AlertsControllerTest {
                 .andReturn();
     }
 
+    @Test
+    void applyGroupAlertAcknowledgedStatus() throws Exception {
+        mockMvc.perform(
+                        MockMvcRequestBuilders
+                                .put("/api/alerts/group/status/acknowledged")
+                                .param("ids", ids.stream().map(String::valueOf).collect(Collectors.joining(",")))
+                )
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(content().json("{\"data\":null,\"msg\":null,\"code\":0}"))
+                .andReturn();
+    }
+
+    @Test
+    void applySingleAlertAcknowledgedStatus() throws Exception {
+        mockMvc.perform(
+                        MockMvcRequestBuilders
+                                .put("/api/alerts/status/acknowledged")
+                                .param("ids", ids.stream().map(String::valueOf).collect(Collectors.joining(",")))
+                )
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(content().json("{\"data\":null,\"msg\":null,\"code\":0}"))
+                .andReturn();
+    }
+
     @Test
     void getAlertsSummary() throws Exception {
         Mockito.when(alertService.getAlertsSummary()).thenReturn(new AlertSummary());
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatchTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatchTest.java
index 8102afcdc0..b025d931b2 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatchTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatchTest.java
@@ -19,7 +19,10 @@ package org.apache.hertzbeat.alert.notice;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyByte;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -74,7 +77,7 @@ class AlertNoticeDispatchTest {
     @BeforeEach
     void setUp() {
         when(alertNotifyHandler.type()).thenReturn((byte) 1);
-        
+
         List<AlertNotifyHandler> alertNotifyHandlerList = List.of(alertNotifyHandler);
         alertNoticeDispatch = new AlertNoticeDispatch(
                 workerPool,
@@ -84,13 +87,13 @@ class AlertNoticeDispatchTest {
                 pluginRunner,
                 emitterManager
         );
-        
+
         receiver = NoticeReceiver.builder()
                 .id(1L)
                 .name("test-receiver")
                 .type((byte) 1)
                 .build();
-        
+
         alert = GroupAlert.builder()
                 .id(1L)
                 .status("firing")
@@ -107,7 +110,7 @@ class AlertNoticeDispatchTest {
         template.setName("default-template");
         when(noticeConfigService.getDefaultNoticeTemplateByType((byte) 1)).thenReturn(template);
         doNothing().when(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert));
-        
+
         assertTrue(alertNoticeDispatch.sendNoticeMsg(receiver, null, alert));
         verify(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert));
     }
@@ -117,7 +120,7 @@ class AlertNoticeDispatchTest {
         GroupAlert alert = new GroupAlert();
         alert.setId(1L);
         alert.setStatus("firing");
-        
+
         boolean result = alertNoticeDispatch.sendNoticeMsg(null, null, alert);
         assertFalse(result);
     }
@@ -127,7 +130,7 @@ class AlertNoticeDispatchTest {
         NoticeReceiver receiver = new NoticeReceiver();
         receiver.setId(1L);
         receiver.setName("test-receiver");
-        
+
         GroupAlert alert = new GroupAlert();
         alert.setId(1L);
         alert.setStatus("firing");
@@ -142,11 +145,39 @@ class AlertNoticeDispatchTest {
         receiver.setId(1L);
         receiver.setName("test-receiver");
         receiver.setType((byte) 2);
-        
+
         GroupAlert alert = new GroupAlert();
         alert.setId(1L);
         alert.setStatus("firing");
 
         assertFalse(alertNoticeDispatch.sendNoticeMsg(receiver, null, alert));
     }
+
+    @Test
+    void testDispatchAlarmUsesTypedNotifyExecution() {
+        NoticeTemplate template = new NoticeTemplate();
+        template.setId(1L);
+        template.setName("default-template");
+
+        when(alertStoreHandler.store(alert)).thenReturn(alert);
+        when(noticeConfigService.getReceiverFilterRule(alert)).thenReturn(Collections.singletonList(
+                org.apache.hertzbeat.common.entity.alerter.NoticeRule.builder()
+                        .receiverId(Collections.singletonList(1L))
+                        .templateId(1L)
+                        .build()));
+        when(noticeConfigService.getReceiverById(1L)).thenReturn(receiver);
+        when(noticeConfigService.getOneTemplateById(1L)).thenReturn(template);
+        doNothing().when(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert));
+        doAnswer(invocation -> {
+            Runnable task = invocation.getArgument(1);
+            task.run();
+            return null;
+        }).when(workerPool).executeNotify(anyByte(), any(Runnable.class));
+
+        alertNoticeDispatch.dispatchAlarm(alert);
+
+        verify(workerPool).executeNotify(eq((byte) 1), any(Runnable.class));
+        verify(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert));
+        verify(emitterManager).broadcast(any(String.class));
+    }
 }
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java
index a98f36dfff..0208188529 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java
@@ -17,7 +17,20 @@
 
 package org.apache.hertzbeat.alert.reduce;
 
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -40,14 +53,86 @@ class AlarmCommonReduceTest {
 
     @BeforeEach
     void setUp() {
-
-        testAlert = SingleAlert.builder().build();
+        testAlert = SingleAlert.builder().labels(new HashMap<>(Map.of("alertname", "test"))).build();
         alarmCommonReduce = new AlarmCommonReduce(alarmGroupReduce);
     }
 
+    @AfterEach
+    void tearDown() {
+        if (alarmCommonReduce != null) {
+            alarmCommonReduce.destroy();
+        }
+    }
+
     @Test
     void testReduceAndSendAlarm() {
         alarmCommonReduce.reduceAndSendAlarm(testAlert);
     }
 
+    @Test
+    void testReduceAndSendAlarmRunsOnVirtualThread() throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicBoolean virtualThread = new AtomicBoolean(false);
+        doAnswer(invocation -> {
+            virtualThread.set(Thread.currentThread().isVirtual());
+            latch.countDown();
+            return null;
+        }).when(alarmGroupReduce).processGroupAlert(any(SingleAlert.class));
+
+        alarmCommonReduce.reduceAndSendAlarm(testAlert);
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertTrue(virtualThread.get());
+    }
+
+    @Test
+    void testReduceAndSendAlarmQueuesWhenConcurrencyLimitReached() throws Exception {
+        VirtualThreadProperties properties = new VirtualThreadProperties(
+                true,
+                VirtualThreadProperties.PoolProperties.collectorDefaults(),
+                VirtualThreadProperties.PoolProperties.commonDefaults(),
+                VirtualThreadProperties.PoolProperties.managerDefaults(),
+                new VirtualThreadProperties.AlerterProperties(
+                        VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(),
+                        10,
+                        VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
+                        new VirtualThreadProperties.QueueProperties(1, 0),
+                        VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
+                        4),
+                VirtualThreadProperties.PoolProperties.warehouseDefaults(),
+                VirtualThreadProperties.AsyncProperties.defaults());
+        alarmCommonReduce.destroy();
+        alarmCommonReduce = new AlarmCommonReduce(alarmGroupReduce, properties);
+
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirst = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        AtomicInteger invocationOrder = new AtomicInteger();
+        doAnswer(invocation -> {
+            int order = invocationOrder.incrementAndGet();
+            if (order == 1) {
+                firstStarted.countDown();
+                try {
+                    releaseFirst.await(5, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            } else if (order == 2) {
+                secondStarted.countDown();
+            }
+            return null;
+        }).when(alarmGroupReduce).processGroupAlert(any(SingleAlert.class));
+
+        alarmCommonReduce.reduceAndSendAlarm(SingleAlert.builder()
+                .labels(new HashMap<>(Map.of("name", "first"))).build());
+        assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
+
+        alarmCommonReduce.reduceAndSendAlarm(SingleAlert.builder()
+                .labels(new HashMap<>(Map.of("name", "second"))).build());
+        assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
+
+        releaseFirst.countDown();
+        assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
+    }
+
 }
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java
index a8e8dc0533..939440b95d 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java
@@ -38,6 +38,9 @@
 
 package org.apache.hertzbeat.alert.reduce;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.never;
@@ -47,9 +50,15 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
 import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
@@ -62,10 +71,10 @@ class AlarmGroupReduceTest {
 
     @Mock
     private AlarmInhibitReduce alarmInhibitReduce;
-    
+
     @Mock
     private AlertGroupConvergeDao alertGroupConvergeDao;
-    
+
     private AlarmGroupReduce alarmGroupReduce;
 
     @BeforeEach
@@ -73,7 +82,15 @@ class AlarmGroupReduceTest {
         MockitoAnnotations.openMocks(this);
         when(alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue())
             .thenReturn(Collections.emptyList());
-        alarmGroupReduce = new AlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao);
+        alarmGroupReduce = new AlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao,
+                new VirtualThreadProperties(), false);
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (alarmGroupReduce != null) {
+            alarmGroupReduce.destroy();
+        }
     }
 
     @Test
@@ -83,10 +100,10 @@ class AlarmGroupReduceTest {
                 .status("firing")
                 .labels(createLabels("severity", "critical"))
                 .build();
-                
+
         alarmGroupReduce.processGroupAlert(alert);
-        
-        verify(alarmInhibitReduce).inhibitAlarm(argThat(group -> 
+
+        verify(alarmInhibitReduce).inhibitAlarm(argThat(group ->
             group.getAlerts().size() == 1 && group.getAlerts().get(0).getFingerprint().equals("fp1")));
     }
 
@@ -99,19 +116,55 @@ class AlarmGroupReduceTest {
         when(alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue())
             .thenReturn(Collections.singletonList(rule));
         alarmGroupReduce.refreshGroupDefines(Collections.singletonList(rule));
-        
+
         SingleAlert alert = SingleAlert.builder()
                 .fingerprint("fp1")
                 .status("firing")
                 .labels(createLabels("severity", "critical", "instance", "host1"))
                 .build();
-                
+
         alarmGroupReduce.processGroupAlert(alert);
-        
+
         // Verify group is created and cached (implicitly tested through internal state)
         verify(alarmInhibitReduce, never()).inhibitAlarm(any());  // Should not send immediately due to group wait
     }
 
+    @Test
+    void dispatchCheckAndSendGroupsRunsOnVirtualThread() throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicBoolean virtualThread = new AtomicBoolean(false);
+        alarmGroupReduce.destroy();
+        alarmGroupReduce = new TestAlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao,
+                new VirtualThreadProperties(), latch, virtualThread, null, null, null, null, null);
+
+        alarmGroupReduce.dispatchCheckAndSendGroups();
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertTrue(virtualThread.get());
+    }
+
+    @Test
+    void dispatchCheckAndSendGroupsDoesNotRunConcurrently() throws Exception {
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirst = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        AtomicInteger maxConcurrent = new AtomicInteger();
+        alarmGroupReduce.destroy();
+        alarmGroupReduce = new TestAlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao,
+                new VirtualThreadProperties(), null, null, firstStarted, releaseFirst, secondStarted,
+                maxConcurrent, new AtomicInteger());
+
+        alarmGroupReduce.dispatchCheckAndSendGroups();
+        assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
+
+        alarmGroupReduce.dispatchCheckAndSendGroups();
+        assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
+
+        releaseFirst.countDown();
+        assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
+        assertEquals(1, maxConcurrent.get());
+    }
+
     private Map<String, String> createLabels(String... keyValues) {
         Map<String, String> labels = new HashMap<>();
         for (int i = 0; i < keyValues.length; i += 2) {
@@ -119,4 +172,67 @@ class AlarmGroupReduceTest {
         }
         return labels;
     }
+
+    private static final class TestAlarmGroupReduce extends AlarmGroupReduce {
+
+        private final CountDownLatch virtualThreadLatch;
+
+        private final AtomicBoolean virtualThread;
+
+        private final CountDownLatch firstStarted;
+
+        private final CountDownLatch releaseFirst;
+
+        private final CountDownLatch secondStarted;
+
+        private final AtomicInteger maxConcurrent;
+
+        private final AtomicInteger concurrent;
+
+        private final AtomicInteger invocations;
+
+        private TestAlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao,
+                                     VirtualThreadProperties properties, CountDownLatch virtualThreadLatch,
+                                     AtomicBoolean virtualThread, CountDownLatch firstStarted,
+                                     CountDownLatch releaseFirst, CountDownLatch secondStarted,
+                                     AtomicInteger maxConcurrent, AtomicInteger invocations) {
+            super(alarmInhibitReduce, alertGroupConvergeDao, properties, false);
+            this.virtualThreadLatch = virtualThreadLatch;
+            this.virtualThread = virtualThread;
+            this.firstStarted = firstStarted;
+            this.releaseFirst = releaseFirst;
+            this.secondStarted = secondStarted;
+            this.maxConcurrent = maxConcurrent;
+            this.invocations = invocations;
+            this.concurrent = maxConcurrent == null ? null : new AtomicInteger();
+        }
+
+        @Override
+        void beforeCheckAndSendGroupsRun() {
+            if (virtualThread != null) {
+                virtualThread.set(Thread.currentThread().isVirtual());
+            }
+            if (virtualThreadLatch != null) {
+                virtualThreadLatch.countDown();
+            }
+            if (maxConcurrent == null || invocations == null) {
+                return;
+            }
+            int running = concurrent.incrementAndGet();
+            maxConcurrent.accumulateAndGet(running, Math::max);
+            int currentInvocation = invocations.incrementAndGet();
+            try {
+                if (currentInvocation == 1 && firstStarted != null && releaseFirst != null) {
+                    firstStarted.countDown();
+                    releaseFirst.await(5, TimeUnit.SECONDS);
+                } else if (currentInvocation == 2 && secondStarted != null) {
+                    secondStarted.countDown();
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } finally {
+                concurrent.decrementAndGet();
+            }
+        }
+    }
 }
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduceTest.java
index 18bc17eb57..6991dace48 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduceTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduceTest.java
@@ -39,6 +39,7 @@
 package org.apache.hertzbeat.alert.reduce;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -49,14 +50,19 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.ArrayList;
 import java.util.List;
-
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.hertzbeat.alert.AlerterProperties;
 import org.apache.hertzbeat.alert.dao.AlertInhibitDao;
+import org.apache.hertzbeat.common.config.VirtualThreadProperties;
 import org.apache.hertzbeat.common.entity.alerter.AlertInhibit;
 import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
@@ -69,10 +75,10 @@ class AlarmInhibitReduceTest {
 
     @Mock
     private AlertInhibitDao alertInhibitDao;
-    
+
     @Mock
     private AlarmSilenceReduce alarmSilenceReduce;
-    
+
     @Mock
     private AlerterProperties alerterProperties;
 
@@ -88,20 +94,28 @@ class AlarmInhibitReduceTest {
         AlerterProperties.InhibitProperties inhibitProperties = new AlerterProperties.InhibitProperties();
         inhibitProperties.setTtl(60000);
         when(alerterProperties.getInhibit()).thenReturn(inhibitProperties);
-        
-        alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties);
+
+        alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties,
+                new VirtualThreadProperties(), false);
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (alarmInhibitReduce != null) {
+            alarmInhibitReduce.destroy();
+        }
     }
 
     @Test
     void whenNoInhibitRules_shouldForwardAlert() {
-        SingleAlert alert = createSingleAlert("firing", "fp1", 
+        SingleAlert alert = createSingleAlert("firing", "fp1",
             createLabels("severity", "warning"));
-        GroupAlert groupAlert = createGroupAlert("firing", 
-            createLabels("severity", "warning"), 
+        GroupAlert groupAlert = createGroupAlert("firing",
+            createLabels("severity", "warning"),
             Stream.of(alert).collect(Collectors.toList()));
-        
+
         alarmInhibitReduce.inhibitAlarm(groupAlert);
-        
+
         verify(alarmSilenceReduce).silenceAlarm(groupAlert);
     }
 
@@ -114,9 +128,9 @@ class AlarmInhibitReduceTest {
                 .targetLabels(createLabels("severity", "warning"))
                 .equalLabels(Collections.singletonList("instance"))
                 .build();
-                
+
         alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule));
-        
+
         // Create and process source alert
         SingleAlert sourceAlert = createSingleAlert("firing", "fp1",
             createLabels("severity", "critical", "instance", "host1"));
@@ -124,7 +138,7 @@ class AlarmInhibitReduceTest {
             createLabels("severity", "critical", "instance", "host1"),
             Stream.of(sourceAlert).collect(Collectors.toList()));
         alarmInhibitReduce.inhibitAlarm(sourceGroupAlert);
-        
+
         // Create and process target alert
         SingleAlert targetAlert = createSingleAlert("firing", "fp2",
             createLabels("severity", "warning", "instance", "host1"));
@@ -132,7 +146,7 @@ class AlarmInhibitReduceTest {
             createLabels("severity", "warning", "instance", "host1"),
             Stream.of(targetAlert).collect(Collectors.toList()));
         alarmInhibitReduce.inhibitAlarm(targetGroupAlert);
-        
+
         verify(alarmSilenceReduce).silenceAlarm(sourceGroupAlert);
         verify(alarmSilenceReduce, never()).silenceAlarm(targetGroupAlert);
     }
@@ -146,15 +160,15 @@ class AlarmInhibitReduceTest {
                 .targetLabels(createLabels("severity", "warning"))
                 .equalLabels(Arrays.asList("instance"))
                 .build();
-                
+
         alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule));
-        
+
         // Create source alert with different instance
         GroupAlert sourceAlert = createGroupAlert("firing",
             createLabels("severity", "critical", "instance", "host1"),
             Collections.emptyList());
         alarmInhibitReduce.inhibitAlarm(sourceAlert);
-        
+
         // Create target alert with different instance
         GroupAlert targetAlert = createGroupAlert("firing",
             createLabels("severity", "warning", "instance", "host2"),
@@ -170,16 +184,16 @@ class AlarmInhibitReduceTest {
                 .sourceLabels(createLabels("severity", "critical"))
                 .targetLabels(createLabels("severity", "warning"))
                 .build();
-                
+
         alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule));
-        
+
         GroupAlert sourceAlert = createGroupAlert("firing",
             createLabels("severity", "critical"),
             Collections.emptyList());
         GroupAlert resolvedAlert = createGroupAlert("resolved",
             createLabels("severity", "warning"),
             Collections.emptyList());
-            
+
         alarmInhibitReduce.inhibitAlarm(sourceAlert);
         alarmInhibitReduce.inhibitAlarm(resolvedAlert);
     }
@@ -195,7 +209,7 @@ class AlarmInhibitReduceTest {
         GroupAlert alert = GroupAlert.builder()
                 .alerts(new ArrayList<>())
                 .build();
-        
+
         alarmInhibitReduce.inhibitAlarm(alert);
         verify(alarmSilenceReduce).silenceAlarm(alert);
     }
@@ -209,9 +223,9 @@ class AlarmInhibitReduceTest {
                 .targetLabels(createLabels("severity", "warning"))
                 .equalLabels(Collections.singletonList("instance"))
                 .build();
-                
+
         alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule));
-        
+
         // Create source alerts
         SingleAlert sourceAlert1 = createSingleAlert("firing", "fp1",
             createLabels("severity", "critical", "instance", "host1"));
@@ -220,19 +234,19 @@ class AlarmInhibitReduceTest {
         GroupAlert sourceGroupAlert = createGroupAlert("firing", null,
             new ArrayList<>(Arrays.asList(sourceAlert1, sourceAlert2)));
         alarmInhibitReduce.inhibitAlarm(sourceGroupAlert);
-        
+
         // Create target alerts
         SingleAlert targetAlert1 = createSingleAlert("firing", "fp3",
             createLabels("severity", "warning", "instance", "host1"));
         SingleAlert targetAlert2 = createSingleAlert("firing", "fp4",
-            createLabels("severity", "warning", "instance", "host2")); 
+            createLabels("severity", "warning", "instance", "host2"));
         SingleAlert targetAlert3 = createSingleAlert("firing", "fp5",
             createLabels("severity", "warning", "instance", "host3"));
         GroupAlert targetGroupAlert = createGroupAlert("firing", null,
             new ArrayList<>(Arrays.asList(targetAlert1, targetAlert2, targetAlert3)));
-        
+
         alarmInhibitReduce.inhibitAlarm(targetGroupAlert);
-        
+
         assertEquals(1, targetGroupAlert.getAlerts().size());
         assertEquals("fp5", targetGroupAlert.getAlerts().get(0).getFingerprint());
     }
@@ -246,7 +260,7 @@ class AlarmInhibitReduceTest {
                 .targetLabels(createLabels("severity", "warning"))
                 .equalLabels(Arrays.asList("instance"))
                 .build();
-                
+
         AlertInhibit rule2 = AlertInhibit.builder()
                 .id(2L)
                 .enable(true)
@@ -254,33 +268,33 @@ class AlarmInhibitReduceTest {
                 .targetLabels(createLabels("type", "memory"))
                 .equalLabels(Arrays.asList("host"))
                 .build();
-                
+
         alarmInhibitReduce.refreshInhibitRules(Arrays.asList(rule1, rule2));
-        
+
         // Test both rules being applied
         SingleAlert sourceAlert = createSingleAlert("firing", "fp1",
-            createLabels("severity", "critical", "type", "disk", 
+            createLabels("severity", "critical", "type", "disk",
                         "instance", "host1", "host", "server1"));
-                        
+
         GroupAlert sourceGroupAlert = GroupAlert.builder()
                 .alerts(Stream.of(sourceAlert).collect(Collectors.toList()))
                 .build();
-                
+
         alarmInhibitReduce.inhibitAlarm(sourceGroupAlert);
-        
+
         // Create alerts that match different rules
         SingleAlert targetAlert1 = createSingleAlert("firing", "fp2",
             createLabels("severity", "warning", "instance", "host1"));
         SingleAlert targetAlert2 = createSingleAlert("firing", "fp3",
             createLabels("type", "memory", "host", "server1"));
-            
+
         GroupAlert targetGroupAlert = GroupAlert.builder()
                 .alerts(new ArrayList<>(Arrays.asList(targetAlert1, targetAlert2)))
                 .status("firing")
                 .build();
-                
+
         alarmInhibitReduce.inhibitAlarm(targetGroupAlert);
-        
+
         assertTrue(targetGroupAlert.getAlerts().isEmpty());
     }
 
@@ -290,8 +304,10 @@ class AlarmInhibitReduceTest {
         AlerterProperties.InhibitProperties inhibitProperties = new AlerterProperties.InhibitProperties();
         inhibitProperties.setTtl(100);
         when(alerterProperties.getInhibit()).thenReturn(inhibitProperties);
-        alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties);
-        
+        alarmInhibitReduce.destroy();
+        alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties,
+                new VirtualThreadProperties(), false);
+
         AlertInhibit rule = AlertInhibit.builder()
                 .id(1L)
                 .enable(true)
@@ -299,9 +315,9 @@ class AlarmInhibitReduceTest {
                 .targetLabels(createLabels("severity", "warning"))
                 .equalLabels(Collections.singletonList("instance"))
                 .build();
-                
+
         alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule));
-        
+
         // Process source alert
         SingleAlert sourceAlert = createSingleAlert("firing", "fp1",
             createLabels("severity", "critical", "instance", "host1"));
@@ -309,10 +325,10 @@ class AlarmInhibitReduceTest {
             createLabels("severity", "critical", "instance", "host1"),
                 Stream.of(sourceAlert).collect(Collectors.toList()));
         alarmInhibitReduce.inhibitAlarm(sourceGroupAlert);
-        
+
         // Wait for source alert to expire
         Thread.sleep(200);
-        
+
         // Target alert should not be inhibited
         SingleAlert targetAlert = createSingleAlert("firing", "fp2",
             createLabels("severity", "warning", "instance", "host1"));
@@ -320,10 +336,46 @@ class AlarmInhibitReduceTest {
             createLabels("severity", "warning", "instance", "host1"),
             Stream.of(targetAlert).collect(Collectors.toList()));
         alarmInhibitReduce.inhibitAlarm(targetGroupAlert);
-        
+
         verify(alarmSilenceReduce).silenceAlarm(targetGroupAlert);
     }
 
+    @Test
+    void dispatchCleanupCacheRunsOnVirtualThread() throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicBoolean virtualThread = new AtomicBoolean(false);
+        alarmInhibitReduce.destroy();
+        alarmInhibitReduce = new TestAlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties,
+                new VirtualThreadProperties(), latch, virtualThread, null, null, null, null, null);
+
+        alarmInhibitReduce.dispatchCleanupCache();
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertTrue(virtualThread.get());
+    }
+
+    @Test
+    void dispatchCleanupCacheDoesNotRunConcurrently() throws Exception {
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirst = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        AtomicInteger maxConcurrent = new AtomicInteger();
+        alarmInhibitReduce.destroy();
+        alarmInhibitReduce = new TestAlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties,
+                new VirtualThreadProperties(), null, null, firstStarted, releaseFirst, secondStarted,
+                maxConcurrent, new AtomicInteger());
+
+        alarmInhibitReduce.dispatchCleanupCache();
+        assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
+
+        alarmInhibitReduce.dispatchCleanupCache();
+        assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
+
+        releaseFirst.countDown();
+        assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
+        assertEquals(1, maxConcurrent.get());
+    }
+
     private GroupAlert createGroupAlert(String status, Map<String, String> labels, List<SingleAlert> alerts) {
         return GroupAlert.builder()
                 .status(status)
@@ -347,4 +399,68 @@ class AlarmInhibitReduceTest {
                 .labels(labels)
                 .build();
     }
-} 
+
+    private static final class TestAlarmInhibitReduce extends AlarmInhibitReduce {
+
+        private final CountDownLatch virtualThreadLatch;
+
+        private final AtomicBoolean virtualThread;
+
+        private final CountDownLatch firstStarted;
+
+        private final CountDownLatch releaseFirst;
+
+        private final CountDownLatch secondStarted;
+
+        private final AtomicInteger maxConcurrent;
+
+        private final AtomicInteger concurrent;
+
+        private final AtomicInteger invocations;
+
+        private TestAlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao,
+                                       AlerterProperties alerterProperties, VirtualThreadProperties properties,
+                                       CountDownLatch virtualThreadLatch, AtomicBoolean virtualThread,
+                                       CountDownLatch firstStarted, CountDownLatch releaseFirst,
+                                       CountDownLatch secondStarted, AtomicInteger maxConcurrent,
+                                       AtomicInteger invocations) {
+            super(alarmSilenceReduce, alertInhibitDao, alerterProperties, properties, false);
+            this.virtualThreadLatch = virtualThreadLatch;
+            this.virtualThread = virtualThread;
+            this.firstStarted = firstStarted;
+            this.releaseFirst = releaseFirst;
+            this.secondStarted = secondStarted;
+            this.maxConcurrent = maxConcurrent;
+            this.invocations = invocations;
+            this.concurrent = maxConcurrent == null ? null : new AtomicInteger();
+        }
+
+        @Override
+        void beforeCleanupCacheRun() {
+            if (virtualThread != null) {
+                virtualThread.set(Thread.currentThread().isVirtual());
+            }
+            if (virtualThreadLatch != null) {
+                virtualThreadLatch.countDown();
+            }
+            if (maxConcurrent == null || invocations == null) {
+                return;
+            }
+            int running = concurrent.incrementAndGet();
+            maxConcurrent.accumulateAndGet(running, Math::max);
+            int currentInvocation = invocations.incrementAndGet();
+            try {
+                if (currentInvocation == 1 && firstStarted != null && releaseFirst != null) {
+                    firstStarted.countDown();
+                    releaseFirst.await(5, TimeUnit.SECONDS);
+                } else if (currentInvocation == 2 && secondStarted != null) {
+                    secondStarted.countDown();
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } finally {
+                concurrent.decrementAndGet();
+            }
+        }
+    }
+}
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduceTest.java
index b9bc9ea9df..994bab5367 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduceTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduceTest.java
@@ -45,7 +45,7 @@ class AlarmSilenceReduceTest {
 
     @Mock
     private AlertSilenceDao alertSilenceDao;
-    
+
     @Mock
     private AlertNoticeDispatch alertNoticeDispatch;
 
@@ -84,11 +84,11 @@ class AlarmSilenceReduceTest {
 
         when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule));
         when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule);
-        
+
         GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
-        
+
         alarmSilenceReduce.silenceAlarm(alert);
-        
+
         verify(alertNoticeDispatch, never()).dispatchAlarm(alert);
         verify(alertSilenceDao).save(silenceRule);
     }
@@ -102,8 +102,67 @@ class AlarmSilenceReduceTest {
                 .matchAll(false)
                 .type((byte) 1) // cyclic
                 .labels(createLabels("service", "web"))
-                .periodStart(now.minusMinutes(30).atZone(ZoneId.systemDefault())) 
-                .periodEnd(now.plusMinutes(30).atZone(ZoneId.systemDefault())) 
+                .periodStart(now.minusMinutes(30).atZone(ZoneId.systemDefault()))
+                .periodEnd(now.plusMinutes(30).atZone(ZoneId.systemDefault()))
+                .days(Collections.singletonList((byte) now.getDayOfWeek().getValue()))
+                .times(0)
+                .build();
+
+        when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule));
+        when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule);
+
+        GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
+
+        alarmSilenceReduce.silenceAlarm(alert);
+
+        verify(alertNoticeDispatch, never()).dispatchAlarm(alert);
+        verify(alertSilenceDao).save(silenceRule);
+    }
+
+    @Test
+    void whenCyclicSilenceRuleTimeMatchesButDateDifferent_shouldNotForwardAlert() {
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime yesterday = now.minusDays(1);
+        LocalDateTime start = yesterday.withHour(now.getHour())
+                .withMinute(Math.max(0, now.getMinute() - 10))
+                .withSecond(0)
+                .withNano(0);
+        LocalDateTime end = yesterday.withHour(now.getHour())
+                .withMinute(Math.min(59, now.getMinute() + 10))
+                .withSecond(59)
+                .withNano(0);
+
+        AlertSilence silenceRule = AlertSilence.builder()
+                .enable(true)
+                .matchAll(false)
+                .type((byte) 1)
+                .labels(createLabels("service", "web"))
+                .periodStart(start.atZone(ZoneId.systemDefault()))
+                .periodEnd(end.atZone(ZoneId.systemDefault()))
+                .days(Collections.singletonList((byte) now.getDayOfWeek().getValue()))
+                .times(0)
+                .build();
+
+        when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule));
+        when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule);
+
+        GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
+        alarmSilenceReduce.silenceAlarm(alert);
+
+        verify(alertNoticeDispatch, never()).dispatchAlarm(alert);
+        verify(alertSilenceDao).save(silenceRule);
+    }
+
+    @Test
+    void whenMatchingCrossMidnightCyclicSilenceRule_shouldNotForwardAlert() {
+        LocalDateTime now = LocalDateTime.now();
+        AlertSilence silenceRule = AlertSilence.builder()
+                .enable(true)
+                .matchAll(false)
+                .type((byte) 1)
+                .labels(createLabels("service", "web"))
+                .periodStart(now.minusHours(1).atZone(ZoneId.systemDefault()))
+                .periodEnd(now.minusHours(2).atZone(ZoneId.systemDefault()))
                 .days(Collections.singletonList((byte) now.getDayOfWeek().getValue()))
                 .times(0)
                 .build();
@@ -112,13 +171,36 @@ class AlarmSilenceReduceTest {
         when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule);
 
         GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
-        
         alarmSilenceReduce.silenceAlarm(alert);
-        
+
         verify(alertNoticeDispatch, never()).dispatchAlarm(alert);
         verify(alertSilenceDao).save(silenceRule);
     }
 
+    @Test
+    void whenCrossMidnightCyclicSilenceRuleDoesNotIncludeCurrentDay_shouldForwardAlert() {
+        LocalDateTime now = LocalDateTime.now();
+        byte previousDay = (byte) now.minusDays(1).getDayOfWeek().getValue();
+        AlertSilence silenceRule = AlertSilence.builder()
+                .enable(true)
+                .matchAll(false)
+                .type((byte) 1)
+                .labels(createLabels("service", "web"))
+                .periodStart(now.minusHours(1).atZone(ZoneId.systemDefault()))
+                .periodEnd(now.minusHours(2).atZone(ZoneId.systemDefault()))
+                .days(Collections.singletonList(previousDay))
+                .times(0)
+                .build();
+
+        when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule));
+
+        GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
+        alarmSilenceReduce.silenceAlarm(alert);
+
+        verify(alertNoticeDispatch).dispatchAlarm(alert);
+        verify(alertSilenceDao, never()).save(any());
+    }
+
     @Test
     void whenSilenceRuleExpired_shouldForwardAlert() {
         AlertSilence silenceRule = AlertSilence.builder()
@@ -132,11 +214,11 @@ class AlarmSilenceReduceTest {
                 .build();
 
         when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule));
-        
+
         GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
-        
+
         alarmSilenceReduce.silenceAlarm(alert);
-        
+
         verify(alertNoticeDispatch).dispatchAlarm(alert);
         verify(alertSilenceDao, never()).save(any());
     }
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java
index de29d2ccd5..882e8b6a5d 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java
@@ -25,6 +25,7 @@ import org.apache.hertzbeat.base.dao.LabelDao;
 import org.apache.hertzbeat.base.service.LabelService;
 import org.apache.hertzbeat.common.cache.CacheFactory;
 import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
+import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -48,6 +49,7 @@ import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_TH
 import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME;
 import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
 import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME;
+import static org.apache.hertzbeat.common.constants.CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -190,6 +192,32 @@ class AlertDefineServiceTest {
         assertEquals(0, result.size());
     }
 
+    @Test
+    void getDefinePreviewSupportsPeriodicTraceSql() {
+        String expr = "SELECT service_name, 0.2 AS __value__ FROM hertzbeat_apm_red_1m";
+        Map<String, Object> row = Map.of("service_name", "checkout", "__value__", 0.2D);
+        when(dataSourceService.query(eq("sql"), eq(expr), eq(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC)))
+                .thenReturn(List.of(row));
+
+        List<Map<String, Object>> result =
+                alertDefineService.getDefinePreview("sql", TRACE_ALERT_THRESHOLD_TYPE_PERIODIC, expr);
+
+        assertEquals(1, result.size());
+        assertEquals(0.2D, result.get(0).get("__value__"));
+    }
+
+    @Test
+    void getDefinePreviewRejectsPeriodicTraceSqlWithoutValueColumn() {
+        String expr = "SELECT service_name FROM hertzbeat_apm_red_1m";
+        when(dataSourceService.query(eq("sql"), eq(expr), eq(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC)))
+                .thenReturn(List.of(Map.of("service_name", "checkout")));
+
+        AlertExpressionException exception = assertThrows(AlertExpressionException.class,
+                () -> alertDefineService.getDefinePreview("sql", TRACE_ALERT_THRESHOLD_TYPE_PERIODIC, expr));
+
+        assertEquals("Trace alert preview SQL must return __value__ column", exception.getMessage());
+    }
+
     @Test
     void getMetricsRealTimeAlertDefines() {
         // Test cache hit scenario
@@ -336,6 +364,17 @@ class AlertDefineServiceTest {
         assertEquals(0, result.size());
         verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
 
+        // Test valid trace periodic alert type
+        reset(alertDefineDao);
+        when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC))
+                .thenReturn(Lists.newArrayList());
+
+        result = alertDefineService.getAlertDefinesByType(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC);
+
+        assertNotNull(result);
+        assertEquals(0, result.size());
+        verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(TRACE_ALERT_THRESHOLD_TYPE_PERIODIC);
+
         // Test empty string type
         assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType(""));
 
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java
index e3f3cfd479..a9f463148c 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java
@@ -18,8 +18,9 @@
 package org.apache.hertzbeat.alert.service;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -32,6 +33,7 @@ import org.apache.hertzbeat.alert.dto.AlertSummary;
 import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
 import org.apache.hertzbeat.alert.service.impl.AlertServiceImpl;
 import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -47,7 +49,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 class AlertServiceTest {
     @Mock
     private GroupAlertDao groupAlertDao;
-    
+
     @Mock
     private SingleAlertDao singleAlertDao;
 
@@ -69,14 +71,85 @@ class AlertServiceTest {
         assertDoesNotThrow(() -> alertService.deleteGroupAlerts(ids));
         verify(groupAlertDao, times(1)).deleteGroupAlertsByIdIn(ids);
     }
-    
+
 
     @Test
     void editGroupAlertStatus() {
         String status = "firing";
         List<Long> ids = List.of(1L, 2L, 3L);
+        GroupAlert groupAlert = GroupAlert.builder()
+                .id(1L)
+                .status(CommonConstants.ALERT_STATUS_RESOLVED)
+                .alertFingerprints(List.of("fingerprint-1"))
+                .build();
+        SingleAlert singleAlert = SingleAlert.builder()
+                .id(1L)
+                .fingerprint("fingerprint-1")
+                .status(CommonConstants.ALERT_STATUS_RESOLVED)
+                .endAt(1L)
+                .build();
+        when(groupAlertDao.findAllById(ids)).thenReturn(List.of(groupAlert));
+        when(singleAlertDao.findSingleAlertsByFingerprintIn(List.of("fingerprint-1"))).thenReturn(List.of(singleAlert));
+
         assertDoesNotThrow(() -> alertService.editGroupAlertStatus(status, ids));
-        verify(groupAlertDao, times(1)).updateGroupAlertsStatus(status, ids);
+        assertEquals(CommonConstants.ALERT_STATUS_FIRING, groupAlert.getStatus());
+        assertEquals(CommonConstants.ALERT_STATUS_FIRING, singleAlert.getStatus());
+        assertNull(singleAlert.getEndAt());
+        verify(groupAlertDao, times(1)).saveAll(List.of(groupAlert));
+        verify(singleAlertDao, times(1)).saveAll(List.of(singleAlert));
+    }
+
+    @Test
+    void editGroupAlertStatusToResolved() {
+        List<Long> ids = List.of(1L);
+        GroupAlert groupAlert = GroupAlert.builder()
+                .id(1L)
+                .status(CommonConstants.ALERT_STATUS_FIRING)
+                .alertFingerprints(List.of("fingerprint-2"))
+                .build();
+        SingleAlert singleAlert = SingleAlert.builder()
+                .id(2L)
+                .fingerprint("fingerprint-2")
+                .status(CommonConstants.ALERT_STATUS_FIRING)
+                .activeAt(123L)
+                .build();
+        when(groupAlertDao.findAllById(ids)).thenReturn(List.of(groupAlert));
+        when(singleAlertDao.findSingleAlertsByFingerprintIn(List.of("fingerprint-2"))).thenReturn(List.of(singleAlert));
+
+        assertDoesNotThrow(() -> alertService.editGroupAlertStatus(CommonConstants.ALERT_STATUS_RESOLVED, ids));
+
+        assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, groupAlert.getStatus());
+        assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, singleAlert.getStatus());
+        assertNull(singleAlert.getActiveAt());
+        verify(groupAlertDao, times(1)).saveAll(List.of(groupAlert));
+        verify(singleAlertDao, times(1)).saveAll(List.of(singleAlert));
+    }
+
+    @Test
+    void editGroupAlertStatusToAcknowledged() {
+        List<Long> ids = List.of(1L);
+        GroupAlert groupAlert = GroupAlert.builder()
+                .id(1L)
+                .status(CommonConstants.ALERT_STATUS_FIRING)
+                .alertFingerprints(List.of("fingerprint-3"))
+                .build();
+        SingleAlert singleAlert = SingleAlert.builder()
+                .id(3L)
+                .fingerprint("fingerprint-3")
+                .status(CommonConstants.ALERT_STATUS_FIRING)
+                .activeAt(456L)
+                .build();
+        when(groupAlertDao.findAllById(ids)).thenReturn(List.of(groupAlert));
+        when(singleAlertDao.findSingleAlertsByFingerprintIn(List.of("fingerprint-3"))).thenReturn(List.of(singleAlert));
+
+        assertDoesNotThrow(() -> alertService.editGroupAlertStatus("acknowledged", ids));
+
+        assertEquals("acknowledged", groupAlert.getStatus());
+        assertEquals("acknowledged", singleAlert.getStatus());
+        assertEquals(456L, singleAlert.getActiveAt());
+        assertNull(singleAlert.getEndAt());
+        verify(groupAlertDao, times(1)).saveAll(List.of(groupAlert));
+        verify(singleAlertDao, times(1)).saveAll(List.of(singleAlert));
     }
 
     @Test
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java
index e9b098b1b0..c959d6b99b 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java
@@ -35,6 +35,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
+import static org.apache.hertzbeat.common.constants.CommonConstants.TRACE_ALERT_THRESHOLD_TYPE_PERIODIC;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
@@ -753,7 +755,7 @@ class DataSourceServiceTest {
         dataSourceService.setExecutors(List.of(mockExecutor));
 
         String complexSql = "SELECT count(*) AS errorCount FROM hertzbeat_logs "
-                + "WHERE time_unix_nano >= NOW() AND severity_text = 'ERROR' "
+                + "WHERE `timestamp` >= NOW() AND severity_text = 'ERROR' "
                 + "GROUP BY severity_text HAVING count(*) > 2 ORDER BY errorCount LIMIT 10";
 
         List<Map<String, Object>> result = dataSourceService.query("sql", complexSql);
@@ -787,4 +789,67 @@ class DataSourceServiceTest {
                 () -> dataSourceService.query("sql", "SELEC * FORM hertzbeat_logs"));
         verify(mockExecutor, never()).execute(anyString());
     }
+
+    @Test
+    void queryTraceScopeAllowsApmRollupAndRawTraceTables() {
+        List<Map<String, Object>> sqlData = List.of(new HashMap<>(Map.of("__value__", 5)));
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        when(mockExecutor.execute(anyString())).thenReturn(sqlData);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        List<Map<String, Object>> rollupResult = dataSourceService.query("sql",
+                "SELECT service_name, SUM(calls_total) AS __value__ FROM hertzbeat_apm_red_1m "
+                        + "WHERE time_window >= NOW() - INTERVAL '5 minutes' GROUP BY service_name",
+                TRACE_ALERT_THRESHOLD_TYPE_PERIODIC);
+        List<Map<String, Object>> rawTraceResult = dataSourceService.query("sql",
+                "SELECT service_name, COUNT(*) AS __value__ FROM hzb_traces "
+                        + "WHERE `timestamp` >= NOW() - INTERVAL '5 minutes' GROUP BY service_name",
+                TRACE_ALERT_THRESHOLD_TYPE_PERIODIC);
+
+        assertEquals(1, rollupResult.size());
+        assertEquals(1, rawTraceResult.size());
+        verify(mockExecutor, Mockito.times(2)).execute(anyString());
+    }
+
+    @Test
+    void queryLogScopeRejectsTraceTables() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "SELECT * FROM hertzbeat_apm_red_1m",
+                        LOG_ALERT_THRESHOLD_TYPE_PERIODIC));
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "SELECT * FROM hzb_traces",
+                        LOG_ALERT_THRESHOLD_TYPE_PERIODIC));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void queryDefaultScopeStillRejectsTraceTables() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "SELECT * FROM hertzbeat_apm_red_1m"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void queryTraceScopeRejectsUnsafeSql() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "DROP TABLE hertzbeat_apm_red_1m",
+                        TRACE_ALERT_THRESHOLD_TYPE_PERIODIC));
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "SELECT * FROM users",
+                        TRACE_ALERT_THRESHOLD_TYPE_PERIODIC));
+        verify(mockExecutor, never()).execute(anyString());
+    }
 }
diff --git a/hertzbeat-common-core/pom.xml b/hertzbeat-common-core/pom.xml
index 8cccffe102..949cf19ed5 100644
--- a/hertzbeat-common-core/pom.xml
+++ b/hertzbeat-common-core/pom.xml
@@ -28,6 +28,7 @@
 
     <artifactId>hertzbeat-common-core</artifactId>
     <name>${project.artifactId}</name>
+    <description>Framework-agnostic shared runtime models, utilities, and protocol support.</description>
 
     <dependencies>
         <!-- Tool dependencies  -->
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/AdmissionMode.java
similarity index 67%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/AdmissionMode.java
index 24d411cff2..5f1a0e4fec 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/AdmissionMode.java
@@ -15,32 +15,25 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.config;
-
-import lombok.Getter;
-import lombok.Setter;
+package org.apache.hertzbeat.common.concurrent;
 
 /**
- * Kafka properties
+ * Task admission mode for managed executors.
  */
-
-@Getter
-@Setter
-public class BaseKafkaProperties {
+public enum AdmissionMode {
 
     /**
-     * kafka's connection server url
+     * Start each task on its own virtual thread without an executor-level concurrency cap.
      */
-    private String servers = "127.0.0.1:9092";
+    UNBOUNDED_VT,
 
     /**
-     * The name of the topic that receives the data
+     * Reject immediately when the configured concurrency cap has been reached.
      */
-    private String topic;
+    LIMIT_AND_REJECT,
 
     /**
-     * Consumer Group ID
+     * Block the submitter until a permit is available.
      */
-    private String groupId;
-
+    LIMIT_AND_BLOCK
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/BackgroundTaskExecutor.java
similarity index 56%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/BackgroundTaskExecutor.java
index 342004062f..030800faff 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/BackgroundTaskExecutor.java
@@ -15,40 +15,34 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.entity.push;
+package org.apache.hertzbeat.common.concurrent;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import java.util.concurrent.RejectedExecutionException;
 
 /**
- * push metrics dto
+ * Generic background task executor abstraction for runtime components.
  */
-@Data
-@Builder
-@AllArgsConstructor
-public class PushMetricsDto {
+public interface BackgroundTaskExecutor {
 
-    List<Metrics> metricsList;
+    /**
+     * Execute a short-lived task.
+     *
+     * @param runnable task
+     * @throws RejectedExecutionException when execution is rejected
+     */
+    void execute(Runnable runnable) throws RejectedExecutionException;
 
-    public PushMetricsDto() {
-        metricsList = new ArrayList<>();
-    }
+    /**
+     * Execute a long-lived background task.
+     *
+     * @param runnable task
+     */
+    void executeLongRunning(Runnable runnable);
 
     /**
-     * metrics
+     * Release executor resources.
+     *
+     * @throws Exception close exception
      */
-    @Data
-    @Builder
-    @AllArgsConstructor
-    @NoArgsConstructor
-    public static class Metrics {
-        private long monitorId;
-        private Long time;
-        private List<Map<String, String>> metrics;
-    }
+    void destroy() throws Exception;
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutor.java
similarity index 68%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutor.java
index d343cb7ef8..c86d7a97d6 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutor.java
@@ -15,18 +15,22 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.support.valid;
+package org.apache.hertzbeat.common.concurrent;
+
+import java.util.concurrent.Executor;
 
 /**
- * SQL security validation exception
+ * Executor with a stable logical name and close semantics.
  */
-public class SqlSecurityException extends RuntimeException {
+public interface ManagedExecutor extends Executor, AutoCloseable {
 
-    public SqlSecurityException(String message) {
-        super(message);
-    }
+    /**
+     * Logical executor name for logging and metrics tags.
+     *
+     * @return executor name
+     */
+    String name();
 
-    public SqlSecurityException(String message, Throwable cause) {
-        super(message, cause);
-    }
+    @Override
+    void close();
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutors.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutors.java
new file mode 100644
index 0000000000..24226deada
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutors.java
@@ -0,0 +1,428 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.concurrent;
+
+import java.util.ArrayDeque;
+import java.util.Objects;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Factory methods for managed executors.
+ */
+public final class ManagedExecutors {
+
+    private ManagedExecutors() {
+    }
+
+    /**
+     * Wrap an existing executor service.
+     *
+     * @param name executor name
+     * @param executorService delegate executor service
+     * @return managed executor wrapper
+     */
+    public static ManagedExecutor wrap(String name, ExecutorService executorService) {
+        return new DefaultManagedExecutor(name, executorService, null, AdmissionMode.UNBOUNDED_VT);
+    }
+
+    /**
+     * Create a per-task virtual-thread executor with optional admission control.
+     *
+     * @param name executor name
+     * @param threadNamePrefix thread name prefix
+     * @param mode admission mode
+     * @param maxConcurrentTasks max concurrent tasks for limited modes
+     * @param handler uncaught exception handler
+     * @return managed executor
+     */
+    public static ManagedExecutor newVirtualExecutor(String name, String threadNamePrefix, AdmissionMode mode,
+                                                     int maxConcurrentTasks, Thread.UncaughtExceptionHandler handler) {
+        ThreadFactory threadFactory = Thread.ofVirtual()
+                .name(threadNamePrefix, 0)
+                .uncaughtExceptionHandler(handler)
+                .factory();
+        ExecutorService executorService = Executors.newThreadPerTaskExecutor(threadFactory);
+        return new DefaultManagedExecutor(name, executorService, semaphore(mode, maxConcurrentTasks), mode);
+    }
+
+    /**
+     * Create a platform-thread-per-task executor for a small number of long-running tasks.
+     *
+     * @param name executor name
+     * @param threadNamePrefix thread name prefix
+     * @param handler uncaught exception handler
+     * @return managed executor
+     */
+    public static ManagedExecutor newPlatformExecutor(String name, String threadNamePrefix,
+                                                      Thread.UncaughtExceptionHandler handler) {
+        ThreadFactory threadFactory = Thread.ofPlatform()
+                .daemon(true)
+                .name(threadNamePrefix, 0)
+                .uncaughtExceptionHandler(handler)
+                .factory();
+        ExecutorService executorService = Executors.newThreadPerTaskExecutor(threadFactory);
+        return new DefaultManagedExecutor(name, executorService, null, AdmissionMode.UNBOUNDED_VT);
+    }
+
+    /**
+     * Create a queued executor that preserves queue semantics while executing tasks on virtual threads.
+     *
+     * @param name executor name
+     * @param threadNamePrefix virtual-thread name prefix
+     * @param maxConcurrentTasks max concurrent tasks
+     * @param queueCapacity queue capacity, {@code <= 0} means unbounded
+     * @param handler uncaught exception handler
+     * @return managed executor
+     */
+    public static ManagedExecutor newQueuedVirtualExecutor(String name, String threadNamePrefix, int maxConcurrentTasks,
+                                                           int queueCapacity, Thread.UncaughtExceptionHandler handler) {
+        if (maxConcurrentTasks <= 0) {
+            throw new IllegalArgumentException("maxConcurrentTasks must be greater than zero for queued executors");
+        }
+        return new QueuedVirtualManagedExecutor(name, threadNamePrefix, maxConcurrentTasks, queueCapacity, handler);
+    }
+
+    /**
+     * Create a virtual-thread executor that preserves {@link java.util.concurrent.ThreadPoolExecutor}
+     * core/max/queue semantics with discard-oldest overflow handling.
+     *
+     * @param name executor name
+     * @param threadNamePrefix virtual-thread name prefix
+     * @param coreConcurrentTasks core concurrent tasks
+     * @param maxConcurrentTasks max concurrent tasks
+     * @param queueCapacity queue capacity
+     * @param handler uncaught exception handler
+     * @return managed executor
+     */
+    public static ManagedExecutor newDiscardOldestVirtualExecutor(String name, String threadNamePrefix,
+                                                                  int coreConcurrentTasks, int maxConcurrentTasks,
+                                                                  int queueCapacity,
+                                                                  Thread.UncaughtExceptionHandler handler) {
+        if (coreConcurrentTasks <= 0) {
+            throw new IllegalArgumentException("coreConcurrentTasks must be greater than zero");
+        }
+        if (maxConcurrentTasks < coreConcurrentTasks) {
+            throw new IllegalArgumentException("maxConcurrentTasks must be greater than or equal to coreConcurrentTasks");
+        }
+        if (queueCapacity <= 0) {
+            throw new IllegalArgumentException("queueCapacity must be greater than zero");
+        }
+        return new DiscardOldestVirtualManagedExecutor(name, threadNamePrefix, coreConcurrentTasks,
+                maxConcurrentTasks, queueCapacity, handler);
+    }
+
+    private static Semaphore semaphore(AdmissionMode mode, int maxConcurrentTasks) {
+        if (mode == AdmissionMode.UNBOUNDED_VT) {
+            return null;
+        }
+        if (maxConcurrentTasks <= 0) {
+            throw new IllegalArgumentException("maxConcurrentTasks must be greater than zero for limited executors");
+        }
+        return new Semaphore(maxConcurrentTasks);
+    }
+
+    private static final class DefaultManagedExecutor implements ManagedExecutor {
+
+        private final String name;
+        private final ExecutorService delegate;
+        private final Semaphore permits;
+        private final AdmissionMode admissionMode;
+
+        private DefaultManagedExecutor(String name, ExecutorService delegate, Semaphore permits,
+                                       AdmissionMode admissionMode) {
+            this.name = Objects.requireNonNull(name, "name");
+            this.delegate = Objects.requireNonNull(delegate, "delegate");
+            this.permits = permits;
+            this.admissionMode = Objects.requireNonNull(admissionMode, "admissionMode");
+        }
+
+        @Override
+        public String name() {
+            return name;
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            Objects.requireNonNull(command, "command");
+            acquirePermit();
+            boolean submitted = false;
+            try {
+                delegate.execute(() -> {
+                    try {
+                        command.run();
+                    } finally {
+                        releasePermit();
+                    }
+                });
+                submitted = true;
+            } finally {
+                if (!submitted) {
+                    releasePermit();
+                }
+            }
+        }
+
+        @Override
+        public void close() {
+            delegate.shutdownNow();
+        }
+
+        private void acquirePermit() {
+            if (permits == null) {
+                return;
+            }
+            switch (admissionMode) {
+                case LIMIT_AND_REJECT:
+                    if (!permits.tryAcquire()) {
+                        throw new RejectedExecutionException(name + " rejected task because concurrency limit was reached");
+                    }
+                    break;
+                case LIMIT_AND_BLOCK:
+                    try {
+                        permits.acquire();
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        throw new RejectedExecutionException(name + " interrupted while waiting for an execution permit", e);
+                    }
+                    break;
+                case UNBOUNDED_VT:
+                    break;
+                default:
+                    throw new IllegalStateException("Unsupported admission mode: " + admissionMode);
+            }
+        }
+
+        private void releasePermit() {
+            if (permits != null) {
+                permits.release();
+            }
+        }
+    }
+
+    private static final class QueuedVirtualManagedExecutor implements ManagedExecutor {
+
+        private final String name;
+        private final ExecutorService delegate;
+        private final ExecutorService dispatcher;
+        private final BlockingDeque<Runnable> queue;
+        private final Semaphore permits;
+        private final Semaphore permitSignals;
+        private final AtomicBoolean closed;
+
+        private QueuedVirtualManagedExecutor(String name, String threadNamePrefix, int maxConcurrentTasks,
+                                             int queueCapacity, Thread.UncaughtExceptionHandler handler) {
+            this.name = Objects.requireNonNull(name, "name");
+            ThreadFactory virtualFactory = Thread.ofVirtual()
+                    .name(threadNamePrefix, 0)
+                    .uncaughtExceptionHandler(handler)
+                    .factory();
+            this.delegate = Executors.newThreadPerTaskExecutor(virtualFactory);
+            this.queue = queueCapacity > 0 ? new LinkedBlockingDeque<>(queueCapacity) : new LinkedBlockingDeque<>();
+            this.permits = new Semaphore(maxConcurrentTasks);
+            this.permitSignals = new Semaphore(0);
+            this.closed = new AtomicBoolean(false);
+            ThreadFactory dispatcherFactory = Thread.ofPlatform()
+                    .daemon(true)
+                    .name(threadNamePrefix + "dispatcher-", 0)
+                    .uncaughtExceptionHandler(handler)
+                    .factory();
+            this.dispatcher = Executors.newSingleThreadExecutor(dispatcherFactory);
+            this.dispatcher.execute(this::dispatchLoop);
+        }
+
+        @Override
+        public String name() {
+            return name;
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            Objects.requireNonNull(command, "command");
+            if (closed.get()) {
+                throw new RejectedExecutionException(name + " rejected task because executor is closed");
+            }
+            if (!queue.offerLast(command)) {
+                throw new RejectedExecutionException(name + " rejected task because queue capacity was reached");
+            }
+        }
+
+        @Override
+        public void close() {
+            if (!closed.compareAndSet(false, true)) {
+                return;
+            }
+            dispatcher.shutdownNow();
+            delegate.shutdownNow();
+            queue.clear();
+        }
+
+        private void dispatchLoop() {
+            try {
+                while (!Thread.currentThread().isInterrupted()) {
+                    Runnable command = queue.takeFirst();
+                    if (!permits.tryAcquire()) {
+                        queue.putFirst(command);
+                        permitSignals.acquire();
+                        continue;
+                    }
+                    submit(command);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+
+        private void submit(Runnable command) {
+            boolean submitted = false;
+            try {
+                delegate.execute(() -> {
+                    try {
+                        command.run();
+                    } finally {
+                        permits.release();
+                        permitSignals.release();
+                    }
+                });
+                submitted = true;
+            } finally {
+                if (!submitted) {
+                    permits.release();
+                    permitSignals.release();
+                }
+            }
+        }
+    }
+
+    private static final class DiscardOldestVirtualManagedExecutor implements ManagedExecutor {
+
+        private final String name;
+        private final ExecutorService delegate;
+        private final int coreConcurrentTasks;
+        private final int maxConcurrentTasks;
+        private final ArrayDeque<Runnable> queue;
+        private final int queueCapacity;
+        private final Object lock;
+        private boolean closed;
+        private int runningTasks;
+
+        private DiscardOldestVirtualManagedExecutor(String name, String threadNamePrefix, int coreConcurrentTasks,
+                                                    int maxConcurrentTasks, int queueCapacity,
+                                                    Thread.UncaughtExceptionHandler handler) {
+            this.name = Objects.requireNonNull(name, "name");
+            ThreadFactory virtualFactory = Thread.ofVirtual()
+                    .name(threadNamePrefix, 0)
+                    .uncaughtExceptionHandler(handler)
+                    .factory();
+            this.delegate = Executors.newThreadPerTaskExecutor(virtualFactory);
+            this.coreConcurrentTasks = coreConcurrentTasks;
+            this.maxConcurrentTasks = maxConcurrentTasks;
+            this.queueCapacity = queueCapacity;
+            this.queue = new ArrayDeque<>(queueCapacity);
+            this.lock = new Object();
+            this.closed = false;
+            this.runningTasks = 0;
+        }
+
+        @Override
+        public String name() {
+            return name;
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            Objects.requireNonNull(command, "command");
+            Runnable taskToStart = null;
+            synchronized (lock) {
+                if (closed) {
+                    throw new RejectedExecutionException(name + " rejected task because executor is closed");
+                }
+                if (runningTasks < coreConcurrentTasks) {
+                    runningTasks++;
+                    taskToStart = command;
+                } else if (queue.size() < queueCapacity) {
+                    queue.offerLast(command);
+                    return;
+                } else if (runningTasks < maxConcurrentTasks) {
+                    runningTasks++;
+                    taskToStart = command;
+                } else {
+                    queue.pollFirst();
+                    queue.offerLast(command);
+                    return;
+                }
+            }
+            submit(taskToStart);
+        }
+
+        @Override
+        public void close() {
+            synchronized (lock) {
+                if (closed) {
+                    return;
+                }
+                closed = true;
+                queue.clear();
+            }
+            delegate.shutdownNow();
+        }
+
+        private void submit(Runnable command) {
+            boolean submitted = false;
+            try {
+                delegate.execute(() -> {
+                    try {
+                        command.run();
+                    } finally {
+                        onTaskComplete();
+                    }
+                });
+                submitted = true;
+            } finally {
+                if (!submitted) {
+                    synchronized (lock) {
+                        runningTasks--;
+                    }
+                    throw new RejectedExecutionException(name + " rejected task because delegate submission failed");
+                }
+            }
+        }
+
+        private void onTaskComplete() {
+            Runnable nextTask = null;
+            synchronized (lock) {
+                if (closed) {
+                    runningTasks--;
+                    return;
+                }
+                nextTask = queue.pollFirst();
+                if (nextTask == null) {
+                    runningTasks--;
+                    return;
+                }
+            }
+            submit(nextTask);
+        }
+    }
+}
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java
similarity index 100%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadProperties.java
new file mode 100644
index 0000000000..956b8a5414
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadProperties.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.config;
+
+import org.apache.hertzbeat.common.concurrent.AdmissionMode;
+
+/**
+ * Framework-agnostic virtual-thread runtime configuration.
+ */
+public record VirtualThreadProperties(
+        boolean enabled,
+        PoolProperties collector,
+        PoolProperties common,
+        PoolProperties manager,
+        AlerterProperties alerter,
+        PoolProperties warehouse,
+        AsyncProperties async) {
+
+    private static final int DEFAULT_COLLECTOR_MAX_CONCURRENT_JOBS = 512;
+    private static final int DEFAULT_MANAGER_MAX_CONCURRENT_JOBS = 10;
+    private static final int DEFAULT_NOTIFY_MAX_CONCURRENT_JOBS = 64;
+    private static final int DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS = 10;
+    private static final int DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL = 4;
+
+    public VirtualThreadProperties {
+        collector = normalizePool(collector, PoolProperties.collectorDefaults());
+        common = common == null ? PoolProperties.commonDefaults() : common;
+        manager = normalizePool(manager, PoolProperties.managerDefaults());
+        alerter = alerter == null ? AlerterProperties.defaults() : alerter;
+        warehouse = warehouse == null ? PoolProperties.warehouseDefaults() : warehouse;
+        async = async == null ? AsyncProperties.defaults() : async;
+    }
+
+    public VirtualThreadProperties() {
+        this(true, PoolProperties.collectorDefaults(), PoolProperties.commonDefaults(),
+                PoolProperties.managerDefaults(), AlerterProperties.defaults(),
+                PoolProperties.warehouseDefaults(), AsyncProperties.defaults());
+    }
+
+    /**
+     * Create a detached properties instance with runtime defaults.
+     *
+     * @return defaults instance
+     */
+    public static VirtualThreadProperties defaults() {
+        return new VirtualThreadProperties();
+    }
+
+    /**
+     * Pool-level configuration.
+     */
+    public record PoolProperties(
+            AdmissionMode mode,
+            int maxConcurrentJobs) {
+
+        public PoolProperties {
+            mode = mode == null ? AdmissionMode.UNBOUNDED_VT : mode;
+        }
+
+        public PoolProperties() {
+            this(AdmissionMode.UNBOUNDED_VT, 0);
+        }
+
+        public static PoolProperties collectorDefaults() {
+            return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, defaultCollectorConcurrency());
+        }
+
+        public static PoolProperties warehouseDefaults() {
+            return new PoolProperties();
+        }
+
+        public static PoolProperties commonDefaults() {
+            return new PoolProperties();
+        }
+
+        public static PoolProperties managerDefaults() {
+            return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, DEFAULT_MANAGER_MAX_CONCURRENT_JOBS);
+        }
+
+        public static PoolProperties alerterNotifyDefaults() {
+            return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, DEFAULT_NOTIFY_MAX_CONCURRENT_JOBS);
+        }
+
+        private static int defaultCollectorConcurrency() {
+            return DEFAULT_COLLECTOR_MAX_CONCURRENT_JOBS;
+        }
+    }
+
+    /**
+     * Alerter-specific executor configuration.
+     */
+    public record AlerterProperties(
+            PoolProperties notifyPool,
+            int periodicMaxConcurrentJobs,
+            QueueProperties logWorker,
+            QueueProperties reduce,
+            QueueProperties windowEvaluator,
+            int notifyMaxConcurrentPerChannel) {
+
+        public AlerterProperties {
+            notifyPool = normalizePool(notifyPool, PoolProperties.alerterNotifyDefaults());
+            periodicMaxConcurrentJobs = periodicMaxConcurrentJobs <= 0
+                    ? DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS : periodicMaxConcurrentJobs;
+            logWorker = normalizeQueue(logWorker, QueueProperties.logWorkerDefaults());
+            reduce = normalizeQueue(reduce, QueueProperties.reduceDefaults());
+            windowEvaluator = normalizeQueue(windowEvaluator, QueueProperties.windowEvaluatorDefaults());
+            notifyMaxConcurrentPerChannel = notifyMaxConcurrentPerChannel <= 0
+                    ? DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL : notifyMaxConcurrentPerChannel;
+        }
+
+        public AlerterProperties() {
+            this(PoolProperties.alerterNotifyDefaults(), DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS,
+                    QueueProperties.logWorkerDefaults(), QueueProperties.reduceDefaults(),
+                    QueueProperties.windowEvaluatorDefaults(), DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL);
+        }
+
+        public static AlerterProperties defaults() {
+            return new AlerterProperties();
+        }
+    }
+
+    /**
+     * Queue-preserving executor configuration.
+     */
+    public record QueueProperties(
+            int maxConcurrentJobs,
+            int queueCapacity) {
+
+        public QueueProperties() {
+            this(0, 0);
+        }
+
+        public static QueueProperties reduceDefaults() {
+            return new QueueProperties(2, 0);
+        }
+
+        public static QueueProperties logWorkerDefaults() {
+            return new QueueProperties(10, 1000);
+        }
+
+        public static QueueProperties windowEvaluatorDefaults() {
+            return new QueueProperties(2, 0);
+        }
+    }
+
+    /**
+     * Async executor configuration.
+     */
+    public record AsyncProperties(
+            boolean enabled,
+            int concurrencyLimit,
+            boolean rejectWhenLimitReached,
+            long taskTerminationTimeout) {
+
+        public AsyncProperties() {
+            this(true, 256, true, 5000L);
+        }
+
+        public static AsyncProperties defaults() {
+            return new AsyncProperties();
+        }
+    }
+
+    private static PoolProperties normalizePool(PoolProperties configured, PoolProperties defaults) {
+        if (configured == null) {
+            return defaults;
+        }
+        if (configured.mode() != AdmissionMode.UNBOUNDED_VT && configured.maxConcurrentJobs() <= 0) {
+            return new PoolProperties(configured.mode(), defaults.maxConcurrentJobs());
+        }
+        return configured;
+    }
+
+    private static QueueProperties normalizeQueue(QueueProperties configured, QueueProperties defaults) {
+        if (configured == null) {
+            return defaults;
+        }
+        int maxConcurrentJobs = configured.maxConcurrentJobs() <= 0
+                ? defaults.maxConcurrentJobs() : configured.maxConcurrentJobs();
+        int queueCapacity = configured.queueCapacity();
+        if (queueCapacity <= 0 && defaults.queueCapacity() > 0) {
+            queueCapacity = defaults.queueCapacity();
+        }
+        return new QueueProperties(maxConcurrentJobs, queueCapacity);
+    }
+}
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CommonConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CommonConstants.java
index c03d872479..80f64da440 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CommonConstants.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CommonConstants.java
@@ -56,7 +56,7 @@ public interface CommonConstants {
      * Response status code: Incorrect login account password
      */
     byte LOGIN_FAILED_CODE = 0x05;
-    
+
     /**
      * Monitoring status 0: Paused, 1: Up, 2: Down
      */
@@ -78,7 +78,7 @@ public interface CommonConstants {
     String SCRAPE_STATIC = "static";
 
     /**
-     * scrape type 
+     * scrape type
      */
     String SCRAPE_HTTP_SD = "http_sd";
 
@@ -101,7 +101,7 @@ public interface CommonConstants {
      * label key: instance name
      */
     String LABEL_INSTANCE_NAME = "instancename";
-    
+
     /**
      * Alarm severity label key
      */
@@ -111,7 +111,7 @@ public interface CommonConstants {
      * Alarm severity fatal level
      */
     String LABEL_ALERT_FATAL = "fatal";
-    
+
     /**
      * alarm severity emergency level
      */
@@ -142,6 +142,11 @@ public interface CommonConstants {
      */
     String ALERT_STATUS_RESOLVED = "resolved";
 
+    /**
+     * Alarm status: acknowledged
+     */
+    String ALERT_STATUS_ACKNOWLEDGED = "acknowledged";
+
     /**
      * Alarm status: pending
      */
@@ -167,6 +172,11 @@ public interface CommonConstants {
      */
     String LOG_ALERT_THRESHOLD_TYPE_PERIODIC = "periodic_log";
 
+    /**
+     * trace alert threshold type: periodic
+     */
+    String TRACE_ALERT_THRESHOLD_TYPE_PERIODIC = "periodic_trace";
+
     /**
      * Alert mode label key
      */
@@ -324,7 +334,7 @@ public interface CommonConstants {
     String PROMETHEUS_APP_PREFIX = "_prometheus_";
 
     /**
-     * prometheus 
+     * prometheus
      */
     String PROMETHEUS = "prometheus";
 
@@ -332,12 +342,12 @@ public interface CommonConstants {
      * status page component state normal
      */
     byte STATUS_PAGE_COMPONENT_STATE_NORMAL = 0;
-    
+
     /**
      * status page component state abnormal
      */
     byte STATUS_PAGE_COMPONENT_STATE_ABNORMAL = 1;
-    
+
     /**
      * status page component state unknown
      */
@@ -357,12 +367,12 @@ public interface CommonConstants {
      * status page org state all abnormal
      */
     byte STATUS_PAGE_ORG_STATE_ALL_ABNORMAL = 2;
-    
+
     /**
      * status page component calculate method auto
      */
     byte STATUE_PAGE_CALCULATE_METHOD_AUTO = 0;
-    
+
     /**
      * status page component calculate method manual
      */
@@ -372,17 +382,17 @@ public interface CommonConstants {
      * status page incident state investigating
      */
     byte STATUS_PAGE_INCIDENT_STATE_INVESTIGATING = 0;
-    
+
     /**
      * status page incident state identified
      */
     byte STATUS_PAGE_INCIDENT_STATE_IDENTIFIED = 1;
-    
+
     /**
      * status page incident state monitoring
      */
     byte STATUS_PAGE_INCIDENT_STATE_MONITORING = 2;
-    
+
     /**
      * status page incident state resolved
      */
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java
index 47e8bacfe3..162a0c76d6 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java
@@ -43,7 +43,7 @@ public interface ConfigConstants {
     interface FunctionModuleConstants {
 
         String MANAGER = "manager";
-        
+
         String ALERT = "alert";
 
         String ALERTER = "alerter";
@@ -69,6 +69,8 @@ public interface ConfigConstants {
         String GRAFANA = "grafana";
 
         String LOG = "log";
+
+        String OBSERVABILITY = "observability";
     }
 
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java
similarity index 89%
rename from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java
rename to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java
index 2b2c3a5f6a..c689b91394 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java
@@ -21,17 +21,13 @@ import jakarta.validation.constraints.NotBlank;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
 
 /**
- * SMS configuration
+ * Framework-agnostic SMS runtime configuration.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-@Component
-@ConfigurationProperties(prefix = "alerter.sms")
 public class SmsConfig {
 
     /**
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
similarity index 93%
rename from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
rename to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
index 4b4e1d5326..e7fb6e52c1 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
@@ -17,7 +17,6 @@
 
 package org.apache.hertzbeat.common.entity.job;
 
-
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import java.util.Collections;
 import java.util.Comparator;
@@ -35,13 +34,11 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
 import org.apache.hertzbeat.common.entity.message.CollectRep;
 import org.apache.hertzbeat.common.util.JsonUtil;
-import org.springframework.util.CollectionUtils;
 
 /**
- * Collect task details
+ * Collect task details.
  */
 @Data
 @AllArgsConstructor
@@ -51,15 +48,15 @@ import org.springframework.util.CollectionUtils;
 public class Job {
 
     /**
-     * Task Job id
+     * Task Job id.
      */
     private long id;
     /**
-     * Tenant id
+     * Tenant id.
      */
     private long tenantId = 0;
     /**
-     * Monitoring Task ID
+     * Monitoring Task ID.
      */
     private long monitorId;
     /**
@@ -68,11 +65,11 @@ public class Job {
      */
     private Map<String, String> metadata;
     /**
-     * bind labels 
+     * bind labels.
      */
     private Map<String, String> labels;
     /**
-     * bind annotations
+     * bind annotations.
      */
     private Map<String, String> annotations;
     /**
@@ -86,12 +83,12 @@ public class Job {
      */
     private String category;
     /**
-     * Type of monitoring eg: linux | mysql | jvm
+     * Type of monitoring eg: linux | mysql | jvm.
      */
     private String app;
     /**
      * The internationalized name of the monitoring type
-     * PING CONNECT
+     * PING CONNECT.
      */
     private Map<String, String> name;
     /**
@@ -101,55 +98,54 @@ public class Job {
      */
     private Map<String, String> help;
     /**
-     * The monitor help link
+     * The monitor help link.
      */
     private Map<String, String> helpLink;
     /**
-     * Task dispatch start timestamp
+     * Task dispatch start timestamp.
      */
     private long timestamp;
     /**
-     * Default task collection time interval (unit: second) eg: 30,60,600
+     * Default task collection time interval (unit: second) eg: 30,60,600.
      */
     private long defaultInterval = 600L;
     /**
-     * Refresh time list for one cycle of the job
+     * Refresh time list for one cycle of the job.
      */
     private ConcurrentLinkedDeque<Long> intervals;
     /**
-     * Whether it is a recurring periodic task true is yes, false is no
+     * Whether it is a recurring periodic task true is yes, false is no.
      */
     private boolean isCyclic = false;
     /**
-     * monitor input need params
+     * monitor input need params.
      */
-    private List<ParamDefine> params;
+    private List<RuntimeParamDefine> params;
     /**
-     * Metrics configuration eg: cpu memory
-     * eg: cpu memory
+     * Metrics configuration eg: cpu memory.
      */
     private List<Metrics> metrics;
     /**
-     * Monitoring configuration parameter properties and values eg: username password timeout host
+     * Monitoring configuration parameter properties and values eg: username password timeout host.
      */
     private List<Configmap> configmap;
     /**
-     * Whether it is a service discovery job, true is yes, false is no
+     * Whether it is a service discovery job, true is yes, false is no.
      */
     private boolean isSd = false;
 
     /**
-     * Whether to use the Prometheus proxy
+     * Whether to use the Prometheus proxy.
      */
     private boolean prometheusProxyMode = false;
 
     /**
-     * Scheduling type: interval or cron
+     * Scheduling type: interval or cron.
      */
     private String scheduleType = "interval";
 
     /**
-     * Cron expression for scheduling, used when scheduleType is "cron"
+     * Cron expression for scheduling, used when scheduleType is "cron".
      */
     private String cronExpression = null;
 
@@ -160,7 +156,7 @@ public class Job {
     private Map<String, Configmap> envConfigmaps;
 
     /**
-     * collector use - timestamp when the task was scheduled by the time wheel
+     * collector use - timestamp when the task was scheduled by the time wheel.
      */
     @JsonIgnore
     private transient long dispatchTime;
@@ -179,13 +175,13 @@ public class Job {
     private transient LinkedList<Set<Metrics>> priorMetrics;
 
     /**
-     * collector use - Temporarily store one-time task metrics response data
+     * collector use - Temporarily store one-time task metrics response data.
      */
     @JsonIgnore
     private transient List<CollectRep.MetricsData> responseDataTemp;
 
     /**
-     * collector use - construct to initialize metrics execution view
+     * collector use - construct to initialize metrics execution view.
      */
     public synchronized void constructPriorMetrics() {
         long now = System.currentTimeMillis();
@@ -232,7 +228,7 @@ public class Job {
     }
 
     /**
-     * collector use - to get the next set of priority metric group tasks
+     * collector use - to get the next set of priority metric group tasks.
      *
      * @param metrics Current Metrics
      * @param first   Is it the first time to get
@@ -314,7 +310,7 @@ public class Job {
     }
 
     /**
-     * The greatest common divisor
+     * The greatest common divisor.
      */
     public static long gcd(long a, long b) {
         while (b != 0) {
@@ -326,7 +322,7 @@ public class Job {
     }
 
     /**
-     * The least common multiple
+     * The least common multiple.
      */
     public static long lcm(List<Long> array) {
         if (array != null && !array.isEmpty()) {
@@ -340,9 +336,8 @@ public class Job {
     }
 
     /**
-     *
      * @param metricsIntervals A unique list composed of intervals for all metrics
-     * Generate a list of refresh intervals for metric collection
+     * Generate a list of refresh intervals for metric collection.
      */
     public synchronized void generateMetricsIntervals(List<Long> metricsIntervals) {
         // 1. To find the least common multiple (LCM) of all metric refresh intervals
@@ -368,7 +363,7 @@ public class Job {
     }
 
     public synchronized long getInterval() {
-        if (!CollectionUtils.isEmpty(this.intervals)) {
+        if (this.intervals != null && !this.intervals.isEmpty()) {
             Long interval = this.intervals.removeFirst();
             if (interval != null) {
                 this.intervals.addLast(interval);
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
similarity index 100%
rename from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
rename to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/job/Configmap.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/RuntimeParamDefine.java
similarity index 56%
rename from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/job/Configmap.java
rename to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/RuntimeParamDefine.java
index f93093157d..eae3e24e28 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/job/Configmap.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/RuntimeParamDefine.java
@@ -17,38 +17,60 @@
 
 package org.apache.hertzbeat.common.entity.job;
 
-import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
 /**
- * Monitoring configuration parameter properties and values
- * During the process, you need to replace the content with the identifier ^_^key^_^
- * in the protocol configuration parameter with the real value in the configuration parameter
+ * Framework-agnostic parameter definition used by runtime templates and jobs.
  */
 @Data
+@Builder
 @AllArgsConstructor
 @NoArgsConstructor
-@Builder
-public class Configmap implements Serializable {
+public class RuntimeParamDefine {
 
-    /**
-     * Parameter key, replace the content with the identifier ^^_key_^^ in the protocol
-     * configuration parameter with the real value in the configuration parameter
-     */
-    private String key;
+    private String app;
 
-    /**
-     * parameter value
-     */
-    private Object value;
+    private Map<String, String> name;
+
+    private String field;
+
+    private String type;
+
+    private boolean required = false;
+
+    private String defaultValue;
+
+    private String placeholder;
+
+    private String range;
+
+    private Short limit;
+
+    private List<Option> options;
+
+    private String keyAlias;
+
+    private String valueAlias;
+
+    private boolean hide = false;
+
+    private Map<String, List<Object>> depend;
 
     /**
-     * Parameter type
-     * 0: number 1: string 2: encrypted string 3: json string mapped by map
-     * number,string,secret
+     * Runtime option definition.
      */
-    private byte type = 1;
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static final class Option {
+
+        private String label;
+
+        private String value;
+    }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/SshTunnel.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/SshTunnel.java
index 8caa8dcd1c..7eb9bb2c79 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/SshTunnel.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/SshTunnel.java
@@ -81,7 +81,7 @@ public class SshTunnel implements CommonRequestProtocol, Protocol {
     @Override
     public boolean isInvalid() {
 
-        // todo: add
-        return true;
+        // todo add
+        return false;
     }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocol.java
index 61b36f0e7a..73cb917433 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocol.java
@@ -17,12 +17,17 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import java.util.List;
 import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * http protocol configuration
@@ -136,8 +141,33 @@ public class HttpProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
+        if (!validateIpDomain(host) || !validPort(port)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(ssl) && !"true".equalsIgnoreCase(ssl) && !"false".equalsIgnoreCase(ssl)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(method) && !isValidHttpMethod(method)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(enableUrlEncoding)
+                && !"true".equalsIgnoreCase(enableUrlEncoding)
+                && !"false".equalsIgnoreCase(enableUrlEncoding)) {
+            return true;
+        }
+        return false;
+    }
 
-        // todo: add
-        return true;
+    private boolean isValidHttpMethod(String method) {
+        return method.equalsIgnoreCase("get")
+            || method.equalsIgnoreCase("post")
+            || method.equalsIgnoreCase("put")
+            || method.equalsIgnoreCase("delete")
+            || method.equalsIgnoreCase("patch")
+            || method.equalsIgnoreCase("head")
+            || method.equalsIgnoreCase("options");
     }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocol.java
index 0454ffcbfc..47fe9ce052 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocol.java
@@ -17,11 +17,16 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.hertzbeat.common.entity.job.SshTunnel;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * Database configuration information implemented by the common jdbc specification
@@ -82,8 +87,19 @@ public class JdbcProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
-
-        // todo: add
-        return true;
+        if (StringUtils.isBlank(url)) {
+            if (!validateIpDomain(host) || !validPort(port)) {
+                return true;
+            }
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(reuseConnection)
+                && !"true".equalsIgnoreCase(reuseConnection)
+                && !"false".equalsIgnoreCase(reuseConnection)) {
+            return true;
+        }
+        return false;
     }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocol.java
index f1a3065577..eda0ae5d27 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocol.java
@@ -17,10 +17,15 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * Kafka protocol
@@ -58,8 +63,17 @@ public class KafkaProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
-
-        // todo: add
-        return true;
+        if (!validateIpDomain(host) || !validPort(port)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(monitorInternalTopic)
+                && !"true".equalsIgnoreCase(monitorInternalTopic)
+                && !"false".equalsIgnoreCase(monitorInternalTopic)) {
+            return true;
+        }
+        return false;
     }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocol.java
index 0f58343bfb..68aa1af8a6 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocol.java
@@ -17,10 +17,15 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * Mongodb Protocol
@@ -79,9 +84,13 @@ public class MongodbProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
-
-        // todo: add
-        return true;
+        if (!validateIpDomain(host) || !validPort(port)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        return false;
     }
 
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocol.java
index e80f660444..b38665cc94 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocol.java
@@ -17,11 +17,15 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * mqtt protocol
@@ -111,9 +115,47 @@ public class MqttProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
-
-        // todo: add
-        return true;
+        if (!validateIpDomain(host) || !validPort(port)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(keepalive) && !CommonUtil.isNumeric(keepalive)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(protocol)
+                && !"MQTT".equalsIgnoreCase(protocol)
+                && !"MQTTS".equalsIgnoreCase(protocol)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(tlsVersion)
+                && !"TLSv1.2".equalsIgnoreCase(tlsVersion)
+                && !"TLSv1.3".equalsIgnoreCase(tlsVersion)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(insecureSkipVerify)
+                && !"true".equalsIgnoreCase(insecureSkipVerify)
+                && !"false".equalsIgnoreCase(insecureSkipVerify)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(enableMutualAuth)
+                && !"true".equalsIgnoreCase(enableMutualAuth)
+                && !"false".equalsIgnoreCase(enableMutualAuth)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(username) && StringUtils.isBlank(password)) {
+            return true;
+        }
+        if (StringUtils.isBlank(username) && StringUtils.isNotBlank(password)) {
+            return true;
+        }
+        if ("true".equalsIgnoreCase(enableMutualAuth)) {
+            if (StringUtils.isBlank(clientCert) || StringUtils.isBlank(clientKey)) {
+                return true;
+            }
+        }
+        return false;
     }
 
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocol.java
index 48d999984a..df6e8cfe3a 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocol.java
@@ -17,10 +17,15 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * NebulaGraph Protocol
@@ -59,9 +64,16 @@ public class NebulaGraphProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
-
-        // todo: add
-        return true;
+        if (!validateIpDomain(host) || !validPort(port)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timePeriod) && !CommonUtil.isNumeric(timePeriod)) {
+            return true;
+        }
+        return false;
     }
 
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocol.java
index 62ca0e38c5..54a5d1b65c 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocol.java
@@ -17,11 +17,15 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * nginx protocol
@@ -62,6 +66,17 @@ public class NginxProtocol implements CommonRequestProtocol, Protocol {
      */
     @Override
     public boolean isInvalid() {
-        return StringUtils.isAnyBlank(host, port, timeout);
+        if (!validateIpDomain(host) || !validPort(port)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(ssl)
+                && !"true".equalsIgnoreCase(ssl)
+                && !"false".equalsIgnoreCase(ssl)) {
+            return true;
+        }
+        return false;
     }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocol.java
index b0c240fcec..d96ef32d04 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocol.java
@@ -17,11 +17,16 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.hertzbeat.common.entity.job.SshTunnel;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * Redis Protocol
@@ -69,9 +74,20 @@ public class RedisProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
+        if (!validateIpDomain(host) || !validPort(port)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(pattern) && !isValidPattern(pattern)) {
+            return true;
+        }
+        return false;
+    }
 
-        // todo: add
-        return true;
+    private boolean isValidPattern(String pattern) {
+        return "1".equals(pattern) || "2".equals(pattern) || "3".equals(pattern);
     }
 
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocol.java
index 3688e23a54..b5f482be8a 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocol.java
@@ -17,10 +17,14 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
 
 /**
  * rocketmq protocol
@@ -68,8 +72,15 @@ public class RocketmqProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
-
-        // todo: add
-        return true;
+        if (!validateIpDomain(namesrvHost) || !validPort(namesrvPort)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(accessKey) && StringUtils.isBlank(secretKey)) {
+            return true;
+        }
+        if (StringUtils.isBlank(accessKey) && StringUtils.isNotBlank(secretKey)) {
+            return true;
+        }
+        return false;
     }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocol.java
index 88e5f8b78d..1b79e45528 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocol.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocol.java
@@ -17,10 +17,15 @@
 
 package org.apache.hertzbeat.common.entity.job.protocol;
 
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort;
+import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain;
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.util.CommonUtil;
 
 /**
  * ssh Protocol parameter configuration
@@ -113,8 +118,27 @@ public class SshProtocol implements CommonRequestProtocol, Protocol {
 
     @Override
     public boolean isInvalid() {
-
-        // todo: add
-        return true;
+        if (!validateIpDomain(host) || !validPort(port)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(reuseConnection)
+                && !"true".equalsIgnoreCase(reuseConnection)
+                && !"false".equalsIgnoreCase(reuseConnection)) {
+            return true;
+        }
+        if (StringUtils.isNotBlank(useProxy)
+                && !"true".equalsIgnoreCase(useProxy)
+                && !"false".equalsIgnoreCase(useProxy)) {
+            return true;
+        }
+        if ("true".equalsIgnoreCase(useProxy)) {
+            if (!validateIpDomain(proxyHost) || !validPort(proxyPort)) {
+                return true;
+            }
+        }
+        return false;
     }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/log/LogEntry.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/log/LogEntry.java
index dfbfc53743..4d59a885bd 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/log/LogEntry.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/log/LogEntry.java
@@ -26,7 +26,7 @@ import java.util.Map;
 
 /**
  * OpenTelemetry Log Entry entity based on OpenTelemetry log data model specification.
- * 
+ *
  * @see <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md">OpenTelemetry Log Data Model</a>
  */
 @Data
@@ -104,12 +104,22 @@ public class LogEntry {
      */
     private Map<String, Object> resource;
 
+    /**
+     * Resource schema url from OTLP ResourceLogs.
+     */
+    private String resourceSchemaUrl;
+
     /**
      * Instrumentation Scope information.
      * Information about the instrumentation scope that emitted the log.
      */
     private InstrumentationScope instrumentationScope;
 
+    /**
+     * Scope schema url from OTLP ScopeLogs.
+     */
+    private String scopeSchemaUrl;
+
     /**
      * Instrumentation Scope information for logs.
      */
@@ -118,7 +128,7 @@ public class LogEntry {
     @NoArgsConstructor
     @AllArgsConstructor
     public static class InstrumentationScope {
-        
+
         /**
          * The name of the instrumentation scope.
          * This should be the fully-qualified name of the instrumentation library.
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java
similarity index 84%
rename from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java
rename to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java
index 3b71ed637c..cd4b4c12ac 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java
@@ -17,23 +17,20 @@
  * under the License.
  */
 
-// Generated by the protocol buffer compiler.  DO NOT EDIT!
-// source: cluster_msg.proto
-
 package org.apache.hertzbeat.common.entity.plugin;
 
 import java.util.ArrayList;
 import java.util.List;
 import lombok.Data;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
+import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine;
 
 /**
- * The configuration file of the plugin, including parameters and other information
+ * The configuration file of the plugin, including parameters and other information.
  */
 @Data
 public class PluginConfig {
 
-    private List<ParamDefine> params;
+    private List<RuntimeParamDefine> params;
 
     public PluginConfig() {
         this.params = new ArrayList<>();
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
similarity index 100%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/OtlpEntityBindingSummaryDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/OtlpEntityBindingSummaryDto.java
new file mode 100644
index 0000000000..b5a0b5797f
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/OtlpEntityBindingSummaryDto.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.observability.dto.binding;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Canonical identity and entity binding summary for OTLP intake center.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class OtlpEntityBindingSummaryDto {
+
+    private List<String> canonicalIdentityKeys;
+
+    private List<String> recentServices;
+
+    private List<CanonicalIdentitySample> recentIdentitySamples;
+
+    private List<BoundEntity> recentBoundEntities;
+
+    /**
+     * Recent canonical identity sample derived from telemetry intake.
+     */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class CanonicalIdentitySample {
+        private String key;
+        private String value;
+        private String signal;
+    }
+
+    /**
+     * Recently bound entity surfaced in the OTLP intake center.
+     */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class BoundEntity {
+        private Long entityId;
+        private String type;
+        private String name;
+        private String displayName;
+        private String namespace;
+        private String primaryIdentityKey;
+        private String primaryIdentityValue;
+        private long monitorBindCount;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/TelemetryBindingResult.java
similarity index 61%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/TelemetryBindingResult.java
index 40482a336d..240cd23ae8 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/TelemetryBindingResult.java
@@ -3,8 +3,8 @@
  * contributor license agreements.  See the NOTICE file distributed with
  * this work for additional information regarding copyright ownership.
  * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
+ * (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -15,23 +15,36 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.binding;
 
 import java.util.List;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
 
 /**
- * Parameters define transfer entities
+ * Binding result derived from the canonical telemetry identities of a signal.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class ParamDefineDto {
+public class TelemetryBindingResult {
 
-    private String app;
+    private Long entityId;
 
-    private List<ParamDefine> param;
+    private String entityType;
+
+    private String entityName;
+
+    private boolean bound;
+
+    private String source;
+
+    private int matchedIdentityCount;
+
+    private List<String> matchedIdentityKeys;
+
+    private String primaryIdentityKey;
+
+    private String primaryIdentityValue;
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/TelemetryIdentitySnapshot.java
similarity index 60%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/TelemetryIdentitySnapshot.java
index 426834c5fc..58bfa869ea 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/TelemetryIdentitySnapshot.java
@@ -3,8 +3,8 @@
  * contributor license agreements.  See the NOTICE file distributed with
  * this work for additional information regarding copyright ownership.
  * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
+ * (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -15,29 +15,36 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.entity.dto;
+package org.apache.hertzbeat.common.observability.dto.binding;
 
-
-import jakarta.validation.constraints.NotNull;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.springframework.web.multipart.MultipartFile;
 
 /**
- * data transfer class for uploading plugins
+ * Normalized telemetry identity snapshot used by workspace/detail read models.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class PluginUpload {
+public class TelemetryIdentitySnapshot {
+
+    private String source;
+
+    private String signal;
+
+    private Map<String, String> canonicalIdentities;
+
+    private String serviceName;
+
+    private String serviceNamespace;
+
+    private String environmentName;
 
-    @NotNull
-    private MultipartFile jarFile;
+    private String serviceInstanceId;
 
-    @NotNull(message = "Plugin name is required")
-    private String name;
+    private String hostName;
 
-    @NotNull(message = "Enable status is required")
-    private Boolean enableStatus;
+    private Long observedAt;
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/LogEvidence.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/LogEvidence.java
new file mode 100644
index 0000000000..34d3ea721c
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/LogEvidence.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.observability.dto.evidence;
+
+import java.util.List;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.observability.dto.binding.TelemetryBindingResult;
+import org.apache.hertzbeat.common.observability.dto.binding.TelemetryIdentitySnapshot;
+import org.apache.hertzbeat.common.observability.model.CodeNavigationHint;
+
+/**
+ * Unified log evidence view.
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class LogEvidence {
+
+    private String source;
+
+    private String signal;
+
+    private Long entityId;
+
+    private TelemetryIdentitySnapshot identitySnapshot;
+
+    private Long observedAt;
+
+    private String severityOrHealth;
+
+    private String queryHint;
+
+    private TelemetryBindingResult bindingResult;
+
+    private CodeNavigationHint codeNavigationHint;
+
+    private String body;
+
+    private String severityText;
+
+    private String traceId;
+
+    private String spanId;
+
+    private Map<String, String> resource;
+
+    private List<String> preferredSearchTerms;
+}
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/MetricEvidence.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/MetricEvidence.java
new file mode 100644
index 0000000000..8f9594c7db
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/MetricEvidence.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.observability.dto.evidence;
+
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.observability.dto.binding.TelemetryBindingResult;
+import org.apache.hertzbeat.common.observability.dto.binding.TelemetryIdentitySnapshot;
+import org.apache.hertzbeat.common.observability.dto.metrics.MetricCorrelationHint;
+import org.apache.hertzbeat.common.observability.model.CodeNavigationHint;
+
+/**
+ * Unified metric evidence view.
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class MetricEvidence {
+
+    private String source;
+
+    private String signal;
+
+    private Long entityId;
+
+    private TelemetryIdentitySnapshot identitySnapshot;
+
+    private Long observedAt;
+
+    private String severityOrHealth;
+
+    private String queryHint;
+
+    private TelemetryBindingResult bindingResult;
+
+    private MetricCorrelationHint correlationHint;
+
+    private CodeNavigationHint codeNavigationHint;
+
+    private String metricName;
+
+    private String displayName;
+
+    private String metricType;
+
+    private String unit;
+
+    private Double value;
+
+    private Map<String, String> attributes;
+
+    private String monitorContext;
+
+    private String otelContext;
+}
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/TraceEvidence.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/TraceEvidence.java
new file mode 100644
index 0000000000..a280b825a0
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/evidence/TraceEvidence.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.observability.dto.evidence;
+
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.observability.dto.binding.TelemetryBindingResult;
+import org.apache.hertzbeat.common.observability.dto.binding.TelemetryIdentitySnapshot;
+import org.apache.hertzbeat.common.observability.model.CodeNavigationHint;
+
+/**
+ * Unified trace evidence view.
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class TraceEvidence {
+
+    private String source;
+
+    private String signal;
+
+    private Long entityId;
+
+    private TelemetryIdentitySnapshot identitySnapshot;
+
+    private Long observedAt;
+
+    private String severityOrHealth;
+
+    private String queryHint;
+
+    private TelemetryBindingResult bindingResult;
+
+    private CodeNavigationHint codeNavigationHint;
+
+    private String traceId;
+
+    private String rootSpanId;
+
+    private String serviceName;
+
+    private String serviceNamespace;
+
+    private String errorState;
+
+    private int spanCount;
+
+    private Long duration;
+
+    private Map<String, String> resource;
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffInfo.java
similarity index 54%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffInfo.java
index 40482a336d..2c16ceb080 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffInfo.java
@@ -15,23 +15,60 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.handoff;
 
-import java.util.List;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
+import org.apache.hertzbeat.common.observability.model.CodeNavigationHint;
 
 /**
- * Parameters define transfer entities
+ * Response handoff query payload.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class ParamDefineDto {
+public class EntityResponseHandoffInfo {
+
+    private String search;
+
+    private String status;
+
+    private String severity;
 
     private String app;
 
-    private List<ParamDefine> param;
+    private String content;
+
+    private String traceId;
+
+    private String spanId;
+
+    private String serviceName;
+
+    private String serviceNamespace;
+
+    private String severityText;
+
+    private String query;
+
+    private String owner;
+
+    private String system;
+
+    private String environment;
+
+    private Long start;
+
+    private Long end;
+
+    private String source;
+
+    private String focus;
+
+    private CodeNavigationHint codeNavigationHint;
+
+    private String returnTo;
+
+    private String returnLabel;
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsInfo.java
similarity index 68%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsInfo.java
index ba3f2f9ffa..9516553b6f 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsInfo.java
@@ -15,29 +15,29 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.handoff;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
-import java.util.List;
 
 /**
- * Popup rendering and parameter values
+ * Entity response handoff collection.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class PluginParametersVO {
+public class EntityResponseHandoffsInfo {
 
-    /**
-     * Stencil rendering
-     */
-    private List<ParamDefine> paramDefines;
+    private EntityResponseHandoffInfo alerts;
 
-    /**
-     * specific parameter
-     */
-    private List<PluginParam> pluginParams;
+    private EntityResponseHandoffInfo monitors;
+
+    private EntityResponseHandoffInfo logs;
+
+    private EntityResponseHandoffInfo traces;
+
+    private EntityResponseHandoffInfo discovery;
+
+    private EntityResponseHandoffInfo editor;
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionGuideDto.java
similarity index 50%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionGuideDto.java
index 342004062f..85362c2e1d 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionGuideDto.java
@@ -15,40 +15,61 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.entity.push;
+package org.apache.hertzbeat.common.observability.dto.ingestion;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import lombok.AllArgsConstructor;
-import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
 /**
- * push metrics dto
+ * OTLP intake guide payload.
  */
 @Data
-@Builder
+@NoArgsConstructor
 @AllArgsConstructor
-public class PushMetricsDto {
+public class OtlpIngestionGuideDto {
 
-    List<Metrics> metricsList;
+    private String httpProtocolLabel;
 
-    public PushMetricsDto() {
-        metricsList = new ArrayList<>();
-    }
+    private String grpcProtocolLabel;
+
+    private String authHeaderName;
+
+    private String authHeaderExample;
+
+    private String grpcAuthorityExample;
+
+    private List<SignalGuide> signals;
+
+    private List<Snippet> snippets;
 
     /**
-     * metrics
+     * Per-signal OTLP ingestion guidance entry.
      */
     @Data
-    @Builder
+    @NoArgsConstructor
     @AllArgsConstructor
+    public static class SignalGuide {
+        private String signal;
+        private String protocol;
+        private String mode;
+        private String endpoint;
+        private String summary;
+        private String note;
+    }
+
+    /**
+     * Example snippet rendered in the OTLP intake guide.
+     */
+    @Data
     @NoArgsConstructor
-    public static class Metrics {
-        private long monitorId;
-        private Long time;
-        private List<Map<String, String>> metrics;
+    @AllArgsConstructor
+    public static class Snippet {
+        private String key;
+        private String protocol;
+        private String title;
+        private String language;
+        private String content;
     }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionOverviewDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionOverviewDto.java
new file mode 100644
index 0000000000..fa54c41486
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionOverviewDto.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.observability.dto.ingestion;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * OTLP ingestion overview for the unified observability intake center.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class OtlpIngestionOverviewDto {
+
+    private SignalOverview metrics;
+
+    private SignalOverview logs;
+
+    private SignalOverview traces;
+
+    private int activeSignalCount;
+
+    private Long latestObservedAt;
+
+    private int recentServiceCount;
+
+    private long boundEntityCount;
+
+    private List<RecentSignalEvent> recentEvents;
+
+    private List<ReadinessCheck> readinessChecks;
+
+    public OtlpIngestionOverviewDto(SignalOverview metrics, SignalOverview logs, SignalOverview traces,
+                                    int activeSignalCount, Long latestObservedAt, int recentServiceCount,
+                                    long boundEntityCount, List<RecentSignalEvent> recentEvents) {
+        this(metrics, logs, traces, activeSignalCount, latestObservedAt, recentServiceCount, boundEntityCount,
+                recentEvents, List.of());
+    }
+
+    /**
+     * Signal-level overview shown in the OTLP intake center.
+     */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class SignalOverview {
+        private String signal;
+        private boolean active;
+        private long totalCount;
+        private Long latestObservedAt;
+        private String intakeMode;
+        private String summary;
+    }
+
+    /**
+     * Recent telemetry event highlighted in the OTLP intake center.
+     */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class RecentSignalEvent {
+        private String signal;
+        private String title;
+        private String detail;
+        private Long observedAt;
+    }
+
+    /**
+     * Backend-backed readiness check shown in the OTLP intake center.
+     */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class ReadinessCheck {
+        private String key;
+        private String title;
+        private String status;
+        private String summary;
+        private String detail;
+        private Long checkedAt;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/log/EntityLogQueryHint.java
similarity index 66%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/log/EntityLogQueryHint.java
index ba3f2f9ffa..e4af261ad3 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/log/EntityLogQueryHint.java
@@ -15,29 +15,39 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.log;
 
+import java.util.List;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
-import java.util.List;
 
 /**
- * Popup rendering and parameter values
+ * Hints used by the UI to jump into log evidence.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class PluginParametersVO {
+public class EntityLogQueryHint {
+
+    private String title;
+
+    private Map<String, String> resourceFilters;
+
+    private List<String> searchTerms;
+
+    private String traceId;
+
+    private String spanId;
+
+    private String serviceName;
+
+    private String serviceNamespace;
+
+    private String environment;
 
-    /**
-     * Stencil rendering
-     */
-    private List<ParamDefine> paramDefines;
+    private Long start;
 
-    /**
-     * specific parameter
-     */
-    private List<PluginParam> pluginParams;
+    private Long end;
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/log/EntityLogSummaryInfo.java
similarity index 70%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/log/EntityLogSummaryInfo.java
index ba3f2f9ffa..dea919bfe6 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/log/EntityLogSummaryInfo.java
@@ -15,29 +15,31 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.log;
 
+import java.util.List;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
-import java.util.List;
 
 /**
- * Popup rendering and parameter values
+ * Log hint summary for entity workspace.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class PluginParametersVO {
+public class EntityLogSummaryInfo {
+
+    private int hintCount;
+
+    private String preferredQueryType;
+
+    private String preferredQueryTitle;
+
+    private Map<String, String> preferredResourceFilters;
 
-    /**
-     * Stencil rendering
-     */
-    private List<ParamDefine> paramDefines;
+    private List<String> preferredSearchTerms;
 
-    /**
-     * specific parameter
-     */
-    private List<PluginParam> pluginParams;
+    private String fallbackSearchTerm;
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/MetricCorrelationHint.java
similarity index 65%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/MetricCorrelationHint.java
index 426834c5fc..0f9325db8e 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/MetricCorrelationHint.java
@@ -15,29 +15,39 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.entity.dto;
+package org.apache.hertzbeat.common.observability.dto.metrics;
 
-
-import jakarta.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.springframework.web.multipart.MultipartFile;
 
 /**
- * data transfer class for uploading plugins
+ * Correlation hint used by metric evidence to jump to related logs or traces.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class PluginUpload {
+public class MetricCorrelationHint {
+
+    private Long entityId;
+
+    private String traceId;
+
+    private String spanId;
+
+    private String serviceName;
+
+    private String serviceNamespace;
+
+    private String environment;
+
+    private Long start;
+
+    private Long end;
 
-    @NotNull
-    private MultipartFile jarFile;
+    private String searchQuery;
 
-    @NotNull(message = "Plugin name is required")
-    private String name;
+    private String traceQuery;
 
-    @NotNull(message = "Enable status is required")
-    private Boolean enableStatus;
+    private String logQuery;
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/OtlpMetricsConsoleDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/OtlpMetricsConsoleDto.java
new file mode 100644
index 0000000000..6f2a964d8a
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/OtlpMetricsConsoleDto.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.observability.dto.metrics;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
+
+/**
+ * Aggregated OTLP metrics console response.
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class OtlpMetricsConsoleDto {
+
+    private Context context;
+
+    private String query;
+
+    private String datasource;
+
+    private String queryMode;
+
+    private DatasourceQueryData results;
+
+    private Stats stats;
+
+    private String emptyStateReason;
+
+    private String errorMessage;
+
+    /**
+     * Resolved OTLP metrics console context.
+     */
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Context {
+
+        private Long entityId;
+
+        private String entityName;
+
+        private String serviceName;
+
+        private String serviceNamespace;
+
+        private String environment;
+
+        private Long start;
+
+        private Long end;
+    }
+
+    /**
+     * OTLP metrics console statistics.
+     */
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Stats {
+
+        private int totalSeries;
+
+        private int nonEmptySeries;
+
+        private Long latestObservedAt;
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/EntityTraceQueryHintDto.java
similarity index 62%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/EntityTraceQueryHintDto.java
index ba3f2f9ffa..c13065e30a 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/EntityTraceQueryHintDto.java
@@ -3,8 +3,8 @@
  * contributor license agreements.  See the NOTICE file distributed with
  * this work for additional information regarding copyright ownership.
  * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
+ * (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -15,29 +15,39 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.trace;
 
+import java.util.List;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
-import java.util.List;
 
 /**
- * Popup rendering and parameter values
+ * Trace query hint for entity workspace.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class PluginParametersVO {
+public class EntityTraceQueryHintDto {
+
+    private String title;
+
+    private Map<String, String> resourceFilters;
+
+    private List<String> searchTerms;
+
+    private String traceId;
+
+    private String spanId;
+
+    private String serviceName;
+
+    private String serviceNamespace;
+
+    private String environment;
 
-    /**
-     * Stencil rendering
-     */
-    private List<ParamDefine> paramDefines;
+    private Long start;
 
-    /**
-     * specific parameter
-     */
-    private List<PluginParam> pluginParams;
+    private Long end;
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/EntityTraceSummaryDto.java
similarity index 70%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/EntityTraceSummaryDto.java
index 40482a336d..be8993dbd7 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/EntityTraceSummaryDto.java
@@ -3,8 +3,8 @@
  * contributor license agreements.  See the NOTICE file distributed with
  * this work for additional information regarding copyright ownership.
  * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
+ * (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -15,23 +15,27 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.trace;
 
-import java.util.List;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
 
 /**
- * Parameters define transfer entities
+ * Entity trace evidence summary.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class ParamDefineDto {
+public class EntityTraceSummaryDto {
 
-    private String app;
+    private int recentTraceCount;
 
-    private List<ParamDefine> param;
+    private int recentErrorTraceCount;
+
+    private Long latestObservedAt;
+
+    private boolean active;
+
+    private String latestTraceId;
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceDetailDto.java
similarity index 65%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceDetailDto.java
index 342004062f..52d17b549b 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceDetailDto.java
@@ -15,40 +15,41 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.entity.push;
+package org.apache.hertzbeat.common.observability.dto.trace;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import lombok.AllArgsConstructor;
-import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
 /**
- * push metrics dto
+ * Trace detail payload.
  */
 @Data
-@Builder
 @AllArgsConstructor
-public class PushMetricsDto {
-
-    List<Metrics> metricsList;
-
-    public PushMetricsDto() {
-        metricsList = new ArrayList<>();
-    }
-
-    /**
-     * metrics
-     */
-    @Data
-    @Builder
-    @AllArgsConstructor
-    @NoArgsConstructor
-    public static class Metrics {
-        private long monitorId;
-        private Long time;
-        private List<Map<String, String>> metrics;
-    }
+@NoArgsConstructor
+public class TraceDetailDto {
+
+    private String traceId;
+
+    private String rootSpanId;
+
+    private String serviceName;
+
+    private String serviceNamespace;
+
+    private String rootSpanName;
+
+    private Long durationNanos;
+
+    private String status;
+
+    private Long startTime;
+
+    private int errorSpanCount;
+
+    private Map<String, String> resourceAttributes;
+
+    private List<TraceSpanNodeDto> spans;
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceListItemDto.java
similarity index 65%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceListItemDto.java
index 426834c5fc..2e585818a2 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/dto/PluginUpload.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceListItemDto.java
@@ -15,29 +15,38 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.entity.dto;
+package org.apache.hertzbeat.common.observability.dto.trace;
 
-
-import jakarta.validation.constraints.NotNull;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.springframework.web.multipart.MultipartFile;
 
 /**
- * data transfer class for uploading plugins
+ * Trace list item for trace workbench.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class PluginUpload {
+public class TraceListItemDto {
+
+    private String traceId;
+
+    private String rootSpanId;
+
+    private String serviceName;
+
+    private String serviceNamespace;
+
+    private String rootSpanName;
+
+    private Long durationNanos;
+
+    private String status;
 
-    @NotNull
-    private MultipartFile jarFile;
+    private Long startTime;
 
-    @NotNull(message = "Plugin name is required")
-    private String name;
+    private int errorSpanCount;
 
-    @NotNull(message = "Enable status is required")
-    private Boolean enableStatus;
+    private Map<String, String> resourceAttributes;
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceOverviewDto.java
similarity index 78%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceOverviewDto.java
index 40482a336d..4e70f8d58c 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceOverviewDto.java
@@ -15,23 +15,25 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.trace;
 
-import java.util.List;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
 
 /**
- * Parameters define transfer entities
+ * Trace overview payload.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class ParamDefineDto {
+public class TraceOverviewDto {
 
-    private String app;
+    private int totalTraceCount;
 
-    private List<ParamDefine> param;
+    private int errorTraceCount;
+
+    private Long latestObservedAt;
+
+    private boolean hasActiveTrace;
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanEventDto.java
similarity index 76%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanEventDto.java
index 40482a336d..39ba34d2c1 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanEventDto.java
@@ -15,23 +15,26 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.trace;
 
-import java.util.List;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
 
 /**
- * Parameters define transfer entities
+ * Trace span event payload.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class ParamDefineDto {
+public class TraceSpanEventDto {
 
-    private String app;
+    private Long timeUnixNano;
 
-    private List<ParamDefine> param;
+    private String name;
+
+    private Map<String, Object> attributes;
+
+    private Integer droppedAttributesCount;
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanLinkDto.java
similarity index 74%
copy from hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanLinkDto.java
index 40482a336d..a5be3bfdb5 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanLinkDto.java
@@ -15,23 +15,28 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.pojo.dto;
+package org.apache.hertzbeat.common.observability.dto.trace;
 
-import java.util.List;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
 
 /**
- * Parameters define transfer entities
+ * Trace span link payload.
  */
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-public class ParamDefineDto {
+public class TraceSpanLinkDto {
 
-    private String app;
+    private String traceId;
 
-    private List<ParamDefine> param;
+    private String spanId;
+
+    private String traceState;
+
+    private Map<String, Object> attributes;
+
+    private Integer droppedAttributesCount;
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanNodeDto.java
similarity index 52%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
copy to hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanNodeDto.java
index 342004062f..6c1fc189fe 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/trace/TraceSpanNodeDto.java
@@ -15,40 +15,58 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.entity.push;
+package org.apache.hertzbeat.common.observability.dto.trace;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import lombok.AllArgsConstructor;
-import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.observability.model.CodeNavigationHint;
 
 /**
- * push metrics dto
+ * Trace span node payload.
  */
 @Data
-@Builder
 @AllArgsConstructor
-public class PushMetricsDto {
-
-    List<Metrics> metricsList;
-
-    public PushMetricsDto() {
-        metricsList = new ArrayList<>();
... 281086 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 17/50: feat(web-next): close dashboard route catalog parity

Posted by zh...@apache.org.
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 cddb70aa561ceffca58652b8cb467a43a49d6235
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 01:51:04 2026 +0800

    feat(web-next): close dashboard route catalog parity
---
 .../app/compatibility-entrypoints.chrome.test.ts   |  48 ++++++---
 web-next/app/dashboard/page.test.ts                |  66 +++++++-----
 web-next/app/dashboard/page.tsx                    |  13 ++-
 web-next/app/entity-detail-family.chrome.test.ts   |   4 +-
 web-next/app/entity-editor-family.chrome.test.ts   |  36 +++++--
 .../app/milestone4-auth-settings.chrome.test.ts    |  38 ++++---
 .../milestone5-shared-route-owners.chrome.test.ts  |  45 ++++----
 web-next/app/monitor-family.chrome.test.ts         |  63 ++++++-----
 web-next/app/monitor-route-ownership.test.ts       |  37 ++++---
 web-next/app/operator-family.chrome.test.ts        |  34 +++---
 web-next/app/page.test.ts                          |  40 ++++++-
 web-next/app/page.tsx                              |  12 ++-
 web-next/app/settings-family.chrome.test.ts        |  11 +-
 web-next/app/three-signal-family.chrome.test.ts    |  21 ++--
 web-next/lib/dashboard/navigation.test.ts          |   6 +-
 web-next/lib/dashboard/navigation.ts               |   6 +-
 web-next/lib/nav.test.ts                           |   4 +-
 web-next/lib/nav.ts                                |  32 +++---
 web-next/lib/parity/route-manifest.json            | 115 +++++++++++---------
 web-next/lib/parity/route-manifest.test.ts         | 116 +++++++++++++--------
 web-next/lib/workspace-navigation.test.ts          |   6 +-
 21 files changed, 465 insertions(+), 288 deletions(-)

diff --git a/web-next/app/compatibility-entrypoints.chrome.test.ts b/web-next/app/compatibility-entrypoints.chrome.test.ts
index f0c73988a3..f8bfc7a0ad 100644
--- a/web-next/app/compatibility-entrypoints.chrome.test.ts
+++ b/web-next/app/compatibility-entrypoints.chrome.test.ts
@@ -16,37 +16,51 @@ describe('compatibility entrypoint posture', () => {
     const logIntegrationSource = readFileSync(resolve(process.cwd(), 'app/log/integration/page.tsx'), 'utf8');
     const logIntegrationSourceAlias = readFileSync(resolve(process.cwd(), 'app/log/integration/[source]/page.tsx'), 'utf8');
 
-    expect(dashboardSource).toContain("from '../overview/page'");
-    expect(dashboardSource).not.toContain('buildDashboardCompatRouteUrl');
-    expect(dashboardSource).not.toContain('redirect(');
-    expect(alertsSource).toContain('AlertCenterPage');
-    expect(alertCenterSource).toContain("from '../../../lib/compat/search-params'");
-    expect(eventsSource).toContain("from '../log/manage/log-manage-page'");
-    expect(eventsSource).toContain('forcedView="explorer"');
-    expect(loginSource).toContain("from '../../lib/compat/search-params'");
-    expect(statusPublicSource).toContain("from '../../../lib/compat/search-params'");
-    expect(settingSource).toContain("from '../../lib/compat/search-params'");
-    expect(settingSettingsSource).toContain("from '../../../lib/compat/search-params'");
+    expect(dashboardSource).toContain('buildDashboardCompatRouteUrl');
+    expect(dashboardSource).toContain('redirect(');
+    expect(dashboardSource).not.toContain("from '../overview/page'");
+    expect(alertsSource).toContain('buildAlertCompatRouteUrlFromSearchParams');
+    expect(alertsSource).not.toContain('createCompatSearchParamReader');
+    expect(alertsSource).toContain('redirect(');
+    expect(alertCenterSource).toContain('buildAlertCompatRouteUrlFromSearchParams');
+    expect(alertCenterSource).not.toContain("from '../../../lib/compat/search-params'");
+    expect(eventsSource).toContain('buildLogCompatRouteUrlFromSearchParams');
+    expect(eventsSource).not.toContain('createCompatSearchParamReader');
+    expect(eventsSource).toContain("view: 'list'");
+    expect(eventsSource).toContain('redirect(');
+    expect(loginSource).toContain('buildLoginCompatRouteUrl');
+    expect(loginSource).not.toContain("from '../../lib/compat/search-params'");
+    expect(statusPublicSource).toContain('buildPublicStatusCompatRouteUrl');
+    expect(statusPublicSource).not.toContain("from '../../../lib/compat/search-params'");
+    expect(settingSource).toContain('buildSettingsCompatRouteUrl');
+    expect(settingSource).not.toContain("from '../../lib/compat/search-params'");
+    expect(settingSettingsSource).toContain('buildSettingsCompatRouteUrl');
+    expect(settingSettingsSource).not.toContain("from '../../../lib/compat/search-params'");
     expect(alertsSource).not.toContain('function createSearchParamReader');
-    expect(alertsSource).not.toContain('redirect(');
+    expect(alertsSource).not.toContain("from '../alert/page'");
     expect(eventsSource).not.toContain('function createSearchParamReader');
-    expect(eventsSource).not.toContain('redirect(');
+    expect(eventsSource).not.toContain("from '../log/manage/log-manage-page'");
+    expect(eventsSource).not.toContain('forcedView="explorer"');
     expect(loginSource).not.toContain('function buildSearchParams');
     expect(alertCenterSource).not.toContain("redirect('/alert')");
     expect(statusPublicSource).not.toContain("redirect('/status')");
     expect(settingSource).not.toContain("redirect('/setting/settings')");
     expect(settingSettingsSource).not.toContain("redirect('/setting/settings/config')");
-    expect(logStreamSource).toContain('data-log-stream-surface="angular-log-stream"');
-    expect(logStreamSource).toContain('data-log-stream-toolbar="angular-actions"');
+    expect(logStreamSource).toContain('data-log-stream-canonical-live-route="log-manage-stream"');
+    expect(logStreamSource).toContain('forcedView="stream"');
+    expect(logStreamSource).toContain('showViewToggle={false}');
+    expect(logStreamSource).not.toContain('data-log-stream-surface="angular-log-stream"');
+    expect(logStreamSource).not.toContain('data-log-stream-toolbar="angular-actions"');
     expect(logStreamSource).not.toContain('buildLogCompatRouteUrl');
     expect(logStreamSource).not.toContain('redirect(');
-    expect(logStreamSource).not.toContain('LogManagePage');
+    expect(logStreamSource).toContain('LogManagePage');
     expect(logIntegrationSource).toContain('buildLogIntegrationIngestionHref');
     expect(logIntegrationSource).toContain('createSearchParamReader');
     expect(logIntegrationSource).toContain('redirect(');
     expect(logIntegrationSource).not.toContain('LogIntegrationRedirectShell');
     expect(logIntegrationSourceAlias).toContain('buildLogIntegrationIngestionHref');
-    expect(logIntegrationSourceAlias).toContain('createSearchParamReader(resolvedSearchParams, resolved.source)');
+    expect(logIntegrationSourceAlias).toContain('createSearchParamReader(resolvedSearchParams)');
+    expect(logIntegrationSourceAlias).not.toContain('resolved.source');
     expect(logIntegrationSourceAlias).toContain('redirect(');
   });
 });
diff --git a/web-next/app/dashboard/page.test.ts b/web-next/app/dashboard/page.test.ts
index 0b6750d3b9..5953ebb831 100644
--- a/web-next/app/dashboard/page.test.ts
+++ b/web-next/app/dashboard/page.test.ts
@@ -1,34 +1,48 @@
-import React from 'react';
-import { readFileSync } from 'node:fs';
-import { resolve } from 'node:path';
-import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 
-vi.mock('../overview/page', () => ({
-  default: () =>
-    React.createElement(
-      'main',
-      { 'data-workspace-shell': 'true' },
-      React.createElement('aside', null, '总览 rail'),
-      React.createElement('section', { 'data-overview-status-grid': 'true' }, '工作区状态'),
-      React.createElement('section', { 'data-overview-guidance': 'true' }, '下一步:先接入一条可用信号链路'),
-      React.createElement('button', null, '刷新')
-    )
+const redirect = vi.fn();
+
+vi.mock('next/navigation', () => ({
+  redirect
 }));
 
 describe('dashboard alias route', () => {
-  it('renders the overview workspace directly instead of a redirect-only shell', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/dashboard/page.tsx'), 'utf8');
+  it('redirects dashboard compatibility traffic to the overview workbench', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: DashboardAliasPage } = await import('./page');
+
+    await expect(DashboardAliasPage({ searchParams: Promise.resolve({}) })).rejects.toThrow('redirect:/overview');
+    expect(redirect).toHaveBeenCalledWith('/overview');
+  });
+
+  it('preserves machine route context and strips display labels when redirecting dashboard aliases', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
     const { default: DashboardAliasPage } = await import('./page');
-    const html = renderToStaticMarkup(React.createElement(DashboardAliasPage));
-
-    expect(html).toContain('data-workspace-shell="true"');
-    expect(html).toContain('data-overview-status-grid="true"');
-    expect(html).toContain('data-overview-guidance="true"');
-    expect(html).toContain('下一步:先接入一条可用信号链路');
-    expect(html).toContain('刷新');
-    expect(source).toContain("from '../overview/page'");
-    expect(source).not.toContain('redirect(');
-    expect(source).not.toContain('buildDashboardCompatRouteUrl');
+
+    await expect(
+      DashboardAliasPage({
+        searchParams: Promise.resolve({
+          start: '10',
+          end: '20',
+          entityId: '7',
+          entityName: 'checkout',
+          returnTo: '/monitors?returnLabel=Monitors',
+          returnLabel: 'Monitors',
+          serviceName: 'checkout',
+          environment: ['prod', 'staging']
+        })
+      })
+    ).rejects.toThrow(
+      'redirect:/overview?start=10&end=20&entityId=7&entityName=checkout&returnTo=%2Fmonitors&serviceName=checkout&environment=prod'
+    );
+    expect(redirect).toHaveBeenLastCalledWith(
+      '/overview?start=10&end=20&entityId=7&entityName=checkout&returnTo=%2Fmonitors&serviceName=checkout&environment=prod'
+    );
   });
 });
diff --git a/web-next/app/dashboard/page.tsx b/web-next/app/dashboard/page.tsx
index 9ac6b44075..52b18c7da2 100644
--- a/web-next/app/dashboard/page.tsx
+++ b/web-next/app/dashboard/page.tsx
@@ -1,6 +1,11 @@
-import React from 'react';
-import OverviewPage from '../overview/page';
+import { redirect } from 'next/navigation';
+import { buildDashboardCompatRouteUrl, type SearchParamsRecord } from '../../lib/dashboard/navigation';
 
-export default function DashboardAliasPage() {
-  return <OverviewPage />;
+export default async function DashboardAliasPage({
+  searchParams
+}: {
+  searchParams?: Promise<SearchParamsRecord>;
+}) {
+  const resolvedSearchParams = await searchParams;
+  redirect(buildDashboardCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/entity-detail-family.chrome.test.ts b/web-next/app/entity-detail-family.chrome.test.ts
index b81c4ed180..f0de789536 100644
--- a/web-next/app/entity-detail-family.chrome.test.ts
+++ b/web-next/app/entity-detail-family.chrome.test.ts
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';
 
 describe('entity detail family cold-workbench chrome', () => {
   it('removes the remaining legacy white-on-black chrome from the current entity slice', () => {
-    const detailSource = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/page.tsx'), 'utf8');
+    const detailSource = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/entity-detail-page.tsx'), 'utf8');
     const definitionSource = readFileSync(resolve(process.cwd(), 'components/pages/entity-definition-workspace-surface.tsx'), 'utf8');
 
     expect(detailSource).not.toContain('text-white/55');
@@ -21,7 +21,7 @@ describe('entity detail family cold-workbench chrome', () => {
   });
 
   it('adopts shared ops tokens across the current entity slice', () => {
-    const detailSource = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/page.tsx'), 'utf8');
+    const detailSource = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/entity-detail-page.tsx'), 'utf8');
     const detailSurfaceSource = readFileSync(resolve(process.cwd(), 'components/pages/entity-detail-surface.tsx'), 'utf8');
     const definitionSource = readFileSync(resolve(process.cwd(), 'components/pages/entity-definition-workspace-surface.tsx'), 'utf8');
 
diff --git a/web-next/app/entity-editor-family.chrome.test.ts b/web-next/app/entity-editor-family.chrome.test.ts
index 8dd0167710..1bb39b245c 100644
--- a/web-next/app/entity-editor-family.chrome.test.ts
+++ b/web-next/app/entity-editor-family.chrome.test.ts
@@ -7,7 +7,7 @@ describe('entity editor family cold-workbench chrome', () => {
     const editorRowsSource = readFileSync(resolve(process.cwd(), 'components/observability/editor-rows.tsx'), 'utf8');
     const monitorEditorSource = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
     const entityEditorSource = readFileSync(resolve(process.cwd(), 'components/pages/entity-editor-surface.tsx'), 'utf8');
-    const entitiesSource = readFileSync(resolve(process.cwd(), 'app/entities/page.tsx'), 'utf8');
+    const entitiesSource = readFileSync(resolve(process.cwd(), 'app/entities/entity-list-page.tsx'), 'utf8');
 
     expect(editorRowsSource).not.toContain('border-white/10');
     expect(editorRowsSource).not.toContain('bg-black/20');
@@ -56,9 +56,11 @@ describe('entity editor family cold-workbench chrome', () => {
 
   it('adopts shared ops tokens across the shared editor slice', () => {
     const editorRowsSource = readFileSync(resolve(process.cwd(), 'components/observability/editor-rows.tsx'), 'utf8');
+    const workbenchPrimitivesSource = readFileSync(resolve(process.cwd(), 'components/workbench/primitives.tsx'), 'utf8');
+    const workbenchPageSource = readFileSync(resolve(process.cwd(), 'components/workbench/workbench-page.tsx'), 'utf8');
     const monitorEditorSource = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
     const entityEditorSource = readFileSync(resolve(process.cwd(), 'components/pages/entity-editor-surface.tsx'), 'utf8');
-    const entitiesSource = readFileSync(resolve(process.cwd(), 'app/entities/page.tsx'), 'utf8');
+    const entitiesSource = readFileSync(resolve(process.cwd(), 'app/entities/entity-list-page.tsx'), 'utf8');
 
     expect(editorRowsSource).toContain('border-[var(--ops-border-color)]');
     expect(editorRowsSource).toContain('bg-[var(--ops-surface-panel)]');
@@ -66,11 +68,19 @@ describe('entity editor family cold-workbench chrome', () => {
     expect(editorRowsSource).toContain('text-[var(--ops-text-secondary)]');
     expect(editorRowsSource).toContain('hover:bg-[var(--ops-surface-hover)]');
 
-    expect(monitorEditorSource).toContain('border-[var(--ops-border-color)]');
-    expect(monitorEditorSource).toContain('bg-[var(--ops-surface-panel)]');
-    expect(monitorEditorSource).toContain('text-[var(--ops-text-primary)]');
-    expect(monitorEditorSource).toContain('text-[var(--ops-text-secondary)]');
-    expect(monitorEditorSource).toContain('text-[var(--ops-text-tertiary)]');
+    expect(monitorEditorSource).toContain('HzMonitorEditorForm');
+    expect(monitorEditorSource).toContain('HzMonitorEditorHeader');
+    expect(monitorEditorSource).toContain('HzMonitorEditorSection');
+    expect(monitorEditorSource).toContain('data-monitor-editor-form-owner="hertzbeat-ui-monitor-editor-form"');
+    expect(monitorEditorSource).not.toContain('WorkbenchPage');
+    expect(monitorEditorSource).not.toContain('SurfaceSection');
+    expect(workbenchPrimitivesSource).toContain('border-[var(--ops-border-color)]');
+    expect(workbenchPrimitivesSource).toContain('bg-[var(--ops-surface-panel)]');
+    expect(workbenchPrimitivesSource).toContain('text-[var(--ops-text-primary)]');
+    expect(workbenchPrimitivesSource).toContain('text-[var(--ops-text-secondary)]');
+    expect(workbenchPageSource).toContain('divide-[var(--ops-border-color)]');
+    expect(workbenchPageSource).toContain('text-[var(--ops-text-primary)]');
+    expect(workbenchPageSource).toContain('text-[var(--ops-text-secondary)]');
 
     expect(entityEditorSource).toContain('data-entity-editor-shell="otlp-cold-entity-composer"');
     expect(entityEditorSource).toContain('data-entity-editor-style-baseline="hertzbeat-cold-matte"');
@@ -87,12 +97,18 @@ describe('entity editor family cold-workbench chrome', () => {
   });
 
   it('keeps the active monitor/entity edit routes composed from the shared editor owners', () => {
-    const monitorEditSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/edit/page.tsx'), 'utf8');
-    const entityNewSource = readFileSync(resolve(process.cwd(), 'app/entities/new/page.tsx'), 'utf8');
-    const entityEditSource = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/edit/page.tsx'), 'utf8');
+    const monitorEditRouteSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/edit/page.tsx'), 'utf8');
+    const monitorEditSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/edit/monitor-edit-page.tsx'), 'utf8');
+    const entityNewRouteSource = readFileSync(resolve(process.cwd(), 'app/entities/new/page.tsx'), 'utf8');
+    const entityNewSource = readFileSync(resolve(process.cwd(), 'app/entities/new/entity-new-page.tsx'), 'utf8');
+    const entityEditRouteSource = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/edit/page.tsx'), 'utf8');
+    const entityEditSource = readFileSync(resolve(process.cwd(), 'app/entities/[entityId]/edit/entity-edit-page.tsx'), 'utf8');
 
+    expect(monitorEditRouteSource).toContain("import MonitorEditPage from './monitor-edit-page'");
     expect(monitorEditSource).toContain('MonitorEditorSurface');
+    expect(entityNewRouteSource).toContain("import EntityNewPage from './entity-new-page'");
     expect(entityNewSource).toContain('EntityEditorSurface');
+    expect(entityEditRouteSource).toContain("import EntityEditPage from './entity-edit-page'");
     expect(entityEditSource).toContain('EntityEditorSurface');
   });
 });
diff --git a/web-next/app/milestone4-auth-settings.chrome.test.ts b/web-next/app/milestone4-auth-settings.chrome.test.ts
index b28798181d..2d709b6a75 100644
--- a/web-next/app/milestone4-auth-settings.chrome.test.ts
+++ b/web-next/app/milestone4-auth-settings.chrome.test.ts
@@ -4,8 +4,8 @@ import { describe, expect, it } from 'vitest';
 
 describe('milestone 4 auth/public and settings cold-workbench chrome', () => {
   it('removes the remaining bright-shell residue from the current auth/settings slice', () => {
-    const passportLockSource = readFileSync(resolve(process.cwd(), 'app/passport/lock/page.tsx'), 'utf8');
-    const settingTokenSource = readFileSync(resolve(process.cwd(), 'app/setting/settings/token/page.tsx'), 'utf8');
+    const passportLockSource = readFileSync(resolve(process.cwd(), 'app/passport/lock/passport-lock-page.tsx'), 'utf8');
+    const settingTokenSource = readFileSync(resolve(process.cwd(), 'app/setting/settings/token/setting-token-page.tsx'), 'utf8');
     const combinedSource = [passportLockSource, settingTokenSource].join('\n');
 
     expect(combinedSource).not.toContain('border-white/35');
@@ -23,24 +23,30 @@ describe('milestone 4 auth/public and settings cold-workbench chrome', () => {
   });
 
   it('adopts shared ops owners and danger states across the current auth/settings slice', () => {
-    const passportLockSource = readFileSync(resolve(process.cwd(), 'app/passport/lock/page.tsx'), 'utf8');
-    const settingTokenSource = readFileSync(resolve(process.cwd(), 'app/setting/settings/token/page.tsx'), 'utf8');
+    const passportLockRouteSource = readFileSync(resolve(process.cwd(), 'app/passport/lock/page.tsx'), 'utf8');
+    const passportLockSource = readFileSync(resolve(process.cwd(), 'app/passport/lock/passport-lock-page.tsx'), 'utf8');
+    const settingTokenSource = readFileSync(resolve(process.cwd(), 'app/setting/settings/token/setting-token-page.tsx'), 'utf8');
 
-    expect(passportLockSource).toContain('components/observability');
-    expect(passportLockSource).toContain('ObservabilityStatusState');
+    expect(passportLockRouteSource).not.toMatch(/^['"]use client['"]/);
+    expect(passportLockRouteSource).toContain("import PassportLockPage from './passport-lock-page'");
+    expect(passportLockSource).toContain('HzPassportLockSurface');
+    expect(passportLockSource).toContain('PassportPanel');
     expect(passportLockSource).not.toContain('components/workbench/primitives');
     expect(passportLockSource).toContain('border-[var(--ops-border-color)]');
-    expect(passportLockSource).toContain('bg-[var(--ops-surface-panel)]');
-    expect(passportLockSource).toContain('bg-[var(--ops-surface-raised)]');
-    expect(passportLockSource).toContain('text-[var(--ops-text-primary)]');
-    expect(passportLockSource).toContain('text-[var(--ops-text-tertiary)]');
-    expect(passportLockSource).toContain('text-[var(--ops-primary)]');
+    expect(passportLockSource).toContain('bg-[#101217]');
+    expect(passportLockSource).toContain('data-passport-lock-panel="angular-wide"');
+    expect(passportLockSource).toContain('data-passport-lock-panel-owner="hertzbeat-ui-passport-lock"');
 
-    expect(settingTokenSource).toContain('ObservabilityStatusState');
-    expect(settingTokenSource).toContain('StageSection');
-    expect(settingTokenSource).toContain('tone="danger"');
-    expect(settingTokenSource).toContain('DrawerCodePreview');
-    expect(settingTokenSource).toContain('SummaryMetricGrid');
+    expect(settingTokenSource).toContain('SettingsConsoleTitle');
+    expect(settingTokenSource).toContain('coldOpsCatalogVisual');
+    expect(settingTokenSource).toContain('data-setting-token-table-panel="cold-dense-table"');
+    expect(settingTokenSource).toContain('data-setting-token-strip-style="cold-inline-counts"');
+    expect(settingTokenSource).toContain('data-setting-token-row-action="cold-row-action"');
+    expect(settingTokenSource).toContain('text-[#fca5a5]');
+    expect(settingTokenSource).not.toContain('ObservabilityStatusState');
+    expect(settingTokenSource).not.toContain('StageSection');
+    expect(settingTokenSource).not.toContain('DrawerCodePreview');
+    expect(settingTokenSource).not.toContain('SummaryMetricGrid');
     expect(settingTokenSource).not.toContain('PayloadPreview density="compact" className="mt-3"');
   });
 });
diff --git a/web-next/app/milestone5-shared-route-owners.chrome.test.ts b/web-next/app/milestone5-shared-route-owners.chrome.test.ts
index 4499341847..ffe0d47d20 100644
--- a/web-next/app/milestone5-shared-route-owners.chrome.test.ts
+++ b/web-next/app/milestone5-shared-route-owners.chrome.test.ts
@@ -38,42 +38,47 @@ function collectNonTestAppFiles(dir: string): string[] {
 
 describe('Milestone 5 shared route owners', () => {
   it('keeps representative route entrypoints pinned to shared observability owners', () => {
-    const overviewSource = readFileSync(resolve(process.cwd(), 'app/overview/page.tsx'), 'utf8');
-    const monitorsSource = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
+    const overviewSource = readFileSync(resolve(process.cwd(), 'app/overview/overview-page.tsx'), 'utf8');
+    const monitorsSource = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
     const logSource = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
-    const traceSource = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
+    const traceSource = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
     const alertIntegrationSource = readFileSync(resolve(process.cwd(), 'app/alert/integration/[source]/page.tsx'), 'utf8');
-    const alertNoticeSource = readFileSync(resolve(process.cwd(), 'app/alert/notice/page.tsx'), 'utf8');
-    const statusSource = readFileSync(resolve(process.cwd(), 'app/status/page.tsx'), 'utf8');
+    const alertNoticeSource = readFileSync(resolve(process.cwd(), 'app/alert/notice/alert-notice-page.tsx'), 'utf8');
+    const statusSource = readFileSync(resolve(process.cwd(), 'app/status/status-page.tsx'), 'utf8');
     const passportSource = readFileSync(resolve(process.cwd(), 'app/passport/login/page.tsx'), 'utf8');
 
     expect(overviewSource).toContain('StageSection');
     expect(overviewSource).toContain('SupportPanel');
     expect(overviewSource).not.toContain('components/workbench/primitives');
 
-    expect(monitorsSource).toContain('StageSection');
-    expect(monitorsSource).toContain('SummaryMetricGrid');
-    expect(monitorsSource).toContain('DrawerSection');
+    expect(monitorsSource).toContain('HzExplorerFrame');
+    expect(monitorsSource).toContain('HzDataTable');
+    expect(monitorsSource).toContain('data-monitor-manage-shell-owner="hertzbeat-ui-explorer-frame"');
+    expect(monitorsSource).not.toContain('StageSection');
+    expect(monitorsSource).not.toContain('SummaryMetricGrid');
+    expect(monitorsSource).not.toContain('DrawerSection');
     expect(monitorsSource).not.toContain('components/workbench/primitives');
 
-    expect(logSource).toContain('FactsStrip');
-    expect(logSource).toContain('StageSection');
-    expect(logSource).toContain('DrawerSection');
-    expect(logSource).toContain('DrawerCodePreview');
+    expect(logSource).toContain('ClientWorkbench');
+    expect(logSource).toContain('TimeRangeControl');
+    expect(logSource).toContain('data-log-manage-time-control="shared-time-context-control"');
     expect(logSource).not.toContain('components/workbench/primitives');
 
-    expect(traceSource).toContain('FactsStrip');
-    expect(traceSource).toContain('StageSection');
-    expect(traceSource).toContain('DrawerSection');
-    expect(traceSource).toContain('DrawerCodePreview');
+    expect(traceSource).toContain('ClientWorkbench');
+    expect(traceSource).toContain('TimeRangeControl');
+    expect(traceSource).toContain('ObservabilityWaterfall');
+    expect(traceSource).toContain('data-trace-manage-time-control="shared-time-context-control"');
     expect(traceSource).not.toContain('components/workbench/primitives');
 
-    expect(alertIntegrationSource).toContain('StageSection');
-    expect(alertIntegrationSource).toContain('DrawerSection');
-    expect(alertIntegrationSource).toContain('DrawerCodePreview');
+    expect(alertIntegrationSource).toContain('AlertIntegrationMarkdown');
+    expect(alertIntegrationSource).toContain('coldOpsCatalogVisual');
+    expect(alertIntegrationSource).toContain('data-alert-integration-surface="otlp-cold-source-doc"');
     expect(alertIntegrationSource).not.toContain('components/workbench/primitives');
 
-    expect(alertNoticeSource).toContain('StageSection');
+    expect(alertNoticeSource).toContain('AlertNoticeConsoleShell');
+    expect(alertNoticeSource).toContain('ClientWorkbench');
+    expect(alertNoticeSource).toContain('HzConfirmDialog');
+    expect(alertNoticeSource).toContain('data-alert-notice-surface="otlp-cold-notice-console"');
     expect(alertNoticeSource).not.toContain('components/workbench/primitives');
 
     expect(statusSource).toContain('PublicStatusShell');
diff --git a/web-next/app/monitor-family.chrome.test.ts b/web-next/app/monitor-family.chrome.test.ts
index 6274be92f6..78ab561df1 100644
--- a/web-next/app/monitor-family.chrome.test.ts
+++ b/web-next/app/monitor-family.chrome.test.ts
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';
 
 describe('monitor family cold-workbench chrome', () => {
   it('removes the remaining legacy white-on-black chrome from the current monitor slice', () => {
-    const monitorsSource = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
+    const monitorsSource = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
     const consoleSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
     const sectionsSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-sections.tsx'), 'utf8');
     const realtimeSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
@@ -40,24 +40,26 @@ describe('monitor family cold-workbench chrome', () => {
   });
 
   it('adopts shared ops tokens across the current monitor slice', () => {
-    const monitorsSource = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
-    const detailRouteSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/page.tsx'), 'utf8');
+    const monitorsSource = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+    const detailRouteSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/monitor-detail-page.tsx'), 'utf8');
     const consoleSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
     const sectionsSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-sections.tsx'), 'utf8');
     const realtimeSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
     const historySource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
 
-    expect(monitorsSource).toContain('border-[var(--ops-border-color)]');
-    expect(monitorsSource).toContain('text-[var(--ops-text-secondary)]');
-    expect(monitorsSource).toContain('text-[var(--ops-text-tertiary)]');
-    expect(monitorsSource).toContain('SummaryMetricGrid');
-    expect(monitorsSource).toContain('StageSection');
-    expect(monitorsSource).toContain("title={t('monitors.section.list.title')}");
-    expect(monitorsSource).toContain("DrawerSection title={t('monitors.rail.selected')}");
-    expect(monitorsSource).toContain("DrawerSection title={t('monitors.rail.labels')}");
-    expect(monitorsSource).toContain("DrawerSection title={t('monitors.rail.controls')}");
-    expect(monitorsSource).toContain('DrawerCodePreview');
-    expect(monitorsSource).toContain('ObservabilityStatusState');
+    expect(monitorsSource).toContain('HzExplorerFrame');
+    expect(monitorsSource).toContain('HzDataTable');
+    expect(monitorsSource).toContain('HzBatchToolbar');
+    expect(monitorsSource).toContain('HzMonitorFilterBar');
+    expect(monitorsSource).toContain('data-monitor-manage-filter-owner="hertzbeat-ui-monitor-filter-bar"');
+    expect(monitorsSource).toContain('data-monitor-manage-shell-owner="hertzbeat-ui-explorer-frame"');
+    expect(monitorsSource).toContain('HzStatusBadge');
+    expect(monitorsSource).not.toContain('HzWorkbenchSurface');
+    expect(monitorsSource).not.toContain('data-monitor-manage-detail-rail');
+    expect(monitorsSource).not.toContain('SummaryMetricGrid');
+    expect(monitorsSource).not.toContain('StageSection');
+    expect(monitorsSource).not.toContain('DrawerSection');
+    expect(monitorsSource).not.toContain('DrawerCodePreview');
     expect(monitorsSource).not.toContain('PayloadPreview density="compact" className="mt-2"');
     expect(monitorsSource).not.toContain('components/workbench/primitives');
     expect(monitorsSource).not.toContain('components/workbench/toolbar');
@@ -65,29 +67,26 @@ describe('monitor family cold-workbench chrome', () => {
     expect(detailRouteSource).toContain('MonitorDetailConsole');
     expect(detailRouteSource).toContain('MonitorDetailSections');
 
-    expect(consoleSource).toContain('border-[var(--ops-border-color)]');
-    expect(consoleSource).toContain('text-[var(--ops-text-primary)]');
-    expect(consoleSource).toContain('text-[var(--ops-text-secondary)]');
-    expect(consoleSource).toContain('text-[var(--ops-text-tertiary)]');
-    expect(consoleSource).toContain('ObservabilityControlChip');
-    expect(consoleSource).toContain('ObservabilityPillButton');
-    expect(consoleSource).toContain('ObservabilityBadge');
+    expect(consoleSource).toContain('HzMonitorDetailConsoleShell');
+    expect(consoleSource).toContain('HzMonitorDetailWorkbenchFrame');
+    expect(consoleSource).toContain('data-monitor-detail-console-shell-owner="hertzbeat-ui-detail-console-shell"');
+    expect(consoleSource).toContain('data-monitor-workbench-stage-owner="hertzbeat-ui-detail-workbench-frame"');
     expect(consoleSource).not.toContain("from '../workbench/primitives'");
 
-    expect(sectionsSource).toContain('border-[var(--ops-border-color)]');
-    expect(sectionsSource).toContain('bg-[var(--ops-surface-panel)]');
-    expect(sectionsSource).toContain('text-[var(--ops-text-secondary)]');
-    expect(sectionsSource).toContain('ObservabilityControlChip');
-    expect(sectionsSource).toContain('ObservabilityBadge');
+    expect(sectionsSource).toContain('HzMonitorDetailStage');
+    expect(sectionsSource).toContain('HzMonitorDetailSignalList');
+    expect(sectionsSource).toContain('data-monitor-detail-stage-owner="hertzbeat-ui-detail-stage"');
+    expect(sectionsSource).toContain('data-monitor-detail-signal-list-owner="hertzbeat-ui-signal-list"');
     expect(sectionsSource).not.toContain('ObservabilityInsetPanel');
     expect(sectionsSource).not.toContain("from '../workbench/primitives'");
 
-    expect(realtimeSource).toContain('border-[var(--ops-border-color)]');
-    expect(realtimeSource).toContain('bg-[var(--ops-surface-panel)]');
-    expect(realtimeSource).toContain('text-[var(--ops-text-secondary)]');
+    expect(realtimeSource).toContain('HzMonitorDetailStage');
+    expect(realtimeSource).toContain('HzMonitorRealtimeToolbar');
+    expect(realtimeSource).toContain('data-monitor-realtime-toolbar-owner="hertzbeat-ui-realtime-toolbar"');
 
-    expect(historySource).toContain('border-[var(--ops-border-color)]');
-    expect(historySource).toContain('bg-[var(--ops-surface-panel)]');
-    expect(historySource).toContain('text-[var(--ops-text-secondary)]');
+    expect(historySource).toContain('HzMonitorDetailStage');
+    expect(historySource).toContain('HzMonitorEvidenceFrame');
+    expect(historySource).toContain('data-monitor-history-stage-owner="hertzbeat-ui-detail-stage"');
+    expect(historySource).toContain('data-monitor-history-chart-owner="hertzbeat-ui-echarts-panel"');
   });
 });
diff --git a/web-next/app/monitor-route-ownership.test.ts b/web-next/app/monitor-route-ownership.test.ts
index 3528100aaf..84909ed0ba 100644
--- a/web-next/app/monitor-route-ownership.test.ts
+++ b/web-next/app/monitor-route-ownership.test.ts
@@ -4,12 +4,16 @@ import { describe, expect, it } from 'vitest';
 
 describe('monitor route ownership posture', () => {
   it('keeps monitor list/detail/create/edit handoffs on shared navigation owners', () => {
-    const listSource = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
-    const detailSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/page.tsx'), 'utf8');
-    const newSource = readFileSync(resolve(process.cwd(), 'app/monitors/new/page.tsx'), 'utf8');
-    const editSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/edit/page.tsx'), 'utf8');
+    const listRouteSource = readFileSync(resolve(process.cwd(), 'app/monitors/page.tsx'), 'utf8');
+    const listSource = readFileSync(resolve(process.cwd(), 'app/monitors/monitor-manage-page.tsx'), 'utf8');
+    const detailSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/monitor-detail-page.tsx'), 'utf8');
+    const newRouteSource = readFileSync(resolve(process.cwd(), 'app/monitors/new/page.tsx'), 'utf8');
+    const newSource = readFileSync(resolve(process.cwd(), 'app/monitors/new/monitor-new-page.tsx'), 'utf8');
+    const editRouteSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/edit/page.tsx'), 'utf8');
+    const editSource = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/edit/monitor-edit-page.tsx'), 'utf8');
     const editorSurfaceSource = readFileSync(resolve(process.cwd(), 'components/pages/monitor-editor-surface.tsx'), 'utf8');
 
+    expect(listRouteSource).toContain("import MonitorManagePage from './monitor-manage-page'");
     expect(listSource).toContain('buildMonitorDetailHref');
     expect(listSource).toContain('buildMonitorEditHref');
     expect(listSource).toContain('buildMonitorNewHref');
@@ -20,20 +24,21 @@ describe('monitor route ownership posture', () => {
     expect(detailSource).not.toContain("from '@/lib/monitor-editor/navigation'");
     expect(detailSource).not.toContain('buildMonitorEditorReturnUrl');
 
-    expect(newSource).toContain('returnContext={{');
-    expect(newSource).toContain("labels: searchParams.get('labels')");
-    expect(newSource).toContain("pageIndex: searchParams.get('pageIndex')");
-    expect(newSource).toContain("pageSize: searchParams.get('pageSize')");
-    expect(newSource).toContain("entityId: searchParams.get('entityId')");
-    expect(newSource).toContain("entityName: searchParams.get('entityName')");
+    expect(newRouteSource).toContain("import MonitorNewPage from './monitor-new-page'");
+    expect(newSource).toContain('returnContext={returnContext}');
+    expect(newRouteSource).toContain('readMonitorNewRouteState(resolvedSearchParams)');
     expect(newSource).not.toContain("returnLabel: searchParams.get('returnLabel')");
-    expect(editSource).toContain('returnContext={{');
-    expect(editSource).toContain("labels: searchParams.get('labels')");
-    expect(editSource).toContain("pageIndex: searchParams.get('pageIndex')");
-    expect(editSource).toContain("pageSize: searchParams.get('pageSize')");
-    expect(editSource).toContain("entityId: searchParams.get('entityId')");
-    expect(editSource).toContain("entityName: searchParams.get('entityName')");
+    expect(editRouteSource).toContain("import MonitorEditPage from './monitor-edit-page'");
+    expect(editSource).toContain('returnContext={returnContext}');
+    expect(editRouteSource).toContain('readMonitorEditRouteState(resolvedSearchParams)');
     expect(editSource).not.toContain("returnLabel: searchParams.get('returnLabel')");
+    const queryStateSource = readFileSync(resolve(process.cwd(), 'lib/monitor-editor/query-state.ts'), 'utf8');
+    expect(queryStateSource).toContain("labels: reader.get('labels')");
+    expect(queryStateSource).toContain("pageIndex: reader.get('pageIndex')");
+    expect(queryStateSource).toContain("pageSize: reader.get('pageSize')");
+    expect(queryStateSource).toContain("entityId: reader.get('entityId')");
+    expect(queryStateSource).toContain("entityName: reader.get('entityName')");
+    expect(queryStateSource).not.toContain("returnLabel: reader.get('returnLabel')");
 
     expect(editorSurfaceSource).toContain('returnContext?:');
     expect(editorSurfaceSource).toContain('...returnContext');
diff --git a/web-next/app/operator-family.chrome.test.ts b/web-next/app/operator-family.chrome.test.ts
index 1f272df894..6fbe9765f5 100644
--- a/web-next/app/operator-family.chrome.test.ts
+++ b/web-next/app/operator-family.chrome.test.ts
@@ -4,8 +4,8 @@ import { describe, expect, it } from 'vitest';
 
 describe('operator family cold-workbench chrome', () => {
   it('removes the remaining route-local rounded panel recipes from the OTLP operator slice', () => {
-    const otlpSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/page.tsx'), 'utf8');
-    const metricsSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/page.tsx'), 'utf8');
+    const otlpSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/otlp-page.tsx'), 'utf8');
+    const metricsSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
     const combinedSource = [otlpSource, metricsSource].join('\n');
 
     expect(combinedSource).not.toContain('rounded-[10px] border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-4 py-3 shadow-none');
@@ -13,24 +13,26 @@ describe('operator family cold-workbench chrome', () => {
     expect(combinedSource).not.toContain('rounded-[10px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)]');
   });
 
-  it('moves the OTLP pilot route onto the new observability owner layer', () => {
-    const otlpSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/page.tsx'), 'utf8');
-    const metricsSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/page.tsx'), 'utf8');
+  it('keeps the OTLP pilot route on the cold-ops catalog owner layer', () => {
+    const otlpSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/otlp-page.tsx'), 'utf8');
+    const metricsSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
 
-    expect(otlpSource).toContain('components/observability');
-    expect(otlpSource).toContain('FactsStrip');
-    expect(otlpSource).toContain('StageSection');
-    expect(otlpSource).toContain('SummaryMetricGrid');
-    expect(otlpSource).toContain('DrawerSection');
-    expect(otlpSource).toContain('SupportActionBar');
+    expect(otlpSource).toContain('coldOpsCatalogVisual');
+    expect(otlpSource).toContain('SearchRow');
+    expect(otlpSource).toContain('ClientWorkbench');
+    expect(otlpSource).toContain('buildReadinessRows');
+    expect(otlpSource).toContain('buildSelfCheckRows');
+    expect(otlpSource).toContain('data-otlp-center-source-grid="hertzbeat-source-catalog"');
+    expect(otlpSource).toContain('data-otlp-center-filter-rail="hertzbeat-prism-filters"');
     expect(otlpSource).not.toContain('components/workbench/primitives');
 
     expect(metricsSource).toContain('components/observability');
-    expect(metricsSource).toContain('FactsStrip');
-    expect(metricsSource).toContain('StageSection');
-    expect(metricsSource).toContain('SummaryMetricGrid');
-    expect(metricsSource).toContain('DrawerSection');
-    expect(metricsSource).toContain('DrawerCodePreview');
+    expect(metricsSource).toContain('components/observability/time-range-control');
+    expect(metricsSource).toContain('EChartsPanel');
+    expect(metricsSource).toContain('ClientWorkbench');
+    expect(metricsSource).toContain('buildConsoleFacts');
+    expect(metricsSource).toContain('buildMetricsHandoffLinks');
+    expect(metricsSource).toContain('data-otlp-metrics-route="otlp-cold-metrics-workbench"');
     expect(metricsSource).not.toContain('components/workbench/primitives');
   });
 });
diff --git a/web-next/app/page.test.ts b/web-next/app/page.test.ts
index 00fb5ab761..25f41bb891 100644
--- a/web-next/app/page.test.ts
+++ b/web-next/app/page.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 const redirect = vi.fn();
 
@@ -7,6 +7,10 @@ vi.mock('next/navigation', () => ({
 }));
 
 describe('root alias route', () => {
+  beforeEach(() => {
+    redirect.mockReset();
+  });
+
   it('redirects the shell entry point to the overview workbench', async () => {
     redirect.mockImplementation((target: string) => {
       throw new Error(`redirect:${target}`);
@@ -14,7 +18,39 @@ describe('root alias route', () => {
 
     const { default: HomePage } = await import('./page');
 
-    expect(() => HomePage()).toThrow('redirect:/overview');
+    await expect(HomePage({})).rejects.toThrow('redirect:/overview');
     expect(redirect).toHaveBeenCalledWith('/overview');
   });
+
+  it('preserves machine query context when redirecting to overview', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: HomePage } = await import('./page');
+
+    await expect(
+      HomePage({
+        searchParams: Promise.resolve({
+          source: 'shell',
+          serviceName: 'checkout',
+          returnTo: '/monitors?returnLabel=Legacy',
+          returnLabel: 'Overview',
+          start: '1700000000000',
+          environment: ['prod', 'ignored']
+        })
+      })
+    ).rejects.toThrow('redirect:/overview?');
+
+    const target = redirect.mock.calls[0]?.[0] as string;
+    const url = new URL(target, 'http://127.0.0.1');
+
+    expect(url.pathname).toBe('/overview');
+    expect(url.searchParams.get('source')).toBe('shell');
+    expect(url.searchParams.get('serviceName')).toBe('checkout');
+    expect(url.searchParams.get('environment')).toBe('prod');
+    expect(url.searchParams.get('returnTo')).toBe('/monitors');
+    expect(url.searchParams.get('returnLabel')).toBeNull();
+    expect(url.searchParams.get('start')).toBe('1700000000000');
+  });
 });
diff --git a/web-next/app/page.tsx b/web-next/app/page.tsx
index db5e41d241..a868bb1d37 100644
--- a/web-next/app/page.tsx
+++ b/web-next/app/page.tsx
@@ -1,5 +1,13 @@
 import { redirect } from 'next/navigation';
 
-export default function HomePage() {
-  redirect('/overview');
+import { buildOverviewCompatRouteUrl, type SearchParamsRecord } from '../lib/overview/navigation';
+
+interface HomePageProps {
+  searchParams?: Promise<SearchParamsRecord>;
+}
+
+export default async function HomePage({ searchParams }: HomePageProps) {
+  const resolvedSearchParams = await searchParams;
+
+  redirect(buildOverviewCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/settings-family.chrome.test.ts b/web-next/app/settings-family.chrome.test.ts
index a55b707919..2e7372c2be 100644
--- a/web-next/app/settings-family.chrome.test.ts
+++ b/web-next/app/settings-family.chrome.test.ts
@@ -36,7 +36,7 @@ describe('settings family cold-matte chrome', () => {
     const labelSource = readFileSync(resolve(process.cwd(), 'components/pages/label-manage-surface.tsx'), 'utf8');
     const defineSource = readFileSync(resolve(process.cwd(), 'components/pages/setting-define-surface.tsx'), 'utf8');
     const statusSource = readFileSync(resolve(process.cwd(), 'components/pages/status-setting-surface.tsx'), 'utf8');
-    const tokenSource = readFileSync(resolve(process.cwd(), 'app/setting/settings/token/page.tsx'), 'utf8');
+    const tokenSource = readFileSync(resolve(process.cwd(), 'app/setting/settings/token/setting-token-page.tsx'), 'utf8');
 
     expect(settingsSurfaceSource).toContain('variant="flat"');
 
@@ -53,11 +53,14 @@ describe('settings family cold-matte chrome', () => {
     expect(pluginSource).not.toContain('WorkbenchTableFrame');
 
     expect(labelSource).toContain('data-label-manage-style-baseline={coldLabelVisual.canvasName}');
-    expect(labelSource).toContain('data-label-table-shell="cold-dense-table"');
+    expect(labelSource).toContain('data-label-card-grid-contract="angular-card-grid"');
+    expect(labelSource).toContain('data-label-card-grid-owner="hertzbeat-ui-label-tag"');
+    expect(labelSource).not.toContain('data-label-table-shell="cold-dense-table"');
     expect(defineSource).toContain('data-setting-define-style-baseline={coldDefineVisual.canvasName}');
     expect(defineSource).toContain('data-setting-define-workspace="cold-define-workspace"');
-    expect(defineSource).toContain("from '../ui/search-row'");
-    expect(defineSource).toContain('data-setting-define-search-owner="shared-search-row"');
+    expect(defineSource).toContain('HzYamlWorkspace');
+    expect(defineSource).not.toContain("from '../ui/search-row'");
+    expect(defineSource).not.toContain('data-setting-define-search-owner="shared-search-row"');
     expect(defineSource).not.toContain('coldDefineVisual.search.row');
     expect(defineSource).not.toContain('coldDefineVisual.search.input');
     expect(statusSource).toContain('data-status-setting-style-baseline={coldStatusVisual.canvasName}');
diff --git a/web-next/app/three-signal-family.chrome.test.ts b/web-next/app/three-signal-family.chrome.test.ts
index 0238a3ab53..1504880614 100644
--- a/web-next/app/three-signal-family.chrome.test.ts
+++ b/web-next/app/three-signal-family.chrome.test.ts
@@ -6,10 +6,11 @@ const sourceFiles = [
   'components/pages/three-signal-desk-shell.tsx',
   'components/overview/overview-console.tsx',
   'components/workbench/primitives.tsx',
+  'app/overview/overview-page.tsx',
   'app/log/manage/log-manage-page.tsx',
-  'app/trace/manage/page.tsx',
-  'app/ingestion/otlp/page.tsx',
-  'app/ingestion/otlp/metrics/page.tsx'
+  'app/trace/manage/trace-manage-page.tsx',
+  'app/ingestion/otlp/otlp-page.tsx',
+  'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'
 ];
 
 describe('three-signal family cold-workbench chrome', () => {
@@ -24,10 +25,10 @@ describe('three-signal family cold-workbench chrome', () => {
 
   it('adopts shared workbench owners across the exactness-sensitive three-signal family', () => {
     const shellSource = readFileSync(resolve(process.cwd(), 'components/pages/three-signal-desk-shell.tsx'), 'utf8');
-    const overviewSource = readFileSync(resolve(process.cwd(), 'app/overview/page.tsx'), 'utf8');
+    const overviewSource = readFileSync(resolve(process.cwd(), 'app/overview/overview-page.tsx'), 'utf8');
     const logSource = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
-    const traceSource = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
-    const metricsSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/page.tsx'), 'utf8');
+    const traceSource = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
+    const metricsSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
 
     expect(shellSource).toContain("../observability/workspace-shell");
     expect(shellSource).not.toContain("../workbench/workspace-shell");
@@ -74,11 +75,11 @@ describe('three-signal family cold-workbench chrome', () => {
   });
 
   it('removes the remaining legacy white-on-black chrome from the three-signal family', () => {
-    const overviewSource = readFileSync(resolve(process.cwd(), 'app/overview/page.tsx'), 'utf8');
+    const overviewSource = readFileSync(resolve(process.cwd(), 'app/overview/overview-page.tsx'), 'utf8');
     const logSource = readFileSync(resolve(process.cwd(), 'app/log/manage/log-manage-page.tsx'), 'utf8');
-    const traceSource = readFileSync(resolve(process.cwd(), 'app/trace/manage/page.tsx'), 'utf8');
-    const otlpSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/page.tsx'), 'utf8');
-    const metricsSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/page.tsx'), 'utf8');
+    const traceSource = readFileSync(resolve(process.cwd(), 'app/trace/manage/trace-manage-page.tsx'), 'utf8');
+    const otlpSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/otlp-page.tsx'), 'utf8');
+    const metricsSource = readFileSync(resolve(process.cwd(), 'app/ingestion/otlp/metrics/otlp-metrics-page.tsx'), 'utf8');
 
     expect(overviewSource).not.toContain('text-white/44');
     expect(overviewSource).not.toContain('#f3eee6');
diff --git a/web-next/lib/dashboard/navigation.test.ts b/web-next/lib/dashboard/navigation.test.ts
index 6692b59427..d650d9df0c 100644
--- a/web-next/lib/dashboard/navigation.test.ts
+++ b/web-next/lib/dashboard/navigation.test.ts
@@ -31,6 +31,10 @@ describe('dashboard navigation', () => {
     expect(source).not.toContain('sanitizeDashboardSearchParams');
     expect(source).not.toContain('delete next.returnLabel');
     expect(source).not.toContain('stripReturnLabelFromHref');
-    expect(source).toContain("buildCompatRedirectTarget('/overview', searchParams)");
+    expect(source).toContain("import { buildOverviewCompatRouteUrl, type SearchParamsRecord } from '../overview/navigation'");
+    expect(source).toContain('export type { SearchParamsRecord }');
+    expect(source).toContain('return buildOverviewCompatRouteUrl(searchParams)');
+    expect(source).not.toContain("buildCompatRedirectTarget('/overview'");
+    expect(source).not.toContain("'/overview'");
   });
 });
diff --git a/web-next/lib/dashboard/navigation.ts b/web-next/lib/dashboard/navigation.ts
index a443d93859..a227a352f4 100644
--- a/web-next/lib/dashboard/navigation.ts
+++ b/web-next/lib/dashboard/navigation.ts
@@ -1,5 +1,7 @@
-import { buildCompatRedirectTarget, type SearchParamsRecord } from '../compat/search-params';
+import { buildOverviewCompatRouteUrl, type SearchParamsRecord } from '../overview/navigation';
+
+export type { SearchParamsRecord };
 
 export function buildDashboardCompatRouteUrl(searchParams?: SearchParamsRecord) {
-  return buildCompatRedirectTarget('/overview', searchParams);
+  return buildOverviewCompatRouteUrl(searchParams);
 }
diff --git a/web-next/lib/nav.test.ts b/web-next/lib/nav.test.ts
index 175e952ba9..3944b47f7b 100644
--- a/web-next/lib/nav.test.ts
+++ b/web-next/lib/nav.test.ts
@@ -89,7 +89,9 @@ describe('navigation information architecture', () => {
     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(placeholderRoutes.map(route => route.href)).toEqual(['/incidents', '/actions', '/explorer']);
+    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']);
   });
 
   it('keeps legacy aliases and route-matrix targets in the route contract', () => {
diff --git a/web-next/lib/nav.ts b/web-next/lib/nav.ts
index 63315350e4..eaac230991 100644
--- a/web-next/lib/nav.ts
+++ b/web-next/lib/nav.ts
@@ -74,7 +74,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.entity.discovery',
     label: 'Discovery',
     href: '/entities/discovery',
-    icon: 'entities',
+    icon: 'entity-discovery',
     navSectionKey: 'objects',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -86,7 +86,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.entity.definition',
     label: 'Definitions',
     href: '/entities/import',
-    icon: 'entities',
+    icon: 'entity-definition',
     navSectionKey: 'objects',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -163,7 +163,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.alert.integration',
     label: 'Integrations',
     href: '/alert/integration/[source]',
-    icon: 'alert',
+    icon: 'alert-integration',
     navSectionKey: 'alerting',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -175,7 +175,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.alert.group',
     label: 'Grouping',
     href: '/alert/group',
-    icon: 'alert',
+    icon: 'alert-group',
     navSectionKey: 'alerting',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -187,7 +187,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.alert.inhibit',
     label: 'Inhibit rules',
     href: '/alert/inhibit',
-    icon: 'alert',
+    icon: 'alert-inhibit',
     navSectionKey: 'alerting',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -199,7 +199,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.alert.silence',
     label: 'Silence rules',
     href: '/alert/silence',
-    icon: 'alert',
+    icon: 'alert-silence',
     navSectionKey: 'alerting',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -213,7 +213,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     href: '/incidents',
     icon: 'incidents',
     routeKind: 'primary',
-    cutoverStatus: 'placeholder',
+    cutoverStatus: 'candidate',
     smokePath: '/incidents',
     includeInRouteMatrix: false
   },
@@ -233,7 +233,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.alert.dispatch',
     label: 'Notifications',
     href: '/alert/notice',
-    icon: 'alert',
+    icon: 'alert-notice',
     navSectionKey: 'alerting',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -319,7 +319,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     label: 'Explorer',
     href: '/explorer',
     routeKind: 'primary',
-    cutoverStatus: 'placeholder',
+    cutoverStatus: 'candidate',
     smokePath: '/explorer',
     includeInRouteMatrix: false
   },
@@ -381,7 +381,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.monitor.collector',
     label: 'Collector',
     href: '/setting/collector',
-    icon: 'monitor',
+    icon: 'collector',
     navSectionKey: 'ingestion',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -393,7 +393,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.monitor.template',
     label: 'Definitions',
     href: '/setting/define',
-    icon: 'monitor',
+    icon: 'monitor-template',
     navSectionKey: 'ingestion',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -405,7 +405,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.monitor.bulletin',
     label: 'Bulletin',
     href: '/bulletin',
-    icon: 'bulletin',
+    icon: 'alert-bulletin',
     navSectionKey: 'alerting',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -431,7 +431,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.advanced.mcp-server',
     label: 'MCP Server',
     href: '/setting/settings/mcp-server',
-    icon: 'settings',
+    icon: 'mcp-server',
     navSectionKey: 'settings',
     includeInNavigation: true,
     routeKind: 'legacy-alias',
@@ -468,7 +468,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.advanced.labels',
     label: 'Labels',
     href: '/setting/labels',
-    icon: 'settings',
+    icon: 'token',
     navSectionKey: 'settings',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -480,7 +480,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.advanced.plugins',
     label: 'Plugins',
     href: '/setting/plugins',
-    icon: 'settings',
+    icon: 'plugins',
     navSectionKey: 'settings',
     routeKind: 'primary',
     cutoverStatus: 'candidate',
@@ -492,7 +492,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     labelKey: 'menu.extras.help',
     label: 'Help center',
     href: 'https://hertzbeat.apache.org/docs/',
-    icon: 'settings',
+    icon: 'help',
     navSectionKey: 'settings',
     includeInNavigation: true,
     routeKind: 'primary',
diff --git a/web-next/lib/parity/route-manifest.json b/web-next/lib/parity/route-manifest.json
index e89f3bf5bf..87baa4905d 100644
--- a/web-next/lib/parity/route-manifest.json
+++ b/web-next/lib/parity/route-manifest.json
@@ -853,7 +853,7 @@
           "[data-entity-list-table-shell=\"cold-dense-table\"]",
           "[data-entity-list-table=\"cold-entity-table\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input",
@@ -897,25 +897,25 @@
           "[data-entity-detail-drilldown-panel=\"cold-drilldown-panel\"]",
           "[data-entity-detail-error=\"cold-inline-error\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "a",
           "button"
         ],
         "textSnippets": [
-          "实体详情",
-          "对象优先调查",
-          "上下文",
-          "相关信号",
-          "下一步",
-          "高级入口"
+          "Entity detail",
+          "Entity-first investigation",
+          "Context",
+          "Related signals",
+          "Next step",
+          "Advanced entries"
         ],
         "actionLabels": [
-          "全部实体",
-          "刷新",
-          "编辑定义",
-          "删除",
-          "编辑"
+          "All entities",
+          "Refresh",
+          "Edit definition",
+          "Delete",
+          "Edit"
         ],
         "minimumVerificationCommand": "npm exec vitest run 'app/entities/[entityId]/page.test.tsx' components/pages/entity-detail-surface.test.tsx app/entity-detail-family.chrome.test.ts lib/entity-detail/view-model.test.ts lib/parity/route-manifest.test.ts"
       },
@@ -948,7 +948,7 @@
           "[data-entity-type-icon=\"service\"]",
           "[data-entity-type-icon=\"database\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "form",
           "input",
           "button"
@@ -1000,7 +1000,7 @@
           "[data-entity-type-icon=\"service\"]",
           "[data-entity-type-icon=\"database\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "form",
           "input",
           "button"
@@ -1042,7 +1042,7 @@
           "[data-entity-definition-template-panel=\"true\"]",
           "[data-entity-definition-batch-panel=\"true\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "textarea",
           "button"
@@ -1084,7 +1084,7 @@
           "[data-entity-discovery-table=\"cold-discovery-table\"]",
           "[data-entity-discovery-row-actions=\"cold-inline-actions\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input",
@@ -1133,7 +1133,7 @@
           "[data-entity-definition-load-error=\"cold-inline\"]",
           "[data-entity-definition-error-placement=\"cold-context-panel\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "textarea",
           "button",
@@ -1185,7 +1185,7 @@
           "[data-alert-center-empty-state=\"cold-table-empty\"]",
           "[data-alert-center-empty-icon=\"cold-empty-box\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "input",
           "select",
@@ -1287,7 +1287,7 @@
           "[data-alert-group-empty-state=\"cold-table-empty\"]",
           "[data-alert-group-empty-icon=\"cold-empty-box\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input"
@@ -1325,7 +1325,7 @@
           "[data-alert-silence-empty-state=\"cold-table-empty\"]",
           "[data-alert-silence-empty-icon=\"cold-empty-box\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input"
@@ -1364,7 +1364,7 @@
           "[data-alert-inhibit-empty-state=\"cold-table-empty\"]",
           "[data-alert-inhibit-empty-icon=\"cold-empty-box\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input"
@@ -1403,7 +1403,7 @@
           "[data-alert-notice-receiver-toolbar=\"cold-query-toolbar\"]",
           "[data-alert-notice-receiver-table-shell=\"cold-dense-table\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "[data-tab=\"receiver\"]",
           "main",
           "button",
@@ -1456,7 +1456,7 @@
           "[data-alert-notice-rule-toolbar=\"cold-query-toolbar\"]",
           "[data-alert-notice-rule-table-shell=\"cold-dense-table\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input"
@@ -1510,7 +1510,7 @@
           "[data-alert-notice-template-table-shell=\"cold-dense-table\"]",
           "[data-alert-notice-pagination=\"cold-dense-pagination\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input",
@@ -1553,7 +1553,7 @@
           "[data-alert-setting-empty-state=\"cold-table-empty\"]",
           "[data-alert-setting-empty-icon=\"cold-empty-box\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input"
@@ -1594,7 +1594,7 @@
           "[data-alert-integration-code-block=\"json\"]",
           "[data-alert-integration-mermaid]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button"
         ],
@@ -1642,7 +1642,7 @@
           "[data-setting-config-form=\"cold-settings-form\"]",
           "[data-setting-config-actions=\"standard-equal-buttons\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "form",
           "select",
           "button"
@@ -1748,7 +1748,7 @@
           "[data-setting-config-form=\"cold-settings-form\"]",
           "[data-setting-config-actions=\"standard-equal-buttons\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "form",
           "select",
           "button"
@@ -1787,7 +1787,7 @@
           "[data-setting-object-store-provider=\"cold-provider-select\"]",
           "[data-setting-object-store-actions=\"standard-equal-buttons\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "form",
           "select",
           "button"
@@ -1830,7 +1830,7 @@
           "[data-settings-summary-action=\"email\"]",
           "[data-settings-summary-action=\"sms\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button"
         ],
@@ -1874,7 +1874,7 @@
           "[data-setting-token-table-panel=\"cold-dense-table\"]",
           "[data-setting-token-table=\"cold-token-table\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "table"
@@ -1958,15 +1958,14 @@
           "[data-setting-define-menu-shell=\"cold-dense-list\"]",
           "[data-setting-define-editor=\"cold-settings-form\"]",
           "[data-setting-define-editor-shell=\"cold-yaml-editor\"]",
-          "[data-setting-define-editor-field=\"cold-code-textarea\"]",
+          "[data-setting-define-editor-field=\"cold-code-editor\"]",
+          "[data-setting-define-code-editor=\"monitor-template-yaml\"]",
           "button",
-          "input",
-          "textarea"
+          "input"
         ],
         "textSnippets": [
           "定义",
-          "管理监控类型定义",
-          "数据源状态"
+          "管理监控模板 YAML"
         ],
         "actionLabels": [
           "新增类型",
@@ -1974,7 +1973,8 @@
           "取消",
           "保存并应用",
           "搜索",
-          "预览查询"
+          "隐藏",
+          "显示"
         ],
         "minimumVerificationCommand": "npm exec vitest run app/setting/define/page.test.tsx components/pages/setting-define-surface.test.tsx lib/setting-define/controller.test.ts lib/setting-define/query-state.test.ts lib/setting-define/view-model.test.ts lib/parity/route-manifest.test.ts"
       },
@@ -1997,7 +1997,7 @@
           "[data-label-table=\"cold-label-table\"]",
           "[data-label-empty-state=\"cold-table-empty\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input",
@@ -2037,7 +2037,7 @@
           "[data-plugin-manage-table=\"cold-plugin-table\"]",
           "[data-plugin-manage-empty-state=\"cold-table-empty\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input",
@@ -2077,7 +2077,7 @@
           "[data-status-component-table-shell=\"cold-dense-table\"]",
           "[data-status-component-table=\"cold-component-table\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "button",
           "input",
@@ -2294,8 +2294,8 @@
     "key": "compatibility-placeholder-family",
     "milestone": 5,
     "legacyArea": "compatibility-and-placeholder",
-    "summary": "Close compatibility aliases plus placeholder twins in the final parity sweep.",
-    "familyVerificationCommand": "npm exec vitest run app/dashboard/page.test.ts app/overview/page.test.tsx lib/overview/navigation.test.ts lib/overview/view-model.test.ts app/events/page.test.ts app/log/manage/page.test.tsx lib/log-manage/query-state.test.ts lib/log-manage/view-model.test.ts app/alerts/page.test.ts app/alert/page.test.tsx components/pages/alert-center-surface.test.tsx lib/alert-manage/query-state.test.ts lib/alert-manage/controller.test.ts lib/alert-manage/view-model.te [...]
+    "summary": "Close compatibility aliases plus placeholder twins and the unified Explorer workbench in the final parity sweep.",
+    "familyVerificationCommand": "npm exec vitest run app/dashboard/page.test.ts app/overview/page.test.tsx lib/overview/navigation.test.ts lib/overview/view-model.test.ts app/events/page.test.ts app/log/manage/page.test.tsx lib/log-manage/query-state.test.ts lib/log-manage/view-model.test.ts app/alerts/page.test.ts app/alert/page.test.tsx components/pages/alert-center-surface.test.tsx lib/alert-manage/query-state.test.ts lib/alert-manage/controller.test.ts lib/alert-manage/view-model.te [...]
     "referenceBaseUrl": "http://127.0.0.1:4301",
     "nextBaseUrl": "http://127.0.0.1:4200",
     "routePairs": [
@@ -2383,7 +2383,7 @@
           "[data-alert-center-empty-state=\"cold-table-empty\"]",
           "[data-alert-center-empty-icon=\"cold-empty-box\"]",
           "[data-platform-footer=\"angular-footer\"]",
-          "[data-shell-help-launcher=\"angular-help\"]",
+          "[data-shell-ai-chat-launcher=\"angular-ai-chat\"]",
           "main",
           "input",
           "select",
@@ -2446,8 +2446,22 @@
         "primarySelectors": [
           "[data-actions-route=\"otlp-cold-ops-entry\"]",
           "[data-actions-style-baseline=\"hertzbeat-cold-matte\"]",
+          "[data-actions-shared-workbench=\"hertzbeat-ui\"]",
+          "[data-hz-action-workbench-owner=\"hertzbeat-ui-action-workbench\"]",
           "[data-actions-shell-panel=\"cold-ops-shell-panel\"]",
           "[data-actions-launch-checklist=\"cold-ops-static-rail\"]",
+          "[data-actions-catalog=\"manual-action-catalog-api\"]",
+          "[data-actions-catalog-owner=\"next-actions-catalog-bff\"]",
+          "[data-actions-catalog-execution-allowed=\"false\"]",
+          "[data-actions-approval-draft=\"manual-approval-draft-api\"]",
+          "[data-actions-approval-draft-owner=\"next-actions-approval-draft-bff\"]",
+          "[data-actions-approval-draft-execution-allowed=\"false\"]",
+          "[data-actions-approval-draft-queue=\"manual-approval-draft-read-api\"]",
+          "[data-actions-approval-draft-queue-owner=\"next-actions-approval-draft-bff\"]",
+          "[data-actions-approval-draft-queue-execution-allowed=\"false\"]",
+          "[data-actions-approval-decision=\"manual-approval-decision-api\"]",
+          "[data-actions-approval-decision-owner=\"next-actions-approval-decision-bff\"]",
+          "[data-actions-approval-decision-execution-allowed=\"false\"]",
           "[data-actions-empty-state=\"cold-ops-domain-adapter\"]",
           "main",
           "section",
@@ -2465,7 +2479,7 @@
           "打开概览",
           "查看对象"
         ],
-        "minimumVerificationCommand": "npm exec vitest run app/actions/page.test.tsx lib/actions-surface/view-model.test.ts lib/actions-surface/model.test.ts lib/parity/route-manifest.test.ts"
+        "minimumVerificationCommand": "npm exec vitest run app/actions/page.test.tsx app/api/actions/approval-drafts/route.test.ts app/api/actions/approval-drafts/[draftId]/decision/route.test.ts app/api/actions/catalog/route.test.ts lib/actions-surface/view-model.test.ts lib/actions-surface/model.test.ts lib/parity/route-manifest.test.ts"
       },
       {
         "key": "topology-placeholder",
@@ -2500,7 +2514,7 @@
         "minimumVerificationCommand": "npm exec vitest run app/topology/page.test.tsx lib/topology-surface/view-model.test.ts lib/parity/route-manifest.test.ts"
       },
       {
-        "key": "explorer-placeholder",
+        "key": "explorer-workbench",
         "nextRoute": "/explorer",
         "referenceRoute": "/explorer",
         "nextPagePath": "web-next/app/explorer/page.tsx",
@@ -2511,9 +2525,16 @@
         "primarySelectors": [
           "[data-explorer-route=\"otlp-cold-workbench\"]",
           "[data-explorer-style-baseline=\"hertzbeat-cold-matte\"]",
+          "[data-explorer-api-owner=\"trace-log-bff-query-api\"]",
+          "[data-explorer-query-state]",
+          "[data-explorer-signal-filter]",
+          "[data-explorer-api-source]",
+          "[data-explorer-shared-frame=\"hertzbeat-ui\"]",
+          "[data-hz-ui=\"explorer-frame\"]",
           "[data-explorer-query-bar=\"cold-query-row\"]",
           "[data-explorer-chart-band=\"cold-chart-band\"]",
           "[data-explorer-result-table=\"cold-dense-table\"]",
+          "[data-explorer-result-table-owner=\"hertzbeat-ui-data-table\"]",
           "[data-explorer-detail-panel=\"cold-detail-panel\"]",
           "main",
           "button",
@@ -2532,7 +2553,7 @@
           "创建告警",
           "加入仪表盘"
         ],
-        "minimumVerificationCommand": "npm exec vitest run app/explorer/page.test.tsx lib/explorer-surface/view-model.test.ts lib/parity/route-manifest.test.ts"
+        "minimumVerificationCommand": "npm exec vitest run app/explorer/page.test.tsx lib/explorer-surface/controller.test.ts lib/explorer-surface/view-model.test.ts lib/otlp-metrics/controller.test.ts lib/otlp-metrics/view-model.test.ts lib/parity/route-manifest.test.ts"
       }
     ]
   }
diff --git a/web-next/lib/parity/route-manifest.test.ts b/web-next/lib/parity/route-manifest.test.ts
index 92a2fee26a..a8909d7f32 100644
--- a/web-next/lib/parity/route-manifest.test.ts
+++ b/web-next/lib/parity/route-manifest.test.ts
@@ -139,7 +139,7 @@ describe('parity route manifest', () => {
         '[data-alert-center-empty-state="cold-table-empty"]',
         '[data-alert-center-empty-icon="cold-empty-box"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'input',
         'select',
@@ -187,8 +187,22 @@ describe('parity route manifest', () => {
       primarySelectors: expect.arrayContaining([
         '[data-actions-route="otlp-cold-ops-entry"]',
         '[data-actions-style-baseline="hertzbeat-cold-matte"]',
+        '[data-actions-shared-workbench="hertzbeat-ui"]',
+        '[data-hz-action-workbench-owner="hertzbeat-ui-action-workbench"]',
         '[data-actions-shell-panel="cold-ops-shell-panel"]',
         '[data-actions-launch-checklist="cold-ops-static-rail"]',
+        '[data-actions-catalog="manual-action-catalog-api"]',
+        '[data-actions-catalog-owner="next-actions-catalog-bff"]',
+        '[data-actions-catalog-execution-allowed="false"]',
+        '[data-actions-approval-draft="manual-approval-draft-api"]',
+        '[data-actions-approval-draft-owner="next-actions-approval-draft-bff"]',
+        '[data-actions-approval-draft-execution-allowed="false"]',
+        '[data-actions-approval-draft-queue="manual-approval-draft-read-api"]',
+        '[data-actions-approval-draft-queue-owner="next-actions-approval-draft-bff"]',
+        '[data-actions-approval-draft-queue-execution-allowed="false"]',
+        '[data-actions-approval-decision="manual-approval-decision-api"]',
+        '[data-actions-approval-decision-owner="next-actions-approval-decision-bff"]',
+        '[data-actions-approval-decision-execution-allowed="false"]',
         '[data-actions-empty-state="cold-ops-domain-adapter"]',
         'main',
         'section',
@@ -198,7 +212,7 @@ describe('parity route manifest', () => {
       textSnippets: expect.arrayContaining(['自动化处置', '按 OTLP 工作台的冷色基线', '冷色入口已接入', '等待接入执行适配器', '你已完成 83% 的平台配置']),
       actionLabels: ['打开概览', '查看对象'],
       minimumVerificationCommand:
-        'npm exec vitest run app/actions/page.test.tsx lib/actions-surface/view-model.test.ts lib/actions-surface/model.test.ts lib/parity/route-manifest.test.ts'
+        'npm exec vitest run app/actions/page.test.tsx app/api/actions/approval-drafts/route.test.ts app/api/actions/approval-drafts/[draftId]/decision/route.test.ts app/api/actions/catalog/route.test.ts lib/actions-surface/view-model.test.ts lib/actions-surface/model.test.ts lib/parity/route-manifest.test.ts'
     });
   });
 
@@ -223,15 +237,22 @@ describe('parity route manifest', () => {
     });
   });
 
-  it('tracks the OTLP cold unified explorer instead of the old external-product-first surface', () => {
-    expect(getParityRoutePair('compatibility-placeholder-family', 'explorer-placeholder')).toMatchObject({
+  it('tracks the OTLP cold unified explorer as an API/query-backed workbench instead of the old external-product-first surface', () => {
+    expect(getParityRoutePair('compatibility-placeholder-family', 'explorer-workbench')).toMatchObject({
       screenshotMode: 'viewport',
       primarySelectors: expect.arrayContaining([
         '[data-explorer-route="otlp-cold-workbench"]',
         '[data-explorer-style-baseline="hertzbeat-cold-matte"]',
+        '[data-explorer-api-owner="trace-log-bff-query-api"]',
+        '[data-explorer-query-state]',
+        '[data-explorer-signal-filter]',
+        '[data-explorer-api-source]',
+        '[data-explorer-shared-frame="hertzbeat-ui"]',
+        '[data-hz-ui="explorer-frame"]',
         '[data-explorer-query-bar="cold-query-row"]',
         '[data-explorer-chart-band="cold-chart-band"]',
         '[data-explorer-result-table="cold-dense-table"]',
+        '[data-explorer-result-table-owner="hertzbeat-ui-data-table"]',
         '[data-explorer-detail-panel="cold-detail-panel"]',
         'main',
         'button',
@@ -241,9 +262,9 @@ describe('parity route manifest', () => {
       textSnippets: expect.arrayContaining(['查询工作台', '信号类型', '运行查询', 'checkout']),
       actionLabels: ['运行查询', '保存视图', '创建告警', '加入仪表盘'],
       minimumVerificationCommand:
-        'npm exec vitest run app/explorer/page.test.tsx lib/explorer-surface/view-model.test.ts lib/parity/route-manifest.test.ts'
+        'npm exec vitest run app/explorer/page.test.tsx lib/explorer-surface/controller.test.ts lib/explorer-surface/view-model.test.ts lib/otlp-metrics/controller.test.ts lib/otlp-metrics/view-model.test.ts lib/parity/route-manifest.test.ts'
     });
-    expect(getParityRoutePair('compatibility-placeholder-family', 'explorer-placeholder').primarySelectors.join(' ')).not.toContain('signoz-');
+    expect(getParityRoutePair('compatibility-placeholder-family', 'explorer-workbench').primarySelectors.join(' ')).not.toContain('signoz-');
   });
 
   it('declares explicit parity owners for the milestone 2 families', () => {
@@ -290,9 +311,9 @@ describe('parity route manifest', () => {
     );
   });
 
-  it('declares a family-closeout verification command for the compatibility aliases and placeholder twins', () => {
+  it('declares a family-closeout verification command for compatibility aliases, placeholder twins, and Explorer', () => {
     expect((getParityFamily('compatibility-placeholder-family') as Record<string, unknown>).familyVerificationCommand).toBe(
-      'npm exec vitest run app/dashboard/page.test.ts app/overview/page.test.tsx lib/overview/navigation.test.ts lib/overview/view-model.test.ts app/events/page.test.ts app/log/manage/page.test.tsx lib/log-manage/query-state.test.ts lib/log-manage/view-model.test.ts app/alerts/page.test.ts app/alert/page.test.tsx components/pages/alert-center-surface.test.tsx lib/alert-manage/query-state.test.ts lib/alert-manage/controller.test.ts lib/alert-manage/view-model.test.ts app/incidents/page.te [...]
+      'npm exec vitest run app/dashboard/page.test.ts app/overview/page.test.tsx lib/overview/navigation.test.ts lib/overview/view-model.test.ts app/events/page.test.ts app/log/manage/page.test.tsx lib/log-manage/query-state.test.ts lib/log-manage/view-model.test.ts app/alerts/page.test.ts app/alert/page.test.tsx components/pages/alert-center-surface.test.tsx lib/alert-manage/query-state.test.ts lib/alert-manage/controller.test.ts lib/alert-manage/view-model.test.ts app/incidents/page.te [...]
     );
   });
 
@@ -741,7 +762,7 @@ describe('parity route manifest', () => {
         '[data-entity-list-table-shell="cold-dense-table"]',
         '[data-entity-list-table="cold-entity-table"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input',
@@ -788,7 +809,7 @@ describe('parity route manifest', () => {
         '[data-entity-type-icon="service"]',
         '[data-entity-type-icon="database"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]'
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]'
       ]),
       textSnippets: expect.arrayContaining(['新建实体', '全部实体', '实体元数据', '页面录入', '基本信息', '你已完成 83% 的平台配置']),
       actionLabels: ['创建实体']
@@ -826,7 +847,7 @@ describe('parity route manifest', () => {
         '[data-entity-type-icon="service"]',
         '[data-entity-type-icon="database"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]'
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]'
       ]),
       textSnippets: expect.arrayContaining(['编辑实体', '全部实体', '实体元数据', '基本信息', '你已完成 0% 的平台配置']),
       actionLabels: ['保存']
@@ -859,7 +880,7 @@ describe('parity route manifest', () => {
         '[data-entity-definition-template-panel="true"]',
         '[data-entity-definition-batch-panel="true"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'textarea',
         'button'
@@ -898,7 +919,7 @@ describe('parity route manifest', () => {
         '[data-entity-definition-load-error="cold-inline"]',
         '[data-entity-definition-error-placement="cold-context-panel"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'textarea',
         'button',
@@ -945,7 +966,7 @@ describe('parity route manifest', () => {
         '[data-entity-discovery-table="cold-discovery-table"]',
         '[data-entity-discovery-row-actions="cold-inline-actions"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input',
@@ -987,21 +1008,28 @@ describe('parity route manifest', () => {
         '[data-entity-detail-drilldown-panel="cold-drilldown-panel"]',
         '[data-entity-detail-error="cold-inline-error"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'a',
         'button'
       ]),
-      textSnippets: expect.arrayContaining(['实体详情', '对象优先调查', '上下文', '相关信号', '下一步', '高级入口']),
-      actionLabels: ['全部实体', '刷新', '编辑定义', '删除', '编辑'],
+      textSnippets: expect.arrayContaining([
+        'Entity detail',
+        'Entity-first investigation',
+        'Context',
+        'Related signals',
+        'Next step',
+        'Advanced entries'
+      ]),
+      actionLabels: ['All entities', 'Refresh', 'Edit definition', 'Delete', 'Edit'],
       minimumVerificationCommand:
         "npm exec vitest run 'app/entities/[entityId]/page.test.tsx' components/pages/entity-detail-surface.test.tsx app/entity-detail-family.chrome.test.ts lib/entity-detail/view-model.test.ts lib/parity/route-manifest.test.ts"
     });
     expect(getParityRoutePair('entity-family', 'entity-detail').primarySelectors).not.toContain('aside');
-    expect(getParityRoutePair('entity-family', 'entity-detail').textSnippets).not.toContain('Entity');
-    expect(getParityRoutePair('entity-family', 'entity-detail').textSnippets).not.toContain('Next steps');
-    expect(getParityRoutePair('entity-family', 'entity-detail').actionLabels).not.toContain('Refresh');
-    expect(getParityRoutePair('entity-family', 'entity-detail').actionLabels).not.toContain('Edit definition');
+    expect(getParityRoutePair('entity-family', 'entity-detail').textSnippets).not.toContain('实体详情');
+    expect(getParityRoutePair('entity-family', 'entity-detail').textSnippets).not.toContain('对象优先调查');
+    expect(getParityRoutePair('entity-family', 'entity-detail').actionLabels).not.toContain('刷新');
+    expect(getParityRoutePair('entity-family', 'entity-detail').actionLabels).not.toContain('编辑定义');
   });
 
   it('tracks the alert center through targeted route, shared-surface, and filter contracts', () => {
@@ -1019,7 +1047,7 @@ describe('parity route manifest', () => {
         '[data-alert-center-empty-state="cold-table-empty"]',
         '[data-alert-center-empty-icon="cold-empty-box"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'input',
         'select',
@@ -1053,7 +1081,7 @@ describe('parity route manifest', () => {
         '[data-alert-group-empty-state="cold-table-empty"]',
         '[data-alert-group-empty-icon="cold-empty-box"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input'
@@ -1115,7 +1143,7 @@ describe('parity route manifest', () => {
         '[data-alert-notice-receiver-toolbar="cold-query-toolbar"]',
         '[data-alert-notice-receiver-table-shell="cold-dense-table"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         '[data-tab="receiver"]',
         'main',
         'button',
@@ -1148,7 +1176,7 @@ describe('parity route manifest', () => {
         '[data-alert-notice-rule-toolbar="cold-query-toolbar"]',
         '[data-alert-notice-rule-table-shell="cold-dense-table"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input'
@@ -1179,7 +1207,7 @@ describe('parity route manifest', () => {
         '[data-alert-notice-template-table-shell="cold-dense-table"]',
         '[data-alert-notice-pagination="cold-dense-pagination"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input',
@@ -1217,7 +1245,7 @@ describe('parity route manifest', () => {
         '[data-alert-setting-empty-state="cold-table-empty"]',
         '[data-alert-setting-empty-icon="cold-empty-box"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input'
@@ -1252,7 +1280,7 @@ describe('parity route manifest', () => {
         '[data-alert-integration-code-block="json"]',
         '[data-alert-integration-mermaid]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button'
       ]),
@@ -1282,7 +1310,7 @@ describe('parity route manifest', () => {
         '[data-alert-silence-empty-state="cold-table-empty"]',
         '[data-alert-silence-empty-icon="cold-empty-box"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input'
@@ -1314,7 +1342,7 @@ describe('parity route manifest', () => {
         '[data-alert-inhibit-empty-state="cold-table-empty"]',
         '[data-alert-inhibit-empty-icon="cold-empty-box"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input'
@@ -1346,7 +1374,7 @@ describe('parity route manifest', () => {
         '[data-setting-config-form="cold-settings-form"]',
         '[data-setting-config-actions="standard-equal-buttons"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'form',
         'select',
         'button'
@@ -1378,7 +1406,7 @@ describe('parity route manifest', () => {
         '[data-setting-config-form="cold-settings-form"]',
         '[data-setting-config-actions="standard-equal-buttons"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'form',
         'select',
         'button'
@@ -1418,7 +1446,7 @@ describe('parity route manifest', () => {
         '[data-setting-object-store-provider="cold-provider-select"]',
         '[data-setting-object-store-actions="standard-equal-buttons"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'form',
         'select',
         'button'
@@ -1464,7 +1492,7 @@ describe('parity route manifest', () => {
         '[data-settings-summary-action="email"]',
         '[data-settings-summary-action="sms"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button'
       ]),
@@ -1510,7 +1538,7 @@ describe('parity route manifest', () => {
         '[data-setting-token-table-panel="cold-dense-table"]',
         '[data-setting-token-table="cold-token-table"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'table'
@@ -1586,14 +1614,14 @@ describe('parity route manifest', () => {
         '[data-setting-define-menu-shell="cold-dense-list"]',
         '[data-setting-define-editor="cold-settings-form"]',
         '[data-setting-define-editor-shell="cold-yaml-editor"]',
-        '[data-setting-define-editor-field="cold-code-textarea"]',
+        '[data-setting-define-editor-field="cold-code-editor"]',
+        '[data-setting-define-code-editor="monitor-template-yaml"]',
         'main',
         'button',
-        'input',
-        'textarea'
+        'input'
       ]),
-      textSnippets: expect.arrayContaining(['定义', '管理监控类型定义', '数据源状态']),
-      actionLabels: ['新增类型', '编辑', '取消', '保存并应用', '搜索', '预览查询'],
+      textSnippets: expect.arrayContaining(['定义', '管理监控模板 YAML']),
+      actionLabels: ['新增类型', '编辑', '取消', '保存并应用', '搜索', '隐藏', '显示'],
       minimumVerificationCommand:
         'npm exec vitest run app/setting/define/page.test.tsx components/pages/setting-define-surface.test.tsx lib/setting-define/controller.test.ts lib/setting-define/query-state.test.ts lib/setting-define/view-model.test.ts lib/parity/route-manifest.test.ts'
     });
@@ -1601,6 +1629,8 @@ describe('parity route manifest', () => {
     expect(getParityRoutePair('setting-family', 'setting-define').primarySelectors).not.toContain('[data-setting-define-summary-rail="cold-static-rail"]');
     expect(getParityRoutePair('setting-family', 'setting-define').textSnippets).not.toContain('Define');
     expect(getParityRoutePair('setting-family', 'setting-define').actionLabels).not.toContain('New Monitor Type');
+    expect(getParityRoutePair('setting-family', 'setting-define').textSnippets).not.toContain('数据源状态');
+    expect(getParityRoutePair('setting-family', 'setting-define').actionLabels).not.toContain('预览查询');
   });
 
   it('pins label management to the OTLP cold-matte dense admin/list contract', () => {
@@ -1618,7 +1648,7 @@ describe('parity route manifest', () => {
         '[data-label-table="cold-label-table"]',
         '[data-label-empty-state="cold-table-empty"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input',
@@ -1654,7 +1684,7 @@ describe('parity route manifest', () => {
         '[data-plugin-manage-table="cold-plugin-table"]',
         '[data-plugin-manage-empty-state="cold-table-empty"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input',
@@ -1689,7 +1719,7 @@ describe('parity route manifest', () => {
         '[data-status-component-table-shell="cold-dense-table"]',
         '[data-status-component-table="cold-component-table"]',
         '[data-platform-footer="angular-footer"]',
-        '[data-shell-help-launcher="angular-help"]',
+        '[data-shell-ai-chat-launcher="angular-ai-chat"]',
         'main',
         'button',
         'input',
diff --git a/web-next/lib/workspace-navigation.test.ts b/web-next/lib/workspace-navigation.test.ts
index 6832203d50..3932cbd8ab 100644
--- a/web-next/lib/workspace-navigation.test.ts
+++ b/web-next/lib/workspace-navigation.test.ts
@@ -1,4 +1,5 @@
 import { describe, expect, it } from 'vitest';
+import { createTranslatorMock } from '../test/i18n-test-helper';
 import {
   buildEntityEditorWorkspaceTabs,
   buildEntitySignalRouteContext,
@@ -7,7 +8,7 @@ import {
   buildSignalWorkspaceTabs
 } from './workspace-navigation';
 
-const t = (key: string) => key;
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('workspace navigation', () => {
   it('prefers the explicit return path for entity workspace navigation', () => {
@@ -61,6 +62,7 @@ describe('workspace navigation', () => {
     const ingestionTab = tabs.find(tab => tab.key === 'ingestion');
     const monitorsTab = tabs.find(tab => tab.key === 'monitors');
 
+    expect(tabs.map(tab => tab.label)).toEqual(['实体详情', '可观测接入', '指标工作台', '日志工作台', '链路工作台', '监控中心']);
     expect(logsTab?.href).toContain('/log/manage?');
     expect(logsTab?.href).toContain('traceId=trace-123');
     expect(logsTab?.href).toContain('severityText=ERROR');
@@ -139,6 +141,7 @@ describe('workspace navigation', () => {
     });
 
     expect(tabs.map(tab => tab.key)).toEqual(['entity', 'metrics', 'monitors', 'logs', 'traces']);
+    expect(tabs.map(tab => tab.label)).toEqual(['实体详情', '指标工作台', '监控中心', '日志工作台', '链路工作台']);
     expect(tabs[2]?.href).toContain('/monitors?');
     expect(tabs[2]?.href).toContain('entityId=7');
     expect(tabs[3]?.href).toContain('/log/manage?');
@@ -153,6 +156,7 @@ describe('workspace navigation', () => {
     });
 
     expect(tabs.map(tab => tab.key)).toEqual(['entity', 'monitors', 'logs', 'traces']);
+    expect(tabs.map(tab => tab.label)).toEqual(['实体详情', '监控中心', '日志工作台', '链路工作台']);
     expect(tabs.find(tab => tab.key === 'entity')?.active).toBe(true);
     expect(tabs.find(tab => tab.key === 'monitors')?.disabled).toBe(true);
     expect(tabs.find(tab => tab.key === 'logs')?.disabled).toBe(true);


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 49/50: refactor: externalize backend localization messages

Posted by zh...@apache.org.
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 3832485f0fe73bca9ebd2f9db86797e970fa3efa
Author: Logic <zq...@dromara.org>
AuthorDate: Wed Jun 3 11:40:48 2026 +0800

    refactor: externalize backend localization messages
---
 .../impl/FeiShuAppAlertNotifyHandlerImpl.java      |  14 +-
 .../notice/impl/FlyBookAlertNotifyHandlerImpl.java |  15 +-
 .../impl/FeiShuAppAlertNotifyHandlerImplTest.java  |  17 ++-
 .../impl/FlyBookAlertNotifyHandlerImplTest.java    |  16 ++-
 .../service/HuaweiCloudExternAlertServiceTest.java |  28 +++-
 .../hertzbeat/common/entity/job/Metrics.java       |   3 +-
 .../entity/job/MetricsSourceLocalizationTest.java  |  41 ++++++
 .../dto/EvidenceDtoMigrationTest.java              |   2 +-
 .../hertzbeat/common/util/CommonUtilTest.java      |   4 +-
 .../common/entity/manager/ParamDefine.java         |   2 +-
 .../manager/ParamDefineSourceLocalizationTest.java |  41 ++++++
 .../EntityObservabilityDtoMigrationTest.java       |   3 +-
 .../support/ResourceBundleUtf8ControlTest.java     |   2 +-
 .../src/test/resources/msg.properties              |   2 +-
 .../manager/controller/AppControllerTest.java      |   4 +-
 .../manager/controller/MonitorControllerTest.java  |   6 +-
 .../manager/service/ObserveEntityServiceTest.java  |   7 +-
 ...ityDetailObservabilityReadModelServiceTest.java |   2 +-
 .../service/impl/OtlpGrpcIngestionServiceImpl.java |  26 ++--
 .../service/impl/OtlpIngestionMessages.java        |  45 ++++++
 .../impl/OtlpIngestionWorkspaceServiceImpl.java    | 156 +++++++++++++--------
 .../impl/EntityObservabilityGatewayImpl.java       |  59 ++++----
 .../shared/service/impl/ObservabilityMessages.java |  45 ++++++
 .../service/impl/TelemetryIntakeServiceImpl.java   |  57 ++++----
 .../main/resources/observability_en_US.properties  | 120 ++++++++++++++++
 .../main/resources/observability_zh_CN.properties  | 120 ++++++++++++++++
 .../controller/OtlpIngestionControllerTest.java    |   5 +-
 ...OtlpIngestionServiceSourceLocalizationTest.java |  47 +++++++
 .../OtlpIngestionWorkspaceServiceImplTest.java     |  21 ++-
 .../impl/EntityObservabilityGatewayImplTest.java   |  66 ++++++---
 .../impl/TelemetryIntakeServiceImplTest.java       |  40 +++++-
 31 files changed, 832 insertions(+), 184 deletions(-)

diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java
index 51f961bff9..83dca2f0b4 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java
@@ -293,9 +293,9 @@ public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
                                                   "tag": "button",
                                                   "text": {
                                                       "tag": "plain_text",
-                                                      "content": "登入控制台",
+                                                      "content": "%s",
                                                       "i18n_content": {
-                                                          "en_us": "Login In"
+                                                          "en_us": "%s"
                                                       }
                                                   },
                                                   "type": "default",
@@ -326,9 +326,9 @@ public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
                       "header": {
                           "title": {
                               "tag": "plain_text",
-                              "content": "HertzBeat 告警",
+                              "content": "%s",
                               "i18n_content": {
-                                  "en_us": "HertzBeat Alarm"
+                                  "en_us": "%s"
                               }
                           },
                           "subtitle": {
@@ -355,7 +355,11 @@ public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
         }
         String jsonStr = String.format(larkCardMessage,
                 notificationContent.replace("\"", "\\\"") + atUserElement,
-                alerterProperties.getConsoleUrl());
+                bundle.getString("alerter.notify.console"),
+                bundle.getString("alerter.notify.console"),
+                alerterProperties.getConsoleUrl(),
+                bundle.getString("alerter.notify.title"),
+                bundle.getString("alerter.notify.title"));
         return JsonUtil.fromJson(jsonStr);
     }
 
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java
index ab61acb199..1aa7944a95 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java
@@ -137,9 +137,9 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
                                                   "tag": "button",
                                                   "text": {
                                                       "tag": "plain_text",
-                                                      "content": "登入控制台",
+                                                      "content": "%s",
                                                       "i18n_content": {
-                                                          "en_us": "Login In"
+                                                          "en_us": "%s"
                                                       }
                                                   },
                                                   "type": "default",
@@ -170,9 +170,9 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
                       "header": {
                           "title": {
                               "tag": "plain_text",
-                              "content": "HertzBeat 告警",
+                              "content": "%s",
                               "i18n_content": {
-                                  "en_us": "HertzBeat Alarm"
+                                  "en_us": "%s"
                               }
                           },
                           "subtitle": {
@@ -200,7 +200,12 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
 
         return String.format(larkCardMessage,
                 notificationContent.replace("\"", "\\\"") + atUserElement,
-                alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]);
+                bundle.getString("alerter.notify.console"),
+                bundle.getString("alerter.notify.console"),
+                alerterProperties.getConsoleUrl(),
+                bundle.getString("alerter.notify.title"),
+                bundle.getString("alerter.notify.title"),
+                TITLE_COLOR[priority]);
     }
 
     @Override
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java
index 8ce2607188..1a05827e97 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java
@@ -26,9 +26,11 @@ import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.client.RestTemplate;
@@ -37,8 +39,11 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.ResourceBundle;
+import java.util.regex.Pattern;
 
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -51,6 +56,8 @@ import static org.mockito.Mockito.when;
 @ExtendWith(MockitoExtension.class)
 class FeiShuAppAlertNotifyHandlerImplTest {
 
+    private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{IsHan}");
+
     @Mock
     private RestTemplate restTemplate;
 
@@ -92,7 +99,9 @@ class FeiShuAppAlertNotifyHandlerImplTest {
         template.setContent("test content");
 
         lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
+        lenient().when(bundle.getString("alerter.notify.console")).thenReturn("Console Login");
         lenient().when(alerterProperties.getConsoleUrl()).thenReturn("https://console.hertzbeat.com");
+        feiShuAppAlertNotifyHandler.bundle = bundle;
     }
 
     /**
@@ -116,6 +125,8 @@ class FeiShuAppAlertNotifyHandlerImplTest {
                 new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
         messageResp.setCode(0);
         messageResp.setMsg("success");
+        ArgumentCaptor<HttpEntity<FeiShuAppAlertNotifyHandlerImpl.FeiShuAppMessageDto>> messageRequestCaptor =
+                ArgumentCaptor.forClass(HttpEntity.class);
 
         // Mock restTemplate calls
         when(restTemplate.exchange(
@@ -128,10 +139,14 @@ class FeiShuAppAlertNotifyHandlerImplTest {
         when(restTemplate.exchange(
                 anyString(),
                 eq(org.springframework.http.HttpMethod.POST),
-                any(),
+                messageRequestCaptor.capture(),
                 eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
                 .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
         feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
+        FeiShuAppAlertNotifyHandlerImpl.FeiShuAppMessageDto messageDto = messageRequestCaptor.getValue().getBody();
+        assertTrue(messageDto.getContent().contains("Alert Notification"), messageDto.getContent());
+        assertTrue(messageDto.getContent().contains("Console Login"), messageDto.getContent());
+        assertFalse(HAN_SCRIPT.matcher(messageDto.getContent()).find(), messageDto.getContent());
     }
 
     /**
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java
index 033de34613..61464c1e8e 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java
@@ -18,11 +18,14 @@
 package org.apache.hertzbeat.alert.notice.impl;
 
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
 import org.apache.hertzbeat.alert.AlerterProperties;
+import org.springframework.http.HttpEntity;
 import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
 import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
 import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
@@ -31,6 +34,7 @@ import org.apache.hertzbeat.alert.notice.AlertNoticeException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -42,6 +46,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.ResourceBundle;
+import java.util.regex.Pattern;
 
 /**
  * Test case for FlyBook Alert Notify
@@ -49,6 +54,8 @@ import java.util.ResourceBundle;
 @ExtendWith(MockitoExtension.class)
 class FlyBookAlertNotifyHandlerImplTest {
 
+    private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{IsHan}");
+
     @Mock
     private RestTemplate restTemplate;
     
@@ -87,6 +94,8 @@ class FlyBookAlertNotifyHandlerImplTest {
         template.setContent("test content");
         
         when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
+        when(bundle.getString("alerter.notify.console")).thenReturn("Console Login");
+        flyBookAlertNotifyHandler.bundle = bundle;
     }
 
     @Test
@@ -101,14 +110,19 @@ class FlyBookAlertNotifyHandlerImplTest {
         successResp.setErrCode(0);
         ResponseEntity<CommonRobotNotifyResp> responseEntity = 
             new ResponseEntity<>(successResp, HttpStatus.OK);
+        ArgumentCaptor<HttpEntity<String>> requestCaptor = ArgumentCaptor.forClass(HttpEntity.class);
 
         when(restTemplate.postForEntity(
                 any(String.class),
-                any(),
+                requestCaptor.capture(),
                 eq(CommonRobotNotifyResp.class)
         )).thenReturn(responseEntity);
 
         flyBookAlertNotifyHandler.send(receiver, template, groupAlert);
+        String body = requestCaptor.getValue().getBody();
+        assertTrue(body.contains("\"content\": \"Alert Notification\""));
+        assertTrue(body.contains("\"content\": \"Console Login\""));
+        assertFalse(HAN_SCRIPT.matcher(body).find());
     }
 
     @Test
diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java
index 3859303c7e..07ec65cadc 100644
--- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java
+++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java
@@ -77,7 +77,10 @@ public class HuaweiCloudExternAlertServiceTest {
                 + "vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX"
                 + "w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E"
                 + "0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
-        externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。");
+        externAlert.setSubject("[\u534e\u4e3a\u4e91][\u7d27\u6025\u544a\u8b66\u6062\u590d]"
+                + "\u4e91\u76d1\u63a7\u901a\u77e5:\u5206\u5e03\u5f0f\u7f13\u5b58\u670d\u52a1-DCS Redis"
+                + "\u5b9e\u4f8b “dcs-h4tv” \u7684\u6bcf\u79d2\u5e76\u53d1\u64cd\u4f5c\u6570"
+                + "\u5df2\u6062\u590d\u6b63\u5e38。");
         externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
         externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
         externAlert.setType("Notification");
@@ -94,15 +97,23 @@ public class HuaweiCloudExternAlertServiceTest {
         externAlert.setSignature("Igs0bBhzw0JGmlgBH+9ejw2xWfPTXjAatAEsKDkkWcC5bZ/jveckdRZdgp/S0JER9eiJfMF427YDABufIN0sv/vBRXaRQKfRBLTJYbSTl+AQpEbIW5yUfJSRLEG3HNEhUDjASolbrW7zdPCoGkkqjifE23FCvw"
                 + "+4tewMzqmHnfJHcFBq3W89CJzdPBjwO1UcY9C39moUZgqZk+qDVLpxb4bHSrEYAwPOSrOPR7TZpETJ30UOgFYajJydQk692edfs0NeVutHoQiOJ5/YC83ULHft0aXhichjtfZE4KF69nROAKez0ubk3l"
                 + "Ey/mBIM9Ylbxn5b84OIrzzZQrIWe8Syw==");
-        externAlert.setSubject("[华为云][紧急告警]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已触发告警。");
+        externAlert.setSubject("[\u534e\u4e3a\u4e91][\u7d27\u6025\u544a\u8b66]"
+                + "\u4e91\u76d1\u63a7\u901a\u77e5:\u5206\u5e03\u5f0f\u7f13\u5b58\u670d\u52a1-DCS Redis"
+                + "\u5b9e\u4f8b “dcs-h4tv” \u7684\u6bcf\u79d2\u5e76\u53d1\u64cd\u4f5c\u6570"
+                + "\u5df2\u89e6\u53d1\u544a\u8b66。");
         externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
         externAlert.setMessageId("1565df032a19494590d61e05f7b0dc0e");
         externAlert.setType("Notification");
-        externAlert.setMessage("{\"version\":\"v1\",\"data\":{\"AccountName\":\"hid_hk6tij5o1v-95zn\",\"Namespace\":\"分布式缓存服务\",\"DimensionName\":\"DCS Redis实例\",\"ResourceName\""
-                + ":\"dcs-h4tv\",\"MetricName\":\"每秒并发操作数\",\"IsAlarm\":true,\"AlarmLevel\":\"紧急\",\"Region\":\"华东-上海一\",\"RegionId\":\"cn-east-3\",\"ResourceId\":\"3dc7b9ea"
+        externAlert.setMessage("{\"version\":\"v1\",\"data\":{\"AccountName\":\"hid_hk6tij5o1v-95zn\","
+                + "\"Namespace\":\"\u5206\u5e03\u5f0f\u7f13\u5b58\u670d\u52a1\","
+                + "\"DimensionName\":\"DCS Redis\u5b9e\u4f8b\",\"ResourceName\""
+                + ":\"dcs-h4tv\",\"MetricName\":\"\u6bcf\u79d2\u5e76\u53d1\u64cd\u4f5c\u6570\","
+                + "\"IsAlarm\":true,\"AlarmLevel\":\"\u7d27\u6025\",\"Region\":\"\u534e\u4e1c-\u4e0a\u6d77\u4e00\","
+                + "\"RegionId\":\"cn-east-3\",\"ResourceId\":\"3dc7b9ea"
                 + "-70b4-4c38-942d-e2636e6d844c\",\"PrivateIp\":\"192.168.0.54\",\"CurrentData\":\"6.00 count\",\"AlarmTime\":\"2025/06/02 22:56:15 GMT+08:00\","
                 + "\"AlarmRecordID\":\"ah1748876175242njvndyzMZ\","
-                + "\"AlarmRuleName\":\"alarm-c5jj\",\"IsOriginalValue\":true,\"Filter\":\"原始值\",\"ComparisonOperator\":\"\\u003e\",\"Value\":\"5 count\",\"Unit\":\"count\",\"Count\":2}}");
+                + "\"AlarmRuleName\":\"alarm-c5jj\",\"IsOriginalValue\":true,\"Filter\":\"\u539f\u59cb\u503c\","
+                + "\"ComparisonOperator\":\"\\u003e\",\"Value\":\"5 count\",\"Unit\":\"count\",\"Count\":2}}");
         externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
         externAlert.setTimestamp("2025-06-02T14:56:17Z");
         externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
@@ -135,7 +146,10 @@ public class HuaweiCloudExternAlertServiceTest {
         externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLbvaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np"
                 + "1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwXw1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9l"
                 + "YyeL7vVI0e4sfRbuQruz3E0+ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
-        externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。");
+        externAlert.setSubject("[\u534e\u4e3a\u4e91][\u7d27\u6025\u544a\u8b66\u6062\u590d]"
+                + "\u4e91\u76d1\u63a7\u901a\u77e5:\u5206\u5e03\u5f0f\u7f13\u5b58\u670d\u52a1-DCS Redis"
+                + "\u5b9e\u4f8b “dcs-h4tv” \u7684\u6bcf\u79d2\u5e76\u53d1\u64cd\u4f5c\u6570"
+                + "\u5df2\u6062\u590d\u6b63\u5e38。");
         externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
         externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
         externAlert.setType("UnsubscribeConfirmation");
@@ -184,4 +198,4 @@ public class HuaweiCloudExternAlertServiceTest {
         verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
     }
 
-}
\ No newline at end of file
+}
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
index 3b010bc399..ca10145bc9 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
@@ -87,8 +87,7 @@ public class Metrics {
     private String name;
     /**
      * metrics name's i18n value
-     * zh-CN: CPU信息
-     * en-US: CPU Info
+     * Example: {"en-US": "CPU Info"}
      */
     private Map<String, String> i18n;
     /**
diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/MetricsSourceLocalizationTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/MetricsSourceLocalizationTest.java
new file mode 100644
index 0000000000..7a633d8f1e
--- /dev/null
+++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/MetricsSourceLocalizationTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.job;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.regex.Pattern;
+import org.junit.jupiter.api.Test;
+
+class MetricsSourceLocalizationTest {
+
+    private static final Path PRODUCTION_SOURCE = Path.of(
+            "src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java");
+    private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}");
+
+    @Test
+    void productionSourceShouldNotContainHanScriptLiterals() throws IOException {
+        String source = Files.readString(PRODUCTION_SOURCE);
+
+        assertFalse(HAN_SCRIPT.matcher(source).find(),
+                () -> "source file must not contain Han-script literals: " + PRODUCTION_SOURCE);
+    }
+}
diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/EvidenceDtoMigrationTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/EvidenceDtoMigrationTest.java
index 8ebc8758da..9c8f57194c 100644
--- a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/EvidenceDtoMigrationTest.java
+++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/EvidenceDtoMigrationTest.java
@@ -71,7 +71,7 @@ class EvidenceDtoMigrationTest {
         EntityResponseHandoffInfo traceHandoff = new EntityResponseHandoffInfo(
                 "trace-1", "open", "critical", "checkout", "trace content", "trace-1", "span-1",
                 "checkout", "commerce", "ERROR", "trace_id='trace-1'", "platform", "checkout-system",
-                "prod", 1000L, 2000L, "otlp", "trace", codeNavigationHint, "/entities/1", "返回实体"
+                "prod", 1000L, 2000L, "otlp", "trace", codeNavigationHint, "/entities/1", "Back to entity"
         );
         EntityResponseHandoffsInfo handoffsInfo = new EntityResponseHandoffsInfo(
                 null, null, null, traceHandoff, null, null
diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java
index c99e77c977..be6f0eb5a0 100644
--- a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java
+++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java
@@ -139,10 +139,10 @@ class CommonUtilTest {
     @Test
     void testGetLangMappingValueFromI18nMap() {
         Map<String, String> i18nMap = new HashMap<>();
-        i18nMap.put("zh-CN", "中文");
+        i18nMap.put("zh-CN", "Simplified Chinese");
         i18nMap.put("ja", null);
         i18nMap.put("en-US", "English");
-        assertEquals("中文", CommonUtil.getLangMappingValueFromI18nMap("zh-CN", i18nMap));
+        assertEquals("Simplified Chinese", CommonUtil.getLangMappingValueFromI18nMap("zh-CN", i18nMap));
         assertEquals("English", CommonUtil.getLangMappingValueFromI18nMap("en-US", i18nMap));
         assertNull(CommonUtil.getLangMappingValueFromI18nMap("zh", new HashMap<>()));
         assertNotNull(CommonUtil.getLangMappingValueFromI18nMap("ja", i18nMap));
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java
index d4b54d2380..437bd43b01 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java
@@ -73,7 +73,7 @@ public class ParamDefine {
      * Parameter field external display name
      * Port
      */
-    @Schema(description = "The parameter field displays the internationalized name", example = "{zh-CN: '端口'}",
+    @Schema(description = "The parameter field displays the internationalized name", example = "{\"en-US\":\"Port\"}",
             accessMode = READ_WRITE)
     @Convert(converter = JsonMapAttributeConverter.class)
     @SuppressWarnings("JpaAttributeTypeInspection")
diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/ParamDefineSourceLocalizationTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/ParamDefineSourceLocalizationTest.java
new file mode 100644
index 0000000000..a5ca7b9801
--- /dev/null
+++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/ParamDefineSourceLocalizationTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.manager;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.regex.Pattern;
+import org.junit.jupiter.api.Test;
+
+class ParamDefineSourceLocalizationTest {
+
+    private static final Path PRODUCTION_SOURCE = Path.of(
+            "src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java");
+    private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}");
+
+    @Test
+    void productionSourceShouldNotContainHanScriptLiterals() throws IOException {
+        String source = Files.readString(PRODUCTION_SOURCE);
+
+        assertFalse(HAN_SCRIPT.matcher(source).find(),
+                () -> "source file must not contain Han-script literals: " + PRODUCTION_SOURCE);
+    }
+}
diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/observability/dto/entity/EntityObservabilityDtoMigrationTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/observability/dto/entity/EntityObservabilityDtoMigrationTest.java
index 327114042d..d288774f89 100644
--- a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/observability/dto/entity/EntityObservabilityDtoMigrationTest.java
+++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/observability/dto/entity/EntityObservabilityDtoMigrationTest.java
@@ -51,7 +51,8 @@ class EntityObservabilityDtoMigrationTest {
         EntityUnifiedEvidenceSummary unifiedSummary = new EntityUnifiedEvidenceSummary(3, true, true, false,
                 2L, 3, 0, 8L, List.of("metrics", "logs"));
         EntityTriageRecommendation recommendation = new EntityTriageRecommendation(
-                "rule", "metrics", "优先查看监控", "监控异常最多", "down monitors", "查看监控", 9L
+                "rule", "metrics", "Review monitors first", "Most monitors are abnormal", "down monitors",
+                "View monitors", 9L
         );
 
         assertEquals("checkout-http", abnormalMonitor.getName());
diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java
index c30f5a29eb..6289642de2 100644
--- a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java
+++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java
@@ -43,7 +43,7 @@ class ResourceBundleUtf8ControlTest {
 
         bundle = control.newBundle(baseName, Locale.ROOT, "java.properties", loader, false);
         assertNotNull(bundle);
-        assertEquals("你好", bundle.getString("hello"));
+        assertEquals("Ola, Mundo!", bundle.getString("hello"));
     }
 
     @Test
diff --git a/hertzbeat-common-spring/src/test/resources/msg.properties b/hertzbeat-common-spring/src/test/resources/msg.properties
index 6dc68e451d..8860d79098 100644
--- a/hertzbeat-common-spring/src/test/resources/msg.properties
+++ b/hertzbeat-common-spring/src/test/resources/msg.properties
@@ -13,4 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-hello=你好
+hello=Ola, Mundo!
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java
index 77c361687d..40b607ad27 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java
@@ -71,7 +71,7 @@ class AppControllerTest {
         paramDefine.setField("port");
         paramDefine.setType("number");
         paramDefine.setDefaultValue("12");
-        paramDefine.setPlaceholder("请输出密码");
+        paramDefine.setPlaceholder("Enter password");
         paramDefine.setCreator("tom");
         paramDefine.setModifier("tom");
         paramDefines.add(paramDefine);
@@ -227,7 +227,7 @@ class AppControllerTest {
     void queryAppsHierarchy() throws Exception {
         // Data to make
         Hierarchy hierarchy = new Hierarchy();
-        hierarchy.setLabel("Linux系统");
+        hierarchy.setLabel("Linux system");
         hierarchy.setValue("linux");
         hierarchy.setCategory("os");
         List<Hierarchy> list = new ArrayList<>();
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java
index 196afb121e..503323ab89 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java
@@ -62,7 +62,7 @@ class MonitorControllerTest {
         monitor.setName("TanCloud");
         monitor.setInstance("192.167.25.11:8989");
         monitor.setIntervals(600);
-        monitor.setDescription("对SAAS网站TanCloud的可用性监控");
+        monitor.setDescription("Availability monitoring for the TanCloud SaaS site");
         monitor.setCreator("tom");
         monitor.setModifier("tom");
 
@@ -160,7 +160,7 @@ class MonitorControllerTest {
         monitor.setName("TanCloud");
         monitor.setInstance("192.167.25.11:8989");
         monitor.setIntervals(600);
-        monitor.setDescription("对SAAS网站TanCloud的可用性监控");
+        monitor.setDescription("Availability monitoring for the TanCloud SaaS site");
         monitor.setCreator("tom");
         monitor.setModifier("tom");
 
@@ -203,7 +203,7 @@ class MonitorControllerTest {
         monitor.setName("TanCloud");
         monitor.setInstance("192.167.25.11:8989");
         monitor.setIntervals(600);
-        monitor.setDescription("对SAAS网站TanCloud的可用性监控");
+        monitor.setDescription("Availability monitoring for the TanCloud SaaS site");
         monitor.setCreator("tom");
         monitor.setModifier("tom");
 
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ObserveEntityServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ObserveEntityServiceTest.java
index 0290444188..78fe8d4879 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ObserveEntityServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ObserveEntityServiceTest.java
@@ -37,6 +37,7 @@ import java.util.HashMap;
 import java.util.Collections;
 import java.util.List;
 import java.time.LocalDateTime;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -250,9 +251,12 @@ class ObserveEntityServiceTest {
     private EntityValidationService entityValidationService;
     private EntityWorkspaceAccessService entityWorkspaceAccessService;
     private EntityWorkspaceQueryService entityWorkspaceQueryService;
+    private Locale previousLocale;
 
     @BeforeEach
     void setUpNoiseControlDefaults() {
+        previousLocale = Locale.getDefault();
+        Locale.setDefault(Locale.US);
         telemetryIntakeService = org.mockito.Mockito.spy(new TelemetryIntakeServiceImpl(logQueryRepository));
         entityObservabilityGateway = org.mockito.Mockito.spy(
                 new EntityObservabilityGatewayImpl(telemetryIntakeService, entityTraceQueryService));
@@ -402,6 +406,7 @@ class ObserveEntityServiceTest {
     @AfterEach
     void tearDownRequestContext() {
         AuthTokenRequestContext.clear();
+        Locale.setDefault(previousLocale);
     }
 
     @Test
@@ -1330,7 +1335,7 @@ class ObserveEntityServiceTest {
         assertFalse(detail.getUnifiedEvidenceSummary().isTracesActive());
         assertNotNull(detail.getTriageRecommendation());
         assertEquals("metrics", detail.getTriageRecommendation().getRecommendedFocus());
-        assertEquals("查看监控", detail.getTriageRecommendation().getActionLabel());
+        assertEquals("View monitors", detail.getTriageRecommendation().getActionLabel());
         assertTrue(detail.getOpsSummary().isOwnerReady());
         assertTrue(detail.getOpsSummary().isRunbookReady());
         assertTrue(detail.getOpsSummary().isRelationReady());
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
index cb95e56700..ec4e2a36c0 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
@@ -153,7 +153,7 @@ class EntityDetailObservabilityReadModelServiceTest {
         EntityUnifiedEvidenceSummary unifiedSummary = new EntityUnifiedEvidenceSummary(
                 2, true, true, false, 1, 1, 0, 987L, List.of("metrics", "logs"));
         EntityTriageRecommendation triage = new EntityTriageRecommendation(
-                "evidence", "metrics", "Metrics first", "Down monitor", "active alert", "查看监控", 987L);
+                "evidence", "metrics", "Metrics first", "Down monitor", "active alert", "View monitors", 987L);
         List<org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence> metricEvidence =
                 Collections.emptyList();
         List<org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence> logEvidence =
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java
index ae4b98867a..d5d3fbbe2a 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java
@@ -1633,7 +1633,7 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
             return observations;
         }
         return List.of(new MetricObservation("metric", null, null, baseMetricMetadata("unsupported", "unsupported",
-                "unsupported", "未知 OTLP metric 类型,当前未被 HertzBeat facade 识别。")));
+                "unsupported", OtlpIngestionMessages.get("observability.otlp.metric.compatibility.unknown-type"))));
     }
 
     private MetricObservation fromNumberDataPoint(String metricType,
@@ -1664,7 +1664,7 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
                 "partial",
                 "supported",
                 "partial",
-                "Histogram 指标已写入 Greptime,但 HertzBeat 当前 facade 仅保留代表值与 bucket/bounds 元信息,未提供完整 histogram 查询语义。"
+                OtlpIngestionMessages.get("observability.otlp.metric.compatibility.histogram.reason")
         );
         appendMetricTimeRange(metadata, point.getStartTimeUnixNano(), point.getTimeUnixNano());
         metadata.put(OTLP_METRIC_DATA_POINT_COUNT, String.valueOf(dataPointCount));
@@ -1692,7 +1692,7 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
                 "unsupported",
                 "unsupported",
                 "partial",
-                "Greptime 当前 OTLP metrics 数据模型不支持 ExponentialHistogram;HertzBeat 仅保留代表值与兼容性元信息。"
+                OtlpIngestionMessages.get("observability.otlp.metric.compatibility.exponential-histogram.reason")
         );
         appendMetricTimeRange(metadata, point.getStartTimeUnixNano(), point.getTimeUnixNano());
         metadata.put(OTLP_METRIC_DATA_POINT_COUNT, String.valueOf(dataPointCount));
@@ -1724,7 +1724,7 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
                 "partial",
                 "partial",
                 "partial",
-                "Summary quantiles 语义受 Greptime 数据模型与当前 HertzBeat facade 限制,当前仅保留 summary/quantiles 元信息。"
+                OtlpIngestionMessages.get("observability.otlp.metric.compatibility.summary.reason")
         );
         appendMetricTimeRange(metadata, point.getStartTimeUnixNano(), point.getTimeUnixNano());
         metadata.put(OTLP_METRIC_DATA_POINT_COUNT, String.valueOf(dataPointCount));
@@ -1802,18 +1802,24 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
             metadata.put(OTLP_METRIC_COMPATIBILITY_REASON, overallReason);
         }
         if ("supported".equals(greptimeCompatibility)) {
-            metadata.put(OTLP_METRIC_GREPTIME_REASON, "Greptime 当前 OTLP metrics 数据模型支持该类型。");
+            metadata.put(OTLP_METRIC_GREPTIME_REASON,
+                    OtlpIngestionMessages.get("observability.otlp.metric.greptime.reason.supported"));
         } else if ("partial".equals(greptimeCompatibility)) {
-            metadata.put(OTLP_METRIC_GREPTIME_REASON, "Greptime 当前 OTLP metrics 数据模型仅部分保留该类型语义。");
+            metadata.put(OTLP_METRIC_GREPTIME_REASON,
+                    OtlpIngestionMessages.get("observability.otlp.metric.greptime.reason.partial"));
         } else {
-            metadata.put(OTLP_METRIC_GREPTIME_REASON, "Greptime 当前 OTLP metrics 数据模型不支持该类型。");
+            metadata.put(OTLP_METRIC_GREPTIME_REASON,
+                    OtlpIngestionMessages.get("observability.otlp.metric.greptime.reason.unsupported"));
         }
         if ("supported".equals(facadeCompatibility)) {
-            metadata.put(OTLP_METRIC_FACADE_REASON, "HertzBeat 当前 facade 可直接消费该类型。");
+            metadata.put(OTLP_METRIC_FACADE_REASON,
+                    OtlpIngestionMessages.get("observability.otlp.metric.facade.reason.supported"));
         } else if ("partial".equals(facadeCompatibility)) {
-            metadata.put(OTLP_METRIC_FACADE_REASON, "HertzBeat 当前 facade 仅保留代表值与兼容元信息。");
+            metadata.put(OTLP_METRIC_FACADE_REASON,
+                    OtlpIngestionMessages.get("observability.otlp.metric.facade.reason.partial"));
         } else {
-            metadata.put(OTLP_METRIC_FACADE_REASON, "HertzBeat 当前 facade 不支持该类型。");
+            metadata.put(OTLP_METRIC_FACADE_REASON,
+                    OtlpIngestionMessages.get("observability.otlp.metric.facade.reason.unsupported"));
         }
         return metadata;
     }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionMessages.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionMessages.java
new file mode 100644
index 0000000000..8b83bad27c
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionMessages.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.service.impl;
+
+import java.text.MessageFormat;
+import java.util.MissingResourceException;
+import org.apache.hertzbeat.common.util.ResourceBundleUtil;
+
+/**
+ * Localized backend copy for OTLP ingestion DTOs.
+ */
+final class OtlpIngestionMessages {
+
+    private static final String BUNDLE_NAME = "observability";
+
+    private OtlpIngestionMessages() {
+    }
+
+    static String get(String key) {
+        try {
+            return ResourceBundleUtil.getBundle(BUNDLE_NAME).getString(key);
+        } catch (MissingResourceException exception) {
+            return key;
+        }
+    }
+
+    static String format(String key, Object... args) {
+        return MessageFormat.format(get(key), args);
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
index 7efc672823..371509bcb2 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
@@ -219,8 +219,10 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                 || latestMetricSnapshot.getObservedAt() >= latestMonitorObservedAt)) {
             recentEvents.add(new OtlpIngestionOverviewDto.RecentSignalEvent(
                     "metrics",
-                    defaultText(latestMetricSnapshot.getServiceName(), "指标已接入"),
-                    defaultText(latestMetricSnapshot.getServiceNamespace(), "最近已收到指标数据"),
+                    defaultText(latestMetricSnapshot.getServiceName(),
+                            message("observability.otlp.overview.event.metrics.title")),
+                    defaultText(latestMetricSnapshot.getServiceNamespace(),
+                            message("observability.otlp.overview.event.metrics.copy")),
                     latestMetricSnapshot.getObservedAt()
             ));
         } else if (latestMonitor.isPresent()) {
@@ -254,7 +256,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                         monitorTotalCount + otlpMetricCount,
                         metricsLatestObservedAt,
                         metricsIntakeMode(monitorTotalCount, otlpMetricCount),
-                        metricsActive ? "最近已收到可查看的指标数据" : "最近 24 小时还没有指标数据"
+                        metricsActive
+                                ? message("observability.otlp.overview.metrics.active")
+                                : message("observability.otlp.overview.metrics.inactive")
                 ),
                 new OtlpIngestionOverviewDto.SignalOverview(
                         "logs",
@@ -262,7 +266,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                         logTotalCount,
                         logsLatestObservedAt,
                         "OTLP",
-                        logTotalCount > 0 ? "最近 24 小时已收到日志数据" : "最近 24 小时还没有日志数据"
+                        logTotalCount > 0
+                                ? message("observability.otlp.overview.logs.active")
+                                : message("observability.otlp.overview.logs.inactive")
                 ),
                 new OtlpIngestionOverviewDto.SignalOverview(
                         "traces",
@@ -270,7 +276,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                         traceOverview.getTotalTraceCount(),
                         traceOverview.getLatestObservedAt(),
                         "OTLP",
-                        traceOverview.getTotalTraceCount() > 0 ? "最近 24 小时已收到链路数据" : "最近 24 小时还没有链路数据"
+                        traceOverview.getTotalTraceCount() > 0
+                                ? message("observability.otlp.overview.traces.active")
+                                : message("observability.otlp.overview.traces.inactive")
                 ),
                 activeSignalCount,
                 latestObservedAt,
@@ -294,72 +302,90 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
         long total = workspaceQueryGateway.countCollectors();
         long online = workspaceQueryGateway.countCollectorsByStatus(CommonConstants.COLLECTOR_STATUS_ONLINE);
         if (total <= 0) {
-            return readinessCheck("collector", "Collector 集群", "warning", "未注册 Collector",
-                    "先部署 Collector 或使用内置采集入口", checkedAt);
+            return readinessCheck("collector", message("observability.otlp.readiness.collector.title"), "warning",
+                    message("observability.otlp.readiness.collector.unregistered"),
+                    message("observability.otlp.readiness.collector.deploy"), checkedAt);
         }
         if (online >= total) {
-            return readinessCheck("collector", "Collector 集群", "success", online + " / " + total + " 在线",
-                    "采集节点可接收任务", checkedAt);
+            return readinessCheck("collector", message("observability.otlp.readiness.collector.title"), "success",
+                    message("observability.otlp.readiness.collector.online", online, total),
+                    message("observability.otlp.readiness.collector.accepting"), checkedAt);
         }
         if (online > 0) {
-            return readinessCheck("collector", "Collector 集群", "warning", online + " / " + total + " 在线",
-                    (total - online) + " 个采集节点离线", checkedAt);
+            return readinessCheck("collector", message("observability.otlp.readiness.collector.title"), "warning",
+                    message("observability.otlp.readiness.collector.online", online, total),
+                    message("observability.otlp.readiness.collector.offline", total - online), checkedAt);
         }
-        return readinessCheck("collector", "Collector 集群", "danger", "0 / " + total + " 在线",
-                "所有采集节点离线", checkedAt);
+        return readinessCheck("collector", message("observability.otlp.readiness.collector.title"), "danger",
+                message("observability.otlp.readiness.collector.online", 0, total),
+                message("observability.otlp.readiness.collector.all-offline"), checkedAt);
     }
 
     private OtlpIngestionOverviewDto.ReadinessCheck buildStorageReadiness(long checkedAt) {
         int total = historyDataReaders.size();
         long available = countAvailableHistoryReaders();
         if (total <= 0) {
-            return readinessCheck("storage", "历史存储", "warning", "未启用历史存储",
-                    "检查历史存储配置", checkedAt);
+            return readinessCheck("storage", message("observability.otlp.readiness.storage.title"), "warning",
+                    message("observability.otlp.readiness.storage.disabled"),
+                    message("observability.otlp.readiness.storage.check-config"), checkedAt);
         }
         if (available >= total) {
-            return readinessCheck("storage", "历史存储", "success", available + " / " + total + " 可用",
-                    "HistoryDataReader 可用", checkedAt);
+            return readinessCheck("storage", message("observability.otlp.readiness.storage.title"), "success",
+                    message("observability.otlp.readiness.storage.available", available, total),
+                    message("observability.otlp.readiness.storage.reader-available"), checkedAt);
         }
         if (available > 0) {
-            return readinessCheck("storage", "历史存储", "warning", available + " / " + total + " 可用",
-                    "部分历史存储不可用", checkedAt);
+            return readinessCheck("storage", message("observability.otlp.readiness.storage.title"), "warning",
+                    message("observability.otlp.readiness.storage.available", available, total),
+                    message("observability.otlp.readiness.storage.partial"), checkedAt);
         }
-        return readinessCheck("storage", "历史存储", "danger", "0 / " + total + " 可用",
-                "检查历史存储配置", checkedAt);
+        return readinessCheck("storage", message("observability.otlp.readiness.storage.title"), "danger",
+                message("observability.otlp.readiness.storage.available", 0, total),
+                message("observability.otlp.readiness.storage.check-config"), checkedAt);
     }
 
     private OtlpIngestionOverviewDto.ReadinessCheck buildQueryReadiness(long checkedAt) {
         boolean promqlAvailable = hasPromqlExecutor();
         boolean historyAvailable = countAvailableHistoryReaders() > 0;
         if (promqlAvailable && historyAvailable) {
-            return readinessCheck("query", "查询服务", "success", "指标、日志和链路查询可用",
-                    "PromQL 与历史查询可用", checkedAt);
+            return readinessCheck("query", message("observability.otlp.readiness.query.title"), "success",
+                    message("observability.otlp.readiness.query.available"),
+                    message("observability.otlp.readiness.query.promql-history"), checkedAt);
         }
         if (promqlAvailable || historyAvailable) {
-            return readinessCheck("query", "查询服务", "warning", "部分查询能力可用",
-                    promqlAvailable ? "PromQL 可用,历史查询待检查" : "历史查询可用,PromQL 待检查", checkedAt);
+            return readinessCheck("query", message("observability.otlp.readiness.query.title"), "warning",
+                    message("observability.otlp.readiness.query.partial"),
+                    promqlAvailable
+                            ? message("observability.otlp.readiness.query.promql-only")
+                            : message("observability.otlp.readiness.query.history-only"), checkedAt);
         }
-        return readinessCheck("query", "查询服务", "danger", "查询服务不可用",
-                "检查 PromQL 与历史查询配置", checkedAt);
+        return readinessCheck("query", message("observability.otlp.readiness.query.title"), "danger",
+                message("observability.otlp.readiness.query.unavailable"),
+                message("observability.otlp.readiness.query.check-config"), checkedAt);
     }
 
     private OtlpIngestionOverviewDto.ReadinessCheck buildGreptimeReadiness(long checkedAt) {
         boolean greptimeEnabled = greptimeProperties.stream().anyMatch(GreptimeProperties::enabled);
         if (!greptimeEnabled) {
-            return readinessCheck("greptime", "GreptimeDB", "neutral", "未启用 GreptimeDB",
-                    "当前使用其他历史存储或尚未配置", checkedAt);
+            return readinessCheck("greptime", "GreptimeDB", "neutral",
+                    message("observability.otlp.readiness.greptime.disabled"),
+                    message("observability.otlp.readiness.greptime.other-storage"), checkedAt);
         }
         if (greptimeSqlQueryExecutors.isEmpty()) {
-            return readinessCheck("greptime", "GreptimeDB", "warning", "GreptimeDB 已启用,SQL 执行器未就绪",
-                    "检查 GreptimeDB HTTP 配置", checkedAt);
+            return readinessCheck("greptime", "GreptimeDB", "warning",
+                    message("observability.otlp.readiness.greptime.sql-not-ready"),
+                    message("observability.otlp.readiness.greptime.check-http"), checkedAt);
         }
         try {
             greptimeSqlQueryExecutors.getFirst().execute("SELECT 1");
-            return readinessCheck("greptime", "GreptimeDB", "success", "SQL 自检通过",
-                    "SELECT 1 成功", checkedAt);
+            return readinessCheck("greptime", "GreptimeDB", "success",
+                    message("observability.otlp.readiness.greptime.sql-ok"),
+                    message("observability.otlp.readiness.greptime.select-ok"), checkedAt);
         } catch (RuntimeException exception) {
-            return readinessCheck("greptime", "GreptimeDB", "danger", "SQL 自检失败",
-                    defaultText(exception.getMessage(), "检查 GreptimeDB 连接"), checkedAt);
+            return readinessCheck("greptime", "GreptimeDB", "danger",
+                    message("observability.otlp.readiness.greptime.sql-failed"),
+                    defaultText(exception.getMessage(),
+                            message("observability.otlp.readiness.greptime.check-connection")), checkedAt);
         }
     }
 
@@ -483,9 +509,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                     "deployment.environment.name": "prod",
                 })
 
-                # 所有 signals 统一发送到 HertzBeat OTLP HTTP 入口:
                 # %s
-                """.formatted(unifiedBaseEndpoint);
+                # %s
+                """.formatted(message("observability.otlp.guide.snippet.python.http.comment"), unifiedBaseEndpoint);
         String pythonGrpcSnippet = """
                 from opentelemetry.sdk.resources import Resource
                 resource = Resource.create({
@@ -494,9 +520,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                     "deployment.environment.name": "prod",
                 })
 
-                # 所有 signals 统一发送到 HertzBeat OTLP gRPC 入口:
                 # %s
-                """.formatted(grpcEndpoint);
+                # %s
+                """.formatted(message("observability.otlp.guide.snippet.python.grpc.comment"), grpcEndpoint);
 
         return new OtlpIngestionGuideDto(
                 "OTLP HTTP",
@@ -510,57 +536,61 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                                 "http",
                                 "OTLP HTTP",
                                 metricsEndpoint,
-                                "把指标数据发送到这个 OTLP HTTP 地址。收到数据后,可直接在监控和实体详情中继续查看。",
-                                "如果已经配置了监控任务,数据会继续显示在对应的监控项中。"
+                                message("observability.otlp.guide.metrics.http.description"),
+                                message("observability.otlp.guide.metrics.http.note")
                         ),
                         new OtlpIngestionGuideDto.SignalGuide(
                                 "logs",
                                 "http",
                                 "OTLP HTTP",
                                 logsEndpoint,
-                                "把日志数据发送到这个 OTLP HTTP 地址。收到数据后,可直接前往日志管理查看。",
-                                "使用接入令牌即可完成认证。"
+                                message("observability.otlp.guide.logs.http.description"),
+                                message("observability.otlp.guide.logs.http.note")
                         ),
                         new OtlpIngestionGuideDto.SignalGuide(
                                 "traces",
                                 "http",
                                 "OTLP HTTP",
                                 traceEndpoint,
-                                "把链路数据发送到这个 OTLP HTTP 地址。收到数据后,可直接前往链路管理查看。",
-                                "系统会根据服务信息自动尝试关联到实体。"
+                                message("observability.otlp.guide.traces.http.description"),
+                                message("observability.otlp.guide.traces.http.note")
                         ),
                         new OtlpIngestionGuideDto.SignalGuide(
                                 "metrics",
                                 "grpc",
                                 "OTLP gRPC",
                                 grpcEndpoint,
-                                "把指标数据发送到这个 OTLP gRPC 地址,适合已经使用 OpenTelemetry Collector 或语言 SDK 的服务。",
-                                "收到数据后,可直接在监控和实体详情中继续查看。"
+                                message("observability.otlp.guide.metrics.grpc.description"),
+                                message("observability.otlp.guide.metrics.grpc.note")
                         ),
                         new OtlpIngestionGuideDto.SignalGuide(
                                 "logs",
                                 "grpc",
                                 "OTLP gRPC",
                                 grpcEndpoint,
-                                "把日志数据发送到这个 OTLP gRPC 地址。收到数据后,可直接前往日志管理查看。",
-                                "使用 Authorization Bearer 接入令牌即可完成认证。"
+                                message("observability.otlp.guide.logs.grpc.description"),
+                                message("observability.otlp.guide.logs.grpc.note")
                         ),
                         new OtlpIngestionGuideDto.SignalGuide(
                                 "traces",
                                 "grpc",
                                 "OTLP gRPC",
                                 grpcEndpoint,
-                                "把链路数据发送到这个 OTLP gRPC 地址。收到数据后,可直接前往链路管理查看。",
-                                "系统会根据服务信息自动尝试关联到实体。"
+                                message("observability.otlp.guide.traces.grpc.description"),
+                                message("observability.otlp.guide.traces.grpc.note")
                         )
                 ),
                 List.of(
                         new OtlpIngestionGuideDto.Snippet("collector-http", "http", "OpenTelemetry Collector", "yaml", collectorSnippet),
-                        new OtlpIngestionGuideDto.Snippet("java-http", "http", "Java 环境变量", "bash", javaSnippet),
-                        new OtlpIngestionGuideDto.Snippet("python-http", "http", "Python 资源属性", "python", pythonSnippet),
+                        new OtlpIngestionGuideDto.Snippet("java-http", "http",
+                                message("observability.otlp.guide.snippet.java-env"), "bash", javaSnippet),
+                        new OtlpIngestionGuideDto.Snippet("python-http", "http",
+                                message("observability.otlp.guide.snippet.python-resource"), "python", pythonSnippet),
                         new OtlpIngestionGuideDto.Snippet("collector-grpc", "grpc", "OpenTelemetry Collector", "yaml", collectorGrpcSnippet),
-                        new OtlpIngestionGuideDto.Snippet("java-grpc", "grpc", "Java 环境变量", "bash", javaGrpcSnippet),
-                        new OtlpIngestionGuideDto.Snippet("python-grpc", "grpc", "Python 资源属性", "python", pythonGrpcSnippet)
+                        new OtlpIngestionGuideDto.Snippet("java-grpc", "grpc",
+                                message("observability.otlp.guide.snippet.java-env"), "bash", javaGrpcSnippet),
+                        new OtlpIngestionGuideDto.Snippet("python-grpc", "grpc",
+                                message("observability.otlp.guide.snippet.python-resource"), "python", pythonGrpcSnippet)
                 )
         );
     }
@@ -666,7 +696,7 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                     null,
                     new OtlpMetricsConsoleDto.Stats(0, 0, null),
                     "no_context",
-                    "缺少可用于构建 OTLP 指标查询的服务上下文。"
+                    message("observability.otlp.metrics-console.no-context")
             );
         }
         if (!metricQueryRepository.hasPromqlExecutor()) {
@@ -678,7 +708,7 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                     null,
                     new OtlpMetricsConsoleDto.Stats(0, 0, null),
                     "load_failed",
-                    "当前环境未配置可用的 PromQL 指标查询执行器。"
+                    message("observability.otlp.metrics-console.promql-unavailable")
             );
         }
         MetricsQueryExecution execution = executeMetricsConsoleQuery(resolvedQuery, resolvedStart, resolvedEnd);
@@ -1166,12 +1196,20 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
 
     private String metricsIntakeMode(long monitorTotalCount, long otlpMetricCount) {
         if (monitorTotalCount > 0 && otlpMetricCount > 0) {
-            return "OTLP + 实体监控";
+            return message("observability.otlp.overview.metrics.mode.mixed");
         }
         if (otlpMetricCount > 0) {
             return "OTLP";
         }
-        return "实体监控";
+        return message("observability.otlp.overview.metrics.mode.monitor");
+    }
+
+    private static String message(String key) {
+        return OtlpIngestionMessages.get(key);
+    }
+
+    private static String message(String key, Object... args) {
+        return OtlpIngestionMessages.format(key, args);
     }
 
     private String defaultText(String primary, String fallback) {
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
index ac65e7546a..b21b6d109e 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
@@ -265,80 +265,80 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa
         if (activeAlertCount > 0) {
             actions.add(new EntityNextActionInfo(
                     "review_alerts",
-                    "处理活跃告警",
-                    activeAlertCount + " 条活跃告警已经关联到当前实体,先确认影响范围和当前状态。",
-                    "查看活跃告警",
+                    ObservabilityMessages.get("observability.entity.next.review-alerts.title"),
+                    ObservabilityMessages.format("observability.entity.next.review-alerts.description", activeAlertCount),
+                    ObservabilityMessages.get("observability.entity.next.review-alerts.action"),
                     100
             ));
         }
         if (opsSummary != null && !opsSummary.isOwnerReady()) {
             actions.add(new EntityNextActionInfo(
                     "complete_owner",
-                    "补负责人",
-                    "当前实体还没有明确负责人,排障和治理动作缺少责任归属。",
-                    "补负责人",
+                    ObservabilityMessages.get("observability.entity.next.complete-owner.title"),
+                    ObservabilityMessages.get("observability.entity.next.complete-owner.description"),
+                    ObservabilityMessages.get("observability.entity.next.complete-owner.action"),
                     90
             ));
         }
         if (opsSummary != null && !opsSummary.isRunbookReady()) {
             actions.add(new EntityNextActionInfo(
                     "complete_runbook",
-                    "补处置手册",
-                    "先补一条 runbook 或处置入口,告警后才能直接推动恢复。",
-                    "补处置手册",
+                    ObservabilityMessages.get("observability.entity.next.complete-runbook.title"),
+                    ObservabilityMessages.get("observability.entity.next.complete-runbook.description"),
+                    ObservabilityMessages.get("observability.entity.next.complete-runbook.action"),
                     80
             ));
         }
         if (evidenceSummary != null && healthyMonitorCount + downMonitorCount == 0) {
             actions.add(new EntityNextActionInfo(
                     "bind_monitor",
-                    "补绑监控",
-                    "实体已经存在,但还没有绑定任何监控任务,当前状态只能停留在目录元数据层。",
-                    "关联监控",
+                    ObservabilityMessages.get("observability.entity.next.bind-monitor.title"),
+                    ObservabilityMessages.get("observability.entity.next.bind-monitor.description"),
+                    ObservabilityMessages.get("observability.entity.next.bind-monitor.action"),
                     75
             ));
         } else if (downMonitorCount > 0 && activeAlertCount == 0) {
             actions.add(new EntityNextActionInfo(
                     "bind_monitor",
-                    "查看异常监控",
-                    downMonitorCount + " 个监控当前异常,建议先确认哪个证据最值得继续排查。",
-                    "查看核心监控",
+                    ObservabilityMessages.get("observability.entity.next.abnormal-monitors.title"),
+                    ObservabilityMessages.format("observability.entity.next.abnormal-monitors.description", downMonitorCount),
+                    ObservabilityMessages.get("observability.entity.next.abnormal-monitors.action"),
                     74
             ));
         }
         if (opsSummary != null && !opsSummary.isTelemetryReady()) {
             actions.add(new EntityNextActionInfo(
                     "open_discovery",
-                    "归并证据到实体",
-                    "当前还没有形成足够的监控、身份或日志线索,建议先回到发现工作台归并证据。",
-                    "打开发现工作台",
+                    ObservabilityMessages.get("observability.entity.next.open-discovery.title"),
+                    ObservabilityMessages.get("observability.entity.next.open-discovery.description"),
+                    ObservabilityMessages.get("observability.entity.next.open-discovery.action"),
                     70
             ));
         }
         if (logSummary != null && logSummary.getHintCount() > 0 && activeAlertCount == 0) {
             actions.add(new EntityNextActionInfo(
                     "inspect_logs",
-                    "查看日志线索",
-                    "日志入口已经具备,先确认 OTel resource 或 fallback 查询是否能快速定位异常。",
-                    "查看日志线索",
+                    ObservabilityMessages.get("observability.entity.next.inspect-logs.title"),
+                    ObservabilityMessages.get("observability.entity.next.inspect-logs.description"),
+                    ObservabilityMessages.get("observability.entity.next.inspect-logs.action"),
                     60
             ));
         }
         if (opsSummary != null && !opsSummary.isRelationReady()) {
             actions.add(new EntityNextActionInfo(
                     "review_relations",
-                    "补关键关系",
-                    "关键上下游关系还不完整,建议先补齐最关键的依赖边界。",
-                    "查看关系",
+                    ObservabilityMessages.get("observability.entity.next.review-relations.title"),
+                    ObservabilityMessages.get("observability.entity.next.review-relations.description"),
+                    ObservabilityMessages.get("observability.entity.next.review-relations.action"),
                     50
             ));
         }
         if (actions.isEmpty()) {
             actions.add(new EntityNextActionInfo(
                     "inspect_logs",
-                    "继续排查当前实体",
-                    "目录归属和证据都已经具备,可以直接从日志或监控继续深入分析。",
-                    "查看日志线索",
+                    ObservabilityMessages.get("observability.entity.next.fallback.title"),
+                    ObservabilityMessages.get("observability.entity.next.fallback.description"),
+                    ObservabilityMessages.get("observability.entity.next.fallback.action"),
                     10
             ));
         }
@@ -366,11 +366,12 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa
     @Override
     public String buildEntityReturnLabel(ObservedEntityContext entityContext) {
         if (entityContext == null || entityContext.getEntity() == null) {
-            return "实体详情";
+            return ObservabilityMessages.get("observability.entity.return-label.fallback");
         }
         return defaultText(
                 trimToNull(entityContext.getEntity().getDisplayName()),
-                defaultText(trimToNull(entityContext.getEntity().getName()), "实体详情")
+                defaultText(trimToNull(entityContext.getEntity().getName()),
+                        ObservabilityMessages.get("observability.entity.return-label.fallback"))
         );
     }
 
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/ObservabilityMessages.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/ObservabilityMessages.java
new file mode 100644
index 0000000000..b1d4c93b77
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/ObservabilityMessages.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.shared.service.impl;
+
+import java.text.MessageFormat;
+import java.util.MissingResourceException;
+import org.apache.hertzbeat.common.util.ResourceBundleUtil;
+
+/**
+ * Localized backend copy for observability DTOs.
+ */
+final class ObservabilityMessages {
+
+    private static final String BUNDLE_NAME = "observability";
+
+    private ObservabilityMessages() {
+    }
+
+    static String get(String key) {
+        try {
+            return ResourceBundleUtil.getBundle(BUNDLE_NAME).getString(key);
+        } catch (MissingResourceException exception) {
+            return key;
+        }
+    }
+
+    static String format(String key, Object... args) {
+        return MessageFormat.format(get(key), args);
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java
index cb79db53b0..7b53fa7675 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java
@@ -591,7 +591,7 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
                             preferredMonitor == null ? Collections.emptyList() : List.of(defaultText(preferredMonitor.getName(), preferredMonitor.getInstance())),
                             defaultText(preferredMonitor == null ? null : preferredMonitor.getName(), "monitor")),
                     "entity.monitor.availability",
-                    "实体监控状态",
+                    ObservabilityMessages.get("observability.telemetry.metric.entity-monitor-status"),
                     "summary",
                     "count",
                     (double) healthyCount,
@@ -965,21 +965,28 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
                 && !"normal".equalsIgnoreCase(item.getSeverityOrHealth()));
         if (metricsRequireAttention) {
             String summary = activeAlerts > 0
-                    ? "当前已有活跃告警,建议先确认监控面是否正在持续异常。"
-                    : "当前监控面已经出现异常或波动,建议先从指标和监控状态入手。";
+                    ? ObservabilityMessages.get("observability.telemetry.triage.metrics.alert.summary")
+                    : ObservabilityMessages.get("observability.telemetry.triage.metrics.monitor.summary");
             String whyNow = activeAlerts > 0
-                    ? "活跃告警通常意味着影响已经明确,可先查看监控和告警上下文。"
-                    : "异常监控最能快速反映健康度变化,适合作为第一观察面。";
-            return new EntityTriageRecommendation("rule", FOCUS_METRICS, "优先查看监控", summary, whyNow, "查看监控", now);
+                    ? ObservabilityMessages.get("observability.telemetry.triage.metrics.alert.reason")
+                    : ObservabilityMessages.get("observability.telemetry.triage.metrics.monitor.reason");
+            return new EntityTriageRecommendation(
+                    "rule",
+                    FOCUS_METRICS,
+                    ObservabilityMessages.get("observability.telemetry.triage.metrics.title"),
+                    summary,
+                    whyNow,
+                    ObservabilityMessages.get("observability.telemetry.triage.metrics.action"),
+                    now);
         }
         if (traceSummary != null && traceSummary.getRecentErrorTraceCount() > 0 || !CollectionUtils.isEmpty(traceEvidence)) {
             return new EntityTriageRecommendation(
                     "rule",
                     FOCUS_TRACES,
-                    "优先查看链路",
-                    "当前链路侧已经出现错误或活跃调用,适合先查看调用路径和错误跨度。",
-                    "链路最容易帮助定位请求经过了哪些组件,以及失败发生在什么位置。",
-                    "查看链路",
+                    ObservabilityMessages.get("observability.telemetry.triage.traces.title"),
+                    ObservabilityMessages.get("observability.telemetry.triage.traces.summary"),
+                    ObservabilityMessages.get("observability.telemetry.triage.traces.reason"),
+                    ObservabilityMessages.get("observability.telemetry.triage.traces.action"),
                     now
             );
         }
@@ -987,20 +994,20 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
             return new EntityTriageRecommendation(
                     "rule",
                     FOCUS_LOGS,
-                    "优先查看日志",
-                    "当前已有可直接使用的日志检索线索,适合先查看最近错误文本和上下文。",
-                    "日志通常能最快补全异常细节,帮助确认具体报错和影响范围。",
-                    "查看日志",
+                    ObservabilityMessages.get("observability.telemetry.triage.logs.title"),
+                    ObservabilityMessages.get("observability.telemetry.triage.logs.summary"),
+                    ObservabilityMessages.get("observability.telemetry.triage.logs.reason"),
+                    ObservabilityMessages.get("observability.telemetry.triage.logs.action"),
                     now
             );
         }
         return new EntityTriageRecommendation(
                 "rule",
                 FOCUS_EVIDENCE,
-                "继续补充证据",
-                "当前还没有足够强的三信号线索,建议先确认接入状态或补充更多运行数据。",
-                "先确认最近是否已经收到指标、日志或链路数据,再继续定位。",
-                "查看证据",
+                ObservabilityMessages.get("observability.telemetry.triage.evidence.title"),
+                ObservabilityMessages.get("observability.telemetry.triage.evidence.summary"),
+                ObservabilityMessages.get("observability.telemetry.triage.evidence.reason"),
+                ObservabilityMessages.get("observability.telemetry.triage.evidence.action"),
                 now
         );
     }
@@ -1269,12 +1276,12 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
 
         List<String> segments = new ArrayList<>();
         if (StringUtils.hasText(compatibility)) {
-            segments.add("OTLP metrics 兼容性:" + switch (compatibility) {
-                case "supported" -> "支持";
-                case "partial" -> "部分支持";
-                case "unsupported" -> "不支持";
+            segments.add(ObservabilityMessages.format("observability.telemetry.metric.context.compatibility", switch (compatibility) {
+                case "supported" -> ObservabilityMessages.get("observability.telemetry.metric.context.supported");
+                case "partial" -> ObservabilityMessages.get("observability.telemetry.metric.context.partial");
+                case "unsupported" -> ObservabilityMessages.get("observability.telemetry.metric.context.unsupported");
                 default -> compatibility;
-            });
+            }));
         }
         if (StringUtils.hasText(greptimeCompatibility) || StringUtils.hasText(facadeCompatibility)) {
             segments.add("Greptime=" + defaultText(greptimeCompatibility, "unknown")
@@ -1287,10 +1294,10 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
             segments.add("monotonic=" + monotonic);
         }
         if ("summary".equals(metricType) && StringUtils.hasText(quantiles)) {
-            segments.add("Summary quantiles 已保留兼容元信息");
+            segments.add(ObservabilityMessages.get("observability.telemetry.metric.context.summary-quantiles"));
         }
         if ("histogram".equals(metricType) && StringUtils.hasText(histogramBuckets) && StringUtils.hasText(histogramBounds)) {
-            segments.add("Histogram buckets/bounds 已保留兼容元信息");
+            segments.add(ObservabilityMessages.get("observability.telemetry.metric.context.histogram-buckets"));
         }
         if (StringUtils.hasText(reason)) {
             segments.add(reason);
diff --git a/hertzbeat-observability/src/main/resources/observability_en_US.properties b/hertzbeat-observability/src/main/resources/observability_en_US.properties
new file mode 100644
index 0000000000..32d90f7f43
--- /dev/null
+++ b/hertzbeat-observability/src/main/resources/observability_en_US.properties
@@ -0,0 +1,120 @@
+observability.entity.next.review-alerts.title=Review active alerts
+observability.entity.next.review-alerts.description={0} active alerts are linked to this entity. Confirm the impact scope and current status first.
+observability.entity.next.review-alerts.action=View active alerts
+observability.entity.next.complete-owner.title=Assign owner
+observability.entity.next.complete-owner.description=This entity has no clear owner yet, so troubleshooting and governance actions lack accountability.
+observability.entity.next.complete-owner.action=Assign owner
+observability.entity.next.complete-runbook.title=Add runbook
+observability.entity.next.complete-runbook.description=Add a runbook or response entry first so alerts can lead directly to recovery.
+observability.entity.next.complete-runbook.action=Add runbook
+observability.entity.next.bind-monitor.title=Bind monitors
+observability.entity.next.bind-monitor.description=The entity exists but has no bound monitor tasks yet, so the current status can only stay at catalog metadata level.
+observability.entity.next.bind-monitor.action=Link monitors
+observability.entity.next.abnormal-monitors.title=Review abnormal monitors
+observability.entity.next.abnormal-monitors.description={0} monitors are abnormal. Confirm which evidence is most useful for deeper investigation.
+observability.entity.next.abnormal-monitors.action=View core monitors
+observability.entity.next.open-discovery.title=Merge evidence into entity
+observability.entity.next.open-discovery.description=There is not enough monitor, identity, or log evidence yet. Return to the discovery workbench to merge evidence first.
+observability.entity.next.open-discovery.action=Open discovery workbench
+observability.entity.next.inspect-logs.title=Review log clues
+observability.entity.next.inspect-logs.description=Log entry points are ready. Check whether OTel resource filters or fallback queries can locate the issue quickly.
+observability.entity.next.inspect-logs.action=Review log clues
+observability.entity.next.review-relations.title=Add key relations
+observability.entity.next.review-relations.description=Critical upstream and downstream relations are incomplete. Add the most important dependency boundaries first.
+observability.entity.next.review-relations.action=View relations
+observability.entity.next.fallback.title=Continue investigating this entity
+observability.entity.next.fallback.description=Catalog ownership and evidence are ready. Continue deeper analysis from logs or monitor status.
+observability.entity.next.fallback.action=Review log clues
+observability.entity.return-label.fallback=Entity detail
+observability.telemetry.metric.entity-monitor-status=Entity monitor status
+observability.telemetry.triage.metrics.alert.summary=Active alerts already exist. Confirm whether the monitor plane is still abnormal.
+observability.telemetry.triage.metrics.monitor.summary=The monitor plane has abnormal or fluctuating signals. Start with metrics and monitor status.
+observability.telemetry.triage.metrics.alert.reason=Active alerts usually mean the impact is clear. Review monitor and alert context first.
+observability.telemetry.triage.metrics.monitor.reason=Abnormal monitors often show health changes fastest, making them a good first observation surface.
+observability.telemetry.triage.metrics.title=Review monitors first
+observability.telemetry.triage.metrics.action=View monitors
+observability.telemetry.triage.traces.title=Review traces first
+observability.telemetry.triage.traces.summary=Trace-side errors or active calls are present. Inspect call paths and error spans first.
+observability.telemetry.triage.traces.reason=Traces help identify which components a request passed through and where the failure happened.
+observability.telemetry.triage.traces.action=View traces
+observability.telemetry.triage.logs.title=Review logs first
+observability.telemetry.triage.logs.summary=Direct log retrieval clues are available. Inspect recent error text and surrounding context first.
+observability.telemetry.triage.logs.reason=Logs usually fill in exception details fastest and help confirm concrete errors and impact scope.
+observability.telemetry.triage.logs.action=View logs
+observability.telemetry.triage.evidence.title=Collect more evidence
+observability.telemetry.triage.evidence.summary=There are not enough strong three-signal clues yet. Check intake status or add more runtime data first.
+observability.telemetry.triage.evidence.reason=Confirm whether recent metrics, logs, or traces have arrived before continuing the investigation.
+observability.telemetry.triage.evidence.action=View evidence
+observability.telemetry.metric.context.compatibility=OTLP metrics compatibility: {0}
+observability.telemetry.metric.context.supported=supported
+observability.telemetry.metric.context.partial=partial support
+observability.telemetry.metric.context.unsupported=unsupported
+observability.telemetry.metric.context.summary-quantiles=Summary quantiles compatibility metadata is retained.
+observability.telemetry.metric.context.histogram-buckets=Histogram buckets and bounds compatibility metadata is retained.
+observability.otlp.metric.compatibility.unknown-type=Unknown OTLP metric type. The current HertzBeat facade does not recognize it yet.
+observability.otlp.metric.compatibility.histogram.reason=Histogram metrics are written to Greptime, while the current HertzBeat facade keeps representative values and bucket/bounds metadata without full histogram query semantics.
+observability.otlp.metric.compatibility.exponential-histogram.reason=The current Greptime OTLP metrics data model does not support ExponentialHistogram. HertzBeat keeps representative values and compatibility metadata.
+observability.otlp.metric.compatibility.summary.reason=Summary quantile semantics are limited by the Greptime data model and the current HertzBeat facade. Summary and quantile metadata is retained.
+observability.otlp.metric.greptime.reason.supported=The current Greptime OTLP metrics data model supports this type.
+observability.otlp.metric.greptime.reason.partial=The current Greptime OTLP metrics data model preserves this type only partially.
+observability.otlp.metric.greptime.reason.unsupported=The current Greptime OTLP metrics data model does not support this type.
+observability.otlp.metric.facade.reason.supported=The current HertzBeat facade can consume this type directly.
+observability.otlp.metric.facade.reason.partial=The current HertzBeat facade keeps only representative values and compatibility metadata.
+observability.otlp.metric.facade.reason.unsupported=The current HertzBeat facade does not support this type.
+observability.otlp.overview.event.metrics.title=Metrics received
+observability.otlp.overview.event.metrics.copy=Recent metrics data received
+observability.otlp.overview.metrics.active=Recent queryable metrics data has been received.
+observability.otlp.overview.metrics.inactive=No metrics data has arrived in the last 24 hours.
+observability.otlp.overview.logs.active=Logs data has arrived in the last 24 hours.
+observability.otlp.overview.logs.inactive=No logs data has arrived in the last 24 hours.
+observability.otlp.overview.traces.active=Trace data has arrived in the last 24 hours.
+observability.otlp.overview.traces.inactive=No trace data has arrived in the last 24 hours.
+observability.otlp.overview.metrics.mode.mixed=OTLP + entity monitors
+observability.otlp.overview.metrics.mode.monitor=Entity monitors
+observability.otlp.readiness.collector.title=Collector cluster
+observability.otlp.readiness.collector.unregistered=No Collector is registered.
+observability.otlp.readiness.collector.deploy=Deploy a Collector or use the built-in intake endpoint first.
+observability.otlp.readiness.collector.online={0} / {1} online
+observability.otlp.readiness.collector.accepting=Collector nodes can receive tasks.
+observability.otlp.readiness.collector.offline={0} collector nodes are offline.
+observability.otlp.readiness.collector.all-offline=All collector nodes are offline.
+observability.otlp.readiness.storage.title=History storage
+observability.otlp.readiness.storage.disabled=History storage is not enabled.
+observability.otlp.readiness.storage.check-config=Check the history storage configuration.
+observability.otlp.readiness.storage.available={0} / {1} available
+observability.otlp.readiness.storage.reader-available=HistoryDataReader is available.
+observability.otlp.readiness.storage.partial=Some history storage backends are unavailable.
+observability.otlp.readiness.query.title=Query service
+observability.otlp.readiness.query.available=Metrics, logs, and traces queries are available.
+observability.otlp.readiness.query.promql-history=PromQL and history queries are available.
+observability.otlp.readiness.query.partial=Some query capabilities are available.
+observability.otlp.readiness.query.promql-only=PromQL is available. History queries need checking.
+observability.otlp.readiness.query.history-only=History queries are available. PromQL needs checking.
+observability.otlp.readiness.query.unavailable=Query service is unavailable.
+observability.otlp.readiness.query.check-config=Check the PromQL and history query configuration.
+observability.otlp.readiness.greptime.disabled=GreptimeDB is not enabled.
+observability.otlp.readiness.greptime.other-storage=Another history storage backend is in use, or storage is not configured yet.
+observability.otlp.readiness.greptime.sql-not-ready=GreptimeDB is enabled, but the SQL executor is not ready.
+observability.otlp.readiness.greptime.check-http=Check the GreptimeDB HTTP configuration.
+observability.otlp.readiness.greptime.sql-ok=SQL self-check passed.
+observability.otlp.readiness.greptime.select-ok=SELECT 1 succeeded.
+observability.otlp.readiness.greptime.sql-failed=SQL self-check failed.
+observability.otlp.readiness.greptime.check-connection=Check the GreptimeDB connection.
+observability.otlp.guide.snippet.python.http.comment=All signals are sent to the HertzBeat OTLP HTTP endpoint:
+observability.otlp.guide.snippet.python.grpc.comment=All signals are sent to the HertzBeat OTLP gRPC endpoint:
+observability.otlp.guide.metrics.http.description=Send metrics data to this OTLP HTTP endpoint. After data arrives, continue from monitors and entity details.
+observability.otlp.guide.metrics.http.note=If monitor tasks are already configured, the data continues to appear in the corresponding monitors.
+observability.otlp.guide.logs.http.description=Send logs data to this OTLP HTTP endpoint. After data arrives, review it from log management.
+observability.otlp.guide.logs.http.note=Use an intake token for authentication.
+observability.otlp.guide.traces.http.description=Send trace data to this OTLP HTTP endpoint. After data arrives, review it from trace management.
+observability.otlp.guide.traces.http.note=The system tries to associate data with entities from service information automatically.
+observability.otlp.guide.metrics.grpc.description=Send metrics data to this OTLP gRPC endpoint. This fits services already using an OpenTelemetry Collector or language SDK.
+observability.otlp.guide.metrics.grpc.note=After data arrives, continue from monitors and entity details.
+observability.otlp.guide.logs.grpc.description=Send logs data to this OTLP gRPC endpoint. After data arrives, review it from log management.
+observability.otlp.guide.logs.grpc.note=Use the Authorization Bearer intake token for authentication.
+observability.otlp.guide.traces.grpc.description=Send trace data to this OTLP gRPC endpoint. After data arrives, review it from trace management.
+observability.otlp.guide.traces.grpc.note=The system tries to associate data with entities from service information automatically.
+observability.otlp.guide.snippet.java-env=Java environment variables
+observability.otlp.guide.snippet.python-resource=Python resource attributes
+observability.otlp.metrics-console.no-context=Missing service context for building the OTLP metrics query.
+observability.otlp.metrics-console.promql-unavailable=No available PromQL metrics query executor is configured in the current environment.
diff --git a/hertzbeat-observability/src/main/resources/observability_zh_CN.properties b/hertzbeat-observability/src/main/resources/observability_zh_CN.properties
new file mode 100644
index 0000000000..a07bf338e6
--- /dev/null
+++ b/hertzbeat-observability/src/main/resources/observability_zh_CN.properties
@@ -0,0 +1,120 @@
+observability.entity.next.review-alerts.title=处理活跃告警
+observability.entity.next.review-alerts.description={0} 条活跃告警已经关联到当前实体,先确认影响范围和当前状态。
+observability.entity.next.review-alerts.action=查看活跃告警
+observability.entity.next.complete-owner.title=补负责人
+observability.entity.next.complete-owner.description=当前实体还没有明确负责人,排障和治理动作缺少责任归属。
+observability.entity.next.complete-owner.action=补负责人
+observability.entity.next.complete-runbook.title=补处置手册
+observability.entity.next.complete-runbook.description=先补一条 runbook 或处置入口,告警后才能直接推动恢复。
+observability.entity.next.complete-runbook.action=补处置手册
+observability.entity.next.bind-monitor.title=补绑监控
+observability.entity.next.bind-monitor.description=实体已经存在,但还没有绑定任何监控任务,当前状态只能停留在目录元数据层。
+observability.entity.next.bind-monitor.action=关联监控
+observability.entity.next.abnormal-monitors.title=查看异常监控
+observability.entity.next.abnormal-monitors.description={0} 个监控当前异常,建议先确认哪个证据最值得继续排查。
+observability.entity.next.abnormal-monitors.action=查看核心监控
+observability.entity.next.open-discovery.title=归并证据到实体
+observability.entity.next.open-discovery.description=当前还没有形成足够的监控、身份或日志线索,建议先回到发现工作台归并证据。
+observability.entity.next.open-discovery.action=打开发现工作台
+observability.entity.next.inspect-logs.title=查看日志线索
+observability.entity.next.inspect-logs.description=日志入口已经具备,先确认 OTel resource 或 fallback 查询是否能快速定位异常。
+observability.entity.next.inspect-logs.action=查看日志线索
+observability.entity.next.review-relations.title=补关键关系
+observability.entity.next.review-relations.description=关键上下游关系还不完整,建议先补齐最关键的依赖边界。
+observability.entity.next.review-relations.action=查看关系
+observability.entity.next.fallback.title=继续排查当前实体
+observability.entity.next.fallback.description=目录归属和证据都已经具备,可以直接从日志或监控继续深入分析。
+observability.entity.next.fallback.action=查看日志线索
+observability.entity.return-label.fallback=实体详情
+observability.telemetry.metric.entity-monitor-status=实体监控状态
+observability.telemetry.triage.metrics.alert.summary=当前已有活跃告警,建议先确认监控面是否正在持续异常。
+observability.telemetry.triage.metrics.monitor.summary=当前监控面已经出现异常或波动,建议先从指标和监控状态入手。
+observability.telemetry.triage.metrics.alert.reason=活跃告警通常意味着影响已经明确,可先查看监控和告警上下文。
+observability.telemetry.triage.metrics.monitor.reason=异常监控最能快速反映健康度变化,适合作为第一观察面。
+observability.telemetry.triage.metrics.title=优先查看监控
+observability.telemetry.triage.metrics.action=查看监控
+observability.telemetry.triage.traces.title=优先查看链路
+observability.telemetry.triage.traces.summary=当前链路侧已经出现错误或活跃调用,适合先查看调用路径和错误跨度。
+observability.telemetry.triage.traces.reason=链路最容易帮助定位请求经过了哪些组件,以及失败发生在什么位置。
+observability.telemetry.triage.traces.action=查看链路
+observability.telemetry.triage.logs.title=优先查看日志
+observability.telemetry.triage.logs.summary=当前已有可直接使用的日志检索线索,适合先查看最近错误文本和上下文。
+observability.telemetry.triage.logs.reason=日志通常能最快补全异常细节,帮助确认具体报错和影响范围。
+observability.telemetry.triage.logs.action=查看日志
+observability.telemetry.triage.evidence.title=继续补充证据
+observability.telemetry.triage.evidence.summary=当前还没有足够强的三信号线索,建议先确认接入状态或补充更多运行数据。
+observability.telemetry.triage.evidence.reason=先确认最近是否已经收到指标、日志或链路数据,再继续定位。
+observability.telemetry.triage.evidence.action=查看证据
+observability.telemetry.metric.context.compatibility=OTLP metrics 兼容性:{0}
+observability.telemetry.metric.context.supported=支持
+observability.telemetry.metric.context.partial=部分支持
+observability.telemetry.metric.context.unsupported=不支持
+observability.telemetry.metric.context.summary-quantiles=Summary quantiles 已保留兼容元信息
+observability.telemetry.metric.context.histogram-buckets=Histogram buckets/bounds 已保留兼容元信息
+observability.otlp.metric.compatibility.unknown-type=未知 OTLP metric 类型,当前未被 HertzBeat facade 识别。
+observability.otlp.metric.compatibility.histogram.reason=Histogram 指标已写入 Greptime,但 HertzBeat 当前 facade 仅保留代表值与 bucket/bounds 元信息,未提供完整 histogram 查询语义。
+observability.otlp.metric.compatibility.exponential-histogram.reason=Greptime 当前 OTLP metrics 数据模型不支持 ExponentialHistogram;HertzBeat 仅保留代表值与兼容性元信息。
+observability.otlp.metric.compatibility.summary.reason=Summary quantiles 语义受 Greptime 数据模型与当前 HertzBeat facade 限制,当前仅保留 summary/quantiles 元信息。
+observability.otlp.metric.greptime.reason.supported=Greptime 当前 OTLP metrics 数据模型支持该类型。
+observability.otlp.metric.greptime.reason.partial=Greptime 当前 OTLP metrics 数据模型仅部分保留该类型语义。
+observability.otlp.metric.greptime.reason.unsupported=Greptime 当前 OTLP metrics 数据模型不支持该类型。
+observability.otlp.metric.facade.reason.supported=HertzBeat 当前 facade 可直接消费该类型。
+observability.otlp.metric.facade.reason.partial=HertzBeat 当前 facade 仅保留代表值与兼容元信息。
+observability.otlp.metric.facade.reason.unsupported=HertzBeat 当前 facade 不支持该类型。
+observability.otlp.overview.event.metrics.title=指标已接入
+observability.otlp.overview.event.metrics.copy=最近已收到指标数据
+observability.otlp.overview.metrics.active=最近已收到可查看的指标数据
+observability.otlp.overview.metrics.inactive=最近 24 小时还没有指标数据
+observability.otlp.overview.logs.active=最近 24 小时已收到日志数据
+observability.otlp.overview.logs.inactive=最近 24 小时还没有日志数据
+observability.otlp.overview.traces.active=最近 24 小时已收到链路数据
+observability.otlp.overview.traces.inactive=最近 24 小时还没有链路数据
+observability.otlp.overview.metrics.mode.mixed=OTLP + 实体监控
+observability.otlp.overview.metrics.mode.monitor=实体监控
+observability.otlp.readiness.collector.title=Collector 集群
+observability.otlp.readiness.collector.unregistered=未注册 Collector
+observability.otlp.readiness.collector.deploy=先部署 Collector 或使用内置采集入口
+observability.otlp.readiness.collector.online={0} / {1} 在线
+observability.otlp.readiness.collector.accepting=采集节点可接收任务
+observability.otlp.readiness.collector.offline={0} 个采集节点离线
+observability.otlp.readiness.collector.all-offline=所有采集节点离线
+observability.otlp.readiness.storage.title=历史存储
+observability.otlp.readiness.storage.disabled=未启用历史存储
+observability.otlp.readiness.storage.check-config=检查历史存储配置
+observability.otlp.readiness.storage.available={0} / {1} 可用
+observability.otlp.readiness.storage.reader-available=HistoryDataReader 可用
+observability.otlp.readiness.storage.partial=部分历史存储不可用
+observability.otlp.readiness.query.title=查询服务
+observability.otlp.readiness.query.available=指标、日志和链路查询可用
+observability.otlp.readiness.query.promql-history=PromQL 与历史查询可用
+observability.otlp.readiness.query.partial=部分查询能力可用
+observability.otlp.readiness.query.promql-only=PromQL 可用,历史查询待检查
+observability.otlp.readiness.query.history-only=历史查询可用,PromQL 待检查
+observability.otlp.readiness.query.unavailable=查询服务不可用
+observability.otlp.readiness.query.check-config=检查 PromQL 与历史查询配置
+observability.otlp.readiness.greptime.disabled=未启用 GreptimeDB
+observability.otlp.readiness.greptime.other-storage=当前使用其他历史存储或尚未配置
+observability.otlp.readiness.greptime.sql-not-ready=GreptimeDB 已启用,SQL 执行器未就绪
+observability.otlp.readiness.greptime.check-http=检查 GreptimeDB HTTP 配置
+observability.otlp.readiness.greptime.sql-ok=SQL 自检通过
+observability.otlp.readiness.greptime.select-ok=SELECT 1 成功
+observability.otlp.readiness.greptime.sql-failed=SQL 自检失败
+observability.otlp.readiness.greptime.check-connection=检查 GreptimeDB 连接
+observability.otlp.guide.snippet.python.http.comment=所有 signals 统一发送到 HertzBeat OTLP HTTP 入口:
+observability.otlp.guide.snippet.python.grpc.comment=所有 signals 统一发送到 HertzBeat OTLP gRPC 入口:
+observability.otlp.guide.metrics.http.description=把指标数据发送到这个 OTLP HTTP 地址。收到数据后,可直接在监控和实体详情中继续查看。
+observability.otlp.guide.metrics.http.note=如果已经配置了监控任务,数据会继续显示在对应的监控项中。
+observability.otlp.guide.logs.http.description=把日志数据发送到这个 OTLP HTTP 地址。收到数据后,可直接前往日志管理查看。
+observability.otlp.guide.logs.http.note=使用接入令牌即可完成认证。
+observability.otlp.guide.traces.http.description=把链路数据发送到这个 OTLP HTTP 地址。收到数据后,可直接前往链路管理查看。
+observability.otlp.guide.traces.http.note=系统会根据服务信息自动尝试关联到实体。
+observability.otlp.guide.metrics.grpc.description=把指标数据发送到这个 OTLP gRPC 地址,适合已经使用 OpenTelemetry Collector 或语言 SDK 的服务。
+observability.otlp.guide.metrics.grpc.note=收到数据后,可直接在监控和实体详情中继续查看。
+observability.otlp.guide.logs.grpc.description=把日志数据发送到这个 OTLP gRPC 地址。收到数据后,可直接前往日志管理查看。
+observability.otlp.guide.logs.grpc.note=使用 Authorization Bearer 接入令牌即可完成认证。
+observability.otlp.guide.traces.grpc.description=把链路数据发送到这个 OTLP gRPC 地址。收到数据后,可直接前往链路管理查看。
+observability.otlp.guide.traces.grpc.note=系统会根据服务信息自动尝试关联到实体。
+observability.otlp.guide.snippet.java-env=Java 环境变量
+observability.otlp.guide.snippet.python-resource=Python 资源属性
+observability.otlp.metrics-console.no-context=缺少可用于构建 OTLP 指标查询的服务上下文。
+observability.otlp.metrics-console.promql-unavailable=当前环境未配置可用的 PromQL 指标查询执行器。
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java
index f7b11a189f..88fa4e03b0 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java
@@ -74,7 +74,8 @@ class OtlpIngestionControllerTest {
         );
         overview.setReadinessChecks(List.of(
                 new OtlpIngestionOverviewDto.ReadinessCheck(
-                        "collector", "Collector 集群", "success", "1 / 1 在线", "采集节点可接收任务", 1_710_000_000_100L)
+                        "collector", "Collector cluster", "success", "1 / 1 online",
+                        "Collector nodes can receive tasks.", 1_710_000_000_100L)
         ));
         when(otlpIngestionWorkspaceService.getOverview()).thenReturn(overview);
 
@@ -85,7 +86,7 @@ class OtlpIngestionControllerTest {
                 .andExpect(jsonPath("$.data.boundEntityCount").value(4))
                 .andExpect(jsonPath("$.data.logs.totalCount").value(5))
                 .andExpect(jsonPath("$.data.readinessChecks[0].key").value("collector"))
-                .andExpect(jsonPath("$.data.readinessChecks[0].summary").value("1 / 1 在线"));
+                .andExpect(jsonPath("$.data.readinessChecks[0].summary").value("1 / 1 online"));
 
         verify(otlpIngestionWorkspaceService).getOverview();
     }
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionServiceSourceLocalizationTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionServiceSourceLocalizationTest.java
new file mode 100644
index 0000000000..ae7576a549
--- /dev/null
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionServiceSourceLocalizationTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.service.impl;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.regex.Pattern;
+import org.junit.jupiter.api.Test;
+
+class OtlpIngestionServiceSourceLocalizationTest {
+
+    private static final List<Path> PRODUCTION_SOURCES = List.of(
+            Path.of("src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/"
+                    + "OtlpGrpcIngestionServiceImpl.java"),
+            Path.of("src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/"
+                    + "OtlpIngestionWorkspaceServiceImpl.java")
+    );
+    private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}");
+
+    @Test
+    void productionSourcesShouldNotContainHanScriptLiterals() throws IOException {
+        for (Path productionSource : PRODUCTION_SOURCES) {
+            String source = Files.readString(productionSource);
+            assertFalse(HAN_SCRIPT.matcher(source).find(),
+                    () -> "source file must not contain Han-script literals: " + productionSource);
+        }
+    }
+}
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java
index 4f3309d874..ab48464a6f 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java
@@ -36,6 +36,7 @@ import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData;
@@ -58,6 +59,7 @@ import org.apache.hertzbeat.warehouse.repository.MetricQueryRepository;
 import org.apache.hertzbeat.warehouse.db.GreptimeSqlQueryExecutor;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -89,8 +91,12 @@ class OtlpIngestionWorkspaceServiceImplTest {
 
     private ObservabilitySignalIntakeGateway observabilitySignalIntakeGateway;
 
+    private Locale previousLocale;
+
     @BeforeEach
     void setUp() {
+        previousLocale = Locale.getDefault();
+        Locale.setDefault(Locale.US);
         observabilitySignalIntakeGateway = new InMemoryObservabilitySignalIntakeGateway();
         otlpIngestionWorkspaceService = new OtlpIngestionWorkspaceServiceImpl(
                 entityTraceQueryService,
@@ -104,6 +110,11 @@ class OtlpIngestionWorkspaceServiceImplTest {
         );
     }
 
+    @AfterEach
+    void restoreLocale() {
+        Locale.setDefault(previousLocale);
+    }
+
     private void stubRecentLogs(LogEntry... logs) {
         when(logQueryRepository.queryRecentLogs(anyLong(), anyLong(), eq(20))).thenReturn(List.of(logs));
     }
@@ -208,16 +219,16 @@ class OtlpIngestionWorkspaceServiceImplTest {
                 overview.getReadinessChecks().stream().map(OtlpIngestionOverviewDto.ReadinessCheck::getKey).toList());
         assertTrue(overview.getReadinessChecks().stream().anyMatch(check -> "collector".equals(check.getKey())
                 && "warning".equals(check.getStatus())
-                && check.getSummary().contains("2 / 3 在线")));
+                && check.getSummary().contains("2 / 3 online")));
         assertTrue(overview.getReadinessChecks().stream().anyMatch(check -> "storage".equals(check.getKey())
                 && "success".equals(check.getStatus())
-                && check.getSummary().contains("1 / 1 可用")));
+                && check.getSummary().contains("1 / 1 available")));
         assertTrue(overview.getReadinessChecks().stream().anyMatch(check -> "query".equals(check.getKey())
                 && "success".equals(check.getStatus())
-                && check.getSummary().contains("指标、日志和链路查询可用")));
+                && check.getSummary().contains("Metrics, logs, and traces queries are available.")));
         assertTrue(overview.getReadinessChecks().stream().anyMatch(check -> "greptime".equals(check.getKey())
                 && "success".equals(check.getStatus())
-                && check.getSummary().contains("SQL 自检通过")));
+                && check.getSummary().contains("SQL self-check passed.")));
     }
 
     @Test
@@ -280,7 +291,7 @@ class OtlpIngestionWorkspaceServiceImplTest {
                 && snippet.getContent().contains("endpoint: demo.hertzbeat.apache.org:4317")
                 && snippet.getContent().contains("Authorization: \"Bearer <api-token>\"")));
         assertTrue(guide.getSignals().stream().filter(signal -> "grpc".equals(signal.getProtocol()))
-                .allMatch(signal -> signal.getNote() == null || !signal.getNote().contains("登录 token")));
+                .allMatch(signal -> signal.getNote() == null || !signal.getNote().contains("login token")));
         assertFalse(guide.getSnippets().isEmpty());
     }
 
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
index 9fbb86ddb3..e2fcb981ff 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
@@ -25,10 +25,15 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.LocalDateTime;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.regex.Pattern;
 import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
@@ -59,16 +64,45 @@ import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceQueryHintD
 import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceSummaryDto;
 import org.apache.hertzbeat.common.observability.model.ObservedEntityContext;
 import org.apache.hertzbeat.observability.traces.service.EntityTraceQueryService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 class EntityObservabilityGatewayImplTest {
 
+    private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}");
+    private static final Path ENTITY_OBSERVABILITY_GATEWAY_IMPL = Path.of(
+            "src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java");
+
     private final TelemetryIntakeServiceImpl telemetryIntakeService = Mockito.mock(TelemetryIntakeServiceImpl.class);
     private final EntityTraceQueryService entityTraceQueryService = Mockito.mock(EntityTraceQueryService.class);
     private final EntityObservabilityGatewayImpl gateway =
             new EntityObservabilityGatewayImpl(telemetryIntakeService, entityTraceQueryService);
 
+    private Locale previousLocale;
+
+    @BeforeEach
+    void useEnglishCatalog() {
+        previousLocale = Locale.getDefault();
+        Locale.setDefault(Locale.US);
+        ResourceBundle.clearCache();
+    }
+
+    @AfterEach
+    void restoreLocale() {
+        Locale.setDefault(previousLocale);
+        ResourceBundle.clearCache();
+    }
+
+    @Test
+    void sourceShouldNotContainHanScriptUserFacingCopy() throws Exception {
+        String source = Files.readString(ENTITY_OBSERVABILITY_GATEWAY_IMPL);
+
+        assertFalse(HAN_SCRIPT.matcher(source).find(),
+                "User-facing entity observability copy belongs in locale resources, not Java source");
+    }
+
     @Test
     void resolveEntityTraceSummaryShouldFallbackToQueryServiceWhenInactive() {
         ObservedEntityContext entityContext = ObservedEntityContext.from(null, Collections.emptyList());
@@ -132,7 +166,7 @@ class EntityObservabilityGatewayImplTest {
         EntityUnifiedEvidenceSummary unifiedEvidenceSummary =
                 new EntityUnifiedEvidenceSummary(3, true, true, true, 1L, 1, 1, 12L, List.of("metrics", "logs", "traces"));
         EntityTriageRecommendation triageRecommendation =
-                new EntityTriageRecommendation("focus_logs", "logs", "处理日志", "查看日志", "high", "进入日志", 13L);
+                new EntityTriageRecommendation("focus_logs", "logs", "Handle logs", "Review logs", "high", "Open logs", 13L);
 
         when(telemetryIntakeService.buildMetricEvidence(any(), any(), any())).thenReturn(metricEvidence);
         when(telemetryIntakeService.buildLogEvidence(any(), any(), any())).thenReturn(logEvidence);
@@ -161,7 +195,7 @@ class EntityObservabilityGatewayImplTest {
     @Test
     void enrichEntityLogQueryHintsShouldPreferEvidenceAndTraceFallbacks() {
         EntityLogQueryHint originalHint = new EntityLogQueryHint();
-        originalHint.setTitle("原始日志");
+        originalHint.setTitle("Original logs");
         originalHint.setResourceFilters(Map.of());
         originalHint.setSearchTerms(List.of());
 
@@ -195,7 +229,7 @@ class EntityObservabilityGatewayImplTest {
 
         assertEquals(1, enriched.size());
         EntityLogQueryHint first = enriched.getFirst();
-        assertEquals("原始日志", first.getTitle());
+        assertEquals("Original logs", first.getTitle());
         assertEquals(Map.of("service.name", "checkout"), first.getResourceFilters());
         assertEquals(List.of("checkout", "error"), first.getSearchTerms());
         assertEquals("trace-1", first.getTraceId());
@@ -211,7 +245,7 @@ class EntityObservabilityGatewayImplTest {
     @Test
     void buildEntityLogSummaryShouldUsePreferredHintAsSummarySource() {
         EntityLogQueryHint hint = new EntityLogQueryHint(
-                "日志入口",
+                "Log entry",
                 Map.of("service.name", "checkout"),
                 List.of("checkout", "error"),
                 "trace-1",
@@ -226,8 +260,8 @@ class EntityObservabilityGatewayImplTest {
         EntityLogSummaryInfo summary = gateway.buildEntityLogSummary(List.of(hint));
 
         assertEquals(1, summary.getHintCount());
-        assertEquals("日志入口", summary.getPreferredQueryType());
-        assertEquals("日志入口", summary.getPreferredQueryTitle());
+        assertEquals("Log entry", summary.getPreferredQueryType());
+        assertEquals("Log entry", summary.getPreferredQueryTitle());
         assertEquals(Map.of("service.name", "checkout"), summary.getPreferredResourceFilters());
         assertEquals(List.of("checkout", "error"), summary.getPreferredSearchTerms());
         assertEquals("checkout", summary.getFallbackSearchTerm());
@@ -494,7 +528,7 @@ class EntityObservabilityGatewayImplTest {
 
         entity.setDisplayName(null);
         assertEquals("checkout-service", gateway.buildEntityReturnLabel(entityContext));
-        assertEquals("实体详情", gateway.buildEntityReturnLabel(ObservedEntityContext.from(null, Collections.emptyList())));
+        assertEquals("Entity detail", gateway.buildEntityReturnLabel(ObservedEntityContext.from(null, Collections.emptyList())));
     }
 
     @Test
@@ -654,7 +688,7 @@ class EntityObservabilityGatewayImplTest {
     void buildEntityDiscoveryHandoffShouldCarryEntityContextAndPreferAlertQuery() {
         EntityResponseHandoffInfo handoff = gateway.buildEntityDiscoveryHandoff(
                 "/entities/1",
-                "结账服务",
+                "Checkout service",
                 "team-a",
                 "checkout",
                 "prod",
@@ -665,7 +699,7 @@ class EntityObservabilityGatewayImplTest {
         );
 
         assertEquals("/entities/1", handoff.getReturnTo());
-        assertEquals("结账服务", handoff.getReturnLabel());
+        assertEquals("Checkout service", handoff.getReturnLabel());
         assertEquals("alert-token", handoff.getQuery());
         assertEquals("team-a", handoff.getOwner());
         assertEquals("checkout", handoff.getSystem());
@@ -676,13 +710,13 @@ class EntityObservabilityGatewayImplTest {
     @Test
     void buildEntityEditorHandoffShouldSelectFocusByReadinessPriority() {
         EntityResponseHandoffInfo ownershipHandoff =
-                gateway.buildEntityEditorHandoff("/entities/1", "结账服务", false, true, true, true);
+                gateway.buildEntityEditorHandoff("/entities/1", "Checkout service", false, true, true, true);
         EntityResponseHandoffInfo relationHandoff =
-                gateway.buildEntityEditorHandoff("/entities/1", "结账服务", true, true, false, true);
+                gateway.buildEntityEditorHandoff("/entities/1", "Checkout service", true, true, false, true);
         EntityResponseHandoffInfo monitorHandoff =
-                gateway.buildEntityEditorHandoff("/entities/1", "结账服务", true, true, true, false);
+                gateway.buildEntityEditorHandoff("/entities/1", "Checkout service", true, true, true, false);
         EntityResponseHandoffInfo defaultHandoff =
-                gateway.buildEntityEditorHandoff("/entities/1", "结账服务", true, true, true, true);
+                gateway.buildEntityEditorHandoff("/entities/1", "Checkout service", true, true, true, true);
 
         assertEquals("ownership", ownershipHandoff.getFocus());
         assertEquals("relations", relationHandoff.getFocus());
@@ -694,7 +728,7 @@ class EntityObservabilityGatewayImplTest {
     void buildEntityResponseHandoffsShouldAssembleAllTargetsFromRequest() {
         ObserveEntity entity = new ObserveEntity();
         entity.setName("checkout");
-        entity.setDisplayName("结账服务");
+        entity.setDisplayName("Checkout service");
         EntityIdentity identity = EntityIdentity.builder()
                 .identityKey("service.name")
                 .identityValue("checkout")
@@ -704,7 +738,7 @@ class EntityObservabilityGatewayImplTest {
         EntityTraceSummaryDto traceSummary = new EntityTraceSummaryDto(1, 1, 20L, true, "trace-1");
         EntityResponseHandoffsRequest request = new EntityResponseHandoffsRequest(
                 "/entities/1",
-                "结账服务",
+                "Checkout service",
                 "team-a",
                 "checkout-system",
                 "prod",
@@ -758,7 +792,7 @@ class EntityObservabilityGatewayImplTest {
         );
         EntityResponseHandoffsRequest request = new EntityResponseHandoffsRequest(
                 "/entities/1",
-                "结账服务",
+                "Checkout service",
                 "team-a",
                 "checkout-system",
                 "prod",
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImplTest.java
index e60f26de0a..0ba7c3bec9 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImplTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImplTest.java
@@ -24,9 +24,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.regex.Pattern;
 import org.apache.hertzbeat.common.entity.log.LogEntry;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
@@ -44,6 +49,8 @@ import org.apache.hertzbeat.common.observability.model.CodeNavigationHint;
 import org.apache.hertzbeat.common.observability.model.ObservedEntityContext;
 import org.apache.hertzbeat.common.util.JsonUtil;
 import org.apache.hertzbeat.warehouse.repository.LogQueryRepository;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
@@ -51,10 +58,37 @@ import org.mockito.Mockito;
 
 class TelemetryIntakeServiceImplTest {
 
+    private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}");
+    private static final Path TELEMETRY_INTAKE_SERVICE_IMPL = Path.of(
+            "src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java");
+
     private final LogQueryRepository logQueryRepository = Mockito.mock(LogQueryRepository.class);
     private final TelemetryIntakeServiceImpl telemetryIntakeService =
             new TelemetryIntakeServiceImpl(logQueryRepository);
 
+    private Locale previousLocale;
+
+    @BeforeEach
+    void useEnglishCatalog() {
+        previousLocale = Locale.getDefault();
+        Locale.setDefault(Locale.US);
+        ResourceBundle.clearCache();
+    }
+
+    @AfterEach
+    void restoreLocale() {
+        Locale.setDefault(previousLocale);
+        ResourceBundle.clearCache();
+    }
+
+    @Test
+    void sourceShouldNotContainHanScriptUserFacingCopy() throws Exception {
+        String source = Files.readString(TELEMETRY_INTAKE_SERVICE_IMPL);
+
+        assertFalse(HAN_SCRIPT.matcher(source).find(),
+                "User-facing telemetry intake copy belongs in locale resources, not Java source");
+    }
+
     @Test
     void buildCodeNavigationHintUsesEntityCodeLocations() {
         ObservedEntityContext entityContext = buildObservedEntityContextWithCodeLocations("https://github.com/apache/hertzbeat.git");
@@ -68,7 +102,7 @@ class TelemetryIntakeServiceImplTest {
                         "code.filepath", "hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObserveEntityServiceImpl.java"
                 ),
                 List.of("localOtlpTraceIngest"),
-                "查看代码"
+                "View code"
         );
 
         assertNotNull(hint);
@@ -657,7 +691,7 @@ class TelemetryIntakeServiceImplTest {
                         "instance", "e2e",
                         "otlp.metric.compatibility", "partial",
                         "otlp.metric.compatibility.reason",
-                        "Summary quantiles 当前仅作为兼容元信息保留,尚未作为一等查询语义暴露。",
+                        "Summary quantiles are retained as compatibility metadata only.",
                         "otlp.metric.greptime.compatibility", "partial",
                         "otlp.metric.facade.compatibility", "partial",
                         "otlp.metric.summary.quantiles",
@@ -708,7 +742,7 @@ class TelemetryIntakeServiceImplTest {
         assertEquals("partial", metricEvidence.getAttributes().get("otlp.metric.compatibility"));
         assertTrue(metricEvidence.getAttributes().get("otlp.metric.summary.quantiles").contains("\"quantile\":0.95"));
         assertNotNull(metricEvidence.getOtelContext());
-        assertTrue(metricEvidence.getOtelContext().contains("部分支持"));
+        assertTrue(metricEvidence.getOtelContext().contains("partial support"));
         assertTrue(metricEvidence.getOtelContext().contains("Summary quantiles"));
     }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 12/50: feat(web-next): restore settings platform parity

Posted by zh...@apache.org.
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 2991c304895cd6040303296ccb976b8fe613c1ce
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 01:07:28 2026 +0800

    feat(web-next): restore settings platform parity
---
 web-next/app/setting/page.tsx                      |   4 +-
 web-next/app/setting/settings/config/page.test.tsx |  82 ++-
 web-next/app/setting/settings/config/page.tsx      | 153 +----
 .../config/{page.tsx => setting-config-page.tsx}   |  48 +-
 web-next/app/setting/settings/layout.tsx           |  30 +-
 .../app/setting/settings/mcp-server/page.test.ts   |  41 ++
 web-next/app/setting/settings/mcp-server/page.tsx  |   4 +-
 .../setting/settings/object-store/page.test.tsx    |  68 +-
 .../app/setting/settings/object-store/page.tsx     | 173 +----
 .../{page.tsx => setting-object-store-page.tsx}    |  41 +-
 web-next/app/setting/settings/page.tsx             |   4 +-
 web-next/app/setting/settings/server/page.test.tsx | 128 ++--
 web-next/app/setting/settings/server/page.tsx      | 461 +------------
 .../server/{page.tsx => setting-server-page.tsx}   |  85 ++-
 .../settings/settings-layout-client.test.tsx       |  73 ++
 .../{layout.tsx => settings-layout-client.tsx}     |   6 +-
 web-next/app/setting/settings/token/page.test.tsx  | 132 +++-
 web-next/app/setting/settings/token/page.tsx       | 193 +-----
 .../setting/settings/token/setting-token-page.tsx  | 498 ++++++++++++++
 web-next/app/setting/status/page.test.tsx          |  38 +-
 web-next/app/setting/status/page.tsx               | 276 +-------
 .../status/{page.tsx => setting-status-page.tsx}   |  81 ++-
 .../pages/settings-surface-page.test.tsx           |  21 +-
 .../components/pages/settings-surface-page.tsx     |   2 +-
 .../pages/status-setting-surface.test.tsx          | 365 +++++++++-
 .../components/pages/status-setting-surface.tsx    | 733 ++++++++++++++++-----
 .../settings/settings-console-shell.test.tsx       |  26 +-
 .../components/settings/settings-console-shell.tsx |  21 +-
 .../settings/settings-dialog-form.test.tsx         |  36 +
 .../components/settings/settings-dialog-form.tsx   |  19 +-
 .../components/settings/settings-form.test.tsx     |   6 +
 web-next/components/settings/settings-form.tsx     |   3 +
 .../settings/settings-summary-list.test.tsx        |   4 +
 .../components/settings/settings-summary-list.tsx  |   2 +
 web-next/lib/object-store/view-model.test.ts       |  28 +
 web-next/lib/object-store/view-model.ts            |  16 +-
 web-next/lib/setting-config/controller.test.ts     |  19 +-
 web-next/lib/setting-config/controller.ts          |   9 +-
 web-next/lib/setting-config/view-model.test.ts     |  24 +-
 web-next/lib/setting-config/view-model.ts          |  15 +-
 web-next/lib/setting-server/view-model.test.ts     |  79 ++-
 web-next/lib/setting-server/view-model.ts          |  68 +-
 web-next/lib/setting-settings-layout/navigation.ts |   9 +
 .../lib/setting-settings-layout/view-model.test.ts |  17 +-
 .../lib/setting-settings-layout/view-model.tsx     |   6 +-
 web-next/lib/setting-status/view-model.test.ts     | 225 ++++++-
 web-next/lib/setting-status/view-model.ts          |  73 +-
 web-next/lib/setting-token/controller.test.ts      |  16 +-
 web-next/lib/setting-token/controller.ts           |   9 +
 web-next/lib/setting-token/view-model.test.ts      |  34 +
 web-next/lib/setting-token/view-model.ts           |  28 +-
 web-next/lib/settings-surface/view-model.test.ts   |  20 +-
 web-next/lib/settings-surface/view-model.ts        |  70 +-
 53 files changed, 2828 insertions(+), 1794 deletions(-)

diff --git a/web-next/app/setting/page.tsx b/web-next/app/setting/page.tsx
index 260c16efdb..8e1281bc90 100644
--- a/web-next/app/setting/page.tsx
+++ b/web-next/app/setting/page.tsx
@@ -1,9 +1,9 @@
 import { redirect } from 'next/navigation';
-import { buildCompatRedirectTarget, type SearchParamsRecord } from '../../lib/compat/search-params';
+import { buildSettingsCompatRouteUrl, type SearchParamsRecord } from '../../lib/setting-settings-layout/navigation';
 
 export default async function SettingIndexPage(props: {
   searchParams?: Promise<SearchParamsRecord>;
 }) {
   const resolvedSearchParams = await props?.searchParams;
-  redirect(buildCompatRedirectTarget('/setting/settings/config', resolvedSearchParams));
+  redirect(buildSettingsCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/setting/settings/config/page.test.tsx b/web-next/app/setting/settings/config/page.test.tsx
index 73bfa8c9d5..46c5fc6c1c 100644
--- a/web-next/app/setting/settings/config/page.test.tsx
+++ b/web-next/app/setting/settings/config/page.test.tsx
@@ -1,4 +1,6 @@
 import React from 'react';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../../test/i18n-test-helper';
@@ -25,22 +27,7 @@ const setLocale = vi.hoisted(() => vi.fn(async () => {}));
 vi.mock('../../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
-      locale: 'en-US',
-      overrides: {
-        'settings.system-config': '系统配置',
-        'settings.system-config.locale': '系统语言',
-        'settings.system-config.timezone': '系统时区',
-        'settings.system-config.theme': '系统主题',
-        'settings.system-config.ok': '确认更新',
-        'settings.system-config.locale.en_US': '英语(en_US)',
-        'settings.system-config.locale.zh_CN': '简体中文(zh_CN)',
-        'settings.system-config.locale.zh_TW': '繁體中文(zh_TW)',
-        'settings.system-config.locale.ja-JP': '日语(ja_JP)',
-        'settings.system-config.locale.pt_BR': '葡萄牙语(pt_BR)',
-        'settings.system-config.theme.default': '默认主题',
-        'settings.system-config.theme.dark': '深色主题',
-        'settings.system-config.theme.compact': '紧凑主题'
-      }
+      locale: 'zh-CN'
     }),
     setLocale
   })
@@ -49,13 +36,15 @@ vi.mock('../../../../components/providers/i18n-provider', () => ({
 vi.mock('../../../../components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    loadingCopy
   }: {
     children: (data: any) => React.ReactNode;
     load: () => Promise<unknown>;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return <div data-client-workbench="true" data-loading-copy={loadingCopy}>{children(mockState.renderData)}</div>;
   }
 }));
 
@@ -73,9 +62,20 @@ vi.mock('../../../../components/settings/settings-form', () => ({
       {children}
     </label>
   ),
-  SettingsFormSelect: ({ children, ...props }: any) => <select data-settings-form-control="cold-select-control" {...props}>{children}</select>,
-  SettingsFormActions: ({ children, ...props }: any) => <div {...props}>{children}</div>,
-  SettingsFormFeedback: ({ children }: any) => <div data-settings-form-feedback="true">{children}</div>
+  SettingsFormSelect: ({ children, searchable, searchPlaceholder, ...props }: any) => (
+    <select
+      data-settings-form-control="cold-select-control"
+      data-settings-form-select-width="angular-400px"
+      data-settings-form-select-style="angular-centered-bold"
+      data-settings-form-select-dropdown-style="angular-bold-larger"
+      data-cold-select-search={searchable ? 'angular-nz-show-search' : undefined}
+      data-cold-select-search-placeholder={searchPlaceholder}
+      {...props}
+    >
+      {children}
+    </select>
+  ),
+  SettingsFormActions: ({ children, ...props }: any) => <div {...props}>{children}</div>
 }));
 
 vi.mock('../../../../components/ui/button', () => ({
@@ -107,12 +107,25 @@ describe('setting config page', () => {
     const html = renderToStaticMarkup(<SettingConfigPage />);
 
     expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-loading-copy="正在加载系统配置。"');
     expect(html).toContain('data-settings-console-title="true"');
     expect(html).toContain('系统配置');
     expect(html).toContain('data-setting-config-surface="otlp-cold-system-config"');
     expect(html).toContain('data-setting-config-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-setting-config-layout="full-width-settings-form"');
     expect(html).toContain('data-setting-config-form="cold-settings-form"');
+    expect(html).toContain('data-setting-config-apply-contract="angular-apply-notify-reload"');
+    expect(html).toContain('data-setting-config-runtime-locale="underscore-to-hyphen"');
+    expect(html).toContain('data-setting-config-select-contract="angular-400px-centered-bold"');
+    expect(html).toContain('data-settings-form-select-width="angular-400px"');
+    expect(html).toContain('data-settings-form-select-style="angular-centered-bold"');
+    expect(html).toContain('data-settings-form-select-dropdown-style="angular-bold-larger"');
+    expect(html).toContain('data-setting-config-timezone-search-contract="angular-nz-show-search"');
+    expect(html).toContain('data-cold-select-search="angular-nz-show-search"');
+    expect(html).toContain('data-setting-config-timezone-dropdown-width-contract="angular-dropdown-match-select-width-false"');
+    expect(html).toContain('data-setting-config-select-kind="locale"');
+    expect(html).toContain('data-setting-config-select-kind="timezone"');
+    expect(html).toContain('data-setting-config-select-kind="theme"');
     expect(html).toContain('data-setting-config-actions="standard-equal-buttons"');
     expect(html).toContain('data-settings-form-owner="cold-settings-form-owner"');
     expect(html).toContain('系统语言');
@@ -138,4 +151,31 @@ describe('setting config page', () => {
     expect(apiMessageGet).toHaveBeenNthCalledWith(1, '/config/system');
     expect(apiMessageGet).toHaveBeenNthCalledWith(2, '/config/timezones');
   });
+
+  it('keeps setting config remounts on a short settled cache window while saves invalidate it', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/settings/config/setting-config-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_CONFIG_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['setting-config', '/config/system', '/config/timezones', reloadVersion].join(':')");
+    expect(source).toContain('void reloadVersion');
+    expect(source).toContain('[reloadVersion]');
+    expect(source).toContain('setReloadVersion(version => version + 1)');
+    expect(source).toContain('cacheKey={settingConfigCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_CONFIG_SETTLED_CACHE_TTL_MS}');
+  });
+
+  it('keeps the save path on the Angular apply notification and reload contract', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/settings/config/setting-config-page.tsx'), 'utf8');
+
+    expect(source).toContain("t('common.notify.apply-success')");
+    expect(source).toContain("t('common.notify.apply-fail')");
+    expect(source).toContain('data-setting-config-apply-contract="angular-apply-notify-reload"');
+    expect(source).toContain('data-setting-config-runtime-locale="underscore-to-hyphen"');
+    expect(source).toContain('data-setting-config-select-contract="angular-400px-centered-bold"');
+    expect(source).toContain('data-setting-config-timezone-search-contract="angular-nz-show-search"');
+    expect(source).toContain('data-setting-config-timezone-dropdown-width-contract="angular-dropdown-match-select-width-false"');
+    expect(source).toContain('searchable');
+    expect(source).toContain('data-setting-config-apply-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('reload: () => reloadWorkbenchWindow(window.location)');
+  });
 });
diff --git a/web-next/app/setting/settings/config/page.tsx b/web-next/app/setting/settings/config/page.tsx
index e252e8889f..de18e283cf 100644
--- a/web-next/app/setting/settings/config/page.tsx
+++ b/web-next/app/setting/settings/config/page.tsx
@@ -1,152 +1,7 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback, useState } from 'react';
-import { ClientWorkbench } from '../../../../components/workbench/client-workbench';
-import { useI18n } from '../../../../components/providers/i18n-provider';
-import { SettingsConsoleTitle } from '../../../../components/settings/settings-console-shell';
-import {
-  SettingsForm,
-  SettingsFormActions,
-  SettingsFormFeedback,
-  SettingsFormField,
-  SettingsFormSelect
-} from '../../../../components/settings/settings-form';
-import { Button } from '../../../../components/ui/button';
-import { coldOpsCatalogVisual } from '../../../../lib/cold-ops-visual';
-import { apiMessageGet, apiMessagePost } from '../../../../lib/api-client';
-import { loadSystemConfigData, persistSystemConfig } from '../../../../lib/setting-config/controller';
-import {
-  canSaveSystemConfig,
-  buildTimezoneOptionLabel,
-  SYSTEM_CONFIG_LOCALE_OPTIONS,
-  SYSTEM_CONFIG_THEME_OPTIONS,
-  resolveConfigSaveFeedback,
-  resolveSystemConfigDraft,
-  updateSystemConfigField,
-  updateSystemConfigTimezone,
-  type SaveTone
-} from '../../../../lib/setting-config/view-model';
-import { applyWorkbenchTheme, reloadWorkbenchWindow } from '../../../../lib/workbench-theme';
-import type { SystemConfig, TimezoneOption } from '../../../../lib/types';
+import SettingConfigPage from './setting-config-page';
 
-type ConfigData = {
-  config: SystemConfig;
-  timezones: TimezoneOption[];
-};
-
-const coldConfigVisual = coldOpsCatalogVisual;
-
-const coldPrimaryButtonClassName =
-  'h-8 min-w-[104px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-[#f8fafc]';
-
-export default function SettingConfigPage() {
-  const { t, setLocale } = useI18n();
-  const [draft, setDraft] = useState<SystemConfig | null>(null);
-  const [saving, setSaving] = useState(false);
-  const [saveMessage, setSaveMessage] = useState<string | null>(null);
-  const [saveTone, setSaveTone] = useState<SaveTone>(null);
-
-  const load = useCallback(async (): Promise<ConfigData> => {
-    return loadSystemConfigData(apiMessageGet);
-  }, []);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy="正在加载系统配置。">
-      {data => {
-        const config = resolveSystemConfigDraft(draft, data.config || {});
-        const saveFeedback = resolveConfigSaveFeedback(saveMessage, saveTone);
-        const canSave = canSaveSystemConfig(config);
-
-        async function save() {
-          setSaving(true);
-          setSaveMessage(null);
-          setSaveTone(null);
-          try {
-            setSaveMessage(
-              await persistSystemConfig(apiMessagePost, config, t('common.save-success'), {
-                setLocale,
-                applyTheme: applyWorkbenchTheme,
-                reload: () => reloadWorkbenchWindow(window.location)
-              })
-            );
-            setSaveTone('success');
-          } catch (error) {
-            setSaveMessage(error instanceof Error ? error.message : t('common.save-failed'));
-            setSaveTone('error');
-          } finally {
-            setSaving(false);
-          }
-        }
-
-        return (
-          <div
-            data-setting-config-surface="otlp-cold-system-config"
-            data-setting-config-style-baseline={coldConfigVisual.canvasName}
-            data-setting-config-layout="full-width-settings-form"
-            className="space-y-4"
-          >
-            <SettingsConsoleTitle>{t('settings.system-config')}</SettingsConsoleTitle>
-            <SettingsForm
-              data-setting-config-form="cold-settings-form"
-              onSubmit={event => {
-                event.preventDefault();
-                if (!saving && canSave) {
-                  void save();
-                }
-              }}
-            >
-              <SettingsFormField label={t('settings.system-config.locale')}>
-                <SettingsFormSelect
-                  value={config.locale || ''}
-                  onChange={e => setDraft(prev => updateSystemConfigField(prev || data.config || {}, 'locale', e.target.value))}
-                >
-                  {SYSTEM_CONFIG_LOCALE_OPTIONS.map(option => (
-                    <option key={option.value} value={option.value}>
-                      {t(option.labelKey)}
-                    </option>
-                  ))}
-                </SettingsFormSelect>
-              </SettingsFormField>
-
-              <SettingsFormField label={t('settings.system-config.timezone')}>
-                <SettingsFormSelect
-                  value={config.timeZoneId || ''}
-                  onChange={e => setDraft(prev => updateSystemConfigTimezone(prev || data.config || {}, e.target.value))}
-                >
-                  {data.timezones.map(zone => (
-                    <option key={zone.zoneId} value={zone.zoneId}>
-                      {buildTimezoneOptionLabel(zone)}
-                    </option>
-                  ))}
-                </SettingsFormSelect>
-              </SettingsFormField>
-
-              <SettingsFormField label={t('settings.system-config.theme')}>
-                <SettingsFormSelect
-                  value={config.theme || ''}
-                  onChange={e => setDraft(prev => updateSystemConfigField(prev || data.config || {}, 'theme', e.target.value))}
-                >
-                  {SYSTEM_CONFIG_THEME_OPTIONS.map(option => (
-                    <option key={option.value} value={option.value}>
-                      {t(option.labelKey)}
-                    </option>
-                  ))}
-                </SettingsFormSelect>
-              </SettingsFormField>
-
-              {saveFeedback ? (
-                <SettingsFormFeedback className={saveFeedback.className}>{saveFeedback.message}</SettingsFormFeedback>
-              ) : null}
-
-              <SettingsFormActions data-setting-config-actions="standard-equal-buttons">
-                <Button type="submit" variant="default" className={coldPrimaryButtonClassName} disabled={saving || !canSave}>
-                  {saving ? t('common.saving') : t('settings.system-config.ok')}
-                </Button>
-              </SettingsFormActions>
-            </SettingsForm>
-          </div>
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function SettingConfigRoutePage() {
+  return <SettingConfigPage />;
 }
diff --git a/web-next/app/setting/settings/config/page.tsx b/web-next/app/setting/settings/config/setting-config-page.tsx
similarity index 74%
copy from web-next/app/setting/settings/config/page.tsx
copy to web-next/app/setting/settings/config/setting-config-page.tsx
index e252e8889f..67f3cb5a4b 100644
--- a/web-next/app/setting/settings/config/page.tsx
+++ b/web-next/app/setting/settings/config/setting-config-page.tsx
@@ -1,13 +1,13 @@
 'use client';
 
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
+import { HzInlineFeedback } from '@hertzbeat/ui';
 import { ClientWorkbench } from '../../../../components/workbench/client-workbench';
 import { useI18n } from '../../../../components/providers/i18n-provider';
 import { SettingsConsoleTitle } from '../../../../components/settings/settings-console-shell';
 import {
   SettingsForm,
   SettingsFormActions,
-  SettingsFormFeedback,
   SettingsFormField,
   SettingsFormSelect
 } from '../../../../components/settings/settings-form';
@@ -36,6 +36,8 @@ type ConfigData = {
 
 const coldConfigVisual = coldOpsCatalogVisual;
 
+const SETTING_CONFIG_SETTLED_CACHE_TTL_MS = 10_000;
+
 const coldPrimaryButtonClassName =
   'h-8 min-w-[104px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-[#f8fafc]';
 
@@ -45,13 +47,24 @@ export default function SettingConfigPage() {
   const [saving, setSaving] = useState(false);
   const [saveMessage, setSaveMessage] = useState<string | null>(null);
   const [saveTone, setSaveTone] = useState<SaveTone>(null);
+  const [reloadVersion, setReloadVersion] = useState(0);
+  const settingConfigCacheKey = useMemo(
+    () => ['setting-config', '/config/system', '/config/timezones', reloadVersion].join(':'),
+    [reloadVersion]
+  );
 
   const load = useCallback(async (): Promise<ConfigData> => {
+    void reloadVersion;
     return loadSystemConfigData(apiMessageGet);
-  }, []);
+  }, [reloadVersion]);
 
   return (
-    <ClientWorkbench load={load} loadingCopy="正在加载系统配置。">
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.settings.config.loading')}
+      cacheKey={settingConfigCacheKey}
+      cacheSettledTtlMs={SETTING_CONFIG_SETTLED_CACHE_TTL_MS}
+    >
       {data => {
         const config = resolveSystemConfigDraft(draft, data.config || {});
         const saveFeedback = resolveConfigSaveFeedback(saveMessage, saveTone);
@@ -60,18 +73,19 @@ export default function SettingConfigPage() {
         async function save() {
           setSaving(true);
           setSaveMessage(null);
-          setSaveTone(null);
+            setSaveTone(null);
           try {
             setSaveMessage(
-              await persistSystemConfig(apiMessagePost, config, t('common.save-success'), {
+              await persistSystemConfig(apiMessagePost, config, t('common.notify.apply-success'), {
                 setLocale,
                 applyTheme: applyWorkbenchTheme,
                 reload: () => reloadWorkbenchWindow(window.location)
               })
             );
             setSaveTone('success');
+            setReloadVersion(version => version + 1);
           } catch (error) {
-            setSaveMessage(error instanceof Error ? error.message : t('common.save-failed'));
+            setSaveMessage(error instanceof Error ? error.message : t('common.notify.apply-fail'));
             setSaveTone('error');
           } finally {
             setSaving(false);
@@ -88,6 +102,8 @@ export default function SettingConfigPage() {
             <SettingsConsoleTitle>{t('settings.system-config')}</SettingsConsoleTitle>
             <SettingsForm
               data-setting-config-form="cold-settings-form"
+              data-setting-config-apply-contract="angular-apply-notify-reload"
+              data-setting-config-runtime-locale="underscore-to-hyphen"
               onSubmit={event => {
                 event.preventDefault();
                 if (!saving && canSave) {
@@ -98,6 +114,8 @@ export default function SettingConfigPage() {
               <SettingsFormField label={t('settings.system-config.locale')}>
                 <SettingsFormSelect
                   value={config.locale || ''}
+                  data-setting-config-select-contract="angular-400px-centered-bold"
+                  data-setting-config-select-kind="locale"
                   onChange={e => setDraft(prev => updateSystemConfigField(prev || data.config || {}, 'locale', e.target.value))}
                 >
                   {SYSTEM_CONFIG_LOCALE_OPTIONS.map(option => (
@@ -111,6 +129,12 @@ export default function SettingConfigPage() {
               <SettingsFormField label={t('settings.system-config.timezone')}>
                 <SettingsFormSelect
                   value={config.timeZoneId || ''}
+                  searchable
+                  searchPlaceholder={t('settings.system-config.timezone')}
+                  data-setting-config-select-contract="angular-400px-centered-bold"
+                  data-setting-config-timezone-search-contract="angular-nz-show-search"
+                  data-setting-config-timezone-dropdown-width-contract="angular-dropdown-match-select-width-false"
+                  data-setting-config-select-kind="timezone"
                   onChange={e => setDraft(prev => updateSystemConfigTimezone(prev || data.config || {}, e.target.value))}
                 >
                   {data.timezones.map(zone => (
@@ -124,6 +148,8 @@ export default function SettingConfigPage() {
               <SettingsFormField label={t('settings.system-config.theme')}>
                 <SettingsFormSelect
                   value={config.theme || ''}
+                  data-setting-config-select-contract="angular-400px-centered-bold"
+                  data-setting-config-select-kind="theme"
                   onChange={e => setDraft(prev => updateSystemConfigField(prev || data.config || {}, 'theme', e.target.value))}
                 >
                   {SYSTEM_CONFIG_THEME_OPTIONS.map(option => (
@@ -135,7 +161,13 @@ export default function SettingConfigPage() {
               </SettingsFormField>
 
               {saveFeedback ? (
-                <SettingsFormFeedback className={saveFeedback.className}>{saveFeedback.message}</SettingsFormFeedback>
+                <HzInlineFeedback
+                  tone={saveTone === 'success' ? 'success' : 'critical'}
+                  title={saveFeedback.message}
+                  variant="embedded"
+                  data-setting-config-apply-feedback="angular-apply-notify"
+                  data-setting-config-apply-feedback-owner="hertzbeat-ui-inline-feedback"
+                />
               ) : null}
 
               <SettingsFormActions data-setting-config-actions="standard-equal-buttons">
diff --git a/web-next/app/setting/settings/layout.tsx b/web-next/app/setting/settings/layout.tsx
index 49b3bcaaf9..44f570efb1 100644
--- a/web-next/app/setting/settings/layout.tsx
+++ b/web-next/app/setting/settings/layout.tsx
@@ -1,28 +1,6 @@
-'use client';
+import type { ReactNode } from 'react';
+import SettingsSettingsLayoutClient from './settings-layout-client';
 
-import { usePathname } from 'next/navigation';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { SettingsConsoleShell } from '@/components/settings/settings-console-shell';
-import { buildSettingsMenuRows } from '@/lib/setting-settings-layout/view-model';
-
-export default function SettingSettingsLayout({ children }: { children: React.ReactNode }) {
-  const { t } = useI18n();
-  const pathname = usePathname();
-  const menus = buildSettingsMenuRows(t).map(row => ({
-    href: row.key,
-    label: row.title
-  }));
-
-  return (
-    <SettingsConsoleShell
-      items={menus}
-      activeHref={pathname}
-      kicker={t('settings.console.kicker')}
-      title={t('settings.console.title')}
-      subtitle={t('settings.console.copy')}
-      className=""
-    >
-      {children}
-    </SettingsConsoleShell>
-  );
+export default function SettingSettingsLayout({ children }: { children: ReactNode }) {
+  return <SettingsSettingsLayoutClient>{children}</SettingsSettingsLayoutClient>;
 }
diff --git a/web-next/app/setting/settings/mcp-server/page.test.ts b/web-next/app/setting/settings/mcp-server/page.test.ts
new file mode 100644
index 0000000000..24eb5eed50
--- /dev/null
+++ b/web-next/app/setting/settings/mcp-server/page.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it, vi } from 'vitest';
+
+const redirect = vi.fn();
+
+vi.mock('next/navigation', () => ({
+  redirect
+}));
+
+describe('setting MCP server alias route', () => {
+  it('redirects legacy MCP settings traffic to the config page', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: SettingMcpServerAliasPage } = await import('./page');
+
+    await expect(SettingMcpServerAliasPage({})).rejects.toThrow('redirect:/setting/settings/config');
+    expect(redirect).toHaveBeenCalledWith('/setting/settings/config');
+  });
+
+  it('preserves incoming query context when redirecting legacy MCP settings traffic', async () => {
+    redirect.mockImplementation((target: string) => {
+      throw new Error(`redirect:${target}`);
+    });
+
+    const { default: SettingMcpServerAliasPage } = await import('./page');
+
+    await expect(
+      SettingMcpServerAliasPage({
+        searchParams: Promise.resolve({
+          focus: 'mcp',
+          returnTo: '/setting/settings/token',
+          mode: 'audit'
+        })
+      })
+    ).rejects.toThrow('redirect:/setting/settings/config?focus=mcp&returnTo=%2Fsetting%2Fsettings%2Ftoken&mode=audit');
+    expect(redirect).toHaveBeenLastCalledWith(
+      '/setting/settings/config?focus=mcp&returnTo=%2Fsetting%2Fsettings%2Ftoken&mode=audit'
+    );
+  });
+});
diff --git a/web-next/app/setting/settings/mcp-server/page.tsx b/web-next/app/setting/settings/mcp-server/page.tsx
index 64793e81bc..e5ff3d707d 100644
--- a/web-next/app/setting/settings/mcp-server/page.tsx
+++ b/web-next/app/setting/settings/mcp-server/page.tsx
@@ -1,9 +1,9 @@
 import { redirect } from 'next/navigation';
-import { buildCompatRedirectTarget, type SearchParamsRecord } from '../../../../lib/compat/search-params';
+import { buildSettingsCompatRouteUrl, type SearchParamsRecord } from '../../../../lib/setting-settings-layout/navigation';
 
 export default async function SettingMcpServerAliasPage(props: {
   searchParams?: Promise<SearchParamsRecord>;
 }) {
   const resolvedSearchParams = await props?.searchParams;
-  redirect(buildCompatRedirectTarget('/setting/settings/config', resolvedSearchParams));
+  redirect(buildSettingsCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/setting/settings/object-store/page.test.tsx b/web-next/app/setting/settings/object-store/page.test.tsx
index 6171456ce5..793f18217c 100644
--- a/web-next/app/setting/settings/object-store/page.test.tsx
+++ b/web-next/app/setting/settings/object-store/page.test.tsx
@@ -27,25 +27,7 @@ const apiMessagePost = vi.hoisted(() => vi.fn());
 vi.mock('../../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'settings.object-store': '文件服务配置',
-        'settings.object-store.type': '文件服务提供商',
-        'settings.object-store.obs.accessKey': 'AccessKey',
-        'settings.object-store.obs.secretKey': 'SecretKey',
-        'settings.object-store.obs.bucketName': 'Bucket',
-        'settings.object-store.obs.endpoint': 'EndPoint',
-        'settings.object-store.obs.savePath': '保存路径',
-        'settings.object-store.obs.accessKey.placeholder': '华为云的AccessKeyId',
-        'settings.object-store.obs.secretKey.placeholder': '华为云的AccessKeySecret',
-        'settings.object-store.obs.bucketName.placeholder': '华为云OBS中您创建的Bucket名称',
-        'settings.object-store.obs.endpoint.placeholder': '华为云OBS地域域名,不包括Bucket名',
-        'settings.object-store.obs.savePath.placeholder': '备份文件保存路径, 默认是hertzbeat',
-        'settings.object-store.type.database': '本地数据库(默认)',
-        'settings.object-store.type.file': '本地文件',
-        'settings.object-store.type.obs': '华为云OBS',
-        'settings.system-config.ok': '确认更新'
-      }
+      locale: 'zh-CN'
     })
   })
 }));
@@ -79,8 +61,15 @@ vi.mock('../../../../components/settings/settings-form', () => ({
   ),
   SettingsFormInput: (props: any) => <input data-settings-form-control="cold-input-control" {...props} />,
   SettingsFormSelect: ({ children, ...props }: any) => <select data-settings-form-control="cold-select-control" {...props}>{children}</select>,
-  SettingsFormActions: ({ children, ...props }: any) => <div {...props}>{children}</div>,
-  SettingsFormFeedback: ({ children }: any) => <div data-settings-form-feedback="true">{children}</div>
+  SettingsFormActions: ({ children, ...props }: any) => <div {...props}>{children}</div>
+}));
+
+vi.mock('@hertzbeat/ui', () => ({
+  HzInlineFeedback: ({ title, ...props }: any) => (
+    <div data-hz-ui="inline-feedback" {...props}>
+      {title}
+    </div>
+  )
 }));
 
 vi.mock('../../../../components/ui/button', () => ({
@@ -108,8 +97,13 @@ describe('setting object store page', () => {
     expect(html).toContain('data-setting-object-store-page="otlp-cold-object-store"');
     expect(html).toContain('data-setting-object-store-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-setting-object-store-layout="full-width-settings-form"');
+    expect(html).toContain('data-setting-object-store-type-change-contract="angular-reset-config-on-type-change"');
+    expect(html).toContain('data-setting-object-store-provider-select-contract="angular-centered-bold-dropdown"');
     expect(html).toContain('data-setting-object-store-form="cold-settings-form"');
+    expect(html).toContain('data-setting-object-store-apply-contract="angular-apply-notify"');
     expect(html).toContain('data-setting-object-store-provider="cold-provider-select"');
+    expect(html).toContain('data-setting-object-store-provider-select="angular-centered-bold-dropdown"');
+    expect(html).toContain('data-setting-object-store-type-change="angular-reset-config-on-type-change"');
     expect(html).toContain('data-setting-object-store-obs-fields="cold-obs-fields"');
     expect(html).toContain('data-setting-object-store-actions="standard-equal-buttons"');
     expect(html).toContain('文件服务配置');
@@ -120,11 +114,16 @@ describe('setting object store page', () => {
     expect(html).toContain('AccessKey');
     expect(html).toContain('SecretKey');
     expect(html).toContain('Bucket');
-    expect(html).toContain('EndPoint');
+    expect(html).toContain('Endpoint');
     expect(html).toContain('保存路径');
+    expect(html).toContain('placeholder="华为云 AccessKeyId"');
+    expect(html).toContain('placeholder="华为云 AccessKeySecret"');
+    expect(html).toContain('placeholder="华为云 OBS 中创建的 Bucket 名称"');
+    expect(html).toContain('placeholder="华为云 OBS 地域域名,不包括 Bucket 名"');
+    expect(html).toContain('placeholder="备份文件保存路径,默认是 hertzbeat"');
     expect(html).toContain('本地数据库(默认)');
     expect(html).toContain('本地文件');
-    expect(html).toContain('华为云OBS');
+    expect(html).toContain('华为云 OBS');
     expect(html).toContain('确认更新');
     expect(html).not.toContain('data-setting-object-store-page="angular-object-store"');
     expect(html).not.toContain('data-setting-object-store-form="angular-vertical-form"');
@@ -143,17 +142,26 @@ describe('setting object store page', () => {
   });
 
   it('keeps the object-store route on the cold settings form owner', () => {
-    const source = readFileSync(resolve(__dirname, 'page.tsx'), 'utf8');
+    const source = readFileSync(resolve(__dirname, 'setting-object-store-page.tsx'), 'utf8');
 
     expect(source).toContain('coldOpsCatalogVisual');
     expect(source).toContain('data-setting-object-store-page="otlp-cold-object-store"');
     expect(source).toContain('data-setting-object-store-style-baseline={coldObjectStoreVisual.canvasName}');
     expect(source).toContain('data-setting-object-store-layout="full-width-settings-form"');
+    expect(source).toContain('data-setting-object-store-type-change-contract="angular-reset-config-on-type-change"');
+    expect(source).toContain('data-setting-object-store-provider-select-contract="angular-centered-bold-dropdown"');
     expect(source).toContain('data-setting-object-store-form="cold-settings-form"');
+    expect(source).toContain('data-setting-object-store-apply-contract="angular-apply-notify"');
     expect(source).toContain('data-setting-object-store-provider="cold-provider-select"');
+    expect(source).toContain('data-setting-object-store-provider-select="angular-centered-bold-dropdown"');
+    expect(source).toContain('data-setting-object-store-type-change="angular-reset-config-on-type-change"');
+    expect(source).toContain('updateObjectStoreType(prev || data.config || {}, e.target.value)');
     expect(source).toContain('data-setting-object-store-actions="standard-equal-buttons"');
     expect(source).toContain('SettingsConsoleTitle');
     expect(source).toContain('SettingsForm');
+    expect(source).toContain("t('common.notify.apply-success')");
+    expect(source).toContain("t('common.notify.apply-fail')");
+    expect(source).toContain('data-setting-object-store-apply-feedback-owner="hertzbeat-ui-inline-feedback"');
     expect(source).not.toContain('angular-object-store');
     expect(source).not.toContain('angular-vertical-form');
     expect(source).not.toContain('angular-provider-select');
@@ -162,4 +170,16 @@ describe('setting object store page', () => {
     expect(source).not.toContain('WorkbenchPage');
     expect(source).not.toContain('@/components/observability');
   });
+
+  it('keeps object-store remounts on a short settled cache window while saves invalidate it', () => {
+    const source = readFileSync(resolve(__dirname, 'setting-object-store-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_OBJECT_STORE_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['setting-object-store', '/config/oss', reloadVersion].join(':')");
+    expect(source).toContain('void reloadVersion');
+    expect(source).toContain('[reloadVersion]');
+    expect(source).toContain('setReloadVersion(version => version + 1)');
+    expect(source).toContain('cacheKey={settingObjectStoreCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_OBJECT_STORE_SETTLED_CACHE_TTL_MS}');
+  });
 });
diff --git a/web-next/app/setting/settings/object-store/page.tsx b/web-next/app/setting/settings/object-store/page.tsx
index 5a517b67eb..d42335efdb 100644
--- a/web-next/app/setting/settings/object-store/page.tsx
+++ b/web-next/app/setting/settings/object-store/page.tsx
@@ -1,172 +1,7 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback, useState } from 'react';
-import { ClientWorkbench } from '../../../../components/workbench/client-workbench';
-import { useI18n } from '../../../../components/providers/i18n-provider';
-import { SettingsConsoleTitle } from '../../../../components/settings/settings-console-shell';
-import {
-  SettingsForm,
-  SettingsFormActions,
-  SettingsFormFeedback,
-  SettingsFormField,
-  SettingsFormInput,
-  SettingsFormSelect
-} from '../../../../components/settings/settings-form';
-import { Button } from '../../../../components/ui/button';
-import { coldOpsCatalogVisual } from '../../../../lib/cold-ops-visual';
-import { apiMessageGet, apiMessagePost } from '../../../../lib/api-client';
-import { loadObjectStoreConfig, saveObjectStoreConfig } from '../../../../lib/object-store/controller';
-import {
-  OBJECT_STORE_TYPE_OPTIONS,
-  canSaveObjectStore,
-  isObsObjectStore,
-  resolveObjectStoreDraft,
-  updateObjectStoreField,
-  updateObjectStoreType
-} from '../../../../lib/object-store/view-model';
-import type { ObjectStoreConfig } from '../../../../lib/types';
+import SettingObjectStorePage from './setting-object-store-page';
 
-const coldObjectStoreVisual = coldOpsCatalogVisual;
-
-const coldPrimaryButtonClassName =
-  'h-8 min-w-[104px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-[#f8fafc]';
-
-export default function SettingObjectStorePage() {
-  const { t } = useI18n();
-  const [draft, setDraft] = useState<ObjectStoreConfig | null>(null);
-  const [message, setMessage] = useState<string | null>(null);
-  const [messageTone, setMessageTone] = useState<'success' | 'error' | null>(null);
-  const [saving, setSaving] = useState(false);
-
-  const load = useCallback(async () => {
-    return loadObjectStoreConfig(apiMessageGet);
-  }, []);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy="正在加载文件服务配置。">
-      {data => {
-        const config = resolveObjectStoreDraft(draft, data.config || {});
-        const obs = (config.config || {}) as Record<string, unknown>;
-        const showObsFields = isObsObjectStore(config);
-        const canSave = canSaveObjectStore(config);
-
-        async function save() {
-          setSaving(true);
-          setMessage(null);
-          setMessageTone(null);
-          try {
-            await saveObjectStoreConfig(apiMessagePost, config);
-            setMessage(t('common.save-success'));
-            setMessageTone('success');
-          } catch (error) {
-            setMessage(error instanceof Error ? error.message : t('common.save-failed'));
-            setMessageTone('error');
-          } finally {
-            setSaving(false);
-          }
-        }
-
-        return (
-          <div
-            data-setting-object-store-page="otlp-cold-object-store"
-            data-setting-object-store-style-baseline={coldObjectStoreVisual.canvasName}
-            data-setting-object-store-layout="full-width-settings-form"
-            className="space-y-4"
-          >
-            <SettingsConsoleTitle>{t('settings.object-store')}</SettingsConsoleTitle>
-            <SettingsForm
-              data-setting-object-store-form="cold-settings-form"
-              onSubmit={event => {
-                event.preventDefault();
-                if (!saving && canSave) {
-                  void save();
-                }
-              }}
-            >
-              <div data-setting-object-store-provider="cold-provider-select">
-                <SettingsFormField label={t('settings.object-store.type')}>
-                  <SettingsFormSelect
-                    value={config.type || 'DATABASE'}
-                    onChange={e => setDraft(prev => updateObjectStoreType(prev || data.config || {}, e.target.value))}
-                  >
-                    {OBJECT_STORE_TYPE_OPTIONS.map(option => (
-                      <option key={option.value} value={option.value}>
-                        {t(option.labelKey)}
-                      </option>
-                    ))}
-                  </SettingsFormSelect>
-                </SettingsFormField>
-              </div>
-
-              {showObsFields ? (
-                <div data-setting-object-store-obs-fields="cold-obs-fields" className="contents">
-                  <SettingsFormField label={t('settings.object-store.obs.accessKey')}>
-                    <SettingsFormInput
-                      placeholder={t('settings.object-store.obs.accessKey.placeholder')}
-                      value={String(obs.accessKey || '')}
-                      onChange={e =>
-                        setDraft(prev => updateObjectStoreField(prev || data.config || {}, 'accessKey', e.target.value))
-                      }
-                    />
-                  </SettingsFormField>
-
-                  <SettingsFormField label={t('settings.object-store.obs.secretKey')}>
-                    <SettingsFormInput
-                      placeholder={t('settings.object-store.obs.secretKey.placeholder')}
-                      value={String(obs.secretKey || '')}
-                      onChange={e =>
-                        setDraft(prev => updateObjectStoreField(prev || data.config || {}, 'secretKey', e.target.value))
-                      }
-                    />
-                  </SettingsFormField>
-
-                  <SettingsFormField label={t('settings.object-store.obs.bucketName')}>
-                    <SettingsFormInput
-                      placeholder={t('settings.object-store.obs.bucketName.placeholder')}
-                      value={String(obs.bucketName || '')}
-                      onChange={e =>
-                        setDraft(prev => updateObjectStoreField(prev || data.config || {}, 'bucketName', e.target.value))
-                      }
-                    />
-                  </SettingsFormField>
-
-                  <SettingsFormField label={t('settings.object-store.obs.endpoint')}>
-                    <SettingsFormInput
-                      placeholder={t('settings.object-store.obs.endpoint.placeholder')}
-                      value={String(obs.endpoint || '')}
-                      onChange={e =>
-                        setDraft(prev => updateObjectStoreField(prev || data.config || {}, 'endpoint', e.target.value))
-                      }
-                    />
-                  </SettingsFormField>
-
-                  <SettingsFormField label={t('settings.object-store.obs.savePath')}>
-                    <SettingsFormInput
-                      placeholder={t('settings.object-store.obs.savePath.placeholder')}
-                      value={String(obs.savePath || '')}
-                      onChange={e =>
-                        setDraft(prev => updateObjectStoreField(prev || data.config || {}, 'savePath', e.target.value))
-                      }
-                    />
-                  </SettingsFormField>
-                </div>
-              ) : null}
-
-              {message ? (
-                <SettingsFormFeedback className={messageTone === 'success' ? 'text-emerald-300' : 'text-rose-300'}>
-                  {message}
-                </SettingsFormFeedback>
-              ) : null}
-
-              <SettingsFormActions data-setting-object-store-actions="standard-equal-buttons">
-                <Button type="submit" variant="default" className={coldPrimaryButtonClassName} disabled={saving || !canSave}>
-                  {saving ? t('common.saving') : t('settings.system-config.ok')}
-                </Button>
-              </SettingsFormActions>
-            </SettingsForm>
-          </div>
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function SettingObjectStoreRoutePage() {
+  return <SettingObjectStorePage />;
 }
diff --git a/web-next/app/setting/settings/object-store/page.tsx b/web-next/app/setting/settings/object-store/setting-object-store-page.tsx
similarity index 81%
copy from web-next/app/setting/settings/object-store/page.tsx
copy to web-next/app/setting/settings/object-store/setting-object-store-page.tsx
index 5a517b67eb..989c2c61d6 100644
--- a/web-next/app/setting/settings/object-store/page.tsx
+++ b/web-next/app/setting/settings/object-store/setting-object-store-page.tsx
@@ -1,13 +1,13 @@
 'use client';
 
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
+import { HzInlineFeedback } from '@hertzbeat/ui';
 import { ClientWorkbench } from '../../../../components/workbench/client-workbench';
 import { useI18n } from '../../../../components/providers/i18n-provider';
 import { SettingsConsoleTitle } from '../../../../components/settings/settings-console-shell';
 import {
   SettingsForm,
   SettingsFormActions,
-  SettingsFormFeedback,
   SettingsFormField,
   SettingsFormInput,
   SettingsFormSelect
@@ -28,6 +28,8 @@ import type { ObjectStoreConfig } from '../../../../lib/types';
 
 const coldObjectStoreVisual = coldOpsCatalogVisual;
 
+const SETTING_OBJECT_STORE_SETTLED_CACHE_TTL_MS = 10_000;
+
 const coldPrimaryButtonClassName =
   'h-8 min-w-[104px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-[#f8fafc]';
 
@@ -37,13 +39,24 @@ export default function SettingObjectStorePage() {
   const [message, setMessage] = useState<string | null>(null);
   const [messageTone, setMessageTone] = useState<'success' | 'error' | null>(null);
   const [saving, setSaving] = useState(false);
+  const [reloadVersion, setReloadVersion] = useState(0);
+  const settingObjectStoreCacheKey = useMemo(
+    () => ['setting-object-store', '/config/oss', reloadVersion].join(':'),
+    [reloadVersion]
+  );
 
   const load = useCallback(async () => {
+    void reloadVersion;
     return loadObjectStoreConfig(apiMessageGet);
-  }, []);
+  }, [reloadVersion]);
 
   return (
-    <ClientWorkbench load={load} loadingCopy="正在加载文件服务配置。">
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.settings.object-store.loading')}
+      cacheKey={settingObjectStoreCacheKey}
+      cacheSettledTtlMs={SETTING_OBJECT_STORE_SETTLED_CACHE_TTL_MS}
+    >
       {data => {
         const config = resolveObjectStoreDraft(draft, data.config || {});
         const obs = (config.config || {}) as Record<string, unknown>;
@@ -56,10 +69,11 @@ export default function SettingObjectStorePage() {
           setMessageTone(null);
           try {
             await saveObjectStoreConfig(apiMessagePost, config);
-            setMessage(t('common.save-success'));
+            setMessage(t('common.notify.apply-success'));
             setMessageTone('success');
+            setReloadVersion(version => version + 1);
           } catch (error) {
-            setMessage(error instanceof Error ? error.message : t('common.save-failed'));
+            setMessage(error instanceof Error ? error.message : t('common.notify.apply-fail'));
             setMessageTone('error');
           } finally {
             setSaving(false);
@@ -71,11 +85,14 @@ export default function SettingObjectStorePage() {
             data-setting-object-store-page="otlp-cold-object-store"
             data-setting-object-store-style-baseline={coldObjectStoreVisual.canvasName}
             data-setting-object-store-layout="full-width-settings-form"
+            data-setting-object-store-type-change-contract="angular-reset-config-on-type-change"
+            data-setting-object-store-provider-select-contract="angular-centered-bold-dropdown"
             className="space-y-4"
           >
             <SettingsConsoleTitle>{t('settings.object-store')}</SettingsConsoleTitle>
             <SettingsForm
               data-setting-object-store-form="cold-settings-form"
+              data-setting-object-store-apply-contract="angular-apply-notify"
               onSubmit={event => {
                 event.preventDefault();
                 if (!saving && canSave) {
@@ -86,6 +103,8 @@ export default function SettingObjectStorePage() {
               <div data-setting-object-store-provider="cold-provider-select">
                 <SettingsFormField label={t('settings.object-store.type')}>
                   <SettingsFormSelect
+                    data-setting-object-store-provider-select="angular-centered-bold-dropdown"
+                    data-setting-object-store-type-change="angular-reset-config-on-type-change"
                     value={config.type || 'DATABASE'}
                     onChange={e => setDraft(prev => updateObjectStoreType(prev || data.config || {}, e.target.value))}
                   >
@@ -153,9 +172,13 @@ export default function SettingObjectStorePage() {
               ) : null}
 
               {message ? (
-                <SettingsFormFeedback className={messageTone === 'success' ? 'text-emerald-300' : 'text-rose-300'}>
-                  {message}
-                </SettingsFormFeedback>
+                <HzInlineFeedback
+                  tone={messageTone === 'success' ? 'success' : 'critical'}
+                  title={message}
+                  variant="embedded"
+                  data-setting-object-store-apply-feedback="angular-apply-notify"
+                  data-setting-object-store-apply-feedback-owner="hertzbeat-ui-inline-feedback"
+                />
               ) : null}
 
               <SettingsFormActions data-setting-object-store-actions="standard-equal-buttons">
diff --git a/web-next/app/setting/settings/page.tsx b/web-next/app/setting/settings/page.tsx
index b43f15dfaa..1c172ce5ce 100644
--- a/web-next/app/setting/settings/page.tsx
+++ b/web-next/app/setting/settings/page.tsx
@@ -1,9 +1,9 @@
 import { redirect } from 'next/navigation';
-import { buildCompatRedirectTarget, type SearchParamsRecord } from '../../../lib/compat/search-params';
+import { buildSettingsCompatRouteUrl, type SearchParamsRecord } from '../../../lib/setting-settings-layout/navigation';
 
 export default async function SettingSettingsIndexPage(props: {
   searchParams?: Promise<SearchParamsRecord>;
 }) {
   const resolvedSearchParams = await props?.searchParams;
-  redirect(buildCompatRedirectTarget('/setting/settings/config', resolvedSearchParams));
+  redirect(buildSettingsCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/setting/settings/server/page.test.tsx b/web-next/app/setting/settings/server/page.test.tsx
index d44fd6d35d..49c1e2a5fc 100644
--- a/web-next/app/setting/settings/server/page.test.tsx
+++ b/web-next/app/setting/settings/server/page.test.tsx
@@ -36,52 +36,22 @@ const apiMessagePost = vi.hoisted(() => vi.fn());
 
 vi.mock('../../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'settings.server': '消息服务配置',
-        'settings.server.email': '邮件服务器',
-        'settings.server.sms': '短信配置',
-        'settings.server.email.setting': '配置邮件服务器',
-        'settings.server.sms.setting': '配置短信参数',
-        'alert.notice.sender.mail.host': '邮箱服务器地址',
-        'alert.notice.sender.mail.port': '邮箱端口',
-        'alert.notice.sender.mail.username': '邮箱账号',
-        'alert.notice.sender.mail.password': '邮箱密码',
-        'alert.notice.sender.mail.ssl': '是否启用SSL',
-        'alert.notice.sender.mail.starttls': '是否启用STARTTLS',
-        'alert.notice.sender.mail.enable': '是否启用邮箱配置',
-        'alert.notice.sender.sms.type': '短信类型',
-        'alert.notice.sender.sms.type.tencent': '腾讯短信',
-        'alert.notice.sender.sms.type.alibaba': '阿里短信',
-        'alert.notice.sender.sms.type.unisms': '合一短信(UniSMS)',
-        'alert.notice.sender.sms.type.smslocal': '当地短信(Smslocal)',
-        'alert.notice.sender.sms.type.aws': 'Aws Sms',
-        'alert.notice.sender.sms.type.twilio': 'Twilio Sms',
-        'alert.notice.sender.sms.tencent.appId': '腾讯短信AppId',
-        'alert.notice.sender.sms.tencent.signName': '腾讯短信SignName',
-        'alert.notice.sender.sms.tencent.templateId': '腾讯短信TemplateId',
-        'common.button.setting': '配置',
-        'common.button.cancel': '取消',
-        'common.button.save': '保存',
-        'common.enable': '启用状态',
-        'common.yes': '是',
-        'common.no': '否'
-      }
-    })
+    t: createTranslatorMock({ locale: 'zh-CN' })
   })
 }));
 
 vi.mock('../../../../components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    loadingCopy
   }: {
     children: (data: unknown) => React.ReactNode;
     load: () => Promise<unknown>;
+    loadingCopy?: string;
   }) => {
     mockState.lastLoad = load;
-    return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
+    return <div data-client-workbench="true" data-loading-copy={loadingCopy}>{children(mockState.renderData)}</div>;
   }
 }));
 
@@ -100,6 +70,7 @@ vi.mock('../../../../components/settings/settings-summary-list', () => ({
       title: React.ReactNode;
       lines: React.ReactNode[];
       actionLabel: string;
+      actionAriaLabel?: string;
     }>;
   }) => (
     <div
@@ -111,7 +82,12 @@ vi.mock('../../../../components/settings/settings-summary-list', () => ({
         <section key={item.key} data-settings-summary-item={item.key} data-settings-summary-row-style="cold-summary-row">
           <h2>{item.title}</h2>
           <div>{item.lines.map((line, index) => <div key={`${item.key}-${index}`}>{line}</div>)}</div>
-          <button type="button" data-settings-summary-action={item.key} data-settings-summary-action-style="cold-compact-action">
+          <button
+            type="button"
+            data-settings-summary-action={item.key}
+            data-settings-summary-action-style="cold-compact-action"
+            aria-label={item.actionAriaLabel}
+          >
             {item.actionLabel}
           </button>
         </section>
@@ -125,14 +101,24 @@ vi.mock('../../../../components/workbench/overlay-dialog', () => ({
     open,
     title,
     footer,
-    children
+    children,
+    maxWidthClassName,
+    overlayProps
   }: {
     open: boolean;
     title: React.ReactNode;
     footer?: React.ReactNode;
     children: React.ReactNode;
+    maxWidthClassName?: string;
+    overlayProps?: React.HTMLAttributes<HTMLDivElement>;
   }) => (
-    <section data-overlay-dialog="true" data-open={open ? 'true' : 'false'} data-overlay-dialog-title={title}>
+    <section
+      data-overlay-dialog="true"
+      data-open={open ? 'true' : 'false'}
+      data-overlay-dialog-title={title}
+      data-overlay-dialog-max-width={maxWidthClassName}
+      {...overlayProps}
+    >
       <header>{title}</header>
       <div>{children}</div>
       <footer>{footer}</footer>
@@ -141,11 +127,13 @@ vi.mock('../../../../components/workbench/overlay-dialog', () => ({
 }));
 
 vi.mock('../../../../components/settings/settings-dialog-form', () => ({
-  SettingsDialogForm: ({ children }: { children: React.ReactNode }) => <form data-settings-dialog-form="true">{children}</form>,
+  SettingsDialogForm: ({ children, ...props }: React.FormHTMLAttributes<HTMLFormElement>) => (
+    <form data-settings-dialog-form="true" {...props}>{children}</form>
+  ),
   SettingsDialogField: ({ label, children }: { label: string; children: React.ReactNode }) => (
-    <label data-settings-dialog-field={label}>
-      <span>{label}</span>
-      {children}
+    <label data-settings-dialog-field={label} data-settings-dialog-field-layout="angular-label-7-control-12">
+      <span data-settings-dialog-label-span="7">{label}</span>
+      <div data-settings-dialog-control-span="12">{children}</div>
     </label>
   ),
   SettingsDialogInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
@@ -160,6 +148,14 @@ vi.mock('../../../../components/ui/button', () => ({
   Button: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => <button {...props}>{children}</button>
 }));
 
+vi.mock('@hertzbeat/ui', () => ({
+  HzInlineFeedback: ({ title, ...props }: any) => (
+    <div data-hz-ui="inline-feedback" {...props}>
+      {title}
+    </div>
+  )
+}));
+
 vi.mock('../../../../lib/api-client', () => ({
   apiMessageGet,
   apiMessagePost
@@ -179,10 +175,13 @@ describe('setting server page', () => {
     const html = renderToStaticMarkup(<SettingServerPage />);
 
     expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-loading-copy="正在加载消息服务配置。"');
     expect(html).toContain('data-settings-console-title="true"');
     expect(html).toContain('data-settings-server-page="otlp-cold-message-server"');
     expect(html).toContain('data-settings-server-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-settings-server-layout="full-width-settings-summary"');
+    expect(html).toContain('data-settings-server-dialog-width-contract="angular-width-40-percent"');
+    expect(html).toContain('data-settings-server-dialog-field-layout-contract="angular-label-7-control-12"');
     expect(html).toContain('data-settings-server-summary="cold-summary-list"');
     expect(html).toContain('data-settings-summary-list="true"');
     expect(html).toContain('data-settings-summary-list-owner="cold-settings-summary-owner"');
@@ -193,16 +192,29 @@ describe('setting server page', () => {
     expect(html).toContain('data-settings-summary-action="email"');
     expect(html).toContain('data-settings-summary-action="sms"');
     expect(html).toContain('data-settings-summary-action-style="cold-compact-action"');
+    expect(html).toContain('aria-label="配置邮件服务器"');
+    expect(html).toContain('aria-label="配置短信配置"');
     expect(html).toContain('消息服务配置');
     expect(html).toContain('邮件服务器');
     expect(html).toContain('短信配置');
     expect(html).toContain('邮箱服务器地址: smtp.example.com');
     expect(html).toContain('邮箱账号: ops');
+    expect(html).toContain('是否启用邮箱配置: 是');
     expect(html).toContain('腾讯短信AppId: 10001');
     expect(html).toContain('腾讯短信SignName: old-sign');
     expect(html).toContain('启用状态: 否');
     expect(html).toContain('data-overlay-dialog-title="配置邮件服务器"');
     expect(html).toContain('data-overlay-dialog-title="配置短信参数"');
+    expect(html).toContain('data-overlay-dialog-max-width="w-[min(92vw,520px)] md:w-[40vw] md:max-w-[40vw]"');
+    expect(html).toContain('data-settings-server-email-dialog-width="angular-width-40-percent"');
+    expect(html).toContain('data-settings-server-email-dialog-mask="angular-mask-closable-false"');
+    expect(html).toContain('data-settings-server-sms-dialog-width="angular-width-40-percent"');
+    expect(html).toContain('data-settings-server-sms-dialog-mask="angular-mask-closable-false"');
+    expect(html).toContain('data-settings-dialog-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-settings-dialog-label-span="7"');
+    expect(html).toContain('data-settings-dialog-control-span="12"');
+    expect(html).toContain('data-settings-server-email-apply-contract="angular-apply-notify"');
+    expect(html).toContain('data-settings-server-sms-apply-contract="angular-apply-notify"');
     expect(html).toContain('邮箱密码');
     expect(html).toContain('短信类型');
     expect(html).toContain('腾讯短信');
@@ -223,16 +235,28 @@ describe('setting server page', () => {
 
     expect(apiMessageGet).toHaveBeenNthCalledWith(1, '/config/email');
     expect(apiMessageGet).toHaveBeenNthCalledWith(2, '/config/sms');
-  });
+  }, 15000);
 
   it('keeps the server route on the cold settings-console summary owner', () => {
-    const source = readFileSync(resolve(__dirname, 'page.tsx'), 'utf8');
+    const source = readFileSync(resolve(__dirname, 'setting-server-page.tsx'), 'utf8');
 
     expect(source).toContain('coldOpsCatalogVisual');
     expect(source).toContain('data-settings-server-page="otlp-cold-message-server"');
     expect(source).toContain('data-settings-server-style-baseline={coldServerVisual.canvasName}');
     expect(source).toContain('data-settings-server-layout="full-width-settings-summary"');
+    expect(source).toContain('data-settings-server-dialog-width-contract="angular-width-40-percent"');
+    expect(source).toContain('data-settings-server-dialog-field-layout-contract="angular-label-7-control-12"');
     expect(source).toContain('data-settings-server-summary="cold-summary-list"');
+    expect(source).toContain('maxWidthClassName="w-[min(92vw,520px)] md:w-[40vw] md:max-w-[40vw]"');
+    expect(source).toContain("'data-settings-server-email-dialog-width': 'angular-width-40-percent'");
+    expect(source).toContain("'data-settings-server-sms-dialog-width': 'angular-width-40-percent'");
+    expect(source).toContain("'data-settings-server-email-dialog-mask': 'angular-mask-closable-false'");
+    expect(source).toContain("'data-settings-server-sms-dialog-mask': 'angular-mask-closable-false'");
+    expect(source).toContain('data-settings-server-email-apply-contract="angular-apply-notify"');
+    expect(source).toContain('data-settings-server-sms-apply-contract="angular-apply-notify"');
+    expect(source).toContain("t('common.notify.apply-success')");
+    expect(source).toContain("t('common.notify.apply-fail')");
+    expect(source).toContain('data-settings-server-apply-feedback-owner="hertzbeat-ui-inline-feedback"');
     expect(source).toContain('SettingsSummaryList');
     expect(source).not.toContain('data-settings-server-summary-rail');
     expect(source).not.toContain('angular-message-server');
@@ -244,7 +268,7 @@ describe('setting server page', () => {
   it('uses the shared cold number stepper for the email port field', async () => {
     const { default: SettingServerPage } = await import('./page');
     const html = renderToStaticMarkup(<SettingServerPage />);
-    const source = readFileSync(resolve(__dirname, 'page.tsx'), 'utf8');
+    const source = readFileSync(resolve(__dirname, 'setting-server-page.tsx'), 'utf8');
 
     expect(source).toContain("from '../../../../components/ui/number-stepper'");
     expect(source).toContain('data-settings-server-email-port-stepper="cold-number-stepper"');
@@ -256,4 +280,18 @@ describe('setting server page', () => {
     expect(html).toContain('data-cold-number-stepper-action="increment"');
     expect(html).toContain('value="587"');
   });
+
+  it('keeps message-server remounts on a short settled cache window while email and SMS saves invalidate it', () => {
+    const source = readFileSync(resolve(__dirname, 'setting-server-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_SERVER_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['setting-server', '/config/email', '/config/sms', reloadVersion].join(':')");
+    expect(source).toContain('void reloadVersion');
+    expect(source).toContain('[reloadVersion]');
+    expect(source).toContain('await saveEmailConfig(apiMessagePost, email);');
+    expect(source).toContain('await saveSmsConfig(apiMessagePost, sms);');
+    expect(source.match(/setReloadVersion\(version => version \+ 1\)/g)?.length).toBeGreaterThanOrEqual(2);
+    expect(source).toContain('cacheKey={settingServerCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_SERVER_SETTLED_CACHE_TTL_MS}');
+  });
 });
diff --git a/web-next/app/setting/settings/server/page.tsx b/web-next/app/setting/settings/server/page.tsx
index 3083702fda..b7065cdb17 100644
--- a/web-next/app/setting/settings/server/page.tsx
+++ b/web-next/app/setting/settings/server/page.tsx
@@ -1,460 +1,7 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback, useState } from 'react';
-import { ClientWorkbench } from '../../../../components/workbench/client-workbench';
-import { useI18n } from '../../../../components/providers/i18n-provider';
-import { OverlayDialog } from '../../../../components/workbench/overlay-dialog';
-import { SettingsConsoleTitle } from '../../../../components/settings/settings-console-shell';
-import {
-  SettingsDialogField,
-  SettingsDialogFooter,
-  SettingsDialogForm,
-  SettingsDialogInput,
-  SettingsDialogSelect,
-  SettingsDialogToggle
-} from '../../../../components/settings/settings-dialog-form';
-import { SettingsSummaryList } from '../../../../components/settings/settings-summary-list';
-import { Button } from '../../../../components/ui/button';
-import { NumberStepper } from '../../../../components/ui/number-stepper';
-import { coldOpsCatalogVisual } from '../../../../lib/cold-ops-visual';
-import { apiMessageGet, apiMessagePost } from '../../../../lib/api-client';
-import { loadMessageServerData, saveEmailConfig, saveSmsConfig } from '../../../../lib/setting-server/controller';
-import {
-  SMS_PROVIDER_OPTIONS,
-  UNISMS_AUTH_MODE_OPTIONS,
-  buildMessageServerSummaryItems,
-  canSaveEmailSender,
-  canSaveSmsSender,
-  cloneSmsSender,
-  isUniSmsAccessKeySecretRequired,
-  normalizeEmailSender,
-  normalizeSmsProviderType,
-  normalizeSmsSender,
-  updateSmsProviderField,
-  updateSmsType
-} from '../../../../lib/setting-server/view-model';
-import type { EmailNoticeSender, SmsNoticeSender } from '../../../../lib/types';
+import SettingServerPage from './setting-server-page';
 
-type MessageServerData = {
-  email: EmailNoticeSender;
-  sms: SmsNoticeSender;
-};
-
-const coldServerVisual = coldOpsCatalogVisual;
-
-const coldPrimaryButtonClassName =
-  'h-8 min-w-[96px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-[#f8fafc]';
-
-const coldSecondaryButtonClassName =
-  'h-8 min-w-[96px] rounded-[3px] border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151923] hover:text-[#f8fafc]';
-
-export default function SettingServerPage() {
-  const { t } = useI18n();
-  const [emailDraft, setEmailDraft] = useState<EmailNoticeSender | null>(null);
-  const [smsDraft, setSmsDraft] = useState<SmsNoticeSender | null>(null);
-  const [smsSnapshot, setSmsSnapshot] = useState<SmsNoticeSender | null>(null);
-  const [openDialog, setOpenDialog] = useState<'email' | 'sms' | null>(null);
-  const [savingDialog, setSavingDialog] = useState<'email' | 'sms' | null>(null);
-  const [message, setMessage] = useState<string | null>(null);
-  const [messageTone, setMessageTone] = useState<'success' | 'error' | null>(null);
-
-  const load = useCallback(async (): Promise<MessageServerData> => {
-    return loadMessageServerData(apiMessageGet);
-  }, []);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy="正在加载消息服务配置。">
-      {data => {
-        const email = normalizeEmailSender(emailDraft || data.email || {});
-        const sms = normalizeSmsSender(smsDraft || data.sms || {});
-        const canSaveEmail = canSaveEmailSender(email);
-        const canSaveSms = canSaveSmsSender(sms);
-        const smsType = normalizeSmsProviderType(sms.type);
-        const unismsNeedsSecret = isUniSmsAccessKeySecretRequired(sms);
-
-        async function saveEmail() {
-          setSavingDialog('email');
-          setMessage(null);
-          setMessageTone(null);
-          try {
-            await saveEmailConfig(apiMessagePost, email);
-            setOpenDialog(null);
-            setMessage(t('common.notify.apply-success'));
-            setMessageTone('success');
-          } catch (error) {
-            setMessage(error instanceof Error ? error.message : t('common.notify.apply-fail'));
-            setMessageTone('error');
-          } finally {
-            setSavingDialog(null);
-          }
-        }
-
-        async function saveSms() {
-          setSavingDialog('sms');
-          setMessage(null);
-          setMessageTone(null);
-          try {
-            await saveSmsConfig(apiMessagePost, sms);
-            setOpenDialog(null);
-            setSmsSnapshot(cloneSmsSender(sms));
-            setMessage(t('common.notify.apply-success'));
-            setMessageTone('success');
-          } catch (error) {
-            setMessage(error instanceof Error ? error.message : t('common.notify.apply-fail'));
-            setMessageTone('error');
-          } finally {
-            setSavingDialog(null);
-          }
-        }
-
-        return (
-          <div
-            className="space-y-4"
-            data-settings-server-page="otlp-cold-message-server"
-            data-settings-server-style-baseline={coldServerVisual.canvasName}
-            data-settings-server-layout="full-width-settings-summary"
-          >
-            <SettingsConsoleTitle>{t('settings.server')}</SettingsConsoleTitle>
-
-            <div data-settings-server-summary="cold-summary-list">
-              <SettingsSummaryList
-                items={buildMessageServerSummaryItems(email, sms, t).map(item => ({
-                  ...item,
-                  actionLabel: t('common.button.setting'),
-                  onAction: () => {
-                    setMessage(null);
-                    setMessageTone(null);
-                    if (item.key === 'email') {
-                      setOpenDialog('email');
-                      return;
-                    }
-
-                    setSmsSnapshot(cloneSmsSender(sms));
-                    setOpenDialog('sms');
-                  }
-                }))}
-              />
-            </div>
-
-            {message ? (
-              <div className={`text-[12px] leading-5 ${messageTone === 'success' ? 'text-emerald-300' : 'text-rose-300'}`}>
-                {message}
-              </div>
-            ) : null}
-
-            <OverlayDialog
-              open={openDialog === 'email'}
-              title={t('settings.server.email.setting')}
-              onClose={() => setOpenDialog(null)}
-              maxWidthClassName="max-w-3xl"
-              footer={
-                <SettingsDialogFooter>
-                  <Button type="button" variant="default" className={coldSecondaryButtonClassName} onClick={() => setOpenDialog(null)}>
-                    {t('common.button.cancel')}
-                  </Button>
-                  <Button
-                    type="button"
-                    variant="default"
-                    className={coldPrimaryButtonClassName}
-                    disabled={!canSaveEmail || savingDialog === 'email'}
-                    onClick={() => void saveEmail()}
-                  >
-                    {savingDialog === 'email' ? t('common.saving') : t('common.button.save')}
-                  </Button>
-                </SettingsDialogFooter>
-              }
-            >
-              <SettingsDialogForm>
-                <SettingsDialogField label={t('alert.notice.sender.mail.host')} required>
-                  <SettingsDialogInput
-                    value={email.emailHost || ''}
-                    onChange={event => setEmailDraft(prev => ({ ...(prev || email), emailHost: event.target.value }))}
-                  />
-                </SettingsDialogField>
-                <SettingsDialogField label={t('alert.notice.sender.mail.port')} required>
-                  <NumberStepper
-                    data-settings-server-email-port-stepper="cold-number-stepper"
-                    min="1"
-                    max="65535"
-                    value={email.emailPort?.toString() || ''}
-                    onValueChange={value =>
-                      setEmailDraft(prev => ({ ...(prev || email), emailPort: Number(value) || undefined }))
-                    }
-                  />
-                </SettingsDialogField>
-                <SettingsDialogField label={t('alert.notice.sender.mail.username')} required>
-                  <SettingsDialogInput
-                    value={email.emailUsername || ''}
-                    onChange={event => setEmailDraft(prev => ({ ...(prev || email), emailUsername: event.target.value }))}
-                  />
-                </SettingsDialogField>
-                <SettingsDialogField label={t('alert.notice.sender.mail.password')} required>
-                  <SettingsDialogInput
-                    type="password"
-                    value={email.emailPassword || ''}
-                    onChange={event => setEmailDraft(prev => ({ ...(prev || email), emailPassword: event.target.value }))}
-                  />
-                </SettingsDialogField>
-                <SettingsDialogField label={t('alert.notice.sender.mail.ssl')} required>
-                  <SettingsDialogToggle
-                    checked={Boolean(email.emailSsl)}
-                    onCheckedChange={checked => setEmailDraft(prev => ({ ...(prev || email), emailSsl: checked }))}
-                  />
-                </SettingsDialogField>
-                <SettingsDialogField label={t('alert.notice.sender.mail.starttls')} required>
-                  <SettingsDialogToggle
-                    checked={Boolean(email.emailStarttls)}
-                    onCheckedChange={checked => setEmailDraft(prev => ({ ...(prev || email), emailStarttls: checked }))}
-                  />
-                </SettingsDialogField>
-                <SettingsDialogField label={t('common.enable')} required>
-                  <SettingsDialogToggle
-                    checked={Boolean(email.enable)}
-                    onCheckedChange={checked => setEmailDraft(prev => ({ ...(prev || email), enable: checked }))}
-                  />
-                </SettingsDialogField>
-              </SettingsDialogForm>
-            </OverlayDialog>
-
-            <OverlayDialog
-              open={openDialog === 'sms'}
-              title={t('settings.server.sms.setting')}
-              onClose={() => {
-                setSmsDraft(cloneSmsSender(smsSnapshot || data.sms || {}));
-                setOpenDialog(null);
-              }}
-              maxWidthClassName="max-w-3xl"
-              footer={
-                <SettingsDialogFooter>
-                  <Button
-                    type="button"
-                    variant="default"
-                    className={coldSecondaryButtonClassName}
-                    onClick={() => {
-                      setSmsDraft(cloneSmsSender(smsSnapshot || data.sms || {}));
-                      setOpenDialog(null);
-                    }}
-                  >
-                    {t('common.button.cancel')}
-                  </Button>
-                  <Button
-                    type="button"
-                    variant="default"
-                    className={coldPrimaryButtonClassName}
-                    disabled={!canSaveSms || savingDialog === 'sms'}
-                    onClick={() => void saveSms()}
-                  >
-                    {savingDialog === 'sms' ? t('common.saving') : t('common.button.save')}
-                  </Button>
-                </SettingsDialogFooter>
-              }
-            >
-              <SettingsDialogForm>
-                <SettingsDialogField label={t('alert.notice.sender.sms.type')} required>
-                  <SettingsDialogSelect
-                    value={smsType}
-                    onChange={event => setSmsDraft(prev => updateSmsType(prev || sms, normalizeSmsProviderType(event.target.value)))}
-                  >
-                    {SMS_PROVIDER_OPTIONS.map(option => (
-                      <option key={option.value} value={option.value}>
-                        {t(option.labelKey)}
-                      </option>
-                    ))}
-                  </SettingsDialogSelect>
-                </SettingsDialogField>
-
-                {smsType === 'tencent' ? (
-                  <>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.tencent.secretId')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.tencent as Record<string, unknown>)?.secretId || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'tencent', 'secretId', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.tencent.secretKey')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.tencent as Record<string, unknown>)?.secretKey || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'tencent', 'secretKey', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.tencent.signName')} required>
-                      <SettingsDialogInput
-                        value={String((sms.tencent as Record<string, unknown>)?.signName || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'tencent', 'signName', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.tencent.appId')} required>
-                      <SettingsDialogInput
-                        value={String((sms.tencent as Record<string, unknown>)?.appId || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'tencent', 'appId', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.tencent.templateId')} required>
-                      <SettingsDialogInput
-                        value={String((sms.tencent as Record<string, unknown>)?.templateId || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'tencent', 'templateId', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                  </>
-                ) : null}
-
-                {smsType === 'alibaba' ? (
-                  <>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.alibaba.accessKeyId')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.alibaba as Record<string, unknown>)?.accessKeyId || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'alibaba', 'accessKeyId', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.alibaba.accessKeySecret')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.alibaba as Record<string, unknown>)?.accessKeySecret || '')}
-                        onChange={event =>
-                          setSmsDraft(prev => updateSmsProviderField(prev || sms, 'alibaba', 'accessKeySecret', event.target.value))
-                        }
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.alibaba.signName')} required>
-                      <SettingsDialogInput
-                        value={String((sms.alibaba as Record<string, unknown>)?.signName || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'alibaba', 'signName', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.alibaba.templateCode')} required>
-                      <SettingsDialogInput
-                        value={String((sms.alibaba as Record<string, unknown>)?.templateCode || '')}
-                        onChange={event =>
-                          setSmsDraft(prev => updateSmsProviderField(prev || sms, 'alibaba', 'templateCode', event.target.value))
-                        }
-                      />
-                    </SettingsDialogField>
-                  </>
-                ) : null}
-
-                {smsType === 'unisms' ? (
-                  <>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.unisms.accessKeyId')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.unisms as Record<string, unknown>)?.accessKeyId || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'unisms', 'accessKeyId', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.unisms.authMode')} required>
-                      <SettingsDialogSelect
-                        value={String((sms.unisms as Record<string, unknown>)?.authMode || 'hmac')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'unisms', 'authMode', event.target.value))}
-                      >
-                        {UNISMS_AUTH_MODE_OPTIONS.map(option => (
-                          <option key={option.value} value={option.value}>
-                            {option.label}
-                          </option>
-                        ))}
-                      </SettingsDialogSelect>
-                    </SettingsDialogField>
-                    {unismsNeedsSecret ? (
-                      <SettingsDialogField label={t('alert.notice.sender.sms.unisms.accessKeySecret')} required>
-                        <SettingsDialogInput
-                          type="password"
-                          value={String((sms.unisms as Record<string, unknown>)?.accessKeySecret || '')}
-                          onChange={event =>
-                            setSmsDraft(prev => updateSmsProviderField(prev || sms, 'unisms', 'accessKeySecret', event.target.value))
-                          }
-                        />
-                      </SettingsDialogField>
-                    ) : null}
-                    <SettingsDialogField label={t('alert.notice.sender.sms.unisms.signature')} required>
-                      <SettingsDialogInput
-                        value={String((sms.unisms as Record<string, unknown>)?.signature || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'unisms', 'signature', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.unisms.templateId')} required>
-                      <SettingsDialogInput
-                        value={String((sms.unisms as Record<string, unknown>)?.templateId || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'unisms', 'templateId', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                  </>
-                ) : null}
-
-                {smsType === 'smslocal' ? (
-                  <SettingsDialogField label={t('alert.notice.sender.sms.smslocal.apiKey')} required>
-                    <SettingsDialogInput
-                      type="password"
-                      value={String((sms.smslocal as Record<string, unknown>)?.apiKey || '')}
-                      onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'smslocal', 'apiKey', event.target.value))}
-                    />
-                  </SettingsDialogField>
-                ) : null}
-
-                {smsType === 'aws' ? (
-                  <>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.aws.accessKeyId')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.aws as Record<string, unknown>)?.accessKeyId || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'aws', 'accessKeyId', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.aws.accessKeySecret')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.aws as Record<string, unknown>)?.accessKeySecret || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'aws', 'accessKeySecret', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.aws.region')} required>
-                      <SettingsDialogInput
-                        value={String((sms.aws as Record<string, unknown>)?.region || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'aws', 'region', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                  </>
-                ) : null}
-
-                {smsType === 'twilio' ? (
-                  <>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.twilio.accountSid')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.twilio as Record<string, unknown>)?.accountSid || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'twilio', 'accountSid', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.twilio.authToken')} required>
-                      <SettingsDialogInput
-                        type="password"
-                        value={String((sms.twilio as Record<string, unknown>)?.authToken || '')}
-                        onChange={event => setSmsDraft(prev => updateSmsProviderField(prev || sms, 'twilio', 'authToken', event.target.value))}
-                      />
-                    </SettingsDialogField>
-                    <SettingsDialogField label={t('alert.notice.sender.sms.twilio.twilioPhoneNumber')} required>
-                      <SettingsDialogInput
-                        value={String((sms.twilio as Record<string, unknown>)?.twilioPhoneNumber || '')}
-                        onChange={event =>
-                          setSmsDraft(prev => updateSmsProviderField(prev || sms, 'twilio', 'twilioPhoneNumber', event.target.value))
-                        }
-                      />
-                    </SettingsDialogField>
-                  </>
-                ) : null}
-
-                <SettingsDialogField label={t('common.enable')} required>
-                  <SettingsDialogToggle
-                    checked={Boolean(sms.enable)}
-                    onCheckedChange={checked => setSmsDraft(prev => ({ ...(prev || sms), enable: checked }))}
-                  />
-                </SettingsDialogField>
-              </SettingsDialogForm>
-            </OverlayDialog>
-          </div>
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function SettingServerRoutePage() {
+  return <SettingServerPage />;
 }
diff --git a/web-next/app/setting/settings/server/page.tsx b/web-next/app/setting/settings/server/setting-server-page.tsx
similarity index 88%
copy from web-next/app/setting/settings/server/page.tsx
copy to web-next/app/setting/settings/server/setting-server-page.tsx
index 3083702fda..1391019b6c 100644
--- a/web-next/app/setting/settings/server/page.tsx
+++ b/web-next/app/setting/settings/server/setting-server-page.tsx
@@ -1,6 +1,7 @@
 'use client';
 
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
+import { HzInlineFeedback } from '@hertzbeat/ui';
 import { ClientWorkbench } from '../../../../components/workbench/client-workbench';
 import { useI18n } from '../../../../components/providers/i18n-provider';
 import { OverlayDialog } from '../../../../components/workbench/overlay-dialog';
@@ -42,6 +43,8 @@ type MessageServerData = {
 
 const coldServerVisual = coldOpsCatalogVisual;
 
+const SETTING_SERVER_SETTLED_CACHE_TTL_MS = 10_000;
+
 const coldPrimaryButtonClassName =
   'h-8 min-w-[96px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-[#f8fafc]';
 
@@ -57,13 +60,24 @@ export default function SettingServerPage() {
   const [savingDialog, setSavingDialog] = useState<'email' | 'sms' | null>(null);
   const [message, setMessage] = useState<string | null>(null);
   const [messageTone, setMessageTone] = useState<'success' | 'error' | null>(null);
+  const [reloadVersion, setReloadVersion] = useState(0);
+  const settingServerCacheKey = useMemo(
+    () => ['setting-server', '/config/email', '/config/sms', reloadVersion].join(':'),
+    [reloadVersion]
+  );
 
   const load = useCallback(async (): Promise<MessageServerData> => {
+    void reloadVersion;
     return loadMessageServerData(apiMessageGet);
-  }, []);
+  }, [reloadVersion]);
 
   return (
-    <ClientWorkbench load={load} loadingCopy="正在加载消息服务配置。">
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.settings.server.loading')}
+      cacheKey={settingServerCacheKey}
+      cacheSettledTtlMs={SETTING_SERVER_SETTLED_CACHE_TTL_MS}
+    >
       {data => {
         const email = normalizeEmailSender(emailDraft || data.email || {});
         const sms = normalizeSmsSender(smsDraft || data.sms || {});
@@ -81,6 +95,7 @@ export default function SettingServerPage() {
             setOpenDialog(null);
             setMessage(t('common.notify.apply-success'));
             setMessageTone('success');
+            setReloadVersion(version => version + 1);
           } catch (error) {
             setMessage(error instanceof Error ? error.message : t('common.notify.apply-fail'));
             setMessageTone('error');
@@ -99,6 +114,7 @@ export default function SettingServerPage() {
             setSmsSnapshot(cloneSmsSender(sms));
             setMessage(t('common.notify.apply-success'));
             setMessageTone('success');
+            setReloadVersion(version => version + 1);
           } catch (error) {
             setMessage(error instanceof Error ? error.message : t('common.notify.apply-fail'));
             setMessageTone('error');
@@ -113,40 +129,55 @@ export default function SettingServerPage() {
             data-settings-server-page="otlp-cold-message-server"
             data-settings-server-style-baseline={coldServerVisual.canvasName}
             data-settings-server-layout="full-width-settings-summary"
+            data-settings-server-dialog-width-contract="angular-width-40-percent"
+            data-settings-server-dialog-field-layout-contract="angular-label-7-control-12"
           >
             <SettingsConsoleTitle>{t('settings.server')}</SettingsConsoleTitle>
 
             <div data-settings-server-summary="cold-summary-list">
               <SettingsSummaryList
-                items={buildMessageServerSummaryItems(email, sms, t).map(item => ({
-                  ...item,
-                  actionLabel: t('common.button.setting'),
-                  onAction: () => {
-                    setMessage(null);
-                    setMessageTone(null);
-                    if (item.key === 'email') {
-                      setOpenDialog('email');
-                      return;
-                    }
+                items={buildMessageServerSummaryItems(email, sms, t).map(item => {
+                  const title = String(item.title);
+
+                  return {
+                    ...item,
+                    actionLabel: t('common.button.setting'),
+                    actionAriaLabel: t('settings.server.summary.configure-action', { title }),
+                    onAction: () => {
+                      setMessage(null);
+                      setMessageTone(null);
+                      if (item.key === 'email') {
+                        setOpenDialog('email');
+                        return;
+                      }
 
-                    setSmsSnapshot(cloneSmsSender(sms));
-                    setOpenDialog('sms');
-                  }
-                }))}
+                      setSmsSnapshot(cloneSmsSender(sms));
+                      setOpenDialog('sms');
+                    }
+                  };
+                })}
               />
             </div>
 
             {message ? (
-              <div className={`text-[12px] leading-5 ${messageTone === 'success' ? 'text-emerald-300' : 'text-rose-300'}`}>
-                {message}
-              </div>
+              <HzInlineFeedback
+                tone={messageTone === 'success' ? 'success' : 'critical'}
+                title={message}
+                variant="embedded"
+                data-settings-server-apply-feedback="angular-apply-notify"
+                data-settings-server-apply-feedback-owner="hertzbeat-ui-inline-feedback"
+              />
             ) : null}
 
             <OverlayDialog
               open={openDialog === 'email'}
               title={t('settings.server.email.setting')}
               onClose={() => setOpenDialog(null)}
-              maxWidthClassName="max-w-3xl"
+              maxWidthClassName="w-[min(92vw,520px)] md:w-[40vw] md:max-w-[40vw]"
+              overlayProps={{
+                'data-settings-server-email-dialog-width': 'angular-width-40-percent',
+                'data-settings-server-email-dialog-mask': 'angular-mask-closable-false'
+              }}
               footer={
                 <SettingsDialogFooter>
                   <Button type="button" variant="default" className={coldSecondaryButtonClassName} onClick={() => setOpenDialog(null)}>
@@ -164,7 +195,7 @@ export default function SettingServerPage() {
                 </SettingsDialogFooter>
               }
             >
-              <SettingsDialogForm>
+              <SettingsDialogForm data-settings-server-email-apply-contract="angular-apply-notify">
                 <SettingsDialogField label={t('alert.notice.sender.mail.host')} required>
                   <SettingsDialogInput
                     value={email.emailHost || ''}
@@ -223,7 +254,11 @@ export default function SettingServerPage() {
                 setSmsDraft(cloneSmsSender(smsSnapshot || data.sms || {}));
                 setOpenDialog(null);
               }}
-              maxWidthClassName="max-w-3xl"
+              maxWidthClassName="w-[min(92vw,520px)] md:w-[40vw] md:max-w-[40vw]"
+              overlayProps={{
+                'data-settings-server-sms-dialog-width': 'angular-width-40-percent',
+                'data-settings-server-sms-dialog-mask': 'angular-mask-closable-false'
+              }}
               footer={
                 <SettingsDialogFooter>
                   <Button
@@ -249,7 +284,7 @@ export default function SettingServerPage() {
                 </SettingsDialogFooter>
               }
             >
-              <SettingsDialogForm>
+              <SettingsDialogForm data-settings-server-sms-apply-contract="angular-apply-notify">
                 <SettingsDialogField label={t('alert.notice.sender.sms.type')} required>
                   <SettingsDialogSelect
                     value={smsType}
@@ -351,7 +386,7 @@ export default function SettingServerPage() {
                       >
                         {UNISMS_AUTH_MODE_OPTIONS.map(option => (
                           <option key={option.value} value={option.value}>
-                            {option.label}
+                            {t(option.labelKey)}
                           </option>
                         ))}
                       </SettingsDialogSelect>
diff --git a/web-next/app/setting/settings/settings-layout-client.test.tsx b/web-next/app/setting/settings/settings-layout-client.test.tsx
new file mode 100644
index 0000000000..1b69691c02
--- /dev/null
+++ b/web-next/app/setting/settings/settings-layout-client.test.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../../test/i18n-test-helper';
+
+describe('settings settings layout client', () => {
+  it('renders localized header chrome and menu labels through the shared settings shell', async () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    vi.resetModules();
+    vi.doMock('next/navigation', () => ({
+      usePathname: () => '/setting/settings/config'
+    }));
+    vi.doMock('@/components/providers/i18n-provider', () => ({
+      useI18n: () => ({ t })
+    }));
+    vi.doMock('@/components/settings/settings-console-shell', () => ({
+      SettingsConsoleShell: ({
+        activeHref,
+        children,
+        contentLabel,
+        items,
+        kicker,
+        navigationLabel,
+        subtitle,
+        title
+      }: {
+        activeHref?: string | null;
+        children: React.ReactNode;
+        contentLabel?: string;
+        items: Array<{ href: string; label: string }>;
+        kicker?: string;
+        navigationLabel?: string;
+        subtitle?: string;
+        title?: string;
+      }) => (
+        <section data-active-href={activeHref}>
+          <p>{kicker}</p>
+          <h1>{title}</h1>
+          <p>{subtitle}</p>
+          <nav aria-label={navigationLabel}>
+            {items.map(item => (
+              <a key={item.href} href={item.href}>
+                {item.label}
+              </a>
+            ))}
+          </nav>
+          <main aria-label={contentLabel}>{children}</main>
+        </section>
+      )
+    }));
+
+    const { default: SettingsSettingsLayoutClient } = await import('./settings-layout-client');
+    const html = renderToStaticMarkup(
+      <SettingsSettingsLayoutClient>
+        <div>content</div>
+      </SettingsSettingsLayoutClient>
+    );
+
+    expect(html).toContain('data-active-href="/setting/settings/config"');
+    expect(html).toContain('设置');
+    expect(html).toContain('系统设置');
+    expect(html).toContain('在同一控制台配置系统、消息服务、对象存储和令牌。');
+    expect(html).toContain('aria-label="设置分区"');
+    expect(html).toContain('aria-label="设置内容"');
+    expect(html).toContain('系统配置');
+    expect(html).toContain('消息服务配置');
+    expect(html).toContain('文件服务配置');
+    expect(html).toContain('令牌管理');
+    expect(html).toContain('content');
+    expect(html).not.toContain('settings.console.');
+  });
+});
diff --git a/web-next/app/setting/settings/layout.tsx b/web-next/app/setting/settings/settings-layout-client.tsx
similarity index 73%
copy from web-next/app/setting/settings/layout.tsx
copy to web-next/app/setting/settings/settings-layout-client.tsx
index 49b3bcaaf9..28957ed601 100644
--- a/web-next/app/setting/settings/layout.tsx
+++ b/web-next/app/setting/settings/settings-layout-client.tsx
@@ -1,11 +1,13 @@
 'use client';
 
+import React from 'react';
+import type { ReactNode } from 'react';
 import { usePathname } from 'next/navigation';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { SettingsConsoleShell } from '@/components/settings/settings-console-shell';
 import { buildSettingsMenuRows } from '@/lib/setting-settings-layout/view-model';
 
-export default function SettingSettingsLayout({ children }: { children: React.ReactNode }) {
+export default function SettingsSettingsLayoutClient({ children }: { children: ReactNode }) {
   const { t } = useI18n();
   const pathname = usePathname();
   const menus = buildSettingsMenuRows(t).map(row => ({
@@ -17,7 +19,9 @@ export default function SettingSettingsLayout({ children }: { children: React.Re
     <SettingsConsoleShell
       items={menus}
       activeHref={pathname}
+      contentLabel={t('settings.console.content')}
       kicker={t('settings.console.kicker')}
+      navigationLabel={t('settings.console.navigation')}
       title={t('settings.console.title')}
       subtitle={t('settings.console.copy')}
       className=""
diff --git a/web-next/app/setting/settings/token/page.test.tsx b/web-next/app/setting/settings/token/page.test.tsx
index 7aaede4a56..f05321148d 100644
--- a/web-next/app/setting/settings/token/page.test.tsx
+++ b/web-next/app/setting/settings/token/page.test.tsx
@@ -43,33 +43,14 @@ const mockState = vi.hoisted(() => ({
 }));
 
 const apiMessageGet = vi.hoisted(() => vi.fn());
+const apiGet = vi.hoisted(() => vi.fn());
+const apiDelete = vi.hoisted(() => vi.fn());
 const loadTokenData = vi.hoisted(() => vi.fn());
 
 vi.mock('../../../../components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'setting.token.loading': '正在加载令牌管理',
-        'settings.token': '令牌管理',
-        'settings.token.console.kicker': 'API 密钥',
-        'settings.token.console.title': '统一管理遥测和自动化令牌',
-        'settings.token.console.copy': '在同一页查看令牌状态、最近使用情况和撤销入口。',
-        'settings.token.console.result.total': '令牌总数',
-        'settings.token.console.result.active': '可用令牌',
-        'settings.token.console.result.expired': '过期令牌',
-        'settings.token.generate': '生成令牌',
-        'settings.token.desc': '管理用于 API 集成访问的长期令牌,可随时创建或撤销。',
-        'settings.token.name': '令牌名称',
-        'settings.token.value': '令牌值',
-        'settings.token.creator': '创建者',
-        'settings.token.create-time': '创建时间',
-        'settings.token.expire-time': '过期时间',
-        'settings.token.expire.never': '永不过期',
-        'settings.token.last-used': '最近使用时间',
-        'common.edit': '操作',
-        'common.button.delete': '删除'
-      }
+      locale: 'zh-CN'
     })
   })
 }));
@@ -98,20 +79,34 @@ vi.mock('../../../../components/settings/settings-console-shell', () => ({
 }));
 
 vi.mock('../../../../lib/api-client', () => ({
+  apiDelete,
+  apiGet,
   apiMessageGet
 }));
 
 vi.mock('../../../../lib/setting-token/controller', () => ({
+  deleteTokenById: vi.fn(),
+  generateTokenValue: vi.fn(),
   loadTokenData
 }));
 
 vi.mock('../../../../lib/setting-token/view-model', () => ({
+  buildTokenExpirationOptions: () => [
+    { value: '-1', label: '永不过期' },
+    { value: '604800', label: '7 天' },
+    { value: '2592000', label: '30 天' },
+    { value: '7776000', label: '90 天' },
+    { value: '15552000', label: '180 天' },
+    { value: '31536000', label: '365 天' }
+  ],
   isExpired: (token: { expireTime?: string | null }) => token.expireTime === '2026-04-02T08:00:00Z'
 }));
 
 describe('setting token page', () => {
   beforeEach(() => {
     mockState.lastLoad = null;
+    apiDelete.mockReset();
+    apiGet.mockReset();
     apiMessageGet.mockReset().mockResolvedValue(mockState.renderData.tokens);
     loadTokenData.mockReset().mockImplementation(async apiGetFn => {
       const tokens = await apiGetFn('/account/token');
@@ -128,14 +123,24 @@ describe('setting token page', () => {
     expect(html).toContain('data-setting-token-surface="otlp-cold-token-console"');
     expect(html).toContain('data-setting-token-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-setting-token-layout-contract="full-width-admin-no-rail"');
+    expect(html).toContain('data-setting-token-generate-dialog-layout-contract="angular-vertical-form"');
+    expect(html).toContain('data-setting-token-generated-dialog-width-contract="angular-width-50-percent"');
+    expect(html).toContain('data-setting-token-generated-dialog-mask-contract="angular-mask-closable-false"');
     expect(html).toContain('data-setting-token-header="cold-compact-header"');
     expect(html).toContain('data-setting-token-command-row="standard-equal-buttons"');
+    expect(html).toContain('data-setting-token-generate-trigger="angular-generate-token-modal"');
     expect(html).toContain('data-setting-token-admin-layout="full-width-admin-list"');
     expect(html).toContain('data-setting-token-strip="cold-token-strip"');
     expect(html).toContain('data-setting-token-strip-style="cold-inline-counts"');
     expect(html).toContain('data-setting-token-table-panel="cold-dense-table"');
     expect(html).toContain('data-setting-token-table="cold-token-table"');
     expect(html).toContain('data-setting-token-row-action="cold-row-action"');
+    expect(html).toContain('data-setting-token-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-setting-token-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-setting-token-delete-confirm-state="closed"');
+    expect(html).toContain('data-setting-token-delete-confirm-trigger="angular-modal-confirm"');
+    expect(html).toContain('data-setting-token-load-failure-contract="angular-load-failed-retry"');
+    expect(html).toContain('data-setting-token-load-failure-contract-owner="hertzbeat-ui-inline-feedback"');
     expect(html).toContain('令牌管理');
     expect(html).toContain('API 密钥');
     expect(html).toContain('统一管理遥测和自动化令牌');
@@ -151,7 +156,9 @@ describe('setting token page', () => {
     expect(html).toContain('过期时间');
     expect(html).toContain('最近使用时间');
     expect(html).toContain('操作');
-    expect(html).toContain('删除');
+    expect(html).toContain('删除选中项');
+    expect(html).toContain('aria-label="删除OTLP ingestion"');
+    expect(html).toContain('aria-label="删除Expired token"');
     expect(html).toContain('永不过期');
     expect(html).toContain('OTLP ingestion');
     expect(html).toContain('Expired token');
@@ -177,7 +184,7 @@ describe('setting token page', () => {
   });
 
   it('keeps the token route on the settings owner instead of observability workbench primitives', () => {
-    const source = readFileSync(resolve(__dirname, 'page.tsx'), 'utf8');
+    const source = readFileSync(resolve(__dirname, 'setting-token-page.tsx'), 'utf8');
 
     expect(source).toContain('coldOpsCatalogVisual');
     expect(source).toContain('data-setting-token-surface="otlp-cold-token-console"');
@@ -185,6 +192,39 @@ describe('setting token page', () => {
     expect(source).toContain('data-setting-token-layout-contract="full-width-admin-no-rail"');
     expect(source).toContain('data-setting-token-header="cold-compact-header"');
     expect(source).toContain('data-setting-token-command-row="standard-equal-buttons"');
+    expect(source).toContain('data-setting-token-generate-trigger="angular-generate-token-modal"');
+    expect(source).toContain('data-setting-token-generate-form="angular-generate-token-modal"');
+    expect(source).toContain('data-setting-token-generate-dialog-layout-contract="angular-vertical-form"');
+    expect(source).toContain('data-setting-token-generate-form-layout="angular-vertical-form"');
+    expect(source).toContain('layout="vertical"');
+    expect(source).toContain('data-setting-token-generate-submit="angular-generate-token"');
+    expect(source).toContain('data-setting-token-generated-dialog-width-contract="angular-width-50-percent"');
+    expect(source).toContain('data-setting-token-generated-dialog-mask-contract="angular-mask-closable-false"');
+    expect(source).toContain('md:w-[50vw] md:max-w-[50vw]');
+    expect(source).toContain('maskClosable={false}');
+    expect(source).toContain('data-setting-token-generated-dialog="angular-token-display-once"');
+    expect(source).toContain('data-setting-token-copy-action="angular-copy-token"');
+    expect(source).toContain('data-setting-token-action-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-setting-token-delete-confirm="angular-modal-confirm"');
+    expect(source).toContain('data-setting-token-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain("data-setting-token-delete-confirm-state={deleteTarget ? 'open' : 'closed'}");
+    expect(source).toContain('data-setting-token-delete-confirm-trigger="angular-modal-confirm"');
+    expect(source).toContain('data-setting-token-delete-confirm-dialog="angular-modal-confirm"');
+    expect(source).toContain('data-setting-token-delete-confirm-ok');
+    expect(source).toContain('data-setting-token-delete-confirm-cancel');
+    expect(source).toContain('renderError={(message, retry) =>');
+    expect(source).toContain('data-setting-token-load-failure="angular-load-failed-retry"');
+    expect(source).toContain('data-setting-token-load-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-setting-token-load-failure-contract="angular-load-failed-retry"');
+    expect(source).toContain('data-setting-token-load-failure-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-setting-token-load-failure-feedback="angular-load-failed-retry"');
+    expect(source).toContain('data-setting-token-load-retry="angular-load-tokens-retry"');
+    expect(source).toContain("t('settings.token.load-fail-title')");
+    expect(source).toContain("t('settings.token.load-fail')");
+    expect(source).toContain("t('common.button.retry')");
+    expect(source).toContain('deleteTokenById(apiDelete');
+    expect(source).toContain('generateTokenValue(apiGet');
+    expect(source).toContain('buildTokenExpirationOptions(t)');
     expect(source).toContain('data-setting-token-admin-layout="full-width-admin-list"');
     expect(source).toContain('data-setting-token-table="cold-token-table"');
     expect(source).toContain('data-setting-token-strip-style="cold-inline-counts"');
@@ -200,4 +240,48 @@ describe('setting token page', () => {
     expect(source).not.toContain('SummaryMetricGrid');
     expect(source).not.toContain('StageSection');
   });
+
+  it('renders localized empty fallbacks for blank token table cells', async () => {
+    const previousRenderData = mockState.renderData;
+    mockState.renderData = {
+      tokens: [
+        {
+          id: 9,
+          name: ' ',
+          tokenMask: '',
+          creator: null,
+          gmtCreate: '',
+          expireTime: null,
+          lastUsedTime: null
+        }
+      ]
+    } as any;
+
+    try {
+      const { default: SettingTokenPage } = await import('./page');
+      const html = renderToStaticMarkup(<SettingTokenPage />);
+
+      expect(html).toContain('data-setting-token-table="cold-token-table"');
+      expect(html).toContain('无');
+      expect(html).toContain('永不过期');
+      expect(html).not.toContain('<td class="border-b border-r border-[#2b3039] bg-[#0b0c0e] px-3 py-3 text-[#d0d5dd] last:border-r-0">-</td>');
+      expect(html).not.toContain('<code class="rounded-[3px] border border-[#2b3039] bg-[#101217] px-1.5 py-0.5 text-[12px] text-[#c8d2df]">-</code>');
+    } finally {
+      mockState.renderData = previousRenderData;
+    }
+  });
+
+  it('keeps token settings remounts on a short settled cache window and refreshes after generation', () => {
+    const source = readFileSync(resolve(__dirname, 'setting-token-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_TOKEN_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['setting-token', '/account/token', reloadVersion].join(':')");
+    expect(source).toContain('void reloadVersion');
+    expect(source).toContain('[reloadVersion]');
+    expect(source).toContain('loadTokenData(apiMessageGet)');
+    expect(source).toContain('setReloadVersion(version => version + 1)');
+    expect(source).toContain('cacheKey={settingTokenCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_TOKEN_SETTLED_CACHE_TTL_MS}');
+    expect(source).not.toContain('fake');
+  });
 });
diff --git a/web-next/app/setting/settings/token/page.tsx b/web-next/app/setting/settings/token/page.tsx
index b51218dca9..8348bc3bb1 100644
--- a/web-next/app/setting/settings/token/page.tsx
+++ b/web-next/app/setting/settings/token/page.tsx
@@ -1,192 +1,7 @@
-'use client';
+import React from 'react';
 
-import React, { useCallback } from 'react';
-import { Plus, Trash2 } from 'lucide-react';
-import { ClientWorkbench } from '../../../../components/workbench/client-workbench';
-import { useI18n } from '../../../../components/providers/i18n-provider';
-import { SettingsConsoleTitle } from '../../../../components/settings/settings-console-shell';
-import { Button } from '../../../../components/ui/button';
-import { coldOpsCatalogVisual } from '../../../../lib/cold-ops-visual';
-import { apiMessageGet } from '../../../../lib/api-client';
-import { loadTokenData } from '../../../../lib/setting-token/controller';
-import { isExpired } from '../../../../lib/setting-token/view-model';
-import type { AuthToken } from '../../../../lib/types';
+import SettingTokenPage from './setting-token-page';
 
-type TokenData = {
-  tokens: AuthToken[];
-};
-
-const coldTokenVisual = coldOpsCatalogVisual;
-
-const coldPrimaryButtonClassName =
-  'h-8 min-w-[104px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-[#f8fafc]';
-
-const tokenHeaderCellClassName =
-  'border-b border-r border-[#2b3039] bg-[#101217] px-3 py-2.5 text-[11px] font-semibold text-[#8d95a5] last:border-r-0';
-
-const tokenBodyCellClassName = 'border-b border-r border-[#2b3039] bg-[#0b0c0e] px-3 py-3 text-[#d0d5dd] last:border-r-0';
-
-function formatTokenDate(value?: string | number | null) {
-  if (value == null || value === '') return '-';
-  const date = new Date(value);
-  if (Number.isNaN(date.getTime())) return String(value);
-
-  const pad = (part: number) => String(part).padStart(2, '0');
-  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
-}
-
-function resolveTokenName(token: AuthToken) {
-  return token.name || token.tokenMask || '-';
-}
-
-function buildTokenCounts(tokens: AuthToken[]) {
-  const expired = tokens.filter(token => isExpired(token)).length;
-  return {
-    total: tokens.length,
-    active: tokens.length - expired,
-    expired
-  };
-}
-
-export default function SettingTokenPage() {
-  const { t } = useI18n();
-
-  const load = useCallback(async (): Promise<TokenData> => {
-    return loadTokenData(apiMessageGet);
-  }, []);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy="正在加载令牌管理。">
-      {data => {
-        const tokens = data.tokens || [];
-        const counts = buildTokenCounts(tokens);
-
-        return (
-          <div
-            data-setting-token-surface="otlp-cold-token-console"
-            data-setting-token-style-baseline={coldTokenVisual.canvasName}
-            data-setting-token-layout-contract="full-width-admin-no-rail"
-            className="space-y-4"
-          >
-            <SettingsConsoleTitle>{t('settings.token')}</SettingsConsoleTitle>
-
-            <div data-setting-token-admin-layout="full-width-admin-list" className="space-y-4">
-              <section
-                data-setting-token-header="cold-compact-header"
-                className="rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-4 shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
-              >
-                <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
-                  <div className="min-w-0">
-                    <div className="text-[11px] font-semibold text-[#8d95a5]">{t('settings.token.console.kicker')}</div>
-                    <h2 className="mt-2 text-[18px] font-semibold leading-6 text-[#f5f7fb]">
-                      {t('settings.token.console.title')}
-                    </h2>
-                    <p className="mt-2 max-w-[680px] text-[12px] leading-5 text-[#98a2b3]">
-                      {t('settings.token.console.copy')}
-                    </p>
-                  </div>
-                  <div data-setting-token-command-row="standard-equal-buttons" className="flex shrink-0 flex-wrap items-center gap-2">
-                    <Button type="button" variant="default" className={coldPrimaryButtonClassName}>
-                      <Plus className="h-3.5 w-3.5" aria-hidden="true" />
-                      {t('settings.token.generate')}
-                    </Button>
-                  </div>
-                </div>
-                <div
-                  data-setting-token-strip="cold-token-strip"
-                  data-setting-token-strip-style="cold-inline-counts"
-                  className="mt-4 grid gap-2 md:grid-cols-3"
-                >
-                  {[
-                    [t('settings.token.console.result.total'), counts.total],
-                    [t('settings.token.console.result.active'), counts.active],
-                    [t('settings.token.console.result.expired'), counts.expired]
-                  ].map(([label, value]) => (
-                    <div key={String(label)} className="flex min-h-9 items-center justify-between rounded-[3px] border border-[#2b3039] bg-[#101217] px-3">
-                      <span className="text-[12px] font-semibold text-[#8d95a5]">{label}</span>
-                      <span className="text-[15px] font-semibold text-[#dbe4f0]">{value}</span>
-                    </div>
-                  ))}
-                </div>
-              </section>
-
-              <section
-                data-setting-token-table-panel="cold-dense-table"
-                className="rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
-              >
-                <div className="border-b border-[#2b3039] px-4 py-3">
-                  <p className="text-[12px] leading-5 text-[#98a2b3]">{t('settings.token.desc')}</p>
-                </div>
-                <div className="overflow-x-auto">
-                  <table data-setting-token-table="cold-token-table" className="w-full min-w-[860px] border-collapse text-left text-[12px]">
-                    <thead>
-                      <tr>
-                        <th className={tokenHeaderCellClassName}>{t('settings.token.name')}</th>
-                        <th className={tokenHeaderCellClassName}>{t('settings.token.value')}</th>
-                        <th className={tokenHeaderCellClassName}>{t('settings.token.creator')}</th>
-                        <th className={tokenHeaderCellClassName}>{t('settings.token.create-time')}</th>
-                        <th className={tokenHeaderCellClassName}>{t('settings.token.expire-time')}</th>
-                        <th className={tokenHeaderCellClassName}>{t('settings.token.last-used')}</th>
-                        <th className={tokenHeaderCellClassName}>{t('common.edit')}</th>
-                      </tr>
-                    </thead>
-                    <tbody>
-                      {tokens.length === 0 ? (
-                        <tr>
-                          <td
-                            data-setting-token-empty-state="cold-table-empty"
-                            colSpan={7}
-                            className="bg-[#0b0c0e] px-3 py-10 text-center text-[12px] text-[#8d95a5]"
-                          >
-                            {t('settings.token.empty')}
-                          </td>
-                        </tr>
-                      ) : (
-                        tokens.map(token => {
-                          const expired = isExpired(token);
-
-                          return (
-                            <tr key={token.id}>
-                              <td className={tokenBodyCellClassName}>{resolveTokenName(token)}</td>
-                              <td className={tokenBodyCellClassName}>
-                                <code className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-1.5 py-0.5 text-[12px] text-[#c8d2df]">
-                                  {token.tokenMask || '-'}
-                                </code>
-                              </td>
-                              <td className={tokenBodyCellClassName}>{token.creator || '-'}</td>
-                              <td className={tokenBodyCellClassName}>{formatTokenDate(token.gmtCreate)}</td>
-                              <td className={tokenBodyCellClassName}>
-                                {token.expireTime ? (
-                                  <span className={expired ? 'text-[#fda29b]' : undefined}>{formatTokenDate(token.expireTime)}</span>
-                                ) : (
-                                  <span className="rounded-[3px] border border-[rgba(34,197,94,0.28)] bg-[rgba(22,163,74,0.12)] px-2 py-1 text-[12px] text-[#d7f5df]">
-                                    {t('settings.token.expire.never')}
-                                  </span>
-                                )}
-                              </td>
-                              <td className={tokenBodyCellClassName}>{formatTokenDate(token.lastUsedTime)}</td>
-                              <td className="border-b border-[#2b3039] bg-[#0b0c0e] px-3 py-3">
-                                <button
-                                  type="button"
-                                  data-setting-token-row-action="cold-row-action"
-                                  className="inline-flex h-8 min-w-[96px] items-center justify-center gap-1 whitespace-nowrap rounded-[3px] border border-[#3a2c31] bg-[#151014] px-2 text-[12px] font-semibold text-[#fca5a5] hover:border-[#7f1d1d] hover:bg-[#1f1115] hover:text-[#fecaca]"
-                                >
-                                  <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
-                                  {t('common.button.delete')}
-                                </button>
-                              </td>
-                            </tr>
-                          );
-                        })
-                      )}
-                    </tbody>
-                  </table>
-                </div>
-              </section>
-            </div>
-          </div>
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function SettingTokenRoutePage() {
+  return <SettingTokenPage />;
 }
diff --git a/web-next/app/setting/settings/token/setting-token-page.tsx b/web-next/app/setting/settings/token/setting-token-page.tsx
new file mode 100644
index 0000000000..4bbf1efd3e
--- /dev/null
+++ b/web-next/app/setting/settings/token/setting-token-page.tsx
@@ -0,0 +1,498 @@
+'use client';
+
+import React, { useCallback, useMemo, useState } from 'react';
+import { Plus, Trash2 } from 'lucide-react';
+import { HzConfirmDialog, HzInlineFeedback } from '@hertzbeat/ui';
+import { ClientWorkbench } from '../../../../components/workbench/client-workbench';
+import { useI18n } from '../../../../components/providers/i18n-provider';
+import { OverlayDialog } from '../../../../components/workbench/overlay-dialog';
+import { SettingsConsoleTitle } from '../../../../components/settings/settings-console-shell';
+import {
+  SettingsDialogField,
+  SettingsDialogFooter,
+  SettingsDialogForm,
+  SettingsDialogInput,
+  SettingsDialogSelect
+} from '../../../../components/settings/settings-dialog-form';
+import { Button } from '../../../../components/ui/button';
+import { coldOpsCatalogVisual } from '../../../../lib/cold-ops-visual';
+import { apiDelete, apiGet, apiMessageGet } from '../../../../lib/api-client';
+import { deleteTokenById, generateTokenValue, loadTokenData } from '../../../../lib/setting-token/controller';
+import { buildTokenExpirationOptions, isExpired } from '../../../../lib/setting-token/view-model';
+import type { AuthToken } from '../../../../lib/types';
+
+type TokenData = {
+  tokens: AuthToken[];
+};
+
+const coldTokenVisual = coldOpsCatalogVisual;
+const SETTING_TOKEN_SETTLED_CACHE_TTL_MS = 10_000;
+
+const coldPrimaryButtonClassName =
+  'h-8 min-w-[104px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-[#f8fafc]';
+
+const coldSecondaryButtonClassName =
+  'h-8 min-w-[96px] rounded-[3px] border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151923] hover:text-[#f8fafc]';
+
+const tokenHeaderCellClassName =
+  'border-b border-r border-[#2b3039] bg-[#101217] px-3 py-2.5 text-[11px] font-semibold text-[#8d95a5] last:border-r-0';
+
+const tokenBodyCellClassName = 'border-b border-r border-[#2b3039] bg-[#0b0c0e] px-3 py-3 text-[#d0d5dd] last:border-r-0';
+
+function formatTokenCell(value: string | number | null | undefined, fallback: string) {
+  const normalized = value == null ? '' : String(value).trim();
+  return normalized || fallback;
+}
+
+function formatTokenDate(value: string | number | null | undefined, fallback: string) {
+  const normalized = formatTokenCell(value, '');
+  if (!normalized) return fallback;
+  const date = new Date(normalized);
+  if (Number.isNaN(date.getTime())) return normalized;
+
+  const pad = (part: number) => String(part).padStart(2, '0');
+  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
+}
+
+function resolveTokenName(token: AuthToken, fallback: string) {
+  return formatTokenCell(token.name, '') || formatTokenCell(token.tokenMask, '') || fallback;
+}
+
+function buildTokenCounts(tokens: AuthToken[]) {
+  const expired = tokens.filter(token => isExpired(token)).length;
+  return {
+    total: tokens.length,
+    active: tokens.length - expired,
+    expired
+  };
+}
+
+export default function SettingTokenPage() {
+  const { t } = useI18n();
+  const [generateDialogOpen, setGenerateDialogOpen] = useState(false);
+  const [resultDialogOpen, setResultDialogOpen] = useState(false);
+  const [tokenName, setTokenName] = useState('');
+  const [expireSeconds, setExpireSeconds] = useState('-1');
+  const [generatedToken, setGeneratedToken] = useState('');
+  const [deleteTarget, setDeleteTarget] = useState<AuthToken | null>(null);
+  const [generating, setGenerating] = useState(false);
+  const [deleting, setDeleting] = useState(false);
+  const [actionMessage, setActionMessage] = useState<string | null>(null);
+  const [actionTone, setActionTone] = useState<'success' | 'warning' | 'error' | null>(null);
+  const [reloadVersion, setReloadVersion] = useState(0);
+  const settingTokenCacheKey = useMemo(() => ['setting-token', '/account/token', reloadVersion].join(':'), [reloadVersion]);
+  const emptyValue = t('common.none');
+  const expireOptions = useMemo(() => buildTokenExpirationOptions(t), [t]);
+
+  const load = useCallback(async (): Promise<TokenData> => {
+    void reloadVersion;
+    return loadTokenData(apiMessageGet);
+  }, [reloadVersion]);
+
+  async function copyGeneratedToken() {
+    if (!generatedToken) return;
+
+    try {
+      if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
+        await navigator.clipboard.writeText(generatedToken);
+      } else {
+        const textarea = document.createElement('textarea');
+        textarea.value = generatedToken;
+        document.body.appendChild(textarea);
+        textarea.select();
+        const success = document.execCommand('copy');
+        document.body.removeChild(textarea);
+        if (!success) {
+          throw new Error(t('common.notify.copy-fail'));
+        }
+      }
+      setActionMessage(t('common.notify.copy-success'));
+      setActionTone('success');
+    } catch {
+      setActionMessage(t('common.notify.copy-fail'));
+      setActionTone('warning');
+    }
+  }
+
+  async function generateToken() {
+    const name = tokenName.trim();
+    if (!name) return;
+
+    setGenerating(true);
+    setActionMessage(null);
+    setActionTone(null);
+    setGenerateDialogOpen(false);
+    try {
+      const token = await generateTokenValue(apiGet, name, expireSeconds, t('settings.token.generate-fail'));
+      setGeneratedToken(token);
+      setResultDialogOpen(true);
+      setTokenName('');
+      setExpireSeconds('-1');
+      setReloadVersion(version => version + 1);
+    } catch (error) {
+      setActionMessage(error instanceof Error ? error.message : t('settings.token.generate-fail'));
+      setActionTone('warning');
+    } finally {
+      setGenerating(false);
+    }
+  }
+
+  function closeGeneratedTokenDialog() {
+    setResultDialogOpen(false);
+    setGeneratedToken('');
+  }
+
+  async function confirmDeleteToken() {
+    if (!deleteTarget) return;
+
+    const target = deleteTarget;
+    setDeleting(true);
+    setDeleteTarget(null);
+    setActionMessage(null);
+    setActionTone(null);
+    try {
+      await deleteTokenById(apiDelete, target.id, t('common.notify.delete-fail'));
+      setActionMessage(t('common.notify.delete-success'));
+      setActionTone('success');
+      setReloadVersion(version => version + 1);
+    } catch (error) {
+      setActionMessage(error instanceof Error ? error.message : t('common.notify.delete-fail'));
+      setActionTone('error');
+    } finally {
+      setDeleting(false);
+    }
+  }
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.token.loading')}
+      cacheKey={settingTokenCacheKey}
+      cacheSettledTtlMs={SETTING_TOKEN_SETTLED_CACHE_TTL_MS}
+      renderError={(message, retry) => (
+        <section
+          data-setting-token-load-failure="angular-load-failed-retry"
+          data-setting-token-load-failure-owner="hertzbeat-ui-inline-feedback"
+          className="space-y-3 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-4 shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
+        >
+          <HzInlineFeedback
+            tone="warning"
+            title={t('settings.token.load-fail-title')}
+            description={message || t('settings.token.load-fail')}
+            variant="embedded"
+            data-setting-token-load-failure-feedback="angular-load-failed-retry"
+            data-setting-token-load-failure-feedback-owner="hertzbeat-ui-inline-feedback"
+          />
+          <Button
+            type="button"
+            variant="default"
+            className={coldSecondaryButtonClassName}
+            data-setting-token-load-retry="angular-load-tokens-retry"
+            onClick={retry}
+          >
+            {t('common.button.retry')}
+          </Button>
+        </section>
+      )}
+    >
+      {data => {
+        const tokens = data.tokens || [];
+        const counts = buildTokenCounts(tokens);
+
+        return (
+          <div
+            data-setting-token-surface="otlp-cold-token-console"
+            data-setting-token-style-baseline={coldTokenVisual.canvasName}
+            data-setting-token-layout-contract="full-width-admin-no-rail"
+            data-setting-token-generate-dialog-layout-contract="angular-vertical-form"
+            data-setting-token-generated-dialog-width-contract="angular-width-50-percent"
+            data-setting-token-generated-dialog-mask-contract="angular-mask-closable-false"
+            className="space-y-4"
+          >
+            <SettingsConsoleTitle>{t('settings.token')}</SettingsConsoleTitle>
+
+            <div data-setting-token-admin-layout="full-width-admin-list" className="space-y-4">
+              <section
+                data-setting-token-header="cold-compact-header"
+                className="rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] p-4 shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
+              >
+                <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
+                  <div className="min-w-0">
+                    <div className="text-[11px] font-semibold text-[#8d95a5]">{t('settings.token.console.kicker')}</div>
+                    <h2 className="mt-2 text-[18px] font-semibold leading-6 text-[#f5f7fb]">
+                      {t('settings.token.console.title')}
+                    </h2>
+                    <p className="mt-2 max-w-[680px] text-[12px] leading-5 text-[#98a2b3]">
+                      {t('settings.token.console.copy')}
+                    </p>
+                  </div>
+                  <div data-setting-token-command-row="standard-equal-buttons" className="flex shrink-0 flex-wrap items-center gap-2">
+                    <Button
+                      type="button"
+                      variant="default"
+                      className={coldPrimaryButtonClassName}
+                      disabled={generating}
+                      data-setting-token-generate-trigger="angular-generate-token-modal"
+                      onClick={() => {
+                        setActionMessage(null);
+                        setActionTone(null);
+                        setTokenName('');
+                        setExpireSeconds('-1');
+                        setGenerateDialogOpen(true);
+                      }}
+                    >
+                      <Plus className="h-3.5 w-3.5" aria-hidden="true" />
+                      {t('settings.token.generate')}
+                    </Button>
+                  </div>
+                </div>
+                <div
+                  data-setting-token-strip="cold-token-strip"
+                  data-setting-token-strip-style="cold-inline-counts"
+                  className="mt-4 grid gap-2 md:grid-cols-3"
+                >
+                  {[
+                    [t('settings.token.console.result.total'), counts.total],
+                    [t('settings.token.console.result.active'), counts.active],
+                    [t('settings.token.console.result.expired'), counts.expired]
+                  ].map(([label, value]) => (
+                    <div key={String(label)} className="flex min-h-9 items-center justify-between rounded-[3px] border border-[#2b3039] bg-[#101217] px-3">
+                      <span className="text-[12px] font-semibold text-[#8d95a5]">{label}</span>
+                      <span className="text-[15px] font-semibold text-[#dbe4f0]">{value}</span>
+                    </div>
+                  ))}
+                </div>
+              </section>
+
+              {actionMessage ? (
+                <HzInlineFeedback
+                  tone={actionTone === 'success' ? 'success' : actionTone === 'warning' ? 'warning' : 'critical'}
+                  title={actionMessage}
+                  variant="embedded"
+                  data-setting-token-action-feedback="angular-token-notify"
+                  data-setting-token-action-feedback-owner="hertzbeat-ui-inline-feedback"
+                />
+              ) : null}
+
+              <div
+                data-setting-token-delete-confirm="angular-modal-confirm"
+                data-setting-token-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+                data-setting-token-delete-confirm-state={deleteTarget ? 'open' : 'closed'}
+                className="sr-only"
+              />
+              <div
+                data-setting-token-load-failure-contract="angular-load-failed-retry"
+                data-setting-token-load-failure-contract-owner="hertzbeat-ui-inline-feedback"
+                className="sr-only"
+              />
+
+              <section
+                data-setting-token-table-panel="cold-dense-table"
+                className="rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
+              >
+                <div className="border-b border-[#2b3039] px-4 py-3">
+                  <p className="text-[12px] leading-5 text-[#98a2b3]">{t('settings.token.desc')}</p>
+                </div>
+                <div className="overflow-x-auto">
+                  <table data-setting-token-table="cold-token-table" className="w-full min-w-[860px] border-collapse text-left text-[12px]">
+                    <thead>
+                      <tr>
+                        <th className={tokenHeaderCellClassName}>{t('settings.token.name')}</th>
+                        <th className={tokenHeaderCellClassName}>{t('settings.token.value')}</th>
+                        <th className={tokenHeaderCellClassName}>{t('settings.token.creator')}</th>
+                        <th className={tokenHeaderCellClassName}>{t('settings.token.create-time')}</th>
+                        <th className={tokenHeaderCellClassName}>{t('settings.token.expire-time')}</th>
+                        <th className={tokenHeaderCellClassName}>{t('settings.token.last-used')}</th>
+                        <th className={tokenHeaderCellClassName}>{t('common.edit')}</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      {tokens.length === 0 ? (
+                        <tr>
+                          <td
+                            data-setting-token-empty-state="cold-table-empty"
+                            colSpan={7}
+                            className="bg-[#0b0c0e] px-3 py-10 text-center text-[12px] text-[#8d95a5]"
+                          >
+                            {t('settings.token.empty')}
+                          </td>
+                        </tr>
+                      ) : (
+                        tokens.map(token => {
+                          const expired = isExpired(token);
+
+                          return (
+                            <tr key={token.id}>
+                              <td className={tokenBodyCellClassName}>{resolveTokenName(token, emptyValue)}</td>
+                              <td className={tokenBodyCellClassName}>
+                                <code className="rounded-[3px] border border-[#2b3039] bg-[#101217] px-1.5 py-0.5 text-[12px] text-[#c8d2df]">
+                                  {formatTokenCell(token.tokenMask, emptyValue)}
+                                </code>
+                              </td>
+                              <td className={tokenBodyCellClassName}>{formatTokenCell(token.creator, emptyValue)}</td>
+                              <td className={tokenBodyCellClassName}>{formatTokenDate(token.gmtCreate, emptyValue)}</td>
+                              <td className={tokenBodyCellClassName}>
+                                {token.expireTime ? (
+                                  <span className={expired ? 'text-[#fda29b]' : undefined}>{formatTokenDate(token.expireTime, emptyValue)}</span>
+                                ) : (
+                                  <span className="rounded-[3px] border border-[rgba(34,197,94,0.28)] bg-[rgba(22,163,74,0.12)] px-2 py-1 text-[12px] text-[#d7f5df]">
+                                    {t('settings.token.expire.never')}
+                                  </span>
+                                )}
+                              </td>
+                              <td className={tokenBodyCellClassName}>{formatTokenDate(token.lastUsedTime, emptyValue)}</td>
+                              <td className="border-b border-[#2b3039] bg-[#0b0c0e] px-3 py-3">
+                                <button
+                                  type="button"
+                                  data-setting-token-row-action="cold-row-action"
+                                  data-setting-token-delete-confirm-trigger="angular-modal-confirm"
+                                  data-setting-token-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+                                  aria-label={t('settings.token.delete-action', {
+                                    name: resolveTokenName(token, emptyValue)
+                                  })}
+                                  className="inline-flex h-8 min-w-[96px] items-center justify-center gap-1 whitespace-nowrap rounded-[3px] border border-[#3a2c31] bg-[#151014] px-2 text-[12px] font-semibold text-[#fca5a5] hover:border-[#7f1d1d] hover:bg-[#1f1115] hover:text-[#fecaca]"
+                                  disabled={deleting}
+                                  onClick={() => {
+                                    setActionMessage(null);
+                                    setActionTone(null);
+                                    setDeleteTarget(token);
+                                  }}
+                                >
+                                  <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
+                                  {t('common.button.delete')}
+                                </button>
+                              </td>
+                            </tr>
+                          );
+                        })
+                      )}
+                    </tbody>
+                  </table>
+                </div>
+              </section>
+            </div>
+
+            <OverlayDialog
+              open={generateDialogOpen}
+              title={t('settings.token.generate-title')}
+              onClose={() => setGenerateDialogOpen(false)}
+              maxWidthClassName="max-w-xl"
+              overlayProps={{
+                'data-setting-token-generate-dialog-layout-contract': 'angular-vertical-form'
+              }}
+              footer={
+                <SettingsDialogFooter>
+                  <Button type="button" variant="default" className={coldSecondaryButtonClassName} onClick={() => setGenerateDialogOpen(false)}>
+                    {t('common.button.cancel')}
+                  </Button>
+                  <Button
+                    type="button"
+                    variant="default"
+                    className={coldPrimaryButtonClassName}
+                    disabled={generating || tokenName.trim().length === 0}
+                    data-setting-token-generate-submit="angular-generate-token"
+                    onClick={() => void generateToken()}
+                  >
+                    {generating ? t('common.saving') : t('settings.token.generate')}
+                  </Button>
+                </SettingsDialogFooter>
+              }
+            >
+              <SettingsDialogForm
+                data-setting-token-generate-form="angular-generate-token-modal"
+                data-setting-token-generate-form-layout="angular-vertical-form"
+              >
+                <SettingsDialogField label={t('settings.token.name')} required layout="vertical">
+                  <SettingsDialogInput
+                    value={tokenName}
+                    placeholder={t('settings.token.name-placeholder')}
+                    onChange={event => setTokenName(event.target.value)}
+                  />
+                </SettingsDialogField>
+                <SettingsDialogField label={t('settings.token.expire-time')} layout="vertical">
+                  <SettingsDialogSelect value={expireSeconds} onChange={event => setExpireSeconds(event.target.value)}>
+                    {expireOptions.map(option => (
+                      <option key={option.value} value={option.value}>
+                        {option.label}
+                      </option>
+                    ))}
+                  </SettingsDialogSelect>
+                </SettingsDialogField>
+              </SettingsDialogForm>
+            </OverlayDialog>
+
+            <OverlayDialog
+              open={resultDialogOpen}
+              title={t('settings.token.generated-title')}
+              onClose={closeGeneratedTokenDialog}
+              maxWidthClassName="w-[min(92vw,640px)] md:w-[50vw] md:max-w-[50vw]"
+              maskClosable={false}
+              overlayProps={{
+                'data-setting-token-generated-dialog-width-contract': 'angular-width-50-percent',
+                'data-setting-token-generated-dialog-mask-contract': 'angular-mask-closable-false'
+              }}
+              footer={
+                <SettingsDialogFooter>
+                  <Button type="button" variant="default" className={coldPrimaryButtonClassName} onClick={closeGeneratedTokenDialog}>
+                    {t('common.button.confirm')}
+                  </Button>
+                </SettingsDialogFooter>
+              }
+            >
+              <div className="space-y-3" data-setting-token-generated-dialog="angular-token-display-once">
+                <HzInlineFeedback
+                  tone="warning"
+                  title={t('settings.token.notice')}
+                  variant="embedded"
+                  data-setting-token-generated-notice="visible-once"
+                />
+                <div className="flex justify-end">
+                  <Button
+                    type="button"
+                    variant="default"
+                    className={coldPrimaryButtonClassName}
+                    data-setting-token-copy-action="angular-copy-token"
+                    onClick={() => void copyGeneratedToken()}
+                  >
+                    {t('common.button.copy')}
+                  </Button>
+                </div>
+                <code
+                  data-setting-token-generated-value="masked-only-after-generation"
+                  className="block max-h-48 overflow-auto rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 py-2 text-[12px] leading-5 text-[#dbe4f0]"
+                >
+                  {generatedToken}
+                </code>
+              </div>
+            </OverlayDialog>
+
+            <HzConfirmDialog
+              open={Boolean(deleteTarget)}
+              tone="critical"
+              title={t('settings.token.delete-confirm')}
+              cancelLabel={t('common.button.cancel')}
+              confirmLabel={deleting ? t('common.saving') : t('common.button.delete')}
+              confirmDisabled={deleting}
+              onClose={() => setDeleteTarget(null)}
+              onConfirm={() => void confirmDeleteToken()}
+              data-setting-token-delete-confirm-dialog="angular-modal-confirm"
+              data-setting-token-delete-confirm-target={deleteTarget ? resolveTokenName(deleteTarget, emptyValue) : ''}
+              cancelButtonProps={{
+                'data-setting-token-delete-confirm-cancel': 'angular-modal-confirm'
+              }}
+              confirmButtonProps={{
+                'data-setting-token-delete-confirm-ok': 'angular-modal-confirm'
+              }}
+            >
+              <p data-setting-token-delete-confirm-copy="angular-modal-confirm">
+                {t('settings.token.delete-confirm-content', {
+                  name: deleteTarget ? resolveTokenName(deleteTarget, emptyValue) : ''
+                })}
+              </p>
+            </HzConfirmDialog>
+          </div>
+        );
+      }}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/setting/status/page.test.tsx b/web-next/app/setting/status/page.test.tsx
index 257085b9a5..97eb011536 100644
--- a/web-next/app/setting/status/page.test.tsx
+++ b/web-next/app/setting/status/page.test.tsx
@@ -18,15 +18,16 @@ const loadStatusManagementData = vi.fn(async () => ({
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: createTranslatorMock()
+    t: createTranslatorMock({ locale: 'en-US' }),
+    locale: 'en-US'
   })
 }));
 
 vi.mock('@/components/workbench/client-workbench', () => ({
-  ClientWorkbench: ({ children, load }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown> }) => {
+  ClientWorkbench: ({ children, load, loadingCopy }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown>; loadingCopy?: string }) => {
     loadState.lastLoad = load;
     return (
-      <div data-client-workbench="true">
+      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
         {children({
           org: { id: 1, name: 'HB Status', description: 'Service health', state: 0 },
           components: [{ id: 1, name: 'API', description: 'public api', state: 0, gmtUpdate: 1712730000000 }],
@@ -150,6 +151,7 @@ vi.mock('@/lib/api-client', () => ({
 }));
 
 vi.mock('@/lib/setting-status/controller', () => ({
+  buildStatusIncidentListUrl: ({ search = '', pageIndex = 0, pageSize = 8 }: any = {}) => `/status/page/incident?pageIndex=${pageIndex}&pageSize=${pageSize}${search ? `&search=${search}` : ''}`,
   createStatusPageComponent: vi.fn(),
   createStatusPageIncident: vi.fn(),
   deleteStatusPageComponent: vi.fn(),
@@ -172,6 +174,7 @@ describe('setting status page', () => {
     await lastLoad?.();
 
     expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-loading-copy="Loading status settings"');
     expect(html).toContain('data-status-setting-surface="otlp-cold-status-console"');
     expect(html).toContain('data-status-setting-style-baseline="hertzbeat-cold-matte"');
     expect(html).toContain('data-status-admin-layout="full-width-admin-list"');
@@ -188,13 +191,40 @@ describe('setting status page', () => {
   }, 15000);
 
   it('keeps the route as load composition and leaves visual ownership in the shared surface', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/setting/status/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/status/setting-status-page.tsx'), 'utf8');
 
     expect(source).toContain('StatusSettingSurface');
+    expect(source).toContain("t('common.notify.apply-success')");
+    expect(source).toContain("t('common.notify.apply-fail')");
+    expect(source).toContain("t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success')");
+    expect(source).toContain("t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail')");
+    expect(source).toContain("t('common.notify.delete-success')");
+    expect(source).toContain("t('common.notify.delete-fail')");
+    expect(source).toContain("t('status.component.notify.need-org')");
+    expect(source).not.toContain("t('common.save-success')");
+    expect(source).not.toContain("t('common.save-failed')");
+    expect(source).not.toContain("t('common.delete-success')");
+    expect(source).not.toContain("t('common.delete-failed')");
     expect(source).not.toContain('window.confirm');
     expect(source).not.toContain('confirm(');
     expect(source).not.toContain('WorkbenchPage');
     expect(source).not.toContain('SurfaceSection');
     expect(source).not.toContain('SelectableEvidenceList');
   });
+
+  it('keeps status setting remounts on a short settled cache window while mutations invalidate it', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/status/setting-status-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_STATUS_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['setting-status', '/status/page/org', '/status/page/component', statusIncidentListUrl, refreshTick].join(':')");
+    expect(source).toContain('void refreshTick');
+    expect(source).toContain('setRefreshTick(value => value + 1)');
+    expect(source).toContain("function handleModeChange(nextMode: 'component' | 'incident')");
+    expect(source).toContain('onModeChange={handleModeChange}');
+    expect(source).toContain('onIncidentPageIndexChange={nextPageIndex => setIncidentPageIndex(nextPageIndex)}');
+    expect(source).toContain('setIncidentError(error instanceof Error ? error.message : t(isEdit ? \'common.notify.edit-fail\' : \'common.notify.new-fail\'))');
+    expect(source).toContain('setEditingIncident(false);');
+    expect(source).toContain('cacheKey={settingStatusCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_STATUS_SETTLED_CACHE_TTL_MS}');
+  });
 });
diff --git a/web-next/app/setting/status/page.tsx b/web-next/app/setting/status/page.tsx
index ebefbad02a..f2fedf6bd8 100644
--- a/web-next/app/setting/status/page.tsx
+++ b/web-next/app/setting/status/page.tsx
@@ -1,277 +1,7 @@
-'use client';
-
 import React from 'react';
-import { useCallback, useState } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { StatusSettingSurface } from '@/components/pages/status-setting-surface';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
-import { formatTime } from '@/lib/format';
-import { createStatusPageComponent, createStatusPageIncident, deleteStatusPageComponent, deleteStatusPageIncident, loadStatusManagementData, saveStatusPageOrg, updateStatusPageComponent, updateStatusPageIncident } from '@/lib/setting-status/controller';
-import { buildStatusComponentDraft, buildStatusComponentPayload, buildStatusIncidentDraft, buildStatusIncidentPayload, buildStatusOrgDraft, buildStatusOrgPayload, type StatusComponentDraft, type StatusIncidentDraft, type StatusOrgDraft, validateStatusComponentDraft, validateStatusIncidentDraft, validateStatusOrgDraft } from '@/lib/setting-status/view-model';
-import type { PageResult, StatusPageComponent, StatusPageIncident, StatusPageOrg } from '@/lib/types';
-
-type StatusManagementData = {
-  org: StatusPageOrg;
-  components: StatusPageComponent[];
-  incidents: PageResult<StatusPageIncident>;
-};
-
-export default function SettingStatusPage() {
-  const { t } = useI18n();
-  const [mode, setMode] = useState<'component' | 'incident'>('component');
-  const [editingOrg, setEditingOrg] = useState(false);
-  const [orgDraft, setOrgDraft] = useState<StatusOrgDraft>(() => buildStatusOrgDraft(null));
-  const [savingOrg, setSavingOrg] = useState(false);
-  const [orgMessage, setOrgMessage] = useState<string | null>(null);
-  const [orgError, setOrgError] = useState<string | null>(null);
-  const [editingComponent, setEditingComponent] = useState(false);
-  const [componentDraft, setComponentDraft] = useState<StatusComponentDraft>(() => buildStatusComponentDraft(null));
-  const [savingComponent, setSavingComponent] = useState(false);
-  const [componentMessage, setComponentMessage] = useState<string | null>(null);
-  const [componentError, setComponentError] = useState<string | null>(null);
-  const [selectedComponentId, setSelectedComponentId] = useState<number | null>(null);
-  const [editingIncident, setEditingIncident] = useState(false);
-  const [incidentDraft, setIncidentDraft] = useState<StatusIncidentDraft>(() => buildStatusIncidentDraft(null));
-  const [savingIncident, setSavingIncident] = useState(false);
-  const [incidentMessage, setIncidentMessage] = useState<string | null>(null);
-  const [incidentError, setIncidentError] = useState<string | null>(null);
-  const [selectedIncidentId, setSelectedIncidentId] = useState<number | null>(null);
-  const [incidentSearchInput, setIncidentSearchInput] = useState('');
-  const [incidentSearch, setIncidentSearch] = useState('');
-  const [incidentPageIndex, setIncidentPageIndex] = useState(0);
-  const [incidentPageSize, setIncidentPageSize] = useState(8);
-  const [refreshTick, setRefreshTick] = useState(0);
-
-  const load = useCallback(async (): Promise<StatusManagementData> => {
-    void refreshTick;
-    return loadStatusManagementData(apiMessageGet, {
-      search: incidentSearch,
-      pageIndex: incidentPageIndex,
-      pageSize: incidentPageSize
-    });
-  }, [incidentPageIndex, incidentPageSize, incidentSearch, refreshTick]);
-
-  function commitIncidentSearch() {
-    const nextSearch = incidentSearchInput.trim();
-    setIncidentSearch(nextSearch);
-    setIncidentSearchInput(nextSearch);
-    setIncidentPageIndex(0);
-    setRefreshTick(value => value + 1);
-  }
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('setting.status.loading')}>
-      {data => {
-        const selectedComponent = data.components.find(component => component.id === selectedComponentId) ?? data.components[0] ?? null;
-        const selectedIncident = data.incidents.content.find(incident => incident.id === selectedIncidentId) ?? data.incidents.content[0] ?? null;
-        const resolvedIncidentPageIndex = Math.max(0, data.incidents.pageIndex ?? incidentPageIndex);
-        const resolvedIncidentPageSize = Math.max(1, data.incidents.pageSize ?? incidentPageSize);
-        const resolvedIncidentTotal = data.incidents.totalElements || 0;
-        const resolvedIncidentTotalPages = Math.max(1, Math.ceil(resolvedIncidentTotal / resolvedIncidentPageSize));
-        const displayedIncidentPageIndex = Math.min(resolvedIncidentPageIndex, resolvedIncidentTotalPages - 1);
-
-        async function handleEditOrg() {
-          setOrgDraft(buildStatusOrgDraft(data.org));
-          setOrgMessage(null);
-          setOrgError(null);
-          setEditingOrg(true);
-        }
-
-        async function handleSaveOrg() {
-          const validationError = validateStatusOrgDraft(orgDraft, t);
-          if (validationError) {
-            setOrgError(validationError);
-            return;
-          }
-          setSavingOrg(true);
-          setOrgMessage(null);
-          setOrgError(null);
-          try {
-            await saveStatusPageOrg(apiMessagePost as any, buildStatusOrgPayload(orgDraft));
-            setOrgMessage(t('common.save-success'));
-            setEditingOrg(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setOrgError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setSavingOrg(false);
-          }
-        }
-
-        async function handleNewComponent() {
-          setComponentDraft(buildStatusComponentDraft({ orgId: data.org.id } as StatusPageComponent));
-          setComponentMessage(null);
-          setComponentError(null);
-          setEditingComponent(true);
-        }
-
-        async function handleEditComponent(componentOverride?: StatusPageComponent) {
-          const component = componentOverride ?? selectedComponent;
-          if (!component) return;
-          setComponentDraft(buildStatusComponentDraft(component));
-          setComponentMessage(null);
-          setComponentError(null);
-          setEditingComponent(true);
-        }
-
-        async function handleSaveComponent() {
-          const validationError = validateStatusComponentDraft(componentDraft, t);
-          if (validationError) {
-            setComponentError(validationError);
-            return;
-          }
-          setSavingComponent(true);
-          setComponentMessage(null);
-          setComponentError(null);
-          try {
-            const payload = buildStatusComponentPayload({ ...componentDraft, orgId: data.org.id || componentDraft.orgId });
-            if (componentDraft.id) {
-              await updateStatusPageComponent(apiMessagePut as any, payload);
-            } else {
-              await createStatusPageComponent(apiMessagePost as any, payload);
-            }
-            setComponentMessage(t('common.save-success'));
-            setEditingComponent(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setComponentError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setSavingComponent(false);
-          }
-        }
-
-        async function handleDeleteComponent(componentOverride?: StatusPageComponent) {
-          const component = componentOverride ?? selectedComponent;
-          if (!component?.id) return;
-          try {
-            await deleteStatusPageComponent(apiMessageDelete as any, component.id);
-            setComponentMessage(t('common.delete-success'));
-            setComponentError(null);
-            setEditingComponent(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setComponentError(error instanceof Error ? error.message : t('common.delete-failed'));
-          }
-        }
-
-        async function handleNewIncident() {
-          setIncidentDraft(buildStatusIncidentDraft({ orgId: data.org.id } as StatusPageIncident));
-          setIncidentMessage(null);
-          setIncidentError(null);
-          setEditingIncident(true);
-        }
-
-        async function handleEditIncident(incidentOverride?: StatusPageIncident) {
-          const incident = incidentOverride ?? selectedIncident;
-          if (!incident) return;
-          setIncidentDraft(buildStatusIncidentDraft(incident));
-          setIncidentMessage(null);
-          setIncidentError(null);
-          setEditingIncident(true);
-        }
-
-        async function handleSaveIncident() {
-          const validationError = validateStatusIncidentDraft(incidentDraft, t);
-          if (validationError) {
-            setIncidentError(validationError);
-            return;
-          }
-          setSavingIncident(true);
-          setIncidentMessage(null);
-          setIncidentError(null);
-          try {
-            const payload = buildStatusIncidentPayload({ ...incidentDraft, orgId: data.org.id || incidentDraft.orgId }, data.components);
-            if (incidentDraft.id) {
-              await updateStatusPageIncident(apiMessagePut as any, payload);
-            } else {
-              await createStatusPageIncident(apiMessagePost as any, payload);
-            }
-            setIncidentMessage(t('common.save-success'));
-            setEditingIncident(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setIncidentError(error instanceof Error ? error.message : t('common.save-failed'));
-          } finally {
-            setSavingIncident(false);
-          }
-        }
 
-        async function handleDeleteIncident(incidentOverride?: StatusPageIncident) {
-          const incident = incidentOverride ?? selectedIncident;
-          if (!incident?.id) return;
-          try {
-            await deleteStatusPageIncident(apiMessageDelete as any, incident.id);
-            setIncidentMessage(t('common.delete-success'));
-            setIncidentError(null);
-            setEditingIncident(false);
-            setRefreshTick(value => value + 1);
-          } catch (error) {
-            setIncidentError(error instanceof Error ? error.message : t('common.delete-failed'));
-          }
-        }
+import SettingStatusPage from './setting-status-page';
 
-        return (
-          <StatusSettingSurface
-            t={t}
-            data={data}
-            mode={mode}
-            editingOrg={editingOrg}
-            orgDraft={orgDraft}
-            savingOrg={savingOrg}
-            orgMessage={orgMessage}
-            orgError={orgError}
-            editingComponent={editingComponent}
-            componentDraft={componentDraft}
-            savingComponent={savingComponent}
-            componentMessage={componentMessage}
-            componentError={componentError}
-            selectedComponentId={selectedComponentId}
-            editingIncident={editingIncident}
-            incidentDraft={incidentDraft}
-            savingIncident={savingIncident}
-            incidentMessage={incidentMessage}
-            incidentError={incidentError}
-            selectedIncidentId={selectedIncidentId}
-            incidentSearchInput={incidentSearchInput}
-            formatTime={formatTime}
-            publicStatusHref="/status"
-            onEditOrg={() => void handleEditOrg()}
-            onModeChange={setMode}
-            onNewComponent={() => void handleNewComponent()}
-            onEditComponent={component => void handleEditComponent(component)}
-            onDeleteComponent={component => void handleDeleteComponent(component)}
-            onNewIncident={() => void handleNewIncident()}
-            onEditIncident={incident => void handleEditIncident(incident)}
-            onDeleteIncident={incident => void handleDeleteIncident(incident)}
-            onOrgDraftChange={patch => setOrgDraft(prev => ({ ...prev, ...patch }))}
-            onSaveOrg={() => void handleSaveOrg()}
-            onCancelOrg={() => setEditingOrg(false)}
-            onComponentDraftChange={patch => setComponentDraft(prev => ({ ...prev, ...patch }))}
-            onSaveComponent={() => void handleSaveComponent()}
-            onCancelComponent={() => setEditingComponent(false)}
-            onSelectComponent={setSelectedComponentId}
-            onIncidentDraftChange={patch => setIncidentDraft(prev => ({ ...prev, ...patch }))}
-            onSaveIncident={() => void handleSaveIncident()}
-            onCancelIncident={() => setEditingIncident(false)}
-            onSelectIncident={setSelectedIncidentId}
-            onIncidentSearchInputChange={setIncidentSearchInput}
-            onCommitIncidentSearch={commitIncidentSearch}
-            onResetIncidentSearch={() => {
-              setIncidentSearchInput('');
-              setIncidentSearch('');
-              setIncidentPageIndex(0);
-              setRefreshTick(value => value + 1);
-            }}
-            onIncidentPageSizeChange={nextPageSize => {
-              setIncidentPageSize(nextPageSize);
-              setIncidentPageIndex(0);
-            }}
-            onIncidentPrevious={() => setIncidentPageIndex(Math.max(0, displayedIncidentPageIndex - 1))}
-            onIncidentNext={() => setIncidentPageIndex(Math.min(resolvedIncidentTotalPages - 1, displayedIncidentPageIndex + 1))}
-          />
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function SettingStatusRoutePage() {
+  return <SettingStatusPage />;
 }
diff --git a/web-next/app/setting/status/page.tsx b/web-next/app/setting/status/setting-status-page.tsx
similarity index 80%
copy from web-next/app/setting/status/page.tsx
copy to web-next/app/setting/status/setting-status-page.tsx
index ebefbad02a..a4ce0cc1c4 100644
--- a/web-next/app/setting/status/page.tsx
+++ b/web-next/app/setting/status/setting-status-page.tsx
@@ -1,13 +1,13 @@
 'use client';
 
 import React from 'react';
-import { useCallback, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
 import { ClientWorkbench } from '@/components/workbench/client-workbench';
 import { StatusSettingSurface } from '@/components/pages/status-setting-surface';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
 import { formatTime } from '@/lib/format';
-import { createStatusPageComponent, createStatusPageIncident, deleteStatusPageComponent, deleteStatusPageIncident, loadStatusManagementData, saveStatusPageOrg, updateStatusPageComponent, updateStatusPageIncident } from '@/lib/setting-status/controller';
+import { buildStatusIncidentListUrl, createStatusPageComponent, createStatusPageIncident, deleteStatusPageComponent, deleteStatusPageIncident, loadStatusManagementData, saveStatusPageOrg, updateStatusPageComponent, updateStatusPageIncident } from '@/lib/setting-status/controller';
 import { buildStatusComponentDraft, buildStatusComponentPayload, buildStatusIncidentDraft, buildStatusIncidentPayload, buildStatusOrgDraft, buildStatusOrgPayload, type StatusComponentDraft, type StatusIncidentDraft, type StatusOrgDraft, validateStatusComponentDraft, validateStatusIncidentDraft, validateStatusOrgDraft } from '@/lib/setting-status/view-model';
 import type { PageResult, StatusPageComponent, StatusPageIncident, StatusPageOrg } from '@/lib/types';
 
@@ -17,6 +17,8 @@ type StatusManagementData = {
   incidents: PageResult<StatusPageIncident>;
 };
 
+const SETTING_STATUS_SETTLED_CACHE_TTL_MS = 10_000;
+
 export default function SettingStatusPage() {
   const { t } = useI18n();
   const [mode, setMode] = useState<'component' | 'incident'>('component');
@@ -43,14 +45,21 @@ export default function SettingStatusPage() {
   const [incidentPageSize, setIncidentPageSize] = useState(8);
   const [refreshTick, setRefreshTick] = useState(0);
 
+  const statusIncidentQuery = useMemo(() => ({
+    search: incidentSearch,
+    pageIndex: incidentPageIndex,
+    pageSize: incidentPageSize
+  }), [incidentPageIndex, incidentPageSize, incidentSearch]);
+  const statusIncidentListUrl = useMemo(() => buildStatusIncidentListUrl(statusIncidentQuery), [statusIncidentQuery]);
+  const settingStatusCacheKey = useMemo(
+    () => ['setting-status', '/status/page/org', '/status/page/component', statusIncidentListUrl, refreshTick].join(':'),
+    [refreshTick, statusIncidentListUrl]
+  );
+
   const load = useCallback(async (): Promise<StatusManagementData> => {
     void refreshTick;
-    return loadStatusManagementData(apiMessageGet, {
-      search: incidentSearch,
-      pageIndex: incidentPageIndex,
-      pageSize: incidentPageSize
-    });
-  }, [incidentPageIndex, incidentPageSize, incidentSearch, refreshTick]);
+    return loadStatusManagementData(apiMessageGet, statusIncidentQuery);
+  }, [refreshTick, statusIncidentQuery]);
 
   function commitIncidentSearch() {
     const nextSearch = incidentSearchInput.trim();
@@ -60,8 +69,18 @@ export default function SettingStatusPage() {
     setRefreshTick(value => value + 1);
   }
 
+  function handleModeChange(nextMode: 'component' | 'incident') {
+    setMode(nextMode);
+    setRefreshTick(value => value + 1);
+  }
+
   return (
-    <ClientWorkbench load={load} loadingCopy={t('setting.status.loading')}>
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.status.loading')}
+      cacheKey={settingStatusCacheKey}
+      cacheSettledTtlMs={SETTING_STATUS_SETTLED_CACHE_TTL_MS}
+    >
       {data => {
         const selectedComponent = data.components.find(component => component.id === selectedComponentId) ?? data.components[0] ?? null;
         const selectedIncident = data.incidents.content.find(incident => incident.id === selectedIncidentId) ?? data.incidents.content[0] ?? null;
@@ -89,11 +108,11 @@ export default function SettingStatusPage() {
           setOrgError(null);
           try {
             await saveStatusPageOrg(apiMessagePost as any, buildStatusOrgPayload(orgDraft));
-            setOrgMessage(t('common.save-success'));
+            setOrgMessage(t('common.notify.apply-success'));
             setEditingOrg(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setOrgError(error instanceof Error ? error.message : t('common.save-failed'));
+            setOrgError(error instanceof Error ? error.message : t('common.notify.apply-fail'));
           } finally {
             setSavingOrg(false);
           }
@@ -121,21 +140,27 @@ export default function SettingStatusPage() {
             setComponentError(validationError);
             return;
           }
+          const componentOrgId = data.org.id || componentDraft.orgId;
+          if (!componentOrgId) {
+            setComponentError(t('status.component.notify.need-org'));
+            return;
+          }
+          const isEdit = Boolean(componentDraft.id);
           setSavingComponent(true);
           setComponentMessage(null);
           setComponentError(null);
           try {
-            const payload = buildStatusComponentPayload({ ...componentDraft, orgId: data.org.id || componentDraft.orgId });
-            if (componentDraft.id) {
+            const payload = buildStatusComponentPayload({ ...componentDraft, orgId: componentOrgId });
+            if (isEdit) {
               await updateStatusPageComponent(apiMessagePut as any, payload);
             } else {
               await createStatusPageComponent(apiMessagePost as any, payload);
             }
-            setComponentMessage(t('common.save-success'));
+            setComponentMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditingComponent(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setComponentError(error instanceof Error ? error.message : t('common.save-failed'));
+            setComponentError(error instanceof Error ? error.message : t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
           } finally {
             setSavingComponent(false);
           }
@@ -146,12 +171,12 @@ export default function SettingStatusPage() {
           if (!component?.id) return;
           try {
             await deleteStatusPageComponent(apiMessageDelete as any, component.id);
-            setComponentMessage(t('common.delete-success'));
+            setComponentMessage(t('common.notify.delete-success'));
             setComponentError(null);
             setEditingComponent(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setComponentError(error instanceof Error ? error.message : t('common.delete-failed'));
+            setComponentError(error instanceof Error ? error.message : t('common.notify.delete-fail'));
           }
         }
 
@@ -172,26 +197,33 @@ export default function SettingStatusPage() {
         }
 
         async function handleSaveIncident() {
+          const incidentOrgId = data.org.id || incidentDraft.orgId;
+          if (!incidentOrgId) {
+            setIncidentError(t('status.component.notify.need-org'));
+            return;
+          }
           const validationError = validateStatusIncidentDraft(incidentDraft, t);
           if (validationError) {
             setIncidentError(validationError);
             return;
           }
+          const isEdit = Boolean(incidentDraft.id);
           setSavingIncident(true);
           setIncidentMessage(null);
           setIncidentError(null);
           try {
-            const payload = buildStatusIncidentPayload({ ...incidentDraft, orgId: data.org.id || incidentDraft.orgId }, data.components);
-            if (incidentDraft.id) {
+            const payload = buildStatusIncidentPayload({ ...incidentDraft, orgId: incidentOrgId }, data.components);
+            if (isEdit) {
               await updateStatusPageIncident(apiMessagePut as any, payload);
             } else {
               await createStatusPageIncident(apiMessagePost as any, payload);
             }
-            setIncidentMessage(t('common.save-success'));
+            setIncidentMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
             setEditingIncident(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setIncidentError(error instanceof Error ? error.message : t('common.save-failed'));
+            setIncidentError(error instanceof Error ? error.message : t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
+            setEditingIncident(false);
           } finally {
             setSavingIncident(false);
           }
@@ -202,12 +234,12 @@ export default function SettingStatusPage() {
           if (!incident?.id) return;
           try {
             await deleteStatusPageIncident(apiMessageDelete as any, incident.id);
-            setIncidentMessage(t('common.delete-success'));
+            setIncidentMessage(t('common.notify.delete-success'));
             setIncidentError(null);
             setEditingIncident(false);
             setRefreshTick(value => value + 1);
           } catch (error) {
-            setIncidentError(error instanceof Error ? error.message : t('common.delete-failed'));
+            setIncidentError(error instanceof Error ? error.message : t('common.notify.delete-fail'));
           }
         }
 
@@ -237,7 +269,7 @@ export default function SettingStatusPage() {
             formatTime={formatTime}
             publicStatusHref="/status"
             onEditOrg={() => void handleEditOrg()}
-            onModeChange={setMode}
+            onModeChange={handleModeChange}
             onNewComponent={() => void handleNewComponent()}
             onEditComponent={component => void handleEditComponent(component)}
             onDeleteComponent={component => void handleDeleteComponent(component)}
@@ -263,6 +295,7 @@ export default function SettingStatusPage() {
               setIncidentPageIndex(0);
               setRefreshTick(value => value + 1);
             }}
+            onIncidentPageIndexChange={nextPageIndex => setIncidentPageIndex(nextPageIndex)}
             onIncidentPageSizeChange={nextPageSize => {
               setIncidentPageSize(nextPageSize);
               setIncidentPageIndex(0);
diff --git a/web-next/components/pages/settings-surface-page.test.tsx b/web-next/components/pages/settings-surface-page.test.tsx
index 84412fac72..fa612a9c91 100644
--- a/web-next/components/pages/settings-surface-page.test.tsx
+++ b/web-next/components/pages/settings-surface-page.test.tsx
@@ -1,18 +1,9 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 
-const t = (key: string) => {
-  const messages: Record<string, string> = {
-    'menu.settings': '平台设置',
-    'settings.surface.title': '设置总览',
-    'settings.surface.copy': '把平台配置入口收拢到一个工作台。',
-    'settings.surface.governance.title': '平台治理',
-    'settings.surface.governance.copy': '只展示当前已有入口;未来大域保留在 roadmap 文档。'
-  };
-
-  return messages[key] ?? key;
-};
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
@@ -60,7 +51,12 @@ describe('SettingsSurfacePage', () => {
     );
 
     expect(html).toContain('平台治理');
+    expect(html).toContain('设置总览');
+    expect(html).toContain('把平台配置入口收拢到一个工作台。');
+    expect(html).toContain('路由契约');
+    expect(html).toContain('API 契约');
     expect(html).toContain('只展示当前已有入口');
+    expect(html).toContain('未来大域保留在路线图文档');
     expect(html).toContain('API 访问');
     expect(html).toContain('/setting/settings/token');
     expect(html).toContain('通知通道');
@@ -68,6 +64,9 @@ describe('SettingsSurfacePage', () => {
     expect(html).toContain('模板与插件');
     expect(html).toContain('/setting/define · /setting/plugins');
     expect(html).toContain('未来大域边界');
+    expect(html).toContain('仅规划');
+    expect(html).toContain('安全治理');
+    expect(html).toContain('路线图能力规划');
     expect(html).toContain('/docs/roadmap/future-security');
     expect(html).not.toContain('href="/security"');
   });
diff --git a/web-next/components/pages/settings-surface-page.tsx b/web-next/components/pages/settings-surface-page.tsx
index 0ed835fcc5..366b8fc17e 100644
--- a/web-next/components/pages/settings-surface-page.tsx
+++ b/web-next/components/pages/settings-surface-page.tsx
@@ -42,7 +42,7 @@ export function SettingsSurfacePage({
             copy={t('settings.surface.governance.copy')}
             variant="flat"
           >
-            <RowList rows={buildPlatformGovernanceRows()} />
+            <RowList rows={buildPlatformGovernanceRows(t)} />
           </SurfaceSection>
         </div>
       }
diff --git a/web-next/components/pages/status-setting-surface.test.tsx b/web-next/components/pages/status-setting-surface.test.tsx
index 4ff492454c..0715b79456 100644
--- a/web-next/components/pages/status-setting-surface.test.tsx
+++ b/web-next/components/pages/status-setting-surface.test.tsx
@@ -4,10 +4,10 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
+import { StatusSettingSurface } from './status-setting-surface';
 
 describe('status setting surface', () => {
-  it('renders the cold-matte status page org form, tabs, toolbar, and component table', async () => {
-    const { StatusSettingSurface } = await import('./status-setting-surface');
+  it('renders the cold-matte status page org form, tabs, toolbar, and component table', () => {
     const t = createTranslatorMock({
       locale: 'zh-CN'
     });
@@ -68,6 +68,7 @@ describe('status setting surface', () => {
         onIncidentSearchInputChange={() => {}}
         onCommitIncidentSearch={() => {}}
         onResetIncidentSearch={() => {}}
+        onIncidentPageIndexChange={() => {}}
         onIncidentPageSizeChange={() => {}}
         onIncidentPrevious={() => {}}
         onIncidentNext={() => {}}
@@ -76,19 +77,46 @@ describe('status setting surface', () => {
 
     expect(html).toContain('data-status-setting-surface="otlp-cold-status-console"');
     expect(html).toContain('data-status-setting-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-status-setting-mutation-feedback="angular-notify-keys"');
+    expect(html).toContain('data-status-setting-mutation-feedback-owner="route-action-feedback-contract"');
     expect(html).toContain('data-status-header="cold-compact-header"');
     expect(html).toContain('data-status-command-row="standard-equal-buttons"');
+    expect(html).not.toContain('data-status-header-public-link');
     expect(html).toContain('data-status-admin-layout="full-width-admin-list"');
     expect(html).toContain('data-status-org-form="cold-settings-form"');
     expect(html).toContain('data-status-tabs="cold-segmented-tabs"');
+    expect(html).toContain('data-status-tab-refresh-contract="angular-nz-tab-click-load"');
+    expect(html).toContain('data-status-tab-refresh-owner="route-refresh-contract"');
+    expect(html).toContain('data-status-tab-refresh-mode="component"');
+    expect(html).toContain('data-status-tab-refresh-mode="incident"');
     expect(html).toContain('data-status-component-toolbar="cold-table-toolbar"');
+    expect(html).toContain('data-status-component-refresh-contract="angular-sync-component"');
+    expect(html).toContain('data-status-component-refresh-owner="route-refresh-contract"');
+    expect(html).toContain('data-status-public-link-contract="angular-tab-toolbar-conditional"');
+    expect(html).toContain('data-status-public-link-owner="status-tab-toolbar"');
+    expect(html).toContain('data-status-public-link-mode="component"');
+    expect(html).toContain('data-status-public-link-mode="incident"');
     expect(html).toContain('data-status-incident-search-owner="shared-search-row"');
+    expect(html).toContain('data-status-incident-refresh-contract="angular-sync-incidence"');
+    expect(html).toContain('data-status-incident-refresh-owner="route-refresh-contract"');
+    expect(html).toContain('data-status-incident-pagination="cold-dense-pagination"');
+    expect(html).toContain('data-status-incident-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-status-incident-pagination-contract="angular-search-pagination"');
+    expect(html).toContain('data-hz-ui="pagination-bar"');
+    expect(html).toContain('data-hz-pagination-page-size="select-menu"');
+    expect(html).toContain('data-hz-pagination-page-jump="number-input"');
+    expect(html).toContain('data-status-incident-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-status-incident-pagination-page-jump-owner="hertzbeat-ui-input"');
     expect(html).toContain('data-cold-search-input="fixed-width-direct"');
     expect(html).toContain('data-cold-search-chrome="no-extra-input-shell"');
     expect(html).not.toContain('data-cold-search-input-shell');
     expect(html).toContain('data-status-component-table-shell="cold-dense-table"');
     expect(html).toContain('data-status-component-table="cold-component-table"');
     expect(html).toContain('data-status-component-row-actions="cold-icon-actions"');
+    expect(html).toContain('data-status-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-status-component-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-status-incident-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-status-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
     expect(html).not.toContain('data-status-summary-rail=');
     expect(html).toContain('状态页面');
     expect(html).toContain('组织名称');
@@ -101,6 +129,7 @@ describe('status setting surface', () => {
     expect(html).toContain('维护事件');
     expect(html).toContain('HB Status');
     expect(html).toContain('/status');
+    expect(html).toContain('公开状态页的组件说明');
     expect(html).toContain('新增组件');
     expect(html).toContain('组件状态');
     expect(html).toContain('状态统计方式');
@@ -112,7 +141,261 @@ describe('status setting surface', () => {
     expect(html).toContain('手动设置');
     expect(html).toContain('source:codex-smoke');
     expect(html).toContain('确定');
-  });
+  }, 15000);
+
+  it('hides the public status link until Angular statusOrg id and name are both available', () => {
+    const t = createTranslatorMock({
+      locale: 'zh-CN'
+    });
+
+    const html = renderToStaticMarkup(
+      <StatusSettingSurface
+        t={t}
+        data={{
+          org: { name: 'HB Status', description: 'Service health', state: 0 },
+          components: [],
+          incidents: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        mode="incident"
+        editingOrg={false}
+        orgDraft={{ name: 'HB Status', state: '0', description: 'Service health', home: '', feedback: '', logo: '', color: '' }}
+        savingOrg={false}
+        orgMessage={null}
+        orgError={null}
+        editingComponent={false}
+        componentDraft={{ name: '', description: '', labelsText: '', method: '1', configState: '0', state: '0' }}
+        savingComponent={false}
+        componentMessage={null}
+        componentError={null}
+        selectedComponentId={null}
+        editingIncident={false}
+        incidentDraft={{ name: '', state: '0', componentIdsText: '', message: '' }}
+        savingIncident={false}
+        incidentMessage={null}
+        incidentError={null}
+        selectedIncidentId={null}
+        incidentSearchInput=""
+        formatTime={() => '2026-04-10 18:00:00'}
+        publicStatusHref="/status"
+        onEditOrg={() => {}}
+        onModeChange={() => {}}
+        onNewComponent={() => {}}
+        onEditComponent={() => {}}
+        onDeleteComponent={() => {}}
+        onNewIncident={() => {}}
+        onEditIncident={() => {}}
+        onDeleteIncident={() => {}}
+        onOrgDraftChange={() => {}}
+        onSaveOrg={() => {}}
+        onCancelOrg={() => {}}
+        onComponentDraftChange={() => {}}
+        onSaveComponent={() => {}}
+        onCancelComponent={() => {}}
+        onSelectComponent={() => {}}
+        onIncidentDraftChange={() => {}}
+        onSaveIncident={() => {}}
+        onCancelIncident={() => {}}
+        onSelectIncident={() => {}}
+        onIncidentSearchInputChange={() => {}}
+        onCommitIncidentSearch={() => {}}
+        onResetIncidentSearch={() => {}}
+        onIncidentPageIndexChange={() => {}}
+        onIncidentPageSizeChange={() => {}}
+        onIncidentPrevious={() => {}}
+        onIncidentNext={() => {}}
+      />
+    );
+
+    expect(html).not.toContain('data-status-public-link-contract="angular-tab-toolbar-conditional"');
+    expect(html).not.toContain('href="/status"');
+  }, 15000);
+
+  it('renders localized table empty-cell fallbacks for missing status setting row facts', () => {
+    const t = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'common.none': '无状态值'
+      }
+    });
+
+    const html = renderToStaticMarkup(
+      <StatusSettingSurface
+        t={t}
+        data={{
+          org: { id: 1, name: 'HB Status', description: 'Service health', state: 0, home: 'https://hb.dev', feedback: 'mailto:ops@hb.dev' },
+          components: [{ id: 1, name: '', description: '', state: 0, method: 1, labels: {}, gmtUpdate: 1712730000000 }],
+          incidents: {
+            content: [{ id: 2, name: '', state: 1, components: [], contents: [], pageIndex: 0 }],
+            totalElements: 1,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        mode="component"
+        editingOrg={false}
+        orgDraft={{ name: 'HB Status', state: '0', description: 'Service health', home: 'https://hb.dev', feedback: 'mailto:ops@hb.dev', logo: '', color: '' }}
+        savingOrg={false}
+        orgMessage={null}
+        orgError={null}
+        editingComponent={false}
+        componentDraft={{ name: '', description: '', labelsText: '', method: '1', configState: '0', state: '0' }}
+        savingComponent={false}
+        componentMessage={null}
+        componentError={null}
+        selectedComponentId={1}
+        editingIncident={false}
+        incidentDraft={{ name: '', state: '0', componentIdsText: '', message: '' }}
+        savingIncident={false}
+        incidentMessage={null}
+        incidentError={null}
+        selectedIncidentId={2}
+        incidentSearchInput=""
+        formatTime={() => '2026-04-10 18:00:00'}
+        publicStatusHref="/status"
+        onEditOrg={() => {}}
+        onModeChange={() => {}}
+        onNewComponent={() => {}}
+        onEditComponent={() => {}}
+        onDeleteComponent={() => {}}
+        onNewIncident={() => {}}
+        onEditIncident={() => {}}
+        onDeleteIncident={() => {}}
+        onOrgDraftChange={() => {}}
+        onSaveOrg={() => {}}
+        onCancelOrg={() => {}}
+        onComponentDraftChange={() => {}}
+        onSaveComponent={() => {}}
+        onCancelComponent={() => {}}
+        onSelectComponent={() => {}}
+        onIncidentDraftChange={() => {}}
+        onSaveIncident={() => {}}
+        onCancelIncident={() => {}}
+        onSelectIncident={() => {}}
+        onIncidentSearchInputChange={() => {}}
+        onCommitIncidentSearch={() => {}}
+        onResetIncidentSearch={() => {}}
+        onIncidentPageIndexChange={() => {}}
+        onIncidentPageSizeChange={() => {}}
+        onIncidentPrevious={() => {}}
+        onIncidentNext={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-status-component-row="1"');
+    expect(html).toContain('data-status-incident-row="2"');
+    expect((html.match(/无状态值/g) ?? []).length).toBeGreaterThanOrEqual(5);
+  }, 15000);
+
+  it('renders Angular-style incident edit history in descending order with shared ownership', () => {
+    const t = createTranslatorMock({
+      locale: 'zh-CN'
+    });
+
+    const html = renderToStaticMarkup(
+      <StatusSettingSurface
+        t={t}
+        data={{
+          org: { id: 1, name: 'HB Status', description: 'Service health', state: 0 },
+          components: [{ id: 1, name: 'API', description: 'public api', state: 0 }],
+          incidents: {
+            content: [{ id: 2, name: 'Incident A', state: 2, components: [{ id: 1 }], contents: [], pageIndex: 0 }],
+            totalElements: 1,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        mode="incident"
+        editingOrg={false}
+        orgDraft={{ name: 'HB Status', state: '0', description: 'Service health', home: '', feedback: '', logo: '', color: '' }}
+        savingOrg={false}
+        orgMessage={null}
+        orgError={null}
+        editingComponent={false}
+        componentDraft={{ name: '', description: '', labelsText: '', method: '1', configState: '0', state: '0' }}
+        savingComponent={false}
+        componentMessage={null}
+        componentError={null}
+        selectedComponentId={1}
+        editingIncident
+        incidentDraft={{
+          id: 2,
+          name: 'Incident A',
+          state: '2',
+          componentIdsText: '1',
+          message: 'Monitoring mitigation',
+          existingContents: [
+            { message: 'Monitoring mitigation after checkout rollback', state: 2, timestamp: 30 },
+            { message: 'Investigating payment latency', state: 1, timestamp: 10 }
+          ]
+        }}
+        savingIncident={false}
+        incidentMessage={null}
+        incidentError={null}
+        selectedIncidentId={2}
+        incidentSearchInput=""
+        formatTime={value => (value === 30 ? '2026-05-26 03:30:00' : '2026-05-26 03:12:00')}
+        publicStatusHref="/status"
+        onEditOrg={() => {}}
+        onModeChange={() => {}}
+        onNewComponent={() => {}}
+        onEditComponent={() => {}}
+        onDeleteComponent={() => {}}
+        onNewIncident={() => {}}
+        onEditIncident={() => {}}
+        onDeleteIncident={() => {}}
+        onOrgDraftChange={() => {}}
+        onSaveOrg={() => {}}
+        onCancelOrg={() => {}}
+        onComponentDraftChange={() => {}}
+        onSaveComponent={() => {}}
+        onCancelComponent={() => {}}
+        onSelectComponent={() => {}}
+        onIncidentDraftChange={() => {}}
+        onSaveIncident={() => {}}
+        onCancelIncident={() => {}}
+        onSelectIncident={() => {}}
+        onIncidentSearchInputChange={() => {}}
+        onCommitIncidentSearch={() => {}}
+        onResetIncidentSearch={() => {}}
+        onIncidentPageIndexChange={() => {}}
+        onIncidentPageSizeChange={() => {}}
+        onIncidentPrevious={() => {}}
+        onIncidentNext={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-status-incident-history-contract="angular-collapse-desc"');
+    expect(html).toContain('data-status-incident-history-owner="hertzbeat-ui-status-incident-history"');
+    expect(html).toContain('data-status-incident-failure-close-contract="angular-cancel-on-api-failure"');
+    expect(html).toContain('data-status-incident-failure-close-owner="status-route-action-feedback"');
+    expect(html).toContain('data-hz-ui="status-incident-history"');
+    expect(html).toContain('data-hz-status-incident-history-row="30"');
+    expect(html).toContain('data-hz-status-incident-history-row="10"');
+    expect(html.indexOf('Monitoring mitigation after checkout rollback')).toBeLessThan(html.indexOf('Investigating payment latency'));
+    expect(html).toContain('data-status-incident-message-input="angular-textarea"');
+    expect(html).toContain('data-status-incident-message-input-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-status-incident-message-placeholder-contract="angular-state-placeholder"');
+    expect(html).toContain('data-status-incident-message-placeholder="status.incident.message.tip.2"');
+    expect(html).toContain('data-hz-ui="textarea"');
+    expect(html).toContain('我们正在监控该事件,观察处理效果');
+    expect(html).toContain('data-status-incident-component-picker="angular-checkbox-group"');
+    expect(html).toContain('data-status-incident-component-picker-owner="hertzbeat-ui-checkbox"');
+    expect(html).toContain('data-status-incident-component-option="1"');
+    expect(html).toContain('data-hz-ui="checkbox"');
+    expect(html).toContain('data-status-incident-state-control="angular-radio-buttons"');
+    expect(html).toContain('data-status-incident-state-owner="hertzbeat-ui-radio-button-group"');
+    expect(html).toContain('data-hz-ui="radio-button-group"');
+    expect(html).toContain('data-hz-radio-button-option="2"');
+    expect(html).toContain('data-hz-radio-button-checked="true"');
+    expect(html).toContain('事件历史');
+    expect(html).toContain('观察中');
+    expect(html).toContain('已确认');
+  }, 15000);
 
   it('uses the shared cold visual owner instead of Workbench or alert primitives', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/status-setting-surface.tsx'), 'utf8');
@@ -123,18 +406,80 @@ describe('status setting surface', () => {
     expect(source).toContain('inputWidthClassName="w-[320px]"');
     expect(source).toContain('data-status-setting-surface="otlp-cold-status-console"');
     expect(source).toContain('data-status-setting-style-baseline={coldStatusVisual.canvasName}');
+    expect(source).toContain('data-status-setting-mutation-feedback="angular-notify-keys"');
+    expect(source).toContain('data-status-setting-mutation-feedback-owner="route-action-feedback-contract"');
     expect(source).toContain('data-status-admin-layout="full-width-admin-list"');
     expect(source).toContain('data-status-org-form="cold-settings-form"');
+    expect(source).toContain('data-status-tab-refresh-contract="angular-nz-tab-click-load"');
+    expect(source).toContain('data-status-tab-refresh-owner="route-refresh-contract"');
+    expect(source).toContain('data-status-tab-refresh-mode={mode}');
+    expect(source).toContain('data-status-component-refresh-contract="angular-sync-component"');
+    expect(source).toContain('data-status-component-refresh-owner="route-refresh-contract"');
+    expect(source).toContain('data-status-incident-refresh-contract="angular-sync-incidence"');
+    expect(source).toContain('data-status-incident-refresh-owner="route-refresh-contract"');
+    expect(source).toContain("onClick={() => onModeChange('incident')}");
+    expect(source).toContain('const hasPublicStatusLink = statusOrg.id != null && statusOrg.name != null');
+    expect(source).toContain('data-status-public-link-contract="angular-tab-toolbar-conditional"');
+    expect(source).toContain('data-status-public-link-owner="status-tab-toolbar"');
+    expect(source).toContain('data-status-public-link-mode="component"');
+    expect(source).toContain('data-status-public-link-mode="incident"');
+    expect(source).toContain('data-status-incident-pagination="cold-dense-pagination"');
+    expect(source).toContain('data-status-incident-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(source).toContain('data-status-incident-pagination-contract="angular-search-pagination"');
+    expect(source).toContain('pageSizeOptions={[8, 15, 25].map(value => ({ value: String(value), label: String(value) }))}');
+    expect(source).toContain('data-status-incident-pagination-page-size-owner');
+    expect(source).toContain('data-status-incident-pagination-page-jump-owner');
     expect(source).toContain('data-status-component-table-shell="cold-dense-table"');
     expect(source).toContain('data-status-component-table="cold-component-table"');
-    expect(source).toContain('data-status-component-delete-confirm-trigger="cold-modal"');
-    expect(source).toContain('data-status-incident-delete-confirm-trigger="cold-modal"');
-    expect(source).toContain('data-status-delete-confirm="cold-modal"');
+    expect(source).toContain("import { HzCheckbox, HzConfirmDialog, HzPaginationBar, HzRadioButtonGroup, HzStatusIncidentHistory, HzTableRowActionButton, HzTextarea } from '@hertzbeat/ui'");
+    expect(source).toContain('MoreHorizontal');
+    expect(source).toContain('const [openRowActionMenuId, setOpenRowActionMenuId] = React.useState<string | null>(null)');
+    expect(source).toContain('data-status-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(source).toContain('data-status-component-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(source).toContain('data-status-incident-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(source).toContain('data-status-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('HzTableRowActionButton');
+    expect(source).toContain('data-status-incident-history-contract="angular-collapse-desc"');
+    expect(source).toContain('data-status-incident-history-owner="hertzbeat-ui-status-incident-history"');
+    expect(source).toContain('data-status-incident-failure-close-contract');
+    expect(source).toContain('angular-cancel-on-api-failure');
+    expect(source).toContain('data-status-incident-failure-close-owner');
+    expect(source).toContain('data-status-incident-state-control="angular-radio-buttons"');
+    expect(source).toContain('data-status-incident-state-owner="hertzbeat-ui-radio-button-group"');
+    expect(source).toContain('data-status-incident-component-picker="angular-checkbox-group"');
+    expect(source).toContain('data-status-incident-component-picker-owner="hertzbeat-ui-checkbox"');
+    expect(source).toContain('data-status-incident-component-option={componentId}');
+    expect(source).toContain('data-status-incident-message-input="angular-textarea"');
+    expect(source).toContain('data-status-incident-message-input-owner="hertzbeat-ui-textarea"');
+    expect(source).toContain('data-status-incident-message-placeholder-contract="angular-state-placeholder"');
+    expect(source).toContain('data-status-incident-message-placeholder={incidentMessagePlaceholderKey(incidentDraft.state)}');
+    expect(source).toContain('data-status-component-delete-confirm-trigger="angular-modal-confirm"');
+    expect(source).toContain('data-status-component-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-status-incident-delete-confirm-trigger="angular-modal-confirm"');
+    expect(source).toContain('data-status-incident-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-status-delete-confirm="angular-modal-confirm"');
+    expect(source).toContain('data-status-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-status-delete-confirm-state={deleteDialog ? \'open\' : \'closed\'}');
+    expect(source).toContain('data-status-delete-confirm-dialog="angular-modal-confirm"');
+    expect(source).toContain('data-status-delete-confirm-ok');
+    expect(source).toContain('data-status-delete-confirm-cancel');
     expect(source).toContain('data-status-delete-confirm-kind');
-    expect(source).toContain('确认删除组件');
-    expect(source).toContain('确认删除维护事件');
-    expect(source).toContain('确认删除');
-    expect(source).toContain('取消');
+    expect(source).toContain("t('setting.status.subtitle')");
+    expect(source).toContain("t('setting.status.org.title')");
+    expect(source).toContain("t('setting.status.org.copy')");
+    expect(source).toContain("t('setting.status.delete.component.title')");
+    expect(source).toContain("t('setting.status.delete.incident.title')");
+    expect(source).toContain("t('setting.status.delete.confirm')");
+    expect(source).toContain("t('common.no-data')");
+    expect(source).toContain("t('common.button.cancel')");
+    expect(source).not.toContain('确认删除组件');
+    expect(source).not.toContain('确认删除维护事件');
+    expect(source).not.toContain('确认删除');
+    expect(source).not.toContain('取消');
+    expect(source).not.toContain('暂无数据');
+    expect(source).not.toContain('管理公开状态页的组织档案');
+    expect(source).not.toContain('组织档案');
+    expect(source).not.toContain('公开状态页展示的组织信息和反馈入口。');
     expect(source).not.toContain('data-status-summary-rail');
     expect(source).not.toContain('coldStatusVisual.layout.heroGrid');
     expect(source).not.toContain('coldStatusVisual.layout.railGrid');
diff --git a/web-next/components/pages/status-setting-surface.tsx b/web-next/components/pages/status-setting-surface.tsx
index cc0db5e974..a8f9759e08 100644
--- a/web-next/components/pages/status-setting-surface.tsx
+++ b/web-next/components/pages/status-setting-surface.tsx
@@ -1,7 +1,8 @@
 'use client';
 
 import React from 'react';
-import { ExternalLink, Inbox, Pencil, Plus, RefreshCw, RotateCw, Trash2 } from 'lucide-react';
+import { ArrowRight, ExternalLink, Inbox, MoreHorizontal, Pencil, Plus, Power, RefreshCw, RotateCw, Trash2 } from 'lucide-react';
+import { HzCheckbox, HzConfirmDialog, HzPaginationBar, HzRadioButtonGroup, HzStatusIncidentHistory, HzTableRowActionButton, HzTextarea } from '@hertzbeat/ui';
 import { OverlayDialog } from '../workbench/overlay-dialog';
 import { Button } from '../ui/button';
 import { Input } from '../ui/input';
@@ -9,10 +10,12 @@ import { SearchRow } from '../ui/search-row';
 import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
 import { latestIncidentMessage } from '../../lib/status-center/display';
 import type { PageResult, StatusPageComponent, StatusPageIncident, StatusPageOrg } from '../../lib/types';
-import type {
-  StatusComponentDraft,
-  StatusIncidentDraft,
-  StatusOrgDraft
+import {
+  settingStatusComponentTagTone,
+  settingStatusIncidentTagTone,
+  type StatusComponentDraft,
+  type StatusIncidentDraft,
+  type StatusOrgDraft
 } from '../../lib/setting-status/view-model';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
@@ -69,6 +72,7 @@ type StatusSettingSurfaceProps = {
   onIncidentSearchInputChange: (value: string) => void;
   onCommitIncidentSearch: () => void;
   onResetIncidentSearch: () => void;
+  onIncidentPageIndexChange: (pageIndex: number) => void;
   onIncidentPageSizeChange: (pageSize: number) => void;
   onIncidentPrevious: () => void;
   onIncidentNext: () => void;
@@ -104,7 +108,11 @@ const coldInputClassName =
 const coldTableToolbarClassName =
   'mb-3 flex min-h-10 w-full min-w-0 flex-wrap items-center gap-2 rounded-[3px] border border-[#282d36] bg-[#101217] px-3 py-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]';
 
-function EmptyTableRow({ colSpan }: { colSpan: number }) {
+const statusActionMenuRootClassName = 'relative z-40 inline-block overflow-visible';
+const statusActionMenuPanelBaseClassName =
+  'mt-2 z-40 min-w-[132px] rounded-[3px] border border-[#2b3039] bg-[#101217] p-1 shadow-[0_18px_42px_rgba(0,0,0,0.42)]';
+
+function EmptyTableRow({ colSpan, t }: { colSpan: number; t: Translator }) {
   return (
     <tr data-status-empty-row="cold-table-empty" className="bg-[#0b0c0e]">
       <td colSpan={colSpan} className="h-[240px] px-3 py-10 text-center text-[#858d9a]">
@@ -112,7 +120,7 @@ function EmptyTableRow({ colSpan }: { colSpan: number }) {
           <span className="inline-flex h-10 w-10 items-center justify-center rounded-[4px] border border-[#303743] bg-[#101217] text-[#cbd5e1]">
             <Inbox className="h-5 w-5" aria-hidden="true" />
           </span>
-          <div className="text-[13px] font-semibold text-[#dbe4f0]">暂无数据</div>
+          <div className="text-[13px] font-semibold text-[#dbe4f0]">{t('common.no-data')}</div>
         </div>
       </td>
     </tr>
@@ -150,6 +158,122 @@ function DialogField({ label, value, placeholder, onChange }: FieldProps) {
   );
 }
 
+type DialogTextareaFieldProps = FieldProps & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'placeholder' | 'onChange'>;
+
+function DialogTextareaField({
+  label,
+  value,
+  placeholder,
+  onChange,
+  ...props
+}: DialogTextareaFieldProps) {
+  return (
+    <label className="grid gap-1.5 text-[12px] font-semibold text-[#a9b0bb]">
+      <span>{label}</span>
+      <HzTextarea
+        height="default"
+        value={value}
+        placeholder={placeholder}
+        onChange={event => onChange(event.target.value)}
+        {...props}
+      />
+    </label>
+  );
+}
+
+function parseComponentIdSelection(text: string) {
+  return new Set(
+    text
+      .split(',')
+      .map(item => Number.parseInt(item.trim(), 10))
+      .filter(value => Number.isFinite(value))
+  );
+}
+
+function serializeComponentIdSelection(ids: Set<number>) {
+  return Array.from(ids).sort((a, b) => a - b).join(', ');
+}
+
+function DialogComponentCheckboxGroup({
+  label,
+  components,
+  value,
+  emptyLabel,
+  onChange
+}: {
+  label: string;
+  components: StatusPageComponent[];
+  value: string;
+  emptyLabel: string;
+  onChange: (value: string) => void;
+}) {
+  const selectedIds = parseComponentIdSelection(value);
+  return (
+    <div className="grid gap-1.5 text-[12px] font-semibold text-[#a9b0bb]">
+      <span>{label}</span>
+      <div
+        className="flex min-h-8 flex-wrap items-center gap-x-4 gap-y-2 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 py-1.5"
+        data-status-incident-component-picker="angular-checkbox-group"
+        data-status-incident-component-picker-owner="hertzbeat-ui-checkbox"
+      >
+        {components.length > 0 ? components.map(component => {
+          const componentId = component.id;
+          if (componentId == null) return null;
+          const checked = selectedIds.has(componentId);
+          return (
+            <HzCheckbox
+              key={componentId}
+              label={component.name || String(componentId)}
+              checked={checked}
+              data-status-incident-component-option={componentId}
+              onChange={event => {
+                const nextIds = new Set(selectedIds);
+                if (event.currentTarget.checked) {
+                  nextIds.add(componentId);
+                } else {
+                  nextIds.delete(componentId);
+                }
+                onChange(serializeComponentIdSelection(nextIds));
+              }}
+            />
+          );
+        }) : <span className="text-[12px] font-semibold text-[#6f7788]">{emptyLabel}</span>}
+      </div>
+    </div>
+  );
+}
+
+function DialogIncidentStateRadioGroup({
+  label,
+  value,
+  onChange,
+  t
+}: {
+  label: string;
+  value: string;
+  onChange: (value: string) => void;
+  t: Translator;
+}) {
+  return (
+    <div className="grid gap-1.5 text-[12px] font-semibold text-[#a9b0bb]">
+      <span>{label}</span>
+      <HzRadioButtonGroup
+        name="status-incident-state"
+        value={value}
+        onChange={onChange}
+        data-status-incident-state-control="angular-radio-buttons"
+        data-status-incident-state-owner="hertzbeat-ui-radio-button-group"
+        options={[
+          { value: '0', label: t('status.incident.state.0'), icon: <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" /> },
+          { value: '1', label: t('status.incident.state.1'), icon: <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" /> },
+          { value: '2', label: t('status.incident.state.2'), icon: <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" /> },
+          { value: '3', label: t('status.incident.state.3'), icon: <Power className="h-3.5 w-3.5" aria-hidden="true" /> }
+        ]}
+      />
+    </div>
+  );
+}
+
 function tagClassName(kind: 'ok' | 'bad' | 'warn' | 'neutral') {
   if (kind === 'ok') return 'border-[#166534]/45 bg-[#0f2f23] text-[#86efac]';
   if (kind === 'bad') return 'border-[#7f1d1d]/55 bg-[#2a1214] text-[#fca5a5]';
@@ -157,21 +281,6 @@ function tagClassName(kind: 'ok' | 'bad' | 'warn' | 'neutral') {
   return 'border-[#303743] bg-[#101217] text-[#cbd5e1]';
 }
 
-function componentTone(state?: number | string | null) {
-  const value = Number(state ?? 0);
-  if (value === 0) return tagClassName('ok');
-  if (value === 1) return tagClassName('bad');
-  return tagClassName('neutral');
-}
-
-function incidentTone(state?: number | string | null) {
-  const value = Number(state ?? 0);
-  if (value === 0) return tagClassName('bad');
-  if (value === 1 || value === 2) return tagClassName('warn');
-  if (value === 3) return tagClassName('ok');
-  return tagClassName('neutral');
-}
-
 function Tag({ className, children }: React.HTMLAttributes<HTMLSpanElement>) {
   return (
     <span className={`inline-flex min-h-6 items-center rounded-[3px] border px-2 text-[11px] font-semibold ${className || ''}`}>
@@ -180,6 +289,23 @@ function Tag({ className, children }: React.HTMLAttributes<HTMLSpanElement>) {
   );
 }
 
+function truncateIncidentHistoryTitle(message?: string | null, maxLength = 20) {
+  const value = message?.trim() || '';
+  return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
+}
+
+function incidentHistoryTone(state?: number | null) {
+  if (state === 0) return 'critical' as const;
+  if (state === 1 || state === 2) return 'warning' as const;
+  if (state === 3) return 'success' as const;
+  return 'neutral' as const;
+}
+
+function incidentMessagePlaceholderKey(state: string) {
+  const normalizedState = Number.parseInt(state, 10);
+  return `status.incident.message.tip.${Number.isFinite(normalizedState) ? normalizedState : 0}`;
+}
+
 function labelEntries(labels?: Record<string, string> | null) {
   return Object.entries(labels || {});
 }
@@ -194,7 +320,12 @@ function StatusTabs({
   onModeChange: (mode: 'component' | 'incident') => void;
 }) {
   return (
-    <div data-status-tabs="cold-segmented-tabs" className="mb-4 inline-flex rounded-[3px] border border-[#2b3039] bg-[#101217] p-1">
+    <div
+      data-status-tabs="cold-segmented-tabs"
+      data-status-tab-refresh-contract="angular-nz-tab-click-load"
+      data-status-tab-refresh-owner="route-refresh-contract"
+      className="mb-4 inline-flex rounded-[3px] border border-[#2b3039] bg-[#101217] p-1"
+    >
       {[
         ['component', t('status.component')],
         ['incident', t('status.incident')]
@@ -208,6 +339,7 @@ function StatusTabs({
             className={`h-7 min-w-[104px] rounded-[3px] px-3 text-[12px] font-semibold transition ${
               active ? 'bg-[#182238] text-[#f5f7fb]' : 'text-[#858d9a] hover:bg-[#151b28] hover:text-[#dbe4f0]'
             }`}
+            data-status-tab-refresh-mode={mode}
             onClick={() => onModeChange(mode)}
           >
             {label}
@@ -260,13 +392,25 @@ export function StatusSettingSurface({
   onIncidentSearchInputChange,
   onCommitIncidentSearch,
   onResetIncidentSearch,
+  onIncidentPageIndexChange,
+  onIncidentPageSizeChange,
+  onIncidentPrevious,
+  onIncidentNext,
   onSelectIncident
 }: StatusSettingSurfaceProps) {
   const statusOrg = data.org || {};
   const components = data.components || [];
   const incidents = data.incidents?.content || [];
+  const [openRowActionMenuId, setOpenRowActionMenuId] = React.useState<string | null>(null);
   const publicStatusLabel = statusOrg.name ? `${statusOrg.name} ${t('menu.advanced.status')}` : t('menu.advanced.status');
+  const hasPublicStatusLink = statusOrg.id != null && statusOrg.name != null;
   const activeMode = mode === 'incident' ? 'incident' : 'component';
+  const incidentTotalElements = Math.max(0, data.incidents?.totalElements ?? incidents.length);
+  const incidentPageSize = Math.max(1, data.incidents?.pageSize ?? 8);
+  const incidentTotalPages = Math.max(1, Math.ceil(incidentTotalElements / incidentPageSize));
+  const incidentPageIndex = Math.min(Math.max(0, data.incidents?.pageIndex ?? 0), incidentTotalPages - 1);
+  const incidentCurrentPage = incidentPageIndex + 1;
+  const incidentPaginationSummary = `${t('common.total')} ${incidentTotalElements}`;
   const orgValues = {
     name: orgDraft.name || statusOrg.name || '',
     description: orgDraft.description || statusOrg.description || '',
@@ -276,12 +420,29 @@ export function StatusSettingSurface({
     color: orgDraft.color || statusOrg.color || ''
   };
   const [deleteDialog, setDeleteDialog] = React.useState<DeleteDialogState>(null);
-  const deleteDialogTitle = deleteDialog?.kind === 'component' ? '确认删除组件' : '确认删除维护事件';
-  const deleteDialogName = deleteDialog?.item.name || '-';
+  const emptyCellText = t('common.none');
+  const incidentHistoryItems = (incidentDraft.existingContents || []).map((content, index) => {
+    const state = Number(content.state ?? incidentDraft.state ?? 0);
+    const message = content.message || emptyCellText;
+    return {
+      id: String(content.id ?? content.timestamp ?? index),
+      title: truncateIncidentHistoryTitle(message) || emptyCellText,
+      message,
+      meta: formatTime(content.timestamp || null),
+      stateLabel: t(`status.incident.state.${state}`),
+      stateTone: incidentHistoryTone(state)
+    };
+  });
+  const incidentMessagePlaceholder = t(incidentMessagePlaceholderKey(incidentDraft.state));
+  const deleteDialogTitle =
+    deleteDialog?.kind === 'component'
+      ? t('setting.status.delete.component.title')
+      : t('setting.status.delete.incident.title');
+  const deleteDialogName = deleteDialog?.item.name || emptyCellText;
   const deleteDialogCopy =
     deleteDialog?.kind === 'component'
-      ? '删除后该组件会从公开状态页移除,相关维护事件需要重新绑定组件。'
-      : '删除后该维护事件会从公开状态页移除,历史说明不会再展示给访问者。';
+      ? t('setting.status.delete.component.copy')
+      : t('setting.status.delete.incident.copy');
 
   function openComponentDeleteDialog(component: StatusPageComponent) {
     if (component.id != null) onSelectComponent(component.id);
@@ -306,11 +467,19 @@ export function StatusSettingSurface({
     onDeleteIncident(target.item);
   }
 
+  function handleIncidentPageJumpChange(value: string) {
+    const parsed = Number.parseInt(value, 10);
+    if (!Number.isFinite(parsed)) return;
+    onIncidentPageIndexChange(Math.min(Math.max(parsed, 1), incidentTotalPages) - 1);
+  }
+
   return (
     <>
       <div
         data-status-setting-surface="otlp-cold-status-console"
         data-status-setting-style-baseline={coldStatusVisual.canvasName}
+        data-status-setting-mutation-feedback="angular-notify-keys"
+        data-status-setting-mutation-feedback-owner="route-action-feedback-contract"
         className={coldStatusVisual.canvas.root}
         style={coldStatusVisual.canvas.backgroundStyle}
       >
@@ -323,17 +492,13 @@ export function StatusSettingSurface({
                     {t('menu.advanced.status')}
                   </h1>
                   <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">
-                    管理公开状态页的组织档案、服务组件和维护事件,保持配置、发布和检索动作在一个紧凑后台工作面中完成。
+                    {t('setting.status.subtitle')}
                   </p>
                   <div data-status-command-row="standard-equal-buttons" className={coldStatusVisual.button.row}>
                     <Button size="sm" variant="default" className={coldButtonClassName} onClick={() => onModeChange(activeMode)}>
                       <RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />
                       {t('common.refresh')}
                     </Button>
-                    <a href={publicStatusHref} target="_blank" rel="noreferrer" className={`inline-flex items-center justify-center gap-1.5 ${coldPrimaryButtonClassName}`}>
-                      <ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
-                      {publicStatusLabel}
-                    </a>
                   </div>
                 </div>
               </div>
@@ -347,8 +512,8 @@ export function StatusSettingSurface({
                 >
                   <div className="mb-4 flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
                     <div>
-                      <div className="text-[12px] font-semibold text-[#f1f4fa]">组织档案</div>
-                      <p className="mt-1 text-[12px] leading-5 text-[#858d9a]">公开状态页展示的组织信息和反馈入口。</p>
+                      <div className="text-[12px] font-semibold text-[#f1f4fa]">{t('setting.status.org.title')}</div>
+                      <p className="mt-1 text-[12px] leading-5 text-[#858d9a]">{t('setting.status.org.copy')}</p>
                     </div>
                     <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onSaveOrg} disabled={savingOrg}>
                       {t('common.button.ok')}
@@ -373,13 +538,34 @@ export function StatusSettingSurface({
                     <div className="min-w-0 flex-1 text-[12px] font-semibold text-[#dbe4f0]">
                       {t('status.component')} <span className="ml-2 text-[#6f7788]">{components.length}</span>
                     </div>
-                    <Button size="sm" variant="default" className={coldButtonClassName} onClick={() => onModeChange('component')}>
+                    <Button
+                      size="sm"
+                      variant="default"
+                      className={coldButtonClassName}
+                      data-status-component-refresh-contract="angular-sync-component"
+                      data-status-component-refresh-owner="route-refresh-contract"
+                      onClick={() => onModeChange('component')}
+                    >
                       {t('common.refresh')}
                     </Button>
                     <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onNewComponent}>
                       <Plus className="h-3.5 w-3.5" aria-hidden="true" />
                       {t('status.component.new')}
                     </Button>
+                    {hasPublicStatusLink ? (
+                      <a
+                        href={publicStatusHref}
+                        target="_blank"
+                        rel="noreferrer"
+                        className={`inline-flex items-center justify-center gap-1.5 ${coldPrimaryButtonClassName}`}
+                        data-status-public-link-contract="angular-tab-toolbar-conditional"
+                        data-status-public-link-owner="status-tab-toolbar"
+                        data-status-public-link-mode="component"
+                      >
+                        <ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
+                        {publicStatusLabel}
+                      </a>
+                    ) : null}
                   </div>
 
                   <div data-status-component-table-shell="cold-dense-table" className="overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]">
@@ -395,59 +581,99 @@ export function StatusSettingSurface({
                         </tr>
                       </thead>
                       <tbody>
-                        {components.length > 0 ? components.map(component => (
-                          <tr
-                            key={component.id || component.name}
-                            data-status-component-row={component.id || component.name}
-                            className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]"
-                          >
-                            <td className="px-3 py-2.5 font-semibold text-[#eef2f7]">{component.name || '-'}</td>
-                            <td className="px-3 py-2.5">
-                              <Tag className={componentTone(component.state)}>{t(`status.component.state.${Number(component.state ?? 0)}`)}</Tag>
-                            </td>
-                            <td className="px-3 py-2.5">
-                              <Tag className={tagClassName('neutral')}>{t(`status.component.method.${Number(component.method ?? 0)}`)}</Tag>
-                            </td>
-                            <td className="px-3 py-2.5">
-                              <div className="flex flex-wrap gap-1.5">
-                                {labelEntries(component.labels).length > 0 ? labelEntries(component.labels).map(([key, value]) => (
-                                  <span key={`${component.id}-${key}`} className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] leading-4 text-[#cbd5e1]">
-                                    {key}:{value}
-                                  </span>
-                                )) : <span className="text-[#6f7788]">-</span>}
-                              </div>
-                            </td>
-                            <td className="px-3 py-2.5 text-[#858d9a]">{formatTime(component.gmtUpdate || component.gmtCreate || null)}</td>
-                            <td className="px-3 py-2.5">
-                              <div data-status-component-row-actions="cold-icon-actions" className="flex gap-1.5">
-                                <Button
-                                  size="icon"
-                                  variant="default"
-                                  className={coldIconButtonClassName}
-                                  title={t('status.component.edit')}
-                                  onClick={() => {
-                                    if (component.id != null) onSelectComponent(component.id);
-                                    onEditComponent(component);
-                                  }}
-                                >
-                                  <Pencil className="h-3.5 w-3.5" aria-hidden="true" />
-                                  <span className="sr-only">{t('status.component.edit')}</span>
-                                </Button>
-                                <Button
-                                  size="icon"
-                                  variant="default"
-                                  className={coldIconButtonClassName}
-                                  title={t('status.component.delete')}
-                                  data-status-component-delete-confirm-trigger="cold-modal"
-                                  onClick={() => openComponentDeleteDialog(component)}
-                                >
-                                  <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
-                                  <span className="sr-only">{t('status.component.delete')}</span>
-                                </Button>
-                              </div>
-                            </td>
-                          </tr>
-                        )) : <EmptyTableRow colSpan={6} />}
+                        {components.length > 0 ? components.map(component => {
+                          const componentLabels = labelEntries(component.labels);
+                          const rowActionMenuId = `component-${component.id || component.name}`;
+                          const isRowActionMenuOpen = openRowActionMenuId === rowActionMenuId;
+
+                          return (
+                            <tr
+                              key={component.id || component.name}
+                              data-status-component-row={component.id || component.name}
+                              className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]"
+                            >
+                              <td className="px-3 py-2.5 font-semibold text-[#eef2f7]">{component.name || emptyCellText}</td>
+                              <td className="px-3 py-2.5">
+                                <Tag className={tagClassName(settingStatusComponentTagTone(component.state))}>{t(`status.component.state.${Number(component.state ?? 0)}`)}</Tag>
+                              </td>
+                              <td className="px-3 py-2.5">
+                                <Tag className={tagClassName('neutral')}>{t(`status.component.method.${Number(component.method ?? 0)}`)}</Tag>
+                              </td>
+                              <td className="px-3 py-2.5">
+                                <div className="flex flex-wrap gap-1.5">
+                                  {componentLabels.length > 0 ? componentLabels.map(([key, value]) => (
+                                    <span key={`${component.id}-${key}`} className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] leading-4 text-[#cbd5e1]">
+                                      {key}:{value}
+                                    </span>
+                                  )) : <span className="text-[#6f7788]">{emptyCellText}</span>}
+                                </div>
+                              </td>
+                              <td className="px-3 py-2.5 text-[#858d9a]">{formatTime(component.gmtUpdate || component.gmtCreate || null)}</td>
+                              <td className="px-3 py-2.5">
+                                <div data-status-component-row-actions="cold-icon-actions" className="flex gap-1.5">
+                                  <Button
+                                    size="icon"
+                                    variant="default"
+                                    className={coldIconButtonClassName}
+                                    title={t('status.component.edit')}
+                                    onClick={() => {
+                                      if (component.id != null) onSelectComponent(component.id);
+                                      onEditComponent(component);
+                                    }}
+                                  >
+                                    <Pencil className="h-3.5 w-3.5" aria-hidden="true" />
+                                    <span className="sr-only">{t('status.component.edit')}</span>
+                                  </Button>
+                                  <div
+                                    className={statusActionMenuRootClassName}
+                                    data-status-component-delete-menu={rowActionMenuId}
+                                    data-status-row-delete-menu-contract="angular-ellipsis-dropdown-delete"
+                                    data-status-component-delete-menu-contract="angular-ellipsis-dropdown-delete"
+                                    data-status-row-delete-menu-open={isRowActionMenuOpen ? 'true' : 'false'}
+                                  >
+                                    <button
+                                      type="button"
+                                      aria-expanded={isRowActionMenuOpen}
+                                      aria-label={t('common.edit')}
+                                      title={t('common.edit')}
+                                      className={`${coldIconButtonClassName} inline-flex cursor-pointer list-none items-center justify-center [&::-webkit-details-marker]:hidden`}
+                                      onClick={() => {
+                                        setOpenRowActionMenuId(current => (current === rowActionMenuId ? null : rowActionMenuId));
+                                      }}
+                                      data-status-component-delete-menu-trigger={rowActionMenuId}
+                                    >
+                                      <MoreHorizontal className="h-3.5 w-3.5" aria-hidden="true" />
+                                      <span className="sr-only">{t('common.edit')}</span>
+                                    </button>
+                                    <div
+                                      role="menu"
+                                      hidden={!isRowActionMenuOpen}
+                                      className={`${statusActionMenuPanelBaseClassName} right-0`}
+                                      data-status-component-delete-menu-panel={rowActionMenuId}
+                                      data-status-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"
+                                      data-status-row-delete-menu-panel-open={isRowActionMenuOpen ? 'true' : 'false'}
+                                    >
+                                      <HzTableRowActionButton
+                                        width="root-span"
+                                        intent="ghost"
+                                        onClick={() => {
+                                          setOpenRowActionMenuId(null);
+                                          openComponentDeleteDialog(component);
+                                        }}
+                                        data-status-component-delete-confirm-trigger="angular-modal-confirm"
+                                        data-status-component-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+                                        className="w-full text-[#fecaca] hover:text-white"
+                                      >
+                                        <Trash2 className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
+                                        <span className="truncate">{t('status.component.delete')}</span>
+                                      </HzTableRowActionButton>
+                                    </div>
+                                  </div>
+                                </div>
+                              </td>
+                            </tr>
+                          );
+                        }) : <EmptyTableRow colSpan={6} t={t} />}
                       </tbody>
                     </table>
                   </div>
@@ -470,10 +696,36 @@ export function StatusSettingSurface({
                     onSearch={onCommitIncidentSearch}
                     onClear={onResetIncidentSearch}
                     trailingActions={
-                      <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onNewIncident}>
-                        <Plus className="h-3.5 w-3.5" aria-hidden="true" />
-                        {t('status.incident.new')}
-                      </Button>
+                      <>
+                        <Button
+                          size="sm"
+                          variant="default"
+                          className={coldButtonClassName}
+                          data-status-incident-refresh-contract="angular-sync-incidence"
+                          data-status-incident-refresh-owner="route-refresh-contract"
+                          onClick={() => onModeChange('incident')}
+                        >
+                          {t('common.refresh')}
+                        </Button>
+                        <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onNewIncident}>
+                          <Plus className="h-3.5 w-3.5" aria-hidden="true" />
+                          {t('status.incident.new')}
+                        </Button>
+                        {hasPublicStatusLink ? (
+                          <a
+                            href={publicStatusHref}
+                            target="_blank"
+                            rel="noreferrer"
+                            className={`inline-flex items-center justify-center gap-1.5 ${coldPrimaryButtonClassName}`}
+                            data-status-public-link-contract="angular-tab-toolbar-conditional"
+                            data-status-public-link-owner="status-tab-toolbar"
+                            data-status-public-link-mode="incident"
+                          >
+                            <ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
+                            {publicStatusLabel}
+                          </a>
+                        ) : null}
+                      </>
                     }
                   />
 
@@ -490,61 +742,136 @@ export function StatusSettingSurface({
                         </tr>
                       </thead>
                       <tbody>
-                        {incidents.length > 0 ? incidents.map(incident => (
-                          <tr
-                            key={incident.id || incident.name}
-                            data-status-incident-row={incident.id || incident.name}
-                            className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]"
-                          >
-                            <td className="px-3 py-2.5 font-semibold text-[#eef2f7]">{incident.name || '-'}</td>
-                            <td className="px-3 py-2.5">
-                              <Tag className={incidentTone(incident.state ?? incident.status)}>{t(`status.incident.state.${Number(incident.state ?? incident.status ?? 0)}`)}</Tag>
-                            </td>
-                            <td className="truncate px-3 py-2.5" title={latestIncidentMessage(incident) || '-'}>
-                              {latestIncidentMessage(incident) || '-'}
-                            </td>
-                            <td className="px-3 py-2.5">
-                              <div className="flex flex-wrap gap-1.5">
-                                {(incident.components || []).length > 0 ? (incident.components || []).map(component => (
-                                  <span key={`${incident.id}-${component.id || component.name}`} className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] leading-4 text-[#cbd5e1]">
-                                    {component.name || component.id}
-                                  </span>
-                                )) : <span className="text-[#6f7788]">-</span>}
-                              </div>
-                            </td>
-                            <td className="px-3 py-2.5 text-[#858d9a]">{formatTime(incident.startTime || incident.gmtCreate || null)}</td>
-                            <td className="px-3 py-2.5">
-                              <div data-status-incident-row-actions="cold-icon-actions" className="flex gap-1.5">
-                                <Button
-                                  size="icon"
-                                  variant="default"
-                                  className={coldIconButtonClassName}
-                                  title={t('status.incident.update')}
-                                  onClick={() => {
-                                    if (incident.id != null) onSelectIncident(incident.id);
-                                    onEditIncident(incident);
-                                  }}
-                                >
-                                  <RotateCw className="h-3.5 w-3.5" aria-hidden="true" />
-                                  <span className="sr-only">{t('status.incident.update')}</span>
-                                </Button>
-                                <Button
-                                  size="icon"
-                                  variant="default"
-                                  className={coldIconButtonClassName}
-                                  title={t('status.incident.delete')}
-                                  data-status-incident-delete-confirm-trigger="cold-modal"
-                                  onClick={() => openIncidentDeleteDialog(incident)}
-                                >
-                                  <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
-                                  <span className="sr-only">{t('status.incident.delete')}</span>
-                                </Button>
-                              </div>
-                            </td>
-                          </tr>
-                        )) : <EmptyTableRow colSpan={6} />}
+                        {incidents.length > 0 ? incidents.map(incident => {
+                          const incidentMessageText = latestIncidentMessage(incident) || emptyCellText;
+                          const incidentComponents = incident.components || [];
+                          const rowActionMenuId = `incident-${incident.id || incident.name}`;
+                          const isRowActionMenuOpen = openRowActionMenuId === rowActionMenuId;
+
+                          return (
+                            <tr
+                              key={incident.id || incident.name}
+                              data-status-incident-row={incident.id || incident.name}
+                              className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]"
+                            >
+                              <td className="px-3 py-2.5 font-semibold text-[#eef2f7]">{incident.name || emptyCellText}</td>
+                              <td className="px-3 py-2.5">
+                                <Tag className={tagClassName(settingStatusIncidentTagTone(incident.state ?? incident.status))}>{t(`status.incident.state.${Number(incident.state ?? incident.status ?? 0)}`)}</Tag>
+                              </td>
+                              <td className="truncate px-3 py-2.5" title={incidentMessageText}>
+                                {incidentMessageText}
+                              </td>
+                              <td className="px-3 py-2.5">
+                                <div className="flex flex-wrap gap-1.5">
+                                  {incidentComponents.length > 0 ? incidentComponents.map((component, index) => (
+                                    <span key={`${incident.id}-${component.id || component.name || index}`} className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] leading-4 text-[#cbd5e1]">
+                                      {component.name || component.id || emptyCellText}
+                                    </span>
+                                  )) : <span className="text-[#6f7788]">{emptyCellText}</span>}
+                                </div>
+                              </td>
+                              <td className="px-3 py-2.5 text-[#858d9a]">{formatTime(incident.startTime || incident.gmtCreate || null)}</td>
+                              <td className="px-3 py-2.5">
+                                <div data-status-incident-row-actions="cold-icon-actions" className="flex gap-1.5">
+                                  <Button
+                                    size="icon"
+                                    variant="default"
+                                    className={coldIconButtonClassName}
+                                    title={t('status.incident.update')}
+                                    onClick={() => {
+                                      if (incident.id != null) onSelectIncident(incident.id);
+                                      onEditIncident(incident);
+                                    }}
+                                  >
+                                    <RotateCw className="h-3.5 w-3.5" aria-hidden="true" />
+                                    <span className="sr-only">{t('status.incident.update')}</span>
+                                  </Button>
+                                  <div
+                                    className={statusActionMenuRootClassName}
+                                    data-status-incident-delete-menu={rowActionMenuId}
+                                    data-status-row-delete-menu-contract="angular-ellipsis-dropdown-delete"
+                                    data-status-incident-delete-menu-contract="angular-ellipsis-dropdown-delete"
+                                    data-status-row-delete-menu-open={isRowActionMenuOpen ? 'true' : 'false'}
+                                  >
+                                    <button
+                                      type="button"
+                                      aria-expanded={isRowActionMenuOpen}
+                                      aria-label={t('common.edit')}
+                                      title={t('common.edit')}
+                                      className={`${coldIconButtonClassName} inline-flex cursor-pointer list-none items-center justify-center [&::-webkit-details-marker]:hidden`}
+                                      onClick={() => {
+                                        setOpenRowActionMenuId(current => (current === rowActionMenuId ? null : rowActionMenuId));
+                                      }}
+                                      data-status-incident-delete-menu-trigger={rowActionMenuId}
+                                    >
+                                      <MoreHorizontal className="h-3.5 w-3.5" aria-hidden="true" />
+                                      <span className="sr-only">{t('common.edit')}</span>
+                                    </button>
+                                    <div
+                                      role="menu"
+                                      hidden={!isRowActionMenuOpen}
+                                      className={`${statusActionMenuPanelBaseClassName} right-0`}
+                                      data-status-incident-delete-menu-panel={rowActionMenuId}
+                                      data-status-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"
+                                      data-status-row-delete-menu-panel-open={isRowActionMenuOpen ? 'true' : 'false'}
+                                    >
+                                      <HzTableRowActionButton
+                                        width="root-span"
+                                        intent="ghost"
+                                        onClick={() => {
+                                          setOpenRowActionMenuId(null);
+                                          openIncidentDeleteDialog(incident);
+                                        }}
+                                        data-status-incident-delete-confirm-trigger="angular-modal-confirm"
+                                        data-status-incident-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+                                        className="w-full text-[#fecaca] hover:text-white"
+                                      >
+                                        <Trash2 className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
+                                        <span className="truncate">{t('status.incident.delete')}</span>
+                                      </HzTableRowActionButton>
+                                    </div>
+                                  </div>
+                                </div>
+                              </td>
+                            </tr>
+                          );
+                        }) : <EmptyTableRow colSpan={6} t={t} />}
                       </tbody>
                     </table>
+                    <div
+                      data-status-incident-pagination="cold-dense-pagination"
+                      data-status-incident-pagination-owner="hertzbeat-ui-pagination-bar"
+                      data-status-incident-pagination-contract="angular-search-pagination"
+                    >
+                      <HzPaginationBar
+                        summary={incidentPaginationSummary}
+                        pageSizeLabel={t('common.page-size')}
+                        pageSizeValue={String(incidentPageSize)}
+                        pageSizeOptions={[8, 15, 25].map(value => ({ value: String(value), label: String(value) }))}
+                        pageJumpLabel={t('common.page')}
+                        pageJumpValue={String(incidentCurrentPage)}
+                        pageJumpMax={incidentTotalPages}
+                        previousLabel={t('common.previous-page')}
+                        nextLabel={t('common.next-page')}
+                        previousDisabled={incidentPageIndex <= 0}
+                        nextDisabled={incidentCurrentPage >= incidentTotalPages}
+                        onPrevious={onIncidentPrevious}
+                        onNext={onIncidentNext}
+                        onPageSizeChange={value => onIncidentPageSizeChange(Number.parseInt(value, 10))}
+                        onPageJumpChange={handleIncidentPageJumpChange}
+                        pageJumpInputProps={
+                          {
+                            'data-status-incident-pagination-page-jump-owner': 'hertzbeat-ui-input'
+                          } as React.ComponentProps<typeof HzPaginationBar>['pageJumpInputProps']
+                        }
+                        pageSizeSelectProps={
+                          {
+                            'data-status-incident-pagination-page-size-owner': 'hertzbeat-ui-select'
+                          } as React.ComponentProps<typeof HzPaginationBar>['pageSizeSelectProps']
+                        }
+                        className="border-x-0"
+                      />
+                    </div>
                   </div>
                   {incidentMessage ? <div className="px-3 py-2 text-sm text-[#a9b0bb]">{incidentMessage}</div> : null}
                   {incidentError ? <div className="px-3 py-2 text-sm text-[#f87171]">{incidentError}</div> : null}
@@ -574,11 +901,11 @@ export function StatusSettingSurface({
       >
         <div className="grid gap-3 md:grid-cols-2">
           <DialogField label={t('status.component.name')} value={componentDraft.name} placeholder={t('status.component.name.tip')} onChange={value => onComponentDraftChange({ name: value })} />
-          <DialogField label={t('status.component.method')} value={componentDraft.method} placeholder="0 / 1" onChange={value => onComponentDraftChange({ method: value })} />
-          <DialogField label={t('status.component.state')} value={componentDraft.state} placeholder="0 / 1 / 2" onChange={value => onComponentDraftChange({ state: value })} />
-          <DialogField label={t('status.component.tag')} value={componentDraft.labelsText} placeholder="env:prod, app:api" onChange={value => onComponentDraftChange({ labelsText: value })} />
+          <DialogField label={t('status.component.method')} value={componentDraft.method} placeholder={t('setting.status.component.method.placeholder')} onChange={value => onComponentDraftChange({ method: value })} />
+          <DialogField label={t('status.component.state')} value={componentDraft.state} placeholder={t('setting.status.component.state.placeholder')} onChange={value => onComponentDraftChange({ state: value })} />
+          <DialogField label={t('status.component.tag')} value={componentDraft.labelsText} placeholder={t('setting.status.component.labels.placeholder')} onChange={value => onComponentDraftChange({ labelsText: value })} />
           <div className="md:col-span-2">
-            <DialogField label={t('status.org.desc')} value={componentDraft.description} placeholder={t('status.component.name.tip')} onChange={value => onComponentDraftChange({ description: value })} />
+            <DialogField label={t('status.org.desc')} value={componentDraft.description} placeholder={t('setting.status.component.description.placeholder')} onChange={value => onComponentDraftChange({ description: value })} />
           </div>
         </div>
       </OverlayDialog>
@@ -589,6 +916,10 @@ export function StatusSettingSurface({
         kicker={t('status.incident')}
         maxWidthClassName="max-w-2xl"
         onClose={onCancelIncident}
+        overlayProps={{
+          'data-status-incident-failure-close-contract': 'angular-cancel-on-api-failure',
+          'data-status-incident-failure-close-owner': 'status-route-action-feedback'
+        } as React.ComponentProps<typeof OverlayDialog>['overlayProps']}
         footer={
           <div className="flex justify-end gap-2">
             <Button size="sm" variant="default" className={coldButtonClassName} onClick={onCancelIncident}>
@@ -602,40 +933,90 @@ export function StatusSettingSurface({
       >
         <div className="grid gap-3 md:grid-cols-2">
           <DialogField label={t('status.incident.name')} value={incidentDraft.name} placeholder={t('status.incident.name.tip')} onChange={value => onIncidentDraftChange({ name: value })} />
-          <DialogField label={t('status.incident.state')} value={incidentDraft.state} placeholder="0 / 1 / 2 / 3" onChange={value => onIncidentDraftChange({ state: value })} />
-          <DialogField label={t('status.incident.component')} value={incidentDraft.componentIdsText} placeholder="1, 2" onChange={value => onIncidentDraftChange({ componentIdsText: value })} />
-          <DialogField label={t('status.incident.message')} value={incidentDraft.message} placeholder={t('status.incident.message.tip.0')} onChange={value => onIncidentDraftChange({ message: value })} />
+          {incidentDraft.id ? (
+            <div
+              className="md:col-span-2"
+              data-status-incident-history-contract="angular-collapse-desc"
+              data-status-incident-history-owner="hertzbeat-ui-status-incident-history"
+            >
+              <HzStatusIncidentHistory
+                title={t('status.incident.history')}
+                items={incidentHistoryItems}
+                emptyLabel={emptyCellText}
+              />
+            </div>
+          ) : null}
+          <div className="md:col-span-2">
+            <DialogIncidentStateRadioGroup
+              label={t('status.incident.state')}
+              value={incidentDraft.state}
+              t={t}
+              onChange={value => onIncidentDraftChange({ state: value })}
+            />
+          </div>
+          <DialogComponentCheckboxGroup
+            label={t('status.incident.component')}
+            components={components}
+            value={incidentDraft.componentIdsText}
+            emptyLabel={emptyCellText}
+            onChange={value => onIncidentDraftChange({ componentIdsText: value })}
+          />
+          <div
+            className="md:col-span-2"
+            data-status-incident-message-input="angular-textarea"
+            data-status-incident-message-input-owner="hertzbeat-ui-textarea"
+            data-status-incident-message-placeholder-contract="angular-state-placeholder"
+          >
+            <DialogTextareaField
+              label={t('status.incident.message')}
+              value={incidentDraft.message}
+              placeholder={incidentMessagePlaceholder}
+              rows={2}
+              data-status-incident-message-placeholder={incidentMessagePlaceholderKey(incidentDraft.state)}
+              onChange={value => onIncidentDraftChange({ message: value })}
+            />
+          </div>
         </div>
       </OverlayDialog>
 
-      <OverlayDialog
-        open={deleteDialog !== null}
-        title={deleteDialogTitle}
-        kicker={t('menu.advanced.status')}
-        maxWidthClassName="max-w-xl"
-        onClose={() => setDeleteDialog(null)}
-        footer={
-          <div className="flex justify-end gap-2">
-            <Button size="sm" variant="default" className={coldButtonClassName} onClick={() => setDeleteDialog(null)}>
-              取消
-            </Button>
-            <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={submitDeleteDialog}>
-              确认删除
-            </Button>
-          </div>
-        }
+      <div
+        data-status-delete-confirm="angular-modal-confirm"
+        data-status-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+        data-status-delete-confirm-state={deleteDialog ? 'open' : 'closed'}
       >
-        <div
-          data-status-delete-confirm="cold-modal"
+        <HzConfirmDialog
+          open={deleteDialog !== null}
+          tone="critical"
+          title={deleteDialogTitle}
+          kicker={t('menu.advanced.status')}
+          cancelLabel={t('common.button.cancel')}
+          confirmLabel={t('setting.status.delete.confirm')}
+          onClose={() => setDeleteDialog(null)}
+          onConfirm={submitDeleteDialog}
+          data-status-delete-confirm-dialog="angular-modal-confirm"
           data-status-delete-confirm-kind={deleteDialog?.kind || ''}
-          className="space-y-3 text-[12px] leading-6 text-[#a9b0bb]"
+          confirmButtonProps={
+            {
+              'data-status-delete-confirm-ok': 'angular-modal-confirm'
+            } as React.ComponentProps<typeof HzConfirmDialog>['confirmButtonProps']
+          }
+          cancelButtonProps={
+            {
+              'data-status-delete-confirm-cancel': 'angular-modal-confirm'
+            } as React.ComponentProps<typeof HzConfirmDialog>['cancelButtonProps']
+          }
         >
-          <p>{deleteDialogCopy}</p>
-          <div className="rounded-[3px] border border-[#3f2228] bg-[#181013] px-3 py-2 font-semibold text-[#f0b8c1]">
-            {deleteDialogName}
+          <div
+            data-status-delete-confirm-copy="angular-modal-confirm"
+            className="space-y-3"
+          >
+            <p>{deleteDialogCopy}</p>
+            <div className="rounded-[3px] border border-[#3f2228] bg-[#181013] px-3 py-2 font-semibold text-[#f0b8c1]">
+              {deleteDialogName}
+            </div>
           </div>
-        </div>
-      </OverlayDialog>
+        </HzConfirmDialog>
+      </div>
     </>
   );
 }
diff --git a/web-next/components/settings/settings-console-shell.test.tsx b/web-next/components/settings/settings-console-shell.test.tsx
index f7d03171fc..994da7e5d9 100644
--- a/web-next/components/settings/settings-console-shell.test.tsx
+++ b/web-next/components/settings/settings-console-shell.test.tsx
@@ -18,7 +18,9 @@ describe('settings console shell', () => {
     const html = renderToStaticMarkup(
       <SettingsConsoleShell
         activeHref="/setting/settings/config"
+        contentLabel="设置内容"
         kicker="设置"
+        navigationLabel="设置分区"
         title="把系统配置、消息通道和接入凭证放在一起"
         subtitle="在同一页查看系统配置、服务器、对象存储和接入令牌。"
         items={[
@@ -32,12 +34,22 @@ describe('settings console shell', () => {
 
     expect(html).toContain('data-settings-console-surface="otlp-cold-settings-console"');
     expect(html).toContain('data-settings-console-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-settings-console-route-contract="angular-settings-shell"');
+    expect(html).toContain('data-settings-console-menu-contract="angular-config-server-object-store-token"');
+    expect(html).toContain('data-settings-console-menu-mode-contract="angular-inline-horizontal-responsive"');
+    expect(html).toContain('data-settings-console-active-title-contract="angular-active-child-title"');
     expect(html).toContain('data-settings-console-header="cold-compact-header"');
     expect(html).toContain('data-settings-console-main="cold-settings-workspace"');
     expect(html).toContain('data-settings-console-menu="cold-static-list"');
+    expect(html).toContain('data-settings-console-menu-mode="angular-responsive-inline-horizontal"');
     expect(html).toContain('data-settings-console-content="cold-settings-content"');
+    expect(html).toContain('data-settings-console-active-title="系统配置"');
+    expect(html).toContain('data-settings-console-active-title-marker="系统配置"');
+    expect(html).toContain('aria-label="设置分区"');
+    expect(html).toContain('aria-label="设置内容"');
     expect(html).toContain('data-settings-console-menu-item="/setting/settings/config"');
     expect(html).toContain('data-settings-console-active="true"');
+    expect(html).toContain('data-settings-console-selected-contract="angular-nz-selected"');
     expect(html).toContain('设置');
     expect(html).toContain('把系统配置、消息通道和接入凭证放在一起');
     expect(html).toContain('在同一页查看系统配置、服务器、对象存储和接入令牌。');
@@ -49,6 +61,9 @@ describe('settings console shell', () => {
     expect(html).toContain('href="/setting/settings/server"');
     expect(html).toContain('rounded-[4px]');
     expect(html).toContain('rounded-[3px]');
+    expect(html).toContain('flex-row');
+    expect(html).toContain('lg:flex-col');
+    expect(html).toContain('overflow-x-auto');
     expect(html).toContain('border-[#2b3039]');
     expect(html).toContain('bg-[#0b0c0e]');
     expect(html).toContain('bg-[#101217]');
@@ -57,7 +72,7 @@ describe('settings console shell', () => {
     expect(html).not.toContain('angular-bordered-main');
     expect(html).not.toContain('angular-menu');
     expect(html).not.toContain('data-settings-console-summary-rail');
-  });
+  }, 15000);
 
   it('uses the shared cold visual owner and keeps the shell free of old markers', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/settings/settings-console-shell.tsx'), 'utf8');
@@ -65,7 +80,16 @@ describe('settings console shell', () => {
     expect(source).toContain('coldOpsCatalogVisual');
     expect(source).toContain('data-settings-console-surface="otlp-cold-settings-console"');
     expect(source).toContain('data-settings-console-style-baseline={coldSettingsVisual.canvasName}');
+    expect(source).toContain('data-settings-console-route-contract="angular-settings-shell"');
+    expect(source).toContain('data-settings-console-menu-contract="angular-config-server-object-store-token"');
+    expect(source).toContain('data-settings-console-menu-mode-contract="angular-inline-horizontal-responsive"');
+    expect(source).toContain('data-settings-console-active-title-contract="angular-active-child-title"');
     expect(source).toContain('data-settings-console-main="cold-settings-workspace"');
+    expect(source).toContain('data-settings-console-menu-mode="angular-responsive-inline-horizontal"');
+    expect(source).toContain('data-settings-console-active-title={activeItem?.label ?? \'\'}');
+    expect(source).toContain('data-settings-console-active-title-marker={activeItem?.label ?? \'\'}');
+    expect(source).toContain('data-settings-console-selected-contract');
+    expect(source).toContain('lg:flex-col');
     expect(source).not.toContain('angular-page-shell');
     expect(source).not.toContain('angular-page-header');
     expect(source).not.toContain('angular-bordered-main');
diff --git a/web-next/components/settings/settings-console-shell.tsx b/web-next/components/settings/settings-console-shell.tsx
index fa6ed6eb93..730c6a556e 100644
--- a/web-next/components/settings/settings-console-shell.tsx
+++ b/web-next/components/settings/settings-console-shell.tsx
@@ -17,7 +17,9 @@ export function SettingsConsoleShell({
   activeHref,
   children,
   className,
+  contentLabel,
   kicker,
+  navigationLabel,
   title,
   subtitle
 }: {
@@ -25,14 +27,22 @@ export function SettingsConsoleShell({
   activeHref?: string | null;
   children: React.ReactNode;
   className?: string;
+  contentLabel?: string;
   kicker?: string;
+  navigationLabel?: string;
   title?: string;
   subtitle?: string;
 }) {
+  const activeItem = items.find(item => item.href === activeHref) ?? items[0] ?? null;
+
   return (
     <div
       data-settings-console-surface="otlp-cold-settings-console"
       data-settings-console-style-baseline={coldSettingsVisual.canvasName}
+      data-settings-console-route-contract="angular-settings-shell"
+      data-settings-console-menu-contract="angular-config-server-object-store-token"
+      data-settings-console-menu-mode-contract="angular-inline-horizontal-responsive"
+      data-settings-console-active-title-contract="angular-active-child-title"
       className={cn(coldSettingsVisual.canvas.root, className)}
       style={coldSettingsVisual.canvas.backgroundStyle}
     >
@@ -53,9 +63,10 @@ export function SettingsConsoleShell({
           >
             <aside
               data-settings-console-menu="cold-static-list"
-              className="self-start overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
+              data-settings-console-menu-mode="angular-responsive-inline-horizontal"
+              className="self-start overflow-x-auto rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)] lg:overflow-hidden"
             >
-              <nav className="flex flex-col gap-2 p-2">
+              <nav aria-label={navigationLabel} className="flex min-w-max flex-row gap-2 p-2 lg:min-w-0 lg:flex-col">
                 {items.map(item => {
                   const active = item.href === activeHref;
 
@@ -66,8 +77,9 @@ export function SettingsConsoleShell({
                       aria-current={active ? 'page' : undefined}
                       data-settings-console-menu-item={item.href}
                       data-settings-console-active={active ? 'true' : undefined}
+                      data-settings-console-selected-contract={active ? 'angular-nz-selected' : undefined}
                       className={cn(
-                        'flex min-h-[36px] items-center rounded-[3px] border px-3 text-[12px] font-semibold leading-5 transition-colors duration-150',
+                        'flex min-h-[36px] shrink-0 items-center rounded-[3px] border px-3 text-[12px] font-semibold leading-5 transition-colors duration-150 lg:shrink',
                         active
                           ? 'border-[#4e74f8] bg-[#121a2a] text-[#eef2f7]'
                           : 'border-[#2b3039] bg-[#101217] text-[#a9b0bb] hover:border-[#3f4654] hover:bg-[#151820] hover:text-[#eef2f7]'
@@ -81,8 +93,11 @@ export function SettingsConsoleShell({
             </aside>
             <section
               data-settings-console-content="cold-settings-content"
+              data-settings-console-active-title={activeItem?.label ?? ''}
+              aria-label={contentLabel}
               className="min-w-0 rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] px-5 py-5 shadow-[0_20px_56px_rgba(0,0,0,0.32)] md:px-6"
             >
+              <div hidden data-settings-console-active-title-marker={activeItem?.label ?? ''} />
               {children}
             </section>
           </div>
diff --git a/web-next/components/settings/settings-dialog-form.test.tsx b/web-next/components/settings/settings-dialog-form.test.tsx
index d83071a1e8..2fdfad2f35 100644
--- a/web-next/components/settings/settings-dialog-form.test.tsx
+++ b/web-next/components/settings/settings-dialog-form.test.tsx
@@ -28,6 +28,9 @@ describe('settings dialog form controls', () => {
 
     expect(html).toContain('data-settings-dialog-form="cold-dialog-form"');
     expect(html).toContain('data-settings-dialog-field="cold-dialog-field"');
+    expect(html).toContain('data-settings-dialog-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-settings-dialog-label-span="7"');
+    expect(html).toContain('data-settings-dialog-control-span="12"');
     expect(html).toContain('data-settings-dialog-control="cold-select-control"');
     expect(html).toContain('data-settings-dialog-control="cold-input-control"');
     expect(html).toContain('data-cold-select-owner="cold-custom-select"');
@@ -38,18 +41,51 @@ describe('settings dialog form controls', () => {
     expect(html).toContain('rounded-[3px]');
     expect(html).toContain('border-[#2b3039]');
     expect(html).toContain('bg-[#101217]');
+    expect(html).toContain('md:grid-cols-[7fr_12fr]');
     expect(html).not.toContain('h-10');
     expect(html).not.toContain('rounded-[2px]');
     expect(html).not.toContain('bg-[var(--ops-surface-raised)]');
+    expect(html).not.toContain('md:grid-cols-[180px_minmax(0,1fr)]');
   });
 
   it('keeps the dialog form owner wired to the shared select component', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/settings/settings-dialog-form.tsx'), 'utf8');
 
     expect(source).toContain("from '../ui/select'");
+    expect(source).toContain("layout = 'horizontal'");
+    expect(source).toContain("layout?: 'horizontal' | 'vertical'");
+    expect(source).toContain("data-settings-dialog-field-layout={isVertical ? 'angular-vertical-form' : 'angular-label-7-control-12'}");
+    expect(source).toContain("'angular-vertical-form' : 'angular-label-7-control-12'");
+    expect(source).toContain("data-settings-dialog-label-span={isVertical ? 'vertical' : '7'}");
+    expect(source).toContain("data-settings-dialog-control-span={isVertical ? 'vertical' : '12'}");
+    expect(source).toContain('md:grid-cols-[7fr_12fr]');
     expect(source).toContain('data-settings-dialog-control="cold-select-control"');
     expect(source).not.toContain('<select');
     expect(source).not.toContain('h-10');
     expect(source).not.toContain('rounded-[2px]');
+    expect(source).not.toContain('md:grid-cols-[180px_minmax(0,1fr)]');
+  });
+
+  it('supports the Angular vertical form layout used by token generation dialogs', () => {
+    const html = renderToStaticMarkup(
+      <SettingsDialogForm>
+        <SettingsDialogField label="令牌名称" required layout="vertical">
+          <SettingsDialogInput value="ops-token" readOnly />
+        </SettingsDialogField>
+        <SettingsDialogField label="过期时间" layout="vertical">
+          <SettingsDialogSelect defaultValue="-1" defaultOpen>
+            <option value="-1">永不过期</option>
+            <option value="604800">7 天</option>
+          </SettingsDialogSelect>
+        </SettingsDialogField>
+      </SettingsDialogForm>
+    );
+
+    expect(html).toContain('data-settings-dialog-field-layout="angular-vertical-form"');
+    expect(html).toContain('data-settings-dialog-label-span="vertical"');
+    expect(html).toContain('data-settings-dialog-control-span="vertical"');
+    expect(html).toContain('data-settings-dialog-control="cold-input-control"');
+    expect(html).toContain('data-settings-dialog-control="cold-select-control"');
+    expect(html).not.toContain('md:grid-cols-[7fr_12fr]');
   });
 });
diff --git a/web-next/components/settings/settings-dialog-form.tsx b/web-next/components/settings/settings-dialog-form.tsx
index 54be837da7..a034a888b6 100644
--- a/web-next/components/settings/settings-dialog-form.tsx
+++ b/web-next/components/settings/settings-dialog-form.tsx
@@ -21,20 +21,31 @@ export function SettingsDialogField({
   label,
   required,
   children,
-  className
+  className,
+  layout = 'horizontal'
 }: {
   label: string;
   required?: boolean;
   children: React.ReactNode;
   className?: string;
+  layout?: 'horizontal' | 'vertical';
 }) {
+  const isVertical = layout === 'vertical';
+
   return (
-    <label data-settings-dialog-field="cold-dialog-field" className={cn('grid gap-2 md:grid-cols-[180px_minmax(0,1fr)] md:items-center', className)}>
-      <span className="text-[12px] font-semibold text-[#dbe4f0]">
+    <label
+      data-settings-dialog-field="cold-dialog-field"
+      data-settings-dialog-field-layout={isVertical ? 'angular-vertical-form' : 'angular-label-7-control-12'}
+      className={cn(isVertical ? 'grid gap-1.5' : 'grid gap-2 md:grid-cols-[7fr_12fr] md:items-center', className)}
+    >
+      <span
+        data-settings-dialog-label-span={isVertical ? 'vertical' : '7'}
+        className="text-[12px] font-semibold text-[#dbe4f0]"
+      >
         {label}
         {required ? <span className="ml-1 text-[#8da2ff]">*</span> : null}
       </span>
-      <div className="min-w-0">{children}</div>
+      <div data-settings-dialog-control-span={isVertical ? 'vertical' : '12'} className="min-w-0">{children}</div>
     </label>
   );
 }
diff --git a/web-next/components/settings/settings-form.test.tsx b/web-next/components/settings/settings-form.test.tsx
index 3049bc29be..d622c8e9c2 100644
--- a/web-next/components/settings/settings-form.test.tsx
+++ b/web-next/components/settings/settings-form.test.tsx
@@ -34,6 +34,9 @@ describe('settings form controls', () => {
     expect(html).toContain('data-settings-form-field="cold-form-field"');
     expect(html).toContain('data-settings-form-control="cold-input-control"');
     expect(html).toContain('data-settings-form-control="cold-select-control"');
+    expect(html).toContain('data-settings-form-select-width="angular-400px"');
+    expect(html).toContain('data-settings-form-select-style="angular-centered-bold"');
+    expect(html).toContain('data-settings-form-select-dropdown-style="angular-bold-larger"');
     expect(html).toContain('data-cold-select-owner="cold-custom-select"');
     expect(html).toContain('data-cold-select-icon="chevron"');
     expect(html).toContain('appearance-none');
@@ -54,6 +57,9 @@ describe('settings form controls', () => {
     expect(source).toContain('data-settings-form-owner="cold-settings-form-owner"');
     expect(source).toContain('data-settings-form-control="cold-input-control"');
     expect(source).toContain('data-settings-form-control="cold-select-control"');
+    expect(source).toContain('data-settings-form-select-width="angular-400px"');
+    expect(source).toContain('data-settings-form-select-style="angular-centered-bold"');
+    expect(source).toContain('data-settings-form-select-dropdown-style="angular-bold-larger"');
     expect(source).toContain("from '../ui/select'");
     expect(source).not.toContain('ToolbarInput');
     expect(source).not.toContain('ToolbarNativeSelect');
diff --git a/web-next/components/settings/settings-form.tsx b/web-next/components/settings/settings-form.tsx
index a98c44bec5..eaec358eb8 100644
--- a/web-next/components/settings/settings-form.tsx
+++ b/web-next/components/settings/settings-form.tsx
@@ -62,6 +62,9 @@ export const SettingsFormSelect = React.forwardRef<HTMLSelectElement, React.Sele
     <Select
       ref={ref}
       data-settings-form-control="cold-select-control"
+      data-settings-form-select-width="angular-400px"
+      data-settings-form-select-style="angular-centered-bold"
+      data-settings-form-select-dropdown-style="angular-bold-larger"
       containerClassName="w-full max-w-[400px]"
       className={cn(
         'max-w-[400px] text-center [text-align-last:center]',
diff --git a/web-next/components/settings/settings-summary-list.test.tsx b/web-next/components/settings/settings-summary-list.test.tsx
index ea0a8ad22e..0e53291f43 100644
--- a/web-next/components/settings/settings-summary-list.test.tsx
+++ b/web-next/components/settings/settings-summary-list.test.tsx
@@ -13,6 +13,7 @@ describe('SettingsSummaryList', () => {
             title: '邮件服务器',
             lines: ['邮箱服务器地址: -', '邮箱账号: -'],
             actionLabel: '配置',
+            actionAriaLabel: '配置邮件服务器',
             onAction: vi.fn()
           },
           {
@@ -20,6 +21,7 @@ describe('SettingsSummaryList', () => {
             title: '短信配置',
             lines: ['短信类型: 腾讯短信', '启用状态: 否'],
             actionLabel: '配置',
+            actionAriaLabel: '配置短信配置',
             onAction: vi.fn()
           }
         ]}
@@ -35,6 +37,8 @@ describe('SettingsSummaryList', () => {
     expect(html).toContain('data-settings-summary-action="email"');
     expect(html).toContain('data-settings-summary-action="sms"');
     expect(html).toContain('data-settings-summary-action-style="cold-compact-action"');
+    expect(html).toContain('aria-label="配置邮件服务器"');
+    expect(html).toContain('aria-label="配置短信配置"');
     expect(html).toContain('rounded-[4px]');
     expect(html).toContain('border-[#2b3039]');
     expect(html).toContain('bg-[#0b0c0e]');
diff --git a/web-next/components/settings/settings-summary-list.tsx b/web-next/components/settings/settings-summary-list.tsx
index 1021d76b37..b6340efce7 100644
--- a/web-next/components/settings/settings-summary-list.tsx
+++ b/web-next/components/settings/settings-summary-list.tsx
@@ -9,6 +9,7 @@ export type SettingsSummaryItem = {
   title: React.ReactNode;
   lines: React.ReactNode[];
   actionLabel: string;
+  actionAriaLabel?: string;
   onAction: () => void;
 };
 
@@ -53,6 +54,7 @@ export function SettingsSummaryList({
               data-settings-summary-action={item.key}
               data-settings-summary-action-style="cold-compact-action"
               variant="default"
+              aria-label={item.actionAriaLabel}
               className="h-8 min-w-[72px] rounded-[3px] border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151923] hover:text-[#f8fafc]"
               onClick={item.onAction}
             >
diff --git a/web-next/lib/object-store/view-model.test.ts b/web-next/lib/object-store/view-model.test.ts
index 6721ca82b8..6f850d2f92 100644
--- a/web-next/lib/object-store/view-model.test.ts
+++ b/web-next/lib/object-store/view-model.test.ts
@@ -31,6 +31,34 @@ describe('object store view model', () => {
     ]);
   });
 
+  it('uses localized empty fallbacks for missing object store facts', () => {
+    const scopedT = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'common.workspace': '工作区',
+        'common.none': '无对象存储值',
+        'setting.object-store.fact.type': '类型',
+        'setting.object-store.fact.bucket': 'Bucket',
+        'setting.object-store.fact.endpoint': 'Endpoint'
+      }
+    });
+
+    expect(
+      buildObjectStoreFacts(
+        {
+          type: '',
+          config: { bucketName: '   ', endpoint: null }
+        } as any,
+        scopedT
+      )
+    ).toEqual([
+      { label: '工作区', value: 'setting/settings/object-store' },
+      { label: '类型', value: '无对象存储值' },
+      { label: 'Bucket', value: '无对象存储值' },
+      { label: 'Endpoint', value: '无对象存储值' }
+    ]);
+  });
+
   it('updates nested config fields immutably', () => {
     expect(
       updateObjectStoreField(
diff --git a/web-next/lib/object-store/view-model.ts b/web-next/lib/object-store/view-model.ts
index a40f0b8ab6..71903b260e 100644
--- a/web-next/lib/object-store/view-model.ts
+++ b/web-next/lib/object-store/view-model.ts
@@ -15,13 +15,19 @@ export function normalizeObjectStoreType(type?: string | null) {
   return 'DATABASE';
 }
 
-export function buildObjectStoreFacts(config: ObjectStoreConfig, _t: Translator) {
+function formatObjectStoreFact(value: unknown, fallback: string) {
+  const text = value == null ? '' : String(value).trim();
+  return text || fallback;
+}
+
+export function buildObjectStoreFacts(config: ObjectStoreConfig, t: Translator) {
   const storeConfig = (config.config || {}) as Record<string, unknown>;
+  const emptyValue = t('common.none');
   return [
-    { label: 'Workspace', value: 'setting/settings/object-store' },
-    { label: 'Type', value: config.type || '-' },
-    { label: 'Bucket', value: String(storeConfig.bucketName || '-') },
-    { label: 'Endpoint', value: String(storeConfig.endpoint || '-') }
+    { label: t('common.workspace'), value: 'setting/settings/object-store' },
+    { label: t('setting.object-store.fact.type'), value: formatObjectStoreFact(config.type, emptyValue) },
+    { label: t('setting.object-store.fact.bucket'), value: formatObjectStoreFact(storeConfig.bucketName, emptyValue) },
+    { label: t('setting.object-store.fact.endpoint'), value: formatObjectStoreFact(storeConfig.endpoint, emptyValue) }
   ];
 }
 
diff --git a/web-next/lib/setting-config/controller.test.ts b/web-next/lib/setting-config/controller.test.ts
index 7d51fdc682..150cdf00b1 100644
--- a/web-next/lib/setting-config/controller.test.ts
+++ b/web-next/lib/setting-config/controller.test.ts
@@ -1,5 +1,11 @@
 import { describe, expect, it, vi } from 'vitest';
-import { applySystemConfigRuntime, loadSystemConfigData, persistSystemConfig, saveSystemConfig } from './controller';
+import {
+  applySystemConfigRuntime,
+  loadSystemConfigData,
+  normalizeSystemConfigRuntimeLocale,
+  persistSystemConfig,
+  saveSystemConfig
+} from './controller';
 
 describe('setting config controller', () => {
   it('loads config and timezone options together', async () => {
@@ -47,7 +53,7 @@ describe('setting config controller', () => {
       )
     ).resolves.toBe('Saved successfully');
 
-    expect(setLocale).toHaveBeenCalledWith('ja_JP');
+    expect(setLocale).toHaveBeenCalledWith('ja-JP');
     expect(applyTheme).toHaveBeenCalledWith('compact');
     expect(reload).toHaveBeenCalled();
   });
@@ -59,8 +65,15 @@ describe('setting config controller', () => {
 
     await applySystemConfigRuntime({ locale: 'pt_BR', theme: 'light-ops' } as any, { setLocale, applyTheme, reload });
 
-    expect(setLocale).toHaveBeenCalledWith('pt_BR');
+    expect(setLocale).toHaveBeenCalledWith('pt-BR');
     expect(applyTheme).toHaveBeenCalledWith('light-ops');
     expect(reload).toHaveBeenCalled();
   });
+
+  it('normalizes Angular system config locale values before runtime reload', () => {
+    expect(normalizeSystemConfigRuntimeLocale('zh_CN')).toBe('zh-CN');
+    expect(normalizeSystemConfigRuntimeLocale('en-US')).toBe('en-US');
+    expect(normalizeSystemConfigRuntimeLocale(' pt_BR ')).toBe('pt-BR');
+    expect(normalizeSystemConfigRuntimeLocale(null)).toBe('');
+  });
 });
diff --git a/web-next/lib/setting-config/controller.ts b/web-next/lib/setting-config/controller.ts
index 982d85bb50..80ee88ee33 100644
--- a/web-next/lib/setting-config/controller.ts
+++ b/web-next/lib/setting-config/controller.ts
@@ -8,6 +8,10 @@ type RuntimeHandlers = {
   reload?: () => void;
 };
 
+export function normalizeSystemConfigRuntimeLocale(locale?: string | null) {
+  return locale?.trim().replace('_', '-') ?? '';
+}
+
 export async function loadSystemConfigData(apiGet: ApiGetter) {
   const [config, timezones] = await Promise.all([
     apiGet<SystemConfig>('/config/system'),
@@ -22,8 +26,9 @@ export async function saveSystemConfig(apiPost: ApiPoster, config: SystemConfig)
 }
 
 export async function applySystemConfigRuntime(config: SystemConfig, handlers?: RuntimeHandlers) {
-  if (config.locale && handlers?.setLocale) {
-    await handlers.setLocale(config.locale);
+  const runtimeLocale = normalizeSystemConfigRuntimeLocale(config.locale);
+  if (runtimeLocale && handlers?.setLocale) {
+    await handlers.setLocale(runtimeLocale);
   }
   handlers?.applyTheme?.(config.theme);
   handlers?.reload?.();
diff --git a/web-next/lib/setting-config/view-model.test.ts b/web-next/lib/setting-config/view-model.test.ts
index bdf665535c..9d88abc852 100644
--- a/web-next/lib/setting-config/view-model.test.ts
+++ b/web-next/lib/setting-config/view-model.test.ts
@@ -25,12 +25,28 @@ describe('setting config view model', () => {
       )
     ).toEqual([
       { label: 'Workspace', value: 'setting/settings/config' },
-      { label: 'System Language', value: 'Simplified Chinese(zh_CN)' },
-      { label: 'System Theme', value: 'Dark Theme' },
-      { label: 'System Time Zone', value: 'Asia/Shanghai' }
+      { label: 'System language', value: 'Simplified Chinese(zh_CN)' },
+      { label: 'System theme', value: 'Dark theme' },
+      { label: 'System timezone', value: 'Asia/Shanghai' }
     ]);
   });
 
+  it('renders missing system config facts with the localized empty fallback', () => {
+    expect(
+      buildConfigFacts(
+        { locale: ' ', theme: 'dark', timeZoneId: '' } as any,
+        t
+      )
+    ).toEqual([
+      { label: 'Workspace', value: 'setting/settings/config' },
+      { label: 'System language', value: 'None' },
+      { label: 'System theme', value: 'Dark theme' },
+      { label: 'System timezone', value: 'None' }
+    ]);
+
+    expect(resolveSystemLocaleLabel('custom-locale', t)).toBe('custom-locale');
+  });
+
   it('updates config fields immutably', () => {
     expect(updateSystemConfigField({ locale: 'zh-CN' } as any, 'theme', 'dark')).toEqual({
       locale: 'zh-CN',
@@ -90,6 +106,6 @@ describe('setting config view model', () => {
 
   it('resolves translated locale and theme labels for supported values', () => {
     expect(resolveSystemLocaleLabel('pt-BR', t)).toBe('Portuguese(pt_BR)');
-    expect(resolveSystemThemeLabel('default', t)).toBe('Default Theme');
+    expect(resolveSystemThemeLabel('default', t)).toBe('Default theme');
   });
 });
diff --git a/web-next/lib/setting-config/view-model.ts b/web-next/lib/setting-config/view-model.ts
index 1ed8c5890e..49994a39c5 100644
--- a/web-next/lib/setting-config/view-model.ts
+++ b/web-next/lib/setting-config/view-model.ts
@@ -17,6 +17,11 @@ export const SYSTEM_CONFIG_THEME_OPTIONS = [
   { value: 'compact', labelKey: 'settings.system-config.theme.compact' }
 ] as const;
 
+function formatSystemConfigFact(value: string | null | undefined, emptyValue: string) {
+  const text = value?.trim() ?? '';
+  return text || emptyValue;
+}
+
 export function normalizeSystemConfigLocale(locale?: string | null) {
   if (!locale) return '';
   return locale
@@ -34,10 +39,10 @@ export function normalizeSystemConfigTheme(theme?: string | null) {
   return 'dark-ops';
 }
 
-export function resolveSystemLocaleLabel(locale: string | null | undefined, t: Translator) {
+export function resolveSystemLocaleLabel(locale: string | null | undefined, t: Translator, emptyValue = t('common.none')) {
   const normalizedLocale = normalizeSystemConfigLocale(locale);
   const option = SYSTEM_CONFIG_LOCALE_OPTIONS.find(item => item.value === normalizedLocale);
-  return option ? t(option.labelKey) : locale || '-';
+  return option ? t(option.labelKey) : formatSystemConfigFact(locale, emptyValue);
 }
 
 export function resolveSystemThemeLabel(theme: string | null | undefined, t: Translator) {
@@ -47,11 +52,13 @@ export function resolveSystemThemeLabel(theme: string | null | undefined, t: Tra
 }
 
 export function buildConfigFacts(config: SystemConfig, t: Translator) {
+  const emptyValue = t('common.none');
+
   return [
     { label: t('common.workspace'), value: 'setting/settings/config' },
-    { label: t('settings.system-config.locale'), value: resolveSystemLocaleLabel(config.locale, t) },
+    { label: t('settings.system-config.locale'), value: resolveSystemLocaleLabel(config.locale, t, emptyValue) },
     { label: t('settings.system-config.theme'), value: resolveSystemThemeLabel(config.theme, t) },
-    { label: t('settings.system-config.timezone'), value: config.timeZoneId || '-' }
+    { label: t('settings.system-config.timezone'), value: formatSystemConfigFact(config.timeZoneId, emptyValue) }
   ];
 }
 
diff --git a/web-next/lib/setting-server/view-model.test.ts b/web-next/lib/setting-server/view-model.test.ts
index 95af5af238..d4b20ed381 100644
--- a/web-next/lib/setting-server/view-model.test.ts
+++ b/web-next/lib/setting-server/view-model.test.ts
@@ -12,6 +12,7 @@ import {
   normalizeSmsSender,
   resolveBooleanText,
   resolveSmsTypeLabel,
+  resolveUniSmsAuthModeLabel,
   updateSmsProviderField,
   updateSmsType
 } from './view-model';
@@ -63,29 +64,86 @@ describe('setting server view model', () => {
     ).toEqual([
       {
         key: 'email',
-        title: 'Email Server',
+        title: 'Email server',
         lines: [
-          'Email Server Address: smtp.example.com',
-          'Email Account: ops',
-          'Email Port: 587',
+          'Email server address: smtp.example.com',
+          'Email account: ops',
+          'Email port: 587',
           'Enable SSL: Yes',
           'Enable STARTTLS: No',
-          'Enable Email Configuration: Yes'
+          'Enable email configuration: Yes'
         ]
       },
       {
         key: 'sms',
-        title: 'SMS Server',
+        title: 'SMS settings',
         lines: [
-          'Sms Type: Twilio Sms',
-          'Twilio Account SID: AC-1',
-          'Twilio Phone Number: +86-10000',
+          'SMS type: Twilio Sms',
+          'Twilio account SID: AC-1',
+          'Twilio phone number: +86-10000',
           'Enable: No'
         ]
       }
     ]);
   });
 
+  it('uses localized empty fallbacks for missing summary facts', () => {
+    const scopedT = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'common.none': '无消息配置值',
+        'common.enable': '启用',
+        'common.yes': '是',
+        'common.no': '否',
+        'settings.server.email': '邮件服务',
+        'settings.server.sms': '短信服务',
+        'alert.notice.sender.mail.host': '邮件服务器地址',
+        'alert.notice.sender.mail.username': '邮件账户',
+        'alert.notice.sender.mail.port': '邮件端口',
+        'alert.notice.sender.mail.ssl': '启用 SSL',
+        'alert.notice.sender.mail.starttls': '启用 STARTTLS',
+        'alert.notice.sender.mail.enable': '启用邮件配置',
+        'alert.notice.sender.sms.type': '短信类型',
+        'alert.notice.sender.sms.type.tencent': '腾讯云短信',
+        'alert.notice.sender.sms.tencent.appId': '腾讯云 App ID',
+        'alert.notice.sender.sms.tencent.signName': '短信签名',
+        'alert.notice.sender.sms.tencent.templateId': '短信模板'
+      }
+    });
+
+    expect(
+      buildMessageServerSummaryItems(
+        { emailHost: '', emailUsername: '  ', emailPort: undefined, emailSsl: true, emailStarttls: false, enable: false } as any,
+        { type: 'tencent', enable: false, tencent: { appId: '', signName: ' ', templateId: null } } as any,
+        scopedT
+      )
+    ).toEqual([
+      {
+        key: 'email',
+        title: '邮件服务',
+        lines: [
+          '邮件服务器地址: 无消息配置值',
+          '邮件账户: 无消息配置值',
+          '邮件端口: 无消息配置值',
+          '启用 SSL: 是',
+          '启用 STARTTLS: 否',
+          '启用邮件配置: 否'
+        ]
+      },
+      {
+        key: 'sms',
+        title: '短信服务',
+        lines: [
+          '短信类型: 腾讯云短信',
+          '腾讯云 App ID: 无消息配置值',
+          '短信签名: 无消息配置值',
+          '短信模板: 无消息配置值',
+          '启用: 否'
+        ]
+      }
+    ]);
+  });
+
   it('updates sms provider state immutably and keeps deep clones for cancel recovery', () => {
     const current = normalizeSmsSender({ type: 'tencent', tencent: { appId: 'old' } } as any);
 
@@ -134,7 +192,8 @@ describe('setting server view model', () => {
   it('resolves provider labels and auth-mode gating', () => {
     expect(normalizeSmsProviderType('aws')).toBe('aws');
     expect(normalizeSmsProviderType('aliyun')).toBe('tencent');
-    expect(resolveSmsTypeLabel('alibaba', t)).toBe('Alibaba Sms');
+    expect(resolveSmsTypeLabel('alibaba', t)).toBe('Alibaba SMS');
+    expect(resolveUniSmsAuthModeLabel('simple', t)).toBe('Simple');
     expect(resolveBooleanText(true, t)).toBe('Yes');
     expect(
       isUniSmsAccessKeySecretRequired({
diff --git a/web-next/lib/setting-server/view-model.ts b/web-next/lib/setting-server/view-model.ts
index 2108bb0e68..f984520af2 100644
--- a/web-next/lib/setting-server/view-model.ts
+++ b/web-next/lib/setting-server/view-model.ts
@@ -14,9 +14,9 @@ export const SMS_PROVIDER_OPTIONS: Array<{ value: SmsProviderType; labelKey: str
   { value: 'twilio', labelKey: 'alert.notice.sender.sms.type.twilio' }
 ] as const;
 
-export const UNISMS_AUTH_MODE_OPTIONS: Array<{ value: UniSmsAuthModeType; label: string }> = [
-  { value: 'hmac', label: 'HMAC' },
-  { value: 'simple', label: 'Simple' }
+export const UNISMS_AUTH_MODE_OPTIONS: Array<{ value: UniSmsAuthModeType; labelKey: string }> = [
+  { value: 'hmac', labelKey: 'alert.notice.sender.sms.unisms.authMode.hmac' },
+  { value: 'simple', labelKey: 'alert.notice.sender.sms.unisms.authMode.simple' }
 ] as const;
 
 function hasText(value: unknown) {
@@ -159,30 +159,53 @@ export function resolveSmsTypeLabel(type: string | null | undefined, t: Translat
   return t(`alert.notice.sender.sms.type.${normalized}`);
 }
 
-function buildSmsSummaryLines(sms: SmsNoticeSender, t: Translator) {
+export function resolveUniSmsAuthModeLabel(mode: string | null | undefined, t: Translator) {
+  const normalized = mode === 'simple' ? 'simple' : 'hmac';
+  return t(`alert.notice.sender.sms.unisms.authMode.${normalized}`);
+}
+
+function formatMessageServerFact(value: unknown, fallback: string) {
+  const text = value == null ? '' : String(value).trim();
+  return text || fallback;
+}
+
+function buildSmsSummaryLines(sms: SmsNoticeSender, t: Translator, emptyValue: string) {
   const lines = [`${t('alert.notice.sender.sms.type')}: ${resolveSmsTypeLabel(sms.type, t)}`];
 
   switch (normalizeSmsProviderType(sms.type)) {
-    case 'tencent':
-      lines.push(`${t('alert.notice.sender.sms.tencent.appId')}: ${String((sms.tencent as Record<string, unknown>)?.appId || '-')}`);
-      lines.push(`${t('alert.notice.sender.sms.tencent.signName')}: ${String((sms.tencent as Record<string, unknown>)?.signName || '-')}`);
-      lines.push(`${t('alert.notice.sender.sms.tencent.templateId')}: ${String((sms.tencent as Record<string, unknown>)?.templateId || '-')}`);
+    case 'tencent': {
+      const config = sms.tencent as Record<string, unknown>;
+      lines.push(`${t('alert.notice.sender.sms.tencent.appId')}: ${formatMessageServerFact(config?.appId, emptyValue)}`);
+      lines.push(`${t('alert.notice.sender.sms.tencent.signName')}: ${formatMessageServerFact(config?.signName, emptyValue)}`);
+      lines.push(`${t('alert.notice.sender.sms.tencent.templateId')}: ${formatMessageServerFact(config?.templateId, emptyValue)}`);
       break;
-    case 'alibaba':
-      lines.push(`${t('alert.notice.sender.sms.alibaba.signName')}: ${String((sms.alibaba as Record<string, unknown>)?.signName || '-')}`);
-      lines.push(`${t('alert.notice.sender.sms.alibaba.templateCode')}: ${String((sms.alibaba as Record<string, unknown>)?.templateCode || '-')}`);
+    }
+    case 'alibaba': {
+      const config = sms.alibaba as Record<string, unknown>;
+      lines.push(`${t('alert.notice.sender.sms.alibaba.signName')}: ${formatMessageServerFact(config?.signName, emptyValue)}`);
+      lines.push(`${t('alert.notice.sender.sms.alibaba.templateCode')}: ${formatMessageServerFact(config?.templateCode, emptyValue)}`);
       break;
-    case 'unisms':
-      lines.push(`${t('alert.notice.sender.sms.unisms.signature')}: ${String((sms.unisms as Record<string, unknown>)?.signature || '-')}`);
-      lines.push(`${t('alert.notice.sender.sms.unisms.templateId')}: ${String((sms.unisms as Record<string, unknown>)?.templateId || '-')}`);
-      lines.push(`${t('alert.notice.sender.sms.unisms.authMode')}: ${String((sms.unisms as Record<string, unknown>)?.authMode || '-')}`);
+    }
+    case 'unisms': {
+      const config = sms.unisms as Record<string, unknown>;
+      lines.push(`${t('alert.notice.sender.sms.unisms.signature')}: ${formatMessageServerFact(config?.signature, emptyValue)}`);
+      lines.push(`${t('alert.notice.sender.sms.unisms.templateId')}: ${formatMessageServerFact(config?.templateId, emptyValue)}`);
+      lines.push(
+        `${t('alert.notice.sender.sms.unisms.authMode')}: ${resolveUniSmsAuthModeLabel(
+          String(config?.authMode || 'hmac'),
+          t
+        )}`
+      );
       break;
-    case 'twilio':
-      lines.push(`${t('alert.notice.sender.sms.twilio.accountSid')}: ${String((sms.twilio as Record<string, unknown>)?.accountSid || '-')}`);
+    }
+    case 'twilio': {
+      const config = sms.twilio as Record<string, unknown>;
+      lines.push(`${t('alert.notice.sender.sms.twilio.accountSid')}: ${formatMessageServerFact(config?.accountSid, emptyValue)}`);
       lines.push(
-        `${t('alert.notice.sender.sms.twilio.twilioPhoneNumber')}: ${String((sms.twilio as Record<string, unknown>)?.twilioPhoneNumber || '-')}`
+        `${t('alert.notice.sender.sms.twilio.twilioPhoneNumber')}: ${formatMessageServerFact(config?.twilioPhoneNumber, emptyValue)}`
       );
       break;
+    }
     default:
       break;
   }
@@ -194,15 +217,16 @@ function buildSmsSummaryLines(sms: SmsNoticeSender, t: Translator) {
 export function buildMessageServerSummaryItems(email: EmailNoticeSender, sms: SmsNoticeSender, t: Translator) {
   const resolvedEmail = normalizeEmailSender(email);
   const resolvedSms = normalizeSmsSender(sms);
+  const emptyValue = t('common.none');
 
   return [
     {
       key: 'email',
       title: t('settings.server.email'),
       lines: [
-        `${t('alert.notice.sender.mail.host')}: ${resolvedEmail.emailHost || '-'}`,
-        `${t('alert.notice.sender.mail.username')}: ${resolvedEmail.emailUsername || '-'}`,
-        `${t('alert.notice.sender.mail.port')}: ${resolvedEmail.emailPort ?? '-'}`,
+        `${t('alert.notice.sender.mail.host')}: ${formatMessageServerFact(resolvedEmail.emailHost, emptyValue)}`,
+        `${t('alert.notice.sender.mail.username')}: ${formatMessageServerFact(resolvedEmail.emailUsername, emptyValue)}`,
+        `${t('alert.notice.sender.mail.port')}: ${formatMessageServerFact(resolvedEmail.emailPort, emptyValue)}`,
         `${t('alert.notice.sender.mail.ssl')}: ${resolveBooleanText(Boolean(resolvedEmail.emailSsl), t)}`,
         `${t('alert.notice.sender.mail.starttls')}: ${resolveBooleanText(Boolean(resolvedEmail.emailStarttls), t)}`,
         `${t('alert.notice.sender.mail.enable')}: ${resolveBooleanText(Boolean(resolvedEmail.enable), t)}`
@@ -211,7 +235,7 @@ export function buildMessageServerSummaryItems(email: EmailNoticeSender, sms: Sm
     {
       key: 'sms',
       title: t('settings.server.sms'),
-      lines: buildSmsSummaryLines(resolvedSms, t)
+      lines: buildSmsSummaryLines(resolvedSms, t, emptyValue)
     }
   ];
 }
diff --git a/web-next/lib/setting-settings-layout/navigation.ts b/web-next/lib/setting-settings-layout/navigation.ts
new file mode 100644
index 0000000000..1926830549
--- /dev/null
+++ b/web-next/lib/setting-settings-layout/navigation.ts
@@ -0,0 +1,9 @@
+import { buildCompatRedirectTarget, type SearchParamsRecord } from '../compat/search-params';
+
+export type { SearchParamsRecord } from '../compat/search-params';
+
+export const SETTING_CONFIG_ROUTE = '/setting/settings/config';
+
+export function buildSettingsCompatRouteUrl(searchParams?: SearchParamsRecord) {
+  return buildCompatRedirectTarget(SETTING_CONFIG_ROUTE, searchParams);
+}
diff --git a/web-next/lib/setting-settings-layout/view-model.test.ts b/web-next/lib/setting-settings-layout/view-model.test.ts
index 3226f73496..ad4202caf9 100644
--- a/web-next/lib/setting-settings-layout/view-model.test.ts
+++ b/web-next/lib/setting-settings-layout/view-model.test.ts
@@ -1,4 +1,6 @@
-import { describe, expect, it, vi } from 'vitest';
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it } from 'vitest';
 import { buildSettingsMenuRows } from './view-model';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
@@ -6,11 +8,22 @@ const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('setting settings layout view model', () => {
   it('builds settings menu rows', () => {
-    expect(buildSettingsMenuRows(t).map(row => ({ key: row.key, title: row.title, copy: row.copy, meta: row.meta }))).toEqual([
+    const rows = buildSettingsMenuRows(t);
+
+    expect(rows.map(row => ({ key: row.key, title: row.title, copy: row.copy, meta: row.meta }))).toEqual([
       { key: '/setting/settings/config', title: '系统配置', copy: '系统、存储、消息和令牌设置', meta: '/setting/settings/config' },
       { key: '/setting/settings/server', title: '消息服务配置', copy: '系统、存储、消息和令牌设置', meta: '/setting/settings/server' },
       { key: '/setting/settings/object-store', title: '文件服务配置', copy: '系统、存储、消息和令牌设置', meta: '/setting/settings/object-store' },
       { key: '/setting/settings/token', title: '令牌管理', copy: '系统、存储、消息和令牌设置', meta: '/setting/settings/token' }
     ]);
+
+    const firstAction = renderToStaticMarkup(React.createElement(React.Fragment, null, rows[0]?.extra));
+
+    expect(firstAction).toContain('打开');
+    expect(firstAction).toContain('aria-label="打开系统配置"');
+    expect(renderToStaticMarkup(React.createElement(React.Fragment, null, rows[3]?.extra))).toContain(
+      'aria-label="打开令牌管理"'
+    );
+    expect(firstAction).not.toContain('common.open');
   });
 });
diff --git a/web-next/lib/setting-settings-layout/view-model.tsx b/web-next/lib/setting-settings-layout/view-model.tsx
index f133fa0c0c..1908863d60 100644
--- a/web-next/lib/setting-settings-layout/view-model.tsx
+++ b/web-next/lib/setting-settings-layout/view-model.tsx
@@ -24,7 +24,11 @@ export function buildSettingsMenuRows(t: Translator): Array<{
     copy: t('setting.settings.menu.copy'),
     meta: item.href,
     extra: (
-      <Link href={item.href} className="text-xs uppercase tracking-[0.16em] text-white/44">
+      <Link
+        href={item.href}
+        aria-label={t('setting.settings.menu.open-action', { title: item.label })}
+        className="text-xs uppercase tracking-[0.16em] text-white/44"
+      >
         {t('common.open')}
       </Link>
     )
diff --git a/web-next/lib/setting-status/view-model.test.ts b/web-next/lib/setting-status/view-model.test.ts
index 7105ec52f8..ab328c7614 100644
--- a/web-next/lib/setting-status/view-model.test.ts
+++ b/web-next/lib/setting-status/view-model.test.ts
@@ -1,5 +1,24 @@
-import { describe, expect, it } from 'vitest';
-import { buildStatusComponentDraft, buildStatusComponentEvidenceRows, buildStatusComponentPayload, buildStatusFacts, buildStatusIncidentDraft, buildStatusIncidentEvidenceRows, buildStatusIncidentPayload, buildStatusMetrics, buildStatusOrgDraft, buildStatusOrgOverviewRows, buildStatusOrgPayload, buildStatusRows, stateLabel, validateStatusComponentDraft, validateStatusIncidentDraft, validateStatusOrgDraft } from './view-model';
+import { describe, expect, it, vi } from 'vitest';
+import {
+  buildStatusComponentDraft,
+  buildStatusComponentEvidenceRows,
+  buildStatusComponentPayload,
+  buildStatusFacts,
+  buildStatusIncidentDraft,
+  buildStatusIncidentEvidenceRows,
+  buildStatusIncidentPayload,
+  buildStatusMetrics,
+  buildStatusOrgDraft,
+  buildStatusOrgOverviewRows,
+  buildStatusOrgPayload,
+  buildStatusRows,
+  settingStatusComponentTagTone,
+  settingStatusIncidentTagTone,
+  stateLabel,
+  validateStatusComponentDraft,
+  validateStatusIncidentDraft,
+  validateStatusOrgDraft
+} from './view-model';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 const t = createTranslatorMock({
@@ -21,7 +40,12 @@ const zhT = createTranslatorMock({
     'setting.status.title': '状态页设置',
     'setting.status.org.title': '组织档案',
     'setting.status.components.title': '组件',
+    'setting.status.components.empty.title': '暂无组件',
+    'setting.status.components.empty.copy': '还没有公开状态组件。',
     'setting.status.incidents.title': '事件',
+    'setting.status.incidents.empty.title': '暂无事件',
+    'setting.status.incidents.empty.copy': '还没有维护事件。',
+    'setting.status.incidents.item.fallback': '事件',
     'setting.status.org.state': '组织状态',
     'setting.status.org.copy': '配置公开状态页展示的组织信息。',
     'setting.status.org.fallback': '未命名状态组织',
@@ -31,11 +55,23 @@ const zhT = createTranslatorMock({
 
 describe('setting status view model', () => {
   it('maps state labels', () => {
-    expect(stateLabel(0)).toBe('Normal');
-    expect(stateLabel(1)).toBe('Abnormal');
-    expect(stateLabel(2)).toBe('Unknown');
+    expect(stateLabel(0, t)).toBe('Normal');
+    expect(stateLabel(1, t)).toBe('Abnormal');
+    expect(stateLabel(2, t)).toBe('Unknown');
     expect(stateLabel(0, zhT)).toBe('正常');
-    expect(stateLabel(undefined)).toBe('-');
+    expect(stateLabel(undefined, t)).toBe('-');
+  });
+
+  it('maps admin status tag tones from normalized state values', () => {
+    expect(settingStatusComponentTagTone(0)).toBe('ok');
+    expect(settingStatusComponentTagTone('degraded')).toBe('bad');
+    expect(settingStatusComponentTagTone(undefined)).toBe('neutral');
+
+    expect(settingStatusIncidentTagTone('investigating')).toBe('bad');
+    expect(settingStatusIncidentTagTone(1)).toBe('warn');
+    expect(settingStatusIncidentTagTone(2)).toBe('warn');
+    expect(settingStatusIncidentTagTone('resolved')).toBe('ok');
+    expect(settingStatusIncidentTagTone(undefined)).toBe('neutral');
   });
 
   it('builds status facts', () => {
@@ -69,6 +105,46 @@ describe('setting status view model', () => {
     ]);
   });
 
+  it('uses runtime none fallback for setting status component rows without descriptions', () => {
+    expect(
+      buildStatusRows(
+        'component',
+        [{ name: 'API', state: 1, gmtUpdate: 1712730000000 }] as any,
+        { content: [] } as any,
+        zhT,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      { title: 'API', copy: '无', meta: '异常 · 2026-04-10 18:00:00' }
+    ]);
+  });
+
+  it('uses runtime none fallback for setting status empty row meta', () => {
+    expect(
+      buildStatusRows(
+        'component',
+        [],
+        { content: [] } as any,
+        zhT,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      { title: '暂无组件', copy: '还没有公开状态组件。', meta: '无' }
+    ]);
+
+    expect(
+      buildStatusRows(
+        'incident',
+        [],
+        { content: [] } as any,
+        zhT,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      { title: '暂无事件', copy: '还没有维护事件。', meta: '无' }
+    ]);
+  });
+
   it('builds selectable component evidence rows', () => {
     expect(
       buildStatusComponentEvidenceRows(
@@ -81,6 +157,18 @@ describe('setting status view model', () => {
     ]);
   });
 
+  it('uses runtime none fallback for setting status component evidence without descriptions', () => {
+    expect(
+      buildStatusComponentEvidenceRows(
+        [{ id: 8, name: 'Worker', state: 0, gmtUpdate: 1712730000000 }] as any,
+        zhT,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      { key: '8', title: 'Worker', copy: '无', meta: '正常 · 2026-04-10 18:00:00' }
+    ]);
+  });
+
   it('builds incident evidence rows and payloads', () => {
     expect(
       buildStatusIncidentEvidenceRows(
@@ -114,7 +202,10 @@ describe('setting status view model', () => {
         name: 'API degraded',
         state: 1,
         components: [{ id: 7 }, { id: 8 }],
-        contents: [{ message: 'Investigating', state: 1, timestamp: 10 }]
+        contents: [
+          { message: 'Investigating', state: 1, timestamp: 10 },
+          { message: 'Monitoring mitigation', state: 2, timestamp: 30 }
+        ]
       } as any)
     ).toEqual({
       id: 9,
@@ -122,8 +213,11 @@ describe('setting status view model', () => {
       name: 'API degraded',
       state: '1',
       componentIdsText: '7, 8',
-      message: 'Investigating',
-      existingContents: [{ message: 'Investigating', state: 1, timestamp: 10 }],
+      message: 'Monitoring mitigation',
+      existingContents: [
+        { message: 'Monitoring mitigation', state: 2, timestamp: 30 },
+        { message: 'Investigating', state: 1, timestamp: 10 }
+      ],
       startTime: null,
       endTime: null
     });
@@ -151,6 +245,91 @@ describe('setting status view model', () => {
     expect(validateStatusIncidentDraft({ name: 'API degraded', state: '0', componentIdsText: '7', message: 'Investigating' }, t)).toBeNull();
   });
 
+  it('appends status incident content with the selected state and incident id', () => {
+    const now = 1779753000000;
+    const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(now);
+
+    try {
+      const payload = buildStatusIncidentPayload(
+        {
+          id: 9,
+          orgId: 1,
+          name: 'API degraded',
+          state: '3',
+          componentIdsText: '7, 8',
+          message: 'Resolved after rollback',
+          existingContents: [{ id: 8, incidentId: 9, message: 'Identified bad deploy', state: 1, timestamp: 1000 }],
+          startTime: 500,
+          endTime: null
+        },
+        [
+          { id: 7, name: 'API' },
+          { id: 8, name: 'Checkout' },
+          { id: 12, name: 'Database' }
+        ] as any
+      );
+
+      expect(payload).toMatchObject({
+        id: 9,
+        orgId: 1,
+        name: 'API degraded',
+        state: 3,
+        startTime: 500,
+        endTime: now,
+        components: [
+          { id: 7, name: 'API' },
+          { id: 8, name: 'Checkout' }
+        ],
+        contents: [
+          { id: 8, incidentId: 9, message: 'Identified bad deploy', state: 1, timestamp: 1000 },
+          { incidentId: 9, message: 'Resolved after rollback', state: 3, timestamp: now }
+        ]
+      });
+    } finally {
+      nowSpy.mockRestore();
+    }
+  });
+
+  it('uses runtime none fallback for setting status incident titles without id', () => {
+    const incidentWithoutIdentity = {
+      state: 1,
+      components: [],
+      contents: [{ message: '正在排查', state: 1, timestamp: 10 }],
+      gmtUpdate: 1712730000000
+    };
+
+    expect(
+      buildStatusRows(
+        'incident',
+        [],
+        { content: [incidentWithoutIdentity] } as any,
+        zhT,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      {
+        title: '事件 无',
+        copy: '正在排查',
+        meta: '已确认 · 组件 0 · 2026-04-10 18:00:00'
+      }
+    ]);
+
+    expect(
+      buildStatusIncidentEvidenceRows(
+        { content: [incidentWithoutIdentity], totalElements: 1 } as any,
+        zhT,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      {
+        key: 'incident',
+        title: '事件 无',
+        copy: '正在排查',
+        meta: '已确认 · 组件 0 · 2026-04-10 18:00:00'
+      }
+    ]);
+  });
+
   it('builds localized org overview rows and zh-CN summaries', () => {
     expect(
       buildStatusOrgOverviewRows(
@@ -217,6 +396,34 @@ describe('setting status view model', () => {
     ]);
   });
 
+  it('uses runtime none fallback for setting status org overview links', () => {
+    expect(
+      buildStatusOrgOverviewRows(
+        {
+          name: 'HB Status',
+          description: 'Public status overview',
+          state: 0,
+          home: '',
+          feedback: '   ',
+          gmtUpdate: 1712730000000
+        } as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      {
+        title: 'HB Status',
+        copy: 'Public status overview',
+        meta: 'All Systems Operational'
+      },
+      {
+        title: 'Feedback',
+        copy: 'None · None',
+        meta: '2026-04-10 18:00:00'
+      }
+    ]);
+  });
+
   it('builds and validates the status org draft', () => {
     expect(buildStatusOrgDraft({ id: 1, name: 'HB', state: 1, description: 'desc', home: 'https://home', feedback: 'https://feedback', logo: 'https://logo', color: '#fff' } as any)).toEqual({
       id: 1,
diff --git a/web-next/lib/setting-status/view-model.ts b/web-next/lib/setting-status/view-model.ts
index ec3ffcb7dd..bb71d9ad41 100644
--- a/web-next/lib/setting-status/view-model.ts
+++ b/web-next/lib/setting-status/view-model.ts
@@ -4,8 +4,8 @@ import {
   incidentStateLabel,
   latestIncidentMessage,
   normalizeComponentState,
+  normalizeIncidentState,
   orgStateLabel,
-  readStatusCopy,
   statusComponentsCountLabel,
   statusComponentsLabel,
   statusFieldLabel,
@@ -19,6 +19,8 @@ import {
 } from '../status-center/display';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
+export type StatusSettingTagTone = 'ok' | 'bad' | 'warn' | 'neutral';
+
 export type StatusOrgDraft = {
   id?: number;
   name: string;
@@ -50,6 +52,7 @@ export type StatusIncidentDraft = {
   message: string;
   existingContents?: Array<{
     id?: number;
+    incidentId?: number;
     message?: string;
     state?: number;
     timestamp?: number | null;
@@ -58,20 +61,28 @@ export type StatusIncidentDraft = {
   endTime?: number | null;
 };
 
-export function stateLabel(state?: number | string | null, t?: Translator): string {
-  if (t) {
-    return componentStateLabel(state, t);
-  }
+export function stateLabel(state: number | string | null | undefined, t: Translator): string {
+  return componentStateLabel(state, t);
+}
+
+export function settingStatusComponentTagTone(state?: number | string | null): StatusSettingTagTone {
   const normalizedState = normalizeComponentState(state);
-  if (normalizedState === 0) return 'Normal';
-  if (normalizedState === 1) return 'Abnormal';
-  if (normalizedState === 2) return 'Unknown';
-  return '-';
+  if (normalizedState === 0) return 'ok';
+  if (normalizedState === 1) return 'bad';
+  return 'neutral';
+}
+
+export function settingStatusIncidentTagTone(state?: number | string | null): StatusSettingTagTone {
+  const normalizedState = normalizeIncidentState(state);
+  if (normalizedState === 0) return 'bad';
+  if (normalizedState === 1 || normalizedState === 2) return 'warn';
+  if (normalizedState === 3) return 'ok';
+  return 'neutral';
 }
 
 export function buildStatusFacts(org: StatusPageOrg, components: StatusPageComponent[], incidents: PageResult<StatusPageIncident>, t: Translator) {
   return [
-    { label: readStatusCopy(t, 'common.workspace', 'Workspace'), value: statusSettingLabel(t) },
+    { label: t('common.workspace'), value: statusSettingLabel(t) },
     { label: statusOrganizationLabel(t), value: org.name || statusOrganizationFallback(t) },
     { label: statusComponentsCountLabel(t), value: String(components.length) },
     { label: statusIncidentsCountLabel(t), value: String(incidents.totalElements || 0) }
@@ -86,6 +97,11 @@ export function buildStatusMetrics(components: StatusPageComponent[], incidents:
   ];
 }
 
+function statusIncidentFallbackTitle(item: StatusPageIncident, t: Translator) {
+  const incidentId = item.id ?? t('common.none');
+  return item.name || `${t('setting.status.incidents.item.fallback')} ${incidentId}`;
+}
+
 export function buildStatusRows(
   mode: 'component' | 'incident',
   components: StatusPageComponent[],
@@ -93,23 +109,25 @@ export function buildStatusRows(
   t: Translator,
   formatTime: (value?: number | string | null) => string
 ) {
+  const emptyValue = t('common.none');
+
   if (mode === 'component') {
     return components.length > 0
       ? components.map(item => ({
           title: item.name || t('setting.status.components.item.fallback'),
-          copy: item.description || '-',
+          copy: item.description || emptyValue,
           meta: `${stateLabel(item.state, t)} · ${formatTime(item.gmtUpdate || item.gmtCreate || null)}`
         }))
-      : [{ title: t('setting.status.components.empty.title'), copy: t('setting.status.components.empty.copy'), meta: '-' }];
+      : [{ title: t('setting.status.components.empty.title'), copy: t('setting.status.components.empty.copy'), meta: emptyValue }];
   }
 
   return incidents.content.length > 0
     ? incidents.content.map(item => ({
-        title: item.name || `${t('setting.status.incidents.item.fallback')} ${item.id || '-'}`,
+        title: statusIncidentFallbackTitle(item, t),
         copy: latestIncidentMessage(item) || `${statusFieldLabel(t)} ${incidentStateLabel(item.state ?? item.status ?? null, t)}`,
         meta: `${incidentStateLabel(item.state ?? item.status ?? null, t)} · ${statusComponentsLabel(t)} ${(item.components || []).length} · ${formatTime(item.gmtUpdate || item.gmtCreate || null)}`
       }))
-    : [{ title: t('setting.status.incidents.empty.title'), copy: t('setting.status.incidents.empty.copy'), meta: '-' }];
+    : [{ title: t('setting.status.incidents.empty.title'), copy: t('setting.status.incidents.empty.copy'), meta: emptyValue }];
 }
 
 export function buildStatusIncidentEvidenceRows(
@@ -119,7 +137,7 @@ export function buildStatusIncidentEvidenceRows(
 ) {
   return incidents.content.map(item => ({
     key: String(item.id || item.name || 'incident'),
-    title: item.name || `${t('setting.status.incidents.item.fallback')} ${item.id || '-'}`,
+    title: statusIncidentFallbackTitle(item, t),
     copy: latestIncidentMessage(item) || `${statusFieldLabel(t)} ${incidentStateLabel(item.state ?? item.status ?? null, t)}`,
     meta: `${incidentStateLabel(item.state ?? item.status ?? null, t)} · ${statusComponentsLabel(t)} ${(item.components || []).length} · ${formatTime(item.gmtUpdate || item.gmtCreate || null)}`
   }));
@@ -130,19 +148,28 @@ export function buildStatusComponentEvidenceRows(
   t: Translator,
   formatTime: (value?: number | string | null) => string
 ) {
+  const emptyValue = t('common.none');
+
   return components.map(component => ({
     key: String(component.id || component.name || 'component'),
     title: component.name || t('setting.status.components.item.fallback'),
-    copy: component.description || '-',
+    copy: component.description || emptyValue,
     meta: `${stateLabel(component.state, t)} · ${formatTime(component.gmtUpdate || component.gmtCreate || null)}`
   }));
 }
 
+function statusOrgLinkFallback(value: string | null | undefined, t: Translator) {
+  return value?.trim() || t('common.none');
+}
+
 export function buildStatusOrgOverviewRows(
   org: StatusPageOrg,
   t: Translator,
   formatTime: (value?: number | string | null) => string
 ) {
+  const home = statusOrgLinkFallback(org.home, t);
+  const feedback = statusOrgLinkFallback(org.feedback, t);
+
   return [
     {
       title: org.name || statusOrganizationFallback(t),
@@ -150,8 +177,8 @@ export function buildStatusOrgOverviewRows(
       meta: orgStateLabel(org.state, t)
     },
     {
-      title: readStatusCopy(t, 'setting.status.org.feedback', 'Feedback'),
-      copy: `${org.home || '-'} · ${org.feedback || '-'}`,
+      title: t('setting.status.org.feedback'),
+      copy: `${home} · ${feedback}`,
       meta: formatTime(org.gmtUpdate || org.gmtCreate || null)
     }
   ];
@@ -249,7 +276,8 @@ function parseIdList(text: string) {
 }
 
 export function buildStatusIncidentDraft(incident?: StatusPageIncident | null): StatusIncidentDraft {
-  const latestMessage = incident?.contents?.slice().sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0]?.message || '';
+  const existingContents = sortStatusIncidentContents(incident?.contents || []);
+  const latestMessage = existingContents[0]?.message || '';
   return {
     id: incident?.id,
     orgId: incident?.orgId,
@@ -257,12 +285,16 @@ export function buildStatusIncidentDraft(incident?: StatusPageIncident | null):
     state: String(incident?.state ?? 0),
     componentIdsText: (incident?.components || []).map(component => component.id).filter(Boolean).join(', '),
     message: latestMessage,
-    existingContents: incident?.contents || [],
+    existingContents,
     startTime: incident?.startTime ?? null,
     endTime: incident?.endTime ?? null,
   };
 }
 
+export function sortStatusIncidentContents(contents: StatusIncidentDraft['existingContents'] = []) {
+  return contents.slice().sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
+}
+
 export function buildStatusIncidentPayload(draft: StatusIncidentDraft, components: StatusPageComponent[]): StatusPageIncident {
   const state = parseNumberString(draft.state, 0);
   const componentIds = new Set(parseIdList(draft.componentIdsText));
@@ -271,6 +303,7 @@ export function buildStatusIncidentPayload(draft: StatusIncidentDraft, component
   const nextContents = [...(draft.existingContents || [])];
   if (draft.message.trim()) {
     nextContents.push({
+      ...(draft.id ? { incidentId: draft.id } : {}),
       message: draft.message.trim(),
       state,
       timestamp: now
diff --git a/web-next/lib/setting-token/controller.test.ts b/web-next/lib/setting-token/controller.test.ts
index 6f776a2616..deb92eedbd 100644
--- a/web-next/lib/setting-token/controller.test.ts
+++ b/web-next/lib/setting-token/controller.test.ts
@@ -1,5 +1,5 @@
 import { describe, expect, it, vi } from 'vitest';
-import { buildGenerateTokenUrl, generateTokenValue, loadTokenData } from './controller';
+import { buildGenerateTokenUrl, deleteTokenById, generateTokenValue, loadTokenData } from './controller';
 
 describe('setting token controller', () => {
   it('loads tokens from the auth token API', async () => {
@@ -26,4 +26,18 @@ describe('setting token controller', () => {
     const apiGet = vi.fn().mockResolvedValue({ code: 1, msg: 'bad request' });
     await expect(generateTokenValue(apiGet as any, 'OTLP', '604800', 'Generate failed')).rejects.toThrow('bad request');
   });
+
+  it('deletes tokens through the Angular account token delete endpoint', async () => {
+    const apiDelete = vi.fn().mockResolvedValue({ code: 0 });
+
+    await expect(deleteTokenById(apiDelete as any, 7, 'Delete failed')).resolves.toBeUndefined();
+
+    expect(apiDelete).toHaveBeenCalledWith('/account/token/7');
+  });
+
+  it('throws the backend or translated failure message when token deletion fails', async () => {
+    const apiDelete = vi.fn().mockResolvedValue({ code: 1, msg: 'in use' });
+
+    await expect(deleteTokenById(apiDelete as any, 7, 'Delete failed')).rejects.toThrow('in use');
+  });
 });
diff --git a/web-next/lib/setting-token/controller.ts b/web-next/lib/setting-token/controller.ts
index 8c4008f04b..516ec4faf9 100644
--- a/web-next/lib/setting-token/controller.ts
+++ b/web-next/lib/setting-token/controller.ts
@@ -1,7 +1,9 @@
 import type { AuthToken } from '@/lib/types';
 
 type ApiGetter = <T>(url: string) => Promise<T>;
+type ApiDeleter = <T>(url: string) => Promise<T>;
 type RawTokenResponse = { code: number; msg?: string; data?: { token?: string } };
+type RawMessageResponse = { code: number; msg?: string };
 
 export async function loadTokenData(apiGet: ApiGetter) {
   const tokens = await apiGet<AuthToken[]>('/account/token');
@@ -22,3 +24,10 @@ export async function generateTokenValue(apiGet: ApiGetter, name: string, expire
   }
   return message.data?.token || '';
 }
+
+export async function deleteTokenById(apiDelete: ApiDeleter, id: number | string, failureMessage: string) {
+  const message = await apiDelete<RawMessageResponse>(`/account/token/${id}`);
+  if (message.code !== 0) {
+    throw new Error(message.msg || failureMessage);
+  }
+}
diff --git a/web-next/lib/setting-token/view-model.test.ts b/web-next/lib/setting-token/view-model.test.ts
index c88315f181..1196d4a495 100644
--- a/web-next/lib/setting-token/view-model.test.ts
+++ b/web-next/lib/setting-token/view-model.test.ts
@@ -18,6 +18,7 @@ describe('setting token view model', () => {
           { expireTime: '2026-04-11T12:00:00Z' },
           { expireTime: '2026-04-09T12:00:00Z' }
         ] as any,
+        t,
         fixedNow
       )
     ).toEqual([
@@ -51,6 +52,39 @@ describe('setting token view model', () => {
     ]);
   });
 
+  it('uses localized empty fallbacks for missing token row facts', () => {
+    const scopedT = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'common.none': '无令牌值',
+        'setting.token.item.fallback': '未命名令牌',
+        'setting.token.row.creator': '创建者 {{creator}}',
+        'setting.token.row.state.active': '有效'
+      }
+    });
+
+    expect(
+      buildTokenRows(
+        [
+          {
+            tokenMask: '   ',
+            creator: '',
+            expireTime: '2026-04-11T12:00:00Z'
+          }
+        ] as any,
+        scopedT,
+        () => '2026-04-10 18:00:00',
+        fixedNow
+      )
+    ).toEqual([
+      {
+        title: '   ',
+        copy: '无令牌值 · 创建者 无令牌值',
+        meta: '有效 · 2026-04-10 18:00:00'
+      }
+    ]);
+  });
+
   it('builds token facts from lifecycle counts', () => {
     expect(
       buildTokenFacts(
diff --git a/web-next/lib/setting-token/view-model.ts b/web-next/lib/setting-token/view-model.ts
index 99841d5657..59b3cbe8ab 100644
--- a/web-next/lib/setting-token/view-model.ts
+++ b/web-next/lib/setting-token/view-model.ts
@@ -7,14 +7,14 @@ export function isExpired(token: AuthToken, now = Date.now()) {
   return new Date(token.expireTime).getTime() < now;
 }
 
-export function buildTokenMetrics(tokens: AuthToken[], now = Date.now()) {
+export function buildTokenMetrics(tokens: AuthToken[], t: Translator, now = Date.now()) {
   const activeCount = tokens.filter(token => !isExpired(token, now)).length;
   const expiredCount = tokens.filter(token => isExpired(token, now)).length;
 
   return [
-    { label: 'total tokens', value: String(tokens.length) },
-    { label: 'active tokens', value: String(activeCount), tone: 'success' },
-    { label: 'expired tokens', value: String(expiredCount), tone: expiredCount > 0 ? 'warning' : 'success' }
+    { label: t('setting.token.metric.total'), value: String(tokens.length) },
+    { label: t('setting.token.metric.active'), value: String(activeCount), tone: 'success' },
+    { label: t('setting.token.metric.expired'), value: String(expiredCount), tone: expiredCount > 0 ? 'warning' : 'success' }
   ];
 }
 
@@ -30,12 +30,22 @@ export function buildTokenFacts(tokens: AuthToken[], t: Translator, now = Date.n
   ];
 }
 
+function formatTokenFact(value: unknown, fallback: string) {
+  const text = value == null ? '' : String(value).trim();
+  return text || fallback;
+}
+
 export function buildTokenRows(tokens: AuthToken[], t: Translator, formatTime: (value?: number | string | null) => string, now = Date.now()) {
-  return tokens.map(token => ({
-    title: token.name || token.tokenMask || t('setting.token.item.fallback'),
-    copy: `${token.tokenMask || '-'} · creator ${token.creator || '-'}`,
-    meta: `${isExpired(token, now) ? 'expired' : 'active'} · ${formatTime(token.expireTime || token.gmtCreate || null)}`
-  }));
+  const emptyValue = t('common.none');
+  return tokens.map(token => {
+    const tokenMask = formatTokenFact(token.tokenMask, emptyValue);
+    const creator = formatTokenFact(token.creator, emptyValue);
+    return {
+      title: token.name || token.tokenMask || t('setting.token.item.fallback'),
+      copy: `${tokenMask} · ${t('setting.token.row.creator', { creator })}`,
+      meta: `${isExpired(token, now) ? t('setting.token.row.state.expired') : t('setting.token.row.state.active')} · ${formatTime(token.expireTime || token.gmtCreate || null)}`
+    };
+  });
 }
 
 export function buildTokenExpirationOptions(t: Translator) {
diff --git a/web-next/lib/settings-surface/view-model.test.ts b/web-next/lib/settings-surface/view-model.test.ts
index 36e93754ea..d3ecd3a90b 100644
--- a/web-next/lib/settings-surface/view-model.test.ts
+++ b/web-next/lib/settings-surface/view-model.test.ts
@@ -14,15 +14,15 @@ describe('settings surface view model', () => {
   it('builds settings facts', () => {
     expect(buildSettingsFacts('Token management', '生成令牌', t)).toEqual([
       { label: '工作区', value: 'token management' },
-      { label: 'Mode', value: 'settings' },
-      { label: 'Focus', value: '生成令牌' }
+      { label: '模式', value: '设置' },
+      { label: '焦点', value: '生成令牌' }
     ]);
   });
 
   it('builds settings surface rows', () => {
     expect(buildSettingsRows(t)).toEqual([
-      { title: '设置导航', copy: '从这里进入状态页、采集器、标签、插件、令牌和系统配置。', meta: 'stable' },
-      { title: '运维配置', copy: '统一维护采集器、标签、插件、令牌、状态页和通知通道。', meta: 'stable' }
+      { title: '设置导航', copy: '从这里进入状态页、采集器、标签、插件、令牌和系统配置。', meta: '路由契约' },
+      { title: '运维配置', copy: '统一维护采集器、标签、插件、令牌、状态页和通知通道。', meta: 'API 契约' }
     ]);
   });
 
@@ -40,11 +40,11 @@ describe('settings surface view model', () => {
       'mcp-ai-foundation'
     ]);
     expect(review.currentGroups.find(group => group.key === 'api-access')).toMatchObject({
-      label: 'API access',
+      labelKey: 'settings.surface.governance.group.api-access',
       routes: ['/setting/settings/token']
     });
     expect(review.currentGroups.find(group => group.key === 'notifications')).toMatchObject({
-      label: 'Notifications',
+      labelKey: 'settings.surface.governance.group.notifications',
       routes: ['/alert/notice']
     });
     expect(review.currentGroups.find(group => group.key === 'template-marketplace')?.routes).toEqual([
@@ -77,7 +77,7 @@ describe('settings surface view model', () => {
   });
 
   it('builds operator-facing governance rows from current routes and roadmap docs only', () => {
-    const rows = buildPlatformGovernanceRows();
+    const rows = buildPlatformGovernanceRows(t);
 
     expect(rows.map(row => row.key)).toEqual([
       'users-permissions',
@@ -100,9 +100,11 @@ describe('settings surface view model', () => {
     const futureBoundary = rows.find(row => row.key === 'future-roadmap-boundary');
     expect(futureBoundary).toMatchObject({
       title: '未来大域边界',
-      meta: 'roadmap only'
+      meta: '仅规划'
     });
-    expect(futureBoundary?.copy).toContain('Security');
+    expect(futureBoundary?.copy).toContain('安全治理');
+    expect(futureBoundary?.copy).toContain('路线图能力规划');
+    expect(futureBoundary?.copy).not.toContain('Data Observability');
     expect(futureBoundary?.copy).toContain('/docs/roadmap/future-security');
     expect(rows.map(row => row.meta).join(' ')).not.toContain('/security');
   });
diff --git a/web-next/lib/settings-surface/view-model.ts b/web-next/lib/settings-surface/view-model.ts
index a6c6f5a745..2f201c1af1 100644
--- a/web-next/lib/settings-surface/view-model.ts
+++ b/web-next/lib/settings-surface/view-model.ts
@@ -18,7 +18,7 @@ export type PlatformGovernanceFutureDomain =
 
 export type PlatformGovernanceGroup = {
   key: PlatformGovernanceGroupKey;
-  label: string;
+  labelKey: string;
   currentStatus: 'implemented-route' | 'foundation-only';
   routes: string[];
 };
@@ -63,53 +63,53 @@ const FUTURE_GOVERNANCE_DOMAINS: PlatformGovernanceFutureDomain[] = [
   'developer-integrations'
 ];
 
-const GOVERNANCE_GROUP_TITLES: Record<PlatformGovernanceGroupKey, string> = {
-  'users-permissions': '用户与权限',
-  'api-access': 'API 访问',
-  notifications: '通知通道',
-  'template-marketplace': '模板与插件',
-  'mcp-ai-foundation': 'MCP 与 AI 基础'
+const GOVERNANCE_GROUP_TITLE_KEYS: Record<PlatformGovernanceGroupKey, string> = {
+  'users-permissions': 'settings.surface.governance.group.users-permissions',
+  'api-access': 'settings.surface.governance.group.api-access',
+  notifications: 'settings.surface.governance.group.notifications',
+  'template-marketplace': 'settings.surface.governance.group.template-marketplace',
+  'mcp-ai-foundation': 'settings.surface.governance.group.mcp-ai-foundation'
 };
 
-const FUTURE_GOVERNANCE_TITLES: Record<PlatformGovernanceFutureDomain, string> = {
-  security: 'Security',
-  'data-observability': 'Data Observability',
-  'digital-experience': 'Digital Experience',
-  'software-delivery': 'Software Delivery',
-  'cloud-cost': 'Cloud Cost',
-  'ai-observability': 'AI Observability',
-  'developer-integrations': 'Developer Integrations'
+const FUTURE_GOVERNANCE_TITLE_KEYS: Record<PlatformGovernanceFutureDomain, string> = {
+  security: 'settings.surface.governance.future.security',
+  'data-observability': 'settings.surface.governance.future.data-observability',
+  'digital-experience': 'settings.surface.governance.future.digital-experience',
+  'software-delivery': 'settings.surface.governance.future.software-delivery',
+  'cloud-cost': 'settings.surface.governance.future.cloud-cost',
+  'ai-observability': 'settings.surface.governance.future.ai-observability',
+  'developer-integrations': 'settings.surface.governance.future.developer-integrations'
 };
 
 export function buildPlatformGovernanceReview(): PlatformGovernanceReview {
   const currentGroups: PlatformGovernanceGroup[] = [
     {
       key: 'users-permissions',
-      label: 'Users and permissions',
+      labelKey: GOVERNANCE_GROUP_TITLE_KEYS['users-permissions'],
       currentStatus: 'foundation-only',
       routes: ['/passport/login', '/passport/lock']
     },
     {
       key: 'api-access',
-      label: 'API access',
+      labelKey: GOVERNANCE_GROUP_TITLE_KEYS['api-access'],
       currentStatus: 'implemented-route',
       routes: ['/setting/settings/token']
     },
     {
       key: 'notifications',
-      label: 'Notifications',
+      labelKey: GOVERNANCE_GROUP_TITLE_KEYS.notifications,
       currentStatus: 'implemented-route',
       routes: ['/alert/notice']
     },
     {
       key: 'template-marketplace',
-      label: 'Template marketplace',
+      labelKey: GOVERNANCE_GROUP_TITLE_KEYS['template-marketplace'],
       currentStatus: 'implemented-route',
       routes: ['/setting/define', '/setting/plugins']
     },
     {
       key: 'mcp-ai-foundation',
-      label: 'MCP and AI foundations',
+      labelKey: GOVERNANCE_GROUP_TITLE_KEYS['mcp-ai-foundation'],
       currentStatus: 'foundation-only',
       routes: ['/setting/settings/config', '/setting/settings/mcp-server']
     }
@@ -126,27 +126,31 @@ export function buildPlatformGovernanceReview(): PlatformGovernanceReview {
   };
 }
 
-export function buildPlatformGovernanceRows(): PlatformGovernanceRow[] {
+export function buildPlatformGovernanceRows(t: Translator): PlatformGovernanceRow[] {
   const review = buildPlatformGovernanceReview();
   const currentRows = review.currentGroups.map(group => ({
     key: group.key,
-    title: GOVERNANCE_GROUP_TITLES[group.key],
+    title: t(GOVERNANCE_GROUP_TITLE_KEYS[group.key]),
     copy:
       group.currentStatus === 'implemented-route'
-        ? '已接入当前平台入口,继续沿用现有页面,不新增空菜单。'
-        : '保留当前基础能力入口,完整治理能力继续在 roadmap 中推进。',
+        ? t('settings.surface.governance.copy.implemented-route')
+        : t('settings.surface.governance.copy.foundation-only'),
     meta: group.routes.join(' · ')
   }));
+  const futureDomains = review.futureRoadmapOnly
+    .map(domain => t(FUTURE_GOVERNANCE_TITLE_KEYS[domain]))
+    .join(t('settings.surface.governance.future.separator'));
 
   return [
     ...currentRows,
     {
       key: 'future-roadmap-boundary',
-      title: '未来大域边界',
-      copy: `${review.futureRoadmapOnly
-        .map(domain => FUTURE_GOVERNANCE_TITLES[domain])
-        .join('、')} 仅作为 roadmap 能力规划,不展示为已上线应用入口。文档:${review.futureRoadmapDocs.join(' · ')}`,
-      meta: 'roadmap only'
+      title: t('settings.surface.governance.future.title'),
+      copy: t('settings.surface.governance.future.copy', {
+        domains: futureDomains,
+        docs: review.futureRoadmapDocs.join(' · ')
+      }),
+      meta: t('settings.surface.governance.future.meta')
     }
   ];
 }
@@ -171,8 +175,8 @@ export function buildPlatformGovernanceClosureReview(): PlatformGovernanceClosur
 export function buildSettingsFacts(title: string, nextStep: string, t: Translator) {
   return [
     { label: t('common.workspace'), value: title.toLowerCase() },
-    { label: t('common.mode'), value: t('common.settings-mode') },
-    { label: t('common.focus'), value: nextStep }
+    { label: t('settings.surface.fact.mode-label'), value: t('settings.surface.fact.mode-value') },
+    { label: t('settings.surface.fact.focus-label'), value: nextStep }
   ];
 }
 
@@ -181,12 +185,12 @@ export function buildSettingsRows(t: Translator) {
     {
       title: t('settings.surface.route-contract.title'),
       copy: t('settings.surface.route-contract.copy'),
-      meta: t('common.stable')
+      meta: t('settings.surface.route-contract.meta')
     },
     {
       title: t('settings.surface.api-contract.title'),
       copy: t('settings.surface.api-contract.copy'),
-      meta: t('common.stable')
+      meta: t('settings.surface.api-contract.meta')
     }
   ];
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 31/50: test(web-next): align topology toolbar fit metadata

Posted by zh...@apache.org.
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 eb5c666d8d050defc9dfd9fb164f5534e9c1540c
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 21:36:10 2026 +0800

    test(web-next): align topology toolbar fit metadata
---
 web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx | 4 ++--
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx      | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
index 11ebfd8f5d..8e675129e8 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
@@ -917,9 +917,9 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(html).toContain('data-hz-topology-g6-action-behavior="zoom-scale"');
     expect(html).toContain('data-hz-topology-g6-action="zoom-in"');
     expect(html).toContain('data-hz-topology-g6-action="fit-view"');
-    expect(html).toContain('data-hz-topology-g6-action-behavior="fit-and-center"');
+    expect(html).toContain('data-hz-topology-g6-action-behavior="overflow-fit-and-center"');
     expect(html).toContain('data-hz-topology-g6-action="reset-view"');
-    expect(html).toContain('data-hz-topology-g6-action-behavior="zoom-fit-center"');
+    expect(html).toContain('data-hz-topology-g6-action-behavior="zoom-one-overflow-fit-center"');
     expect(html).toContain('data-hz-topology-g6-action="focus-selected-node"');
     expect(html).toContain('data-hz-topology-g6-action-target="scale-svc-000"');
     expect(html).toContain('data-hz-topology-g6-action="focus-search-result"');
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
index 57fe3556f6..b590d2c5e1 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
@@ -2210,7 +2210,7 @@ export function HzTopologyG6Canvas({
           data-hz-topology-g6-action="fit-view"
           data-hz-topology-g6-action-owner="hertzbeat-ui-g6-action"
           data-hz-topology-g6-action-state="ready"
-          data-hz-topology-g6-action-behavior="fit-and-center"
+          data-hz-topology-g6-action-behavior="overflow-fit-and-center"
           data-hz-topology-g6-action-target="viewport"
           onClick={fitView}
         >
@@ -2224,7 +2224,7 @@ export function HzTopologyG6Canvas({
           data-hz-topology-g6-action="reset-view"
           data-hz-topology-g6-action-owner="hertzbeat-ui-g6-action"
           data-hz-topology-g6-action-state="ready"
-          data-hz-topology-g6-action-behavior="zoom-fit-center"
+          data-hz-topology-g6-action-behavior="zoom-one-overflow-fit-center"
           data-hz-topology-g6-action-target="viewport"
           onClick={resetView}
         >


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 44/50: fix(web-next): bound topology operator zoom

Posted by zh...@apache.org.
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 04aa6d5fdff5a8be816052f50561be0bec148709
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 10:05:13 2026 +0800

    fix(web-next): bound topology operator zoom
---
 web-next/app/topology/page.test.tsx                     |  3 ++-
 web-next/app/ui-lab/page.test.tsx                       |  3 ++-
 web-next/lib/hertzbeat-2-gap-audit.test.ts              |  1 +
 web-next/lib/hertzbeat-2-gap-audit.ts                   |  1 +
 web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx | 15 +++++++++++++--
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx      |  5 +++--
 6 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 5b2c37689a..fc92222fca 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -884,7 +884,8 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
     expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
     expect(html).toContain('data-hz-topology-g6-wheel-origin="pointer-clamped"');
-    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-4.8"');
+    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-2.2"');
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts="plus-minus-zero-fit"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts-owner="hertzbeat-ui-g6-keyboard"');
     expect(html).toContain('data-hz-topology-g6-keyboard-actions="zoom-in zoom-out reset-view fit-view"');
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index 3093c7c6d8..fe58142a55 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2389,7 +2389,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
     expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
     expect(html).toContain('data-hz-topology-g6-wheel-origin="pointer-clamped"');
-    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-4.8"');
+    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-2.2"');
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts="plus-minus-zero-fit"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts-owner="hertzbeat-ui-g6-keyboard"');
     expect(html).toContain('data-hz-topology-g6-keyboard-actions="zoom-in zoom-out reset-view fit-view"');
diff --git a/web-next/lib/hertzbeat-2-gap-audit.test.ts b/web-next/lib/hertzbeat-2-gap-audit.test.ts
index 24dc9a9c1a..b55c6c6bac 100644
--- a/web-next/lib/hertzbeat-2-gap-audit.test.ts
+++ b/web-next/lib/hertzbeat-2-gap-audit.test.ts
@@ -238,6 +238,7 @@ describe('HertzBeat 2.0 release gap audit', () => {
         '/topology reuses HzTopologyMetricTable header, row, route, RED cell, badge, and action ownership for edge ranking',
         '/topology reuses HzTopologyMetricTable graph-first density for compact companion RED ranking',
         '/topology wires HzTopologyMetricTable render-window companion metadata from the same G6 graph so hidden nodes keep table investigation context',
+        '/topology exposes windowed API graph RED rankings as a graph-bottom HzTopologyMetricTable so hidden render-window edges stay inspectable after the right rail is removed',
         '/topology marks HzTopologyMetricTable rows with source and target visibility from the current G6 render window instead of losing hidden-edge context',
         '/topology exposes HzTopologyMetricTable render-window row count summaries for future visible, partial, and hidden relationship filters',
         '/topology reuses HzTopologyMetricTable in-page render-window filters so ordinary RED table filtering does not navigate or remount the G6 canvas',
diff --git a/web-next/lib/hertzbeat-2-gap-audit.ts b/web-next/lib/hertzbeat-2-gap-audit.ts
index ce605d7ade..f1493966a1 100644
--- a/web-next/lib/hertzbeat-2-gap-audit.ts
+++ b/web-next/lib/hertzbeat-2-gap-audit.ts
@@ -340,6 +340,7 @@ const functionalGaps: HertzBeat2Gap[] = [
       '/topology reuses HzTopologyMetricTable header, row, route, RED cell, badge, and action ownership for edge ranking',
       '/topology reuses HzTopologyMetricTable graph-first density for compact companion RED ranking',
       '/topology wires HzTopologyMetricTable render-window companion metadata from the same G6 graph so hidden nodes keep table investigation context',
+      '/topology exposes windowed API graph RED rankings as a graph-bottom HzTopologyMetricTable so hidden render-window edges stay inspectable after the right rail is removed',
       '/topology marks HzTopologyMetricTable rows with source and target visibility from the current G6 render window instead of losing hidden-edge context',
       '/topology exposes HzTopologyMetricTable render-window row count summaries for future visible, partial, and hidden relationship filters',
       '/topology reuses HzTopologyMetricTable in-page render-window filters so ordinary RED table filtering does not navigate or remount the G6 canvas',
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
index f8f730f6ec..47bd846a98 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
@@ -15,6 +15,7 @@ import {
   clampHzTopologyG6AutoFitZoom,
   getHzTopologyG6NodeIcon,
   HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM,
+  HZ_TOPOLOGY_G6_MAX_ZOOM,
   HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   HzTopologyG6Canvas
 } from './topology-g6';
@@ -373,7 +374,8 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(html).toContain('data-hz-topology-g6-auto-fit-max-zoom="1"');
     expect(html).toContain('data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"');
     expect(html).toContain('data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range"');
-    expect(html).toContain('data-hz-topology-g6-operator-zoom-bounds="0.18-4.8"');
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-bounds="0.18-2.2"');
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"');
     expect(html).toContain('data-hz-topology-g6-fit-mode="overflow-only-center"');
     expect(clampHzTopologyG6AutoFitZoom(4.8)).toBe(HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM);
     expect(clampHzTopologyG6AutoFitZoom(0.72)).toBe(0.72);
@@ -396,6 +398,15 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(html).toContain('data-hz-topology-g6-reset-behavior="zoom-one-overflow-fit-center"');
   });
 
+  it('keeps operator zoom below the giant-node range that can make a small graph fill wide browsers', () => {
+    const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={graph} />);
+
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-bounds="0.18-2.2"');
+    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-2.2"');
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"');
+    expect(HZ_TOPOLOGY_G6_MAX_ZOOM).toBe(2.2);
+  });
+
   it('preserves operator wheel and pan zoom while hover or selection styling updates redraw the G6 graph', () => {
     const source = String(HzTopologyG6Canvas);
     const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={graph} selectedNodeId="svc-checkout" />);
@@ -1060,7 +1071,7 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
     expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
     expect(html).toContain('data-hz-topology-g6-wheel-origin="pointer-clamped"');
-    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-4.8"');
+    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-2.2"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts="plus-minus-zero-fit"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts-owner="hertzbeat-ui-g6-keyboard"');
     expect(html).toContain('data-hz-topology-g6-keyboard-actions="zoom-in zoom-out reset-view fit-view"');
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
index e913a7de1f..4374eb251d 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
@@ -239,7 +239,7 @@ type G6GraphRuntime = {
 };
 
 export const HZ_TOPOLOGY_G6_MIN_ZOOM = 0.18;
-export const HZ_TOPOLOGY_G6_MAX_ZOOM = 4.8;
+export const HZ_TOPOLOGY_G6_MAX_ZOOM = 2.2;
 export const HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM = 1;
 
 type HzTopologyG6ViewportSnapshot = {
@@ -1855,6 +1855,7 @@ export function HzTopologyG6Canvas({
       data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"
       data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range"
       data-hz-topology-g6-operator-zoom-bounds={`${HZ_TOPOLOGY_G6_MIN_ZOOM}-${HZ_TOPOLOGY_G6_MAX_ZOOM}`}
+      data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"
       data-hz-topology-g6-fit-mode="overflow-only-center"
       data-hz-topology-g6-viewport-interaction-state={viewportInteractionState}
       data-hz-topology-g6-viewport-interaction-owner="hertzbeat-ui-g6-viewport-interaction"
@@ -1882,7 +1883,7 @@ export function HzTopologyG6Canvas({
       data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"
       data-hz-topology-g6-wheel-listener-passive="false-control"
       data-hz-topology-g6-wheel-origin="pointer-clamped"
-      data-hz-topology-g6-wheel-zoom-bounds="0.18-4.8"
+      data-hz-topology-g6-wheel-zoom-bounds={`${HZ_TOPOLOGY_G6_MIN_ZOOM}-${HZ_TOPOLOGY_G6_MAX_ZOOM}`}
       data-hz-topology-g6-node-motion="locked-layout"
       data-hz-topology-g6-node-motion-owner="hertzbeat-ui-g6-node-motion"
       data-hz-topology-g6-selection-engine="hertzbeat-controlled"


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 14/50: feat(web-next): restore status actions parity

Posted by zh...@apache.org.
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 585626c26969105035acc517fc39fce676354c65
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 01:20:40 2026 +0800

    feat(web-next): restore status actions parity
---
 web-next/app/actions/actions-page.tsx              | 300 ++++++++++++++
 web-next/app/actions/page.test.tsx                 | 101 ++++-
 web-next/app/actions/page.tsx                      | 196 +--------
 .../[draftId]/decision/route.test.ts               | 106 +++++
 .../approval-drafts/[draftId]/decision/route.ts    | 120 ++++++
 .../app/api/actions/approval-drafts/route.test.ts  | 258 ++++++++++++
 web-next/app/api/actions/approval-drafts/route.ts  | 162 ++++++++
 web-next/app/api/actions/catalog/route.test.ts     | 155 +++++++
 web-next/app/api/actions/catalog/route.ts          | 165 ++++++++
 web-next/app/incidents/incidents-page.tsx          | 154 +++++++
 web-next/app/incidents/page.test.tsx               | 253 +++++++++++-
 web-next/app/incidents/page.tsx                    | 104 +----
 web-next/app/status/page.test.tsx                  |  51 ++-
 web-next/app/status/page.tsx                       | 136 +------
 web-next/app/status/public/page.tsx                |   4 +-
 web-next/app/status/{page.tsx => status-page.tsx}  |  39 +-
 .../components/pages/ops-surface-page.test.tsx     |  37 +-
 web-next/components/pages/ops-surface-page.tsx     |   6 +-
 .../components/pages/public-status-shell.test.tsx  |  12 +
 web-next/components/pages/public-status-shell.tsx  |  44 +-
 .../lib/actions-approval-draft-fallback-store.ts   |  70 ++++
 web-next/lib/actions-surface/model.test.ts         | 244 +++++++++--
 web-next/lib/actions-surface/model.ts              | 453 +++++++++++++++++----
 web-next/lib/actions-surface/query-state.test.ts   |  56 +++
 web-next/lib/actions-surface/query-state.ts        |  35 ++
 web-next/lib/actions-surface/view-model.test.ts    | 134 ++++--
 web-next/lib/actions-surface/view-model.ts         |  95 +++--
 web-next/lib/incidents-surface/controller.test.ts  | 191 +++++++++
 web-next/lib/incidents-surface/controller.ts       | 304 ++++++++++++++
 web-next/lib/incidents-surface/model.test.ts       |  48 ++-
 web-next/lib/incidents-surface/model.ts            |  88 ++--
 web-next/lib/incidents-surface/view-model.test.ts  |  53 ++-
 web-next/lib/incidents-surface/view-model.ts       |  61 +--
 web-next/lib/ops-surface/view-model.test.ts        |  10 +-
 web-next/lib/ops-surface/view-model.ts             |   6 +-
 web-next/lib/status-center/controller.test.ts      |  35 ++
 web-next/lib/status-center/controller.ts           |  17 +-
 web-next/lib/status-center/display.ts              | 127 ++++--
 web-next/lib/status-center/view-model.test.ts      | 262 +++++++++++-
 web-next/lib/status-center/view-model.ts           |  57 ++-
 40 files changed, 3890 insertions(+), 859 deletions(-)

diff --git a/web-next/app/actions/actions-page.tsx b/web-next/app/actions/actions-page.tsx
new file mode 100644
index 0000000000..4b879988c2
--- /dev/null
+++ b/web-next/app/actions/actions-page.tsx
@@ -0,0 +1,300 @@
+'use client';
+
+import React from 'react';
+import { HzActionWorkbench } from '@hertzbeat/ui';
+import { useI18n } from '../../components/providers/i18n-provider';
+import { buildActionsPlaceholderState } from '../../lib/actions-surface/view-model';
+import type { ActionSuggestionContext } from '../../lib/actions-surface/model';
+import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
+
+type ApprovalDraftResult = {
+  draftId: string;
+  state: string;
+  executionState: string;
+  actionId?: string;
+  catalogId?: string;
+  adapterOwner?: string;
+  managerBacked?: boolean;
+};
+
+type ApprovalDecisionResult = {
+  draftId: string;
+  decision: string;
+  state: string;
+  executionState: string;
+  adapterOwner?: string;
+  managerBacked?: boolean;
+};
+
+type ActionCatalogResult = {
+  state: string;
+  adapterOwner: string;
+  managerBacked: boolean;
+  items: Array<{
+    catalogId?: string;
+    name?: string;
+    risk?: string;
+    status?: string;
+    executionMode?: string;
+    adapterOwner?: string;
+  }>;
+};
+
+type ApprovalDraftQueueResult = {
+  state: string;
+  adapterOwner: string;
+  managerBacked: boolean;
+  drafts: ApprovalDraftResult[];
+};
+
+export default function ActionsPage({ suggestionContext }: { suggestionContext?: ActionSuggestionContext } = {}) {
+  const { t } = useI18n();
+  const state = buildActionsPlaceholderState(t, suggestionContext);
+  const coldOpsVisual = coldOpsCatalogVisual;
+  const [approvalDraftStatus, setApprovalDraftStatus] = React.useState<'ready' | 'submitting' | 'created' | 'failed' | 'blocked'>(
+    state.approvalDraft.state === 'ready' ? 'ready' : 'blocked'
+  );
+  const [approvalDraftResult, setApprovalDraftResult] = React.useState<ApprovalDraftResult | undefined>();
+  const [approvalDraftError, setApprovalDraftError] = React.useState<string | undefined>();
+  const [approvalDecisionStatus, setApprovalDecisionStatus] = React.useState<'blocked' | 'ready' | 'submitting' | 'decided' | 'failed'>('blocked');
+  const [approvalDecisionResult, setApprovalDecisionResult] = React.useState<ApprovalDecisionResult | undefined>();
+  const [approvalDecisionError, setApprovalDecisionError] = React.useState<string | undefined>();
+  const [catalogResult, setCatalogResult] = React.useState<ActionCatalogResult>({
+    state: state.catalogAdapter.state,
+    adapterOwner: state.catalogAdapter.adapterOwner,
+    managerBacked: state.catalogAdapter.managerBacked,
+    items: []
+  });
+  const [approvalDraftQueueResult, setApprovalDraftQueueResult] = React.useState<ApprovalDraftQueueResult>({
+    state: state.approvalDraftQueue.state,
+    adapterOwner: state.approvalDraftQueue.adapterOwner,
+    managerBacked: state.approvalDraftQueue.managerBacked,
+    drafts: []
+  });
+
+  const loadApprovalDraftQueue = React.useCallback(async () => {
+    try {
+      const response = await fetch(state.approvalDraftQueue.endpoint, { method: state.approvalDraftQueue.method });
+      const payload = await response.json() as ApprovalDraftQueueResult & { message?: string };
+      if (!response.ok) throw new Error(payload.message || 'approval draft queue read failed');
+      setApprovalDraftQueueResult({
+        state: payload.state,
+        adapterOwner: payload.adapterOwner,
+        managerBacked: Boolean(payload.managerBacked),
+        drafts: Array.isArray(payload.drafts) ? payload.drafts : []
+      });
+    } catch {
+      setApprovalDraftQueueResult({
+        state: 'failed',
+        adapterOwner: 'next-actions-approval-draft-bff',
+        managerBacked: false,
+        drafts: []
+      });
+    }
+  }, [state.approvalDraftQueue.endpoint, state.approvalDraftQueue.method]);
+
+  React.useEffect(() => {
+    let active = true;
+    async function loadCatalog() {
+      try {
+        const response = await fetch(state.catalogAdapter.endpoint, { method: state.catalogAdapter.method });
+        const payload = await response.json() as ActionCatalogResult & { message?: string };
+        if (!response.ok) throw new Error(payload.message || 'catalog read failed');
+        if (active) {
+          setCatalogResult({
+            state: payload.state,
+            adapterOwner: payload.adapterOwner,
+            managerBacked: Boolean(payload.managerBacked),
+            items: Array.isArray(payload.items) ? payload.items : []
+          });
+        }
+      } catch {
+        if (active) {
+          setCatalogResult({
+            state: 'failed',
+            adapterOwner: 'next-actions-catalog-bff',
+            managerBacked: false,
+            items: []
+          });
+        }
+      }
+    }
+    void loadCatalog();
+    return () => {
+      active = false;
+    };
+  }, [state.catalogAdapter.endpoint, state.catalogAdapter.method]);
+
+  React.useEffect(() => {
+    void loadApprovalDraftQueue();
+  }, [loadApprovalDraftQueue]);
+
+  React.useEffect(() => {
+    if (approvalDraftResult?.draftId) {
+      setApprovalDecisionStatus('ready');
+      setApprovalDecisionResult(undefined);
+      setApprovalDecisionError(undefined);
+    } else {
+      setApprovalDecisionStatus('blocked');
+    }
+  }, [approvalDraftResult?.draftId]);
+
+  async function createApprovalDraft() {
+    if (!state.approvalDraft.request) return;
+    setApprovalDraftStatus('submitting');
+    setApprovalDraftError(undefined);
+    try {
+      const response = await fetch(state.approvalDraft.endpoint, {
+        method: state.approvalDraft.method,
+        headers: { 'content-type': 'application/json' },
+        body: JSON.stringify(state.approvalDraft.request)
+      });
+      const payload = await response.json() as ApprovalDraftResult & { message?: string };
+      if (!response.ok) throw new Error(payload.message || 'approval draft failed');
+      setApprovalDraftResult({
+        draftId: payload.draftId,
+        state: payload.state,
+        executionState: payload.executionState,
+        actionId: payload.actionId,
+        catalogId: payload.catalogId,
+        adapterOwner: payload.adapterOwner,
+        managerBacked: payload.managerBacked
+      });
+      setApprovalDraftStatus('created');
+      void loadApprovalDraftQueue();
+    } catch (error) {
+      setApprovalDraftError(error instanceof Error ? error.message : 'approval draft failed');
+      setApprovalDraftStatus('failed');
+    }
+  }
+
+  async function decideApprovalDraft(decision: 'approved' | 'rejected') {
+    if (!approvalDraftResult?.draftId) return;
+    const endpoint = state.approvalDecision.endpointTemplate.replace(
+      ':draftId',
+      encodeURIComponent(approvalDraftResult.draftId)
+    );
+    setApprovalDecisionStatus('submitting');
+    setApprovalDecisionError(undefined);
+    try {
+      const response = await fetch(endpoint, {
+        method: state.approvalDecision.method,
+        headers: { 'content-type': 'application/json' },
+        body: JSON.stringify({
+          decision,
+          reviewer: 'hertzbeat-ui-operator',
+          reason: 'manual approval decision from actions workbench',
+          executionMode: state.approvalDecision.executionMode,
+          executionAllowed: false
+        })
+      });
+      const payload = await response.json() as ApprovalDecisionResult & { message?: string };
+      if (!response.ok) throw new Error(payload.message || 'approval decision failed');
+      setApprovalDecisionResult({
+        draftId: payload.draftId,
+        decision: payload.decision,
+        state: payload.state,
+        executionState: payload.executionState,
+        adapterOwner: payload.adapterOwner,
+        managerBacked: payload.managerBacked
+      });
+      setApprovalDecisionStatus('decided');
+      void loadApprovalDraftQueue();
+    } catch (error) {
+      setApprovalDecisionError(error instanceof Error ? error.message : 'approval decision failed');
+      setApprovalDecisionStatus('failed');
+    }
+  }
+
+  const approvalDecisionEndpoint = approvalDraftResult?.draftId
+    ? state.approvalDecision.endpointTemplate.replace(':draftId', encodeURIComponent(approvalDraftResult.draftId))
+    : state.approvalDecision.endpointTemplate;
+
+  return (
+    <main
+      className={coldOpsVisual.entry.main}
+      data-actions-route="otlp-cold-ops-entry"
+      data-actions-style-baseline={coldOpsVisual.canvasName}
+      data-actions-placeholder-replacement="api-backed-workbench"
+      data-actions-legacy-open-context="adapter-boundary-panel"
+      data-actions-legacy-entity-handoff="/entities"
+    >
+      <div className={coldOpsVisual.entry.container}>
+        <HzActionWorkbench
+          data-actions-shared-workbench="hertzbeat-ui"
+          data-actions-placeholder-replacement="api-backed-workbench"
+          data-actions-legacy-open-context="adapter-boundary-panel"
+          data-actions-legacy-entity-handoff="/entities"
+          title={state.title}
+          subtitle={state.subtitle}
+          sourceLabel={state.kicker}
+          actions={state.actions}
+          shell={state.shell}
+          adapterBoundary={state.adapterBoundary}
+          catalogAdapter={{
+            ...state.catalogAdapter,
+            state: catalogResult.state,
+            adapterOwner: catalogResult.adapterOwner,
+            managerBacked: catalogResult.managerBacked,
+            items: catalogResult.items.map(item => ({
+              catalogId: item.catalogId || 'unknown-catalog-item',
+              name: item.name || item.catalogId || 'unknown catalog item',
+              risk: item.risk || 'unknown',
+              status: item.status,
+              executionMode: item.executionMode,
+              adapterOwner: item.adapterOwner
+            }))
+          }}
+          approvalDraft={{
+            ...state.approvalDraft,
+            status: approvalDraftStatus,
+            onCreate: state.approvalDraft.state === 'ready' ? createApprovalDraft : undefined,
+            result: approvalDraftResult,
+            error: approvalDraftError
+          }}
+          approvalDraftQueue={{
+            ...state.approvalDraftQueue,
+            state: approvalDraftQueueResult.state,
+            adapterOwner: approvalDraftQueueResult.adapterOwner,
+            managerBacked: approvalDraftQueueResult.managerBacked,
+            drafts: approvalDraftQueueResult.drafts.map(draft => ({
+              draftId: draft.draftId,
+              state: draft.state,
+              actionId: draft.actionId,
+              catalogId: draft.catalogId,
+              executionState: draft.executionState,
+              adapterOwner: draft.adapterOwner
+            }))
+          }}
+          approvalDecision={{
+            ...state.approvalDecision,
+            state: approvalDraftResult?.draftId ? 'ready' : state.approvalDecision.state,
+            status: approvalDecisionStatus,
+            endpoint: approvalDecisionEndpoint,
+            managerBacked: Boolean(approvalDecisionResult?.managerBacked),
+            requestPreview: approvalDraftResult?.draftId
+              ? JSON.stringify({
+                draftId: approvalDraftResult.draftId,
+                decision: 'approved',
+                executionAllowed: false
+              })
+              : state.approvalDecision.requestPreview,
+            onApprove: approvalDraftResult?.draftId ? () => void decideApprovalDraft('approved') : undefined,
+            onReject: approvalDraftResult?.draftId ? () => void decideApprovalDraft('rejected') : undefined,
+            result: approvalDecisionResult,
+            error: approvalDecisionError
+          }}
+          checklistTitle={state.checklistTitle}
+          checklist={state.checklist}
+          suggestedActions={state.suggestedActions}
+          suggestedTitle={t('actions.entry.suggested.title')}
+          suggestedCopy={t('actions.entry.suggested.copy')}
+          suggestedEvidenceLabel={t('actions.entry.suggested.evidence')}
+          suggestedConfirmLabel={t('actions.entry.suggested.confirm')}
+          emptyTitle={state.empty.title}
+          emptyCopy={state.empty.copy}
+        />
+      </div>
+    </main>
+  );
+}
diff --git a/web-next/app/actions/page.test.tsx b/web-next/app/actions/page.test.tsx
index 79baab34d3..ebbcd15558 100644
--- a/web-next/app/actions/page.test.tsx
+++ b/web-next/app/actions/page.test.tsx
@@ -2,19 +2,66 @@ import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+
+const t = createTranslatorMock({ locale: 'zh-CN' });
+
+vi.mock('../../components/providers/i18n-provider', () => ({
+  useI18n: () => ({ t })
+}));
 
 describe('actions page', () => {
   it('renders the OTLP cold-matte entry shell without the previous placeholder stack', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/actions/page.tsx'), 'utf8');
-    const { default: ActionsPage } = await import('./page');
+    const routeSource = readFileSync(resolve(process.cwd(), 'app/actions/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/actions/actions-page.tsx'), 'utf8');
+    const { default: ActionsPage } = await import('./actions-page');
     const html = renderToStaticMarkup(<ActionsPage />);
 
+    expect(routeSource).toContain("import ActionsPage from './actions-page'");
+    expect(routeSource).toContain("import { readActionsSuggestionContext, type ActionsSearchParams } from '../../lib/actions-surface/query-state'");
+    expect(routeSource).toContain('const resolvedSearchParams = await searchParams');
+    expect(routeSource).toContain('const suggestionContext = readActionsSuggestionContext(resolvedSearchParams)');
+    expect(routeSource).toContain('return <ActionsPage suggestionContext={suggestionContext} />');
     expect(html).toContain('data-actions-route="otlp-cold-ops-entry"');
     expect(html).toContain('data-actions-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-actions-placeholder-replacement="api-backed-workbench"');
+    expect(html).toContain('data-actions-legacy-open-context="adapter-boundary-panel"');
+    expect(html).toContain('data-actions-legacy-entity-handoff="/entities"');
+    expect(html).toContain('data-actions-shared-workbench="hertzbeat-ui"');
+    expect(html).toContain('data-hz-ui="action-workbench"');
+    expect(html).toContain('data-hz-action-workbench-owner="hertzbeat-ui-action-workbench"');
+    expect(html).toContain('data-hz-action-workbench-density="operator-compact"');
+    expect(html).toContain('data-hz-action-workbench-style="cold-matte-hard-edge"');
     expect(html).toContain('data-actions-shell-panel="cold-ops-shell-panel"');
     expect(html).toContain('data-actions-launch-checklist="cold-ops-static-rail"');
     expect(html).toContain('data-actions-adapter-boundary="adapter-pending"');
+    expect(html).toContain('data-actions-catalog="manual-action-catalog-api"');
+    expect(html).toContain('data-actions-catalog-state="loading"');
+    expect(html).toContain('data-actions-catalog-owner="next-actions-catalog-bff"');
+    expect(html).toContain('data-actions-catalog-endpoint="/api/actions/catalog?limit=8"');
+    expect(html).toContain('data-actions-catalog-manager-backed="false"');
+    expect(html).toContain('data-actions-catalog-execution-mode="manual-approval-draft-only"');
+    expect(html).toContain('data-actions-catalog-execution-allowed="false"');
+    expect(html).toContain('data-actions-catalog-item-count="0"');
+    expect(html).toContain('data-actions-approval-draft="manual-approval-draft-api"');
+    expect(html).toContain('data-actions-approval-draft-state="awaiting-context"');
+    expect(html).toContain('data-actions-approval-draft-owner="next-actions-approval-draft-bff"');
+    expect(html).toContain('data-actions-approval-draft-endpoint="/api/actions/approval-drafts"');
+    expect(html).toContain('data-actions-approval-draft-execution-mode="manual-approval-draft-only"');
+    expect(html).toContain('data-actions-approval-draft-execution-allowed="false"');
+    expect(html).toContain('data-actions-approval-draft-queue="manual-approval-draft-read-api"');
+    expect(html).toContain('data-actions-approval-draft-queue-state="loading"');
+    expect(html).toContain('data-actions-approval-draft-queue-owner="next-actions-approval-draft-bff"');
+    expect(html).toContain('data-actions-approval-draft-queue-endpoint="/api/actions/approval-drafts?limit=8"');
+    expect(html).toContain('data-actions-approval-draft-queue-execution-mode="manual-approval-draft-only"');
+    expect(html).toContain('data-actions-approval-draft-queue-execution-allowed="false"');
+    expect(html).toContain('data-actions-approval-decision="manual-approval-decision-api"');
+    expect(html).toContain('data-actions-approval-decision-state="awaiting-draft"');
+    expect(html).toContain('data-actions-approval-decision-owner="next-actions-approval-decision-bff"');
+    expect(html).toContain('data-actions-approval-decision-endpoint="/api/actions/approval-drafts/:draftId/decision"');
+    expect(html).toContain('data-actions-approval-decision-execution-mode="manual-approval-draft-only"');
+    expect(html).toContain('data-actions-approval-decision-execution-allowed="false"');
     expect(html).toContain('data-actions-empty-state="cold-ops-domain-adapter"');
     expect(html).toContain('自动化处置');
     expect(html).toContain('按 OTLP 工作台的冷色基线统一入口、上下文和审批语义。');
@@ -23,6 +70,10 @@ describe('actions page', () => {
     expect(html).toContain('roadmap 示例快照');
     expect(html).toContain('不代表实时运行状态');
     expect(html).toContain('告警上下文建议是当前证据生成的人工交接');
+    expect(html).toContain('工作流自动化');
+    expect(html).toContain('处置手册编排');
+    expect(html).not.toContain('workflow-automation');
+    expect(html).not.toContain('runbook-orchestration');
     expect(html).toContain('等待接入执行适配器');
     expect(html).toContain('打开概览');
     expect(html).toContain('查看对象');
@@ -37,6 +88,14 @@ describe('actions page', () => {
     expect(html).not.toContain('V1 SHELL IS LIVE');
     expect(html).not.toContain('Domain adapter comes next');
     expect(source).toContain('coldOpsCatalogVisual');
+    expect(source).toContain('HzActionWorkbench');
+    expect(source).toContain('data-actions-placeholder-replacement="api-backed-workbench"');
+    expect(source).toContain('data-actions-legacy-open-context="adapter-boundary-panel"');
+    expect(source).toContain('data-actions-legacy-entity-handoff="/entities"');
+    expect(source).toContain('fetch(state.catalogAdapter.endpoint');
+    expect(source).toContain('fetch(state.approvalDraft.endpoint');
+    expect(source).toContain('fetch(state.approvalDraftQueue.endpoint');
+    expect(source).toContain('fetch(endpoint');
     expect(source).not.toContain('rounded-[16px]');
     expect(source).not.toContain('rounded-[14px]');
     expect(source).not.toContain('#4f6cff');
@@ -51,10 +110,10 @@ describe('actions page', () => {
   });
 
   it('renders alert-context suggested remediation actions as human-confirmed suggestions only', async () => {
-    const { default: ActionsPage } = await import('./page');
+    const { default: ActionsPage } = await import('./actions-page');
     const html = renderToStaticMarkup(
       <ActionsPage
-        searchParams={{
+        suggestionContext={{
           source: 'alert',
           signal: 'traces',
           severity: 'critical',
@@ -74,12 +133,26 @@ describe('actions page', () => {
     );
 
     expect(html).toContain('data-actions-suggested-remediation="alert-context-human-confirmation"');
+    expect(html).toContain('data-hz-action-workbench-owner="hertzbeat-ui-action-workbench"');
     expect(html).toContain('data-actions-adapter-boundary="adapter-pending"');
     expect(html).toContain('建议动作');
     expect(html).toContain('只生成建议,不自动执行。');
     expect(html).toContain('data-actions-suggested-action="suggest-restart-checkout"');
     expect(html).toContain('建议重启 checkout-api');
+    expect(html).toContain('高风险 · 重启 checkout 部署');
+    expect(html).toContain('中风险 · 静默边缘饱和告警');
+    expect(html).not.toContain('high risk · restart-checkout');
+    expect(html).not.toContain('medium risk · mute-edge-alerts');
+    expect(html).toContain('来源 告警中心');
+    expect(html).toContain('信号 链路');
+    expect(html).not.toContain('来源 alert');
+    expect(html).not.toContain('信号 traces');
     expect(html).toContain('data-actions-suggested-action-confirm="manual-required"');
+    expect(html).toContain('data-actions-approval-draft-state="ready"');
+    expect(html).toContain('data-actions-approval-draft-status="ready"');
+    expect(html).toContain('data-actions-approval-draft-request="preview"');
+    expect(html).toContain('&quot;actionId&quot;:&quot;suggest-restart-checkout&quot;');
+    expect(html).toContain('&quot;executionAllowed&quot;:false');
     expect(html).toContain('人工确认后执行');
     expect(html).toContain('data-actions-suggested-action-evidence="suggest-restart-checkout"');
     expect(html).toContain('/alert?status=firing');
@@ -87,4 +160,22 @@ describe('actions page', () => {
     expect(html).not.toContain('data-actions-auto-execute');
     expect(html).not.toContain('/actions/run');
   });
+
+  it('renders entity-id-only suggested remediation targets with localized fallback copy', async () => {
+    const { default: ActionsPage } = await import('./actions-page');
+    const html = renderToStaticMarkup(
+      <ActionsPage
+        suggestionContext={{
+          entityId: 'service:commerce/checkout',
+          source: 'entity',
+          returnTo: '/entities/service%3Acommerce%2Fcheckout?returnLabel=实体'
+        }}
+      />
+    );
+
+    expect(html).toContain('建议重启 实体 service:commerce/checkout');
+    expect(html).not.toContain('建议重启 service:commerce/checkout');
+    expect(html).toContain('entityId=service%3Acommerce%2Fcheckout');
+    expect(html).not.toContain('returnLabel=');
+  });
 });
diff --git a/web-next/app/actions/page.tsx b/web-next/app/actions/page.tsx
index d26126a517..bd735a7e52 100644
--- a/web-next/app/actions/page.tsx
+++ b/web-next/app/actions/page.tsx
@@ -1,187 +1,13 @@
 import React from 'react';
-import Link from 'next/link';
-import { Archive } from 'lucide-react';
-import { buildActionsPlaceholderState } from '../../lib/actions-surface/view-model';
-import type { ActionSuggestionContext } from '../../lib/actions-surface/model';
-import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
-
-type ActionsSearchParams = Record<string, string | string[] | undefined>;
-
-function firstParam(value: string | string[] | undefined) {
-  return Array.isArray(value) ? value[0] : value;
-}
-
-function readActionsSuggestionContext(searchParams: ActionsSearchParams | undefined): ActionSuggestionContext {
-  if (!searchParams) return {};
-  return {
-    start: firstParam(searchParams.start),
-    end: firstParam(searchParams.end),
-    timeRange: firstParam(searchParams.timeRange),
-    entityId: firstParam(searchParams.entityId),
-    entityName: firstParam(searchParams.entityName),
-    returnTo: firstParam(searchParams.returnTo),
-    serviceName: firstParam(searchParams.serviceName),
-    serviceNamespace: firstParam(searchParams.serviceNamespace),
-    environment: firstParam(searchParams.environment),
-    traceId: firstParam(searchParams.traceId),
-    spanId: firstParam(searchParams.spanId),
-    source: firstParam(searchParams.source),
-    collector: firstParam(searchParams.collector),
-    template: firstParam(searchParams.template),
-    search: firstParam(searchParams.search),
-    signal: firstParam(searchParams.signal),
-    severity: firstParam(searchParams.severity),
-    status: firstParam(searchParams.status),
-    alertGroupId: firstParam(searchParams.alertGroupId),
-    viewMode: firstParam(searchParams.viewMode),
-    sourceKind: firstParam(searchParams.sourceKind),
-    edgeId: firstParam(searchParams.edgeId)
-  };
-}
-
-function actionClass(variant: 'primary' | 'subtle') {
-  const coldOpsVisual = coldOpsCatalogVisual;
-  const base =
-    'inline-flex h-8 items-center justify-center rounded-[3px] border px-3 text-[12px] font-semibold transition-colors';
-  if (variant === 'primary') {
-    return `${base} ${coldOpsVisual.button.primaryCompact}`;
-  }
-  return `${base} ${coldOpsVisual.button.compact} border-[#263041] bg-[#101217] text-[#d5d9e3] hover:border-[#3a465c] hover:bg-[#151820]`;
-}
-
-export default function ActionsPage({ searchParams }: { searchParams?: ActionsSearchParams } = {}) {
-  const state = buildActionsPlaceholderState(readActionsSuggestionContext(searchParams));
-  const coldOpsVisual = coldOpsCatalogVisual;
-
-  return (
-    <main
-      className={coldOpsVisual.entry.main}
-      data-actions-route="otlp-cold-ops-entry"
-      data-actions-style-baseline={coldOpsVisual.canvasName}
-    >
-      <div className={coldOpsVisual.entry.container}>
-        <header className={coldOpsVisual.entry.header}>
-          <div className={coldOpsVisual.entry.headerLayout}>
-            <div>
-              <div className={coldOpsVisual.entry.kicker}>{state.kicker}</div>
-              <h1 className={coldOpsVisual.entry.title}>{state.title}</h1>
-              <p className={coldOpsVisual.entry.subtitle}>{state.subtitle}</p>
-            </div>
-            <div className={coldOpsVisual.button.row}>
-              {state.actions.map(action => (
-                <Link className={actionClass(action.variant)} href={action.href} key={action.href}>
-                  {action.label}
-                </Link>
-              ))}
-            </div>
-          </div>
-        </header>
-
-        <div className={coldOpsVisual.entry.grid}>
-          <section
-            className={coldOpsVisual.entry.panel}
-            data-actions-shell-panel="cold-ops-shell-panel"
-          >
-            <div className={coldOpsVisual.entry.panelEyebrow}>{state.shell.eyebrow}</div>
-            <p className={coldOpsVisual.entry.panelCopy}>{state.shell.copy}</p>
-            <div className="mt-4 flex flex-wrap gap-2">
-              {state.shell.chips.map(chip => (
-                <span className={coldOpsVisual.entry.chip} key={chip}>
-                  {chip}
-                </span>
-              ))}
-            </div>
-          </section>
-
-          <aside
-            className={coldOpsVisual.entry.rail}
-            data-actions-launch-checklist="cold-ops-static-rail"
-          >
-            <h2 className={coldOpsVisual.entry.railTitle}>{state.checklistTitle}</h2>
-            <div className="mt-3 space-y-3">
-              {state.checklist.map(item => (
-                <div className={coldOpsVisual.entry.checklistItem} key={item.title}>
-                  <span className={`${coldOpsVisual.entry.checklistDot} ${item.tone}`} aria-hidden="true" />
-                  <div>
-                    <div className={coldOpsVisual.entry.checklistTitle}>{item.title}</div>
-                    <div className={coldOpsVisual.entry.checklistCopy}>{item.copy}</div>
-                  </div>
-                </div>
-              ))}
-            </div>
-          </aside>
-
-          <section
-            className={`${coldOpsVisual.entry.panel} lg:col-span-2`}
-            data-actions-adapter-boundary={state.adapterBoundary.state}
-          >
-            <div className={coldOpsVisual.entry.panelEyebrow}>{state.adapterBoundary.label}</div>
-            <p className={coldOpsVisual.entry.panelCopy}>{state.adapterBoundary.copy}</p>
-            <div className="mt-4 flex flex-wrap gap-2">
-              {state.adapterBoundary.roadmapOnly.map(item => (
-                <span className={coldOpsVisual.entry.chip} key={item}>
-                  {item}
-                </span>
-              ))}
-            </div>
-          </section>
-
-          {state.suggestedActions.length > 0 ? (
-            <section
-              className={`${coldOpsVisual.entry.panel} lg:col-span-2`}
-              data-actions-suggested-remediation="alert-context-human-confirmation"
-            >
-              <div className={coldOpsVisual.entry.panelEyebrow}>建议动作</div>
-              <p className={coldOpsVisual.entry.panelCopy}>来自告警、实体和三信号上下文,只生成建议,不自动执行。</p>
-              <div className="mt-4 grid gap-2 md:grid-cols-3">
-                {state.suggestedActions.map(action => (
-                  <div
-                    key={action.id}
-                    data-actions-suggested-action={action.id}
-                    className="rounded-[4px] border border-[#303743] bg-[#101217] px-3 py-3"
-                  >
-                    <div className="text-[11px] font-semibold tracking-[0.12em] text-[#7e8494]">{action.risk} · {action.catalogId}</div>
-                    <div className="mt-2 text-[13px] font-semibold text-[#eef2f7]">{action.title}</div>
-                    <div className="mt-1 text-[12px] leading-5 text-[#8f99ab]">{action.copy}</div>
-                    <div className="mt-2 text-[11px] leading-4 text-[#7e8494]">{action.evidence}</div>
-                    <div className="mt-3 flex flex-wrap gap-2">
-                      <Link
-                        data-actions-suggested-action-evidence={action.id}
-                        href={action.evidenceHref}
-                        className="inline-flex h-7 items-center rounded-[3px] border border-[#303743] bg-[#0b0c0e] px-2.5 text-[12px] font-semibold text-[#dbe4f0]"
-                      >
-                        查看证据
-                      </Link>
-                      <button
-                        type="button"
-                        disabled
-                        data-actions-suggested-action-confirm={action.confirmation}
-                        className="inline-flex h-7 items-center rounded-[3px] border border-[#31405c] bg-[#182238] px-2.5 text-[12px] font-semibold text-[#d8e4ff] disabled:cursor-not-allowed disabled:opacity-70"
-                      >
-                        人工确认后执行
-                      </button>
-                    </div>
-                    <div className="mt-2 text-[11px] leading-4 text-[#8f99ab]">{action.posture}</div>
-                  </div>
-                ))}
-              </div>
-            </section>
-          ) : null}
-
-          <section
-            className={coldOpsVisual.entry.empty}
-            data-actions-empty-state="cold-ops-domain-adapter"
-          >
-            <div className="max-w-[720px] text-center">
-              <div className={coldOpsVisual.entry.emptyIcon}>
-                <Archive aria-hidden="true" size={22} strokeWidth={1.8} />
-              </div>
-              <h2 className={coldOpsVisual.entry.emptyTitle}>{state.empty.title}</h2>
-              <p className={coldOpsVisual.entry.emptyCopy}>{state.empty.copy}</p>
-            </div>
-          </section>
-        </div>
-      </div>
-    </main>
-  );
+import ActionsPage from './actions-page';
+import { readActionsSuggestionContext, type ActionsSearchParams } from '../../lib/actions-surface/query-state';
+
+export default async function ActionsRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<ActionsSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const suggestionContext = readActionsSuggestionContext(resolvedSearchParams);
+  return <ActionsPage suggestionContext={suggestionContext} />;
 }
diff --git a/web-next/app/api/actions/approval-drafts/[draftId]/decision/route.test.ts b/web-next/app/api/actions/approval-drafts/[draftId]/decision/route.test.ts
new file mode 100644
index 0000000000..bbe2025bd1
--- /dev/null
+++ b/web-next/app/api/actions/approval-drafts/[draftId]/decision/route.test.ts
@@ -0,0 +1,106 @@
+import { NextRequest } from 'next/server';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { POST } from './route';
+import { resetFallbackApprovalDraftsForTest } from '../../../../../../lib/actions-approval-draft-fallback-store';
+
+function decisionRequest(body: unknown) {
+  return new NextRequest('http://localhost/api/actions/approval-drafts/draft-1/decision', {
+    method: 'POST',
+    body: JSON.stringify(body),
+    headers: {
+      'content-type': 'application/json',
+      cookie: 'hb_ui_access=session-token'
+    }
+  });
+}
+
+function params(draftId = 'draft-1') {
+  return { params: Promise.resolve({ draftId }) };
+}
+
+afterEach(() => {
+  vi.restoreAllMocks();
+  resetFallbackApprovalDraftsForTest();
+});
+
+describe('actions approval draft decision API', () => {
+  it('records a manager-backed non-executing approval decision when the manager contract is live', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
+      code: 0,
+      data: {
+        draftId: 'draft-1',
+        decision: 'approved',
+        reviewer: 'ops-lead',
+        state: 'approval-draft-approved',
+        executionState: 'not-executed',
+        executionAllowed: false,
+        adapterOwner: 'manager-action-approval-draft'
+      }
+    }), { status: 200, headers: { 'content-type': 'application/json' } }));
+
+    const response = await POST(decisionRequest({
+      decision: 'approved',
+      reviewer: 'ops-lead',
+      reason: 'reviewed rollback evidence',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false
+    }), params());
+    const payload = await response.json();
+
+    expect(response.status).toBe(202);
+    expect(payload).toMatchObject({
+      draftId: 'draft-1',
+      decision: 'approved',
+      state: 'approval-draft-approved',
+      executionState: 'not-executed',
+      executionAllowed: false,
+      adapterOwner: 'manager-action-approval-draft',
+      managerBacked: true
+    });
+    expect(globalThis.fetch).toHaveBeenCalledWith(
+      'http://127.0.0.1:1157/api/actions/approval-drafts/draft-1/decision',
+      expect.objectContaining({ method: 'POST', cache: 'no-store' })
+    );
+  });
+
+  it('falls back to a local non-executing rejected decision when the manager route is unavailable', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('missing', { status: 404 }));
+
+    const response = await POST(decisionRequest({
+      decision: 'rejected',
+      reviewer: 'ops-lead',
+      reason: 'risk too high',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false
+    }), params());
+    const payload = await response.json();
+
+    expect(response.status).toBe(202);
+    expect(payload).toMatchObject({
+      draftId: 'draft-1',
+      decision: 'rejected',
+      reviewer: 'ops-lead',
+      reason: 'risk too high',
+      state: 'approval-draft-rejected',
+      executionState: 'not-executed',
+      executionAllowed: false,
+      adapterOwner: 'next-actions-approval-decision-bff',
+      managerBacked: false
+    });
+  });
+
+  it('rejects decisions that try to execute an action', async () => {
+    const response = await POST(decisionRequest({
+      decision: 'approved',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: true
+    }), params());
+    const payload = await response.json();
+
+    expect(response.status).toBe(409);
+    expect(payload).toEqual({
+      state: 'blocked',
+      message: 'approval decisions must stay manual and non-executing'
+    });
+  });
+});
diff --git a/web-next/app/api/actions/approval-drafts/[draftId]/decision/route.ts b/web-next/app/api/actions/approval-drafts/[draftId]/decision/route.ts
new file mode 100644
index 0000000000..8acddbf9ff
--- /dev/null
+++ b/web-next/app/api/actions/approval-drafts/[draftId]/decision/route.ts
@@ -0,0 +1,120 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { buildBackendApiUrl, HB_UI_ACCESS_COOKIE, readJsonPayload, readSessionCookieValue } from '../../../../../../lib/session-bff';
+import { decideFallbackApprovalDraft } from '../../../../../../lib/actions-approval-draft-fallback-store';
+
+export const dynamic = 'force-dynamic';
+
+type ApprovalDecisionPayload = {
+  decision?: unknown;
+  reviewer?: unknown;
+  reason?: unknown;
+  executionMode?: unknown;
+  executionAllowed?: unknown;
+};
+
+type ManagerMessagePayload = {
+  code?: number;
+  msg?: string;
+  data?: Record<string, unknown> | null;
+};
+
+function text(value: unknown) {
+  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
+}
+
+function validateDecisionPayload(payload: ApprovalDecisionPayload) {
+  const decision = text(payload.decision);
+  if (decision !== 'approved' && decision !== 'rejected') {
+    return { error: 'approval decision must be approved or rejected' };
+  }
+  if (payload.executionAllowed !== false || payload.executionMode !== 'manual-approval-draft-only') {
+    return { error: 'approval decisions must stay manual and non-executing', blocked: true };
+  }
+  return { decision };
+}
+
+function managerHeaders(request: NextRequest) {
+  const headers = new Headers();
+  headers.set('content-type', 'application/json');
+  const accessToken = readSessionCookieValue(request, HB_UI_ACCESS_COOKIE);
+  if (accessToken) {
+    headers.set('authorization', `Bearer ${accessToken}`);
+  }
+  const acceptLanguage = request.headers.get('accept-language');
+  if (acceptLanguage) {
+    headers.set('accept-language', acceptLanguage);
+  }
+  return headers;
+}
+
+function normalizeManagerDecision(data: Record<string, unknown>) {
+  return {
+    ...data,
+    executionAllowed: false,
+    executionState: text(data.executionState) || 'not-executed',
+    adapterOwner: text(data.adapterOwner) || 'manager-action-approval-draft',
+    managerBacked: true
+  };
+}
+
+function fallbackDecision(draftId: string, payload: ApprovalDecisionPayload, decision: string) {
+  return decideFallbackApprovalDraft(
+    draftId,
+    decision,
+    text(payload.reviewer) || 'hertzbeat-ui-operator',
+    text(payload.reason) || 'manual approval decision from actions workbench'
+  );
+}
+
+async function postManagerDecision(request: NextRequest, draftId: string, payload: ApprovalDecisionPayload) {
+  try {
+    const upstream = await fetch(
+      buildBackendApiUrl(`/actions/approval-drafts/${encodeURIComponent(draftId)}/decision`),
+      {
+        method: 'POST',
+        headers: managerHeaders(request),
+        body: JSON.stringify(payload),
+        cache: 'no-store'
+      }
+    );
+    if (upstream.status === 404) return null;
+    const managerPayload = await readJsonPayload(upstream) as ManagerMessagePayload;
+    if (!upstream.ok || managerPayload.code !== 0 || !managerPayload.data) {
+      return null;
+    }
+    return normalizeManagerDecision(managerPayload.data);
+  } catch {
+    return null;
+  }
+}
+
+export async function POST(
+  request: NextRequest,
+  { params }: { params: Promise<{ draftId?: string }> }
+) {
+  const { draftId } = await params;
+  if (!draftId) {
+    return NextResponse.json({ state: 'invalid-request', message: 'draftId is required' }, { status: 400 });
+  }
+
+  let payload: ApprovalDecisionPayload;
+  try {
+    payload = await request.json() as ApprovalDecisionPayload;
+  } catch {
+    return NextResponse.json({ state: 'invalid-request', message: 'invalid approval decision payload' }, { status: 400 });
+  }
+
+  const validation = validateDecisionPayload(payload);
+  if (validation.error) {
+    return NextResponse.json(
+      { state: validation.blocked ? 'blocked' : 'invalid-request', message: validation.error },
+      { status: validation.blocked ? 409 : 400 }
+    );
+  }
+
+  const managerDecision = await postManagerDecision(request, draftId, payload);
+  return NextResponse.json(
+    managerDecision || fallbackDecision(draftId, payload, validation.decision as string),
+    { status: 202 }
+  );
+}
diff --git a/web-next/app/api/actions/approval-drafts/route.test.ts b/web-next/app/api/actions/approval-drafts/route.test.ts
new file mode 100644
index 0000000000..0422130264
--- /dev/null
+++ b/web-next/app/api/actions/approval-drafts/route.test.ts
@@ -0,0 +1,258 @@
+import { NextRequest } from 'next/server';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { GET, POST } from './route';
+import { POST as POST_DECISION } from './[draftId]/decision/route';
+import { resetFallbackApprovalDraftsForTest } from '../../../../lib/actions-approval-draft-fallback-store';
+
+function approvalDraftRequest(body: unknown) {
+  return new NextRequest('http://localhost/api/actions/approval-drafts', {
+    method: 'POST',
+    body: JSON.stringify(body),
+    headers: { 'content-type': 'application/json' }
+  });
+}
+
+function approvalDraftListRequest() {
+  return new NextRequest('http://localhost/api/actions/approval-drafts?limit=8', {
+    method: 'GET',
+    headers: { cookie: 'hb_ui_access=session-token' }
+  });
+}
+
+function approvalDraftDecisionRequest(draftId: string) {
+  return new NextRequest(`http://localhost/api/actions/approval-drafts/${draftId}/decision`, {
+    method: 'POST',
+    body: JSON.stringify({
+      decision: 'approved',
+      reviewer: 'ops-lead',
+      reason: 'reviewed fallback lifecycle evidence',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false
+    }),
+    headers: {
+      'content-type': 'application/json',
+      cookie: 'hb_ui_access=session-token'
+    }
+  });
+}
+
+function approvalDraftDecisionParams(draftId: string) {
+  return { params: Promise.resolve({ draftId }) };
+}
+
+afterEach(() => {
+  vi.restoreAllMocks();
+  resetFallbackApprovalDraftsForTest();
+});
+
+describe('actions approval draft API', () => {
+  it('creates a manager-backed non-executing approval draft when the manager contract is live', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
+      code: 0,
+      data: {
+        draftId: 'manager-draft-1',
+        state: 'approval-draft-created',
+        executionState: 'not-executed',
+        executionAllowed: false,
+        adapterOwner: 'manager-action-approval-draft',
+        actionId: 'suggest-restart-checkout',
+        catalogId: 'restart-checkout'
+      }
+    }), { status: 200, headers: { 'content-type': 'application/json' } }));
+
+    const response = await POST(approvalDraftRequest({
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout',
+      confirmation: 'manual-required',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      context: {
+        source: 'alert',
+        entityId: 'service:commerce/checkout',
+        traceId: 'trace-123'
+      },
+      evidenceHref: '/alert?status=firing'
+    }));
+    const payload = await response.json();
+
+    expect(response.status).toBe(202);
+    expect(payload).toMatchObject({
+      draftId: 'manager-draft-1',
+      state: 'approval-draft-created',
+      executionState: 'not-executed',
+      executionAllowed: false,
+      adapterOwner: 'manager-action-approval-draft',
+      managerBacked: true,
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout'
+    });
+    expect(globalThis.fetch).toHaveBeenCalledWith(
+      'http://127.0.0.1:1157/api/actions/approval-drafts',
+      expect.objectContaining({ method: 'POST', cache: 'no-store' })
+    );
+  });
+
+  it('falls back to a local non-executing approval draft when the manager route is unavailable', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('missing', { status: 404 }));
+
+    const response = await POST(approvalDraftRequest({
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout',
+      confirmation: 'manual-required',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      context: {
+        source: 'alert',
+        entityId: 'service:commerce/checkout',
+        traceId: 'trace-123'
+      },
+      evidenceHref: '/alert?status=firing'
+    }));
+    const payload = await response.json();
+
+    expect(response.status).toBe(202);
+    expect(payload).toMatchObject({
+      state: 'approval-draft-created',
+      executionState: 'not-executed',
+      executionAllowed: false,
+      adapterOwner: 'next-actions-approval-draft-bff',
+      managerBacked: false,
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout'
+    });
+    expect(payload.draftId).toMatch(/^approval-draft-suggest-restart-checkout-/);
+  });
+
+  it('lists locally created fallback approval drafts until the manager read contract is live', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('missing', { status: 404 }));
+
+    const createResponse = await POST(approvalDraftRequest({
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout',
+      confirmation: 'manual-required',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      context: {
+        source: 'alert',
+        entityId: 'service:commerce/checkout',
+        traceId: 'trace-123'
+      },
+      evidenceHref: '/alert?status=firing'
+    }));
+    const created = await createResponse.json();
+    const listResponse = await GET(approvalDraftListRequest());
+    const listed = await listResponse.json();
+
+    expect(listed).toMatchObject({
+      state: 'fallback-local-drafts',
+      adapterOwner: 'next-actions-approval-draft-bff',
+      managerBacked: false,
+      drafts: [
+        {
+          draftId: created.draftId,
+          state: 'approval-draft-created',
+          executionState: 'not-executed',
+          executionAllowed: false,
+          actionId: 'suggest-restart-checkout',
+          catalogId: 'restart-checkout'
+        }
+      ]
+    });
+  });
+
+  it('lists locally approved fallback approval drafts from the shared runtime store', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('missing', { status: 404 }));
+
+    const createResponse = await POST(approvalDraftRequest({
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout',
+      confirmation: 'manual-required',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      context: {
+        source: 'alert',
+        entityId: 'service:commerce/checkout',
+        traceId: 'trace-123'
+      },
+      evidenceHref: '/alert?status=firing'
+    }));
+    const created = await createResponse.json();
+    await POST_DECISION(
+      approvalDraftDecisionRequest(created.draftId),
+      approvalDraftDecisionParams(created.draftId)
+    );
+
+    const listResponse = await GET(approvalDraftListRequest());
+    const listed = await listResponse.json();
+
+    expect(listed).toMatchObject({
+      state: 'fallback-local-drafts',
+      adapterOwner: 'next-actions-approval-draft-bff',
+      managerBacked: false,
+      drafts: [
+        {
+          draftId: created.draftId,
+          state: 'approval-draft-approved',
+          executionState: 'not-executed',
+          executionAllowed: false,
+          adapterOwner: 'next-actions-approval-decision-bff',
+          actionId: 'suggest-restart-checkout',
+          catalogId: 'restart-checkout'
+        }
+      ]
+    });
+  });
+
+  it('rejects payloads that try to bypass manual non-execution guardrails', async () => {
+    const response = await POST(approvalDraftRequest({
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout',
+      confirmation: 'manual-required',
+      executionMode: 'execute-now',
+      executionAllowed: true
+    }));
+    const payload = await response.json();
+
+    expect(response.status).toBe(409);
+    expect(payload).toEqual({
+      state: 'blocked',
+      message: 'approval drafts must stay manual and non-executing'
+    });
+  });
+
+  it('lists manager-backed approval drafts when the manager read contract is live', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
+      code: 0,
+      data: [
+        {
+          draftId: 'manager-draft-1',
+          state: 'approval-draft-created',
+          executionState: 'not-executed',
+          executionAllowed: false,
+          adapterOwner: 'manager-action-approval-draft'
+        }
+      ]
+    }), { status: 200, headers: { 'content-type': 'application/json' } }));
+
+    const response = await GET(approvalDraftListRequest());
+    const payload = await response.json();
+
+    expect(payload).toMatchObject({
+      state: 'manager-approval-drafts',
+      adapterOwner: 'manager-action-approval-draft',
+      managerBacked: true,
+      drafts: [
+        {
+          draftId: 'manager-draft-1',
+          executionState: 'not-executed',
+          executionAllowed: false,
+          managerBacked: true
+        }
+      ]
+    });
+    expect(globalThis.fetch).toHaveBeenCalledWith(
+      'http://127.0.0.1:1157/api/actions/approval-drafts?limit=8',
+      expect.objectContaining({ method: 'GET', cache: 'no-store' })
+    );
+  });
+});
diff --git a/web-next/app/api/actions/approval-drafts/route.ts b/web-next/app/api/actions/approval-drafts/route.ts
new file mode 100644
index 0000000000..e4c328a417
--- /dev/null
+++ b/web-next/app/api/actions/approval-drafts/route.ts
@@ -0,0 +1,162 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { buildBackendApiUrl, HB_UI_ACCESS_COOKIE, readJsonPayload, readSessionCookieValue } from '../../../../lib/session-bff';
+import { listFallbackApprovalDrafts, saveFallbackApprovalDraft } from '../../../../lib/actions-approval-draft-fallback-store';
+
+export const dynamic = 'force-dynamic';
+
+type ApprovalDraftPayload = {
+  draftId?: unknown;
+  actionId?: unknown;
+  catalogId?: unknown;
+  confirmation?: unknown;
+  executionMode?: unknown;
+  executionAllowed?: unknown;
+  context?: unknown;
+  evidenceHref?: unknown;
+};
+
+type ManagerMessagePayload = {
+  code?: number;
+  msg?: string;
+  data?: Record<string, unknown> | Record<string, unknown>[] | null;
+};
+
+function stableDraftSuffix(value: string) {
+  let hash = 0;
+  for (let index = 0; index < value.length; index += 1) {
+    hash = (hash * 31 + value.charCodeAt(index)) % 100000;
+  }
+  return String(hash).padStart(5, '0');
+}
+
+function text(value: unknown) {
+  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
+}
+
+function validatePayload(payload: ApprovalDraftPayload) {
+  const actionId = text(payload.actionId);
+  const catalogId = text(payload.catalogId);
+  if (!actionId || !catalogId) {
+    return { error: 'actionId and catalogId are required' };
+  }
+
+  if (payload.confirmation !== 'manual-required' || payload.executionMode !== 'manual-approval-draft-only' || payload.executionAllowed !== false) {
+    return { error: 'approval drafts must stay manual and non-executing', blocked: true };
+  }
+
+  return { actionId, catalogId };
+}
+
+function fallbackDraft(payload: ApprovalDraftPayload, actionId: string, catalogId: string) {
+  const context = payload.context && typeof payload.context === 'object' ? payload.context : {};
+  const fingerprint = stableDraftSuffix(JSON.stringify({ actionId, catalogId, context, evidenceHref: payload.evidenceHref }));
+
+  return saveFallbackApprovalDraft({
+    draftId: text(payload.draftId) || `approval-draft-${actionId}-${fingerprint}`,
+    state: 'approval-draft-created',
+    executionState: 'not-executed',
+    executionAllowed: false,
+    adapterOwner: 'next-actions-approval-draft-bff',
+    managerBacked: false,
+    actionId,
+    catalogId
+  });
+}
+
+function managerHeaders(request: NextRequest) {
+  const headers = new Headers();
+  headers.set('content-type', 'application/json');
+  const accessToken = readSessionCookieValue(request, HB_UI_ACCESS_COOKIE);
+  if (accessToken) {
+    headers.set('authorization', `Bearer ${accessToken}`);
+  }
+  const acceptLanguage = request.headers.get('accept-language');
+  if (acceptLanguage) {
+    headers.set('accept-language', acceptLanguage);
+  }
+  return headers;
+}
+
+function normalizeManagerDraft(data: Record<string, unknown>) {
+  return {
+    ...data,
+    executionAllowed: false,
+    executionState: text(data.executionState) || 'not-executed',
+    adapterOwner: text(data.adapterOwner) || 'manager-action-approval-draft',
+    managerBacked: true
+  };
+}
+
+async function postManagerApprovalDraft(request: NextRequest, payload: ApprovalDraftPayload) {
+  try {
+    const upstream = await fetch(buildBackendApiUrl('/actions/approval-drafts'), {
+      method: 'POST',
+      headers: managerHeaders(request),
+      body: JSON.stringify(payload),
+      cache: 'no-store'
+    });
+    if (upstream.status === 404) return null;
+    const managerPayload = await readJsonPayload(upstream) as ManagerMessagePayload;
+    if (!upstream.ok || managerPayload.code !== 0 || !managerPayload.data || Array.isArray(managerPayload.data)) {
+      return null;
+    }
+    return normalizeManagerDraft(managerPayload.data);
+  } catch {
+    return null;
+  }
+}
+
+async function getManagerApprovalDrafts(request: NextRequest) {
+  const requestUrl = new URL(request.url);
+  try {
+    const upstream = await fetch(buildBackendApiUrl('/actions/approval-drafts', requestUrl.search), {
+      method: 'GET',
+      headers: managerHeaders(request),
+      cache: 'no-store'
+    });
+    if (upstream.status === 404) return null;
+    const managerPayload = await readJsonPayload(upstream) as ManagerMessagePayload;
+    if (!upstream.ok || managerPayload.code !== 0 || !Array.isArray(managerPayload.data)) {
+      return null;
+    }
+    return managerPayload.data.map(item => normalizeManagerDraft(item));
+  } catch {
+    return null;
+  }
+}
+
+export async function POST(request: NextRequest) {
+  let payload: ApprovalDraftPayload;
+  try {
+    payload = await request.json() as ApprovalDraftPayload;
+  } catch {
+    return NextResponse.json({ state: 'invalid-request', message: 'invalid approval draft payload' }, { status: 400 });
+  }
+
+  const validation = validatePayload(payload);
+  if (validation.error) {
+    return NextResponse.json(
+      { state: validation.blocked ? 'blocked' : 'invalid-request', message: validation.error },
+      { status: validation.blocked ? 409 : 400 }
+    );
+  }
+
+  const managerDraft = await postManagerApprovalDraft(request, payload);
+  return NextResponse.json(
+    managerDraft || fallbackDraft(payload, validation.actionId as string, validation.catalogId as string),
+    { status: 202 }
+  );
+}
+
+export async function GET(request: NextRequest) {
+  const managerDrafts = await getManagerApprovalDrafts(request);
+  const requestUrl = new URL(request.url);
+  const limit = Number(requestUrl.searchParams.get('limit') || '8');
+  const fallbackDrafts = managerDrafts ? [] : listFallbackApprovalDrafts(Number.isFinite(limit) ? limit : 8);
+  return NextResponse.json({
+    state: managerDrafts ? 'manager-approval-drafts' : fallbackDrafts.length > 0 ? 'fallback-local-drafts' : 'fallback-empty',
+    adapterOwner: managerDrafts ? 'manager-action-approval-draft' : 'next-actions-approval-draft-bff',
+    managerBacked: Boolean(managerDrafts),
+    drafts: managerDrafts || fallbackDrafts
+  });
+}
diff --git a/web-next/app/api/actions/catalog/route.test.ts b/web-next/app/api/actions/catalog/route.test.ts
new file mode 100644
index 0000000000..c3aebd9fb9
--- /dev/null
+++ b/web-next/app/api/actions/catalog/route.test.ts
@@ -0,0 +1,155 @@
+import { NextRequest } from 'next/server';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { GET, POST } from './route';
+
+function catalogPostRequest(body: unknown) {
+  return new NextRequest('http://localhost/api/actions/catalog', {
+    method: 'POST',
+    body: JSON.stringify(body),
+    headers: {
+      'content-type': 'application/json',
+      cookie: 'hb_ui_access=session-token'
+    }
+  });
+}
+
+function catalogListRequest() {
+  return new NextRequest('http://localhost/api/actions/catalog?limit=8', {
+    method: 'GET',
+    headers: { cookie: 'hb_ui_access=session-token' }
+  });
+}
+
+afterEach(() => {
+  vi.restoreAllMocks();
+});
+
+describe('actions catalog API', () => {
+  it('saves a manager-backed manual-only catalog item when the manager contract is live', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
+      code: 0,
+      data: {
+        catalogId: 'restart-checkout',
+        name: 'Restart checkout service',
+        risk: 'high',
+        executionMode: 'manual-approval-draft-only',
+        executionAllowed: false,
+        adapterOwner: 'manager-action-catalog',
+        status: 'catalog-item-ready'
+      }
+    }), { status: 200, headers: { 'content-type': 'application/json' } }));
+
+    const response = await POST(catalogPostRequest({
+      catalogId: 'restart-checkout',
+      name: 'Restart checkout service',
+      risk: 'high',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      metadata: { playbook: 'checkout-restart' }
+    }));
+    const payload = await response.json();
+
+    expect(response.status).toBe(200);
+    expect(payload).toMatchObject({
+      catalogId: 'restart-checkout',
+      name: 'Restart checkout service',
+      risk: 'high',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      adapterOwner: 'manager-action-catalog',
+      status: 'catalog-item-ready',
+      managerBacked: true
+    });
+    expect(globalThis.fetch).toHaveBeenCalledWith(
+      'http://127.0.0.1:1157/api/actions/catalog',
+      expect.objectContaining({ method: 'POST', cache: 'no-store' })
+    );
+  });
+
+  it('falls back to a local non-executing non-persisted catalog item when manager is unavailable', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('missing', { status: 404 }));
+
+    const response = await POST(catalogPostRequest({
+      catalogId: 'restart-checkout',
+      name: 'Restart checkout service',
+      risk: 'high',
+      category: 'remediation',
+      scope: 'service:commerce/checkout',
+      owner: 'sre',
+      executionAllowed: false,
+      metadata: { playbook: 'checkout-restart' }
+    }));
+    const payload = await response.json();
+
+    expect(response.status).toBe(202);
+    expect(payload).toMatchObject({
+      catalogId: 'restart-checkout',
+      name: 'Restart checkout service',
+      risk: 'high',
+      category: 'remediation',
+      scope: 'service:commerce/checkout',
+      owner: 'sre',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      adapterOwner: 'next-actions-catalog-bff',
+      status: 'catalog-item-fallback-not-persisted',
+      managerBacked: false,
+      metadata: { playbook: 'checkout-restart' }
+    });
+  });
+
+  it('rejects catalog payloads that try to enable direct execution', async () => {
+    const response = await POST(catalogPostRequest({
+      catalogId: 'restart-checkout',
+      name: 'Restart checkout service',
+      risk: 'high',
+      executionMode: 'execute-now',
+      executionAllowed: true
+    }));
+    const payload = await response.json();
+
+    expect(response.status).toBe(409);
+    expect(payload).toEqual({
+      state: 'blocked',
+      message: 'action catalog items must stay manual and non-executing'
+    });
+  });
+
+  it('lists manager-backed catalog items when the manager read contract is live', async () => {
+    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
+      code: 0,
+      data: [
+        {
+          catalogId: 'restart-checkout',
+          name: 'Restart checkout service',
+          risk: 'high',
+          executionMode: 'manual-approval-draft-only',
+          executionAllowed: false,
+          adapterOwner: 'manager-action-catalog',
+          status: 'catalog-item-ready'
+        }
+      ]
+    }), { status: 200, headers: { 'content-type': 'application/json' } }));
+
+    const response = await GET(catalogListRequest());
+    const payload = await response.json();
+
+    expect(payload).toMatchObject({
+      state: 'manager-action-catalog',
+      adapterOwner: 'manager-action-catalog',
+      managerBacked: true,
+      items: [
+        {
+          catalogId: 'restart-checkout',
+          executionMode: 'manual-approval-draft-only',
+          executionAllowed: false,
+          managerBacked: true
+        }
+      ]
+    });
+    expect(globalThis.fetch).toHaveBeenCalledWith(
+      'http://127.0.0.1:1157/api/actions/catalog?limit=8',
+      expect.objectContaining({ method: 'GET', cache: 'no-store' })
+    );
+  });
+});
diff --git a/web-next/app/api/actions/catalog/route.ts b/web-next/app/api/actions/catalog/route.ts
new file mode 100644
index 0000000000..58ee2ce4db
--- /dev/null
+++ b/web-next/app/api/actions/catalog/route.ts
@@ -0,0 +1,165 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { buildBackendApiUrl, HB_UI_ACCESS_COOKIE, readJsonPayload, readSessionCookieValue } from '../../../../lib/session-bff';
+
+export const dynamic = 'force-dynamic';
+
+type ActionCatalogPayload = {
+  catalogId?: unknown;
+  name?: unknown;
+  category?: unknown;
+  scope?: unknown;
+  owner?: unknown;
+  risk?: unknown;
+  executionMode?: unknown;
+  executionAllowed?: unknown;
+  metadata?: unknown;
+};
+
+type ManagerMessagePayload = {
+  code?: number;
+  msg?: string;
+  data?: Record<string, unknown> | Record<string, unknown>[] | null;
+};
+
+function text(value: unknown) {
+  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
+}
+
+function validatePayload(payload: ActionCatalogPayload) {
+  const catalogId = text(payload.catalogId);
+  const name = text(payload.name);
+  const risk = text(payload.risk);
+  if (!catalogId || !name || !risk) {
+    return { error: 'catalogId, name and risk are required' };
+  }
+
+  const executionMode = text(payload.executionMode);
+  if ((executionMode && executionMode !== 'manual-approval-draft-only') || payload.executionAllowed === true) {
+    return { error: 'action catalog items must stay manual and non-executing', blocked: true };
+  }
+
+  return { catalogId, name, risk };
+}
+
+function managerHeaders(request: NextRequest) {
+  const headers = new Headers();
+  headers.set('content-type', 'application/json');
+  const accessToken = readSessionCookieValue(request, HB_UI_ACCESS_COOKIE);
+  if (accessToken) {
+    headers.set('authorization', `Bearer ${accessToken}`);
+  }
+  const acceptLanguage = request.headers.get('accept-language');
+  if (acceptLanguage) {
+    headers.set('accept-language', acceptLanguage);
+  }
+  return headers;
+}
+
+function normalizeManagerCatalogItem(data: Record<string, unknown>) {
+  return {
+    ...data,
+    executionAllowed: false,
+    executionMode: text(data.executionMode) || 'manual-approval-draft-only',
+    adapterOwner: text(data.adapterOwner) || 'manager-action-catalog',
+    status: text(data.status) || 'catalog-item-ready',
+    managerBacked: true
+  };
+}
+
+function fallbackCatalogItem(payload: ActionCatalogPayload, catalogId: string, name: string, risk: string) {
+  const metadata = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {};
+  return {
+    catalogId,
+    name,
+    risk,
+    category: text(payload.category),
+    scope: text(payload.scope),
+    owner: text(payload.owner),
+    executionMode: 'manual-approval-draft-only',
+    executionAllowed: false,
+    adapterOwner: 'next-actions-catalog-bff',
+    status: 'catalog-item-fallback-not-persisted',
+    managerBacked: false,
+    metadata
+  };
+}
+
+async function postManagerCatalogItem(request: NextRequest, payload: ActionCatalogPayload) {
+  try {
+    const upstream = await fetch(buildBackendApiUrl('/actions/catalog'), {
+      method: 'POST',
+      headers: managerHeaders(request),
+      body: JSON.stringify({
+        ...payload,
+        executionMode: 'manual-approval-draft-only',
+        executionAllowed: false
+      }),
+      cache: 'no-store'
+    });
+    if (upstream.status === 404) return null;
+    const managerPayload = await readJsonPayload(upstream) as ManagerMessagePayload;
+    if (!upstream.ok || managerPayload.code !== 0 || !managerPayload.data || Array.isArray(managerPayload.data)) {
+      return null;
+    }
+    return normalizeManagerCatalogItem(managerPayload.data);
+  } catch {
+    return null;
+  }
+}
+
+async function getManagerCatalogItems(request: NextRequest) {
+  const requestUrl = new URL(request.url);
+  try {
+    const upstream = await fetch(buildBackendApiUrl('/actions/catalog', requestUrl.search), {
+      method: 'GET',
+      headers: managerHeaders(request),
+      cache: 'no-store'
+    });
+    if (upstream.status === 404) return null;
+    const managerPayload = await readJsonPayload(upstream) as ManagerMessagePayload;
+    if (!upstream.ok || managerPayload.code !== 0 || !Array.isArray(managerPayload.data)) {
+      return null;
+    }
+    return managerPayload.data.map(item => normalizeManagerCatalogItem(item));
+  } catch {
+    return null;
+  }
+}
+
+export async function POST(request: NextRequest) {
+  let payload: ActionCatalogPayload;
+  try {
+    payload = await request.json() as ActionCatalogPayload;
+  } catch {
+    return NextResponse.json({ state: 'invalid-request', message: 'invalid action catalog payload' }, { status: 400 });
+  }
+
+  const validation = validatePayload(payload);
+  if (validation.error) {
+    return NextResponse.json(
+      { state: validation.blocked ? 'blocked' : 'invalid-request', message: validation.error },
+      { status: validation.blocked ? 409 : 400 }
+    );
+  }
+
+  const managerItem = await postManagerCatalogItem(request, payload);
+  return NextResponse.json(
+    managerItem || fallbackCatalogItem(
+      payload,
+      validation.catalogId as string,
+      validation.name as string,
+      validation.risk as string
+    ),
+    { status: managerItem ? 200 : 202 }
+  );
+}
+
+export async function GET(request: NextRequest) {
+  const managerItems = await getManagerCatalogItems(request);
+  return NextResponse.json({
+    state: managerItems ? 'manager-action-catalog' : 'fallback-empty',
+    adapterOwner: managerItems ? 'manager-action-catalog' : 'next-actions-catalog-bff',
+    managerBacked: Boolean(managerItems),
+    items: managerItems || []
+  });
+}
diff --git a/web-next/app/incidents/incidents-page.tsx b/web-next/app/incidents/incidents-page.tsx
new file mode 100644
index 0000000000..c382340e2d
--- /dev/null
+++ b/web-next/app/incidents/incidents-page.tsx
@@ -0,0 +1,154 @@
+'use client';
+
+import React, { useCallback, useMemo, useState } from 'react';
+import { HzIncidentWorkbench } from '@hertzbeat/ui';
+import { ClientWorkbench } from '../../components/workbench/client-workbench';
+import { useI18n } from '../../components/providers/i18n-provider';
+import { apiMessageGet, apiMessagePut } from '../../lib/api-client';
+import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
+import { formatTime } from '../../lib/format';
+import {
+  INCIDENT_WORKBENCH_DEFAULT_QUERY,
+  type IncidentTransitionState,
+  loadIncidentWorkbenchData,
+  transitionIncidentStatus,
+  type IncidentWorkbenchData
+} from '../../lib/incidents-surface/controller';
+import type { StatusIncidentListQuery } from '../../lib/setting-status/controller';
+
+const INCIDENT_WORKBENCH_SETTLED_CACHE_TTL_MS = 10_000;
+const INCIDENT_TRANSITION_STATES: IncidentTransitionState[] = [1, 2, 3];
+
+function IncidentWorkbenchSurface({
+  state,
+  onTransition
+}: {
+  state: IncidentWorkbenchData;
+  onTransition: (incident: NonNullable<IncidentWorkbenchData['selectedIncident']>, nextState: IncidentTransitionState) => Promise<void>;
+}) {
+  const coldOpsVisual = coldOpsCatalogVisual;
+  const { t } = useI18n();
+  const [transitionBusy, setTransitionBusy] = useState<IncidentTransitionState | null>(null);
+  const transitionDisabled = state.transitionState !== 'ready' || !state.selectedIncident || transitionBusy != null;
+
+  async function handleTransition(nextState: IncidentTransitionState) {
+    if (!state.selectedIncident || transitionBusy != null) {
+      return;
+    }
+    setTransitionBusy(nextState);
+    try {
+      await onTransition(state.selectedIncident, nextState);
+    } finally {
+      setTransitionBusy(null);
+    }
+  }
+
+  return (
+    <main
+      className={coldOpsVisual.entry.main}
+      data-incidents-route="incident-workbench-api-ui-lab-shared"
+      data-incidents-style-baseline={coldOpsVisual.canvasName}
+      data-incidents-workbench="hertzbeat-ui"
+      data-incidents-api-owner="status-page-incident-api"
+      data-incidents-api-source={state.apiSource}
+      data-incidents-api-state={state.apiState}
+      data-incidents-api-total={state.totalElements}
+      data-incidents-detail-owner="status-page-incident-detail-api"
+      data-incidents-detail-source={state.detailSource}
+      data-incidents-detail-state={state.detailState}
+      data-incidents-detail-id={state.detailId || ''}
+      data-incidents-transition-owner="status-page-incident-put-api"
+      data-incidents-transition-source="/status/page/incident"
+      data-incidents-transition-state={state.transitionState}
+      data-incidents-transition-pending={transitionBusy == null ? 'none' : String(transitionBusy)}
+      data-incidents-query-contract="angular-search-pagination"
+      data-incidents-query-label={state.queryLabel}
+    >
+      <div className={coldOpsVisual.entry.container}>
+        <HzIncidentWorkbench
+          data-incidents-shared-workbench="hertzbeat-ui"
+          title={state.title}
+          subtitle={state.subtitle}
+          sourceLabel={state.kicker}
+          queryLabel={state.queryLabel}
+          metrics={state.metrics}
+          incidents={state.incidents}
+          timeline={state.timelineRows.map((item, index) => ({
+            id: item.id || `incident-timeline-${index + 1}`,
+            title: item.title,
+            copy: item.copy,
+            meta: item.meta,
+            tone: item.tone || (index === 0 ? 'warning' : 'info')
+          }))}
+          ownership={state.ownershipRows.map((item, index) => ({
+            id: item.id || `incident-owner-${index + 1}`,
+            owner: item.owner,
+            queue: item.queue,
+            copy: item.copy,
+            meta: item.meta,
+            tone: item.tone || (index === 0 ? 'info' : 'neutral')
+          }))}
+          selectedIncidentId={state.selectedIncidentId}
+          actions={state.nextHops.map(action => ({
+            label: action.label,
+            href: action.href,
+            variant: action.variant === 'default' ? 'default' : action.variant
+          }))}
+          transitionLabel={t('common.status')}
+          transitionActions={INCIDENT_TRANSITION_STATES.map(nextState => ({
+            id: `state-${nextState}`,
+            label: t(`status.incident.state.${nextState}`),
+            state: nextState,
+            variant: nextState === 3 ? 'primary' : 'default',
+            disabled: transitionDisabled,
+            pending: transitionBusy === nextState,
+            onClick: () => {
+              void handleTransition(nextState);
+            }
+          }))}
+        />
+      </div>
+    </main>
+  );
+}
+
+export default function IncidentsPage({ initialQuery = INCIDENT_WORKBENCH_DEFAULT_QUERY }: { initialQuery?: StatusIncidentListQuery } = {}) {
+  const { t } = useI18n();
+  const [refreshTick, setRefreshTick] = useState(0);
+  const query = useMemo(
+    () => ({
+      search: initialQuery.search || '',
+      pageIndex: initialQuery.pageIndex ?? INCIDENT_WORKBENCH_DEFAULT_QUERY.pageIndex,
+      pageSize: initialQuery.pageSize ?? INCIDENT_WORKBENCH_DEFAULT_QUERY.pageSize
+    }),
+    [initialQuery.pageIndex, initialQuery.pageSize, initialQuery.search]
+  );
+  const incidentWorkbenchCacheKey = useMemo(
+    () => ['incident-workbench', query.search, query.pageIndex, query.pageSize, refreshTick].join(':'),
+    [query, refreshTick]
+  );
+  const load = useCallback(() => {
+    void refreshTick;
+    return loadIncidentWorkbenchData(apiMessageGet, t, formatTime, query);
+  }, [query, refreshTick, t]);
+  const handleTransition = useCallback(async (incident: NonNullable<IncidentWorkbenchData['selectedIncident']>, nextState: IncidentTransitionState) => {
+    await transitionIncidentStatus(
+      apiMessagePut,
+      incident,
+      nextState,
+      `${t(`status.incident.state.${nextState}`)} · /incidents`
+    );
+    setRefreshTick(value => value + 1);
+  }, [t]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('common.workbench.loading.copy')}
+      cacheKey={incidentWorkbenchCacheKey}
+      cacheSettledTtlMs={INCIDENT_WORKBENCH_SETTLED_CACHE_TTL_MS}
+    >
+      {state => <IncidentWorkbenchSurface state={state} onTransition={handleTransition} />}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/incidents/page.test.tsx b/web-next/app/incidents/page.test.tsx
index 49542e1ae2..6eae1a998f 100644
--- a/web-next/app/incidents/page.test.tsx
+++ b/web-next/app/incidents/page.test.tsx
@@ -2,46 +2,259 @@ import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+
+const t = createTranslatorMock({ locale: 'zh-CN' });
+const loadState = vi.hoisted(() => ({
+  lastLoad: null as null | (() => Promise<unknown>)
+}));
+const apiMessageGet = vi.hoisted(() => vi.fn());
+const apiMessagePut = vi.hoisted(() => vi.fn());
+
+vi.mock('../../components/providers/i18n-provider', () => ({
+  useI18n: () => ({ t })
+}));
+
+vi.mock('../../components/workbench/client-workbench', () => ({
+  ClientWorkbench: ({ children, load, loadingCopy, cacheKey, cacheSettledTtlMs }: any) => {
+    loadState.lastLoad = load;
+    return (
+      <div
+        data-client-workbench="true"
+        data-loading-copy={loadingCopy}
+        data-cache-key={cacheKey}
+        data-cache-ttl={cacheSettledTtlMs}
+      >
+        {children({
+          apiState: 'ready',
+          apiSource: 'status-page-incident-list',
+          detailState: 'ready',
+          detailSource: 'status-page-incident-detail',
+          detailId: '42',
+          queryLabel: '/status/page/incident?pageIndex=0&pageSize=8',
+          title: '故障事件',
+          subtitle: '按 OTLP 工作台的冷色基线统一响应时间线、责任人和证据入口。',
+          kicker: '事件响应台',
+          metrics: [
+            { label: '打开事件', value: '1', tone: 'warning' },
+            { label: '严重事件', value: '1', tone: 'critical' },
+            { label: '缓解中', value: '1', tone: 'info' },
+            { label: '责任队列', value: '1', tone: 'info' }
+          ],
+          incidents: [
+            {
+              id: '42',
+              title: 'API 延迟事件',
+              severity: 'critical',
+              stage: '调查中',
+              service: 'api-gateway',
+              owner: 'platform-oncall',
+              openedAt: '2026-04-10 18:00:00',
+              blastRadius: '1 组件'
+            }
+          ],
+          timelineRows: [
+            {
+              id: 'incident-timeline-42-8',
+              title: '2026-04-10 18:05:00 · 已识别',
+              copy: '已开始回滚',
+              meta: 'API 延迟事件',
+              tone: 'warning'
+            }
+          ],
+          ownershipRows: [
+            {
+              id: 'incident-owner-42',
+              owner: 'platform-oncall',
+              queue: '调查中',
+              copy: 'API 延迟事件',
+              meta: '1 组件 · 2026-04-10 18:00:00',
+              tone: 'critical'
+            }
+          ],
+          nextHops: [
+            { label: '打开概览', href: '/overview', variant: 'subtle' },
+            { label: '查看日志', href: '/log/manage', variant: 'default' }
+          ],
+          selectedIncidentId: '42',
+          selectedIncident: {
+            id: 42,
+            name: 'API 延迟事件',
+            state: 1,
+            contents: []
+          },
+          transitionState: 'ready',
+          totalElements: 1
+        })}
+      </div>
+    );
+  }
+}));
+
+vi.mock('../../lib/api-client', () => ({
+  apiMessageGet,
+  apiMessagePut
+}));
 
 describe('incidents page', () => {
-  it('renders the OTLP cold-matte entry shell without the previous placeholder stack', async () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/incidents/page.tsx'), 'utf8');
+  it('renders the API-backed UI Lab shared incident workbench instead of the placeholder entry shell', async () => {
+    loadState.lastLoad = null;
+    apiMessageGet.mockReset();
+    apiMessagePut.mockReset();
+    apiMessageGet
+      .mockResolvedValueOnce({
+        content: [
+          {
+            id: 42,
+            title: 'API 延迟事件',
+            state: 0,
+            startTime: 1712730000000,
+            creator: 'platform-oncall',
+            components: [{ id: 7, name: 'api-gateway' }],
+            contents: []
+          }
+        ],
+        totalElements: 1,
+        pageIndex: 0,
+        pageSize: 8
+      })
+      .mockResolvedValueOnce({
+        id: 42,
+        name: 'API 延迟事件详情',
+        state: 1,
+        startTime: 1712730000000,
+        modifier: 'platform-oncall',
+        components: [{ id: 7, name: 'api-gateway' }],
+        contents: [{ id: 8, state: 1, message: '详情已加载', timestamp: 1712730300000 }]
+      });
+    const routeSource = readFileSync(resolve(process.cwd(), 'app/incidents/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/incidents/incidents-page.tsx'), 'utf8');
     const { default: IncidentsPage } = await import('./page');
-    const html = renderToStaticMarkup(<IncidentsPage />);
+    const html = renderToStaticMarkup(await IncidentsPage());
+    const lastLoad = loadState.lastLoad as (() => Promise<unknown>) | null;
+    await lastLoad?.();
 
-    expect(html).toContain('data-incidents-route="otlp-cold-ops-entry"');
+    expect(routeSource).toContain("import IncidentsPage from './incidents-page'");
+    expect(routeSource).toContain('readIncidentWorkbenchQuery');
+    expect(routeSource).toContain('const resolvedSearchParams = await searchParams');
+    expect(routeSource).toContain('return <IncidentsPage initialQuery={initialQuery} />');
+    expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-loading-copy="正在准备页面数据,请稍等。"');
+    expect(html).toContain('data-cache-key="incident-workbench::0:8:0"');
+    expect(html).toContain('data-cache-ttl="10000"');
+    expect(html).toContain('data-incidents-route="incident-workbench-api-ui-lab-shared"');
     expect(html).toContain('data-incidents-style-baseline="hertzbeat-cold-matte"');
-    expect(html).toContain('data-incidents-shell-panel="cold-ops-shell-panel"');
-    expect(html).toContain('data-incidents-launch-checklist="cold-ops-static-rail"');
-    expect(html).toContain('data-incidents-empty-state="cold-ops-domain-adapter"');
+    expect(html).toContain('data-incidents-workbench="hertzbeat-ui"');
+    expect(html).toContain('data-incidents-api-owner="status-page-incident-api"');
+    expect(html).toContain('data-incidents-api-source="status-page-incident-list"');
+    expect(html).toContain('data-incidents-api-state="ready"');
+    expect(html).toContain('data-incidents-api-total="1"');
+    expect(html).toContain('data-incidents-detail-owner="status-page-incident-detail-api"');
+    expect(html).toContain('data-incidents-detail-source="status-page-incident-detail"');
+    expect(html).toContain('data-incidents-detail-state="ready"');
+    expect(html).toContain('data-incidents-detail-id="42"');
+    expect(html).toContain('data-incidents-transition-owner="status-page-incident-put-api"');
+    expect(html).toContain('data-incidents-transition-source="/status/page/incident"');
+    expect(html).toContain('data-incidents-transition-state="ready"');
+    expect(html).toContain('data-incidents-transition-pending="none"');
+    expect(html).toContain('data-incidents-query-contract="angular-search-pagination"');
+    expect(html).toContain('data-incidents-query-label="/status/page/incident?pageIndex=0&amp;pageSize=8"');
+    expect(html).toContain('data-incidents-shared-workbench="hertzbeat-ui"');
+    expect(html).toContain('data-hz-ui="incident-workbench"');
+    expect(html).toContain('data-hz-incident-workbench-owner="hertzbeat-ui-incident-workbench"');
+    expect(html).toContain('data-hz-incident-workbench-density="operator-compact"');
+    expect(html).toContain('data-hz-incident-workbench-style="cold-matte-hard-edge"');
+    expect(html).toContain('data-hz-incident-workbench-table="shared"');
+    expect(html).toContain('data-hz-incident-workbench-query="/status/page/incident?pageIndex=0&amp;pageSize=8"');
+    expect(html).toContain('data-hz-incident-transition-actions="shared"');
+    expect(html).toContain('data-hz-incident-transition-owner="hertzbeat-ui-incident-transition-actions"');
+    expect(html).toContain('data-hz-incident-transition-action="state-1"');
+    expect(html).toContain('data-hz-incident-transition-action="state-2"');
+    expect(html).toContain('data-hz-incident-transition-action="state-3"');
+    expect(html).toContain('data-hz-incident-transition-disabled="false"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).toContain('data-hz-incident-timeline-item="incident-timeline-42-8"');
+    expect(html).toContain('data-hz-incident-owner-item="incident-owner-42"');
     expect(html).toContain('故障事件');
-    expect(html).toContain('按 OTLP 工作台的冷色基线统一响应时间线、责任人和证据入口。');
-    expect(html).toContain('冷色入口已接入');
-    expect(html).toContain('等待接入事件适配器');
-    expect(html).toContain('打开概览');
-    expect(html).toContain('查看对象');
-    expect(html).toContain('事件入口');
-    expect(html).toContain('响应时间线');
-    expect(html).toContain('责任人优先');
-    expect(html).toContain('统一入口上下文');
-    expect(html).toContain('接入事件适配器');
-    expect(html).toContain('保留证据跳转');
+    expect(html).toContain('API 延迟事件');
+    expect(html).toContain('data-hz-row-selected="true"');
+    expect(apiMessageGet).toHaveBeenNthCalledWith(1, '/status/page/incident?pageIndex=0&pageSize=8');
+    expect(apiMessageGet).toHaveBeenNthCalledWith(2, '/status/page/incident/42');
     expect(html).not.toContain('angular-dark-ops-placeholder');
     expect(html).not.toContain('DARK OPS');
     expect(html).not.toContain('V1 SHELL IS LIVE');
     expect(html).not.toContain('Domain adapter comes next');
+    expect(html).not.toContain('data-incidents-shell-panel="cold-ops-shell-panel"');
+    expect(html).not.toContain('data-incidents-launch-checklist="cold-ops-static-rail"');
+    expect(html).not.toContain('data-incidents-empty-state="cold-ops-domain-adapter"');
     expect(source).toContain('coldOpsCatalogVisual');
+    expect(source).toContain('HzIncidentWorkbench');
+    expect(source).toContain('ClientWorkbench');
+    expect(source).toContain('loadIncidentWorkbenchData');
+    expect(source).toContain('apiMessageGet');
+    expect(source).toContain('apiMessagePut');
+    expect(source).toContain('transitionIncidentStatus');
+    expect(source).toContain('data-incidents-detail-source');
+    expect(source).toContain('data-incidents-transition-source');
+    expect(source).toContain('data-incidents-query-contract="angular-search-pagination"');
     expect(source).not.toContain('rounded-[16px]');
     expect(source).not.toContain('rounded-[14px]');
     expect(source).not.toContain('#4f6cff');
     expect(source).not.toContain('#101c31');
     expect(html).not.toContain('incidents.subtitle');
-    expect(html).not.toContain('Checkout latency spike across prod-ap');
     expect(html).not.toContain('data-summary-metric-grid');
     expect(source).not.toContain('WorkbenchPage');
     expect(source).not.toContain('StageSection');
     expect(source).not.toContain('SummaryMetricGrid');
     expect(source).not.toContain('DrawerSection');
   });
+
+  it('preserves the Angular incident search and server-side pagination read contract from route query params', async () => {
+    loadState.lastLoad = null;
+    apiMessageGet.mockReset();
+    apiMessagePut.mockReset();
+    apiMessageGet
+      .mockResolvedValueOnce({
+        content: [
+          {
+            id: 77,
+            name: 'API latency from query',
+            state: 2,
+            startTime: 1712730000000,
+            creator: 'platform-oncall',
+            components: [{ id: 9, name: 'checkout-api' }],
+            contents: []
+          }
+        ],
+        totalElements: 1,
+        pageIndex: 2,
+        pageSize: 15
+      })
+      .mockResolvedValueOnce({
+        id: 77,
+        name: 'API latency from query',
+        state: 2,
+        startTime: 1712730000000,
+        modifier: 'platform-oncall',
+        components: [{ id: 9, name: 'checkout-api' }],
+        contents: [{ id: 10, state: 2, message: 'query detail loaded', timestamp: 1712730300000 }]
+      });
+
+    const { default: IncidentsPage } = await import('./page');
+    const html = renderToStaticMarkup(await IncidentsPage({
+      searchParams: Promise.resolve({
+        search: 'api latency',
+        pageIndex: '2',
+        pageSize: '15'
+      })
+    }));
+    const lastLoad = loadState.lastLoad as (() => Promise<unknown>) | null;
+    await lastLoad?.();
+
+    expect(html).toContain('data-cache-key="incident-workbench:api latency:2:15:0"');
+    expect(html).toContain('data-incidents-query-contract="angular-search-pagination"');
+    expect(apiMessageGet).toHaveBeenNthCalledWith(1, '/status/page/incident?pageIndex=2&pageSize=15&search=api+latency');
+    expect(apiMessageGet).toHaveBeenNthCalledWith(2, '/status/page/incident/77');
+  });
 });
diff --git a/web-next/app/incidents/page.tsx b/web-next/app/incidents/page.tsx
index c06fbe2feb..6a0a95a28c 100644
--- a/web-next/app/incidents/page.tsx
+++ b/web-next/app/incidents/page.tsx
@@ -1,95 +1,13 @@
 import React from 'react';
-import Link from 'next/link';
-import { Archive } from 'lucide-react';
-import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
-import { buildIncidentsPlaceholderState } from '../../lib/incidents-surface/view-model';
-
-function actionClass(variant: 'primary' | 'subtle') {
-  const coldOpsVisual = coldOpsCatalogVisual;
-  const base =
-    'inline-flex h-8 items-center justify-center rounded-[3px] border px-3 text-[12px] font-semibold transition-colors';
-  if (variant === 'primary') {
-    return `${base} ${coldOpsVisual.button.primaryCompact}`;
-  }
-  return `${base} ${coldOpsVisual.button.compact} border-[#263041] bg-[#101217] text-[#d5d9e3] hover:border-[#3a465c] hover:bg-[#151820]`;
-}
-
-export default function IncidentsPage() {
-  const state = buildIncidentsPlaceholderState();
-  const coldOpsVisual = coldOpsCatalogVisual;
-
-  return (
-    <main
-      className={coldOpsVisual.entry.main}
-      data-incidents-route="otlp-cold-ops-entry"
-      data-incidents-style-baseline={coldOpsVisual.canvasName}
-    >
-      <div className={coldOpsVisual.entry.container}>
-        <header className={coldOpsVisual.entry.header}>
-          <div className={coldOpsVisual.entry.headerLayout}>
-            <div>
-              <div className={coldOpsVisual.entry.kicker}>{state.kicker}</div>
-              <h1 className={coldOpsVisual.entry.title}>{state.title}</h1>
-              <p className={coldOpsVisual.entry.subtitle}>{state.subtitle}</p>
-            </div>
-            <div className={coldOpsVisual.button.row}>
-              {state.actions.map(action => (
-                <Link className={actionClass(action.variant)} href={action.href} key={action.href}>
-                  {action.label}
-                </Link>
-              ))}
-            </div>
-          </div>
-        </header>
-
-        <div className={coldOpsVisual.entry.grid}>
-          <section
-            className={coldOpsVisual.entry.panel}
-            data-incidents-shell-panel="cold-ops-shell-panel"
-          >
-            <div className={coldOpsVisual.entry.panelEyebrow}>{state.shell.eyebrow}</div>
-            <p className={coldOpsVisual.entry.panelCopy}>{state.shell.copy}</p>
-            <div className="mt-4 flex flex-wrap gap-2">
-              {state.shell.chips.map(chip => (
-                <span className={coldOpsVisual.entry.chip} key={chip}>
-                  {chip}
-                </span>
-              ))}
-            </div>
-          </section>
-
-          <aside
-            className={coldOpsVisual.entry.rail}
-            data-incidents-launch-checklist="cold-ops-static-rail"
-          >
-            <h2 className={coldOpsVisual.entry.railTitle}>{state.checklistTitle}</h2>
-            <div className="mt-3 space-y-3">
-              {state.checklist.map(item => (
-                <div className={coldOpsVisual.entry.checklistItem} key={item.title}>
-                  <span className={`${coldOpsVisual.entry.checklistDot} ${item.tone}`} aria-hidden="true" />
-                  <div>
-                    <div className={coldOpsVisual.entry.checklistTitle}>{item.title}</div>
-                    <div className={coldOpsVisual.entry.checklistCopy}>{item.copy}</div>
-                  </div>
-                </div>
-              ))}
-            </div>
-          </aside>
-
-          <section
-            className={coldOpsVisual.entry.empty}
-            data-incidents-empty-state="cold-ops-domain-adapter"
-          >
-            <div className="max-w-[720px] text-center">
-              <div className={coldOpsVisual.entry.emptyIcon}>
-                <Archive aria-hidden="true" size={22} strokeWidth={1.8} />
-              </div>
-              <h2 className={coldOpsVisual.entry.emptyTitle}>{state.empty.title}</h2>
-              <p className={coldOpsVisual.entry.emptyCopy}>{state.empty.copy}</p>
-            </div>
-          </section>
-        </div>
-      </div>
-    </main>
-  );
+import IncidentsPage from './incidents-page';
+import { readIncidentWorkbenchQuery, type IncidentWorkbenchSearchParams } from '../../lib/incidents-surface/controller';
+
+export default async function IncidentsRoutePage({
+  searchParams
+}: {
+  searchParams?: Promise<IncidentWorkbenchSearchParams>;
+} = {}) {
+  const resolvedSearchParams = await searchParams;
+  const initialQuery = readIncidentWorkbenchQuery(resolvedSearchParams);
+  return <IncidentsPage initialQuery={initialQuery} />;
 }
diff --git a/web-next/app/status/page.test.tsx b/web-next/app/status/page.test.tsx
index 17d1b48de2..2627fe3244 100644
--- a/web-next/app/status/page.test.tsx
+++ b/web-next/app/status/page.test.tsx
@@ -1,3 +1,6 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
@@ -45,7 +48,7 @@ vi.mock('../../components/providers/i18n-provider', async () => {
   const actual = await import('../../lib/i18n');
   return {
     useI18n: () => ({
-      t: createTranslatorMock(),
+      t: createTranslatorMock({ locale: 'en-US' }),
       locale: 'en-US',
       locales: actual.LOCALES,
       setLocale: vi.fn(async () => {})
@@ -54,10 +57,10 @@ vi.mock('../../components/providers/i18n-provider', async () => {
 });
 
 vi.mock('../../components/workbench/client-workbench', () => ({
-  ClientWorkbench: ({ children, load }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown> }) => {
+  ClientWorkbench: ({ children, load, loadingCopy }: { children: (data: any) => React.ReactNode; load: () => Promise<unknown>; loadingCopy?: string }) => {
     loadState.lastLoad = load;
     return (
-      <div data-client-workbench="true">
+      <div data-client-workbench="true" data-loading-copy={loadingCopy}>
         {children({
           org: {
             name: 'Codex Demo Status',
@@ -97,14 +100,20 @@ vi.mock('../../components/workbench/client-workbench', () => ({
 
 vi.mock('../../components/pages/public-status-shell', () => ({
   PublicStatusShell: ({ brand, mode, poweredByLabel, componentCards, incidentCards, refreshLabel }: any) => (
-    <main data-public-status-shell="true" data-public-status-mode={mode}>
+    <main
+      data-public-status-shell="true"
+      data-public-status-mode={mode}
+      data-public-status-api-contract="angular-public-status"
+      data-public-status-api-owner="status-center-public-controller"
+      data-public-status-mode-switch-contract="component-incident"
+    >
       <span>{brand.title}</span>
       <span>{brand.copy}</span>
       <span>{poweredByLabel}</span>
       <span>{refreshLabel}</span>
       <span>{componentCards.length}</span>
       <span>{incidentCards.length}</span>
-      <button type="button">Refresh</button>
+      <button type="button">{refreshLabel}</button>
     </main>
   )
 }));
@@ -113,10 +122,14 @@ vi.mock('../../lib/api-client', () => ({
   apiMessageGet
 }));
 
-vi.mock('../../lib/status-center/controller', () => ({
-  loadStatusPageData,
-  loadStatusPageIncidentFeed: vi.fn(async () => [])
-}));
+vi.mock('../../lib/status-center/controller', async importOriginal => {
+  const actual = await importOriginal<typeof import('../../lib/status-center/controller')>();
+  return {
+    ...actual,
+    loadStatusPageData,
+    loadStatusPageIncidentFeed: vi.fn(async () => [])
+  };
+});
 
 vi.mock('../../lib/format', () => ({
   formatTime: () => '2026-04-16 22:00:00'
@@ -134,11 +147,29 @@ describe('status page', () => {
     await lastLoad?.();
 
     expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-loading-copy="Loading status page"');
     expect(html).toContain('data-public-status-shell="true"');
     expect(html).toContain('data-public-status-mode="component"');
+    expect(html).toContain('data-public-status-api-contract="angular-public-status"');
+    expect(html).toContain('data-public-status-api-owner="status-center-public-controller"');
+    expect(html).toContain('data-public-status-mode-switch-contract="component-incident"');
     expect(html).toContain('Codex Demo Status');
-    expect(html).toContain('Power by Apache HertzBeat™. Star Us !');
+    expect(html).toContain('Powered by HertzBeat');
     expect(html).toContain('Refresh');
     expect(loadStatusPageData).toHaveBeenCalledWith(apiMessageGet);
   }, 15000);
+
+  it('keeps public status remounts on a short settled cache window while incident reload stays explicit', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/status/status-page.tsx'), 'utf8');
+
+    expect(source).toContain('PUBLIC_STATUS_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('/status/page/public/org');
+    expect(source).toContain('/status/page/public/component');
+    expect(source).toContain('loadStatusPageData(apiMessageGet)');
+    expect(source).toContain('publicStatusIncidentListUrl');
+    expect(source).toContain('reloadToken: incidentReloadToken');
+    expect(source).toContain('onRefreshIncidents={() => setIncidentReloadToken(value => value + 1)}');
+    expect(source).toContain('cacheKey={publicStatusCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={PUBLIC_STATUS_SETTLED_CACHE_TTL_MS}');
+  });
 });
diff --git a/web-next/app/status/page.tsx b/web-next/app/status/page.tsx
index fd3ec90ab9..ea3f423156 100644
--- a/web-next/app/status/page.tsx
+++ b/web-next/app/status/page.tsx
@@ -1,135 +1,7 @@
-'use client';
+import React from 'react';
 
-import React, { useEffect, useMemo, useState } from 'react';
-import { PublicStatusShell } from '../../components/pages/public-status-shell';
-import { useI18n } from '../../components/providers/i18n-provider';
-import { ClientWorkbench } from '../../components/workbench/client-workbench';
-import { apiMessageGet } from '../../lib/api-client';
-import { formatTime } from '../../lib/format';
-import { loadStatusPageData, loadStatusPageIncidentFeed } from '../../lib/status-center/controller';
-import {
-  statusIncidentPublicStartAtLabel,
-  statusIncidentPublicUpdateAtLabel,
-  statusPageLabel,
-  statusPublicPoweredByLabel,
-  statusPublicThirtyDayLabel,
-  statusPublicTodayLabel,
-  statusPublicToComponentLabel,
-  statusPublicToIncidentLabel,
-  statusPublicYearLabel,
-  statusPublicUptimeLabel
-} from '../../lib/status-center/display';
-import { buildStatusBrand, buildStatusComponentHistoryRows, buildStatusIncidentCards, clampStatusIncidentYear } from '../../lib/status-center/view-model';
-import type { StatusPageComponent, StatusPageIncident, StatusPageOrg } from '../../lib/types';
+import StatusPage from './status-page';
 
-type StatusPageData = {
-  org: StatusPageOrg;
-  components: StatusPageComponent[];
-  incidents: StatusPageIncident[];
-  incidentsError?: string | null;
-};
-
-function StatusPublicSurface({ data }: { data: StatusPageData }) {
-  const { t, locale, locales, setLocale } = useI18n();
-  const currentYear = useMemo(() => new Date().getFullYear(), []);
-  const [mode, setMode] = useState<'component' | 'incident'>('component');
-  const [selectedYear, setSelectedYear] = useState(currentYear);
-  const [incidentReloadToken, setIncidentReloadToken] = useState(0);
-  const [incidentFeed, setIncidentFeed] = useState(data.incidents);
-  const [incidentLoading, setIncidentLoading] = useState(false);
-  const [incidentError, setIncidentError] = useState<string | null>(data.incidentsError ?? null);
-
-  const brand = useMemo(() => buildStatusBrand(data.org, t, formatTime), [data.org, t]);
-  const componentCards = useMemo(() => buildStatusComponentHistoryRows(data.components, t, formatTime), [data.components, t]);
-  const incidentCards = useMemo(() => buildStatusIncidentCards(incidentFeed, t, formatTime), [incidentFeed, t]);
-
-  useEffect(() => {
-    let active = true;
-
-    if (selectedYear === currentYear && incidentReloadToken === 0) {
-      setIncidentFeed(data.incidents);
-      setIncidentLoading(false);
-      setIncidentError(data.incidentsError ?? null);
-      return () => {
-        active = false;
-      };
-    }
-
-    setIncidentLoading(true);
-    setIncidentError(null);
-
-    loadStatusPageIncidentFeed(apiMessageGet, {
-      selectedYear,
-      currentYear,
-      reloadToken: incidentReloadToken,
-      initialIncidents: data.incidents
-    })
-      .then(result => {
-        if (!active) return;
-        setIncidentFeed(result);
-      })
-      .catch(error => {
-        if (!active) return;
-        setIncidentError(error instanceof Error ? error.message : t('common.load-failed'));
-      })
-      .finally(() => {
-        if (active) {
-          setIncidentLoading(false);
-        }
-      });
-
-    return () => {
-      active = false;
-    };
-  }, [currentYear, data.incidents, data.incidentsError, incidentReloadToken, selectedYear, t]);
-
-  return (
-    <PublicStatusShell
-      pageLabel={statusPageLabel(t)}
-      mode={mode}
-      onModeChange={setMode}
-      brand={brand}
-      locale={locale}
-      locales={locales}
-      onSelectLocale={setLocale}
-      componentCards={componentCards}
-      componentEmptyTitle={t('status.components.empty.title')}
-      componentEmptyCopy={t('status.components.empty.copy')}
-      componentWindowStartLabel={statusPublicTodayLabel(t)}
-      componentWindowEndLabel={statusPublicThirtyDayLabel(t)}
-      incidentTitle={statusPublicToIncidentLabel(t)}
-      incidentCards={incidentCards}
-      incidentLoading={incidentLoading}
-      incidentError={incidentError}
-      incidentEmptyTitle={t('status.incidents.empty.title')}
-      incidentEmptyCopy={t('status.incidents.empty.copy')}
-      loadingTitle={t('common.loading')}
-      loadFailedTitle={t('common.load-failed')}
-      selectedYear={selectedYear}
-      currentYear={currentYear}
-      onSelectedYearChange={nextYear => setSelectedYear(clampStatusIncidentYear(nextYear, currentYear))}
-      onRefreshIncidents={() => setIncidentReloadToken(value => value + 1)}
-      yearLabel={statusPublicYearLabel(t)}
-      refreshLabel={t('common.refresh')}
-      incidentStartLabel={statusIncidentPublicStartAtLabel(t)}
-      incidentUpdateLabel={statusIncidentPublicUpdateAtLabel(t)}
-      poweredByLabel={statusPublicPoweredByLabel(t)}
-      toIncidentLabel={statusPublicToIncidentLabel(t)}
-      toComponentLabel={statusPublicToComponentLabel(t)}
-      uptimeLabel={statusPublicUptimeLabel(t)}
-    />
-  );
-}
-
-export default function StatusPage() {
-  const { t } = useI18n();
-  const load = React.useCallback(async (): Promise<StatusPageData> => {
-    return loadStatusPageData(apiMessageGet);
-  }, []);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('status.loading')}>
-      {data => <StatusPublicSurface data={data as StatusPageData} />}
-    </ClientWorkbench>
-  );
+export default function StatusRoutePage() {
+  return <StatusPage />;
 }
diff --git a/web-next/app/status/public/page.tsx b/web-next/app/status/public/page.tsx
index b9aeff7915..0d0a95e44c 100644
--- a/web-next/app/status/public/page.tsx
+++ b/web-next/app/status/public/page.tsx
@@ -1,9 +1,9 @@
 import { redirect } from 'next/navigation';
-import { buildCompatRedirectTarget, type SearchParamsRecord } from '../../../lib/compat/search-params';
+import { buildPublicStatusCompatRouteUrl, type SearchParamsRecord } from '../../../lib/status-center/controller';
 
 export default async function StatusPublicAliasPage(props: {
   searchParams?: Promise<SearchParamsRecord>;
 }) {
   const resolvedSearchParams = await props?.searchParams;
-  redirect(buildCompatRedirectTarget('/status', resolvedSearchParams));
+  redirect(buildPublicStatusCompatRouteUrl(resolvedSearchParams));
 }
diff --git a/web-next/app/status/page.tsx b/web-next/app/status/status-page.tsx
similarity index 81%
copy from web-next/app/status/page.tsx
copy to web-next/app/status/status-page.tsx
index fd3ec90ab9..2728f01b7c 100644
--- a/web-next/app/status/page.tsx
+++ b/web-next/app/status/status-page.tsx
@@ -6,7 +6,13 @@ import { useI18n } from '../../components/providers/i18n-provider';
 import { ClientWorkbench } from '../../components/workbench/client-workbench';
 import { apiMessageGet } from '../../lib/api-client';
 import { formatTime } from '../../lib/format';
-import { loadStatusPageData, loadStatusPageIncidentFeed } from '../../lib/status-center/controller';
+import {
+  buildStatusIncidentListUrl,
+  buildStatusIncidentYearQuery,
+  describeStatusIncidentLoadFailure,
+  loadStatusPageData,
+  loadStatusPageIncidentFeed
+} from '../../lib/status-center/controller';
 import {
   statusIncidentPublicStartAtLabel,
   statusIncidentPublicUpdateAtLabel,
@@ -29,6 +35,8 @@ type StatusPageData = {
   incidentsError?: string | null;
 };
 
+const PUBLIC_STATUS_SETTLED_CACHE_TTL_MS = 10_000;
+
 function StatusPublicSurface({ data }: { data: StatusPageData }) {
   const { t, locale, locales, setLocale } = useI18n();
   const currentYear = useMemo(() => new Date().getFullYear(), []);
@@ -70,7 +78,7 @@ function StatusPublicSurface({ data }: { data: StatusPageData }) {
       })
       .catch(error => {
         if (!active) return;
-        setIncidentError(error instanceof Error ? error.message : t('common.load-failed'));
+        setIncidentError(describeStatusIncidentLoadFailure(error, t('status.public.incidents.load-failed')));
       })
       .finally(() => {
         if (active) {
@@ -123,12 +131,37 @@ function StatusPublicSurface({ data }: { data: StatusPageData }) {
 
 export default function StatusPage() {
   const { t } = useI18n();
+  const publicStatusCurrentYear = useMemo(() => new Date().getFullYear(), []);
+  const publicStatusIncidentListUrl = useMemo(
+    () =>
+      buildStatusIncidentListUrl({
+        ...buildStatusIncidentYearQuery(publicStatusCurrentYear),
+        pageIndex: 0,
+        pageSize: 9999
+      }),
+    [publicStatusCurrentYear]
+  );
+  const publicStatusCacheKey = useMemo(
+    () =>
+      [
+        'public-status',
+        '/status/page/public/org',
+        '/status/page/public/component',
+        publicStatusIncidentListUrl
+      ].join(':'),
+    [publicStatusIncidentListUrl]
+  );
   const load = React.useCallback(async (): Promise<StatusPageData> => {
     return loadStatusPageData(apiMessageGet);
   }, []);
 
   return (
-    <ClientWorkbench load={load} loadingCopy={t('status.loading')}>
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('status.loading')}
+      cacheKey={publicStatusCacheKey}
+      cacheSettledTtlMs={PUBLIC_STATUS_SETTLED_CACHE_TTL_MS}
+    >
       {data => <StatusPublicSurface data={data as StatusPageData} />}
     </ClientWorkbench>
   );
diff --git a/web-next/components/pages/ops-surface-page.test.tsx b/web-next/components/pages/ops-surface-page.test.tsx
index 2fe941e8f9..b86e8c1422 100644
--- a/web-next/components/pages/ops-surface-page.test.tsx
+++ b/web-next/components/pages/ops-surface-page.test.tsx
@@ -3,21 +3,9 @@ import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 
-const t = (key: string) => {
-  const messages: Record<string, string> = {
-    'ops.surface.kicker': 'Operator surface',
-    'ops.surface.route-contract.title': 'Route contract',
-    'ops.surface.route-contract.copy': 'Shared shell status stays visible during the landing phase.',
-    'ops.surface.launch-checklist': 'Launch checklist',
-    'ops.surface.route-posture': 'Route posture',
-    'ops.surface.entry-shell': 'Entry shell ready',
-    'common.mode': 'Mode',
-    'menu.dashboard.back-en': 'Open overview'
-  };
-
-  return messages[key] ?? key;
-};
+const t = createTranslatorMock();
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
@@ -126,4 +114,25 @@ describe('OpsSurfacePage', () => {
     expect(source).not.toContain('ObservabilityRailShell as RailSection');
     expect(source).not.toContain('ObservabilityStatusState as StatusState');
   });
+
+  it('renders localized default overview and entity handoffs', async () => {
+    const { OpsSurfacePage } = await import('./ops-surface-page');
+
+    const html = renderToStaticMarkup(
+      <OpsSurfacePage
+        title="Safe automation"
+        subtitle="Manual control-plane entry."
+        tags={['manual approval']}
+        focus="Approval-safe action entry"
+        summary="Actions stay bounded to the operator entry shell."
+        lanes={[{ title: 'Action catalog', copy: 'Catalog posture stays visible.' }]}
+        checklist={[{ title: 'Entry shell ready', copy: 'The route uses shared shell primitives.' }]}
+      />
+    );
+
+    expect(html).toContain('Open overview');
+    expect(html).toContain('Entity center');
+    expect(html).not.toContain('menu.entity.browse');
+    expect(html).not.toContain('menu.dashboard.back-en');
+  });
 });
diff --git a/web-next/components/pages/ops-surface-page.tsx b/web-next/components/pages/ops-surface-page.tsx
index 00dc83b81b..d9bf6390b6 100644
--- a/web-next/components/pages/ops-surface-page.tsx
+++ b/web-next/components/pages/ops-surface-page.tsx
@@ -52,7 +52,7 @@ export function OpsSurfacePage({
             {t('menu.dashboard.back')}
           </Link>
           <Link className={buttonVariants({ variant: 'primary', size: 'sm' })} href="/entities">
-            {t('menu.entity.browse')}
+            {t('menu.entity.center')}
           </Link>
         </>
       );
@@ -100,12 +100,12 @@ export function OpsSurfacePage({
           <RailSection title={t('ops.surface.route-posture')}>
             <div className="space-y-3">
               <div className="rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-3.5 py-3">
-                <div className="text-[10px] uppercase tracking-[0.18em] text-[var(--ops-text-tertiary)]">{t('common.mode')}</div>
+                <div className="text-[10px] uppercase tracking-[0.18em] text-[var(--ops-text-tertiary)]">{t('ops.surface.route-posture.mode-label')}</div>
                 <div className="mt-2 text-base font-semibold text-[var(--ops-text-primary)]">{t('ops.surface.entry-shell')}</div>
                 <div className="mt-2 text-sm leading-6 text-[var(--ops-text-secondary)]">{focus}</div>
               </div>
               <Link className={buttonVariants({ variant: 'default', size: 'sm' })} href="/overview">
-                {t('menu.dashboard.back-en')}
+                {t('menu.dashboard.back')}
               </Link>
             </div>
           </RailSection>
diff --git a/web-next/components/pages/public-status-shell.test.tsx b/web-next/components/pages/public-status-shell.test.tsx
index 03756e2a33..b0537773f4 100644
--- a/web-next/components/pages/public-status-shell.test.tsx
+++ b/web-next/components/pages/public-status-shell.test.tsx
@@ -52,6 +52,7 @@ describe('PublicStatusShell', () => {
             title: 'Public API',
             copy: 'Primary signal intake service.',
             state: 0,
+            tone: 'success',
             statusLabel: 'Operational',
             latestTimeLabel: '19:10',
             latestUptimeLabel: '99.99%',
@@ -94,6 +95,11 @@ describe('PublicStatusShell', () => {
 
     expect(html).toContain('data-public-status-shell="true"');
     expect(html).toContain('data-public-status-mode="component"');
+    expect(html).toContain('data-public-status-api-contract="angular-public-status"');
+    expect(html).toContain('data-public-status-api-owner="status-center-public-controller"');
+    expect(html).toContain('data-public-status-mode-switch-contract="component-incident"');
+    expect(html).toContain('data-public-status-mode-switch="component-to-incident"');
+    expect(html).toContain('data-public-status-mode-switch-owner="hertzbeat-ui-button"');
     expect(html).toContain('Operator Status');
     expect(html).toContain('Unified public signal posture for the active operator desk.');
     expect(html).toContain('English(en_US)');
@@ -135,6 +141,12 @@ describe('PublicStatusShell', () => {
     expect(source).toContain("from '../workbench/primitives'");
     expect(source).toContain('WorkbenchPanel');
     expect(source).toContain('WorkbenchActionButton');
+    expect(source).toContain('data-public-status-api-contract="angular-public-status"');
+    expect(source).toContain('data-public-status-api-owner="status-center-public-controller"');
+    expect(source).toContain('data-public-status-mode-switch-contract="component-incident"');
+    expect(source).toContain('data-public-status-mode-switch="component-to-incident"');
+    expect(source).toContain('data-public-status-mode-switch="incident-to-component"');
+    expect(source).toContain('data-public-status-mode-switch-owner="hertzbeat-ui-button"');
     expect(source).not.toContain('inline-flex h-9 items-center gap-2 rounded-[2px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-3 text-[12px] text-[var(--ops-text-secondary)] transition-colors hover:border-[var(--ops-primary)] hover:bg-[var(--ops-surface-elevated)] hover:text-[var(--ops-text-primary)]');
   });
 
diff --git a/web-next/components/pages/public-status-shell.tsx b/web-next/components/pages/public-status-shell.tsx
index 8bd7911924..798dc3d432 100644
--- a/web-next/components/pages/public-status-shell.tsx
+++ b/web-next/components/pages/public-status-shell.tsx
@@ -25,11 +25,14 @@ type PublicStatusBrand = {
   updatedAt: string;
 };
 
+type PublicStatusBadgeTone = 'success' | 'danger' | 'default';
+
 type PublicStatusComponentCard = {
   key: string;
   title: string;
   copy: string;
   state: number | string | null;
+  tone: PublicStatusBadgeTone;
   statusLabel: string;
   latestTimeLabel: string;
   latestUptimeLabel: string;
@@ -46,6 +49,7 @@ type PublicStatusIncidentCard = {
   title: string;
   copy: string;
   state: number | string | null;
+  tone: PublicStatusBadgeTone;
   stateLabel: string;
   stateColor: string;
   startAtLabel: string;
@@ -54,6 +58,7 @@ type PublicStatusIncidentCard = {
     timestampLabel: string;
     stateLabel: string;
     state: number | null;
+    tone: PublicStatusBadgeTone;
     stateColor: string;
     message: string;
   }>;
@@ -102,26 +107,6 @@ const shellTextPrimaryClass = 'text-[var(--ops-text-primary)]';
 const shellTextSecondaryClass = 'text-[var(--ops-text-secondary)]';
 const shellTextTertiaryClass = 'text-[var(--ops-text-tertiary)]';
 
-function componentTone(state?: number | string | null) {
-  if (state === 0 || state === '0') {
-    return 'success' as const;
-  }
-  if (state === 1 || state === '1') {
-    return 'danger' as const;
-  }
-  return 'default' as const;
-}
-
-function incidentTone(state?: number | string | null) {
-  if (state === 3 || state === '3') {
-    return 'success' as const;
-  }
-  if (state === 0 || state === '0') {
-    return 'danger' as const;
-  }
-  return 'default' as const;
-}
-
 function accentStyle(accent?: string | null) {
   const color = accent || 'var(--ops-text-tertiary)';
   return {
@@ -163,7 +148,7 @@ function PublicStatusComponentRows({
             <div className="min-w-0">
               <div className="flex flex-wrap items-center gap-2">
                 <h3 className={cn('text-[15px] font-semibold', shellTextPrimaryClass)}>{card.title}</h3>
-                <Badge variant={componentTone(card.state)}>{card.statusLabel}</Badge>
+                <Badge variant={card.tone}>{card.statusLabel}</Badge>
               </div>
               <p className={cn('mt-1 text-[12px] leading-6', shellTextSecondaryClass)}>{card.copy}</p>
             </div>
@@ -269,7 +254,7 @@ function PublicStatusIncidentRows({
           </div>
           <div className="flex shrink-0 items-center gap-2">
             <span className="h-2.5 w-2.5 rounded-full" style={{ background: card.stateColor || '#6b7280' }} />
-            <Badge variant={incidentTone(card.state)} style={accentStyle(card.stateColor)}>
+            <Badge variant={card.tone} style={accentStyle(card.stateColor)}>
               {card.stateLabel}
             </Badge>
           </div>
@@ -290,7 +275,7 @@ function PublicStatusIncidentRows({
               <div key={`${card.key}-${contentItem.timestampLabel}-${contentItem.message}`} className="grid gap-2 md:grid-cols-[140px_minmax(0,120px)_1fr]">
                 <div className={cn('text-[11px] uppercase tracking-[0.16em]', shellTextTertiaryClass)}>{contentItem.timestampLabel}</div>
                 <div>
-                  <Badge variant={incidentTone(contentItem.state)} style={accentStyle(contentItem.stateColor || card.stateColor)}>
+                  <Badge variant={contentItem.tone} style={accentStyle(contentItem.stateColor || card.stateColor)}>
                     {contentItem.stateLabel}
                   </Badge>
                 </div>
@@ -382,7 +367,14 @@ export function PublicStatusShell({
   const bannerColor = brand.color || '#121317';
 
   return (
-    <div className="min-h-screen bg-[var(--ops-background)] text-[var(--ops-text-primary)]" data-public-status-shell="true" data-public-status-mode={mode}>
+    <div
+      className="min-h-screen bg-[var(--ops-background)] text-[var(--ops-text-primary)]"
+      data-public-status-shell="true"
+      data-public-status-mode={mode}
+      data-public-status-api-contract="angular-public-status"
+      data-public-status-api-owner="status-center-public-controller"
+      data-public-status-mode-switch-contract="component-incident"
+    >
       <div
         className={cn('border-b bg-[var(--ops-surface-panel)]', shellBorderClass)}
         style={{ backgroundImage: 'linear-gradient(180deg,rgba(18,19,23,0.96),rgba(11,12,14,0.98))' }}
@@ -556,6 +548,8 @@ export function PublicStatusShell({
             <Button
               type="button"
               data-testid="status-public-mode-incident"
+              data-public-status-mode-switch="component-to-incident"
+              data-public-status-mode-switch-owner="hertzbeat-ui-button"
               variant="ghost"
               className="justify-start rounded-[2px] px-0 text-[var(--ops-text-secondary)] hover:bg-transparent hover:text-[var(--ops-text-primary)]"
               onClick={() => onModeChange('incident')}
@@ -567,6 +561,8 @@ export function PublicStatusShell({
             <Button
               type="button"
               data-testid="status-public-mode-component"
+              data-public-status-mode-switch="incident-to-component"
+              data-public-status-mode-switch-owner="hertzbeat-ui-button"
               variant="ghost"
               className="justify-start rounded-[2px] px-0 text-[var(--ops-text-secondary)] hover:bg-transparent hover:text-[var(--ops-text-primary)]"
               onClick={() => onModeChange('component')}
diff --git a/web-next/lib/actions-approval-draft-fallback-store.ts b/web-next/lib/actions-approval-draft-fallback-store.ts
new file mode 100644
index 0000000000..0177a93219
--- /dev/null
+++ b/web-next/lib/actions-approval-draft-fallback-store.ts
@@ -0,0 +1,70 @@
+export type FallbackApprovalDraftRecord = {
+  draftId: string;
+  state: string;
+  executionState: string;
+  executionAllowed: false;
+  adapterOwner: 'next-actions-approval-draft-bff' | 'next-actions-approval-decision-bff';
+  managerBacked: false;
+  actionId?: string;
+  catalogId?: string;
+  decision?: string;
+  reviewer?: string;
+  reason?: string;
+  gmtUpdate: string;
+};
+
+type ApprovalDraftFallbackStoreGlobal = typeof globalThis & {
+  __hertzbeatActionsApprovalDraftFallbackStore?: Map<string, FallbackApprovalDraftRecord>;
+};
+
+const fallbackStoreGlobal = globalThis as ApprovalDraftFallbackStoreGlobal;
+const fallbackApprovalDrafts = fallbackStoreGlobal.__hertzbeatActionsApprovalDraftFallbackStore
+  ?? new Map<string, FallbackApprovalDraftRecord>();
+
+fallbackStoreGlobal.__hertzbeatActionsApprovalDraftFallbackStore = fallbackApprovalDrafts;
+
+export function saveFallbackApprovalDraft(
+  draft: Omit<FallbackApprovalDraftRecord, 'gmtUpdate'>
+): FallbackApprovalDraftRecord {
+  const record = {
+    ...draft,
+    gmtUpdate: new Date().toISOString()
+  };
+  fallbackApprovalDrafts.set(record.draftId, record);
+  return record;
+}
+
+export function listFallbackApprovalDrafts(limit = 8): FallbackApprovalDraftRecord[] {
+  return Array.from(fallbackApprovalDrafts.values())
+    .sort((left, right) => right.gmtUpdate.localeCompare(left.gmtUpdate))
+    .slice(0, limit);
+}
+
+export function decideFallbackApprovalDraft(
+  draftId: string,
+  decision: string,
+  reviewer: string,
+  reason: string
+): FallbackApprovalDraftRecord {
+  const existing = fallbackApprovalDrafts.get(draftId);
+  return saveFallbackApprovalDraft({
+    ...(existing || {
+      draftId,
+      actionId: undefined,
+      catalogId: undefined
+    }),
+    draftId,
+    decision,
+    reviewer,
+    reason,
+    state: decision === 'approved' ? 'approval-draft-approved' : 'approval-draft-rejected',
+    executionState: 'not-executed',
+    executionAllowed: false,
+    adapterOwner: 'next-actions-approval-decision-bff',
+    managerBacked: false
+  });
+}
+
+export function resetFallbackApprovalDraftsForTest() {
+  fallbackApprovalDrafts.clear();
+}
diff --git a/web-next/lib/actions-surface/model.test.ts b/web-next/lib/actions-surface/model.test.ts
index c499a53a50..bd709081a4 100644
--- a/web-next/lib/actions-surface/model.test.ts
+++ b/web-next/lib/actions-surface/model.test.ts
@@ -1,38 +1,54 @@
 import { describe, expect, it } from 'vitest';
-import { buildActionsDomainModel, buildAutomationActionCatalogReview, buildSuggestedRemediationActions } from './model';
-
-const t = (key: string) => {
-  const messages: Record<string, string> = {
-    'actions.subtitle': 'Automation catalog, execution history, and approval-aware operations all stay in one context.',
-    'actions.focus': 'Automation inventory and execution guardrails',
-    'actions.summary': 'The actions entry page carries automation catalog, execution history, and approval semantics without requiring a page redesign for future data.',
-    'actions.checklist.entry.title': 'Entry shell ready',
-    'actions.checklist.entry.copy': 'The action catalog already uses the shared workbench language.',
-    'actions.checklist.adapters.title': 'Execution adapters',
-    'actions.checklist.adapters.copy': 'Action list, run result, and approval flow keep the current integration boundary.',
-    'actions.checklist.context.title': 'Context continuity',
-    'actions.checklist.context.copy': 'Jump to the actions page directly from entity, alert, or topology context.',
-    'menu.dashboard.back': 'Open overview',
-    'menu.entity.center': 'Entity center',
-    'menu.monitor.center': 'Monitor center'
-  };
-  return messages[key] ?? key;
-};
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+import {
+  buildActionApprovalDecision,
+  buildActionApprovalDraft,
+  buildActionApprovalDraftQueue,
+  buildActionCatalogReadAdapter,
+  buildActionsDomainModel,
+  buildAutomationActionCatalogReview,
+  buildSuggestedRemediationActions
+} from './model';
+
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('actions domain model', () => {
   it('builds a domain-specific automation workspace snapshot', () => {
     const model = buildActionsDomainModel(t);
 
-    expect(model.title).toBe('Actions');
+    expect(model.title).toBe('自动化处置');
+    expect(model.tags).toEqual(['自动化目录', '风险感知动作', '审批流']);
     expect(model.catalog).toHaveLength(3);
+    expect(model.catalog[0]).toMatchObject({
+      name: '重启 checkout 部署',
+      category: '运行时恢复',
+      scope: '服务 checkout / prod-ap',
+      owner: '平台运维',
+      lastRun: '12 分钟前',
+      posture: '需要审批和回滚说明'
+    });
     expect(model.runs[0]?.status).toBe('awaiting-approval');
+    expect(model.runs[0]).toMatchObject({
+      name: '重启 checkout 部署',
+      target: 'checkout / prod-ap',
+      actor: 'li.na',
+      duration: '待审批'
+    });
     expect(model.approvals[0]?.status).toBe('pending');
+    expect(model.approvals[0]).toMatchObject({
+      summary: '重启生产 checkout Pod 以处理饱和告警突增',
+      owner: 'checkout-oncall',
+      evidence: '已附加告警风暴和链路延迟回退证据。'
+    });
     expect(model.metrics).toEqual([
-      { label: 'Catalog entries', value: '3' },
-      { label: 'Pending approvals', value: '1' },
-      { label: 'Recent runs', value: '3' },
-      { label: 'High-risk actions', value: '1' }
+      { label: '目录动作', value: '3' },
+      { label: '待审批', value: '1' },
+      { label: '近期执行', value: '3' },
+      { label: '高风险动作', value: '1' }
     ]);
+    expect(model.checklist.map(item => item.meta)).toEqual(['已完成', '下一步', '预留']);
+    expect(model.nextHops.map(item => item.label)).toEqual(['打开总览', '实体中心', '监控中心']);
+    expect(model.nextHops.map(item => item.href)).toEqual(['/overview', '/entities', '/monitors']);
   });
 
   it('marks catalog, run, and approval snapshots as roadmap demo data until adapters land', () => {
@@ -55,6 +71,15 @@ describe('actions domain model', () => {
       'scripts',
       'runbook-orchestration'
     ]);
+    expect(model.adapterBoundary.roadmapOnlyLabels).toEqual([
+      '工作流自动化',
+      '动作目录',
+      '应用编排器',
+      '自助动作',
+      '审批流',
+      '脚本执行',
+      '处置手册编排'
+    ]);
     expect(model.catalog.every(item => item.snapshotState === 'roadmap-demo')).toBe(true);
     expect(model.runs.every(item => item.snapshotState === 'roadmap-demo')).toBe(true);
     expect(model.approvals.every(item => item.snapshotState === 'roadmap-demo')).toBe(true);
@@ -62,22 +87,25 @@ describe('actions domain model', () => {
   });
 
   it('builds alert-context remediation suggestions that require human confirmation', () => {
-    const suggestions = buildSuggestedRemediationActions({
-      source: 'alert',
-      signal: 'traces',
-      severity: 'critical',
-      entityId: 'service:commerce/checkout',
-      entityName: 'checkout-api',
-      serviceName: 'checkout-api',
-      serviceNamespace: 'commerce',
-      environment: 'prod',
-      timeRange: 'last-1h',
-      traceId: 'trace-123',
-      spanId: 'span-456',
-      collector: 'edge-collector-a',
-      template: 'java-service',
-      returnTo: '/alert?status=firing&returnLabel=告警'
-    });
+    const suggestions = buildSuggestedRemediationActions(
+      {
+        source: 'alert',
+        signal: 'traces',
+        severity: 'critical',
+        entityId: 'service:commerce/checkout',
+        entityName: 'checkout-api',
+        serviceName: 'checkout-api',
+        serviceNamespace: 'commerce',
+        environment: 'prod',
+        timeRange: 'last-1h',
+        traceId: 'trace-123',
+        spanId: 'span-456',
+        collector: 'edge-collector-a',
+        template: 'java-service',
+        returnTo: '/alert?status=firing&returnLabel=告警'
+      },
+      t
+    );
 
     expect(suggestions.map(item => item.id)).toEqual([
       'suggest-restart-checkout',
@@ -86,13 +114,24 @@ describe('actions domain model', () => {
     ]);
     expect(suggestions[0]).toMatchObject({
       catalogId: 'restart-checkout',
+      catalogLabel: '重启 checkout 部署',
+      displayMeta: '高风险 · 重启 checkout 部署',
       title: '建议重启 checkout-api',
       source: 'alert-context-handoff',
       risk: 'high',
       confirmation: 'manual-required',
       posture: '只生成建议,人工确认后才能进入执行。'
     });
+    expect(suggestions[1]).toMatchObject({
+      catalogId: 'mute-edge-alerts',
+      catalogLabel: '静默边缘饱和告警',
+      displayMeta: '中风险 · 静默边缘饱和告警'
+    });
     expect(suggestions.every(item => item.confirmation === 'manual-required')).toBe(true);
+    expect(suggestions[0]?.evidence).toContain('来源 告警中心');
+    expect(suggestions[0]?.evidence).toContain('信号 链路');
+    expect(suggestions[0]?.evidence).not.toContain('来源 alert');
+    expect(suggestions[0]?.evidence).not.toContain('信号 traces');
 
     const evidenceUrl = new URL(suggestions[0]?.evidenceHref || '/', 'http://localhost');
     expect(evidenceUrl.pathname).toBe('/alert');
@@ -105,6 +144,131 @@ describe('actions domain model', () => {
     expect(evidenceUrl.searchParams.get('returnLabel')).toBeNull();
   });
 
+  it('localizes unknown evidence source and signal values while preserving the route tokens', () => {
+    const suggestions = buildSuggestedRemediationActions(
+      {
+        source: 'custom-source',
+        signal: 'profiling',
+        entityName: 'checkout-api'
+      },
+      t
+    );
+
+    expect(suggestions[0]?.evidence).toContain('来源 未知来源 custom-source');
+    expect(suggestions[0]?.evidence).toContain('信号 未知信号 profiling');
+  });
+
+  it('localizes entity-id-only suggested-action targets while preserving the route token', () => {
+    const suggestions = buildSuggestedRemediationActions(
+      {
+        entityId: 'service:commerce/checkout',
+        source: 'entity'
+      },
+      t
+    );
+
+    expect(suggestions[0]?.title).toBe('建议重启 实体 service:commerce/checkout');
+    expect(suggestions[1]?.title).toBe('建议为 实体 service:commerce/checkout 创建临时静默');
+    expect(suggestions[2]?.title).toBe('建议查看 实体 service:commerce/checkout 处置手册');
+    expect(suggestions[0]?.title).not.toBe('建议重启 service:commerce/checkout');
+
+    const evidenceUrl = new URL(suggestions[0]?.evidenceHref || '/', 'http://localhost');
+    expect(evidenceUrl.searchParams.get('entityId')).toBe('service:commerce/checkout');
+  });
+
+  it('builds a non-executing approval draft request from the first suggested action', () => {
+    const draft = buildActionApprovalDraft(
+      {
+        source: 'alert',
+        signal: 'traces',
+        entityId: 'service:commerce/checkout',
+        serviceName: 'checkout-api',
+        traceId: 'trace-123',
+        returnTo: '/alert?status=firing&returnLabel=告警'
+      },
+      t
+    );
+
+    expect(draft).toMatchObject({
+      state: 'ready',
+      adapterOwner: 'next-actions-approval-draft-bff',
+      endpoint: '/api/actions/approval-drafts',
+      method: 'POST',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      title: '审批草稿适配器'
+    });
+    expect(draft.request).toMatchObject({
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout',
+      risk: 'high',
+      confirmation: 'manual-required',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      evidenceHref: expect.stringContaining('/alert?')
+    });
+    expect(draft.request?.context.returnTo).toBe('/alert?status=firing');
+    expect(draft.requestPreview).toContain('"actionId":"suggest-restart-checkout"');
+    expect(draft.requestPreview).toContain('"executionAllowed":false');
+  });
+
+  it('blocks approval draft creation when no evidence context is present', () => {
+    const draft = buildActionApprovalDraft({}, t);
+
+    expect(draft.state).toBe('awaiting-context');
+    expect(draft.request).toBeUndefined();
+    expect(draft.requestPreview).toBe('{}');
+    expect(draft.disabledReason).toBe('需要告警、实体或链路上下文后才能生成审批草稿。');
+  });
+
+  it('builds a non-executing action catalog read adapter contract', () => {
+    const adapter = buildActionCatalogReadAdapter(t);
+
+    expect(adapter).toMatchObject({
+      state: 'loading',
+      adapterOwner: 'next-actions-catalog-bff',
+      endpoint: '/api/actions/catalog?limit=8',
+      method: 'GET',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      managerBacked: false,
+      title: '动作目录适配器',
+      items: []
+    });
+  });
+
+  it('builds a non-executing approval decision adapter contract', () => {
+    const decision = buildActionApprovalDecision(t);
+
+    expect(decision).toMatchObject({
+      state: 'awaiting-draft',
+      adapterOwner: 'next-actions-approval-decision-bff',
+      endpointTemplate: '/api/actions/approval-drafts/:draftId/decision',
+      method: 'POST',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      title: '审批决策适配器'
+    });
+    expect(decision.requestPreview).toContain('"decision":"approved"');
+    expect(decision.requestPreview).toContain('"executionAllowed":false');
+  });
+
+  it('builds a non-executing approval draft queue read adapter contract', () => {
+    const queue = buildActionApprovalDraftQueue(t);
+
+    expect(queue).toMatchObject({
+      state: 'loading',
+      adapterOwner: 'next-actions-approval-draft-bff',
+      endpoint: '/api/actions/approval-drafts?limit=8',
+      method: 'GET',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false,
+      managerBacked: false,
+      title: '审批草稿队列',
+      drafts: []
+    });
+  });
+
   it('summarizes milestone 8 closure without promoting roadmap-only automation domains', () => {
     const review = buildAutomationActionCatalogReview();
 
diff --git a/web-next/lib/actions-surface/model.ts b/web-next/lib/actions-surface/model.ts
index 675b09bf57..04a257023b 100644
--- a/web-next/lib/actions-surface/model.ts
+++ b/web-next/lib/actions-surface/model.ts
@@ -9,6 +9,9 @@ export type ActionConfirmationMode = 'manual-required';
 export type ActionSnapshotState = 'roadmap-demo';
 export type ActionAdapterBoundaryState = 'adapter-pending';
 export type ActionLiveHandoff = 'alert-context-suggestions';
+export type ActionApprovalDraftState = 'awaiting-context' | 'ready';
+export type ActionApprovalDraftExecutionMode = 'manual-approval-draft-only';
+export type ActionApprovalDecisionValue = 'approved' | 'rejected';
 
 export type ActionCatalogItem = {
   id: string;
@@ -56,6 +59,8 @@ export type ActionSuggestionContext = SignalRouteContext & {
 export type SuggestedRemediationAction = {
   id: string;
   catalogId: string;
+  catalogLabel: string;
+  displayMeta: string;
   source: 'alert-context-handoff';
   title: string;
   copy: string;
@@ -66,12 +71,107 @@ export type SuggestedRemediationAction = {
   posture: string;
 };
 
+export type ActionApprovalDraftRequest = {
+  actionId: string;
+  catalogId: string;
+  risk: ActionRisk;
+  confirmation: ActionConfirmationMode;
+  executionMode: ActionApprovalDraftExecutionMode;
+  executionAllowed: false;
+  context: ActionSuggestionContext;
+  evidenceHref: string;
+};
+
+export type ActionApprovalDraft = {
+  state: ActionApprovalDraftState;
+  adapterOwner: 'next-actions-approval-draft-bff';
+  endpoint: '/api/actions/approval-drafts';
+  method: 'POST';
+  executionMode: ActionApprovalDraftExecutionMode;
+  executionAllowed: false;
+  title: string;
+  copy: string;
+  createLabel: string;
+  pendingLabel: string;
+  successLabel: string;
+  failedLabel: string;
+  disabledReason: string;
+  request?: ActionApprovalDraftRequest;
+  requestPreview: string;
+};
+
+export type ActionApprovalDecisionRequest = {
+  decision: ActionApprovalDecisionValue;
+  reviewer: string;
+  reason: string;
+  executionMode: ActionApprovalDraftExecutionMode;
+  executionAllowed: false;
+};
+
+export type ActionApprovalDecision = {
+  state: 'awaiting-draft';
+  adapterOwner: 'next-actions-approval-decision-bff';
+  endpointTemplate: '/api/actions/approval-drafts/:draftId/decision';
+  method: 'POST';
+  executionMode: ActionApprovalDraftExecutionMode;
+  executionAllowed: false;
+  title: string;
+  copy: string;
+  approveLabel: string;
+  rejectLabel: string;
+  pendingLabel: string;
+  successLabel: string;
+  failedLabel: string;
+  disabledReason: string;
+  requestPreview: string;
+};
+
+export type ActionApprovalDraftQueueItem = {
+  draftId: string;
+  state: string;
+  actionId?: string;
+  catalogId?: string;
+  executionState?: string;
+  adapterOwner?: string;
+};
+
+export type ActionApprovalDraftQueue = {
+  state: 'loading';
+  adapterOwner: 'next-actions-approval-draft-bff';
+  endpoint: '/api/actions/approval-drafts?limit=8';
+  method: 'GET';
+  executionMode: ActionApprovalDraftExecutionMode;
+  executionAllowed: false;
+  managerBacked: false;
+  title: string;
+  copy: string;
+  loadingLabel: string;
+  emptyLabel: string;
+  drafts: [];
+};
+
+export type ActionCatalogReadAdapter = {
+  state: 'loading';
+  adapterOwner: 'next-actions-catalog-bff';
+  endpoint: '/api/actions/catalog?limit=8';
+  method: 'GET';
+  executionMode: ActionApprovalDraftExecutionMode;
+  executionAllowed: false;
+  managerBacked: false;
+  title: string;
+  copy: string;
+  loadingLabel: string;
+  emptyLabel: string;
+  items: [];
+};
+
 export type ActionAdapterBoundary = {
   state: ActionAdapterBoundaryState;
   label: string;
   copy: string;
   liveHandoff: ActionLiveHandoff;
   roadmapOnly: string[];
+  roadmapOnlyLabels: string[];
 };
 
 export type AutomationActionCatalogReview = {
@@ -102,27 +202,43 @@ export type ActionsDomainModel = {
   nextHops: Array<{ label: string; href: string; variant: 'subtle' | 'default' | 'primary' }>;
 };
 
-export function buildActionsAdapterBoundary(): ActionAdapterBoundary {
+const catalogReviewTranslator: Translator = key => key;
+
+const actionAdapterBoundaryRoadmapIds = [
+  'workflow-automation',
+  'action-catalog',
+  'app-builder',
+  'self-service-actions',
+  'approvals',
+  'scripts',
+  'runbook-orchestration'
+];
+
+function actionAdapterBoundaryRoadmapLabels(t: Translator) {
+  return [
+    t('actions.adapter-boundary.roadmap.workflow-automation'),
+    t('actions.adapter-boundary.roadmap.action-catalog'),
+    t('actions.adapter-boundary.roadmap.app-builder'),
+    t('actions.adapter-boundary.roadmap.self-service-actions'),
+    t('actions.adapter-boundary.roadmap.approvals'),
+    t('actions.adapter-boundary.roadmap.scripts'),
+    t('actions.adapter-boundary.roadmap.runbook-orchestration')
+  ];
+}
+
+export function buildActionsAdapterBoundary(t: Translator = catalogReviewTranslator): ActionAdapterBoundary {
   return {
     state: 'adapter-pending',
-    label: '执行边界',
-    copy:
-      '动作目录、执行历史和审批流当前是 roadmap 示例快照,还未接入执行适配器,不代表实时运行状态;告警上下文建议是当前证据生成的人工交接,不会自动执行。',
+    label: t('actions.adapter-boundary.label'),
+    copy: t('actions.adapter-boundary.copy'),
     liveHandoff: 'alert-context-suggestions',
-    roadmapOnly: [
-      'workflow-automation',
-      'action-catalog',
-      'app-builder',
-      'self-service-actions',
-      'approvals',
-      'scripts',
-      'runbook-orchestration'
-    ]
+    roadmapOnly: actionAdapterBoundaryRoadmapIds,
+    roadmapOnlyLabels: actionAdapterBoundaryRoadmapLabels(t)
   };
 }
 
 export function buildAutomationActionCatalogReview(): AutomationActionCatalogReview {
-  const adapterBoundary = buildActionsAdapterBoundary();
+  const adapterBoundary = buildActionsAdapterBoundary(catalogReviewTranslator);
 
   return {
     milestone: 8,
@@ -184,95 +300,270 @@ function buildSuggestionEvidenceHref(context: ActionSuggestionContext) {
   return withQuery('/alert', params);
 }
 
-function actionTarget(context: ActionSuggestionContext) {
-  return normalizeText(context.serviceName) || normalizeText(context.entityName) || normalizeText(context.entityId) || '当前告警';
+function actionTarget(context: ActionSuggestionContext, t: Translator) {
+  const serviceName = normalizeText(context.serviceName);
+  if (serviceName) return serviceName;
+  const entityName = normalizeText(context.entityName);
+  if (entityName) return entityName;
+  const entityId = normalizeText(context.entityId);
+  if (entityId) return t('actions.suggestion.target.entity-id', { entityId });
+  return t('actions.suggestion.target.current-alert');
+}
+
+function suggestedActionSourceLabel(source: string | null | undefined, t: Translator) {
+  const normalized = normalizeText(source);
+  if (normalized === 'alert') return t('actions.suggestion.source.alert');
+  if (normalized === 'entity') return t('actions.suggestion.source.entity');
+  if (normalized === 'topology') return t('actions.suggestion.source.topology');
+  if (normalized === 'monitor') return t('actions.suggestion.source.monitor');
+  return t('actions.suggestion.source.unknown', { source: normalized ?? '' });
+}
+
+function suggestedActionSignalLabel(signal: string | null | undefined, t: Translator) {
+  const normalized = normalizeText(signal);
+  if (normalized === 'metrics') return t('actions.suggestion.signal.metrics');
+  if (normalized === 'logs') return t('actions.suggestion.signal.logs');
+  if (normalized === 'traces') return t('actions.suggestion.signal.traces');
+  if (normalized === 'alerts') return t('actions.suggestion.signal.alerts');
+  return t('actions.suggestion.signal.unknown', { signal: normalized ?? '' });
 }
 
-export function buildSuggestedRemediationActions(context: ActionSuggestionContext = {}): SuggestedRemediationAction[] {
+function suggestedActionRiskLabel(risk: ActionRisk, t: Translator) {
+  if (risk === 'high') return t('actions.risk.high');
+  if (risk === 'medium') return t('actions.risk.medium');
+  return t('actions.risk.low');
+}
+
+function suggestedActionCatalogLabel(catalogId: string, t: Translator) {
+  if (catalogId === 'restart-checkout') return t('actions.catalog.restart.name');
+  if (catalogId === 'mute-edge-alerts') return t('actions.catalog.mute.name');
+  return catalogId;
+}
+
+function suggestedActionDisplayMeta(risk: ActionRisk, catalogLabel: string, t: Translator) {
+  return `${suggestedActionRiskLabel(risk, t)} · ${catalogLabel}`;
+}
+
+function suggestedActionMeta(catalogId: string, risk: ActionRisk, t: Translator) {
+  const catalogLabel = suggestedActionCatalogLabel(catalogId, t);
+  return {
+    catalogLabel,
+    displayMeta: suggestedActionDisplayMeta(risk, catalogLabel, t)
+  };
+}
+
+export function buildSuggestedRemediationActions(context: ActionSuggestionContext = {}, t: Translator = catalogReviewTranslator): SuggestedRemediationAction[] {
   if (!hasSuggestionContext(context)) return [];
 
-  const target = actionTarget(context);
+  const target = actionTarget(context, t);
   const evidenceHref = buildSuggestionEvidenceHref(context);
+  const restartMeta = suggestedActionMeta('restart-checkout', 'high', t);
+  const silenceMeta = suggestedActionMeta('mute-edge-alerts', 'medium', t);
+  const runbookMeta = suggestedActionMeta('restart-checkout', 'low', t);
   const evidence = [
-    normalizeText(context.source) ? `来源 ${context.source}` : undefined,
-    normalizeText(context.signal) ? `信号 ${context.signal}` : undefined,
-    normalizeText(context.traceId) ? `trace ${context.traceId}` : undefined,
-    normalizeText(context.alertGroupId) ? `告警组 ${context.alertGroupId}` : undefined
-  ].filter(Boolean).join(' · ') || '当前告警证据';
+    normalizeText(context.source) ? t('actions.suggestion.evidence.source', { value: suggestedActionSourceLabel(context.source, t) }) : undefined,
+    normalizeText(context.signal) ? t('actions.suggestion.evidence.signal', { value: suggestedActionSignalLabel(context.signal, t) }) : undefined,
+    normalizeText(context.traceId) ? t('actions.suggestion.evidence.trace', { value: context.traceId }) : undefined,
+    normalizeText(context.alertGroupId) ? t('actions.suggestion.evidence.alert-group', { value: context.alertGroupId }) : undefined
+  ].filter(Boolean).join(' · ') || t('actions.suggestion.evidence.current-alert');
 
   return [
     {
       id: 'suggest-restart-checkout',
       catalogId: 'restart-checkout',
-      title: `建议重启 ${target}`,
-      copy: '仅作为高风险恢复建议,执行前需要确认影响范围、回滚窗口和负责人。',
+      catalogLabel: restartMeta.catalogLabel,
+      displayMeta: restartMeta.displayMeta,
+      title: t('actions.suggestion.restart.title', { target }),
+      copy: t('actions.suggestion.restart.copy'),
       evidence,
       evidenceHref,
       source: 'alert-context-handoff',
       risk: 'high',
       confirmation: 'manual-required',
-      posture: '只生成建议,人工确认后才能进入执行。'
+      posture: t('actions.suggestion.manual-required')
     },
     {
       id: 'suggest-create-silence',
       catalogId: 'mute-edge-alerts',
-      title: `建议为 ${target} 创建临时静默`,
-      copy: '用于短时降噪,先确认告警已接手并设置自动过期。',
+      catalogLabel: silenceMeta.catalogLabel,
+      displayMeta: silenceMeta.displayMeta,
+      title: t('actions.suggestion.silence.title', { target }),
+      copy: t('actions.suggestion.silence.copy'),
       evidence,
       evidenceHref,
       source: 'alert-context-handoff',
       risk: 'medium',
       confirmation: 'manual-required',
-      posture: '只生成建议,人工确认后才能进入执行。'
+      posture: t('actions.suggestion.manual-required')
     },
     {
       id: 'suggest-review-runbook',
       catalogId: 'restart-checkout',
-      title: `建议查看 ${target} 处置手册`,
-      copy: '先对齐实体、告警、链路和采集器证据,再决定是否执行动作。',
+      catalogLabel: runbookMeta.catalogLabel,
+      displayMeta: runbookMeta.displayMeta,
+      title: t('actions.suggestion.runbook.title', { target }),
+      copy: t('actions.suggestion.runbook.copy'),
       evidence,
       evidenceHref,
       source: 'alert-context-handoff',
       risk: 'low',
       confirmation: 'manual-required',
-      posture: '只生成建议,人工确认后才能进入执行。'
+      posture: t('actions.suggestion.manual-required')
     }
   ];
 }
 
+function buildApprovalDraftRequest(
+  context: ActionSuggestionContext,
+  action: SuggestedRemediationAction
+): ActionApprovalDraftRequest {
+  return {
+    actionId: action.id,
+    catalogId: action.catalogId,
+    risk: action.risk,
+    confirmation: action.confirmation,
+    executionMode: 'manual-approval-draft-only',
+    executionAllowed: false,
+    context: {
+      ...context,
+      returnTo: stripReturnLabelFromHref(context.returnTo)
+    },
+    evidenceHref: action.evidenceHref
+  };
+}
+
+function previewApprovalDraftRequest(request: ActionApprovalDraftRequest | undefined) {
+  if (!request) return '{}';
+  return JSON.stringify({
+    actionId: request.actionId,
+    catalogId: request.catalogId,
+    risk: request.risk,
+    confirmation: request.confirmation,
+    executionMode: request.executionMode,
+    executionAllowed: request.executionAllowed,
+    entityId: request.context.entityId,
+    serviceName: request.context.serviceName,
+    traceId: request.context.traceId
+  });
+}
+
+export function buildActionApprovalDecision(t: Translator = catalogReviewTranslator): ActionApprovalDecision {
+  return {
+    state: 'awaiting-draft',
+    adapterOwner: 'next-actions-approval-decision-bff',
+    endpointTemplate: '/api/actions/approval-drafts/:draftId/decision',
+    method: 'POST',
+    executionMode: 'manual-approval-draft-only',
+    executionAllowed: false,
+    title: t('actions.approval-decision.title'),
+    copy: t('actions.approval-decision.copy'),
+    approveLabel: t('actions.approval-decision.approve'),
+    rejectLabel: t('actions.approval-decision.reject'),
+    pendingLabel: t('actions.approval-decision.pending'),
+    successLabel: t('actions.approval-decision.success'),
+    failedLabel: t('actions.approval-decision.failed'),
+    disabledReason: t('actions.approval-decision.disabled'),
+    requestPreview: JSON.stringify({
+      decision: 'approved',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false
+    })
+  };
+}
+
+export function buildActionApprovalDraftQueue(t: Translator = catalogReviewTranslator): ActionApprovalDraftQueue {
+  return {
+    state: 'loading',
+    adapterOwner: 'next-actions-approval-draft-bff',
+    endpoint: '/api/actions/approval-drafts?limit=8',
+    method: 'GET',
+    executionMode: 'manual-approval-draft-only',
+    executionAllowed: false,
+    managerBacked: false,
+    title: t('actions.approval-draft-queue.title'),
+    copy: t('actions.approval-draft-queue.copy'),
+    loadingLabel: t('actions.approval-draft-queue.loading'),
+    emptyLabel: t('actions.approval-draft-queue.empty'),
+    drafts: []
+  };
+}
+
+export function buildActionApprovalDraft(
+  context: ActionSuggestionContext = {},
+  t: Translator = catalogReviewTranslator
+): ActionApprovalDraft {
+  const [firstSuggestion] = buildSuggestedRemediationActions(context, t);
+  const request = firstSuggestion ? buildApprovalDraftRequest(context, firstSuggestion) : undefined;
+
+  return {
+    state: request ? 'ready' : 'awaiting-context',
+    adapterOwner: 'next-actions-approval-draft-bff',
+    endpoint: '/api/actions/approval-drafts',
+    method: 'POST',
+    executionMode: 'manual-approval-draft-only',
+    executionAllowed: false,
+    title: t('actions.approval-draft.title'),
+    copy: t('actions.approval-draft.copy'),
+    createLabel: t('actions.approval-draft.create'),
+    pendingLabel: t('actions.approval-draft.pending'),
+    successLabel: t('actions.approval-draft.success'),
+    failedLabel: t('actions.approval-draft.failed'),
+    disabledReason: t('actions.approval-draft.disabled'),
+    request,
+    requestPreview: previewApprovalDraftRequest(request)
+  };
+}
+
+export function buildActionCatalogReadAdapter(t: Translator = catalogReviewTranslator): ActionCatalogReadAdapter {
+  return {
+    state: 'loading',
+    adapterOwner: 'next-actions-catalog-bff',
+    endpoint: '/api/actions/catalog?limit=8',
+    method: 'GET',
+    executionMode: 'manual-approval-draft-only',
+    executionAllowed: false,
+    managerBacked: false,
+    title: t('actions.catalog-adapter.title'),
+    copy: t('actions.catalog-adapter.copy'),
+    loadingLabel: t('actions.catalog-adapter.loading'),
+    emptyLabel: t('actions.catalog-adapter.empty'),
+    items: []
+  };
+}
+
 export function buildActionsDomainModel(t: Translator, context: ActionSuggestionContext = {}): ActionsDomainModel {
   const catalog: ActionCatalogItem[] = [
     {
       id: 'restart-checkout',
-      name: 'Restart checkout deployment',
-      category: 'runtime recovery',
-      scope: 'service checkout / prod-ap',
-      owner: 'Platform operations',
+      name: t('actions.catalog.restart.name'),
+      category: t('actions.catalog.restart.category'),
+      scope: t('actions.catalog.restart.scope'),
+      owner: t('actions.catalog.restart.owner'),
       risk: 'high',
-      lastRun: '12m ago',
-      posture: 'Requires approval and rollback note',
+      lastRun: t('actions.catalog.restart.last-run'),
+      posture: t('actions.catalog.restart.posture'),
       snapshotState: 'roadmap-demo'
     },
     {
       id: 'replay-metrics',
-      name: 'Replay delayed OTLP metrics',
-      category: 'signal remediation',
-      scope: 'otlp gateway / metrics',
-      owner: 'Observability',
+      name: t('actions.catalog.replay.name'),
+      category: t('actions.catalog.replay.category'),
+      scope: t('actions.catalog.replay.scope'),
+      owner: t('actions.catalog.replay.owner'),
       risk: 'medium',
-      lastRun: '38m ago',
-      posture: 'Safe within current replay window',
+      lastRun: t('actions.catalog.replay.last-run'),
+      posture: t('actions.catalog.replay.posture'),
       snapshotState: 'roadmap-demo'
     },
     {
       id: 'mute-edge-alerts',
-      name: 'Mute edge saturation alerts',
-      category: 'alert hygiene',
-      scope: 'entity edge-proxy',
-      owner: 'SRE on-call',
+      name: t('actions.catalog.mute.name'),
+      category: t('actions.catalog.mute.category'),
+      scope: t('actions.catalog.mute.scope'),
+      owner: t('actions.catalog.mute.owner'),
       risk: 'low',
-      lastRun: '2h ago',
-      posture: 'Auto-expires after 30 minutes',
+      lastRun: t('actions.catalog.mute.last-run'),
+      posture: t('actions.catalog.mute.posture'),
       snapshotState: 'roadmap-demo'
     }
   ];
@@ -280,32 +571,32 @@ export function buildActionsDomainModel(t: Translator, context: ActionSuggestion
   const runs: ActionRunItem[] = [
     {
       id: 'run-2144',
-      name: 'Restart checkout deployment',
-      target: 'checkout / prod-ap',
-      actor: 'li.na',
+      name: t('actions.run.restart.name'),
+      target: t('actions.run.restart.target'),
+      actor: t('actions.run.restart.actor'),
       status: 'awaiting-approval',
       startedAt: '2026-04-16 09:14',
-      duration: 'pending approval',
+      duration: t('actions.run.restart.duration'),
       snapshotState: 'roadmap-demo'
     },
     {
       id: 'run-2141',
-      name: 'Replay delayed OTLP metrics',
-      target: 'gateway metrics shard-3',
-      actor: 'automation',
+      name: t('actions.run.replay.name'),
+      target: t('actions.run.replay.target'),
+      actor: t('actions.run.replay.actor'),
       status: 'running',
       startedAt: '2026-04-16 08:56',
-      duration: '06m 12s',
+      duration: t('actions.run.replay.duration'),
       snapshotState: 'roadmap-demo'
     },
     {
       id: 'run-2138',
-      name: 'Mute edge saturation alerts',
-      target: 'edge-proxy / cn-hz',
-      actor: 'lin.qi',
+      name: t('actions.run.mute.name'),
+      target: t('actions.run.mute.target'),
+      actor: t('actions.run.mute.actor'),
       status: 'completed',
       startedAt: '2026-04-16 08:03',
-      duration: '01m 42s',
+      duration: t('actions.run.mute.duration'),
       snapshotState: 'roadmap-demo'
     }
   ];
@@ -313,43 +604,47 @@ export function buildActionsDomainModel(t: Translator, context: ActionSuggestion
   const approvals: ActionApprovalItem[] = [
     {
       id: 'apr-71',
-      summary: 'Restart production checkout pods after saturation alert burst',
-      owner: 'checkout-oncall',
-      evidence: 'Alert storm and trace latency regression are attached.',
+      summary: t('actions.approval.restart.summary'),
+      owner: t('actions.approval.restart.owner'),
+      evidence: t('actions.approval.restart.evidence'),
       status: 'pending',
       snapshotState: 'roadmap-demo'
     },
     {
       id: 'apr-69',
-      summary: 'Replay OTLP metrics for delayed collector queue',
-      owner: 'obs-admin',
-      evidence: 'Collector backlog has stabilized and replay window is open.',
+      summary: t('actions.approval.replay.summary'),
+      owner: t('actions.approval.replay.owner'),
+      evidence: t('actions.approval.replay.evidence'),
       status: 'approved',
       snapshotState: 'roadmap-demo'
     }
   ];
 
   return {
-    title: 'Actions',
+    title: t('actions.entry.title'),
     subtitle: t('actions.subtitle'),
     focus: t('actions.focus'),
     summary: t('actions.summary'),
-    tags: ['automation catalog', 'risk-aware actions', 'approval flow'],
+    tags: [
+      t('actions.tag.catalog'),
+      t('actions.tag.risk-aware'),
+      t('actions.tag.approval-flow')
+    ],
     metrics: [
-      { label: 'Catalog entries', value: String(catalog.length) },
-      { label: 'Pending approvals', value: String(approvals.filter(item => item.status === 'pending').length) },
-      { label: 'Recent runs', value: String(runs.length) },
-      { label: 'High-risk actions', value: String(catalog.filter(item => item.risk === 'high').length) }
+      { label: t('actions.metric.catalog-entries'), value: String(catalog.length) },
+      { label: t('actions.metric.pending-approvals'), value: String(approvals.filter(item => item.status === 'pending').length) },
+      { label: t('actions.metric.recent-runs'), value: String(runs.length) },
+      { label: t('actions.metric.high-risk-actions'), value: String(catalog.filter(item => item.risk === 'high').length) }
     ],
     catalog,
     runs,
     approvals,
-    adapterBoundary: buildActionsAdapterBoundary(),
-    suggestedActions: buildSuggestedRemediationActions(context),
+    adapterBoundary: buildActionsAdapterBoundary(t),
+    suggestedActions: buildSuggestedRemediationActions(context, t),
     checklist: [
-      { title: t('actions.checklist.entry.title'), copy: t('actions.checklist.entry.copy'), meta: 'done' },
-      { title: t('actions.checklist.adapters.title'), copy: t('actions.checklist.adapters.copy'), meta: 'next' },
-      { title: t('actions.checklist.context.title'), copy: t('actions.checklist.context.copy'), meta: 'reserved' }
+      { title: t('actions.checklist.entry.title'), copy: t('actions.checklist.entry.copy'), meta: t('actions.checklist.entry.meta') },
+      { title: t('actions.checklist.adapters.title'), copy: t('actions.checklist.adapters.copy'), meta: t('actions.checklist.adapters.meta') },
+      { title: t('actions.checklist.context.title'), copy: t('actions.checklist.context.copy'), meta: t('actions.checklist.context.meta') }
     ],
     nextHops: [
       { label: t('menu.dashboard.back'), href: '/overview', variant: 'subtle' },
diff --git a/web-next/lib/actions-surface/query-state.test.ts b/web-next/lib/actions-surface/query-state.test.ts
new file mode 100644
index 0000000000..69e13246b3
--- /dev/null
+++ b/web-next/lib/actions-surface/query-state.test.ts
@@ -0,0 +1,56 @@
+import { describe, expect, it } from 'vitest';
+import { readActionsSuggestionContext } from './query-state';
+
+describe('actions query state', () => {
+  it('normalizes multi-value URL search params into the first suggestion context value', () => {
+    expect(
+      readActionsSuggestionContext({
+        source: ['alert', 'ignored-source'],
+        signal: ['traces', 'logs'],
+        status: ['firing', 'resolved'],
+        entityId: ['service:commerce/checkout', 'service:commerce/cart'],
+        entityName: ['checkout-api'],
+        serviceName: ['checkout-api'],
+        serviceNamespace: ['commerce'],
+        environment: ['prod'],
+        timeRange: ['last-1h'],
+        start: ['1710000000000'],
+        end: ['1710003600000'],
+        traceId: ['trace-123'],
+        spanId: ['span-456'],
+        collector: ['edge-collector-a'],
+        template: ['java-service'],
+        search: ['severity:critical'],
+        severity: ['critical'],
+        alertGroupId: ['group-7'],
+        viewMode: ['evidence'],
+        sourceKind: ['topology'],
+        edgeId: ['edge-9'],
+        returnTo: ['/alert?status=firing&returnLabel=告警']
+      })
+    ).toEqual({
+      source: 'alert',
+      signal: 'traces',
+      status: 'firing',
+      entityId: 'service:commerce/checkout',
+      entityName: 'checkout-api',
+      serviceName: 'checkout-api',
+      serviceNamespace: 'commerce',
+      environment: 'prod',
+      timeRange: 'last-1h',
+      start: '1710000000000',
+      end: '1710003600000',
+      traceId: 'trace-123',
+      spanId: 'span-456',
+      collector: 'edge-collector-a',
+      template: 'java-service',
+      search: 'severity:critical',
+      severity: 'critical',
+      alertGroupId: 'group-7',
+      viewMode: 'evidence',
+      sourceKind: 'topology',
+      edgeId: 'edge-9',
+      returnTo: '/alert?status=firing&returnLabel=告警'
+    });
+  });
+});
diff --git a/web-next/lib/actions-surface/query-state.ts b/web-next/lib/actions-surface/query-state.ts
new file mode 100644
index 0000000000..71491fe742
--- /dev/null
+++ b/web-next/lib/actions-surface/query-state.ts
@@ -0,0 +1,35 @@
+import type { ActionSuggestionContext } from './model';
+
+export type ActionsSearchParams = Record<string, string | string[] | undefined>;
+
+function firstParam(value: string | string[] | undefined) {
+  return Array.isArray(value) ? value[0] : value;
+}
+
+export function readActionsSuggestionContext(searchParams: ActionsSearchParams | undefined): ActionSuggestionContext {
+  if (!searchParams) return {};
+  return {
+    start: firstParam(searchParams.start),
+    end: firstParam(searchParams.end),
+    timeRange: firstParam(searchParams.timeRange),
+    entityId: firstParam(searchParams.entityId),
+    entityName: firstParam(searchParams.entityName),
+    returnTo: firstParam(searchParams.returnTo),
+    serviceName: firstParam(searchParams.serviceName),
+    serviceNamespace: firstParam(searchParams.serviceNamespace),
+    environment: firstParam(searchParams.environment),
+    traceId: firstParam(searchParams.traceId),
+    spanId: firstParam(searchParams.spanId),
+    source: firstParam(searchParams.source),
+    collector: firstParam(searchParams.collector),
+    template: firstParam(searchParams.template),
+    search: firstParam(searchParams.search),
+    signal: firstParam(searchParams.signal),
+    severity: firstParam(searchParams.severity),
+    status: firstParam(searchParams.status),
+    alertGroupId: firstParam(searchParams.alertGroupId),
+    viewMode: firstParam(searchParams.viewMode),
+    sourceKind: firstParam(searchParams.sourceKind),
+    edgeId: firstParam(searchParams.edgeId)
+  };
+}
diff --git a/web-next/lib/actions-surface/view-model.test.ts b/web-next/lib/actions-surface/view-model.test.ts
index 2e0e341e72..25e273907a 100644
--- a/web-next/lib/actions-surface/view-model.test.ts
+++ b/web-next/lib/actions-surface/view-model.test.ts
@@ -1,32 +1,12 @@
 import { describe, expect, it } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 import { buildActionsPlaceholderState, buildActionsSurfaceViewModel } from './view-model';
 
-const t = (key: string) => {
-  const messages: Record<string, string> = {
-    'actions.subtitle': 'Automation catalog, execution history, and approval-aware operations all stay in one context.',
-    'actions.focus': 'Automation inventory and execution guardrails',
-    'actions.summary': 'The actions entry page carries automation catalog, execution history, and approval semantics without requiring a page redesign for future data.',
-    'actions.checklist.entry.title': 'Entry shell ready',
-    'actions.checklist.entry.copy': 'The action catalog already uses the shared workbench language.',
-    'actions.checklist.adapters.title': 'Execution adapters',
-    'actions.checklist.adapters.copy': 'Action list, run result, and approval flow keep the current integration boundary.',
-    'actions.checklist.context.title': 'Context continuity',
-    'actions.checklist.context.copy': 'Jump to the actions page directly from entity, alert, or topology context.',
-    'common.workspace': 'Workspace',
-    'common.focus': 'Focus',
-    'common.mode': 'Mode',
-    'common.signals': 'Signals',
-    'ops.surface.mode.entry': 'entry surface',
-    'menu.dashboard.back': 'Open overview',
-    'menu.entity.center': 'Entity center',
-    'menu.monitor.center': 'Monitor center'
-  };
-  return messages[key] ?? key;
-};
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('actions surface view model', () => {
   it('describes the OTLP cold-matte placeholder shell', () => {
-    const state = buildActionsPlaceholderState();
+    const state = buildActionsPlaceholderState(t);
 
     expect(state).toMatchObject({
       kicker: '自动化入口',
@@ -34,7 +14,34 @@ describe('actions surface view model', () => {
       subtitle: '按 OTLP 工作台的冷色基线统一入口、上下文和审批语义。',
       adapterBoundary: {
         state: 'adapter-pending',
-        label: '执行边界'
+        label: '执行边界',
+        roadmapOnlyLabels: ['工作流自动化', '动作目录', '应用编排器', '自助动作', '审批流', '脚本执行', '处置手册编排']
+      },
+      approvalDraft: {
+        state: 'awaiting-context',
+        adapterOwner: 'next-actions-approval-draft-bff',
+        endpoint: '/api/actions/approval-drafts',
+        executionAllowed: false
+      },
+      approvalDecision: {
+        state: 'awaiting-draft',
+        adapterOwner: 'next-actions-approval-decision-bff',
+        endpointTemplate: '/api/actions/approval-drafts/:draftId/decision',
+        executionAllowed: false
+      },
+      approvalDraftQueue: {
+        state: 'loading',
+        adapterOwner: 'next-actions-approval-draft-bff',
+        endpoint: '/api/actions/approval-drafts?limit=8',
+        executionAllowed: false,
+        managerBacked: false
+      },
+      catalogAdapter: {
+        state: 'loading',
+        adapterOwner: 'next-actions-catalog-bff',
+        endpoint: '/api/actions/catalog?limit=8',
+        executionAllowed: false,
+        managerBacked: false
       },
       shell: {
         eyebrow: '冷色入口已接入',
@@ -70,19 +77,54 @@ describe('actions surface view model', () => {
   it('derives catalog, run, and approval sections from the domain snapshot', () => {
     const viewModel = buildActionsSurfaceViewModel(t);
 
-    expect(viewModel.kicker).toBe('Action control plane');
+    expect(viewModel.kicker).toBe('自动化控制面');
+    expect(viewModel.subtitle).toBe('自动化目录、执行历史和审批感知操作保持在同一上下文中。');
+    expect(viewModel.tags).toEqual(['自动化目录', '风险感知动作', '审批流']);
+    expect(viewModel.facts.find(fact => fact.label === '信号')?.value).toBe('3');
     expect(viewModel.catalogCards).toHaveLength(3);
     expect(viewModel.catalogCards[0]).toMatchObject({
-      title: 'Restart checkout deployment',
-      eyebrow: 'runtime recovery'
+      title: '重启 checkout 部署',
+      eyebrow: '运行时恢复',
+      copy: '服务 checkout / prod-ap · 平台运维',
+      posture: '需要审批和回滚说明'
+    });
+    expect(viewModel.catalogCards[0]?.meta).toContain('12 分钟前');
+    expect(viewModel.runRows[0]).toMatchObject({
+      title: '重启 checkout 部署',
+      copy: 'checkout / prod-ap · li.na · 待审批'
+    });
+    expect(viewModel.runRows[0]?.meta).toContain('等待审批');
+    expect(viewModel.approvalRows[0]?.meta).toContain('等待负责人决策');
+    expect(viewModel.approvalRows[0]).toMatchObject({
+      title: '重启生产 checkout Pod 以处理饱和告警突增',
+      copy: 'checkout-oncall · 已附加告警风暴和链路延迟回退证据。'
+    });
+    expect(viewModel.checklist[0]).toMatchObject({
+      title: '入口壳层就绪',
+      copy: '动作目录已经沿用共享工作台语言。',
+      meta: '已完成'
+    });
+    expect(viewModel.checklist[1]).toMatchObject({
+      title: '执行适配器',
+      copy: '动作列表、执行结果和审批流保持当前集成边界。',
+      meta: '下一步'
+    });
+    expect(viewModel.handoffRows[0]).toMatchObject({
+      title: '实体上下文交接',
+      copy: '扩大动作目标范围前,先打开实体目录核对。',
+      meta: 'entities'
     });
-    expect(viewModel.runRows[0]?.meta).toContain('awaiting approval');
-    expect(viewModel.approvalRows[0]?.meta).toContain('pending owner decision');
-    expect(viewModel.handoffRows).toHaveLength(3);
+    expect(viewModel.handoffRows[1]).toMatchObject({
+      title: '监控姿态交接',
+      copy: '下发恢复动作前先校验信号健康状态。',
+      meta: 'monitors'
+    });
+    expect(viewModel.nextHops.map(item => item.label)).toEqual(['打开总览', '实体中心', '监控中心']);
+    expect(viewModel.nextHops.map(item => item.href)).toEqual(['/overview', '/entities', '/monitors']);
   });
 
   it('keeps alert-context suggested actions in a human-confirmed posture', () => {
-    const state = buildActionsPlaceholderState({
+    const state = buildActionsPlaceholderState(t, {
       source: 'alert',
       signal: 'traces',
       severity: 'critical',
@@ -98,9 +140,39 @@ describe('actions surface view model', () => {
     expect(state.suggestedActions[0]).toMatchObject({
       id: 'suggest-restart-checkout',
       title: '建议重启 checkout-api',
+      catalogLabel: '重启 checkout 部署',
+      displayMeta: '高风险 · 重启 checkout 部署',
       source: 'alert-context-handoff',
       confirmation: 'manual-required'
     });
+    expect(state.approvalDraft).toMatchObject({
+      state: 'ready',
+      endpoint: '/api/actions/approval-drafts',
+      executionMode: 'manual-approval-draft-only',
+      executionAllowed: false
+    });
+    expect(state.approvalDraft.request).toMatchObject({
+      actionId: 'suggest-restart-checkout',
+      catalogId: 'restart-checkout',
+      executionAllowed: false
+    });
     expect(state.suggestedActions.every(action => action.posture.includes('人工确认'))).toBe(true);
+    expect(state.suggestedActions[0]?.evidence).toContain('来源 告警中心');
+    expect(state.suggestedActions[0]?.evidence).toContain('信号 链路');
+    expect(state.suggestedActions[0]?.evidence).not.toContain('来源 alert');
+    expect(state.suggestedActions[0]?.evidence).not.toContain('信号 traces');
+  });
+
+  it('keeps entity-id-only suggested action targets localized without changing handoff data', () => {
+    const state = buildActionsPlaceholderState(t, {
+      entityId: 'service:commerce/checkout',
+      source: 'entity'
+    });
+
+    expect(state.suggestedActions[0]?.title).toBe('建议重启 实体 service:commerce/checkout');
+    expect(state.suggestedActions[0]?.title).not.toBe('建议重启 service:commerce/checkout');
+
+    const evidenceUrl = new URL(state.suggestedActions[0]?.evidenceHref || '/', 'http://localhost');
+    expect(evidenceUrl.searchParams.get('entityId')).toBe('service:commerce/checkout');
   });
 });
diff --git a/web-next/lib/actions-surface/view-model.ts b/web-next/lib/actions-surface/view-model.ts
index 0c6d892b4e..a5c0235fb9 100644
--- a/web-next/lib/actions-surface/view-model.ts
+++ b/web-next/lib/actions-surface/view-model.ts
@@ -1,8 +1,16 @@
 import { buildOpsFacts } from '../ops-surface/view-model';
 import {
   buildActionsAdapterBoundary,
+  buildActionApprovalDecision,
+  buildActionApprovalDraftQueue,
+  buildActionCatalogReadAdapter,
   buildActionsDomainModel,
+  buildActionApprovalDraft,
   buildSuggestedRemediationActions,
+  type ActionApprovalDraft,
+  type ActionApprovalDecision,
+  type ActionApprovalDraftQueue,
+  type ActionCatalogReadAdapter,
   type ActionAdapterBoundary,
   type ActionRisk,
   type ActionRunStatus,
@@ -24,6 +32,10 @@ export type ActionsPlaceholderState = {
     chips: string[];
   };
   adapterBoundary: ActionAdapterBoundary;
+  approvalDraft: ActionApprovalDraft;
+  approvalDecision: ActionApprovalDecision;
+  approvalDraftQueue: ActionApprovalDraftQueue;
+  catalogAdapter: ActionCatalogReadAdapter;
   checklistTitle: string;
   checklist: Array<{ title: string; copy: string; tone: string }>;
   suggestedActions: SuggestedRemediationAction[];
@@ -33,62 +45,69 @@ export type ActionsPlaceholderState = {
   };
 };
 
-export function buildActionsPlaceholderState(context: ActionSuggestionContext = {}): ActionsPlaceholderState {
+export function buildActionsPlaceholderState(t: Translator, context: ActionSuggestionContext = {}): ActionsPlaceholderState {
   return {
-    kicker: '自动化入口',
-    title: '自动化处置',
-    subtitle: '按 OTLP 工作台的冷色基线统一入口、上下文和审批语义。',
+    kicker: t('actions.entry.kicker'),
+    title: t('actions.entry.title'),
+    subtitle: t('actions.entry.subtitle'),
     actions: [
-      { label: '打开概览', href: '/overview', variant: 'primary' },
-      { label: '查看对象', href: '/entities', variant: 'subtle' }
+      { label: t('actions.entry.action.overview'), href: '/overview', variant: 'primary' },
+      { label: t('actions.entry.action.entities'), href: '/entities', variant: 'subtle' }
     ],
     shell: {
-      eyebrow: '冷色入口已接入',
-      copy:
-        '此入口继承 OTLP 接入页的近黑画布、小按钮、硬边面板和静态右栏节奏,后续只需要接入执行适配器。',
-      chips: ['自动化目录', '风险动作', '审批流']
+      eyebrow: t('actions.entry.shell.eyebrow'),
+      copy: t('actions.entry.shell.copy'),
+      chips: [
+        t('actions.entry.chip.catalog'),
+        t('actions.entry.chip.risk'),
+        t('actions.entry.chip.approval')
+      ]
     },
-    adapterBoundary: buildActionsAdapterBoundary(),
-    checklistTitle: '接入清单',
+    adapterBoundary: buildActionsAdapterBoundary(t),
+    catalogAdapter: buildActionCatalogReadAdapter(t),
+    approvalDraft: buildActionApprovalDraft(context, t),
+    approvalDecision: buildActionApprovalDecision(t),
+    approvalDraftQueue: buildActionApprovalDraftQueue(t),
+    checklistTitle: t('actions.entry.checklist.title'),
     checklist: [
       {
-        title: '统一入口上下文',
-        copy: '已和 OTLP 基线共用同一套页面节奏。',
+        title: t('actions.entry.checklist.context.title'),
+        copy: t('actions.entry.checklist.context.copy'),
         tone: 'bg-[#75ad86]'
       },
       {
-        title: '接入执行适配器',
-        copy: '等动作目录和审批流数据落地后直接挂载。',
+        title: t('actions.entry.checklist.adapter.title'),
+        copy: t('actions.entry.checklist.adapter.copy'),
         tone: 'bg-[#c2a86b]'
       },
       {
-        title: '保留证据跳转',
-        copy: '实体、告警和链路入口保持在同一操作上下文。',
+        title: t('actions.entry.checklist.evidence.title'),
+        copy: t('actions.entry.checklist.evidence.copy'),
         tone: 'bg-[#9aa9cf]'
       }
     ],
-    suggestedActions: buildSuggestedRemediationActions(context),
+    suggestedActions: buildSuggestedRemediationActions(context, t),
     empty: {
-      title: '等待接入执行适配器',
-      copy: '视觉骨架已经统一到 OTLP 基线,后续数据接入可以增量完成。'
+      title: t('actions.entry.empty.title'),
+      copy: t('actions.entry.empty.copy')
     }
   };
 }
 
-function formatRiskTone(risk: ActionRisk) {
-  if (risk === 'high') return 'high risk';
-  if (risk === 'medium') return 'medium risk';
-  return 'low risk';
+function formatRiskTone(risk: ActionRisk, t: Translator) {
+  if (risk === 'high') return t('actions.risk.high');
+  if (risk === 'medium') return t('actions.risk.medium');
+  return t('actions.risk.low');
 }
 
-function formatRunTone(status: ActionRunStatus) {
-  if (status === 'awaiting-approval') return 'awaiting approval';
-  if (status === 'running') return 'running now';
-  return 'completed';
+function formatRunTone(status: ActionRunStatus, t: Translator) {
+  if (status === 'awaiting-approval') return t('actions.run-status.awaiting-approval');
+  if (status === 'running') return t('actions.run-status.running');
+  return t('actions.run-status.completed');
 }
 
-function formatApprovalTone(status: ApprovalStatus) {
-  return status === 'pending' ? 'pending owner decision' : 'approved';
+function formatApprovalTone(status: ApprovalStatus, t: Translator) {
+  return status === 'pending' ? t('actions.approval-status.pending') : t('actions.approval-status.approved');
 }
 
 export function buildActionsSurfaceViewModel(t: Translator) {
@@ -96,14 +115,14 @@ export function buildActionsSurfaceViewModel(t: Translator) {
 
   return {
     ...model,
-    kicker: 'Action control plane',
+    kicker: t('actions.surface.kicker'),
     facts: buildOpsFacts(model.title, model.focus, model.tags, t),
     catalogCards: model.catalog.map(item => ({
       id: item.id,
       eyebrow: item.category,
       title: item.name,
       copy: `${item.scope} · ${item.owner}`,
-      meta: `${formatRiskTone(item.risk)} · ${item.lastRun} · 示例快照`,
+      meta: `${formatRiskTone(item.risk, t)} · ${item.lastRun} · ${t('actions.snapshot.label')}`,
       posture: item.posture,
       badges: [item.risk, item.lastRun],
       snapshotState: item.snapshotState
@@ -111,19 +130,19 @@ export function buildActionsSurfaceViewModel(t: Translator) {
     runRows: model.runs.map(item => ({
       title: item.name,
       copy: `${item.target} · ${item.actor} · ${item.duration}`,
-      meta: `${formatRunTone(item.status)} · ${item.startedAt} · 示例快照`,
+      meta: `${formatRunTone(item.status, t)} · ${item.startedAt} · ${t('actions.snapshot.label')}`,
       snapshotState: item.snapshotState
     })),
     approvalRows: model.approvals.map(item => ({
       title: item.summary,
       copy: `${item.owner} · ${item.evidence}`,
-      meta: `${formatApprovalTone(item.status)} · ${item.id} · 示例快照`,
+      meta: `${formatApprovalTone(item.status, t)} · ${item.id} · ${t('actions.snapshot.label')}`,
       snapshotState: item.snapshotState
     })),
     handoffRows: [
-      { title: 'Entity context handoff', copy: 'Open entity inventory before expanding action target scope.', meta: 'entities' },
-      { title: 'Monitor posture handoff', copy: 'Validate signal health before issuing recovery actions.', meta: 'monitors' },
-      { title: 'Approval evidence handoff', copy: 'Keep alerts, traces, and rollback notes together in the same operator flow.', meta: 'approval' }
+      { title: t('actions.surface.handoff.entity.title'), copy: t('actions.surface.handoff.entity.copy'), meta: 'entities' },
+      { title: t('actions.surface.handoff.monitor.title'), copy: t('actions.surface.handoff.monitor.copy'), meta: 'monitors' },
+      { title: t('actions.surface.handoff.approval.title'), copy: t('actions.surface.handoff.approval.copy'), meta: 'approval' }
     ]
   };
 }
diff --git a/web-next/lib/incidents-surface/controller.test.ts b/web-next/lib/incidents-surface/controller.test.ts
new file mode 100644
index 0000000000..e003403ac9
--- /dev/null
+++ b/web-next/lib/incidents-surface/controller.test.ts
@@ -0,0 +1,191 @@
+import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
+import {
+  buildIncidentStatusTransitionPayload,
+  buildIncidentWorkbenchData,
+  INCIDENT_WORKBENCH_DEFAULT_QUERY,
+  loadIncidentWorkbenchData,
+  readIncidentWorkbenchQuery,
+  transitionIncidentStatus
+} from './controller';
+
+const t = createTranslatorMock({ locale: 'en-US' });
+const formatTime = (value?: number | string | null) => (value == null ? '-' : `time:${value}`);
+
+describe('incidents surface controller', () => {
+  it('loads the shared incident workbench from the status-page incident list endpoint', async () => {
+    const apiGet = vi.fn()
+      .mockResolvedValueOnce({
+        content: [
+          {
+            id: 42,
+            title: 'API latency incident',
+            state: 0,
+            startTime: 1712730000000,
+            creator: 'platform-oncall',
+            components: [{ id: 7, name: 'api-gateway' }]
+          }
+        ],
+        totalElements: 3,
+        pageIndex: 0,
+        pageSize: 8
+      })
+      .mockResolvedValueOnce({
+        id: 42,
+        name: 'API latency incident detail',
+        state: 1,
+        startTime: 1712730000000,
+        modifier: 'detail-responder',
+        components: [{ id: 7, name: 'api-gateway' }, { id: 8, name: 'checkout' }],
+        contents: [{ id: 8, state: 1, message: 'Rollback started from detail endpoint', timestamp: 1712730300000 }]
+      });
+
+    const result = await loadIncidentWorkbenchData(apiGet as any, t, formatTime, INCIDENT_WORKBENCH_DEFAULT_QUERY);
+
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/status/page/incident?pageIndex=0&pageSize=8');
+    expect(apiGet).toHaveBeenNthCalledWith(2, '/status/page/incident/42');
+    expect(result).toMatchObject({
+      apiState: 'ready',
+      apiSource: 'status-page-incident-list',
+      detailState: 'ready',
+      detailSource: 'status-page-incident-detail',
+      detailId: '42',
+      queryLabel: '/status/page/incident?pageIndex=0&pageSize=8',
+      title: 'Incidents',
+      kicker: 'Incident response desk',
+      totalElements: 3,
+      selectedIncidentId: '42',
+      transitionState: 'ready'
+    });
+    expect(result.selectedIncident).toMatchObject({
+      id: 42,
+      name: 'API latency incident detail',
+      state: 1
+    });
+    expect(result.metrics).toEqual([
+      { label: 'Open incidents', value: '3', tone: 'warning' },
+      { label: 'Critical', value: '0', tone: 'success' },
+      { label: 'Mitigating', value: '1', tone: 'info' },
+      { label: 'Ownership queues', value: '1', tone: 'info' }
+    ]);
+    expect(result.incidents[0]).toMatchObject({
+      id: '42',
+      title: 'API latency incident detail',
+      severity: 'warning',
+      stage: 'Identified',
+      service: 'api-gateway',
+      owner: 'detail-responder',
+      openedAt: 'time:1712730000000',
+      blastRadius: '2 Components'
+    });
+    expect(result.timelineRows[0]).toMatchObject({
+      id: 'incident-timeline-42-8',
+      title: 'time:1712730300000 · Identified',
+      copy: 'Rollback started from detail endpoint',
+      meta: 'API latency incident detail',
+      tone: 'warning'
+    });
+    expect(result.ownershipRows[0]).toMatchObject({
+      id: 'incident-owner-42',
+      owner: 'detail-responder',
+      queue: 'Identified',
+      copy: 'API latency incident detail'
+    });
+  });
+
+  it('keeps the workbench empty state API-backed without falling back to fixture incidents', () => {
+    const result = buildIncidentWorkbenchData(
+      { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 },
+      t,
+      formatTime
+    );
+
+    expect(result.apiState).toBe('empty');
+    expect(result.detailState).toBe('not-requested');
+    expect(result.detailSource).toBe('none');
+    expect(result.selectedIncident).toBeNull();
+    expect(result.transitionState).toBe('disabled');
+    expect(result.incidents).toEqual([]);
+    expect(result.timelineRows).toEqual([]);
+    expect(result.ownershipRows).toEqual([]);
+    expect(result.metrics.map(metric => metric.value)).toEqual(['0', '0', '0', '0']);
+  });
+
+  it('does not request detail when the incident list is empty', async () => {
+    const apiGet = vi.fn().mockResolvedValueOnce({ content: [], totalElements: 0, pageIndex: 0, pageSize: 8 });
+
+    const result = await loadIncidentWorkbenchData(apiGet as any, t, formatTime);
+
+    expect(apiGet).toHaveBeenCalledTimes(1);
+    expect(apiGet).toHaveBeenCalledWith('/status/page/incident?pageIndex=0&pageSize=8');
+    expect(result).toMatchObject({
+      apiState: 'empty',
+      detailState: 'not-requested',
+      detailSource: 'none',
+      transitionState: 'disabled'
+    });
+  });
+
+  it('reads Angular incident search and server-side pagination params from the route query', () => {
+    expect(readIncidentWorkbenchQuery({
+      search: ' api latency ',
+      pageIndex: '2',
+      pageSize: '15'
+    })).toEqual({
+      search: ' api latency ',
+      pageIndex: 2,
+      pageSize: 15
+    });
+    expect(readIncidentWorkbenchQuery({
+      search: ['checkout', 'ignored'],
+      pageIndex: 'not-a-number',
+      pageSize: ''
+    })).toEqual({
+      search: 'checkout',
+      pageIndex: 0,
+      pageSize: 8
+    });
+  });
+
+  it('builds and sends a status transition payload through the existing status-page PUT contract', async () => {
+    const apiPut = vi.fn().mockResolvedValue(undefined);
+    const incident = {
+      id: 42,
+      name: 'API latency incident',
+      state: 1,
+      orgId: 3,
+      components: [{ id: 7, name: 'api-gateway' }],
+      contents: [{ id: 8, incidentId: 42, message: 'Identified bad deploy', state: 1, timestamp: 1000 }]
+    };
+
+    const payload = buildIncidentStatusTransitionPayload(
+      incident,
+      2,
+      'Monitoring after rollback',
+      2000
+    );
+
+    expect(payload).toMatchObject({
+      id: 42,
+      name: 'API latency incident',
+      state: 2,
+      orgId: 3,
+      components: [{ id: 7, name: 'api-gateway' }],
+      contents: [
+        { id: 8, incidentId: 42, message: 'Identified bad deploy', state: 1, timestamp: 1000 },
+        { incidentId: 42, message: 'Monitoring after rollback', state: 2, timestamp: 2000 }
+      ]
+    });
+
+    await transitionIncidentStatus(apiPut as any, incident, 3, 'Resolved from incidents workbench', 3000);
+
+    expect(apiPut).toHaveBeenCalledWith('/status/page/incident', {
+      ...incident,
+      state: 3,
+      contents: [
+        { id: 8, incidentId: 42, message: 'Identified bad deploy', state: 1, timestamp: 1000 },
+        { incidentId: 42, message: 'Resolved from incidents workbench', state: 3, timestamp: 3000 }
+      ]
+    });
+  });
+});
diff --git a/web-next/lib/incidents-surface/controller.ts b/web-next/lib/incidents-surface/controller.ts
new file mode 100644
index 0000000000..1ef910b027
--- /dev/null
+++ b/web-next/lib/incidents-surface/controller.ts
@@ -0,0 +1,304 @@
+import type { PageResult, StatusPageIncident } from '@/lib/types';
+import { buildStatusIncidentListUrl, type StatusIncidentListQuery } from '@/lib/setting-status/controller';
+import {
+  incidentStateLabel,
+  latestIncidentMessage,
+  normalizeIncidentState,
+  readStatusCopy,
+  statusComponentsLabel
+} from '@/lib/status-center/display';
+import { buildIncidentsSurfaceViewModel } from './view-model';
+
+type ApiGetter = <T>(url: string) => Promise<T>;
+type ApiPut = <T>(url: string, payload: unknown) => Promise<T>;
+type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
+type FormatTime = (value?: number | string | null) => string;
+type IncidentWorkbenchTone = 'critical' | 'warning' | 'success' | 'info' | 'neutral';
+export type IncidentTransitionState = 1 | 2 | 3;
+
+export const INCIDENT_WORKBENCH_DEFAULT_QUERY = {
+  search: '',
+  pageIndex: 0,
+  pageSize: 8
+} satisfies StatusIncidentListQuery;
+
+export type IncidentWorkbenchSearchParams = Record<string, string | string[] | undefined>;
+
+export type IncidentWorkbenchData = {
+  apiState: 'ready' | 'empty';
+  apiSource: 'status-page-incident-list';
+  detailState: 'ready' | 'not-requested';
+  detailSource: 'status-page-incident-detail' | 'none';
+  detailId?: string;
+  queryLabel: string;
+  title: string;
+  subtitle: string;
+  kicker: string;
+  metrics: Array<{ label: string; value: string; tone?: IncidentWorkbenchTone }>;
+  incidents: Array<{
+    id: string;
+    title: string;
+    severity: IncidentWorkbenchTone;
+    stage: string;
+    service: string;
+    owner: string;
+    openedAt: string;
+    blastRadius: string;
+  }>;
+  timelineRows: Array<{
+    id: string;
+    title: string;
+    copy: string;
+    meta: string;
+    tone?: IncidentWorkbenchTone;
+  }>;
+  ownershipRows: Array<{
+    id: string;
+    owner: string;
+    queue: string;
+    copy: string;
+    meta: string;
+    tone?: IncidentWorkbenchTone;
+  }>;
+  nextHops: Array<{ label: string; href: string; variant: 'subtle' | 'default' | 'primary' }>;
+  selectedIncidentId?: string;
+  selectedIncident?: StatusPageIncident | null;
+  transitionState: 'ready' | 'disabled';
+  totalElements: number;
+};
+
+function incidentComparableTime(value: unknown) {
+  if (typeof value === 'number' && Number.isFinite(value)) {
+    return value;
+  }
+  if (typeof value === 'string') {
+    const parsed = Date.parse(value);
+    return Number.isFinite(parsed) ? parsed : 0;
+  }
+  return 0;
+}
+
+function incidentTitle(incident: StatusPageIncident, t: Translator) {
+  const incidentId = incident.id ?? readStatusCopy(t, 'common.none', '-');
+  return incident.name || incident.title || `${readStatusCopy(t, 'status.incident.default-title', 'Incident')} ${incidentId}`;
+}
+
+function incidentTimestamp(incident: StatusPageIncident) {
+  return incident.updateTime || incident.createTime || incident.endTime || incident.startTime || incident.gmtUpdate || incident.gmtCreate || null;
+}
+
+function incidentService(incident: StatusPageIncident, t: Translator) {
+  const firstComponent = (incident.components || [])[0];
+  return firstComponent?.name || firstComponent?.endpoint || statusComponentsLabel(t);
+}
+
+function incidentOwner(incident: StatusPageIncident) {
+  return incident.modifier || incident.creator || 'status-page';
+}
+
+function incidentTone(value: unknown): IncidentWorkbenchTone {
+  switch (normalizeIncidentState(value)) {
+    case 0:
+      return 'critical';
+    case 1:
+      return 'warning';
+    case 2:
+      return 'info';
+    case 3:
+      return 'success';
+    default:
+      return 'neutral';
+  }
+}
+
+function incidentKey(incident: StatusPageIncident, index = 0) {
+  return String(incident.id || incident.name || `status-page-incident-${index + 1}`);
+}
+
+function selectedIncidentDetailUrl(incident: StatusPageIncident | undefined) {
+  return typeof incident?.id === 'number' && Number.isFinite(incident.id) ? `/status/page/incident/${incident.id}` : null;
+}
+
+function firstParam(value: string | string[] | undefined) {
+  return Array.isArray(value) ? value[0] : value;
+}
+
+function numericParam(value: string | string[] | undefined) {
+  const first = firstParam(value);
+  if (first == null || first.trim() === '') return undefined;
+  const parsed = Number(first);
+  return Number.isFinite(parsed) ? parsed : undefined;
+}
+
+export function readIncidentWorkbenchQuery(searchParams: IncidentWorkbenchSearchParams | undefined): StatusIncidentListQuery {
+  if (!searchParams) return INCIDENT_WORKBENCH_DEFAULT_QUERY;
+  return {
+    search: firstParam(searchParams.search) || '',
+    pageIndex: numericParam(searchParams.pageIndex) ?? INCIDENT_WORKBENCH_DEFAULT_QUERY.pageIndex,
+    pageSize: numericParam(searchParams.pageSize) ?? INCIDENT_WORKBENCH_DEFAULT_QUERY.pageSize
+  };
+}
+
+function mergeSelectedIncidentDetail(incidents: StatusPageIncident[], detail?: StatusPageIncident | null) {
+  if (!detail) {
+    return incidents;
+  }
+
+  const detailId = detail.id != null ? String(detail.id) : null;
+  const detailName = detail.name?.trim();
+  let replaced = false;
+  const merged = incidents.map(incident => {
+    const isMatch = (detailId && String(incident.id) === detailId) || (detailName && incident.name === detailName);
+    if (!isMatch) {
+      return incident;
+    }
+    replaced = true;
+    return {
+      ...incident,
+      ...detail,
+      components: detail.components ?? incident.components,
+      contents: detail.contents ?? incident.contents
+    };
+  });
+
+  return replaced ? merged : [detail, ...merged];
+}
+
+export function buildIncidentWorkbenchData(
+  page: PageResult<StatusPageIncident>,
+  t: Translator,
+  formatTime: FormatTime,
+  query: StatusIncidentListQuery = INCIDENT_WORKBENCH_DEFAULT_QUERY,
+  selectedDetail?: StatusPageIncident | null
+): IncidentWorkbenchData {
+  const surface = buildIncidentsSurfaceViewModel(t);
+  const listIncidents = page.content || [];
+  const incidents = mergeSelectedIncidentDetail(listIncidents, selectedDetail);
+  const statusComponentLabel = statusComponentsLabel(t);
+  const totalElements = page.totalElements || listIncidents.length;
+
+  const mappedIncidents = incidents.map((incident, index) => {
+    const state = incident.state ?? incident.status ?? null;
+    const components = incident.components || [];
+    return {
+      id: incidentKey(incident, index),
+      title: incidentTitle(incident, t),
+      severity: incidentTone(state),
+      stage: incidentStateLabel(state, t),
+      service: incidentService(incident, t),
+      owner: incidentOwner(incident),
+      openedAt: formatTime(incident.startTime || incident.createTime || incident.gmtCreate || null),
+      blastRadius: `${components.length} ${statusComponentLabel}`
+    };
+  });
+
+  const criticalCount = mappedIncidents.filter(incident => incident.severity === 'critical').length;
+  const activeCount = incidents.filter(incident => normalizeIncidentState(incident.state ?? incident.status ?? null) !== 3).length;
+  const ownerCount = new Set(mappedIncidents.map(incident => incident.owner)).size;
+
+  const timelineRows = incidents
+    .flatMap(incident =>
+      (incident.contents || []).map((content, contentIndex) => ({
+        id: `incident-timeline-${incident.id || incident.name || 'item'}-${content.id || contentIndex + 1}`,
+        title: `${formatTime(content.timestamp || incidentTimestamp(incident))} · ${incidentStateLabel(content.state ?? incident.state ?? incident.status ?? null, t)}`,
+        copy: content.message?.trim() || latestIncidentMessage(incident) || incidentTitle(incident, t),
+        meta: incidentTitle(incident, t),
+        tone: incidentTone(content.state ?? incident.state ?? incident.status ?? null),
+        sortTime: incidentComparableTime(content.timestamp || incidentTimestamp(incident))
+      }))
+    )
+    .sort((left, right) => right.sortTime - left.sortTime)
+    .slice(0, 4)
+    .map(({ sortTime: _sortTime, ...item }) => item);
+
+  const fallbackTimelineRows = mappedIncidents.slice(0, 4).map(incident => ({
+    id: `incident-timeline-${incident.id}`,
+    title: `${incident.openedAt} · ${incident.stage}`,
+    copy: latestIncidentMessage(incidents.find(item => String(item.id || item.name) === incident.id) || {}) || String(incident.title),
+    meta: `${incident.service} · ${incident.owner}`,
+    tone: incident.severity
+  }));
+
+  const ownershipRows = mappedIncidents.slice(0, 4).map(incident => ({
+    id: `incident-owner-${incident.id}`,
+    owner: incident.owner,
+    queue: incident.stage,
+    copy: String(incident.title),
+    meta: `${incident.blastRadius} · ${incident.openedAt}`,
+    tone: incident.severity === 'critical' ? 'critical' : incident.severity === 'success' ? 'success' : 'info'
+  }));
+
+  return {
+    apiState: listIncidents.length > 0 ? 'ready' : 'empty',
+    apiSource: 'status-page-incident-list',
+    detailState: selectedDetail ? 'ready' : 'not-requested',
+    detailSource: selectedDetail ? 'status-page-incident-detail' : 'none',
+    detailId: selectedDetail ? incidentKey(selectedDetail) : undefined,
+    queryLabel: buildStatusIncidentListUrl(query),
+    title: surface.title,
+    subtitle: surface.subtitle,
+    kicker: surface.kicker,
+    metrics: [
+      { label: t('incidents.metric.open'), value: String(totalElements), tone: totalElements > 0 ? 'warning' : 'success' },
+      { label: t('incidents.metric.critical'), value: String(criticalCount), tone: criticalCount > 0 ? 'critical' : 'success' },
+      { label: t('incidents.metric.mitigating'), value: String(activeCount), tone: activeCount > 0 ? 'info' : 'success' },
+      { label: t('incidents.metric.ownership-queues'), value: String(ownerCount), tone: ownerCount > 0 ? 'info' : 'neutral' }
+    ],
+    incidents: mappedIncidents,
+    timelineRows: timelineRows.length > 0 ? timelineRows : fallbackTimelineRows,
+    ownershipRows,
+    nextHops: surface.nextHops,
+    selectedIncidentId: mappedIncidents[0]?.id,
+    selectedIncident: selectedDetail || incidents[0] || null,
+    transitionState: selectedDetail || incidents[0] ? 'ready' : 'disabled',
+    totalElements
+  };
+}
+
+export async function loadIncidentWorkbenchData(
+  apiGet: ApiGetter,
+  t: Translator,
+  formatTime: FormatTime,
+  query: StatusIncidentListQuery = INCIDENT_WORKBENCH_DEFAULT_QUERY
+) {
+  const incidents = await apiGet<PageResult<StatusPageIncident>>(buildStatusIncidentListUrl(query));
+  const detailUrl = selectedIncidentDetailUrl((incidents.content || [])[0]);
+  const selectedDetail = detailUrl ? await apiGet<StatusPageIncident>(detailUrl) : null;
+  return buildIncidentWorkbenchData(incidents, t, formatTime, query, selectedDetail);
+}
+
+export function buildIncidentStatusTransitionPayload(
+  incident: StatusPageIncident,
+  nextState: IncidentTransitionState,
+  message: string,
+  timestamp = Date.now()
+): StatusPageIncident {
+  if (incident.id == null) {
+    throw new Error('Incident id is required for status transition');
+  }
+
+  return {
+    ...incident,
+    state: nextState,
+    contents: [
+      ...(incident.contents || []),
+      {
+        incidentId: incident.id,
+        message,
+        state: nextState,
+        timestamp
+      }
+    ]
+  };
+}
+
+export async function transitionIncidentStatus(
+  apiPut: ApiPut,
+  incident: StatusPageIncident,
+  nextState: IncidentTransitionState,
+  message: string,
+  timestamp = Date.now()
+) {
+  const payload = buildIncidentStatusTransitionPayload(incident, nextState, message, timestamp);
+  return apiPut<void>('/status/page/incident', payload);
+}
diff --git a/web-next/lib/incidents-surface/model.test.ts b/web-next/lib/incidents-surface/model.test.ts
index cded761ccd..841f88b879 100644
--- a/web-next/lib/incidents-surface/model.test.ts
+++ b/web-next/lib/incidents-surface/model.test.ts
@@ -1,37 +1,41 @@
 import { describe, expect, it } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 import { buildIncidentsDomainModel } from './model';
 
-const t = (key: string) => {
-  const messages: Record<string, string> = {
-    'incidents.subtitle': 'Response timeline, ownership, and next steps stay grouped in a single operator context.',
-    'incidents.focus': 'Incident posture and response handoff',
-    'incidents.summary': 'The incidents entry page keeps response state, owners, and related evidence aligned while the deeper domain is still landing.',
-    'incidents.checklist.shell.title': 'Shell ready',
-    'incidents.checklist.shell.copy': 'The incidents route already shares the workbench shell and routing posture.',
-    'incidents.checklist.adapter.title': 'Adapters next',
-    'incidents.checklist.adapter.copy': 'Incident list, owner state, and response timeline can plug into the current API boundary.',
-    'incidents.checklist.drilldown.title': 'Drilldown reserved',
-    'incidents.checklist.drilldown.copy': 'Log, trace, and entity handoff stay on the planned expansion path.',
-    'menu.dashboard.back': 'Open overview',
-    'menu.log.manage': 'Log manage',
-    'menu.trace.manage': 'Trace manage'
-  };
-  return messages[key] ?? key;
-};
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('incidents domain model', () => {
   it('builds an incident-centered workspace snapshot', () => {
     const model = buildIncidentsDomainModel(t);
 
-    expect(model.title).toBe('Incidents');
+    expect(model.title).toBe('故障事件');
+    expect(model.tags).toEqual(['事件入口外壳', '响应时间线', '责任人优先']);
     expect(model.incidents).toHaveLength(3);
     expect(model.incidents[0]?.severity).toBe('critical');
+    expect(model.incidents[0]).toMatchObject({
+      title: 'checkout 在 prod-ap 出现延迟突增',
+      service: 'checkout',
+      owner: 'checkout-oncall',
+      blastRadius: '2 个区域'
+    });
     expect(model.timeline).toHaveLength(3);
+    expect(model.timeline[0]).toEqual({
+      title: '09:08 开始缓解',
+      copy: '回滚和重启动作已从告警证据包打开。',
+      meta: 'checkout 延迟事件'
+    });
+    expect(model.ownership[0]).toEqual({
+      owner: 'checkout-oncall',
+      queue: '主响应人',
+      copy: '保持告警、链路和发布上下文绑定到当前缓解线程。',
+      meta: '2 个活跃交接'
+    });
     expect(model.metrics).toEqual([
-      { label: 'Open incidents', value: '3' },
-      { label: 'Critical', value: '1' },
-      { label: 'Mitigating', value: '1' },
-      { label: 'Ownership queues', value: '3' }
+      { label: '打开事件', value: '3' },
+      { label: '严重事件', value: '1' },
+      { label: '缓解中', value: '1' },
+      { label: '责任队列', value: '3' }
     ]);
+    expect(model.checklist.map(item => item.meta)).toEqual(['已完成', '下一步', '预留']);
   });
 });
diff --git a/web-next/lib/incidents-surface/model.ts b/web-next/lib/incidents-surface/model.ts
index 1ed651d949..04618e336b 100644
--- a/web-next/lib/incidents-surface/model.ts
+++ b/web-next/lib/incidents-surface/model.ts
@@ -45,90 +45,94 @@ export function buildIncidentsDomainModel(t: Translator): IncidentsDomainModel {
   const incidents: IncidentItem[] = [
     {
       id: 'inc-204',
-      title: 'Checkout latency spike across prod-ap',
+      title: t('incidents.row.checkout.title'),
       severity: 'critical',
-      service: 'checkout',
-      owner: 'checkout-oncall',
+      service: t('incidents.row.checkout.service'),
+      owner: t('incidents.row.checkout.owner'),
       stage: 'mitigating',
       openedAt: '2026-04-16 09:06',
-      blastRadius: '2 regions'
+      blastRadius: t('incidents.row.checkout.blast-radius')
     },
     {
       id: 'inc-203',
-      title: 'Collector queue delay on metrics ingestion',
+      title: t('incidents.row.collector.title'),
       severity: 'warning',
-      service: 'otlp-gateway',
-      owner: 'observability',
+      service: t('incidents.row.collector.service'),
+      owner: t('incidents.row.collector.owner'),
       stage: 'monitoring',
       openedAt: '2026-04-16 08:22',
-      blastRadius: 'metrics only'
+      blastRadius: t('incidents.row.collector.blast-radius')
     },
     {
       id: 'inc-202',
-      title: 'Edge proxy saturation after deploy',
+      title: t('incidents.row.edge.title'),
       severity: 'warning',
-      service: 'edge-proxy',
-      owner: 'edge-sre',
+      service: t('incidents.row.edge.service'),
+      owner: t('incidents.row.edge.owner'),
       stage: 'investigating',
       openedAt: '2026-04-16 07:48',
-      blastRadius: 'cn-hz ingress'
+      blastRadius: t('incidents.row.edge.blast-radius')
     }
   ];
 
   return {
-    title: 'Incidents',
+    title: t('incidents.entry.title'),
     subtitle: t('incidents.subtitle'),
     focus: t('incidents.focus'),
     summary: t('incidents.summary'),
-    tags: ['incident shell', 'response timeline', 'owner-first'],
+    tags: [
+      t('incidents.tag.shell'),
+      t('incidents.tag.timeline'),
+      t('incidents.tag.owner-first')
+    ],
     metrics: [
-      { label: 'Open incidents', value: String(incidents.length) },
-      { label: 'Critical', value: String(incidents.filter(item => item.severity === 'critical').length) },
-      { label: 'Mitigating', value: String(incidents.filter(item => item.stage === 'mitigating').length) },
-      { label: 'Ownership queues', value: '3' }
+      { label: t('incidents.metric.open'), value: String(incidents.length) },
+      { label: t('incidents.metric.critical'), value: String(incidents.filter(item => item.severity === 'critical').length) },
+      { label: t('incidents.metric.mitigating'), value: String(incidents.filter(item => item.stage === 'mitigating').length) },
+      { label: t('incidents.metric.ownership-queues'), value: '3' }
     ],
     incidents,
     timeline: [
       {
-        title: '09:08 mitigation started',
-        copy: 'Rollback and restart actions were opened from the alert evidence bundle.',
-        meta: 'checkout latency incident'
+        title: t('incidents.timeline.checkout.title'),
+        copy: t('incidents.timeline.checkout.copy'),
+        meta: t('incidents.timeline.checkout.meta')
       },
       {
-        title: '08:33 metrics replay approved',
-        copy: 'Collector backlog dropped below the replay threshold and handoff stayed in the same rail.',
-        meta: 'ingestion incident'
+        title: t('incidents.timeline.collector.title'),
+        copy: t('incidents.timeline.collector.copy'),
+        meta: t('incidents.timeline.collector.meta')
       },
       {
-        title: '07:56 edge saturation escalated',
-        copy: 'Operator routed the incident from monitor detail to trace and log evidence.',
-        meta: 'edge incident'
+        title: t('incidents.timeline.edge.title'),
+        copy: t('incidents.timeline.edge.copy'),
+        meta: t('incidents.timeline.edge.meta')
       }
     ],
     ownership: [
       {
-        owner: 'checkout-oncall',
-        queue: 'primary responder',
-        copy: 'Keeps alert, trace, and deploy context bound to the active mitigation thread.',
-        meta: '2 active handoffs'
+        owner: t('incidents.ownership.checkout.owner'),
+        queue: t('incidents.ownership.checkout.queue'),
+        copy: t('incidents.ownership.checkout.copy'),
+        meta: t('incidents.ownership.checkout.meta')
       },
       {
-        owner: 'observability',
-        queue: 'signal validation',
-        copy: 'Validates replay safety and ingestion recovery before closing the incident.',
-        meta: '1 active handoff'
+        owner: t('incidents.ownership.observability.owner'),
+        queue: t('incidents.ownership.observability.queue'),
+        copy: t('incidents.ownership.observability.copy'),
+        meta: t('incidents.ownership.observability.meta')
       },
       {
-        owner: 'edge-sre',
-        queue: 'capacity review',
-        copy: 'Tracks post-deploy regressions and the next rollback checkpoint.',
-        meta: 'awaiting rollback note'
+        owner: t('incidents.ownership.edge.owner'),
+        queue: t('incidents.ownership.edge.queue'),
+        copy: t('incidents.ownership.edge.copy'),
+        meta: t('incidents.ownership.edge.meta')
       }
     ],
     checklist: [
-      { title: t('incidents.checklist.shell.title'), copy: t('incidents.checklist.shell.copy'), meta: 'done' },
-      { title: t('incidents.checklist.adapter.title'), copy: t('incidents.checklist.adapter.copy'), meta: 'next' },
-      { title: t('incidents.checklist.drilldown.title'), copy: t('incidents.checklist.drilldown.copy'), meta: 'reserved' }
+      { title: t('incidents.checklist.shell.title'), copy: t('incidents.checklist.shell.copy'), meta: t('incidents.checklist.shell.meta') },
+      { title: t('incidents.checklist.adapter.title'), copy: t('incidents.checklist.adapter.copy'), meta: t('incidents.checklist.adapter.meta') },
+      { title: t('incidents.checklist.drilldown.title'), copy: t('incidents.checklist.drilldown.copy'), meta: t('incidents.checklist.drilldown.meta') }
     ],
     nextHops: [
       { label: t('menu.dashboard.back'), href: '/overview', variant: 'subtle' },
diff --git a/web-next/lib/incidents-surface/view-model.test.ts b/web-next/lib/incidents-surface/view-model.test.ts
index d075f5b0ca..79eb051d7d 100644
--- a/web-next/lib/incidents-surface/view-model.test.ts
+++ b/web-next/lib/incidents-surface/view-model.test.ts
@@ -1,32 +1,12 @@
 import { describe, expect, it } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 import { buildIncidentsPlaceholderState, buildIncidentsSurfaceViewModel } from './view-model';
 
-const t = (key: string) => {
-  const messages: Record<string, string> = {
-    'incidents.subtitle': 'Response timeline, ownership, and next steps stay grouped in a single operator context.',
-    'incidents.focus': 'Incident posture and response handoff',
-    'incidents.summary': 'The incidents entry page keeps response state, owners, and related evidence aligned while the deeper domain is still landing.',
-    'incidents.checklist.shell.title': 'Shell ready',
-    'incidents.checklist.shell.copy': 'The incidents route already shares the workbench shell and routing posture.',
-    'incidents.checklist.adapter.title': 'Adapters next',
-    'incidents.checklist.adapter.copy': 'Incident list, owner state, and response timeline can plug into the current API boundary.',
-    'incidents.checklist.drilldown.title': 'Drilldown reserved',
-    'incidents.checklist.drilldown.copy': 'Log, trace, and entity handoff stay on the planned expansion path.',
-    'common.workspace': 'Workspace',
-    'common.focus': 'Focus',
-    'common.mode': 'Mode',
-    'common.signals': 'Signals',
-    'ops.surface.mode.entry': 'entry surface',
-    'menu.dashboard.back': 'Open overview',
-    'menu.log.manage': 'Log manage',
-    'menu.trace.manage': 'Trace manage'
-  };
-  return messages[key] ?? key;
-};
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('incidents surface view model', () => {
   it('describes the OTLP cold-matte placeholder shell', () => {
-    const state = buildIncidentsPlaceholderState();
+    const state = buildIncidentsPlaceholderState(t);
 
     expect(state).toMatchObject({
       kicker: '事件入口',
@@ -54,13 +34,32 @@ describe('incidents surface view model', () => {
   it('derives incident cards, response timeline, and ownership lanes', () => {
     const viewModel = buildIncidentsSurfaceViewModel(t);
 
-    expect(viewModel.kicker).toBe('Incident response desk');
+    expect(viewModel.kicker).toBe('事件响应台');
+    expect(viewModel.tags).toEqual(['事件入口外壳', '响应时间线', '责任人优先']);
+    expect(viewModel.facts.find(fact => fact.label === '信号')?.value).toBe('3');
+    expect(viewModel.metrics).toEqual([
+      { label: '打开事件', value: '3' },
+      { label: '严重事件', value: '1' },
+      { label: '缓解中', value: '1' },
+      { label: '责任队列', value: '3' }
+    ]);
     expect(viewModel.incidentCards).toHaveLength(3);
     expect(viewModel.incidentCards[0]).toMatchObject({
-      title: 'Checkout latency spike across prod-ap'
+      title: 'checkout 在 prod-ap 出现延迟突增',
+      copy: 'checkout · checkout-oncall',
+      eyebrow: 'critical · 2 个区域'
+    });
+    expect(viewModel.timelineRows[0]).toMatchObject({
+      title: '09:08 开始缓解',
+      copy: '回滚和重启动作已从告警证据包打开。',
+      meta: 'checkout 延迟事件'
+    });
+    expect(viewModel.ownershipRows[0]).toMatchObject({
+      title: 'checkout-oncall · 主响应人',
+      copy: '保持告警、链路和发布上下文绑定到当前缓解线程。',
+      meta: '2 个活跃交接'
     });
-    expect(viewModel.timelineRows[0]?.meta).toContain('checkout latency incident');
-    expect(viewModel.ownershipRows[0]?.title).toContain('checkout-oncall');
+    expect(viewModel.checklist.map(item => item.meta)).toEqual(['已完成', '下一步', '预留']);
     expect(viewModel.handoffRows).toHaveLength(3);
   });
 });
diff --git a/web-next/lib/incidents-surface/view-model.ts b/web-next/lib/incidents-surface/view-model.ts
index 2d618f0b60..e755816f33 100644
--- a/web-next/lib/incidents-surface/view-model.ts
+++ b/web-next/lib/incidents-surface/view-model.ts
@@ -21,42 +21,45 @@ export type IncidentsPlaceholderState = {
   };
 };
 
-export function buildIncidentsPlaceholderState(): IncidentsPlaceholderState {
+export function buildIncidentsPlaceholderState(t: Translator): IncidentsPlaceholderState {
   return {
-    kicker: '事件入口',
-    title: '故障事件',
-    subtitle: '按 OTLP 工作台的冷色基线统一响应时间线、责任人和证据入口。',
+    kicker: t('incidents.entry.kicker'),
+    title: t('incidents.entry.title'),
+    subtitle: t('incidents.entry.subtitle'),
     actions: [
-      { label: '打开概览', href: '/overview', variant: 'primary' },
-      { label: '查看对象', href: '/entities', variant: 'subtle' }
+      { label: t('incidents.entry.action.overview'), href: '/overview', variant: 'primary' },
+      { label: t('incidents.entry.action.entities'), href: '/entities', variant: 'subtle' }
     ],
     shell: {
-      eyebrow: '冷色入口已接入',
-      copy:
-        '此入口继承 OTLP 接入页的近黑画布、小按钮、硬边面板和静态右栏节奏,后续只需要接入事件适配器。',
-      chips: ['事件入口', '响应时间线', '责任人优先']
+      eyebrow: t('incidents.entry.shell.eyebrow'),
+      copy: t('incidents.entry.shell.copy'),
+      chips: [
+        t('incidents.entry.chip.entry'),
+        t('incidents.entry.chip.timeline'),
+        t('incidents.entry.chip.owner-first')
+      ]
     },
-    checklistTitle: '接入清单',
+    checklistTitle: t('incidents.entry.checklist.title'),
     checklist: [
       {
-        title: '统一入口上下文',
-        copy: '已和 OTLP 基线共用同一套页面节奏。',
+        title: t('incidents.entry.checklist.context.title'),
+        copy: t('incidents.entry.checklist.context.copy'),
         tone: 'bg-[#75ad86]'
       },
       {
-        title: '接入事件适配器',
-        copy: '等事件列表、责任人和时间线数据落地后直接挂载。',
+        title: t('incidents.entry.checklist.adapter.title'),
+        copy: t('incidents.entry.checklist.adapter.copy'),
         tone: 'bg-[#c2a86b]'
       },
       {
-        title: '保留证据跳转',
-        copy: '日志、链路和对象入口保持在同一响应上下文。',
+        title: t('incidents.entry.checklist.evidence.title'),
+        copy: t('incidents.entry.checklist.evidence.copy'),
         tone: 'bg-[#9aa9cf]'
       }
     ],
     empty: {
-      title: '等待接入事件适配器',
-      copy: '视觉骨架已经统一到 OTLP 基线,后续数据接入可以增量完成。'
+      title: t('incidents.entry.empty.title'),
+      copy: t('incidents.entry.empty.copy')
     }
   };
 }
@@ -76,7 +79,7 @@ export function buildIncidentsSurfaceViewModel(t: Translator) {
 
   return {
     ...model,
-    kicker: 'Incident response desk',
+    kicker: t('incidents.surface.kicker'),
     facts: buildOpsFacts(model.title, model.focus, model.tags, t),
     incidentCards: model.incidents.map(item => ({
       id: item.id,
@@ -93,9 +96,21 @@ export function buildIncidentsSurfaceViewModel(t: Translator) {
       meta: item.meta
     })),
     handoffRows: [
-      { title: 'Log evidence', copy: 'Jump into logs when mitigation needs fresh payload evidence.', meta: 'logs' },
-      { title: 'Trace evidence', copy: 'Preserve trace-first drilldown for latency and dependency regressions.', meta: 'traces' },
-      { title: 'Overview return path', copy: 'Keep incident status visible in the same global response rhythm.', meta: 'overview' }
+      {
+        title: t('incidents.surface.handoff.logs.title'),
+        copy: t('incidents.surface.handoff.logs.copy'),
+        meta: 'logs'
+      },
+      {
+        title: t('incidents.surface.handoff.traces.title'),
+        copy: t('incidents.surface.handoff.traces.copy'),
+        meta: 'traces'
+      },
+      {
+        title: t('incidents.surface.handoff.overview.title'),
+        copy: t('incidents.surface.handoff.overview.copy'),
+        meta: 'overview'
+      }
     ]
   };
 }
diff --git a/web-next/lib/ops-surface/view-model.test.ts b/web-next/lib/ops-surface/view-model.test.ts
index 086062ded9..786c8d2c7e 100644
--- a/web-next/lib/ops-surface/view-model.test.ts
+++ b/web-next/lib/ops-surface/view-model.test.ts
@@ -8,16 +8,16 @@ describe('ops surface view model', () => {
   it('builds ops facts', () => {
     expect(buildOpsFacts('Actions', '入口工作台', ['workflow', 'queue'], t)).toEqual([
       { label: '工作区', value: 'actions' },
-      { label: 'Focus', value: '入口工作台' },
-      { label: 'Mode', value: 'entry surface' },
-      { label: 'Signals', value: '2' }
+      { label: '焦点', value: '入口工作台' },
+      { label: '模式', value: '入口面' },
+      { label: '信号', value: '2' }
     ]);
   });
 
   it('builds shared status rows', () => {
     expect(buildOpsStatusRows(t)).toEqual([
-      { title: 'Shared shell', copy: '入口页沿用同一套壳层和信息层级。' },
-      { title: 'Focused scope', copy: '只承接导航、上下文和下一跳。' }
+      { title: '共享壳层', copy: '入口页沿用同一套壳层和信息层级。' },
+      { title: '聚焦范围', copy: '只承接导航、上下文和下一跳。' }
     ]);
   });
 });
diff --git a/web-next/lib/ops-surface/view-model.ts b/web-next/lib/ops-surface/view-model.ts
index 1129110b91..b01c522669 100644
--- a/web-next/lib/ops-surface/view-model.ts
+++ b/web-next/lib/ops-surface/view-model.ts
@@ -3,9 +3,9 @@ type Translator = (key: string, params?: Record<string, string | number | null |
 export function buildOpsFacts(title: string, focus: string, tags: string[], t: Translator) {
   return [
     { label: t('common.workspace'), value: title.toLowerCase() },
-    { label: t('common.focus'), value: focus },
-    { label: t('common.mode'), value: t('ops.surface.mode.entry') },
-    { label: t('common.signals'), value: String(tags.length) }
+    { label: t('ops.surface.fact.focus-label'), value: focus },
+    { label: t('ops.surface.fact.mode-label'), value: t('ops.surface.fact.mode-entry') },
+    { label: t('ops.surface.fact.signals-label'), value: String(tags.length) }
   ];
 }
 
diff --git a/web-next/lib/status-center/controller.test.ts b/web-next/lib/status-center/controller.test.ts
index 75eb9cdd79..ccfb0c656b 100644
--- a/web-next/lib/status-center/controller.test.ts
+++ b/web-next/lib/status-center/controller.test.ts
@@ -1,13 +1,28 @@
 import { describe, expect, it, vi } from 'vitest';
 import {
+  buildPublicStatusCompatRouteUrl,
   buildStatusIncidentListUrl,
   buildStatusIncidentYearQuery,
+  describeStatusIncidentLoadFailure,
   loadStatusPageData,
   loadStatusPageIncidentFeed,
   loadStatusPageIncidents
 } from './controller';
 
 describe('status center controller', () => {
+  it('builds the public status compatibility alias URL while preserving status filters', () => {
+    expect(buildPublicStatusCompatRouteUrl()).toBe('/status');
+    expect(
+      buildPublicStatusCompatRouteUrl({
+        component: 'api',
+        status: 'down',
+        year: '2026',
+        returnTo: '/overview',
+        returnLabel: 'Overview'
+      })
+    ).toBe('/status?component=api&status=down&year=2026&returnTo=%2Foverview');
+  });
+
   it('builds public incident urls with search and range params', () => {
     expect(
       buildStatusIncidentListUrl({
@@ -209,4 +224,24 @@ describe('status center controller', () => {
       incidentsError: 'incident feed unavailable'
     });
   });
+
+  it('uses runtime fallback copy for blank incident load failures', async () => {
+    const apiGet = vi
+      .fn()
+      .mockResolvedValueOnce({ name: 'OpenStatus' })
+      .mockResolvedValueOnce([{ info: { id: 1, name: 'api', state: 0 } }])
+      .mockRejectedValueOnce(new Error('   '));
+
+    const result = await loadStatusPageData(apiGet as any);
+
+    expect(result.incidents).toEqual([]);
+    expect(result.incidentsError).toBe('Failed to load public incidents');
+  });
+
+  it('shares public incident load-failure fallback for refresh failures', () => {
+    expect(describeStatusIncidentLoadFailure(new Error('backend incident error'))).toBe('backend incident error');
+    expect(describeStatusIncidentLoadFailure(new Error('   '))).toBe('Failed to load public incidents');
+    expect(describeStatusIncidentLoadFailure('')).toBe('Failed to load public incidents');
+    expect(describeStatusIncidentLoadFailure(new Error('   '), 'Localized refresh failure')).toBe('Localized refresh failure');
+  });
 });
diff --git a/web-next/lib/status-center/controller.ts b/web-next/lib/status-center/controller.ts
index 84be0994ee..1d80fc6130 100644
--- a/web-next/lib/status-center/controller.ts
+++ b/web-next/lib/status-center/controller.ts
@@ -1,7 +1,17 @@
 import type { StatusPageComponent, StatusPageHistory, StatusPageIncident, StatusPageOrg } from '@/lib/types';
+import { buildCompatRedirectTarget, type SearchParamsRecord } from '../compat/search-params';
+import { SUPPLEMENTAL_MESSAGES } from '../i18n-runtime-messages';
+
+export type { SearchParamsRecord } from '../compat/search-params';
 
 type ApiGetter = <T>(url: string) => Promise<T>;
 
+export const PUBLIC_STATUS_ROUTE = '/status';
+
+export function buildPublicStatusCompatRouteUrl(searchParams?: SearchParamsRecord) {
+  return buildCompatRedirectTarget(PUBLIC_STATUS_ROUTE, searchParams);
+}
+
 export type StatusIncidentListQuery = {
   search?: string;
   startTime?: number;
@@ -31,6 +41,7 @@ export type StatusIncidentFeedOptions = {
 };
 
 const STATUS_PAGE_ORG_NOT_FOUND = 'Status Page Organization Not Found';
+const STATUS_INCIDENTS_LOAD_FAILED_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.incidents.load-failed'] ?? 'status.public.incidents.load-failed';
 
 function isStatusPageOrgNotFound(error: unknown) {
   return error instanceof Error && error.message.includes(STATUS_PAGE_ORG_NOT_FOUND);
@@ -50,6 +61,10 @@ function describeLoadFailure(error: unknown, fallback: string) {
   return fallback;
 }
 
+export function describeStatusIncidentLoadFailure(error: unknown, fallback = STATUS_INCIDENTS_LOAD_FAILED_FALLBACK) {
+  return describeLoadFailure(error, fallback || STATUS_INCIDENTS_LOAD_FAILED_FALLBACK);
+}
+
 function normalizePageIndex(value?: number) {
   return Number.isFinite(value) && value != null && value >= 0 ? Math.trunc(value) : 0;
 }
@@ -215,7 +230,7 @@ export async function loadStatusPageData(apiGet: ApiGetter, query: StatusInciden
   try {
     incidents = await loadStatusPageIncidents(apiGet, incidentQuery);
   } catch (error) {
-    incidentsError = describeLoadFailure(error, 'Failed to load public incidents');
+    incidentsError = describeStatusIncidentLoadFailure(error);
   }
 
   return {
diff --git a/web-next/lib/status-center/display.ts b/web-next/lib/status-center/display.ts
index c28b7b275b..5a5dd8d856 100644
--- a/web-next/lib/status-center/display.ts
+++ b/web-next/lib/status-center/display.ts
@@ -1,8 +1,52 @@
 import type { StatusPageIncident } from '@/lib/types';
 import type { StatusPageHistory } from '@/lib/types';
+import { SUPPLEMENTAL_MESSAGES } from '@/lib/i18n-runtime-messages';
 
 export type StatusCopyTranslator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
 
+const STATUS_PUBLIC_HOME_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.home'] ?? 'status.public.home';
+const STATUS_PUBLIC_FEEDBACK_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.feedback'] ?? 'status.public.feedback';
+const STATUS_PUBLIC_YEAR_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.year'] ?? 'status.public.year';
+const STATUS_PUBLIC_HISTORY_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.history'] ?? 'status.public.history';
+const STATUS_PUBLIC_UPTIME_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.uptime'] ?? 'status.public.uptime';
+const STATUS_PUBLIC_UPDATED_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.updated'] ?? 'status.public.updated';
+const STATUS_PUBLIC_COMPONENT_HISTORY_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.component-history'] ?? 'status.public.component-history';
+const STATUS_PUBLIC_INCIDENT_RANGE_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.incident-range'] ?? 'status.public.incident-range';
+const STATUS_PUBLIC_TODAY_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.today'] ?? 'status.public.today';
+const STATUS_PUBLIC_THIRTY_DAY_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.30-day'] ?? 'status.public.30-day';
+const STATUS_PUBLIC_POWERED_BY_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.power-by'] ?? 'status.public.power-by';
+const STATUS_PUBLIC_TO_INCIDENT_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.to-incident'] ?? 'status.public.to-incident';
+const STATUS_PUBLIC_TO_COMPONENT_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.to-component'] ?? 'status.public.to-component';
+const STATUS_INCIDENT_PUBLIC_START_AT_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.incident.public.start-at'] ?? 'status.incident.public.start-at';
+const STATUS_INCIDENT_PUBLIC_UPDATE_AT_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.incident.public.update-at'] ?? 'status.incident.public.update-at';
+const STATUS_PAGE_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.page'] ?? 'status.page';
+const STATUS_SETTING_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['setting.status.title'] ?? 'setting.status.title';
+const STATUS_SETTING_COMPONENTS_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['setting.status.components.title'] ?? 'setting.status.components.title';
+const STATUS_COMPONENTS_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.components.title'] ?? 'status.components.title';
+const STATUS_SETTING_INCIDENTS_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['setting.status.incidents.title'] ?? 'setting.status.incidents.title';
+const STATUS_INCIDENTS_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.incidents.title'] ?? 'status.incidents.title';
+const STATUS_SETTING_ORGANIZATION_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['setting.status.org.title'] ?? 'setting.status.org.title';
+const STATUS_ORGANIZATION_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.org.title'] ?? 'status.org.title';
+const STATUS_SETTING_ORGANIZATION_STATE_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['setting.status.org.state'] ?? 'setting.status.org.state';
+const COMMON_CURRENT_VIEW_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['common.current-view'] ?? 'common.current-view';
+const COMMON_STATUS_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['common.status'] ?? 'common.status';
+const STATUS_FACT_COMPONENTS_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.fact.components'] ?? 'status.fact.components';
+const STATUS_FACT_INCIDENTS_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.fact.incidents'] ?? 'status.fact.incidents';
+const STATUS_SETTING_ORGANIZATION_COPY_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['setting.status.org.copy'] ?? 'setting.status.org.copy';
+const STATUS_ORGANIZATION_COPY_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['status.org.copy'] ?? 'status.org.copy';
+const STATUS_SETTING_ORGANIZATION_NAME_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['setting.status.org.fallback'] ?? 'setting.status.org.fallback';
+const STATUS_ORGANIZATION_NAME_FALLBACK = SUPPLEMENTAL_MESSAGES['en-US']?.['status.org.empty'] ?? 'status.org.empty';
+const STATUS_COMPONENT_STATE_0_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.component.state.0'] ?? 'status.component.state.0';
+const STATUS_COMPONENT_STATE_1_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.component.state.1'] ?? 'status.component.state.1';
+const STATUS_COMPONENT_STATE_2_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.component.state.2'] ?? 'status.component.state.2';
+const STATUS_INCIDENT_STATE_0_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.incident.state.0'] ?? 'status.incident.state.0';
+const STATUS_INCIDENT_STATE_1_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.incident.state.1'] ?? 'status.incident.state.1';
+const STATUS_INCIDENT_STATE_2_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.incident.state.2'] ?? 'status.incident.state.2';
+const STATUS_INCIDENT_STATE_3_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.incident.state.3'] ?? 'status.incident.state.3';
+const STATUS_PUBLIC_ORG_STATE_0_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.org.state.0'] ?? 'status.public.org.state.0';
+const STATUS_PUBLIC_ORG_STATE_1_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.org.state.1'] ?? 'status.public.org.state.1';
+const STATUS_PUBLIC_ORG_STATE_2_FALLBACK_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['status.public.org.state.2'] ?? 'status.public.org.state.2';
+
 function readNumericToken(value: unknown) {
   if (typeof value === 'number' && Number.isFinite(value)) {
     return value;
@@ -120,11 +164,11 @@ function normalizeOrgState(value: unknown) {
 export function componentStateLabel(value: unknown, t: StatusCopyTranslator) {
   switch (normalizeComponentState(value)) {
     case 0:
-      return readStatusCopy(t, 'status.component.state.0', 'Normal');
+      return readStatusCopy(t, 'status.component.state.0', STATUS_COMPONENT_STATE_0_FALLBACK_LABEL);
     case 1:
-      return readStatusCopy(t, 'status.component.state.1', 'Abnormal');
+      return readStatusCopy(t, 'status.component.state.1', STATUS_COMPONENT_STATE_1_FALLBACK_LABEL);
     case 2:
-      return readStatusCopy(t, 'status.component.state.2', 'Unknown');
+      return readStatusCopy(t, 'status.component.state.2', STATUS_COMPONENT_STATE_2_FALLBACK_LABEL);
     default:
       return '-';
   }
@@ -133,13 +177,13 @@ export function componentStateLabel(value: unknown, t: StatusCopyTranslator) {
 export function incidentStateLabel(value: unknown, t: StatusCopyTranslator) {
   switch (normalizeIncidentState(value)) {
     case 0:
-      return readStatusCopy(t, 'status.incident.state.0', 'Investigating');
+      return readStatusCopy(t, 'status.incident.state.0', STATUS_INCIDENT_STATE_0_FALLBACK_LABEL);
     case 1:
-      return readStatusCopy(t, 'status.incident.state.1', 'Identified');
+      return readStatusCopy(t, 'status.incident.state.1', STATUS_INCIDENT_STATE_1_FALLBACK_LABEL);
     case 2:
-      return readStatusCopy(t, 'status.incident.state.2', 'Monitoring');
+      return readStatusCopy(t, 'status.incident.state.2', STATUS_INCIDENT_STATE_2_FALLBACK_LABEL);
     case 3:
-      return readStatusCopy(t, 'status.incident.state.3', 'Resolved');
+      return readStatusCopy(t, 'status.incident.state.3', STATUS_INCIDENT_STATE_3_FALLBACK_LABEL);
     default:
       return '-';
   }
@@ -148,11 +192,11 @@ export function incidentStateLabel(value: unknown, t: StatusCopyTranslator) {
 export function orgStateLabel(value: unknown, t: StatusCopyTranslator) {
   switch (normalizeOrgState(value)) {
     case 0:
-      return readStatusCopy(t, 'status.public.org.state.0', 'All systems operational');
+      return readStatusCopy(t, 'status.public.org.state.0', STATUS_PUBLIC_ORG_STATE_0_FALLBACK_LABEL);
     case 1:
-      return readStatusCopy(t, 'status.public.org.state.1', 'Some systems abnormal');
+      return readStatusCopy(t, 'status.public.org.state.1', STATUS_PUBLIC_ORG_STATE_1_FALLBACK_LABEL);
     case 2:
-      return readStatusCopy(t, 'status.public.org.state.2', 'All systems abnormal');
+      return readStatusCopy(t, 'status.public.org.state.2', STATUS_PUBLIC_ORG_STATE_2_FALLBACK_LABEL);
     default:
       return '-';
   }
@@ -176,14 +220,14 @@ export function currentStatusViewLabel(mode: 'component' | 'incident', t: Status
     return readStatusCopy(
       t,
       'status.components.view',
-      readStatusCopy(t, 'status.public.to-component', 'Status Page')
+      readStatusCopy(t, 'status.public.to-component', STATUS_PUBLIC_TO_COMPONENT_FALLBACK_LABEL)
     );
   }
 
   return readStatusCopy(
     t,
     'status.incidents.view',
-    readStatusCopy(t, 'status.public.to-incident', 'Incident History')
+    readStatusCopy(t, 'status.public.to-incident', STATUS_PUBLIC_TO_INCIDENT_FALLBACK_LABEL)
   );
 }
 
@@ -222,129 +266,134 @@ export function statusIncidentStateColor(value: unknown) {
 }
 
 export function statusPublicHomeLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.home', 'Home');
+  return readStatusCopy(t, 'status.public.home', STATUS_PUBLIC_HOME_FALLBACK_LABEL);
 }
 
 export function statusPublicFeedbackLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.feedback', 'Feedback');
+  return readStatusCopy(t, 'status.public.feedback', STATUS_PUBLIC_FEEDBACK_FALLBACK_LABEL);
 }
 
 export function statusPublicYearLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.year', 'Year');
+  return readStatusCopy(t, 'status.public.year', STATUS_PUBLIC_YEAR_FALLBACK_LABEL);
 }
 
 export function statusPublicHistoryLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.history', 'History');
+  return readStatusCopy(t, 'status.public.history', STATUS_PUBLIC_HISTORY_FALLBACK_LABEL);
 }
 
 export function statusPublicUptimeLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.uptime', 'Uptime');
+  return readStatusCopy(t, 'status.public.uptime', STATUS_PUBLIC_UPTIME_FALLBACK_LABEL);
 }
 
 export function statusPublicUpdatedLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.updated', 'Updated');
+  return readStatusCopy(t, 'status.public.updated', STATUS_PUBLIC_UPDATED_FALLBACK_LABEL);
 }
 
 export function statusPublicComponentHistoryLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.component-history', 'Component history');
+  return readStatusCopy(t, 'status.public.component-history', STATUS_PUBLIC_COMPONENT_HISTORY_FALLBACK_LABEL);
 }
 
 export function statusPublicIncidentRangeLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.incident-range', 'Incident history');
+  return readStatusCopy(t, 'status.public.incident-range', STATUS_PUBLIC_INCIDENT_RANGE_FALLBACK_LABEL);
 }
 
 export function statusPublicTodayLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.today', 'Today');
+  return readStatusCopy(t, 'status.public.today', STATUS_PUBLIC_TODAY_FALLBACK_LABEL);
 }
 
 export function statusPublicThirtyDayLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.30-day', '30 days');
+  return readStatusCopy(t, 'status.public.30-day', STATUS_PUBLIC_THIRTY_DAY_FALLBACK_LABEL);
 }
 
 export function statusPublicPoweredByLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.power-by', 'Powered by HertzBeat');
+  return readStatusCopy(t, 'status.public.power-by', STATUS_PUBLIC_POWERED_BY_FALLBACK_LABEL);
 }
 
 export function statusPublicToIncidentLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.to-incident', 'Incident History');
+  return readStatusCopy(t, 'status.public.to-incident', STATUS_PUBLIC_TO_INCIDENT_FALLBACK_LABEL);
 }
 
 export function statusPublicToComponentLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.public.to-component', 'Status Page');
+  return readStatusCopy(t, 'status.public.to-component', STATUS_PUBLIC_TO_COMPONENT_FALLBACK_LABEL);
 }
 
 export function statusIncidentPublicStartAtLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.incident.public.start-at', 'Start At');
+  return readStatusCopy(t, 'status.incident.public.start-at', STATUS_INCIDENT_PUBLIC_START_AT_FALLBACK_LABEL);
 }
 
 export function statusIncidentPublicUpdateAtLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.incident.public.update-at', 'Update At');
+  return readStatusCopy(t, 'status.incident.public.update-at', STATUS_INCIDENT_PUBLIC_UPDATE_AT_FALLBACK_LABEL);
 }
 
 export function statusPageLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.page', 'Status Page');
+  return readStatusCopy(t, 'status.page', STATUS_PAGE_FALLBACK_LABEL);
 }
 
 export function statusSettingLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'setting.status.title', 'Status page settings');
+  return readStatusCopy(t, 'setting.status.title', STATUS_SETTING_FALLBACK_LABEL);
 }
 
 export function statusComponentsLabel(t: StatusCopyTranslator) {
+  const sharedLabel = readStatusCopy(t, 'status.components.title', STATUS_COMPONENTS_FALLBACK_LABEL);
   return readStatusCopy(
     t,
     'setting.status.components.title',
-    readStatusCopy(t, 'status.components.title', 'Components')
+    sharedLabel === STATUS_COMPONENTS_FALLBACK_LABEL ? STATUS_SETTING_COMPONENTS_FALLBACK_LABEL : sharedLabel
   );
 }
 
 export function statusIncidentsLabel(t: StatusCopyTranslator) {
+  const sharedLabel = readStatusCopy(t, 'status.incidents.title', STATUS_INCIDENTS_FALLBACK_LABEL);
   return readStatusCopy(
     t,
     'setting.status.incidents.title',
-    readStatusCopy(t, 'status.incidents.title', 'Incidents')
+    sharedLabel === STATUS_INCIDENTS_FALLBACK_LABEL ? STATUS_SETTING_INCIDENTS_FALLBACK_LABEL : sharedLabel
   );
 }
 
 export function statusOrganizationLabel(t: StatusCopyTranslator) {
+  const sharedLabel = readStatusCopy(t, 'status.org.title', STATUS_ORGANIZATION_FALLBACK_LABEL);
   return readStatusCopy(
     t,
     'setting.status.org.title',
-    readStatusCopy(t, 'status.org.title', 'Organization')
+    sharedLabel === STATUS_ORGANIZATION_FALLBACK_LABEL ? STATUS_SETTING_ORGANIZATION_FALLBACK_LABEL : sharedLabel
   );
 }
 
 export function statusOrganizationStateLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'setting.status.org.state', 'Org state');
+  return readStatusCopy(t, 'setting.status.org.state', STATUS_SETTING_ORGANIZATION_STATE_FALLBACK_LABEL);
 }
 
 export function statusViewLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.fact.view', readStatusCopy(t, 'common.current-view', 'Current view'));
+  return readStatusCopy(t, 'status.fact.view', readStatusCopy(t, 'common.current-view', COMMON_CURRENT_VIEW_FALLBACK_LABEL));
 }
 
 export function statusFieldLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'common.status', 'Status');
+  return readStatusCopy(t, 'common.status', COMMON_STATUS_FALLBACK_LABEL);
 }
 
 export function statusComponentsCountLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.fact.components', 'Components');
+  return readStatusCopy(t, 'status.fact.components', STATUS_FACT_COMPONENTS_FALLBACK_LABEL);
 }
 
 export function statusIncidentsCountLabel(t: StatusCopyTranslator) {
-  return readStatusCopy(t, 'status.fact.incidents', 'Incidents');
+  return readStatusCopy(t, 'status.fact.incidents', STATUS_FACT_INCIDENTS_FALLBACK_LABEL);
 }
 
 export function statusOrganizationCopy(t: StatusCopyTranslator) {
+  const sharedCopy = readStatusCopy(t, 'status.org.copy', STATUS_ORGANIZATION_COPY_FALLBACK);
   return readStatusCopy(
     t,
     'setting.status.org.copy',
-    readStatusCopy(t, 'status.org.copy', 'Publish your public org profile to explain who owns this status page.')
+    sharedCopy === STATUS_ORGANIZATION_COPY_FALLBACK ? STATUS_SETTING_ORGANIZATION_COPY_FALLBACK : sharedCopy
   );
 }
 
 export function statusOrganizationFallback(t: StatusCopyTranslator) {
+  const sharedName = readStatusCopy(t, 'status.org.empty', STATUS_ORGANIZATION_NAME_FALLBACK);
   return readStatusCopy(
     t,
     'setting.status.org.fallback',
-    readStatusCopy(t, 'status.org.empty', 'Status organization')
+    sharedName === STATUS_ORGANIZATION_NAME_FALLBACK ? STATUS_SETTING_ORGANIZATION_NAME_FALLBACK : sharedName
   );
 }
diff --git a/web-next/lib/status-center/view-model.test.ts b/web-next/lib/status-center/view-model.test.ts
index 39732773e8..b45f1f4077 100644
--- a/web-next/lib/status-center/view-model.test.ts
+++ b/web-next/lib/status-center/view-model.test.ts
@@ -12,15 +12,53 @@ import {
   clampStatusIncidentYear,
   componentTone
 } from './view-model';
+import {
+  componentStateLabel,
+  incidentStateLabel,
+  orgStateLabel,
+  statusIncidentPublicStartAtLabel,
+  statusIncidentPublicUpdateAtLabel,
+  statusComponentsCountLabel,
+  statusComponentsLabel,
+  statusFieldLabel,
+  statusIncidentsCountLabel,
+  statusIncidentsLabel,
+  statusOrganizationCopy,
+  statusOrganizationFallback,
+  statusOrganizationLabel,
+  statusOrganizationStateLabel,
+  statusPageLabel,
+  statusPublicComponentHistoryLabel,
+  statusPublicFeedbackLabel,
+  statusPublicHistoryLabel,
+  statusPublicHomeLabel,
+  statusPublicIncidentRangeLabel,
+  statusPublicPoweredByLabel,
+  statusPublicThirtyDayLabel,
+  statusPublicTodayLabel,
+  statusPublicToComponentLabel,
+  statusPublicToIncidentLabel,
+  statusPublicUpdatedLabel,
+  statusPublicUptimeLabel,
+  statusPublicYearLabel,
+  statusSettingLabel,
+  statusViewLabel
+} from './display';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 const t = createTranslatorMock({
   locale: 'zh-CN',
   overrides: {
+    'status.incident.default-title': '事件',
+    'status.components.empty.title': '还没有组件',
+    'status.components.empty.copy': '配置完成后,公开状态页组件会显示在这里。',
+    'status.incidents.empty.title': '还没有事件',
+    'status.incidents.empty.copy': '发布事件更新后,公开事件历史会显示在这里。',
     'status.incidents.view': '事件历史视图',
     'status.incidents.copy': '查看最近的公开事件和恢复进展。',
   }
 });
+const enT = createTranslatorMock({ locale: 'en-US' });
 
 describe('status center view model', () => {
   it('maps component status to tone buckets', () => {
@@ -71,6 +109,24 @@ describe('status center view model', () => {
     ]);
   });
 
+  it('uses runtime none fallback for status component rows without description or endpoint', () => {
+    expect(
+      buildStatusRows(
+        'component',
+        [{ name: 'queue', status: 1, latestTime: 1712730000000 }] as any,
+        [],
+        t,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      {
+        title: 'queue',
+        copy: '无',
+        meta: '异常 · 2026-04-10 18:00:00'
+      }
+    ]);
+  });
+
   it('builds incident rows for incident mode', () => {
     expect(
       buildStatusRows(
@@ -95,6 +151,46 @@ describe('status center view model', () => {
     ]);
   });
 
+  it('uses runtime none fallback for status incident rows without title or id', () => {
+    expect(
+      buildStatusRows(
+        'incident',
+        [],
+        [{
+          status: 0,
+          updateTime: 1712730000000,
+          contents: [{ message: 'Checking gateway errors', timestamp: 1712730000000 }]
+        }] as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )
+    ).toEqual([
+      {
+        title: '事件 无',
+        copy: 'Checking gateway errors',
+        meta: '调查中 · 2026-04-10 18:00:00'
+      }
+    ]);
+  });
+
+  it('uses runtime none fallback for status empty row meta', () => {
+    expect(buildStatusRows('component', [], [], t, () => '2026-04-10 18:00:00')).toEqual([
+      {
+        title: '还没有组件',
+        copy: '配置完成后,公开状态页组件会显示在这里。',
+        meta: '无'
+      }
+    ]);
+
+    expect(buildStatusRows('incident', [], [], t, () => '2026-04-10 18:00:00')).toEqual([
+      {
+        title: '还没有事件',
+        copy: '发布事件更新后,公开事件历史会显示在这里。',
+        meta: '无'
+      }
+    ]);
+  });
+
   it('builds status org rows and public posture', () => {
     expect(buildStatusOrgRows('component', { name: 'Ops', description: 'Public board', state: 0 } as any, t)).toEqual([
       { title: 'Ops', copy: 'Public board', meta: '所有系统正常运行' },
@@ -108,6 +204,65 @@ describe('status center view model', () => {
     });
   });
 
+  it('keeps English incident posture locale copy without localized fallback text', () => {
+    const copy = enT('status.incidents.posture');
+
+    expect(copy).toBe('Focus the public incident history entry.');
+    expect(copy).not.toMatch(/[\u4e00-\u9fff]/);
+  });
+
+  it('uses English runtime fallbacks for public status history labels when translations are missing', () => {
+    const missingTranslator = (key: string) => key;
+
+    expect(statusPublicHomeLabel(missingTranslator)).toBe('Home');
+    expect(statusPublicFeedbackLabel(missingTranslator)).toBe('Feedback');
+    expect(statusPublicYearLabel(missingTranslator)).toBe('Year');
+    expect(statusPublicHistoryLabel(missingTranslator)).toBe('History');
+    expect(statusPublicUptimeLabel(missingTranslator)).toBe('Uptime');
+    expect(statusPublicUpdatedLabel(missingTranslator)).toBe('Updated');
+    expect(statusPublicComponentHistoryLabel(missingTranslator)).toBe('Component history');
+    expect(statusPublicIncidentRangeLabel(missingTranslator)).toBe('Incident range');
+    expect(statusPublicTodayLabel(missingTranslator)).toBe('Today');
+    expect(statusPublicThirtyDayLabel(missingTranslator)).toBe('30 days');
+    expect(statusPublicPoweredByLabel(missingTranslator)).toBe('Powered by HertzBeat');
+    expect(statusPublicToIncidentLabel(missingTranslator)).toBe('Incident History');
+    expect(statusPublicToComponentLabel(missingTranslator)).toBe('Status Page');
+    expect(statusIncidentPublicStartAtLabel(missingTranslator)).toBe('Start At');
+    expect(statusIncidentPublicUpdateAtLabel(missingTranslator)).toBe('Update At');
+  });
+
+  it('uses English runtime fallbacks for status center admin and fact labels when translations are missing', () => {
+    const missingTranslator = (key: string) => key;
+
+    expect(statusPageLabel(missingTranslator)).toBe('Status Page');
+    expect(statusSettingLabel(missingTranslator)).toBe('Status page settings');
+    expect(statusComponentsLabel(missingTranslator)).toBe('Components');
+    expect(statusIncidentsLabel(missingTranslator)).toBe('Incidents');
+    expect(statusOrganizationLabel(missingTranslator)).toBe('Org profile');
+    expect(statusOrganizationStateLabel(missingTranslator)).toBe('Org state');
+    expect(statusViewLabel(missingTranslator)).toBe('Current view');
+    expect(statusFieldLabel(missingTranslator)).toBe('Status');
+    expect(statusComponentsCountLabel(missingTranslator)).toBe('Components');
+    expect(statusIncidentsCountLabel(missingTranslator)).toBe('Incidents');
+    expect(statusOrganizationCopy(missingTranslator)).toBe('Configure the public org information shown on the status page.');
+    expect(statusOrganizationFallback(missingTranslator)).toBe('Unnamed status org');
+  });
+
+  it('uses English runtime fallbacks for status center state labels when translations are missing', () => {
+    const missingTranslator = (key: string) => key;
+
+    expect(componentStateLabel(0, missingTranslator)).toBe('Normal');
+    expect(componentStateLabel(1, missingTranslator)).toBe('Abnormal');
+    expect(componentStateLabel(2, missingTranslator)).toBe('Unknown');
+    expect(incidentStateLabel(0, missingTranslator)).toBe('Investigating');
+    expect(incidentStateLabel(1, missingTranslator)).toBe('Identified');
+    expect(incidentStateLabel(2, missingTranslator)).toBe('Monitoring');
+    expect(incidentStateLabel(3, missingTranslator)).toBe('Resolved');
+    expect(orgStateLabel(0, missingTranslator)).toBe('All Systems Operational');
+    expect(orgStateLabel(1, missingTranslator)).toBe('Some Systems Abnormal');
+    expect(orgStateLabel(2, missingTranslator)).toBe('All Systems Abnormal');
+  });
+
   it('builds the public brand block and year selector options', () => {
     expect(
       buildStatusBrand(
@@ -136,7 +291,7 @@ describe('status center view model', () => {
       homeLabel: '主页',
       feedbackLabel: '问题反馈',
       historyLabel: '历史',
-      uptimeLabel: 'Uptime',
+      uptimeLabel: '可用率',
       updatedLabel: '更新于',
       updatedAt: '2026-04-10 18:00:00'
     });
@@ -177,11 +332,12 @@ describe('status center view model', () => {
         title: 'API',
         copy: 'public api',
         state: 0,
+        tone: 'success',
         statusLabel: '正常',
         latestTimeLabel: '2026-04-10 18:00:00',
         latestUptimeLabel: '99.00%',
         historyLabel: '组件历史',
-        uptimeLabel: 'Uptime',
+        uptimeLabel: '可用率',
         history: [
           { timestamp: 1712730000000, state: 0, uptime: 0.99, abnormal: 1, normal: 70, unknowing: 0 },
           { timestamp: 1712640000000, state: 1, uptime: 0.84, abnormal: 10, normal: 50, unknowing: 2 }
@@ -190,13 +346,13 @@ describe('status center view model', () => {
           {
             timestampLabel: '2026-04-10 18:00:00',
             uptimeLabel: '99.00%',
-            title: '2026-04-10 18:00:00 · Uptime 99.00%',
+            title: '2026-04-10 18:00:00 · 可用率 99.00%',
             color: '#28a745'
           },
           {
             timestampLabel: '2026-04-10 18:00:00',
             uptimeLabel: '84.00%',
-            title: '2026-04-10 18:00:00 · Uptime 84.00%',
+            title: '2026-04-10 18:00:00 · 可用率 84.00%',
             color: 'rgb(255, 252, 0)'
           }
         ]
@@ -227,6 +383,7 @@ describe('status center view model', () => {
         title: 'API degraded',
         copy: 'Monitoring progress',
         state: 1,
+        tone: 'default',
         stateLabel: '已确认',
         stateColor: '#e56c23',
         meta: '已确认 · 2026-04-10 18:00:00',
@@ -238,6 +395,7 @@ describe('status center view model', () => {
             timestampLabel: '2026-04-10 18:00:00',
             stateLabel: '调查中',
             state: 0,
+            tone: 'danger',
             stateColor: '#ff2f2f',
             message: 'Investigating payment latency'
           },
@@ -245,6 +403,7 @@ describe('status center view model', () => {
             timestampLabel: '2026-04-10 18:00:00',
             stateLabel: '观察中',
             state: 2,
+            tone: 'default',
             stateColor: '#19a7e7',
             message: 'Monitoring progress'
           }
@@ -252,4 +411,99 @@ describe('status center view model', () => {
       }
     ]);
   });
+
+  it('uses runtime none fallback for incident content without state', () => {
+    expect(
+      buildStatusIncidentCards(
+        [
+          {
+            id: 10,
+            title: 'API latency',
+            state: 2,
+            updateTime: 1712730000000,
+            contents: [{ message: 'Waiting for upstream confirmation', timestamp: 1712730000000 }]
+          }
+        ] as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )[0].contents[0]
+    ).toEqual({
+      timestampLabel: '2026-04-10 18:00:00',
+      stateLabel: '无',
+      state: null,
+      tone: 'default',
+      stateColor: '#6b7280',
+      message: 'Waiting for upstream confirmation'
+    });
+  });
+
+  it('uses runtime none fallback for public incident cards without title, name, or id', () => {
+    expect(
+      buildStatusIncidentCards(
+        [
+          {
+            state: 0,
+            updateTime: 1712730000000,
+            contents: [{ message: 'Checking public impact', timestamp: 1712730000000 }]
+          }
+        ] as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )[0]
+    ).toMatchObject({
+      key: 'incident',
+      title: '事件 无',
+      copy: 'Checking public impact',
+      stateLabel: '调查中'
+    });
+  });
+
+  it('uses runtime none fallback for component history without uptime', () => {
+    expect(
+      buildStatusComponentHistoryRows(
+        [
+          {
+            id: 11,
+            name: 'Gateway',
+            endpoint: '/ready',
+            state: 1,
+            latestTime: 1712730000000,
+            history: [{ timestamp: 1712730000000, state: 2 }]
+          }
+        ] as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )[0]
+    ).toMatchObject({
+      key: '11',
+      title: 'Gateway',
+      copy: '/ready',
+      latestUptimeLabel: '无',
+      blocks: [
+        {
+          timestampLabel: '2026-04-10 18:00:00',
+          uptimeLabel: '无',
+          title: '2026-04-10 18:00:00 · 可用率 无'
+        }
+      ]
+    });
+  });
+
+  it('uses runtime none fallback for component history without description or endpoint', () => {
+    expect(
+      buildStatusComponentHistoryRows(
+        [
+          {
+            id: 12,
+            name: 'Worker',
+            state: 0,
+            latestTime: 1712730000000,
+            history: [{ timestamp: 1712730000000, state: 0, uptime: 1 }]
+          }
+        ] as any,
+        t,
+        () => '2026-04-10 18:00:00'
+      )[0].copy
+    ).toBe('无');
+  });
 });
diff --git a/web-next/lib/status-center/view-model.ts b/web-next/lib/status-center/view-model.ts
index 501fa61029..9b8cb34f3b 100644
--- a/web-next/lib/status-center/view-model.ts
+++ b/web-next/lib/status-center/view-model.ts
@@ -5,6 +5,7 @@ import {
   incidentStateLabel,
   latestIncidentMessage,
   normalizeComponentState,
+  normalizeIncidentState,
   resolveStatusHref,
   readStatusCopy,
   statusHistoryBlockColor,
@@ -29,6 +30,7 @@ export { statusIncidentStateColor } from './display';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
 type TranslatorWithValues = (key: string, params?: Record<string, string | number | null | undefined>) => string;
+type PublicStatusBadgeTone = 'success' | 'danger' | 'default';
 
 export function componentTone(status?: number | string | null): string {
   const normalizedState = normalizeComponentState(status);
@@ -37,6 +39,20 @@ export function componentTone(status?: number | string | null): string {
   return 'unknown';
 }
 
+function publicComponentBadgeTone(state: number | string | null | undefined): PublicStatusBadgeTone {
+  const normalizedState = normalizeComponentState(state);
+  if (normalizedState === 0) return 'success';
+  if (normalizedState === 1) return 'danger';
+  return 'default';
+}
+
+function publicIncidentBadgeTone(state: number | string | null | undefined): PublicStatusBadgeTone {
+  const normalizedState = normalizeIncidentState(state);
+  if (normalizedState === 3) return 'success';
+  if (normalizedState === 0) return 'danger';
+  return 'default';
+}
+
 export function buildStatusMetrics(components: StatusPageComponent[], incidents: StatusPageIncident[], t: Translator) {
   const normalCount = components.filter(item => componentTone(item.status ?? item.state ?? null) === 'normal').length;
   return [
@@ -70,23 +86,28 @@ export function buildStatusRows(
   t: Translator,
   formatTime: (value?: number | string | null) => string
 ) {
+  const emptyValue = t('common.none');
+
   if (mode === 'component') {
     return components.length > 0
       ? components.map(item => ({
           title: item.name || t('status.component.default-title'),
-          copy: item.description || item.endpoint || '-',
+          copy: item.description || item.endpoint || emptyValue,
           meta: `${componentStateLabel(item.status ?? item.state ?? null, t)} · ${formatTime(item.latestTime || item.gmtUpdate || item.gmtCreate || null)}`
         }))
-      : [{ title: t('status.components.empty.title'), copy: t('status.components.empty.copy'), meta: '-' }];
+      : [{ title: t('status.components.empty.title'), copy: t('status.components.empty.copy'), meta: emptyValue }];
   }
 
   return incidents.length > 0
-    ? incidents.map(item => ({
-        title: item.title || `${t('status.incident.default-title')} ${item.id || '-'}`,
-        copy: latestIncidentMessage(item) || `${readStatusCopy(t, 'common.status', 'Status')} ${incidentStateLabel(item.status ?? item.state ?? null, t)}`,
-        meta: `${incidentStateLabel(item.status ?? item.state ?? null, t)} · ${formatTime(item.updateTime || item.createTime || null)}`
-      }))
-    : [{ title: t('status.incidents.empty.title'), copy: t('status.incidents.empty.copy'), meta: '-' }];
+    ? incidents.map(item => {
+        const incidentId = item.id || emptyValue;
+        return {
+          title: item.title || `${t('status.incident.default-title')} ${incidentId}`,
+          copy: latestIncidentMessage(item) || `${readStatusCopy(t, 'common.status', 'Status')} ${incidentStateLabel(item.status ?? item.state ?? null, t)}`,
+          meta: `${incidentStateLabel(item.status ?? item.state ?? null, t)} · ${formatTime(item.updateTime || item.createTime || null)}`
+        };
+      })
+    : [{ title: t('status.incidents.empty.title'), copy: t('status.incidents.empty.copy'), meta: emptyValue }];
 }
 
 export function buildStatusOrgRows(mode: 'component' | 'incident', org: StatusPageOrg, t: Translator) {
@@ -153,19 +174,22 @@ export function buildStatusComponentHistoryRows(
   t: Translator,
   formatTime: (value?: number | string | null) => string
 ) {
+  const emptyValue = t('common.none');
+
   return components.map(component => {
     const history = [...(component.history || [])].sort(
       (left, right) => Number(right.timestamp ?? 0) - Number(left.timestamp ?? 0)
     ) as StatusPageHistory[];
     const latestHistory = history[0];
     const latestTime = component.latestTime || component.gmtUpdate || component.gmtCreate || latestHistory?.timestamp || null;
-    const latestUptime = latestHistory?.uptime != null ? `${(latestHistory.uptime * 100).toFixed(2)}%` : '-';
+    const latestUptime = latestHistory?.uptime != null ? `${(latestHistory.uptime * 100).toFixed(2)}%` : emptyValue;
 
     return {
       key: String(component.id || component.name || 'component'),
       title: component.name || t('status.component.default-title'),
-      copy: component.description || component.endpoint || '-',
+      copy: component.description || component.endpoint || emptyValue,
       state: component.status ?? component.state ?? null,
+      tone: publicComponentBadgeTone(component.status ?? component.state ?? null),
       statusLabel: componentStateLabel(component.status ?? component.state ?? null, t),
       latestTimeLabel: formatTime(latestTime),
       latestUptimeLabel: latestUptime,
@@ -174,8 +198,8 @@ export function buildStatusComponentHistoryRows(
       history,
       blocks: history.map(item => ({
         timestampLabel: formatTime(item.timestamp || null),
-        uptimeLabel: item.uptime != null ? `${(item.uptime * 100).toFixed(2)}%` : '-',
-        title: `${formatTime(item.timestamp || null)} · ${statusPublicUptimeLabel(t)} ${item.uptime != null ? `${(item.uptime * 100).toFixed(2)}%` : '-'}`,
+        uptimeLabel: item.uptime != null ? `${(item.uptime * 100).toFixed(2)}%` : emptyValue,
+        title: `${formatTime(item.timestamp || null)} · ${statusPublicUptimeLabel(t)} ${item.uptime != null ? `${(item.uptime * 100).toFixed(2)}%` : emptyValue}`,
         color: statusHistoryBlockColor(item)
       }))
     };
@@ -187,17 +211,21 @@ export function buildStatusIncidentCards(
   t: Translator,
   formatTime: (value?: number | string | null) => string
 ) {
+  const emptyValue = t('common.none');
+
   return incidents.map(incident => {
     const contents = [...(incident.contents || [])];
     const latestMessage = latestIncidentMessage(incident);
     const stateLabel = incidentStateLabel(incident.status ?? incident.state ?? null, t);
     const stateColor = statusIncidentStateColor(incident.status ?? incident.state ?? null);
+    const incidentId = incident.id || emptyValue;
 
     return {
       key: String(incident.id || incident.title || incident.name || 'incident'),
-      title: incident.title || incident.name || `${t('status.incident.default-title')} ${incident.id || '-'}`,
+      title: incident.title || incident.name || `${t('status.incident.default-title')} ${incidentId}`,
       copy: latestMessage || `${readStatusCopy(t, 'status.incident.status', 'Status')} ${stateLabel}`,
       state: incident.status ?? incident.state ?? null,
+      tone: publicIncidentBadgeTone(incident.status ?? incident.state ?? null),
       stateLabel,
       stateColor,
       meta: `${stateLabel} · ${formatTime(incident.updateTime || incident.createTime || incident.endTime || incident.startTime || null)}`,
@@ -206,8 +234,9 @@ export function buildStatusIncidentCards(
       updateAtLabel: formatTime(incident.endTime || incident.updateTime || incident.startTime || incident.createTime || null),
       contents: contents.map(content => ({
         timestampLabel: formatTime(content.timestamp || null),
-        stateLabel: content.state != null ? incidentStateLabel(content.state, t) : '-',
+        stateLabel: content.state != null ? incidentStateLabel(content.state, t) : emptyValue,
         state: content.state ?? null,
+        tone: publicIncidentBadgeTone(content.state ?? null),
         stateColor: statusIncidentStateColor(content.state ?? null),
         message: content.message || ''
       }))


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 47/50: Prepare HertzBeat community alpha preview

Posted by zh...@apache.org.
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 d068e0a73a1c02c7168b413bed98cb5fbdd50ab1
Author: Logic <zq...@dromara.org>
AuthorDate: Tue Jun 2 21:29:25 2026 +0800

    Prepare HertzBeat community alpha preview
    
    Add alpha preview docs and validation checklist.
    
    Harden entity topology, signal evidence, and Greptime trace read models.
    
    Add Next topology/entity shell surfaces with shared UI and G6 smoke coverage.
    
    Fix hertzbeat-ai Maven child version model warning.
---
 .gitignore                                         |    9 +
 README.md                                          |    3 +
 docs/alpha-preview.md                              |  139 +
 hertzbeat-ai/pom.xml                               |    1 -
 .../EntitySignalEvidenceBundle.java}               |   60 +-
 .../dto/handoff/EntityResponseHandoffsRequest.java |   28 +
 .../hertzbeat/manager/dao/ObserveEntityDao.java    |    3 +
 .../manager/pojo/dto/EntityDetailDto.java          |    3 +
 .../EntityDetailObservabilityReadModelService.java |   10 +-
 .../EntityResponseHandoffReadModelService.java     |   41 +-
 .../service/entity/EntityTopologyQueryService.java |   60 +-
 .../entity/EntityWorkspaceAccessService.java       |   11 +
 .../entity/EntityWorkspaceQueryService.java        |    7 +
 .../entity/LocalTopologyDemoRelationSeeder.java    |  238 ++
 .../entity/TraceCallTopologyQueryService.java      |   61 +-
 ...ityDetailObservabilityReadModelServiceTest.java |   20 +-
 .../entity/EntityTopologyQueryServiceTest.java     |   84 +-
 .../entity/EntityWorkspaceAccessServiceTest.java   |   24 +
 .../entity/EntityWorkspaceQueryServiceTest.java    |   26 +
 .../LocalTopologyDemoRelationSeederTest.java       |  222 ++
 .../entity/TraceCallTopologyQueryServiceTest.java  |   79 +
 .../logs/service/impl/LogQueryServiceImpl.java     |   84 +-
 .../impl/EntityObservabilityGatewayImpl.java       |   24 +-
 .../service/impl/EntityTraceQueryServiceImpl.java  |   88 +
 .../logs/controller/LogQueryControllerTest.java    |  104 +
 .../impl/EntityObservabilityGatewayImplTest.java   |   54 +
 .../impl/EntityTraceQueryServiceImplTest.java      |  137 +
 .../repository/GreptimeTraceQueryRepository.java   |  195 +
 .../warehouse/repository/TraceQueryRepository.java |  105 +
 .../store/history/tsdb/HistoryDataReader.java      |   76 +
 .../tsdb/greptime/GreptimeDbDataStorage.java       |   96 +-
 .../GreptimeTraceQueryRepositoryTest.java          |  143 +
 .../tsdb/greptime/GreptimeDbDataStorageTest.java   |  141 +
 web-next/app/topology/page.test.tsx                |  953 ++++-
 web-next/app/topology/page.tsx                     |   12 +-
 web-next/app/topology/topology-page.tsx            | 1025 +++--
 web-next/app/topology/topology-route-client.tsx    |   44 +
 web-next/app/topology/topology-route-shell.tsx     |   26 +
 web-next/app/ui-lab/page.test.tsx                  |  218 +-
 web-next/app/ui-lab/page.tsx                       |  160 +-
 .../pages/entity-detail-surface.test.tsx           |   35 +
 .../components/pages/entity-detail-surface.tsx     |    4 +-
 .../components/shell/app-frame.chrome.test.tsx     |   15 +
 web-next/components/shell/app-frame.tsx            |    6 +-
 web-next/components/shell/auth-gate.tsx            |   13 +-
 .../components/shell/auth-recovery.chrome.test.ts  |   14 +
 web-next/components/shell/locale-option-list.tsx   |    2 +-
 web-next/lib/app-frame-state.test.ts               |   11 +-
 web-next/lib/app-frame-state.ts                    |    8 +-
 web-next/lib/entity-contract.test.ts               |   30 +
 web-next/lib/entity-contract.ts                    |   20 +-
 web-next/lib/entity-detail/view-model.test.ts      |   44 +
 web-next/lib/entity-detail/view-model.ts           |   42 +-
 web-next/lib/hertzbeat-2-gap-audit.test.ts         |   42 +-
 web-next/lib/hertzbeat-2-gap-audit.ts              |   42 +-
 web-next/lib/i18n-runtime-messages.ts              |   56 +-
 web-next/lib/session-client.ts                     |   10 +
 web-next/lib/topology-surface/controller.test.ts   |  358 +-
 web-next/lib/topology-surface/controller.ts        |  264 +-
 web-next/lib/topology-surface/query-state.test.ts  |    4 +
 web-next/lib/topology-surface/query-state.ts       |    2 +
 web-next/lib/topology-surface/view-model.test.ts   |    3 +-
 web-next/lib/topology-surface/view-model.ts        |    4 +-
 web-next/lib/types.ts                              |   13 +
 web-next/packages/hertzbeat-ui/src/index.test.tsx  |  172 +-
 web-next/packages/hertzbeat-ui/src/index.tsx       |  323 +-
 web-next/packages/hertzbeat-ui/src/shell.tsx       |  690 ++++
 .../src/topology-companion.interaction.test.tsx    |  260 +-
 .../hertzbeat-ui/src/topology-g6-runtime.ts        |   15 +
 .../packages/hertzbeat-ui/src/topology-g6.test.tsx |  768 +++-
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx | 1436 ++++++-
 .../src/topology-metric-table.interaction.test.tsx |  277 ++
 web-next/packages/hertzbeat-ui/src/topology.tsx    | 4201 ++++++++++++++++++++
 web-next/scripts/topology-g6-browser-smoke.spec.ts |  231 ++
 web-next/test/i18n-test-helper.ts                  |   50 +-
 web-next/tsconfig.json                             |    9 +
 web-next/vitest.config.ts                          |    2 +
 77 files changed, 13388 insertions(+), 902 deletions(-)

diff --git a/.gitignore b/.gitignore
index 8e3b8815b5..02887dff77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,15 @@ node_modules
 .codex-artifacts/
 .playwright-mcp/
 web-next-*.png
+progress.md
+test-results/
+web-next/test-results/
+playwright-report/
+web-next/playwright-report/
+reports/
+screenshots/
+web-next/scripts/topology-scale-proof-seed-lib.mjs
+web-next/scripts/topology-scale-proof-seed-lib.test.ts
 
 # local temp runtime data
 .tmp/
diff --git a/README.md b/README.md
index 0d8cff96a6..3ac9a8b8a8 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,9 @@
 
 ## 🐕 Quick Start   
 
+- Community alpha preview users can start from [docs/alpha-preview.md](docs/alpha-preview.md) for the current
+  Next.js frontend, entity-centered observability, local smoke, and known alpha limitations.
+
 - If you wish to deploy HertzBeat locally, please refer to the following Deployment Documentation for instructions.
 
 ### 🍞 Install HertzBeat   
diff --git a/docs/alpha-preview.md b/docs/alpha-preview.md
new file mode 100644
index 0000000000..a0819467ea
--- /dev/null
+++ b/docs/alpha-preview.md
@@ -0,0 +1,139 @@
+# HertzBeat Community Alpha Preview
+
+This document is for the GitHub community alpha preview branch. It is not a
+stable Apache HertzBeat release checklist. Use it to try the new entity-centered
+observability and Next.js frontend work, report issues, and help harden the
+project toward a beta.
+
+## What Is In Scope
+
+- Entity-centered observability read models for metrics, logs, traces, topology,
+  alerts, owners, runbooks, and handoff context.
+- Next.js `web-next` frontend work for the new shell, entity detail, topology,
+  and UI Lab surfaces.
+- OTLP trace/log/metric query paths backed by the existing warehouse and
+  Greptime integrations.
+- Local demo topology relation seeding for the small Checkout API -> Payment API
+  -> Orders DB example when the `local` Spring profile is active.
+
+## Local Source Quickstart
+
+1. Start the backend from `hertzbeat-startup` with Java 21.
+
+   Keep the Arrow JVM open option when running locally:
+
+   ```shell
+   --add-opens=java.base/java.nio=ALL-UNNAMED
+   ```
+
+   The local backend listens on `http://127.0.0.1:1157` by default.
+
+2. Start the Next.js frontend:
+
+   ```shell
+   cd web-next
+   npm install
+   npm run dev
+   ```
+
+   The preview frontend listens on `http://127.0.0.1:4200`.
+
+3. Open `http://127.0.0.1:4200` and sign in with the local development account
+   configured for your environment.
+
+## Alpha Validation Checklist
+
+Before opening an alpha issue or pull request, run the smallest check that
+matches the area you changed.
+
+Backend entity, topology, observability, and Greptime query changes:
+
+```shell
+./mvnw -pl hertzbeat-manager,hertzbeat-observability,hertzbeat-warehouse \
+  -Dtest=EntityDetailObservabilityReadModelServiceTest,EntityTopologyQueryServiceTest,EntityWorkspaceAccessServiceTest,EntityWorkspaceQueryServiceTest,TraceCallTopologyQueryServiceTest,LocalTopologyDemoRelationSeederTest,LogQueryControllerTest,EntityObservabilityGatewayImplTest,EntityTraceQueryServiceImplTest,GreptimeTraceQueryRepositoryTest,GreptimeDbDataStorageTest \
+  test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false
+```
+
+Startup source package proof:
+
+```shell
+./mvnw -pl hertzbeat-startup -am -DskipTests package
+```
+
+Next.js topology, entity detail, shell, session, and shared UI changes:
+
+```shell
+cd web-next
+npm exec -- vitest run app/topology/page.test.tsx app/ui-lab/page.test.tsx packages/hertzbeat-ui/src/index.test.tsx packages/hertzbeat-ui/src/topology-g6.test.tsx packages/hertzbeat-ui/src/topology-companion.interaction.test.tsx packages/hertzbeat-ui/src/topology-metric-table.interaction.test.tsx components/pages/entity-detail-surface.test.tsx components/shell/app-frame.chrome.test.tsx components/shell/auth-recovery.chrome.test.ts lib/app-frame-state.test.ts lib/entity-contract.test.ts l [...]
+```
+
+Focused frontend lint for touched alpha files:
+
+```shell
+cd web-next
+ESLINT_USE_FLAT_CONFIG=false npm exec -- eslint app components lib packages/hertzbeat-ui/src scripts/topology-g6-browser-smoke.spec.ts test --ext .ts,.tsx
+```
+
+## Optional Local Smoke
+
+The topology G6 browser smoke is intentionally opt-in. It skips unless the
+route and credentials are supplied by the developer:
+
+```shell
+cd web-next
+TOPOLOGY_G6_BROWSER_USERNAME=admin \
+TOPOLOGY_G6_BROWSER_PASSWORD=hertzbeat \
+TOPOLOGY_G6_BROWSER_ROUTE='/topology?environment=prod&timeRange=last-1h&viewMode=service-call&sourceKind=otlp-trace-call&depth=2&groupBy=source-kind' \
+npm exec -- playwright test scripts/topology-g6-browser-smoke.spec.ts
+```
+
+Optional strict assertions can be added with:
+
+- `TOPOLOGY_G6_BROWSER_EXPECTED_NODES`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_EDGES`
+- `TOPOLOGY_G6_BROWSER_FOCUS_NODE_ID`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_RUNTIME_VERSION`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_INITIAL_FIT_STRATEGY`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_WIDE_ZOOM`
+- `TOPOLOGY_G6_BROWSER_EXPECTED_OPERATOR_BOUNDS`
+
+## Local Scale Proof Data
+
+Large topology scale proof data is not seeded by default. The local demo seeder
+only creates the small demo relation repair during normal `local` profile
+startup.
+
+To explicitly seed the local mixed scale proof entity catalog, start the backend
+with:
+
+```shell
+--hertzbeat.topology.local-scale-proof-seed=true
+```
+
+This is intended for local performance investigation only. Do not treat it as
+required alpha setup.
+
+## Known Alpha Limitations
+
+- The Next.js frontend is still an alpha preview and may not yet match every
+  legacy Angular workflow.
+- Topology large-graph behavior is optimized for inspection with render
+  windows, table drilldown, and optional browser smoke. Continue filing cases
+  where real data feels confusing or slow.
+- Runtime verification may depend on local H2 or Greptime data shape. Greptime
+  scale proof fixtures are local-only and should not be committed as generated
+  proof artifacts.
+- Source package rebuilds are expected to pass for the alpha candidate. If a
+  local module or dependency warning blocks packaging, include the focused
+  Maven command and output with the issue report.
+
+## Reporting Issues
+
+When filing alpha feedback, include:
+
+- The route or API endpoint.
+- The storage mode, for example H2-only or Greptime-backed traces.
+- The browser viewport and whether the in-app refresh was used.
+- Expected versus actual node, edge, table, or handoff behavior.
+- Any focused test, Maven, Vitest, ESLint, or browser-smoke command that
+  reproduces the issue.
diff --git a/hertzbeat-ai/pom.xml b/hertzbeat-ai/pom.xml
index 2b9298b6b5..2b270c5bf9 100644
--- a/hertzbeat-ai/pom.xml
+++ b/hertzbeat-ai/pom.xml
@@ -24,7 +24,6 @@
 		<version>2.0-SNAPSHOT</version>
 	</parent>
 	<artifactId>hertzbeat-ai</artifactId>
-	<version>${hertzbeat.version}</version>
 	<properties>
 		<spring-ai.version>1.1.1</spring-ai.version>
 		<java.version>21</java.version>
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java
similarity index 55%
copy from hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
copy to hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java
index 8bbc0fa5c1..ba0172cb53 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java
@@ -15,47 +15,28 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.common.observability.dto.handoff;
+package org.apache.hertzbeat.common.observability.dto.entity;
 
+import java.util.Collections;
 import java.util.List;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
-import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.TraceEvidence;
+import org.apache.hertzbeat.common.observability.dto.log.EntityLogQueryHint;
 import org.apache.hertzbeat.common.observability.dto.log.EntityLogSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceQueryHintDto;
 import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceSummaryDto;
-import org.apache.hertzbeat.common.observability.model.ObservedEntityContext;
 
 /**
- * Request payload for building entity response handoffs.
+ * Shared entity evidence payload consumed by signal handoffs and entity detail surfaces.
  */
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-public class EntityResponseHandoffsRequest {
-
-    private String returnTo;
-
-    private String returnLabel;
-
-    private String entityOwner;
-
-    private String entitySystem;
-
-    private String entityEnvironment;
-
-    private String entitySource;
-
-    private ObservedEntityContext entityContext;
-
-    private List<SingleAlert> activeAlerts;
-
-    private List<Monitor> monitors;
+public class EntitySignalEvidenceBundle {
 
     private EntityLogSummaryInfo logSummary;
 
@@ -67,13 +48,30 @@ public class EntityResponseHandoffsRequest {
 
     private List<TraceEvidence> traceEvidence;
 
-    private List<EntityTraceQueryHintDto> traceQueryHints;
-
-    private boolean ownerReady;
-
-    private boolean runbookReady;
+    private List<EntityLogQueryHint> logQueryHints;
 
-    private boolean relationReady;
+    private List<EntityTraceQueryHintDto> traceQueryHints;
 
-    private boolean telemetryReady;
+    private EntityUnifiedEvidenceSummary unifiedEvidenceSummary;
+
+    private EntityTriageRecommendation triageRecommendation;
+
+    public static EntitySignalEvidenceBundle from(EntityObservabilityDetailBundle detailBundle) {
+        if (detailBundle == null) {
+            return new EntitySignalEvidenceBundle(
+                    null, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
+                    Collections.emptyList(), Collections.emptyList(), null, null);
+        }
+        return new EntitySignalEvidenceBundle(
+                detailBundle.getLogSummary(),
+                detailBundle.getTraceSummary(),
+                detailBundle.getMetricEvidence(),
+                detailBundle.getLogEvidence(),
+                detailBundle.getTraceEvidence(),
+                detailBundle.getLogQueryHints(),
+                detailBundle.getTraceQueryHints(),
+                detailBundle.getUnifiedEvidenceSummary(),
+                detailBundle.getTriageRecommendation()
+        );
+    }
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
index 8bbc0fa5c1..99e9f64326 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java
@@ -23,6 +23,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.TraceEvidence;
@@ -69,6 +70,8 @@ public class EntityResponseHandoffsRequest {
 
     private List<EntityTraceQueryHintDto> traceQueryHints;
 
+    private EntitySignalEvidenceBundle signalEvidence;
+
     private boolean ownerReady;
 
     private boolean runbookReady;
@@ -76,4 +79,29 @@ public class EntityResponseHandoffsRequest {
     private boolean relationReady;
 
     private boolean telemetryReady;
+
+    public EntityResponseHandoffsRequest(String returnTo,
+                                         String returnLabel,
+                                         String entityOwner,
+                                         String entitySystem,
+                                         String entityEnvironment,
+                                         String entitySource,
+                                         ObservedEntityContext entityContext,
+                                         List<SingleAlert> activeAlerts,
+                                         List<Monitor> monitors,
+                                         EntityLogSummaryInfo logSummary,
+                                         EntityTraceSummaryDto traceSummary,
+                                         List<MetricEvidence> metricEvidence,
+                                         List<LogEvidence> logEvidence,
+                                         List<TraceEvidence> traceEvidence,
+                                         List<EntityTraceQueryHintDto> traceQueryHints,
+                                         boolean ownerReady,
+                                         boolean runbookReady,
+                                         boolean relationReady,
+                                         boolean telemetryReady) {
+        this(returnTo, returnLabel, entityOwner, entitySystem, entityEnvironment, entitySource,
+                entityContext, activeAlerts, monitors, logSummary, traceSummary, metricEvidence,
+                logEvidence, traceEvidence, traceQueryHints, null, ownerReady, runbookReady,
+                relationReady, telemetryReady);
+    }
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
index a004b00c4a..9bd509760b 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ObserveEntityDao.java
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.manager.dao;
 import java.util.List;
 import java.util.Optional;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -39,4 +40,6 @@ public interface ObserveEntityDao extends JpaRepository<ObserveEntity, Long>, Jp
     Optional<ObserveEntity> findFirstByWorkspaceIdAndTypeAndName(String workspaceId, String type, String name);
 
     List<ObserveEntity> findAllByWorkspaceId(String workspaceId, Sort sort);
+
+    List<ObserveEntity> findAllByWorkspaceId(String workspaceId, Pageable pageable);
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java
index 1a79548e36..3b8f9c93a1 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java
@@ -27,6 +27,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityEvidenceSummar
 import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -69,6 +70,8 @@ public class EntityDetailDto {
 
     private List<TraceEvidence> traceEvidence;
 
+    private EntitySignalEvidenceBundle signalEvidence;
+
     private EntityUnifiedEvidenceSummary unifiedEvidenceSummary;
 
     private EntityTriageRecommendation triageRecommendation;
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java
index 294797e37e..56d8907c5f 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java
@@ -27,6 +27,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummary
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -130,6 +131,7 @@ public class EntityDetailObservabilityReadModelService {
         EntityLogSummaryInfo logSummary = entityObservabilityGateway.buildEntityLogSummary(logQueryHints);
         EntityObservabilityDetailBundle observabilityDetail = entityObservabilityGateway.resolveEntityDetailBundle(
                 entityContext, statusInfo, evidenceSummary, monitorSummary, logSummary, monitors, logQueryHints);
+        EntitySignalEvidenceBundle signalEvidence = EntitySignalEvidenceBundle.from(observabilityDetail);
         logSummary = observabilityDetail.getLogSummary();
         logQueryHints = observabilityDetail.getLogQueryHints();
         EntityTraceSummaryDto traceSummary = observabilityDetail.getTraceSummary();
@@ -147,10 +149,8 @@ public class EntityDetailObservabilityReadModelService {
                         entityDto.getEntity(), evidenceSummary, logSummary, opsSummary);
         EntityStatusPageSummaryInfo statusPageSummary =
                 entityObservabilityGateway.buildEntityStatusPageSummary(entity, opsSummary);
-        EntityResponseHandoffsInfo responseHandoffs =
-                entityResponseHandoffReadModelService.buildResponseHandoffs(
-                        entityId, entityContext, activeAlerts, monitors, logSummary, traceSummary,
-                        metricEvidence, logEvidence, traceEvidence, traceQueryHints, opsSummary);
+        EntityResponseHandoffsInfo responseHandoffs = entityResponseHandoffReadModelService.buildResponseHandoffs(
+                entityId, entityContext, activeAlerts, monitors, signalEvidence, opsSummary);
         EntityDetailDto.EntityNoiseControlSummaryInfo noiseControlSummary =
                 resolveRequestWorkspaceAtEvidenceBoundaries
                         ? entityNoiseControlReadModelService.buildNoiseControlSummary(entityDto, monitors, activeAlerts)
@@ -161,7 +161,7 @@ public class EntityDetailObservabilityReadModelService {
                         ? entityActivityReadModelService.getDefinitionActivities(entityId, 12)
                         : entityActivityReadModelService.getDefinitionActivities(entityId, 12, requestWorkspaceId);
         return new EntityDetailDto(entityDto, statusInfo, evidenceSummary, alertSummary, monitorSummary, logSummary,
-                traceSummary, metricEvidence, logEvidence, traceEvidence, unifiedEvidenceSummary,
+                traceSummary, metricEvidence, logEvidence, traceEvidence, signalEvidence, unifiedEvidenceSummary,
                 triageRecommendation, opsSummary, nextActions, statusPageSummary, responseHandoffs,
                 noiseControlSummary, boundMonitors, activeAlerts, logQueryHints, traceQueryHints,
                 definitionActivities);
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java
index 924e62d8f5..bd04b12ca7 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java
@@ -21,6 +21,7 @@ import java.util.List;
 import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence;
 import org.apache.hertzbeat.common.observability.dto.evidence.TraceEvidence;
@@ -49,13 +50,11 @@ public class EntityResponseHandoffReadModelService {
                                                             ObservedEntityContext entityContext,
                                                             List<SingleAlert> activeAlerts,
                                                             List<Monitor> monitors,
-                                                            EntityLogSummaryInfo logSummary,
-                                                            EntityTraceSummaryDto traceSummary,
-                                                            List<MetricEvidence> metricEvidence,
-                                                            List<LogEvidence> logEvidence,
-                                                            List<TraceEvidence> traceEvidence,
-                                                            List<EntityTraceQueryHintDto> traceQueryHints,
+                                                            EntitySignalEvidenceBundle signalEvidence,
                                                             EntityOpsSummaryInfo opsSummary) {
+        EntitySignalEvidenceBundle safeEvidence = signalEvidence == null
+                ? new EntitySignalEvidenceBundle()
+                : signalEvidence;
         return entityObservabilityGateway.buildEntityResponseHandoffs(new EntityResponseHandoffsRequest(
                 "/entities/" + entityId,
                 entityObservabilityGateway.buildEntityReturnLabel(entityContext),
@@ -67,16 +66,34 @@ public class EntityResponseHandoffReadModelService {
                 entityContext,
                 activeAlerts,
                 monitors,
-                logSummary,
-                traceSummary,
-                metricEvidence,
-                logEvidence,
-                traceEvidence,
-                traceQueryHints,
+                safeEvidence.getLogSummary(),
+                safeEvidence.getTraceSummary(),
+                safeEvidence.getMetricEvidence(),
+                safeEvidence.getLogEvidence(),
+                safeEvidence.getTraceEvidence(),
+                safeEvidence.getTraceQueryHints(),
+                safeEvidence,
                 opsSummary != null && opsSummary.isOwnerReady(),
                 opsSummary != null && opsSummary.isRunbookReady(),
                 opsSummary != null && opsSummary.isRelationReady(),
                 opsSummary != null && opsSummary.isTelemetryReady()
         ));
     }
+
+    public EntityResponseHandoffsInfo buildResponseHandoffs(long entityId,
+                                                            ObservedEntityContext entityContext,
+                                                            List<SingleAlert> activeAlerts,
+                                                            List<Monitor> monitors,
+                                                            EntityLogSummaryInfo logSummary,
+                                                            EntityTraceSummaryDto traceSummary,
+                                                            List<MetricEvidence> metricEvidence,
+                                                            List<LogEvidence> logEvidence,
+                                                            List<TraceEvidence> traceEvidence,
+                                                            List<EntityTraceQueryHintDto> traceQueryHints,
+                                                            EntityOpsSummaryInfo opsSummary) {
+        return buildResponseHandoffs(entityId, entityContext, activeAlerts, monitors,
+                new EntitySignalEvidenceBundle(logSummary, traceSummary, metricEvidence, logEvidence, traceEvidence,
+                        null, traceQueryHints, null, null),
+                opsSummary);
+    }
 }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java
index 51de579a8f..fc5a62141f 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java
@@ -33,6 +33,7 @@ import org.apache.hertzbeat.common.entity.manager.EntityRelation;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
 import org.apache.hertzbeat.manager.pojo.dto.EntityTopologyGraphInfo;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
@@ -45,7 +46,7 @@ public class EntityTopologyQueryService {
 
     private static final int DEFAULT_DEPTH = 1;
     private static final int MAX_DEPTH = 2;
-    private static final int DEFAULT_TOPOLOGY_ENTITY_LIMIT = 12;
+    private static final int DEFAULT_TOPOLOGY_ENTITY_LIMIT = 64;
     private static final int MAX_TOPOLOGY_EDGE_PAGE_SIZE = 200;
     private static final int IMPACT_TIMELINE_LIMIT = 12;
     private static final String SOURCE_KIND_ENTITY_RELATION = "entity-relation";
@@ -111,7 +112,7 @@ public class EntityTopologyQueryService {
         graph.setApiBacked(true);
         graph.setFocusEntityId(focusEntityId);
         graph.setDepth(depth);
-        graph.setSourceKinds(List.of(normalizedSourceKind));
+        graph.setSourceKinds(List.of());
         if (focusEntityId == null) {
             return buildDefaultTopologyGraph(graph, depth, environment, normalizedSourceKind,
                     sourceSelection, start, end, normalizedRelationType, hideInternal, pageIndex, pageSize);
@@ -145,10 +146,10 @@ public class EntityTopologyQueryService {
                 ? collectMonitorBinds(entityById.keySet())
                 : List.of();
         Map<Long, Monitor> monitorById = monitorMap(monitorBinds);
-        graph.setSourceKinds(buildSourceKinds(normalizedSourceKind, monitorBinds, traceCallEdges));
         List<EntityTopologyGraphInfo.Edge> visibleEdges = filterAndPageEdges(
                 buildEdges(relations, entityById.keySet(), monitorBinds, monitorById, traceCallEdges),
                 normalizedRelationType, hideInternal, pageIndex, pageSize);
+        graph.setSourceKinds(buildSourceKinds(normalizedSourceKind, visibleEdges));
         graph.setNodes(filterNodesByVisibleEdges(
                 buildNodes(entityById, monitorBinds, monitorById, focusEntityId, traceCallEdges),
                 visibleEdges, focusEntityId, normalizedRelationType, pageSize));
@@ -186,7 +187,7 @@ public class EntityTopologyQueryService {
         }
 
         TraceCallTopologyReadModel traceCallReadModel = sourceSelection.includeTraceCalls()
-                ? traceCallTopologyReadModel(entityById.values(), environment, start, end, hideInternal)
+                ? traceCallTopologyOverviewReadModel(environment, start, end, hideInternal)
                 : TraceCallTopologyReadModel.empty();
         Map<Long, ObserveEntity> traceEntityById = traceCallReadModel.entityById();
         List<TraceCallTopologyEdgeInfo> traceCallEdges = traceCallReadModel.edges();
@@ -200,10 +201,10 @@ public class EntityTopologyQueryService {
                 ? collectMonitorBinds(entityById.keySet())
                 : List.of();
         Map<Long, Monitor> monitorById = monitorMap(monitorBinds);
-        graph.setSourceKinds(buildSourceKinds(sourceKind, monitorBinds, traceCallEdges));
         List<EntityTopologyGraphInfo.Edge> visibleEdges = filterAndPageEdges(
                 buildEdges(relations, entityById.keySet(), monitorBinds, monitorById, traceCallEdges),
                 relationType, hideInternal, pageIndex, pageSize);
+        graph.setSourceKinds(buildSourceKinds(sourceKind, visibleEdges));
         graph.setNodes(filterNodesByVisibleEdges(
                 buildNodes(entityById, monitorBinds, monitorById, null, traceCallEdges),
                 visibleEdges, null, relationType, pageSize));
@@ -214,14 +215,14 @@ public class EntityTopologyQueryService {
 
     private List<ObserveEntity> defaultSeedEntities(String environment) {
         Sort sort = Sort.by(Sort.Order.desc("gmtUpdate"), Sort.Order.desc("id"));
-        List<ObserveEntity> entities = entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(sort);
+        List<ObserveEntity> entities = entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(
+                PageRequest.of(0, DEFAULT_TOPOLOGY_ENTITY_LIMIT, sort));
         if (entities == null) {
             return List.of();
         }
         return entities.stream()
                 .filter(entity -> entity.getId() != null)
                 .filter(entity -> matchesEnvironment(entity, environment))
-                .limit(DEFAULT_TOPOLOGY_ENTITY_LIMIT)
                 .toList();
     }
 
@@ -235,6 +236,15 @@ public class EntityTopologyQueryService {
         return readModel == null ? TraceCallTopologyReadModel.empty() : readModel;
     }
 
+    private TraceCallTopologyReadModel traceCallTopologyOverviewReadModel(String environment,
+                                                                          Long start,
+                                                                          Long end,
+                                                                          Boolean hideInternal) {
+        TraceCallTopologyReadModel readModel = traceCallTopologyQueryService.findTraceCallEdgesForOverview(
+                environment, start, end, hideInternal == null || hideInternal);
+        return readModel == null ? TraceCallTopologyReadModel.empty() : readModel;
+    }
+
     private TopologySourceSelection sourceSelection(String sourceKind) {
         if (!StringUtils.hasText(sourceKind)) {
             return TopologySourceSelection.all();
@@ -329,19 +339,41 @@ public class EntityTopologyQueryService {
     }
 
     private List<String> buildSourceKinds(String requestedSourceKind,
-                                          Collection<EntityMonitorBind> monitorBinds,
-                                          Collection<TraceCallTopologyEdgeInfo> traceCallEdges) {
+                                          Collection<EntityTopologyGraphInfo.Edge> visibleEdges) {
+        if (visibleEdges == null || visibleEdges.isEmpty()) {
+            return List.of();
+        }
         Set<String> sourceKinds = new LinkedHashSet<>();
         sourceKinds.add(requestedSourceKind);
-        if (!monitorBinds.isEmpty()) {
-            sourceKinds.add(SOURCE_KIND_MONITOR_BIND);
-        }
-        if (!traceCallEdges.isEmpty()) {
-            sourceKinds.add(SOURCE_KIND_OTLP_TRACE_CALL);
+        for (EntityTopologyGraphInfo.Edge edge : visibleEdges) {
+            if (edge == null) {
+                continue;
+            }
+            addSourceKindsFromEdgeEvidence(sourceKinds, edge.getEvidenceBadges());
+            addSourceKindFromEdgeSource(sourceKinds, edge.getRelationSource());
         }
         return new ArrayList<>(sourceKinds);
     }
 
+    private void addSourceKindsFromEdgeEvidence(Set<String> sourceKinds, Collection<String> evidenceBadges) {
+        if (evidenceBadges == null) {
+            return;
+        }
+        evidenceBadges.forEach(source -> addSourceKindFromEdgeSource(sourceKinds, source));
+    }
+
+    private void addSourceKindFromEdgeSource(Set<String> sourceKinds, String source) {
+        if (!StringUtils.hasText(source)) {
+            return;
+        }
+        String normalized = source.trim().toLowerCase();
+        if (SOURCE_KIND_ENTITY_RELATION.equals(normalized)
+                || SOURCE_KIND_MONITOR_BIND.equals(normalized)
+                || SOURCE_KIND_OTLP_TRACE_CALL.equals(normalized)) {
+            sourceKinds.add(normalized);
+        }
+    }
+
     private Set<Long> collectEntityIds(Long focusEntityId, Collection<EntityRelation> relations) {
         return collectEntityIds(List.of(focusEntityId), relations);
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessService.java
index a735c8dea7..a71d0bf0cd 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessService.java
@@ -27,6 +27,7 @@ import java.util.Set;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
 import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
 import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
@@ -77,10 +78,20 @@ public class EntityWorkspaceAccessService {
         return filterEntitiesByRequestWorkspace(entities, normalizedWorkspaceId);
     }
 
+    public List<ObserveEntity> findAccessibleEntities(String requestWorkspaceId, Pageable pageable) {
+        String normalizedWorkspaceId = normalizeWorkspaceId(requestWorkspaceId);
+        List<ObserveEntity> entities = entityWorkspaceQueryService.findEntities(normalizedWorkspaceId, pageable);
+        return filterEntitiesByRequestWorkspace(entities, normalizedWorkspaceId);
+    }
+
     public List<ObserveEntity> findAccessibleEntitiesForRequestWorkspace(Sort sort) {
         return findAccessibleEntities(currentRequestWorkspaceId(), sort);
     }
 
+    public List<ObserveEntity> findAccessibleEntitiesForRequestWorkspace(Pageable pageable) {
+        return findAccessibleEntities(currentRequestWorkspaceId(), pageable);
+    }
+
     public Optional<ObserveEntity> findEntityById(long entityId) {
         return entityWorkspaceQueryService.findEntityById(entityId);
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryService.java
index 3a467c2c48..25fa541198 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryService.java
@@ -52,6 +52,13 @@ public class EntityWorkspaceQueryService {
         return observeEntityDao.findAll(sort);
     }
 
+    public List<ObserveEntity> findEntities(String workspaceId, Pageable pageable) {
+        if (StringUtils.hasText(workspaceId)) {
+            return observeEntityDao.findAllByWorkspaceId(workspaceId, pageable);
+        }
+        return observeEntityDao.findAll(pageable).getContent();
+    }
+
     public Page<ObserveEntity> findEntityPage(Specification<ObserveEntity> specification, Pageable pageable) {
         return observeEntityDao.findAll(specification, pageable);
     }
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeeder.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeeder.java
new file mode 100644
index 0000000000..1b2e965a4b
--- /dev/null
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeeder.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
+import org.apache.hertzbeat.manager.dao.EntityRelationDao;
+import org.apache.hertzbeat.manager.dao.ObserveEntityDao;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Seeds local-only demo topology relations so fresh local H2 catalogs do not render node-only graphs.
+ */
+@Component
+@Profile("local")
+@Slf4j
+public class LocalTopologyDemoRelationSeeder implements ApplicationRunner {
+
+    private static final String MIXED_SCALE_PROOF_SEED_OPTION = "hertzbeat.topology.local-scale-proof-seed";
+    private static final String WORKSPACE_ID = AuthTokenScopes.DEFAULT_WORKSPACE_ID;
+    private static final String NAMESPACE = "commerce";
+    private static final String ENVIRONMENT = "prod";
+    private static final String SEED_NAME = "local-topology-demo-seed";
+    private static final String MIXED_PROOF_BATCH = "hb-mix-1780329856";
+    private static final String MIXED_PROOF_NAMESPACE = "scale-mix";
+    private static final long MIXED_PROOF_ENTITY_ID_BASE = 646566130000000L;
+
+    private final ObserveEntityDao observeEntityDao;
+    private final EntityRelationDao entityRelationDao;
+    private final EntityIdentityDao entityIdentityDao;
+
+    public LocalTopologyDemoRelationSeeder(ObserveEntityDao observeEntityDao,
+                                           EntityRelationDao entityRelationDao,
+                                           EntityIdentityDao entityIdentityDao) {
+        this.observeEntityDao = observeEntityDao;
+        this.entityRelationDao = entityRelationDao;
+        this.entityIdentityDao = entityIdentityDao;
+    }
+
+    @Override
+    @Transactional
+    public void run(ApplicationArguments args) {
+        seedDemoRelations();
+        if (isMixedScaleProofSeedEnabled(args)) {
+            seedMixedScaleProofEntities();
+        } else {
+            log.info("Skipped local mixed topology scale proof seed. Enable with --{}=true.",
+                    MIXED_SCALE_PROOF_SEED_OPTION);
+        }
+    }
+
+    boolean isMixedScaleProofSeedEnabled(ApplicationArguments args) {
+        if (args == null || !args.containsOption(MIXED_SCALE_PROOF_SEED_OPTION)) {
+            return false;
+        }
+        List<String> values = args.getOptionValues(MIXED_SCALE_PROOF_SEED_OPTION);
+        return values != null && values.stream().anyMatch(value -> "true".equalsIgnoreCase(value));
+    }
+
+    void seedDemoRelations() {
+        Optional<ObserveEntity> checkout = findDemoEntity("service", "Checkout API");
+        Optional<ObserveEntity> payment = findDemoEntity("service", "Payment API");
+        Optional<ObserveEntity> orders = findDemoEntity("database", "Orders DB");
+        if (checkout.isEmpty() || payment.isEmpty() || orders.isEmpty()) {
+            return;
+        }
+
+        List<EntityRelation> missingRelations = new ArrayList<>();
+        if (!relationExists(checkout.get().getId(), payment.get().getId(), "trace-call")) {
+            missingRelations.add(EntityRelation.builder()
+                    .sourceEntityId(checkout.get().getId())
+                    .targetEntityId(payment.get().getId())
+                    .targetRef("service:commerce/Payment API")
+                    .relationType("trace-call")
+                    .relationSource("otlp-trace-call")
+                    .status("confirmed")
+                    .score(96)
+                    .description("Checkout API calls Payment API in the local topology demo.")
+                    .attributes(seedAttributes("outbound"))
+                    .build());
+        }
+        if (!relationExists(payment.get().getId(), orders.get().getId(), "depends_on")) {
+            missingRelations.add(EntityRelation.builder()
+                    .sourceEntityId(payment.get().getId())
+                    .targetEntityId(orders.get().getId())
+                    .targetRef("database:commerce/Orders DB")
+                    .relationType("depends_on")
+                    .relationSource("cmdb-manual-label")
+                    .status("confirmed")
+                    .score(94)
+                    .description("Payment API depends on Orders DB in the local topology demo.")
+                    .attributes(seedAttributes("outbound"))
+                    .build());
+        }
+        if (!missingRelations.isEmpty()) {
+            entityRelationDao.saveAll(missingRelations);
+            log.info("Seeded {} local topology demo relation(s).", missingRelations.size());
+        }
+    }
+
+    void seedMixedScaleProofEntities() {
+        List<ObserveEntity> missingEntities = new ArrayList<>();
+        List<EntityIdentity> missingIdentities = new ArrayList<>();
+        List<String> serviceNames = mixedScaleProofServiceNames();
+
+        for (int index = 0; index < serviceNames.size(); index += 1) {
+            String serviceName = serviceNames.get(index);
+            Optional<ObserveEntity> existingEntity = findMixedScaleProofEntity(serviceName);
+            if (existingEntity.isPresent()) {
+                if (!hasServiceNameIdentity(existingEntity.get().getId(), serviceName)) {
+                    missingIdentities.add(serviceNameIdentity(existingEntity.get().getId(), serviceName));
+                }
+                continue;
+            }
+            long entityId = MIXED_PROOF_ENTITY_ID_BASE + index;
+            missingEntities.add(ObserveEntity.builder()
+                    .id(entityId)
+                    .workspaceId(WORKSPACE_ID)
+                    .type("service")
+                    .namespace(MIXED_PROOF_NAMESPACE)
+                    .name(serviceName)
+                    .displayName(displayName(serviceName))
+                    .environment(ENVIRONMENT)
+                    .status("unknown")
+                    .criticality("medium")
+                    .owner("local-scale-proof")
+                    .lifecycle("local")
+                    .build());
+            missingIdentities.add(serviceNameIdentity(entityId, serviceName));
+        }
+
+        if (!missingEntities.isEmpty()) {
+            observeEntityDao.saveAll(missingEntities);
+            log.info("Seeded {} local mixed topology scale proof entity row(s).", missingEntities.size());
+        }
+        if (!missingIdentities.isEmpty()) {
+            entityIdentityDao.saveAll(missingIdentities);
+            log.info("Seeded {} local mixed topology scale proof service.name identity row(s).",
+                    missingIdentities.size());
+        }
+    }
+
+    private boolean hasServiceNameIdentity(Long entityId, String serviceName) {
+        return entityIdentityDao.findAllByEntityIdOrderByPriorityDescIdAsc(entityId)
+                .stream()
+                .anyMatch(identity -> "service.name".equals(identity.getIdentityKey())
+                        && serviceName.equals(identity.getIdentityValue()));
+    }
+
+    private EntityIdentity serviceNameIdentity(Long entityId, String serviceName) {
+        return EntityIdentity.builder()
+                .entityId(entityId)
+                .identityType("otel_resource")
+                .identityKey("service.name")
+                .identityValue(serviceName)
+                .normalizedValue(serviceName.toLowerCase(java.util.Locale.ROOT))
+                .priority(100)
+                .primaryIdentity(true)
+                .build();
+    }
+
+    private Optional<ObserveEntity> findDemoEntity(String type, String name) {
+        return observeEntityDao
+                .findFirstByWorkspaceIdAndTypeAndNamespaceAndName(WORKSPACE_ID, type, NAMESPACE, name)
+                .filter(entity -> ENVIRONMENT.equals(entity.getEnvironment()));
+    }
+
+    private Optional<ObserveEntity> findMixedScaleProofEntity(String name) {
+        return observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                WORKSPACE_ID, "service", MIXED_PROOF_NAMESPACE, name);
+    }
+
+    private List<String> mixedScaleProofServiceNames() {
+        List<String> serviceNames = new ArrayList<>();
+        serviceNames.add(MIXED_PROOF_BATCH + "-edge-gateway");
+        for (int domainIndex = 0; domainIndex < 12; domainIndex += 1) {
+            serviceNames.add("%s-domain-%02d".formatted(MIXED_PROOF_BATCH, domainIndex));
+        }
+        for (int domainIndex = 0; domainIndex < 12; domainIndex += 1) {
+            for (int serviceIndex = 0; serviceIndex < 165; serviceIndex += 1) {
+                serviceNames.add("%s-svc-%02d-%03d".formatted(MIXED_PROOF_BATCH, domainIndex, serviceIndex));
+            }
+        }
+        return serviceNames;
+    }
+
+    private String displayName(String serviceName) {
+        if (serviceName.equals(MIXED_PROOF_BATCH + "-edge-gateway")) {
+            return "Mixed Edge Gateway";
+        }
+        if (serviceName.contains("-domain-")) {
+            return "Domain " + serviceName.substring(serviceName.lastIndexOf('-') + 1) + " Gateway";
+        }
+        return serviceName;
+    }
+
+    private boolean relationExists(Long sourceEntityId, Long targetEntityId, String relationType) {
+        return entityRelationDao.findBySourceEntityIdOrTargetEntityId(sourceEntityId, sourceEntityId)
+                .stream()
+                .anyMatch(relation -> sourceEntityId.equals(relation.getSourceEntityId())
+                        && targetEntityId.equals(relation.getTargetEntityId())
+                        && relationType.equals(relation.getRelationType()));
+    }
+
+    private Map<String, String> seedAttributes(String direction) {
+        return Map.of(
+                "seed", SEED_NAME,
+                "direction", direction
+        );
+    }
+}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java
index dd02aa33c7..50e9a5002f 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java
@@ -102,14 +102,30 @@ public class TraceCallTopologyQueryService {
         if (CollectionUtils.isEmpty(seedEntities)) {
             return TraceCallTopologyReadModel.empty();
         }
+        return findTraceCallEdges(seedEntities, environment, start, end, hideInternal, true);
+    }
+
+    public TraceCallTopologyReadModel findTraceCallEdgesForOverview(String environment,
+                                                                    Long start,
+                                                                    Long end,
+                                                                    Boolean hideInternal) {
+        return findTraceCallEdges(Collections.emptyList(), environment, start, end, hideInternal, false);
+    }
+
+    private TraceCallTopologyReadModel findTraceCallEdges(Collection<ObserveEntity> seedEntities,
+                                                         String environment,
+                                                         Long start,
+                                                         Long end,
+                                                         Boolean hideInternal,
+                                                         boolean scopeToSeedEntities) {
         boolean shouldHideInternal = hideInternal == null || hideInternal;
         TraceCallTopologyReadModel serviceGraphReadModel =
-                findServiceGraphReadModel(seedEntities, environment, start, end, shouldHideInternal);
+                findServiceGraphReadModel(seedEntities, environment, start, end, shouldHideInternal,
+                        scopeToSeedEntities);
         if (!serviceGraphReadModel.edges().isEmpty()) {
             return serviceGraphReadModel;
         }
-        List<TraceSpanRow> spans = safeList(traceQueryRepository.queryRecentTraceRows(
-                        TRACE_ROW_LIMIT, start, end, null, environment, shouldHideInternal))
+        List<TraceSpanRow> spans = queryRecentTraceRows(start, end, environment, shouldHideInternal)
                 .stream()
                 .map(this::toTraceSpanRow)
                 .filter(Objects::nonNull)
@@ -128,7 +144,7 @@ public class TraceCallTopologyQueryService {
         Map<Long, ObserveEntity> entityById = new LinkedHashMap<>();
         entityByService.values().forEach(entity -> entityById.putIfAbsent(entity.getId(), entity));
         List<TraceCallTopologyEdgeInfo> edges = buildTraceCallEdgesFromSpans(
-                spans, entityByService, seedEntities, start, end);
+                spans, entityByService, seedEntities, start, end, scopeToSeedEntities);
         return new TraceCallTopologyReadModel(entityById, edges);
     }
 
@@ -136,8 +152,9 @@ public class TraceCallTopologyQueryService {
                                                                  String environment,
                                                                  Long start,
                                                                  Long end,
-                                                                 Boolean hideInternal) {
-        Set<String> seedServiceNames = seedServiceNames(seedEntities);
+                                                                 Boolean hideInternal,
+                                                                 boolean scopeToSeedEntities) {
+        Set<String> seedServiceNames = scopeToSeedEntities ? seedServiceNames(seedEntities) : Collections.emptySet();
         List<TraceServiceGraphRow> serviceGraphRows = queryServiceGraphRows(
                 start, end, environment, seedServiceNames, hideInternal);
         if (serviceGraphRows.isEmpty()) {
@@ -151,7 +168,7 @@ public class TraceCallTopologyQueryService {
         Map<String, ObserveEntity> entityByService = resolveEntityByService(seedEntities, observedServices,
                 environment);
         List<TraceCallTopologyEdgeInfo> edges = buildTraceCallEdgesFromServiceGraphRows(
-                serviceGraphRows, entityByService, seedEntities, start, end);
+                serviceGraphRows, entityByService, seedEntities, start, end, scopeToSeedEntities);
         Map<Long, ObserveEntity> entityById = new LinkedHashMap<>();
         for (TraceCallTopologyEdgeInfo edge : edges) {
             ObserveEntity source = findEntityById(entityByService.values(), edge.sourceEntityId());
@@ -187,6 +204,22 @@ public class TraceCallTopologyQueryService {
         }
     }
 
+    private List<Map<String, Object>> queryRecentTraceRows(Long start,
+                                                           Long end,
+                                                           String environment,
+                                                           Boolean hideInternal) {
+        try {
+            return CompletableFuture.supplyAsync(() -> safeList(traceQueryRepository.queryRecentTraceRows(
+                            TRACE_ROW_LIMIT, start, end, null, environment, hideInternal)))
+                    .get(serviceGraphQueryTimeout.toMillis(), TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            return Collections.emptyList();
+        } catch (ExecutionException | TimeoutException | RuntimeException ex) {
+            return Collections.emptyList();
+        }
+    }
+
     private Set<String> seedServiceNames(Collection<ObserveEntity> seedEntities) {
         Set<String> serviceNames = new LinkedHashSet<>();
         for (ObserveEntity entity : safeList(seedEntities)) {
@@ -305,7 +338,8 @@ public class TraceCallTopologyQueryService {
                                                                          Map<String, ObserveEntity> entityByService,
                                                                          Collection<ObserveEntity> seedEntities,
                                                                          Long start,
-                                                                         Long end) {
+                                                                         Long end,
+                                                                         boolean scopeToSeedEntities) {
         Map<String, TraceSpanRow> spanByTraceAndSpan = new LinkedHashMap<>();
         for (TraceSpanRow span : spans) {
             spanByTraceAndSpan.put(spanKey(span.traceId(), span.spanId()), span);
@@ -329,7 +363,9 @@ public class TraceCallTopologyQueryService {
             if (source == null || target == null || Objects.equals(source.getId(), target.getId())) {
                 continue;
             }
-            if (!seedEntityIds.contains(source.getId()) && !seedEntityIds.contains(target.getId())) {
+            if (scopeToSeedEntities
+                    && !seedEntityIds.contains(source.getId())
+                    && !seedEntityIds.contains(target.getId())) {
                 continue;
             }
             String edgeKey = source.getId() + ":" + target.getId();
@@ -346,7 +382,8 @@ public class TraceCallTopologyQueryService {
             Map<String, ObserveEntity> entityByService,
             Collection<ObserveEntity> seedEntities,
             Long start,
-            Long end) {
+            Long end,
+            boolean scopeToSeedEntities) {
         Set<Long> seedEntityIds = seedEntities.stream()
                 .filter(Objects::nonNull)
                 .map(ObserveEntity::getId)
@@ -359,7 +396,9 @@ public class TraceCallTopologyQueryService {
             if (source == null || target == null || Objects.equals(source.getId(), target.getId())) {
                 continue;
             }
-            if (!seedEntityIds.contains(source.getId()) && !seedEntityIds.contains(target.getId())) {
+            if (scopeToSeedEntities
+                    && !seedEntityIds.contains(source.getId())
+                    && !seedEntityIds.contains(target.getId())) {
                 continue;
             }
             edges.add(toServiceGraphEdge(row, source.getId(), target.getId(), start, end));
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
index 05a24d50c8..cb95e56700 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java
@@ -42,6 +42,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummary
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -206,8 +207,7 @@ class EntityDetailObservabilityReadModelServiceTest {
                 .thenReturn(statusPageSummary);
         when(entityResponseHandoffReadModelService.buildResponseHandoffs(
                 eq(501L), any(ObservedEntityContext.class), same(activeAlerts), same(monitors),
-                same(enrichedLogSummary), same(traceSummary), same(metricEvidence), same(logEvidence),
-                same(traceEvidence), same(traceQueryHints), same(opsSummary))).thenReturn(handoffs);
+                any(EntitySignalEvidenceBundle.class), same(opsSummary))).thenReturn(handoffs);
         when(entityNoiseControlReadModelService.buildNoiseControlSummary(
                 same(entityDto), same(monitors), same(activeAlerts), eq("team-a")))
                 .thenReturn(noiseSummary);
@@ -223,6 +223,8 @@ class EntityDetailObservabilityReadModelServiceTest {
         assertSame(monitorSummary, detail.getMonitorSummary());
         assertSame(enrichedLogSummary, detail.getLogSummary());
         assertSame(traceSummary, detail.getTraceSummary());
+        assertEquals(enrichedLogSummary, detail.getSignalEvidence().getLogSummary());
+        assertEquals(traceSummary, detail.getSignalEvidence().getTraceSummary());
         assertSame(unifiedSummary, detail.getUnifiedEvidenceSummary());
         assertSame(triage, detail.getTriageRecommendation());
         assertSame(opsSummary, detail.getOpsSummary());
@@ -235,6 +237,20 @@ class EntityDetailObservabilityReadModelServiceTest {
         assertSame(enrichedLogHints, detail.getLogQueryHints());
         assertSame(traceQueryHints, detail.getTraceQueryHints());
         assertSame(definitionActivities, detail.getDefinitionActivities());
+        verify(entityResponseHandoffReadModelService).buildResponseHandoffs(
+                eq(501L), any(ObservedEntityContext.class), same(activeAlerts), same(monitors),
+                org.mockito.ArgumentMatchers.argThat(bundle ->
+                        bundle != null
+                                && bundle.getLogSummary() == enrichedLogSummary
+                                && bundle.getTraceSummary() == traceSummary
+                                && bundle.getMetricEvidence() == metricEvidence
+                                && bundle.getLogEvidence() == logEvidence
+                                && bundle.getTraceEvidence() == traceEvidence
+                                && bundle.getLogQueryHints() == enrichedLogHints
+                                && bundle.getTraceQueryHints() == traceQueryHints
+                                && bundle.getUnifiedEvidenceSummary() == unifiedSummary
+                                && bundle.getTriageRecommendation() == triage),
+                same(opsSummary));
     }
 
     @Test
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryServiceTest.java
index 9622b5489b..03b768864c 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryServiceTest.java
@@ -45,6 +45,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 
 /**
@@ -188,7 +189,7 @@ class EntityTopologyQueryServiceTest {
                 .status((byte) 1)
                 .build();
 
-        lenient().when(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(any(Sort.class)))
+        lenient().when(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(any(Pageable.class)))
                 .thenReturn(List.of(checkout, payment));
         lenient().when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(argThat(ids ->
                 ids != null && ids.containsAll(List.of(10L, 20L))))).thenReturn(List.of(checkout, payment));
@@ -217,7 +218,83 @@ class EntityTopologyQueryServiceTest {
                 Long.valueOf(501L).equals(edge.getRelationId())
                         && "10".equals(edge.getSourceNodeId())
                         && "monitor:701".equals(edge.getTargetNodeId())));
-        verify(entityWorkspaceAccessService).findAccessibleEntitiesForRequestWorkspace(any(Sort.class));
+        verify(entityWorkspaceAccessService).findAccessibleEntitiesForRequestWorkspace(argThat((Pageable pageable) ->
+                pageable != null
+                        && pageable.isPaged()
+                        && pageable.getPageSize() == 64
+                        && pageable.getSort().getOrderFor("gmtUpdate") != null
+                        && pageable.getSort().getOrderFor("id") != null));
+        verify(entityWorkspaceAccessService, never()).findAccessibleEntitiesForRequestWorkspace(any(Sort.class));
+    }
+
+    @Test
+    void queriesDefaultTraceOverviewWithoutSeedScopeSoLargeGraphsAreNotTruncated() {
+        List<ObserveEntity> seeds = new java.util.ArrayList<>();
+        seeds.add(entity(1L, "service", "hb-mix-1780329856-edge-gateway", "scale-mix", "prod", "unknown"));
+        for (int index = 0; index < 12; index++) {
+            seeds.add(entity(100L + index, "service",
+                    "hb-mix-1780329856-domain-%02d".formatted(index), "scale-mix", "prod", "unknown"));
+        }
+        TraceCallTopologyEdgeInfo traceEdge = new TraceCallTopologyEdgeInfo(
+                "trace-call:100:200:trace-domain-00",
+                100L,
+                200L,
+                "trace-domain-00",
+                "span-domain-00",
+                "CALL hb-mix-1780329856-svc-00-000",
+                "2026-05-20T03:01:00Z",
+                "2026-05-20T03:08:00Z",
+                "ok",
+                100,
+                new TraceCallTopologyEdgeInfo.RedMetrics(1D / 3600D, 1L, 0D, 0L, 42D, 21D)
+        );
+        ObserveEntity leaf = entity(200L, "service", "hb-mix-1780329856-svc-00-000",
+                "scale-mix", "prod", "healthy");
+
+        when(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(any(Pageable.class)))
+                .thenReturn(seeds);
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(argThat(ids ->
+                ids != null && ids.contains(1L) && ids.contains(100L) && ids.contains(111L))))
+                .thenReturn(seeds);
+        when(traceCallTopologyQueryService.findTraceCallEdgesForOverview(
+                eq("prod"), eq(1710000000000L), eq(1710003600000L), eq(true))).thenReturn(
+                        new TraceCallTopologyReadModel(Map.of(200L, leaf), List.of(traceEdge)));
+
+        EntityTopologyGraphInfo graph = entityTopologyQueryService.buildFocusedTopology(
+                null, 2, "prod", "otlp-trace-call", 1710000000000L, 1710003600000L);
+
+        assertEquals(1, graph.getEdges().size());
+        assertTrue(graph.getSourceKinds().contains("otlp-trace-call"));
+        assertTrue(graph.getNodes().stream().anyMatch(node ->
+                "hb-mix-1780329856-svc-00-000".equals(node.getEntityName())));
+        verify(entityWorkspaceAccessService).findAccessibleEntitiesForRequestWorkspace(argThat((Pageable pageable) ->
+                pageable != null
+                        && pageable.isPaged()
+                        && pageable.getPageSize() == 64
+                        && pageable.getSort().getOrderFor("gmtUpdate") != null
+                        && pageable.getSort().getOrderFor("id") != null));
+        verify(traceCallTopologyQueryService, never()).findTraceCallEdges(any(), any(), any(), any(), any());
+    }
+
+    @Test
+    void leavesSourceKindsEmptyWhenDefaultTopologyHasOnlyIsolatedNodes() {
+        ObserveEntity checkout = entity(10L, "service", "checkout-api", "commerce", "prod", "warning");
+        ObserveEntity payment = entity(20L, "service", "payment-api", "commerce", "prod", "healthy");
+
+        when(entityWorkspaceAccessService.findAccessibleEntitiesForRequestWorkspace(any(Pageable.class)))
+                .thenReturn(List.of(checkout, payment));
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(argThat(ids ->
+                ids != null && ids.containsAll(List.of(10L, 20L))))).thenReturn(List.of(checkout, payment));
+        when(entityRelationQueryService.findEntityRelations(10L)).thenReturn(List.of());
+        when(entityRelationQueryService.findEntityRelations(20L)).thenReturn(List.of());
+
+        EntityTopologyGraphInfo graph = entityTopologyQueryService.buildFocusedTopology(
+                null, 1, "prod", "entity-relation");
+
+        assertEquals(List.of(), graph.getSourceKinds());
+        assertEquals(List.of(10L, 20L),
+                graph.getNodes().stream().map(EntityTopologyGraphInfo.Node::getEntityId).toList());
+        assertEquals(List.of(), graph.getEdges());
     }
 
     @Test
@@ -423,7 +500,8 @@ class EntityTopologyQueryServiceTest {
         EntityTopologyGraphInfo graph = entityTopologyQueryService.buildFocusedTopology(
                 10L, 1, "prod", "monitor-bind", 1710000000000L, 1710003600000L);
 
-        assertEquals(List.of("monitor-bind"), graph.getSourceKinds());
+        assertEquals(List.of(), graph.getSourceKinds());
+        assertEquals(List.of(), graph.getEdges());
         verify(traceCallTopologyQueryService, never()).findTraceCallEdges(any(), any(), any(), any(), any());
     }
 
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessServiceTest.java
index ef9d723142..1161ee54b7 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceAccessServiceTest.java
@@ -36,6 +36,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 
 /**
@@ -199,6 +200,29 @@ class EntityWorkspaceAccessServiceTest {
         assertEquals(List.of(teamAlphaEntity), entities);
     }
 
+    @Test
+    void findAccessibleEntitiesForRequestWorkspacePreservesPageableLimit() {
+        Sort sort = Sort.by(Sort.Order.desc("gmtUpdate"), Sort.Order.desc("id"));
+        PageRequest pageable = PageRequest.of(0, 12, sort);
+        ObserveEntity teamAlphaEntity = ObserveEntity.builder()
+                .id(421L)
+                .name("checkout")
+                .workspaceId("team-a")
+                .build();
+        ObserveEntity teamBetaEntity = ObserveEntity.builder()
+                .id(422L)
+                .name("billing")
+                .workspaceId("team-b")
+                .build();
+        when(entityWorkspaceQueryService.findEntities("team-a", pageable))
+                .thenReturn(List.of(teamAlphaEntity, teamBetaEntity));
+        AuthTokenRequestContext.bindWorkspaceId(" team-a ");
+
+        List<ObserveEntity> entities = workspaceAccessService.findAccessibleEntitiesForRequestWorkspace(pageable);
+
+        assertEquals(List.of(teamAlphaEntity), entities);
+    }
+
     @Test
     void findAccessibleEntityUsesCurrentRequestWorkspaceAndRejectsCrossWorkspaceRows() {
         ObserveEntity teamAlphaEntity = ObserveEntity.builder()
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryServiceTest.java
index 0e4603f3d8..5c96967467 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityWorkspaceQueryServiceTest.java
@@ -88,6 +88,32 @@ class EntityWorkspaceQueryServiceTest {
         verify(observeEntityDao).findAll(sort);
     }
 
+    @Test
+    void findEntitiesRoutesWorkspaceAndUnscopedPagedLookup() {
+        PageRequest pageable = PageRequest.of(0, 12, Sort.by(Sort.Order.desc("gmtUpdate"), Sort.Order.desc("id")));
+        ObserveEntity checkout = ObserveEntity.builder()
+                .id(411L)
+                .name("checkout")
+                .workspaceId("team-a")
+                .build();
+        ObserveEntity billing = ObserveEntity.builder()
+                .id(412L)
+                .name("billing")
+                .workspaceId("team-b")
+                .build();
+        Page<ObserveEntity> persistedPage = new PageImpl<>(List.of(checkout, billing), pageable, 2);
+        when(observeEntityDao.findAllByWorkspaceId("team-a", pageable)).thenReturn(List.of(checkout));
+        when(observeEntityDao.findAll(pageable)).thenReturn(persistedPage);
+
+        List<ObserveEntity> teamEntities = entityWorkspaceQueryService.findEntities("team-a", pageable);
+        List<ObserveEntity> allEntities = entityWorkspaceQueryService.findEntities(null, pageable);
+
+        assertEquals(List.of(checkout), teamEntities);
+        assertEquals(List.of(checkout, billing), allEntities);
+        verify(observeEntityDao).findAllByWorkspaceId("team-a", pageable);
+        verify(observeEntityDao).findAll(pageable);
+    }
+
     @Test
     void findEntityPageUsesPersistedCatalogRows() {
         PageRequest pageable = PageRequest.of(0, 8, Sort.by(Sort.Order.desc("gmtUpdate")));
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeederTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeederTest.java
new file mode 100644
index 0000000000..dc38ac840d
--- /dev/null
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/LocalTopologyDemoRelationSeederTest.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.entity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.EntityRelation;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.manager.dao.EntityIdentityDao;
+import org.apache.hertzbeat.manager.dao.EntityRelationDao;
+import org.apache.hertzbeat.manager.dao.ObserveEntityDao;
+import org.springframework.boot.DefaultApplicationArguments;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Contract for the local demo topology relation seed that prevents node-only demo graphs.
+ */
+@ExtendWith(MockitoExtension.class)
+class LocalTopologyDemoRelationSeederTest {
+
+    @Mock
+    private ObserveEntityDao observeEntityDao;
+    @Mock
+    private EntityRelationDao entityRelationDao;
+    @Mock
+    private EntityIdentityDao entityIdentityDao;
+
+    @Test
+    void seedsCheckoutPaymentAndOrdersRelationsWhenDemoEntitiesExistWithoutEdges() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        ObserveEntity checkout = entity(10L, "service", "Checkout API");
+        ObserveEntity payment = entity(20L, "service", "Payment API");
+        ObserveEntity orders = entity(30L, "database", "Orders DB");
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "service", "commerce", "Checkout API"))
+                .thenReturn(Optional.of(checkout));
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "service", "commerce", "Payment API"))
+                .thenReturn(Optional.of(payment));
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "database", "commerce", "Orders DB"))
+                .thenReturn(Optional.of(orders));
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(10L, 10L)).thenReturn(List.of());
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(20L, 20L)).thenReturn(List.of());
+
+        seeder.seedDemoRelations();
+
+        ArgumentCaptor<List<EntityRelation>> rowsCaptor = ArgumentCaptor.forClass(List.class);
+        verify(entityRelationDao).saveAll(rowsCaptor.capture());
+        List<EntityRelation> rows = rowsCaptor.getValue();
+        assertEquals(2, rows.size());
+        EntityRelation checkoutToPayment = rows.get(0);
+        assertEquals(10L, checkoutToPayment.getSourceEntityId());
+        assertEquals(20L, checkoutToPayment.getTargetEntityId());
+        assertEquals("service:commerce/Payment API", checkoutToPayment.getTargetRef());
+        assertEquals("trace-call", checkoutToPayment.getRelationType());
+        assertEquals("otlp-trace-call", checkoutToPayment.getRelationSource());
+        assertEquals("local-topology-demo-seed", checkoutToPayment.getAttributes().get("seed"));
+        EntityRelation paymentToOrders = rows.get(1);
+        assertEquals(20L, paymentToOrders.getSourceEntityId());
+        assertEquals(30L, paymentToOrders.getTargetEntityId());
+        assertEquals("database:commerce/Orders DB", paymentToOrders.getTargetRef());
+        assertEquals("depends_on", paymentToOrders.getRelationType());
+        assertEquals("cmdb-manual-label", paymentToOrders.getRelationSource());
+    }
+
+    @Test
+    void doesNotSeedWhenRelationsAlreadyExist() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "service", "commerce", "Checkout API"))
+                .thenReturn(Optional.of(entity(10L, "service", "Checkout API")));
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "service", "commerce", "Payment API"))
+                .thenReturn(Optional.of(entity(20L, "service", "Payment API")));
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                AuthTokenScopes.DEFAULT_WORKSPACE_ID, "database", "commerce", "Orders DB"))
+                .thenReturn(Optional.of(entity(30L, "database", "Orders DB")));
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(10L, 10L)).thenReturn(List.of(
+                EntityRelation.builder().sourceEntityId(10L).targetEntityId(20L).relationType("trace-call").build()));
+        when(entityRelationDao.findBySourceEntityIdOrTargetEntityId(20L, 20L)).thenReturn(List.of(
+                EntityRelation.builder().sourceEntityId(20L).targetEntityId(30L).relationType("depends_on").build()));
+
+        seeder.seedDemoRelations();
+
+        verify(entityRelationDao, never()).saveAll(eq(List.of()));
+        verify(entityRelationDao, never()).saveAll(org.mockito.ArgumentMatchers.anyList());
+    }
+
+    @Test
+    void startupDoesNotSeedLargeMixedScaleProofByDefault() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+
+        seeder.run(new DefaultApplicationArguments());
+
+        verify(observeEntityDao, never()).saveAll(org.mockito.ArgumentMatchers.anyList());
+        verify(entityIdentityDao, never()).saveAll(org.mockito.ArgumentMatchers.anyList());
+    }
+
+    @Test
+    void startupSeedsLargeMixedScaleProofOnlyWhenExplicitlyEnabled() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                anyString(), anyString(), anyString(), anyString()))
+                .thenReturn(Optional.empty());
+
+        seeder.run(new DefaultApplicationArguments("--hertzbeat.topology.local-scale-proof-seed=true"));
+
+        ArgumentCaptor<List<ObserveEntity>> entitiesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(observeEntityDao).saveAll(entitiesCaptor.capture());
+        assertEquals(1993, entitiesCaptor.getValue().size());
+    }
+
+    @Test
+    void seedsMixedScaleProofEntitiesAndServiceNameIdentitiesForEveryGreptimeService() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                eq(AuthTokenScopes.DEFAULT_WORKSPACE_ID), eq("service"), eq("scale-mix"), anyString()))
+                .thenReturn(Optional.empty());
+
+        seeder.seedMixedScaleProofEntities();
+
+        ArgumentCaptor<List<ObserveEntity>> entitiesCaptor = ArgumentCaptor.forClass(List.class);
+        ArgumentCaptor<List<EntityIdentity>> identitiesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(observeEntityDao).saveAll(entitiesCaptor.capture());
+        verify(entityIdentityDao).saveAll(identitiesCaptor.capture());
+        List<ObserveEntity> entities = entitiesCaptor.getValue();
+        List<EntityIdentity> identities = identitiesCaptor.getValue();
+        assertEquals(1993, entities.size());
+        assertEquals(1993, identities.size());
+        assertEquals("hb-mix-1780329856-edge-gateway", entities.getFirst().getName());
+        assertEquals("scale-mix", entities.getFirst().getNamespace());
+        assertEquals("prod", entities.getFirst().getEnvironment());
+        assertEquals("service", entities.getFirst().getType());
+        assertEquals("hb-mix-1780329856-edge-gateway", identities.getFirst().getIdentityValue());
+        assertEquals("service.name", identities.getFirst().getIdentityKey());
+        assertEquals("otel_resource", identities.getFirst().getIdentityType());
+        assertEquals("hb-mix-1780329856-svc-11-164", entities.getLast().getName());
+        assertEquals("hb-mix-1780329856-svc-11-164", identities.getLast().getIdentityValue());
+    }
+
+    @Test
+    void backfillsMixedScaleProofServiceNameIdentityWhenEntityAlreadyExists() {
+        LocalTopologyDemoRelationSeeder seeder = new LocalTopologyDemoRelationSeeder(
+                observeEntityDao, entityRelationDao, entityIdentityDao);
+        when(observeEntityDao.findFirstByWorkspaceIdAndTypeAndNamespaceAndName(
+                eq(AuthTokenScopes.DEFAULT_WORKSPACE_ID), eq("service"), eq("scale-mix"), anyString()))
+                .thenAnswer(invocation -> {
+                    String serviceName = invocation.getArgument(3, String.class);
+                    return Optional.of(ObserveEntity.builder()
+                            .id(646566130000000L + Math.abs(serviceName.hashCode()))
+                            .workspaceId(AuthTokenScopes.DEFAULT_WORKSPACE_ID)
+                            .type("service")
+                            .namespace("scale-mix")
+                            .name(serviceName)
+                            .environment("prod")
+                            .build());
+                });
+        when(entityIdentityDao.findAllByEntityIdOrderByPriorityDescIdAsc(anyLong()))
+                .thenReturn(List.of());
+
+        seeder.seedMixedScaleProofEntities();
+
+        ArgumentCaptor<List<EntityIdentity>> identitiesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(observeEntityDao, never()).saveAll(org.mockito.ArgumentMatchers.anyList());
+        verify(entityIdentityDao).saveAll(identitiesCaptor.capture());
+        List<EntityIdentity> identities = identitiesCaptor.getValue();
+        EntityIdentity gatewayIdentity = identities.stream()
+                .filter(identity -> "hb-mix-1780329856-edge-gateway".equals(identity.getIdentityValue()))
+                .findFirst()
+                .orElseThrow();
+        assertEquals("service.name", gatewayIdentity.getIdentityKey());
+        assertEquals("hb-mix-1780329856-edge-gateway", gatewayIdentity.getIdentityValue());
+        assertEquals("hb-mix-1780329856-edge-gateway", gatewayIdentity.getNormalizedValue());
+        assertEquals(1993, identities.size());
+    }
+
+    private ObserveEntity entity(Long id, String type, String name) {
+        return ObserveEntity.builder()
+                .id(id)
+                .workspaceId(AuthTokenScopes.DEFAULT_WORKSPACE_ID)
+                .type(type)
+                .namespace("commerce")
+                .name(name)
+                .environment("prod")
+                .build();
+    }
+}
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryServiceTest.java
index 28044ed467..4720a64659 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryServiceTest.java
@@ -249,6 +249,43 @@ class TraceCallTopologyQueryServiceTest {
         assertEquals(1L, edge.redMetrics().requestCount());
     }
 
+    @Test
+    void returnsEmptyWhenRawTraceFallbackTimesOutAfterServiceGraphHasNoEdges() throws InterruptedException {
+        TraceCallTopologyQueryService boundedService = new TraceCallTopologyQueryService(
+                traceQueryRepository,
+                entityIdentityQueryService,
+                entityWorkspaceAccessService,
+                Duration.ofMillis(25));
+        ObserveEntity checkout = entity(10L, "checkout-api", "commerce", "prod");
+        CountDownLatch releaseRawTraceQuery = new CountDownLatch(1);
+        when(entityIdentityQueryService.findIdentities(10L)).thenReturn(List.of(
+                identity(10L, "service.name", "checkout-api")));
+        when(traceQueryRepository.queryTraceServiceGraphRows(
+                eq(1500), eq(1710000000000L), eq(1710003600000L), eq("prod"),
+                argThat(serviceNames -> serviceNames != null && serviceNames.contains("checkout-api")),
+                eq(true)))
+                .thenReturn(List.of());
+        when(traceQueryRepository.queryRecentTraceRows(1500, 1710000000000L, 1710003600000L, null, "prod", true))
+                .thenAnswer(invocation -> {
+                    releaseRawTraceQuery.await(5, TimeUnit.SECONDS);
+                    return List.of(traceRow("trace-late", "root", null, "GET /checkout",
+                            "checkout-api", "STATUS_CODE_OK", 20D));
+                });
+
+        long startedAt = System.nanoTime();
+        TraceCallTopologyReadModel readModel;
+        try {
+            readModel = boundedService.findTraceCallEdges(
+                    List.of(checkout), "prod", 1710000000000L, 1710003600000L);
+        } finally {
+            releaseRawTraceQuery.countDown();
+        }
+
+        assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt) < 1000);
+        assertTrue(readModel.entityById().isEmpty());
+        assertTrue(readModel.edges().isEmpty());
+    }
+
     @Test
     void pushesSeedServiceScopeIntoGreptimeServiceGraphAggregation() {
         ObserveEntity checkout = entity(10L, "Checkout API", "commerce", "prod");
@@ -325,6 +362,48 @@ class TraceCallTopologyQueryServiceTest {
                 1500, 1710000000000L, 1710003600000L, null, "prod", false);
     }
 
+    @Test
+    void derivesDefaultOverviewServiceGraphWithoutSeedScope() {
+        ObserveEntity gateway = entity(10L, "edge-gateway", "scale-mix", "prod");
+        ObserveEntity domain = entity(20L, "domain-00", "scale-mix", "prod");
+        ObserveEntity service = entity(30L, "svc-00-000", "scale-mix", "prod");
+        when(traceQueryRepository.queryTraceServiceGraphRows(
+                eq(1500), eq(1710000000000L), eq(1710003600000L), eq("prod"),
+                argThat(serviceNames -> serviceNames != null && serviceNames.isEmpty()),
+                eq(true)))
+                .thenReturn(List.of(
+                        serviceGraphRow("edge-gateway", "domain-00", 12L, 0L, 75D, 42D,
+                                "trace-star", "span-domain", "GET /domain",
+                                "2026-05-20T04:01:00Z", "2026-05-20T04:08:00Z"),
+                        serviceGraphRow("domain-00", "svc-00-000", 124L, 1L, 95D, 54D,
+                                "trace-mesh", "span-service", "CALL svc-00-000",
+                                "2026-05-20T04:02:00Z", "2026-05-20T04:09:00Z")));
+        when(entityIdentityQueryService.findMatchingIdentities(argThat(keys ->
+                keys != null && keys.contains("service.name")), argThat(values ->
+                values != null && values.containsAll(Set.of("edge gateway", "domain 00", "svc 00 000")))))
+                .thenReturn(List.of(
+                        identity(10L, "service.name", "edge-gateway"),
+                        identity(20L, "service.name", "domain-00"),
+                        identity(30L, "service.name", "svc-00-000")));
+        when(entityWorkspaceAccessService.findAccessibleEntitiesByIdsForRequestWorkspace(argThat(ids ->
+                ids != null && ids.containsAll(Set.of(10L, 20L, 30L)))))
+                .thenReturn(List.of(gateway, domain, service));
+
+        TraceCallTopologyReadModel readModel = traceCallTopologyQueryService.findTraceCallEdgesForOverview(
+                "prod", 1710000000000L, 1710003600000L, true);
+
+        assertEquals(Set.of(10L, 20L, 30L), readModel.entityById().keySet());
+        assertEquals(2, readModel.edges().size());
+        assertEquals(10L, readModel.edges().get(0).sourceEntityId());
+        assertEquals(20L, readModel.edges().get(0).targetEntityId());
+        assertEquals(20L, readModel.edges().get(1).sourceEntityId());
+        assertEquals(30L, readModel.edges().get(1).targetEntityId());
+        assertEquals(124L, readModel.edges().get(1).redMetrics().requestCount());
+        assertEquals(1L, readModel.edges().get(1).redMetrics().errorCount());
+        verify(traceQueryRepository, never()).queryRecentTraceRows(
+                1500, 1710000000000L, 1710003600000L, null, "prod", true);
+    }
+
     @Test
     void resolvesGreptimeServiceGraphRowsByAccessibleEntityNameWhenIdentityIsMissing() {
         ObserveEntity checkout = entity(10L, "Checkout API", "commerce", "prod");
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
index 2f8daf53ff..71250b4e0b 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
@@ -192,14 +192,22 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readSeverityBuckets(Long start, Long end, String traceId, String spanId,
                                                   Integer severityNumber, String severityText, String search,
                                                   boolean hideInternal, boolean hideNoise) {
-        if (hasWorkspaceContext()) {
-            return null;
-        }
+        String normalizedWorkspaceId = hasWorkspaceContext()
+                ? AuthTokenScopes.normalizeWorkspaceId(AuthTokenRequestContext.currentWorkspaceId())
+                : null;
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
-                Map<String, Long> aggregate = historyDataReader.countLogsBySeverityBuckets(
-                        start, end, traceId, spanId, severityNumber, severityText, search,
-                        hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                Map<String, Long> aggregate;
+                if (StringUtils.hasText(normalizedWorkspaceId)) {
+                    aggregate = historyDataReader.countLogsBySeverityBuckets(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise),
+                            shouldRequireServiceName(hideInternal, hideNoise), normalizedWorkspaceId);
+                } else {
+                    aggregate = historyDataReader.countLogsBySeverityBuckets(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                }
                 if (aggregate != null && !aggregate.isEmpty()) {
                     return aggregate;
                 }
@@ -213,14 +221,22 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readTraceCoverage(Long start, Long end, String traceId, String spanId,
                                                 Integer severityNumber, String severityText, String search,
                                                 boolean hideInternal, boolean hideNoise) {
-        if (hasWorkspaceContext()) {
-            return null;
-        }
+        String normalizedWorkspaceId = hasWorkspaceContext()
+                ? AuthTokenScopes.normalizeWorkspaceId(AuthTokenRequestContext.currentWorkspaceId())
+                : null;
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
-                Map<String, Long> aggregate = historyDataReader.countLogTraceCoverage(
-                        start, end, traceId, spanId, severityNumber, severityText, search,
-                        hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                Map<String, Long> aggregate;
+                if (StringUtils.hasText(normalizedWorkspaceId)) {
+                    aggregate = historyDataReader.countLogTraceCoverage(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise),
+                            shouldRequireServiceName(hideInternal, hideNoise), normalizedWorkspaceId);
+                } else {
+                    aggregate = historyDataReader.countLogTraceCoverage(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                }
                 if (aggregate != null && !aggregate.isEmpty()) {
                     return aggregate;
                 }
@@ -234,14 +250,22 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readHourlyStats(Long start, Long end, String traceId, String spanId,
                                               Integer severityNumber, String severityText, String search,
                                               boolean hideInternal, boolean hideNoise) {
-        if (hasWorkspaceContext()) {
-            return null;
-        }
+        String normalizedWorkspaceId = hasWorkspaceContext()
+                ? AuthTokenScopes.normalizeWorkspaceId(AuthTokenRequestContext.currentWorkspaceId())
+                : null;
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
-                Map<String, Long> aggregate = historyDataReader.countLogsByHour(
-                        start, end, traceId, spanId, severityNumber, severityText, search,
-                        hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                Map<String, Long> aggregate;
+                if (StringUtils.hasText(normalizedWorkspaceId)) {
+                    aggregate = historyDataReader.countLogsByHour(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise),
+                            shouldRequireServiceName(hideInternal, hideNoise), normalizedWorkspaceId);
+                } else {
+                    aggregate = historyDataReader.countLogsByHour(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                }
                 if (aggregate != null && !aggregate.isEmpty()) {
                     return aggregate;
                 }
@@ -331,6 +355,30 @@ public class LogQueryServiceImpl implements LogQueryService {
                                                  Integer severityNumber, String severityText, String search,
                                                  PageRequest pageRequest, int offset, int pageSize,
                                                  boolean hideInternal, boolean hideNoise) {
+        String normalizedWorkspaceId = AuthTokenScopes.normalizeWorkspaceId(AuthTokenRequestContext.currentWorkspaceId());
+        Set<String> hiddenServiceNames = hiddenServiceNames(hideInternal, hideNoise);
+        boolean requireServiceName = shouldRequireServiceName(hideInternal, hideNoise);
+        for (HistoryDataReader historyDataReader : historyDataReaders) {
+            try {
+                long totalElements = historyDataReader.countLogsByMultipleConditions(
+                        start, end, traceId, spanId, severityNumber, severityText, search,
+                        hiddenServiceNames, requireServiceName, normalizedWorkspaceId);
+                if (totalElements <= 0) {
+                    continue;
+                }
+                List<LogEntry> pagedLogs = historyDataReader.queryLogsByMultipleConditionsWithPagination(
+                        start, end, traceId, spanId, severityNumber, severityText, search, offset, pageSize,
+                        hiddenServiceNames, requireServiceName, normalizedWorkspaceId);
+                List<LogEntry> guardedLogs = filterQueryLogs(pagedLogs, hideInternal, hideNoise);
+                long safeTotal = guardedLogs.size() < (pagedLogs == null ? 0 : pagedLogs.size())
+                        ? offset + guardedLogs.size()
+                        : totalElements;
+                return new PageImpl<>(guardedLogs, pageRequest, safeTotal);
+            } catch (UnsupportedOperationException ex) {
+                // Fall back to row-based workspace filtering for history stores without native workspace predicates.
+            }
+        }
+
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
                 List<LogEntry> logs;
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
index 729053e240..ac65e7546a 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java
@@ -38,6 +38,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummary
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -581,20 +582,29 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa
         if (request == null) {
             return new EntityResponseHandoffsInfo();
         }
+        EntitySignalEvidenceBundle signalEvidence = request.getSignalEvidence();
+        EntityLogSummaryInfo logSummary = signalEvidence == null ? request.getLogSummary() : signalEvidence.getLogSummary();
+        EntityTraceSummaryDto traceSummary = signalEvidence == null ? request.getTraceSummary() : signalEvidence.getTraceSummary();
+        List<MetricEvidence> metricEvidence = signalEvidence == null ? request.getMetricEvidence() : signalEvidence.getMetricEvidence();
+        List<LogEvidence> logEvidence = signalEvidence == null ? request.getLogEvidence() : signalEvidence.getLogEvidence();
+        List<TraceEvidence> traceEvidence = signalEvidence == null ? request.getTraceEvidence() : signalEvidence.getTraceEvidence();
+        List<EntityTraceQueryHintDto> traceQueryHints = signalEvidence == null
+                ? request.getTraceQueryHints()
+                : signalEvidence.getTraceQueryHints();
         String alertSearchToken = buildEntityAlertSearchToken(request.getEntityContext(), request.getActiveAlerts());
-        String logSearchToken = buildEntityLogSearchToken(request.getEntityContext(), request.getLogSummary());
-        String traceSearchToken = buildEntityTraceSearchToken(request.getEntityContext(), request.getTraceSummary());
+        String logSearchToken = buildEntityLogSearchToken(request.getEntityContext(), logSummary);
+        String traceSearchToken = buildEntityTraceSearchToken(request.getEntityContext(), traceSummary);
         String fallbackSearchToken = defaultText(
                 trimToNull(request.getEntityContext() == null || request.getEntityContext().getEntity() == null
                         ? null : request.getEntityContext().getEntity().getName()),
                 trimToNull(request.getEntityContext() == null || request.getEntityContext().getEntity() == null
                         ? null : request.getEntityContext().getEntity().getDisplayName()));
         MetricEvidence preferredMetricEvidence =
-                CollectionUtils.isEmpty(request.getMetricEvidence()) ? null : request.getMetricEvidence().getFirst();
+                CollectionUtils.isEmpty(metricEvidence) ? null : metricEvidence.getFirst();
         LogEvidence preferredLogEvidence =
-                CollectionUtils.isEmpty(request.getLogEvidence()) ? null : request.getLogEvidence().getFirst();
+                CollectionUtils.isEmpty(logEvidence) ? null : logEvidence.getFirst();
         TraceEvidence preferredTraceEvidence =
-                CollectionUtils.isEmpty(request.getTraceEvidence()) ? null : request.getTraceEvidence().getFirst();
+                CollectionUtils.isEmpty(traceEvidence) ? null : traceEvidence.getFirst();
         return new EntityResponseHandoffsInfo(
                 buildEntityAlertHandoff(request.getReturnTo(), request.getReturnLabel(),
                         request.getEntityContext(), request.getActiveAlerts()),
@@ -602,9 +612,9 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa
                         request.getMonitors(), fallbackSearchToken, preferredMetricEvidence),
                 buildEntityLogHandoff(request.getReturnTo(), request.getReturnLabel(),
                         logSearchToken, request.getActiveAlerts(), preferredLogEvidence,
-                        request.getTraceSummary(), preferredTraceEvidence, request.getTraceQueryHints()),
+                        traceSummary, preferredTraceEvidence, traceQueryHints),
                 buildEntityTraceHandoff(request.getReturnTo(), request.getReturnLabel(),
-                        traceSearchToken, request.getTraceSummary(), preferredTraceEvidence),
+                        traceSearchToken, traceSummary, preferredTraceEvidence),
                 buildEntityDiscoveryHandoff(request.getReturnTo(), request.getReturnLabel(),
                         request.getEntityOwner(), request.getEntitySystem(),
                         request.getEntityEnvironment(), request.getEntitySource(),
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
index 8b23e9b6af..b9e760b621 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
@@ -100,6 +100,22 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
             return new EntityTraceSummaryDto(0, 0, null, false, null);
         }
         long now = System.currentTimeMillis();
+        if (traceQueryRepository.supportsTraceSummaryRows()) {
+            Map<String, Object> row = traceQueryRepository.queryTraceSummaryRows(
+                    Math.max(0L, now - DEFAULT_LOOKBACK_MILLIS),
+                    now,
+                    preferredIdentityValue(identityValues, "service.name"),
+                    preferredIdentityValue(identityValues, "service.namespace"),
+                    preferredIdentityValue(identityValues, "deployment.environment.name"),
+                    AuthTokenRequestContext.currentWorkspaceId(),
+                    identityValues,
+                    false
+            );
+            EntityTraceSummaryDto summary = toEntityTraceSummary(row);
+            if (summary != null) {
+                return summary;
+            }
+        }
         List<TraceAggregate> traces = aggregateTraceRows(queryRecentRows(identityValues, now))
                 .stream()
                 .filter(trace -> matchesEntity(trace, identityValues))
@@ -229,6 +245,42 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
     @Override
     public TraceOverviewDto getTraceOverview(Long entityId, Long start, Long end, String traceId, Boolean errorOnly,
                                              String serviceName, String serviceNamespace, String environment, Boolean hideInternal) {
+        Map<String, Set<String>> identityValues = entityId == null ? Collections.emptyMap() : canonicalIdentityValues(loadEntityContext(entityId));
+        if (StringUtils.hasText(traceId) && traceQueryRepository.supportsTraceIdOverviewRows()) {
+            Map<String, Object> row = traceQueryRepository.queryTraceIdOverviewRows(
+                    traceId,
+                    start,
+                    end,
+                    errorOnly,
+                    serviceName,
+                    serviceNamespace,
+                    environment,
+                    AuthTokenRequestContext.currentWorkspaceId(),
+                    identityValues,
+                    hideInternal
+            );
+            TraceOverviewDto overview = toTraceOverview(row);
+            if (overview != null) {
+                return overview;
+            }
+        }
+        if (!StringUtils.hasText(traceId) && traceQueryRepository.supportsTraceOverviewRows()) {
+            Map<String, Object> row = traceQueryRepository.queryTraceOverviewRows(
+                    start,
+                    end,
+                    errorOnly,
+                    serviceName,
+                    serviceNamespace,
+                    environment,
+                    AuthTokenRequestContext.currentWorkspaceId(),
+                    identityValues,
+                    hideInternal
+            );
+            TraceOverviewDto overview = toTraceOverview(row);
+            if (overview != null) {
+                return overview;
+            }
+        }
         Page<TraceListItemDto> result = queryTraceList(entityId, start, end, traceId, errorOnly, serviceName, serviceNamespace, environment, 0,
                 TRACE_LIST_SAMPLE_LIMIT, hideInternal);
         Long latestObservedAt = result.getContent().stream()
@@ -241,6 +293,42 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         return new TraceOverviewDto((int) result.getTotalElements(), errorTraceCount, latestObservedAt, active);
     }
 
+    private TraceOverviewDto toTraceOverview(Map<String, Object> row) {
+        if (CollectionUtils.isEmpty(row)) {
+            return null;
+        }
+        int totalTraceCount = Optional.ofNullable(readIntValue(row, "total_trace_count", "totalTraceCount"))
+                .orElse(0);
+        int errorTraceCount = Optional.ofNullable(readIntValue(row, "error_trace_count", "errorTraceCount"))
+                .orElse(0);
+        Long latestObservedAt = readTimestamp(row, "latest_observed_at");
+        if (latestObservedAt == null) {
+            latestObservedAt = readTimestamp(row, "latestObservedAt");
+        }
+        boolean active = latestObservedAt != null
+                && latestObservedAt >= System.currentTimeMillis() - ACTIVE_TRACE_WINDOW_MILLIS;
+        return new TraceOverviewDto(totalTraceCount, errorTraceCount, latestObservedAt, active);
+    }
+
+    private EntityTraceSummaryDto toEntityTraceSummary(Map<String, Object> row) {
+        if (CollectionUtils.isEmpty(row)) {
+            return null;
+        }
+        int totalTraceCount = Optional.ofNullable(readIntValue(row, "total_trace_count", "totalTraceCount"))
+                .orElse(0);
+        int errorTraceCount = Optional.ofNullable(readIntValue(row, "error_trace_count", "errorTraceCount"))
+                .orElse(0);
+        Long latestObservedAt = readTimestamp(row, "latest_observed_at");
+        if (latestObservedAt == null) {
+            latestObservedAt = readTimestamp(row, "latestObservedAt");
+        }
+        String latestTraceId = defaultText(readTextValue(row, "latest_trace_id"),
+                readTextValue(row, "latestTraceId"));
+        boolean active = latestObservedAt != null
+                && latestObservedAt >= System.currentTimeMillis() - ACTIVE_TRACE_WINDOW_MILLIS;
+        return new EntityTraceSummaryDto(totalTraceCount, errorTraceCount, latestObservedAt, active, latestTraceId);
+    }
+
     private ObservedEntityContext loadEntityContext(Long entityId) {
         if (entityId == null || entityId <= 0) {
             return null;
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
index 93cb25bdb0..bf93fbfa2e 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
@@ -278,6 +278,41 @@ class LogQueryControllerTest {
                 any(), any(), any(), any());
     }
 
+    @Test
+    void testListLogsPushesWorkspacePaginationIntoStorageWhenSupported() throws Exception {
+        LogEntry teamAlphaLog = LogEntry.builder()
+                .timeUnixNano(1734005477630000000L)
+                .severityText("INFO")
+                .body("team-a checkout log")
+                .resource(new HashMap<>(java.util.Map.of(
+                        "service.name", "checkout",
+                        "hertzbeat.workspace_id", "team-a")))
+                .build();
+
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        when(historyDataReader.countLogsByMultipleConditions(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"))).thenReturn(41L);
+        when(historyDataReader.queryLogsByMultipleConditionsWithPagination(any(), any(), any(), any(), any(), any(),
+                any(), eq(40), eq(20), anySet(), eq(false), eq("team-a")))
+                .thenReturn(List.of(teamAlphaLog));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/list")
+                        .param("pageIndex", "2")
+                        .param("pageSize", "20"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.content.length()").value(1))
+                .andExpect(jsonPath("$.data.totalElements").value(41))
+                .andExpect(jsonPath("$.data.content[0].body").value("team-a checkout log"));
+
+        verify(historyDataReader).countLogsByMultipleConditions(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader).queryLogsByMultipleConditionsWithPagination(any(), any(), any(), any(), any(), any(),
+                any(), eq(40), eq(20), anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader, never()).queryLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+    }
+
     @Test
     void testListLogsDoesNotLeakUnscopedTotalWhenWorkspacePageAlreadyMatches() throws Exception {
         LogEntry teamAlphaLog = LogEntry.builder()
@@ -304,6 +339,33 @@ class LogQueryControllerTest {
                 any(), any(), any(), any());
     }
 
+    @Test
+    void testOverviewStatsPushesWorkspaceAggregateIntoStorageWhenSupported() throws Exception {
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        when(historyDataReader.countLogsBySeverityBuckets(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"))).thenReturn(java.util.Map.of(
+                        "totalCount", 3L,
+                        "infoCount", 2L,
+                        "errorCount", 1L,
+                        "fatalCount", 0L,
+                        "warnCount", 0L,
+                        "debugCount", 0L,
+                        "traceCount", 0L
+                ));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/stats/overview"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.totalCount").value(3))
+                .andExpect(jsonPath("$.data.infoCount").value(2))
+                .andExpect(jsonPath("$.data.errorCount").value(1));
+
+        verify(historyDataReader).countLogsBySeverityBuckets(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader, never()).queryLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+    }
+
     @Test
     void testOverviewStatsUsesRequestWorkspaceContext() throws Exception {
         List<LogEntry> mockLogs = Arrays.asList(
@@ -523,6 +585,30 @@ class LogQueryControllerTest {
                 any(), any(), any(), any());
     }
 
+    @Test
+    void testTraceCoverageStatsPushesWorkspaceAggregateIntoStorageWhenSupported() throws Exception {
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        when(historyDataReader.countLogTraceCoverage(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"))).thenReturn(java.util.Map.of(
+                        "withTrace", 5L,
+                        "withoutTrace", 2L,
+                        "withSpan", 4L,
+                        "withBothTraceAndSpan", 3L
+                ));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/stats/trace-coverage"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.traceCoverage.withTrace").value(5))
+                .andExpect(jsonPath("$.data.traceCoverage.withoutTrace").value(2))
+                .andExpect(jsonPath("$.data.traceCoverage.withBothTraceAndSpan").value(3));
+
+        verify(historyDataReader).countLogTraceCoverage(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader, never()).queryLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+    }
+
     @Test
     void testTrendStats() throws Exception {
         // Create logs with timestamps that fall into different hours
@@ -580,4 +666,22 @@ class LogQueryControllerTest {
                 any(), any(), any(), any());
     }
 
+    @Test
+    void testTrendStatsPushesWorkspaceAggregateIntoStorageWhenSupported() throws Exception {
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        when(historyDataReader.countLogsByHour(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a")))
+                .thenReturn(java.util.Map.of("2026-04-29 21:00", 12L));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/stats/trend"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.hourlyStats['2026-04-29 21:00']").value(12));
+
+        verify(historyDataReader).countLogsByHour(any(), any(), any(), any(), any(), any(), any(),
+                anySet(), eq(false), eq("team-a"));
+        verify(historyDataReader, never()).queryLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+    }
+
 }
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
index a002e44fa8..9fbb86ddb3 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java
@@ -41,6 +41,7 @@ import org.apache.hertzbeat.common.observability.dto.entity.EntityMonitorSummary
 import org.apache.hertzbeat.common.observability.dto.entity.EntityNextActionInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityObservabilityDetailBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityOpsSummaryInfo;
+import org.apache.hertzbeat.common.observability.dto.entity.EntitySignalEvidenceBundle;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusPageSummaryInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityStatusInfo;
 import org.apache.hertzbeat.common.observability.dto.entity.EntityTriageRecommendation;
@@ -732,4 +733,57 @@ class EntityObservabilityGatewayImplTest {
         assertEquals("checkout-system", handoffs.getDiscovery().getSystem());
         assertEquals("ownership", handoffs.getEditor().getFocus());
     }
+
+    @Test
+    void buildEntityResponseHandoffsShouldPreferSharedSignalEvidenceBundle() {
+        ObserveEntity entity = new ObserveEntity();
+        entity.setName("checkout");
+        EntityIdentity identity = EntityIdentity.builder()
+                .identityKey("service.name")
+                .identityValue("checkout")
+                .build();
+        ObservedEntityContext entityContext = ObservedEntityContext.from(entity, List.of(identity));
+        EntityTraceSummaryDto staleTraceSummary = new EntityTraceSummaryDto(1, 0, 10L, true, "stale-trace");
+        EntityTraceSummaryDto bundleTraceSummary = new EntityTraceSummaryDto(2, 1, 20L, true, "bundle-trace");
+        EntitySignalEvidenceBundle signalEvidence = new EntitySignalEvidenceBundle(
+                new EntityLogSummaryInfo(1, "bundle", "resource", Map.of(), List.of("bundle-log"), "bundle-log"),
+                bundleTraceSummary,
+                List.of(),
+                List.of(),
+                List.of(),
+                List.of(),
+                List.of(),
+                null,
+                null
+        );
+        EntityResponseHandoffsRequest request = new EntityResponseHandoffsRequest(
+                "/entities/1",
+                "结账服务",
+                "team-a",
+                "checkout-system",
+                "prod",
+                "manual",
+                entityContext,
+                List.of(),
+                List.of(),
+                new EntityLogSummaryInfo(1, "stale", "resource", Map.of(), List.of("stale-log"), "stale-log"),
+                staleTraceSummary,
+                List.of(),
+                List.of(),
+                List.of(),
+                List.of(),
+                signalEvidence,
+                true,
+                true,
+                true,
+                true
+        );
+
+        EntityResponseHandoffsInfo handoffs = gateway.buildEntityResponseHandoffs(request);
+
+        assertEquals("bundle-trace", handoffs.getLogs().getTraceId());
+        assertEquals("bundle-trace", handoffs.getLogs().getSearch());
+        assertEquals("bundle-trace", handoffs.getTraces().getTraceId());
+        assertEquals("bundle-trace", handoffs.getTraces().getSearch());
+    }
 }
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
index 13716a30d2..397ee565c1 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
@@ -116,6 +116,51 @@ class EntityTraceQueryServiceImplTest {
         assertEquals(Set.of("commerce"), identityFilterCaptor.getValue().get("service.namespace"));
     }
 
+    @Test
+    void buildEntityTraceSummaryUsesGreptimeAggregateWhenRepositorySupportsIt() {
+        long now = System.currentTimeMillis();
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        ObservedEntityContext entityContext = ObservedEntityContext.from(
+                ObserveEntity.builder().id(1L).type("service").name("checkout-service").build(),
+                List.of(
+                        identity(1L, "service.name", "checkout-service", 100, true),
+                        identity(1L, "service.namespace", "commerce", 80, false),
+                        identity(1L, "host.name", "checkout-1", 50, false)
+                ));
+        when(traceQueryRepository.supportsTraceSummaryRows()).thenReturn(true);
+        when(traceQueryRepository.queryTraceSummaryRows(
+                org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                eq("checkout-service"), eq("commerce"), org.mockito.ArgumentMatchers.isNull(),
+                eq("team-a"), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), eq(false)))
+                .thenReturn(Map.of(
+                        "total_trace_count", 7L,
+                        "error_trace_count", 2L,
+                        "latest_observed_at", now - 30_000,
+                        "latest_trace_id", "trace-latest"
+                ));
+
+        EntityTraceSummaryDto summary = entityTraceQueryService.buildEntityTraceSummary(entityContext);
+
+        assertEquals(7, summary.getRecentTraceCount());
+        assertEquals(2, summary.getRecentErrorTraceCount());
+        assertEquals("trace-latest", summary.getLatestTraceId());
+        assertEquals(now - 30_000, summary.getLatestObservedAt());
+        assertTrue(summary.isActive());
+        ArgumentCaptor<Map<String, Set<String>>> identityFilterCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(traceQueryRepository).queryTraceSummaryRows(
+                org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                eq("checkout-service"), eq("commerce"), org.mockito.ArgumentMatchers.isNull(),
+                eq("team-a"), identityFilterCaptor.capture(), eq(false));
+        assertEquals(Set.of("checkout-service"), identityFilterCaptor.getValue().get("service.name"));
+        assertEquals(Set.of("commerce"), identityFilterCaptor.getValue().get("service.namespace"));
+        assertEquals(Set.of("checkout-1"), identityFilterCaptor.getValue().get("host.name"));
+        verify(traceQueryRepository, never()).queryRecentTraceRows(
+                eq(1500), org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), org.mockito.ArgumentMatchers.any());
+    }
+
     @Test
     void queryTraceListAndDetailRespectEntityBinding() {
         long now = System.currentTimeMillis();
@@ -408,6 +453,98 @@ class EntityTraceQueryServiceImplTest {
                 eq("prod"), eq("team-a"), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), eq(true));
     }
 
+    @Test
+    void getTraceOverviewUsesGreptimeAggregateWhenRepositorySupportsIt() {
+        long now = System.currentTimeMillis();
+        long start = now - 120_000;
+        long end = now;
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        ObserveEntity entity = ObserveEntity.builder().id(1L).type("service").name("checkout-service").build();
+        when(workspaceQueryGateway.findEntityById(1L)).thenReturn(Optional.of(entity));
+        when(workspaceQueryGateway.findIdentitiesByEntityId(1L)).thenReturn(List.of(
+                identity(1L, "service.name", "checkout-service", 100, true),
+                identity(1L, "service.namespace", "commerce", 80, false)
+        ));
+        when(traceQueryRepository.supportsTraceOverviewRows()).thenReturn(true);
+        when(traceQueryRepository.queryTraceOverviewRows(
+                eq(start), eq(end), eq(true), eq("checkout-service"), eq("commerce"),
+                eq("prod"), eq("team-a"), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                eq(true))).thenReturn(Map.of(
+                "total_trace_count", 41L,
+                "error_trace_count", 41L,
+                "latest_observed_at", now - 30_000
+        ));
+
+        TraceOverviewDto overview = entityTraceQueryService.getTraceOverview(1L, start, end, null,
+                true, "checkout-service", "commerce", "prod", true);
+
+        assertEquals(41, overview.getTotalTraceCount());
+        assertEquals(41, overview.getErrorTraceCount());
+        assertEquals(now - 30_000, overview.getLatestObservedAt());
+        assertTrue(overview.isHasActiveTrace());
+        ArgumentCaptor<Map<String, Set<String>>> identityFilterCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(traceQueryRepository).queryTraceOverviewRows(
+                eq(start), eq(end), eq(true), eq("checkout-service"), eq("commerce"),
+                eq("prod"), eq("team-a"), identityFilterCaptor.capture(), eq(true));
+        assertEquals(Set.of("checkout-service"), identityFilterCaptor.getValue().get("service.name"));
+        assertEquals(Set.of("commerce"), identityFilterCaptor.getValue().get("service.namespace"));
+        verify(traceQueryRepository, never()).queryTraceListRows(
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyInt(),
+                org.mockito.ArgumentMatchers.anyInt());
+        verify(traceQueryRepository, never()).queryRecentTraceRows(
+                eq(1500), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), org.mockito.ArgumentMatchers.any());
+    }
+
+    @Test
+    void getTraceOverviewUsesGreptimeTraceIdAggregateWhenRepositorySupportsIt() {
+        long now = System.currentTimeMillis();
+        long start = now - 120_000;
+        long end = now;
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        ObserveEntity entity = ObserveEntity.builder().id(1L).type("service").name("checkout-service").build();
+        when(workspaceQueryGateway.findEntityById(1L)).thenReturn(Optional.of(entity));
+        when(workspaceQueryGateway.findIdentitiesByEntityId(1L)).thenReturn(List.of(
+                identity(1L, "service.name", "checkout-service", 100, true),
+                identity(1L, "service.namespace", "commerce", 80, false)
+        ));
+        when(traceQueryRepository.supportsTraceIdOverviewRows()).thenReturn(true);
+        when(traceQueryRepository.queryTraceIdOverviewRows(
+                eq("trace-filtered"), eq(start), eq(end), eq(true), eq("checkout-service"), eq("commerce"),
+                eq("prod"), eq("team-a"), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                eq(true))).thenReturn(Map.of(
+                "total_trace_count", 1L,
+                "error_trace_count", 1L,
+                "latest_observed_at", now - 30_000
+        ));
+
+        TraceOverviewDto overview = entityTraceQueryService.getTraceOverview(1L, start, end, "trace-filtered",
+                true, "checkout-service", "commerce", "prod", true);
+
+        assertEquals(1, overview.getTotalTraceCount());
+        assertEquals(1, overview.getErrorTraceCount());
+        assertEquals(now - 30_000, overview.getLatestObservedAt());
+        assertTrue(overview.isHasActiveTrace());
+        ArgumentCaptor<Map<String, Set<String>>> identityFilterCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(traceQueryRepository).queryTraceIdOverviewRows(
+                eq("trace-filtered"), eq(start), eq(end), eq(true), eq("checkout-service"), eq("commerce"),
+                eq("prod"), eq("team-a"), identityFilterCaptor.capture(), eq(true));
+        assertEquals(Set.of("checkout-service"), identityFilterCaptor.getValue().get("service.name"));
+        assertEquals(Set.of("commerce"), identityFilterCaptor.getValue().get("service.namespace"));
+        verify(traceQueryRepository, never()).queryTraceRows(
+                eq("trace-filtered"), eq(5000), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                org.mockito.ArgumentMatchers.any());
+    }
+
     @Test
     void queryTraceListPushesTraceIdFiltersToRepository() {
         long now = System.currentTimeMillis();
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java
index 52a9a0f89c..72cb6d3713 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java
@@ -227,6 +227,195 @@ public class GreptimeTraceQueryRepository implements TraceQueryRepository {
         return queryRows(sql);
     }
 
+    @Override
+    public boolean supportsTraceOverviewRows() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsTraceIdOverviewRows() {
+        return true;
+    }
+
+    @Override
+    public Map<String, Object> queryTraceOverviewRows(Long start,
+                                                      Long end,
+                                                      Boolean errorOnly,
+                                                      String serviceName,
+                                                      String serviceNamespace,
+                                                      String environment,
+                                                      String workspaceId,
+                                                      Map<String, Set<String>> resourceIdentityFilters,
+                                                      Boolean hideInternal) {
+        String errorExpression = "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END)";
+        StringBuilder innerSql = new StringBuilder("SELECT trace_id, ")
+                .append("MIN(timestamp) AS trace_start_time, ")
+                .append(errorExpression)
+                .append(" AS error_span_count FROM ")
+                .append(TRACE_TABLE);
+        List<String> filters = new LinkedList<>();
+        if (start != null) {
+            filters.add("timestamp >= to_timestamp_millis(" + start + ")");
+        }
+        if (end != null) {
+            filters.add("timestamp <= to_timestamp_millis(" + end + ")");
+        }
+        if (StringUtils.hasText(serviceName)) {
+            filters.add("service_name = '" + escapeSql(serviceName) + "'");
+        }
+        if (StringUtils.hasText(serviceNamespace)) {
+            filters.add(resourceAttributeFilter(null, "service.namespace", serviceNamespace));
+        }
+        if (StringUtils.hasText(environment) && !"all".equalsIgnoreCase(environment.trim())) {
+            String filter = environmentFilter(null, environment);
+            if (StringUtils.hasText(filter)) {
+                filters.add(filter);
+            }
+        }
+        if (StringUtils.hasText(workspaceId)) {
+            String filter = workspaceFilter(null, workspaceId);
+            if (StringUtils.hasText(filter)) {
+                filters.add(filter);
+            }
+        }
+        if (!CollectionUtils.isEmpty(resourceIdentityFilters)) {
+            addResourceIdentityFilters(filters, null, resourceIdentityFilters, serviceName, serviceNamespace);
+        }
+        if (Boolean.TRUE.equals(hideInternal)) {
+            filters.add(SELF_TELEMETRY_SERVICE_FILTER);
+        }
+        if (!filters.isEmpty()) {
+            innerSql.append(" WHERE ").append(String.join(" AND ", filters));
+        }
+        innerSql.append(" GROUP BY trace_id");
+        if (Boolean.TRUE.equals(errorOnly)) {
+            innerSql.append(" HAVING ").append(errorExpression).append(" > 0");
+        }
+        String sql = "SELECT COUNT(*) AS total_trace_count, "
+                + "SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count, "
+                + "MAX(trace_start_time) AS latest_observed_at FROM ("
+                + innerSql
+                + ") trace_overview";
+        List<Map<String, Object>> rows = queryRows(sql);
+        return rows.isEmpty() ? Map.of() : rows.getFirst();
+    }
+
+    @Override
+    public Map<String, Object> queryTraceIdOverviewRows(String traceId,
+                                                        Long start,
+                                                        Long end,
+                                                        Boolean errorOnly,
+                                                        String serviceName,
+                                                        String serviceNamespace,
+                                                        String environment,
+                                                        String workspaceId,
+                                                        Map<String, Set<String>> resourceIdentityFilters,
+                                                        Boolean hideInternal) {
+        String errorExpression = "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END)";
+        StringBuilder innerSql = buildTraceGroupedSql(start, end, serviceName, serviceNamespace, environment,
+                workspaceId, resourceIdentityFilters, hideInternal, errorExpression, traceId);
+        if (Boolean.TRUE.equals(errorOnly)) {
+            innerSql.append(" HAVING ").append(errorExpression).append(" > 0");
+        }
+        String sql = "SELECT COUNT(*) AS total_trace_count, "
+                + "SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count, "
+                + "MAX(trace_start_time) AS latest_observed_at FROM ("
+                + innerSql
+                + ") trace_id_overview";
+        List<Map<String, Object>> rows = queryRows(sql);
+        return rows.isEmpty() ? Map.of() : rows.getFirst();
+    }
+
+    @Override
+    public boolean supportsTraceSummaryRows() {
+        return true;
+    }
+
+    @Override
+    public Map<String, Object> queryTraceSummaryRows(Long start,
+                                                     Long end,
+                                                     String serviceName,
+                                                     String serviceNamespace,
+                                                     String environment,
+                                                     String workspaceId,
+                                                     Map<String, Set<String>> resourceIdentityFilters,
+                                                     Boolean hideInternal) {
+        String errorExpression = "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END)";
+        String groupedSql = buildTraceGroupedSql(start, end, serviceName, serviceNamespace, environment,
+                workspaceId, resourceIdentityFilters, hideInternal, errorExpression, null).toString();
+        String sql = "SELECT summary.total_trace_count, summary.error_trace_count, "
+                + "latest.trace_start_time AS latest_observed_at, latest.trace_id AS latest_trace_id "
+                + "FROM (SELECT COUNT(*) AS total_trace_count, "
+                + "SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count "
+                + "FROM ("
+                + groupedSql
+                + ") entity_trace_summary) summary "
+                + "LEFT JOIN (SELECT trace_id, trace_start_time FROM ("
+                + groupedSql
+                + ") entity_trace_latest ORDER BY trace_start_time DESC LIMIT 1) latest ON TRUE";
+        List<Map<String, Object>> rows = queryRows(sql);
+        return rows.isEmpty() ? Map.of() : rows.getFirst();
+    }
+
+    private StringBuilder buildTraceGroupedSql(Long start,
+                                               Long end,
+                                               String serviceName,
+                                               String serviceNamespace,
+                                               String environment,
+                                               String workspaceId,
+                                               Map<String, Set<String>> resourceIdentityFilters,
+                                               Boolean hideInternal,
+                                               String errorExpression,
+                                               String traceId) {
+        StringBuilder innerSql = new StringBuilder("SELECT trace_id, ")
+                .append("MIN(timestamp) AS trace_start_time, ")
+                .append(errorExpression)
+                .append(" AS error_span_count FROM ")
+                .append(TRACE_TABLE);
+        List<String> filters = new LinkedList<>();
+        if (StringUtils.hasText(traceId)) {
+            filters.add("trace_id = '" + escapeSql(traceId) + "'");
+        }
+        if (start != null) {
+            filters.add("timestamp >= to_timestamp_millis(" + start + ")");
+        }
+        if (end != null) {
+            filters.add("timestamp <= to_timestamp_millis(" + end + ")");
+        }
+        if (StringUtils.hasText(serviceName)) {
+            filters.add("service_name = '" + escapeSql(serviceName) + "'");
+        }
+        if (StringUtils.hasText(serviceNamespace)) {
+            filters.add(resourceAttributeFilter(null, "service.namespace", serviceNamespace));
+        }
+        if (StringUtils.hasText(environment) && !"all".equalsIgnoreCase(environment.trim())) {
+            String filter = environmentFilter(null, environment);
+            if (StringUtils.hasText(filter)) {
+                filters.add(filter);
+            }
+        }
+        if (StringUtils.hasText(workspaceId)) {
+            String filter = workspaceFilter(null, workspaceId);
+            if (StringUtils.hasText(filter)) {
+                filters.add(filter);
+            }
+        }
+        if (!CollectionUtils.isEmpty(resourceIdentityFilters)) {
+            addResourceIdentityFilters(filters, null, resourceIdentityFilters, serviceName, serviceNamespace);
+        }
+        if (Boolean.TRUE.equals(hideInternal)) {
+            filters.add(SELF_TELEMETRY_SERVICE_FILTER);
+        }
+        if (!filters.isEmpty()) {
+            innerSql.append(" WHERE ").append(String.join(" AND ", filters));
+        }
+        innerSql.append(" GROUP BY trace_id");
+        return innerSql;
+    }
+
     @Override
     public List<Map<String, Object>> queryTraceServiceGraphRows(int limit,
                                                                 Long start,
@@ -275,15 +464,21 @@ public class GreptimeTraceQueryRepository implements TraceQueryRepository {
         filters.add("LOWER(child.service_name) != LOWER(parent.service_name)");
         if (start != null) {
             filters.add("child.timestamp >= to_timestamp_millis(" + start + ")");
+            filters.add("parent.timestamp >= to_timestamp_millis(" + start + ")");
         }
         if (end != null) {
             filters.add("child.timestamp <= to_timestamp_millis(" + end + ")");
+            filters.add("parent.timestamp <= to_timestamp_millis(" + end + ")");
         }
         if (StringUtils.hasText(environment) && !"all".equalsIgnoreCase(environment.trim())) {
             String filter = environmentFilter("child", environment);
             if (StringUtils.hasText(filter)) {
                 filters.add(filter);
             }
+            String parentFilter = environmentFilter("parent", environment);
+            if (StringUtils.hasText(parentFilter)) {
+                filters.add(parentFilter);
+            }
         }
         String serviceScopeFilter = serviceGraphServiceScopeFilter(serviceNames);
         if (StringUtils.hasText(serviceScopeFilter)) {
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java
index 553f8af85a..001639b44d 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java
@@ -134,6 +134,111 @@ public interface TraceQueryRepository {
         return List.of();
     }
 
+    /**
+     * Whether this repository can aggregate trace overview statistics in storage.
+     *
+     * @return true when storage-owned trace overview rows are supported
+     */
+    default boolean supportsTraceOverviewRows() {
+        return false;
+    }
+
+    /**
+     * Query trace overview statistics grouped in the storage layer.
+     *
+     * @param start start time in epoch milliseconds
+     * @param end end time in epoch milliseconds
+     * @param errorOnly whether only error traces should be counted
+     * @param serviceName service name filter
+     * @param serviceNamespace service namespace filter
+     * @param environment deployment environment filter
+     * @param workspaceId workspace id filter
+     * @param resourceIdentityFilters canonical resource identity filters
+     * @param hideInternal whether internal HertzBeat traces should be hidden
+     * @return row with total_trace_count, error_trace_count, and latest_observed_at
+     */
+    default Map<String, Object> queryTraceOverviewRows(Long start,
+                                                       Long end,
+                                                       Boolean errorOnly,
+                                                       String serviceName,
+                                                       String serviceNamespace,
+                                                       String environment,
+                                                       String workspaceId,
+                                                       Map<String, Set<String>> resourceIdentityFilters,
+                                                       Boolean hideInternal) {
+        return Map.of();
+    }
+
+    /**
+     * Whether this repository can aggregate a single trace-id overview in storage.
+     *
+     * @return true when storage-owned trace-id overview rows are supported
+     */
+    default boolean supportsTraceIdOverviewRows() {
+        return false;
+    }
+
+    /**
+     * Query trace overview statistics for a single trace id in the storage layer.
+     *
+     * @param traceId trace id
+     * @param start start time in epoch milliseconds
+     * @param end end time in epoch milliseconds
+     * @param errorOnly whether only error traces should be counted
+     * @param serviceName service name filter
+     * @param serviceNamespace service namespace filter
+     * @param environment deployment environment filter
+     * @param workspaceId workspace id filter
+     * @param resourceIdentityFilters canonical resource identity filters
+     * @param hideInternal whether internal HertzBeat traces should be hidden
+     * @return row with total_trace_count, error_trace_count, and latest_observed_at
+     */
+    default Map<String, Object> queryTraceIdOverviewRows(String traceId,
+                                                         Long start,
+                                                         Long end,
+                                                         Boolean errorOnly,
+                                                         String serviceName,
+                                                         String serviceNamespace,
+                                                         String environment,
+                                                         String workspaceId,
+                                                         Map<String, Set<String>> resourceIdentityFilters,
+                                                         Boolean hideInternal) {
+        return Map.of();
+    }
+
+    /**
+     * Whether this repository can aggregate entity trace summaries in storage.
+     *
+     * @return true when storage-owned entity trace summary rows are supported
+     */
+    default boolean supportsTraceSummaryRows() {
+        return false;
+    }
+
+    /**
+     * Query entity trace summary statistics in the storage layer.
+     *
+     * @param start start time in epoch milliseconds
+     * @param end end time in epoch milliseconds
+     * @param serviceName service name filter
+     * @param serviceNamespace service namespace filter
+     * @param environment deployment environment filter
+     * @param workspaceId workspace id filter
+     * @param resourceIdentityFilters canonical resource identity filters
+     * @param hideInternal whether internal HertzBeat traces should be hidden
+     * @return row with total_trace_count, error_trace_count, latest_observed_at, and latest_trace_id
+     */
+    default Map<String, Object> queryTraceSummaryRows(Long start,
+                                                      Long end,
+                                                      String serviceName,
+                                                      String serviceNamespace,
+                                                      String environment,
+                                                      String workspaceId,
+                                                      Map<String, Set<String>> resourceIdentityFilters,
+                                                      Boolean hideInternal) {
+        return Map.of();
+    }
+
     /**
      * Query service-to-service trace call graph rows with RED metrics aggregated by the storage layer.
      *
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
index 8946c2a377..9dd4d73fe4 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
@@ -174,6 +174,22 @@ public interface HistoryDataReader {
                 severityText, searchContent, offset, limit);
     }
 
+    /**
+     * Query logs with pagination, workspace scope, and storage-side noise filters when the backend supports them.
+     *
+     * @param workspaceId normalized workspace id that should own the returned logs
+     * @return filtered log entries with pagination
+     */
+    default List<LogEntry> queryLogsByMultipleConditionsWithPagination(Long startTime, Long endTime, String traceId,
+                                                                       String spanId, Integer severityNumber,
+                                                                       String severityText, String searchContent,
+                                                                       Integer offset, Integer limit,
+                                                                       Set<String> excludedServiceNames,
+                                                                       boolean requireServiceName,
+                                                                       String workspaceId) {
+        throw new UnsupportedOperationException("query workspace logs with pagination is not supported");
+    }
+
     /**
      * Count logs with multiple filter conditions (Legacy)
      */
@@ -216,6 +232,21 @@ public interface HistoryDataReader {
                 severityText, searchContent);
     }
 
+    /**
+     * Count logs with workspace scope and storage-side noise filters when the backend supports them.
+     *
+     * @param workspaceId normalized workspace id that should own the counted logs
+     * @return count of matching log entries
+     */
+    default long countLogsByMultipleConditions(Long startTime, Long endTime, String traceId,
+                                               String spanId, Integer severityNumber,
+                                               String severityText, String searchContent,
+                                               Set<String> excludedServiceNames,
+                                               boolean requireServiceName,
+                                               String workspaceId) {
+        throw new UnsupportedOperationException("count workspace logs is not supported");
+    }
+
     /**
      * Aggregate log severity buckets in the storage engine when supported.
      *
@@ -229,6 +260,21 @@ public interface HistoryDataReader {
         throw new UnsupportedOperationException("count log severity buckets is not supported");
     }
 
+    /**
+     * Aggregate log severity buckets with workspace scope in the storage engine when supported.
+     *
+     * @param workspaceId normalized workspace id that should own the aggregated logs
+     * @return a map with totalCount, fatalCount, errorCount, warnCount, infoCount, debugCount, traceCount
+     */
+    default Map<String, Long> countLogsBySeverityBuckets(Long startTime, Long endTime, String traceId,
+                                                         String spanId, Integer severityNumber,
+                                                         String severityText, String searchContent,
+                                                         Set<String> excludedServiceNames,
+                                                         boolean requireServiceName,
+                                                         String workspaceId) {
+        throw new UnsupportedOperationException("count workspace log severity buckets is not supported");
+    }
+
     /**
      * Aggregate log trace coverage in the storage engine when supported.
      *
@@ -242,6 +288,21 @@ public interface HistoryDataReader {
         throw new UnsupportedOperationException("count log trace coverage is not supported");
     }
 
+    /**
+     * Aggregate log trace coverage with workspace scope in the storage engine when supported.
+     *
+     * @param workspaceId normalized workspace id that should own the aggregated logs
+     * @return a map with withTrace, withoutTrace, withSpan, withBothTraceAndSpan
+     */
+    default Map<String, Long> countLogTraceCoverage(Long startTime, Long endTime, String traceId,
+                                                    String spanId, Integer severityNumber,
+                                                    String severityText, String searchContent,
+                                                    Set<String> excludedServiceNames,
+                                                    boolean requireServiceName,
+                                                    String workspaceId) {
+        throw new UnsupportedOperationException("count workspace log trace coverage is not supported");
+    }
+
     /**
      * Aggregate log counts by hour in the storage engine when supported.
      *
@@ -254,4 +315,19 @@ public interface HistoryDataReader {
                                              boolean requireServiceName) {
         throw new UnsupportedOperationException("count logs by hour is not supported");
     }
+
+    /**
+     * Aggregate log counts by hour with workspace scope in the storage engine when supported.
+     *
+     * @param workspaceId normalized workspace id that should own the aggregated logs
+     * @return a map keyed by yyyy-MM-dd HH:00
+     */
+    default Map<String, Long> countLogsByHour(Long startTime, Long endTime, String traceId,
+                                             String spanId, Integer severityNumber,
+                                             String severityText, String searchContent,
+                                             Set<String> excludedServiceNames,
+                                             boolean requireServiceName,
+                                             String workspaceId) {
+        throw new UnsupportedOperationException("count workspace logs by hour is not supported");
+    }
 }
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
index 594a2aea1a..86d6cc60ae 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
@@ -661,11 +661,23 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                                                       Integer offset, Integer limit,
                                                                       Set<String> excludedServiceNames,
                                                                       boolean requireServiceName) {
+        return queryLogsByMultipleConditionsWithPagination(startTime, endTime, traceId, spanId, severityNumber,
+                severityText, searchContent, offset, limit, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public List<LogEntry> queryLogsByMultipleConditionsWithPagination(Long startTime, Long endTime, String traceId,
+                                                                      String spanId, Integer severityNumber,
+                                                                      String severityText, String searchContent,
+                                                                      Integer offset, Integer limit,
+                                                                      Set<String> excludedServiceNames,
+                                                                      boolean requireServiceName,
+                                                                      String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT ").append(NATIVE_LOG_SELECT_COLUMNS)
                     .append(" FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
             sql.append(" ORDER BY timestamp DESC");
 
             // Add pagination
@@ -698,10 +710,21 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                              String severityText, String searchContent,
                                              Set<String> excludedServiceNames,
                                              boolean requireServiceName) {
+        return countLogsByMultipleConditions(startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public long countLogsByMultipleConditions(Long startTime, Long endTime, String traceId,
+                                             String spanId, Integer severityNumber,
+                                             String severityText, String searchContent,
+                                             Set<String> excludedServiceNames,
+                                             boolean requireServiceName,
+                                             String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT COUNT(*) as count FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
 
             List<Map<String, Object>> rows = greptimeSqlQueryExecutor.execute(sql.toString());
             if (rows != null && !rows.isEmpty()) {
@@ -723,6 +746,17 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                                         String severityText, String searchContent,
                                                         Set<String> excludedServiceNames,
                                                         boolean requireServiceName) {
+        return countLogsBySeverityBuckets(startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public Map<String, Long> countLogsBySeverityBuckets(Long startTime, Long endTime, String traceId,
+                                                        String spanId, Integer severityNumber,
+                                                        String severityText, String searchContent,
+                                                        Set<String> excludedServiceNames,
+                                                        boolean requireServiceName,
+                                                        String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT ")
                     .append("COUNT(*) as totalCount, ")
@@ -734,7 +768,7 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                     .append("SUM(CASE WHEN severity_number >= 1 AND severity_number <= 4 THEN 1 ELSE 0 END) as traceCount ")
                     .append("FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
             List<Map<String, Object>> rows = greptimeSqlQueryExecutor.execute(sql.toString());
             if (rows == null || rows.isEmpty()) {
                 return Map.of();
@@ -761,6 +795,17 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                                    String severityText, String searchContent,
                                                    Set<String> excludedServiceNames,
                                                    boolean requireServiceName) {
+        return countLogTraceCoverage(startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public Map<String, Long> countLogTraceCoverage(Long startTime, Long endTime, String traceId,
+                                                   String spanId, Integer severityNumber,
+                                                   String severityText, String searchContent,
+                                                   Set<String> excludedServiceNames,
+                                                   boolean requireServiceName,
+                                                   String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT ")
                     .append("COUNT(*) as totalCount, ")
@@ -770,7 +815,7 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                     .append("AND span_id IS NOT NULL AND span_id != '' THEN 1 ELSE 0 END) as withBothTraceAndSpan ")
                     .append("FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
             List<Map<String, Object>> rows = greptimeSqlQueryExecutor.execute(sql.toString());
             if (rows == null || rows.isEmpty()) {
                 return Map.of();
@@ -796,11 +841,22 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
                                              String severityText, String searchContent,
                                              Set<String> excludedServiceNames,
                                              boolean requireServiceName) {
+        return countLogsByHour(startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    @Override
+    public Map<String, Long> countLogsByHour(Long startTime, Long endTime, String traceId,
+                                             String spanId, Integer severityNumber,
+                                             String severityText, String searchContent,
+                                             Set<String> excludedServiceNames,
+                                             boolean requireServiceName,
+                                             String workspaceId) {
         try {
             StringBuilder sql = new StringBuilder("SELECT date_bin('1 hour', timestamp) as hour, ")
                     .append("COUNT(*) as count FROM ").append(LOG_TABLE_NAME);
             buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
-                    searchContent, excludedServiceNames, requireServiceName);
+                    searchContent, excludedServiceNames, requireServiceName, workspaceId);
             sql.append(" GROUP BY hour ORDER BY hour ASC");
             List<Map<String, Object>> rows = greptimeSqlQueryExecutor.execute(sql.toString());
             if (rows == null || rows.isEmpty()) {
@@ -891,6 +947,14 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
     private void buildWhereConditions(StringBuilder sql, Long startTime, Long endTime, String traceId,
                                      String spanId, Integer severityNumber, String severityText, String searchContent,
                                      Set<String> excludedServiceNames, boolean requireServiceName) {
+        buildWhereConditions(sql, startTime, endTime, traceId, spanId, severityNumber, severityText,
+                searchContent, excludedServiceNames, requireServiceName, null);
+    }
+
+    private void buildWhereConditions(StringBuilder sql, Long startTime, Long endTime, String traceId,
+                                     String spanId, Integer severityNumber, String severityText, String searchContent,
+                                     Set<String> excludedServiceNames, boolean requireServiceName,
+                                     String workspaceId) {
         List<String> conditions = new ArrayList<>();
 
         if (startTime != null && endTime != null) {
@@ -939,12 +1003,34 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
             }
         }
 
+        String workspaceCondition = workspaceCondition(workspaceId);
+        if (StringUtils.hasText(workspaceCondition)) {
+            conditions.add(workspaceCondition);
+        }
+
         // Add WHERE clause if there are conditions
         if (!conditions.isEmpty()) {
             sql.append(" WHERE ").append(String.join(" AND ", conditions));
         }
     }
 
+    private String workspaceCondition(String workspaceId) {
+        if (!StringUtils.hasText(workspaceId)) {
+            return null;
+        }
+        String normalizedWorkspaceId = safeString(workspaceId.trim());
+        String hertzbeatWorkspace = "json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]')";
+        String workspace = "json_get_string(resource_attributes, '$[\"workspace.id\"]')";
+        List<String> predicates = new ArrayList<>();
+        predicates.add(hertzbeatWorkspace + " = '" + normalizedWorkspaceId + "'");
+        predicates.add(workspace + " = '" + normalizedWorkspaceId + "'");
+        if ("default".equals(normalizedWorkspaceId)) {
+            predicates.add("((" + hertzbeatWorkspace + " IS NULL OR " + hertzbeatWorkspace + " = '')"
+                    + " AND (" + workspace + " IS NULL OR " + workspace + " = ''))");
+        }
+        return "(" + String.join(" OR ", predicates) + ")";
+    }
+
     private List<LogEntry> mapRowsToLogEntries(List<Map<String, Object>> rows) {
         List<LogEntry> list = new LinkedList<>();
         if (rows == null || rows.isEmpty()) {
diff --git a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepositoryTest.java b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepositoryTest.java
index ca798a4fb1..c0bd4b756e 100644
--- a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepositoryTest.java
+++ b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepositoryTest.java
@@ -198,6 +198,8 @@ class GreptimeTraceQueryRepositoryTest {
         assertTrue(sql.contains("FROM (SELECT trace_id"));
         assertTrue(sql.contains("SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
                 + "THEN 1 ELSE 0 END) AS error_span_count"));
+        assertEquals(1, sql.split(" AS resource_attributes", -1).length - 1,
+                "trace-list SQL must project resource_attributes with exactly one alias");
         assertTrue(sql.contains("FROM hzb_traces WHERE timestamp >= to_timestamp_millis(1710000000000)"));
         assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710003600000)"));
         assertTrue(sql.contains("service_name = 'checkout'"));
@@ -213,6 +215,143 @@ class GreptimeTraceQueryRepositoryTest {
         assertTrue(sql.endsWith("ORDER BY timestamp DESC LIMIT 20 OFFSET 40"));
     }
 
+    @Test
+    void queryTraceOverviewRowsPushesAggregateFiltersIntoGreptimeSql() {
+        when(greptimeSqlQueryExecutorProvider.getIfAvailable()).thenReturn(greptimeSqlQueryExecutor);
+        when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(List.of(Map.of(
+                "total_trace_count", 42L,
+                "error_trace_count", 7L,
+                "latest_observed_at", 1710003600000L)));
+
+        Map<String, Object> overview = repository.queryTraceOverviewRows(
+                1710000000000L,
+                1710003600000L,
+                true,
+                "checkout",
+                "commerce",
+                "prod",
+                "team-a",
+                Map.of("host.name", Set.of("checkout-1", "checkout-2")),
+                true);
+
+        assertEquals(42L, overview.get("total_trace_count"));
+        ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+        verify(greptimeSqlQueryExecutor, times(2)).execute(sqlCaptor.capture());
+        String sql = sqlCaptor.getValue();
+        assertTrue(sql.startsWith("SELECT COUNT(*) AS total_trace_count"));
+        assertTrue(sql.contains("SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count"));
+        assertTrue(sql.contains("MAX(trace_start_time) AS latest_observed_at"));
+        assertTrue(sql.contains("FROM (SELECT trace_id, MIN(timestamp) AS trace_start_time"));
+        assertTrue(sql.contains("SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END) AS error_span_count"));
+        assertTrue(sql.contains("FROM hzb_traces WHERE timestamp >= to_timestamp_millis(1710000000000)"));
+        assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710003600000)"));
+        assertTrue(sql.contains("service_name = 'checkout'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"service.namespace\"]') = 'commerce'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"deployment.environment.name\"]') "
+                + "= 'prod'"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a' "
+                + "OR json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a')"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-1' "
+                + "OR json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-2')"));
+        assertTrue(sql.contains("LOWER(service_name) NOT IN ('hertzbeat', 'apache-hertzbeat')"));
+        assertTrue(sql.contains("GROUP BY trace_id HAVING "
+                + "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') THEN 1 ELSE 0 END) > 0"));
+        assertTrue(sql.endsWith(") trace_overview"));
+    }
+
+    @Test
+    void queryTraceIdOverviewRowsPushesTraceIdAggregateFiltersIntoGreptimeSql() {
+        when(greptimeSqlQueryExecutorProvider.getIfAvailable()).thenReturn(greptimeSqlQueryExecutor);
+        when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(List.of(Map.of(
+                "total_trace_count", 1L,
+                "error_trace_count", 1L,
+                "latest_observed_at", 1710003600000L)));
+
+        Map<String, Object> overview = repository.queryTraceIdOverviewRows(
+                "trace-'filtered",
+                1710000000000L,
+                1710003600000L,
+                true,
+                "checkout",
+                "commerce",
+                "prod",
+                "team-a",
+                Map.of("host.name", Set.of("checkout-1", "checkout-2")),
+                true);
+
+        assertEquals(1L, overview.get("total_trace_count"));
+        ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+        verify(greptimeSqlQueryExecutor, times(2)).execute(sqlCaptor.capture());
+        String sql = sqlCaptor.getValue();
+        assertTrue(sql.startsWith("SELECT COUNT(*) AS total_trace_count"));
+        assertTrue(sql.contains("SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count"));
+        assertTrue(sql.contains("MAX(trace_start_time) AS latest_observed_at"));
+        assertTrue(sql.contains("FROM (SELECT trace_id, MIN(timestamp) AS trace_start_time"));
+        assertTrue(sql.contains("trace_id = 'trace-''filtered'"));
+        assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+        assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710003600000)"));
+        assertTrue(sql.contains("service_name = 'checkout'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"service.namespace\"]') = 'commerce'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"deployment.environment.name\"]') "
+                + "= 'prod'"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a' "
+                + "OR json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a')"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-1' "
+                + "OR json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-2')"));
+        assertTrue(sql.contains("LOWER(service_name) NOT IN ('hertzbeat', 'apache-hertzbeat')"));
+        assertTrue(sql.contains("GROUP BY trace_id HAVING "
+                + "SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') THEN 1 ELSE 0 END) > 0"));
+        assertTrue(sql.endsWith(") trace_id_overview"));
+    }
+
+    @Test
+    void queryTraceSummaryRowsPushesAggregateAndLatestTraceIntoGreptimeSql() {
+        when(greptimeSqlQueryExecutorProvider.getIfAvailable()).thenReturn(greptimeSqlQueryExecutor);
+        when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(List.of(Map.of(
+                "total_trace_count", 7L,
+                "error_trace_count", 2L,
+                "latest_observed_at", 1710003600000L,
+                "latest_trace_id", "trace-latest")));
+
+        Map<String, Object> summary = repository.queryTraceSummaryRows(
+                1710000000000L,
+                1710003600000L,
+                "checkout",
+                "commerce",
+                "prod",
+                "team-a",
+                Map.of("host.name", Set.of("checkout-1", "checkout-2")),
+                true);
+
+        assertEquals("trace-latest", summary.get("latest_trace_id"));
+        ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+        verify(greptimeSqlQueryExecutor, times(2)).execute(sqlCaptor.capture());
+        String sql = sqlCaptor.getValue();
+        assertTrue(sql.startsWith("SELECT summary.total_trace_count"));
+        assertTrue(sql.contains("summary.error_trace_count"));
+        assertTrue(sql.contains("latest.trace_start_time AS latest_observed_at"));
+        assertTrue(sql.contains("latest.trace_id AS latest_trace_id"));
+        assertTrue(sql.contains("SELECT COUNT(*) AS total_trace_count"));
+        assertTrue(sql.contains("SUM(CASE WHEN error_span_count > 0 THEN 1 ELSE 0 END) AS error_trace_count"));
+        assertTrue(sql.contains("FROM (SELECT trace_id, MIN(timestamp) AS trace_start_time"));
+        assertTrue(sql.contains("SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') "
+                + "THEN 1 ELSE 0 END) AS error_span_count"));
+        assertTrue(sql.contains("FROM hzb_traces WHERE timestamp >= to_timestamp_millis(1710000000000)"));
+        assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710003600000)"));
+        assertTrue(sql.contains("service_name = 'checkout'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"service.namespace\"]') = 'commerce'"));
+        assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"deployment.environment.name\"]') "
+                + "= 'prod'"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a' "
+                + "OR json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a')"));
+        assertTrue(sql.contains("(json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-1' "
+                + "OR json_get_string(resource_attributes, '$[\"host.name\"]') = 'checkout-2')"));
+        assertTrue(sql.contains("LOWER(service_name) NOT IN ('hertzbeat', 'apache-hertzbeat')"));
+        assertTrue(sql.contains("ORDER BY trace_start_time DESC LIMIT 1"));
+        assertTrue(sql.endsWith(") latest ON TRUE"));
+    }
+
     @Test
     void queryTraceServiceGraphRowsPushesServiceGraphRedAggregationIntoGreptimeSql() {
         when(greptimeSqlQueryExecutorProvider.getIfAvailable()).thenReturn(greptimeSqlQueryExecutor);
@@ -249,8 +388,12 @@ class GreptimeTraceQueryRepositoryTest {
         assertTrue(sql.contains("child.parent_span_id = parent.span_id"));
         assertTrue(sql.contains("child.timestamp >= to_timestamp_millis(1710000000000)"));
         assertTrue(sql.contains("child.timestamp <= to_timestamp_millis(1710003600000)"));
+        assertTrue(sql.contains("parent.timestamp >= to_timestamp_millis(1710000000000)"));
+        assertTrue(sql.contains("parent.timestamp <= to_timestamp_millis(1710003600000)"));
         assertTrue(sql.contains("json_get_string(child.resource_attributes, '$[\"deployment.environment.name\"]') "
                 + "= 'prod'"));
+        assertTrue(sql.contains("json_get_string(parent.resource_attributes, '$[\"deployment.environment.name\"]') "
+                + "= 'prod'"));
         assertTrue(sql.contains("((child.service_name = 'checkout-api' OR child.service_name = 'payment-api') "
                 + "OR (parent.service_name = 'checkout-api' OR parent.service_name = 'payment-api'))"));
         assertTrue(sql.contains("LOWER(child.service_name) NOT IN ('hertzbeat', 'apache-hertzbeat')"));
diff --git a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
index c20c019747..21ca023e5f 100644
--- a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
+++ b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
@@ -352,6 +352,51 @@ class GreptimeDbDataStorageTest {
         }
     }
 
+    @Test
+    void testQueryLogsCanPushDownWorkspaceScopeWithPagination() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(createNativeLogRows());
+
+            greptimeDbDataStorage.queryLogsByMultipleConditionsWithPagination(
+                    1710000000000L, 1710000060000L, null, null, null, null, "checkout", 40, 20,
+                    Set.of("otelcol-contrib"), true, "team-a");
+
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+            assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710000060000)"));
+            assertTrue(sql.contains("matches_term(body, 'checkout')"));
+            assertTrue(sql.contains("LOWER(service_name) NOT IN ('otelcol-contrib')"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+            assertTrue(sql.contains("ORDER BY timestamp DESC LIMIT 20 OFFSET 40"));
+        }
+    }
+
+    @Test
+    void testCountLogsCanPushDownWorkspaceScope() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString())).thenReturn(List.of(Map.of("count", 7L)));
+
+            long count = greptimeDbDataStorage.countLogsByMultipleConditions(
+                    1710000000000L, 1710000060000L, null, null, null, null, null,
+                    Set.of(), false, "team-a");
+
+            assertEquals(7L, count);
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("SELECT COUNT(*) as count FROM hertzbeat_logs"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+        }
+    }
+
     @Test
     void testLogStatsUseGreptimeAggregates() {
         try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
@@ -383,6 +428,42 @@ class GreptimeDbDataStorageTest {
         }
     }
 
+    @Test
+    void testLogStatsCanPushDownWorkspaceScope() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString()))
+                    .thenReturn(List.of(Map.of(
+                            "totalcount", 9L,
+                            "fatalcount", 0L,
+                            "errorcount", 1L,
+                            "warncount", 2L,
+                            "infocount", 6L,
+                            "debugcount", 0L,
+                            "tracecount", 0L)));
+
+            Map<String, Long> result = greptimeDbDataStorage.countLogsBySeverityBuckets(
+                    1710000000000L, 1710000060000L, null, null, null, null, "checkout",
+                    Set.of("otelcol-contrib"), true, "team-a");
+
+            assertEquals(9L, result.get("totalCount"));
+            assertEquals(1L, result.get("errorCount"));
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("COUNT(*) as totalCount"));
+            assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+            assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710000060000)"));
+            assertTrue(sql.contains("matches_term(body, 'checkout')"));
+            assertTrue(sql.contains("service_name IS NOT NULL"));
+            assertTrue(sql.contains("LOWER(service_name) NOT IN ('otelcol-contrib')"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+            assertFalse(sql.contains("ORDER BY"));
+        }
+    }
+
     @Test
     void testLogTraceCoverageUsesGreptimeAggregates() {
         try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
@@ -410,6 +491,38 @@ class GreptimeDbDataStorageTest {
         }
     }
 
+    @Test
+    void testLogTraceCoverageCanPushDownWorkspaceScope() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString()))
+                    .thenReturn(List.of(Map.of(
+                            "totalcount", 12L,
+                            "withtrace", 7L,
+                            "withspan", 6L,
+                            "withbothtraceandspan", 5L)));
+
+            Map<String, Long> result = greptimeDbDataStorage.countLogTraceCoverage(
+                    1710000000000L, 1710000060000L, null, null, null, null, "checkout",
+                    Set.of("otelcol-contrib"), true, "team-a");
+
+            assertEquals(7L, result.get("withTrace"));
+            assertEquals(5L, result.get("withoutTrace"));
+            assertEquals(5L, result.get("withBothTraceAndSpan"));
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+            assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710000060000)"));
+            assertTrue(sql.contains("matches_term(body, 'checkout')"));
+            assertTrue(sql.contains("LOWER(service_name) NOT IN ('otelcol-contrib')"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+            assertFalse(sql.contains("ORDER BY timestamp DESC"));
+        }
+    }
+
     @Test
     void testLogTrendUsesGreptimeHourAggregate() {
         try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
@@ -431,6 +544,34 @@ class GreptimeDbDataStorageTest {
         }
     }
 
+    @Test
+    void testLogTrendCanPushDownWorkspaceScope() {
+        try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> GreptimeDB.create(any())).thenReturn(greptimeDb);
+            greptimeDbDataStorage = new GreptimeDbDataStorage(greptimeProperties, restTemplate, greptimeSqlQueryExecutor);
+            when(greptimeSqlQueryExecutor.execute(anyString()))
+                    .thenReturn(List.of(Map.of("hour", 1777467600000000000L, "count", 12L)));
+
+            Map<String, Long> result = greptimeDbDataStorage.countLogsByHour(
+                    1710000000000L, 1710000060000L, null, null, null, null, "checkout",
+                    Set.of("otelcol-contrib"), true, "team-a");
+
+            assertEquals(12L, result.values().iterator().next());
+            ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+            verify(greptimeSqlQueryExecutor).execute(sqlCaptor.capture());
+            String sql = sqlCaptor.getValue();
+            assertTrue(sql.contains("date_bin('1 hour', timestamp) as hour"));
+            assertTrue(sql.contains("timestamp >= to_timestamp_millis(1710000000000)"));
+            assertTrue(sql.contains("timestamp <= to_timestamp_millis(1710000060000)"));
+            assertTrue(sql.contains("matches_term(body, 'checkout')"));
+            assertTrue(sql.contains("LOWER(service_name) NOT IN ('otelcol-contrib')"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"hertzbeat.workspace_id\"]') = 'team-a'"));
+            assertTrue(sql.contains("json_get_string(resource_attributes, '$[\"workspace.id\"]') = 'team-a'"));
+            assertTrue(sql.contains("GROUP BY hour ORDER BY hour ASC"));
+            assertFalse(sql.contains("SELECT timestamp, trace_id"));
+        }
+    }
+
     @Test
     void testQueryLogsWithPagination() {
         try (MockedStatic<GreptimeDB> mockedStatic = mockStatic(GreptimeDB.class)) {
diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index fc92222fca..99e0ce9978 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -231,15 +231,23 @@ describe('topology page', () => {
 
   it('keeps topology on the HertzBeat entity relationship surface instead of copied service-map chrome', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
+    const controllerSource = readFileSync(resolve(process.cwd(), 'lib/topology-surface/controller.ts'), 'utf8');
 
     expect(source).toContain('loadTopologyGraph');
     expect(source).toContain('resolveTopologyApiTimeoutMs');
-    expect(source).toContain('const TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;');
-    expect(source).toContain('const DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;');
-    expect(source).toContain('return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;');
-    expect(source).toContain('return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;');
+    expect(controllerSource).toContain('TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;');
+    expect(controllerSource).toContain('CMDB_TOPOLOGY_API_TIMEOUT_MS = 60000;');
+    expect(controllerSource).toContain('DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;');
+    expect(controllerSource).toContain('return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;');
+    expect(controllerSource).toContain('return CMDB_TOPOLOGY_API_TIMEOUT_MS;');
+    expect(controllerSource).toContain('return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;');
     expect(source).not.toContain('return 8000');
-    expect(source).toContain('{ timeoutMs: resolveTopologyApiTimeoutMs(routeContext) }');
+    expect(source).toContain('const [topologyManualRefreshSequence, setTopologyManualRefreshSequence]');
+    expect(source).toContain('const handleTopologyRefresh = React.useCallback');
+    expect(source).toContain('cacheBust: () => `manual-${topologyManualRefreshSequence}`');
+    expect(source).toContain('onClick: handleTopologyRefresh');
+    expect(source).toContain("'data-topology-refresh-action-behavior': 'in-page-api-reload'");
+    expect(source).toContain('timeoutMs: resolveTopologyApiTimeoutMs(routeContext)');
     expect(source).toContain('buildTopologyServiceMapFromApiGraph');
     expect(source).toContain('HzTopologyWorkbenchFrame');
     expect(source).toContain('HzTopologyWorkbenchHeader');
@@ -248,8 +256,14 @@ describe('topology page', () => {
     expect(source).toContain('HzTopologyCanvas');
     expect(source).toContain('HzTopologyCanvasAnnotation');
     expect(source).toContain('HzTopologyG6Canvas');
+    expect(source).toContain("from '@hertzbeat/ui/topology'");
+    expect(source).not.toContain("from '@hertzbeat/ui'");
     expect(source).toContain("from '@hertzbeat/ui/topology-g6'");
     expect(source).toContain("selectedFocusLabel={t('topology.view.focus-selected')}");
+    expect(source).toContain("edgeDensityDrilldownLabel={t('topology.edge-density.open-table')}");
+    expect(source).toContain('edgeDensityDrilldownTargetId="topology-metric-table"');
+    expect(source).toContain("nodeOnlyExplanationLabel={t('topology.node-only.explanation')}");
+    expect(readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8')).toContain("'topology.node-only.explanation'");
     expect(source).toContain('HzTopologyFocusTrail');
     expect(source).toContain('HzTopologyScopeBar');
     expect(source).toContain('HzTopologyToolbar');
@@ -276,10 +290,12 @@ describe('topology page', () => {
     expect(source).toContain('copyVisibility="assistive"');
     expect(source).not.toContain('data-topology-header-density="concise-operational"');
     expect(source).toContain('data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
-    expect(source).toContain('layout="canvas-only"');
+    expect(source).toContain("const topologyWorkbenchLayout = topologyShouldShowCompanionRail ? 'canvas-companion' : 'canvas-only';");
+    expect(source).toContain('layout={topologyWorkbenchLayout}');
     expect(source).toContain('data-topology-canvas-slot-owner="hertzbeat-ui-workbench-slot"');
     expect(source).toContain('data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"');
-    expect(source).toContain('data-topology-companion-rail-visibility="hidden-from-layout"');
+    expect(source).toContain("const topologyCompanionRailVisibility = topologyShouldShowCompanionRail ? 'visible-side-rail' : 'hidden-from-layout';");
+    expect(source).toContain('data-topology-companion-rail-visibility={topologyCompanionRailVisibility}');
     expect(source).toContain('kind="canvas"');
     expect(source).toContain('kind="companion"');
     expect(source).toContain('surface="content"');
@@ -334,7 +350,7 @@ describe('topology page', () => {
     expect(source).toContain('const map = React.useMemo(');
     expect(source).toContain('buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t)');
     expect(source).toContain('const topologyG6Graph = React.useMemo<HzTopologyG6GraphInput>(() => ({');
-    expect(source).toContain('focus: node.focus');
+    expect(source).toContain("focus: topologyCanShowApiFocus ? node.focus : 'normal'");
     expect(source).not.toContain('topologyCanvasFocusNodeId');
     expect(source).toContain('topologyG6Graph');
     expect(source).toContain('graph={topologyG6Graph}');
@@ -418,6 +434,46 @@ describe('topology page', () => {
     expect(source).not.toContain('Select Environment/s');
   });
 
+  it('keeps the route entry lightweight so topology page cold compilation can stream a shell first', () => {
+    const routeSource = readFileSync(resolve(process.cwd(), 'app/topology/page.tsx'), 'utf8');
+    const clientEntrySource = readFileSync(resolve(process.cwd(), 'app/topology/topology-route-client.tsx'), 'utf8');
+
+    expect(routeSource).toContain("import TopologyRouteClient from './topology-route-client'");
+    expect(routeSource).toContain("import TopologyRouteShell from './topology-route-shell'");
+    expect(routeSource).not.toContain("import TopologyPage from './topology-page'");
+    expect(routeSource).toContain('<TopologyRouteShell />');
+    expect(routeSource).toContain('<TopologyRouteClient routeContext={routeContext} shellElementId="topology-route-deferred-shell" />');
+    expect(clientEntrySource).toContain("dynamic(() => import('./topology-page')");
+    expect(clientEntrySource).toContain('ssr: false');
+    expect(clientEntrySource).toContain("from '@hertzbeat/ui/topology-g6-runtime'");
+    expect(clientEntrySource).toContain('void preloadHzTopologyG6Runtime();');
+    expect(clientEntrySource).toContain('preloadTopologyGraph(routeContext ?? {},');
+    expect(clientEntrySource).toContain('if (routeContext?.scaleProof?.trim()) return;');
+    expect(clientEntrySource).toContain('data-topology-route-client-scale-proof-prefetch="skipped"');
+    expect(clientEntrySource).toContain("data-topology-route-client-prefetch=\"topology-api-before-heavy-page\"");
+    expect(clientEntrySource).toContain("document.getElementById(shellElementId)?.setAttribute('hidden', '')");
+    expect(readFileSync(resolve(process.cwd(), 'app/topology/topology-route-shell.tsx'), 'utf8')).toContain(
+      'data-topology-route-shell="deferred-client-entry"'
+    );
+  });
+
+  it('keeps the optional G6 browser smoke configurable for alpha/local runs', () => {
+    const source = readFileSync(resolve(process.cwd(), 'scripts/topology-g6-browser-smoke.spec.ts'), 'utf8');
+
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_BASE_URL');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_USERNAME');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_PASSWORD');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_ROUTE');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_EXPECTED_NODES');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_EXPECTED_EDGES');
+    expect(source).toContain('TOPOLOGY_G6_BROWSER_FOCUS_NODE_ID');
+    expect(source).toContain('test.skip(!hasSmokeConfig');
+    expect(source).not.toContain("identifier: 'admin'");
+    expect(source).not.toContain("credential: 'hertzbeat'");
+    expect(source).not.toContain('entityId=642126742338816');
+    expect(source).not.toContain('Payment+API');
+  });
+
   it('keeps G6 single-click node and edge selection inside the topology page drawer', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
 
@@ -430,10 +486,10 @@ describe('topology page', () => {
     expect(source).toContain("setTopologyLocalSelection({ nodeId, edgeId: undefined, source: 'node-click' })");
     expect(source).toContain("setTopologyLocalSelection({ nodeId: undefined, edgeId, source: 'edge-click' })");
     expect(source).toContain("setTopologyLocalSelection({ nodeId: undefined, edgeId: row.id, source: 'table-row-click' })");
-    expect(source).toContain('const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map);');
+    expect(source).toContain('const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map, topologyCanvasEdges);');
     expect(source).toContain('const topologyCanvasSelectedEdgeId = topologyLocalSelection.nodeId');
     expect(source).toContain("? undefined");
-    expect(source).toContain(': topologyLocalSelectedEdge?.id ?? map.selectedEdgeId ?? topologyDetailEdge?.id;');
+    expect(source).toContain(': topologyLocalSelectedEdge?.id ?? map.selectedEdgeId;');
     expect(source).toContain('const topologyMetricSelectedRowId = topologyLocalSelection.nodeId');
     expect(source).toContain('selectedRowId={topologyMetricSelectedRowId}');
     expect(source).toContain('data-topology-selection-behavior="in-page-drawer"');
@@ -452,12 +508,16 @@ describe('topology page', () => {
     expect(source).toContain('data-topology-focus-route-preservation-owner="hertzbeat-ui-g6-focus-entry"');
     expect(source).toContain('data-topology-focus-route-preservation-source="loaded-api-graph-state"');
     expect(source).toContain('buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t)');
+    expect(source).toContain('shouldPreservePreviousTopologyGraphDuringLoad(routeContext ?? {})');
+    expect(source).toContain('const topologyShouldPreservePreviousGraphDuringLoad =');
+    expect(source).toContain('topologyManualRefreshSequence === 0 && shouldPreservePreviousTopologyGraphDuringLoad(routeContext ?? {})');
+    expect(source).toContain('if (!topologyShouldPreservePreviousGraphDuringLoad)');
+    expect(source).toContain('setLoadedApiGraph(undefined);');
     expect(source).toContain('navigateTopologyFocus(topologyNodeFocusHrefById.get(nodeId), topologyRouter.push);');
     expect(source).toContain('const handleTopologyFocusExit = React.useCallback');
     expect(source).toContain('event.preventDefault();');
     expect(source).toContain('navigateTopologyFocus(topologyFocusExitHref, topologyRouter.push);');
     expect(source).toContain('onClick: handleTopologyFocusExit');
-    expect(source).not.toContain('setLoadedApiGraph(undefined);');
     expect(source).not.toContain('window.location.href = href;');
     expect(source).not.toContain('const topologyNodeHrefById');
     expect(source).not.toContain('[topologyNodeHrefById]');
@@ -466,6 +526,18 @@ describe('topology page', () => {
     expect(source).not.toContain('navigateTopologySelection(topologyEdgeHrefById.get(row.id));');
   });
 
+  it('keeps selected path summary content ahead of hover-only preview content', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
+
+    expect(source).toContain('const topologySelectedPathSummaryEdge = topologyCanvasSelectedEdgeId');
+    expect(source).toContain('const topologyInvestigationEdge = topologySelectedPathSummaryEdge ?? topologyHoveredDetailEdge ?? topologyDetailEdge;');
+    expect(source).not.toContain('const topologyInvestigationEdge = topologyHoveredDetailEdge ?? topologyDetailEdge;');
+    expect(source).toContain('const topologyPathSummaryInteractionState = topologyCanvasSelectedEdgeId');
+    expect(source).toContain("? 'selected'");
+    expect(source).toContain(': topologyG6HoveredEdgeId');
+    expect(source).toContain("? 'hovered'");
+  });
+
   it('routes toolbar scope controls through soft navigation without clearing the G6 graph', () => {
     const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
 
@@ -474,7 +546,8 @@ describe('topology page', () => {
     expect(source).toContain('const topologyG6Layout = topologyLayout ===');
     expect(source).not.toContain('const handleTopologyLayoutChange = React.useCallback');
     expect(source).not.toContain('setTopologyLayout(value as TopologyLayoutMode);');
-    expect(source).toContain('const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(undefined);');
+    expect(source).toContain('const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(routeContext?.search);');
+    expect(source).toContain('setTopologySearchQuery(routeContext?.search);');
     expect(source).toContain('const topologyEffectiveSearchQuery = topologySearchQuery ?? map.filterContext.search;');
     expect(source).toContain("const topologyToolbarSearchQuery = topologySearchQuery ?? '';");
     expect(source).toContain('const topologyG6SearchQuery = topologyToolbarSearchQuery;');
@@ -496,7 +569,7 @@ describe('topology page', () => {
     expect(source).toContain('const handleTopologyGroupByChange = React.useCallback');
     expect(source).toContain("buildTopologyScopeHref(topologyG6ResetParams, { groupBy: value })");
     expect(source).toMatch(
-      /const topologyG6ResetParams = React\.useMemo\(\(\) => \{[\s\S]*if \(map\.filterContext\.groupBy !== 'none'\) params\.set\('groupBy', map\.filterContext\.groupBy\);/
+      /const topologyG6ResetParams = React\.useMemo\(\(\) => \{[\s\S]*if \(topologyHasRenderableGraph && map\.filterContext\.groupBy !== 'none'\) params\.set\('groupBy', map\.filterContext\.groupBy\);/
     );
     expect(source).toContain('const handleTopologyResetScope = React.useCallback');
     expect(source).toContain('event.preventDefault();');
@@ -517,7 +590,7 @@ describe('topology page', () => {
     expect(source).toContain("layout={topologyG6Layout}");
     expect(source).toContain('searchQuery={topologyG6SearchQuery}');
     expect(source).toContain("if (topologyToolbarSearchQuery) topologyG6SearchParams.set('search', topologyToolbarSearchQuery);");
-    expect(source).not.toContain('setLoadedApiGraph(undefined);');
+    expect(source).toContain('if (!topologyShouldPreservePreviousGraphDuringLoad)');
   });
 
   it('exposes initial in-page selection source markers for real topology browser checks', async () => {
@@ -528,6 +601,29 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-selection-source="none"');
     expect(html).toContain('data-topology-selection-node-id="none"');
     expect(html).toContain('data-topology-selection-edge-id="none"');
+    expect(html).toContain('data-topology-live-interaction-proof="wheel-hover-click-preserve-url-viewport"');
+    expect(html).toContain('data-topology-live-interaction-proof-owner="hertzbeat-ui-g6-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-live-interaction-owner="hertzbeat-ui-g6-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-live-interaction-invariants="no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-hz-topology-g6-edge-live-interaction-owner="hertzbeat-ui-g6-edge-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-edge-live-interaction-invariants="edge-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-topology-normal-browser-giant-node-proof="compact-1x-no-fill"');
+    expect(html).toContain('data-topology-normal-browser-giant-node-proof-owner="hertzbeat-ui-g6-viewport"');
+    expect(html).toContain('data-topology-ordinary-inspection-proof="api-current-window-no-stale-empty-gap"');
+    expect(html).toContain('data-topology-ordinary-inspection-proof-owner="hertzbeat-ui-workbench-frame"');
+    expect(html).toContain('data-hz-topology-g6-focused-edge="none"');
+    expect(html).toContain('data-hz-topology-g6-selected-node-rendered=');
+    expect(html).toContain('data-hz-topology-g6-selected-edge-rendered=');
+    expect(html).toContain('data-hz-topology-g6-selected-edge-count="0"');
+    expect(html).toContain('data-topology-path-summary="selected-edge-context"');
+    expect(html).toContain('data-topology-path-summary-interaction-state="preview"');
+    expect(html).toContain('data-hz-topology-path-summary-interaction-state="preview"');
+    expect(html).toContain('data-topology-path-summary-selected-edge-id="none"');
+    expect(html).toContain('data-topology-path-summary-hovered-edge-id="none"');
+    expect(html).toContain('data-hz-topology-path-selected-edge="none"');
+    expect(html).toContain('data-hz-topology-path-hovered-edge="none"');
+    expect(html).toContain('路径预览');
+    expect(html).not.toMatch(/data-hz-topology-path-summary-title-owner="hertzbeat-ui-path-summary-title"[^>]*>选中路径</);
     expect(html).toContain('data-topology-g6-mount-lifecycle-browser-regression="selection-preserve-viewport"');
     expect(html).toContain('data-topology-g6-mount-lifecycle-browser-regression-owner="hertzbeat-ui-g6-mount-lifecycle"');
     expect(html).toContain('data-topology-g6-mount-lifecycle-selection-policy="node-edge-table-drawer-only"');
@@ -554,6 +650,7 @@ describe('topology page', () => {
 
   it('renders HertzBeat topology sources, graph nodes, and operations closure actions', async () => {
     const { default: TopologyPage } = await import('./topology-page');
+    const source = readFileSync(resolve(process.cwd(), 'app/topology/topology-page.tsx'), 'utf8');
     const html = renderToStaticMarkup(<TopologyPage apiGraph={buildApiTopologyFixture()} />);
 
     expect(html).toContain('data-topology-route="hertzbeat-entity-topology"');
@@ -583,7 +680,8 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
     expect(html).toContain('data-hz-ui="topology-workbench-grid"');
     expect(html).toContain('data-hz-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
-    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-only"');
+    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('data-topology-companion-rail-visibility="visible-side-rail"');
     expect(html).toContain('data-topology-scope-bar-owner="hertzbeat-ui-scope-bar"');
     expect(html).toContain('data-topology-scope-bar-boundary-owner="hertzbeat-ui-scope-bar-boundary"');
     expect(html).toContain('data-hz-ui="topology-scope-bar"');
@@ -642,8 +740,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-focus-filter="time-range"');
     expect(html).toContain('data-hz-topology-focus-filter="source"');
     expect(html).toContain('data-hz-topology-focus-filter="view"');
-    expect(html).toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
-    expect(html).toContain('data-topology-focus-trail-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('data-topology-focus-trail-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('当前范围隐藏 0 个');
     expect(html).toContain('data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit"');
     expect(html).toContain('data-topology-focus-trail-exit-owner="hertzbeat-ui-focus-trail-exit"');
     expect(html).toMatch(
@@ -669,8 +768,14 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-primitive="path-summary"');
     expect(html).toContain('data-hz-topology-path-summary-boundary="section"');
     expect(html).toContain('data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"');
-    expect(html).toContain('data-hz-topology-path-selected-edge="svc-frontend--svc-checkout"');
-    expect(html).toContain('data-hz-topology-path-hovered-edge="svc-frontend--svc-checkout"');
+    expect(html).toContain('data-topology-path-summary-interaction-state="preview"');
+    expect(html).toContain('data-hz-topology-path-summary-interaction-state="preview"');
+    expect(html).toContain('data-topology-path-summary-selected-edge-id="none"');
+    expect(html).toContain('data-topology-path-summary-hovered-edge-id="none"');
+    expect(html).toContain('data-hz-topology-path-selected-edge="none"');
+    expect(html).toContain('data-hz-topology-path-hovered-edge="none"');
+    expect(html).toContain('路径预览');
+    expect(html).not.toMatch(/data-hz-topology-path-summary-title-owner="hertzbeat-ui-path-summary-title"[^>]*>选中路径</);
     expect(html).toContain('data-hz-topology-path-source-id="svc-frontend"');
     expect(html).toContain('data-hz-topology-path-target-id="svc-checkout"');
     expect(html).toContain('data-hz-topology-path-relation-type="trace-call"');
@@ -698,10 +803,13 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
     expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
     expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
+    expect(html).toContain('data-hz-topology-toolbar-control-flow="single-grid-row"');
+    expect(html).toContain('data-hz-topology-toolbar-empty-offset="none"');
+    expect(html).toContain('[grid-template-columns:112px_minmax(260px,1fr)_148px_88px_132px_auto]');
     expect(html).not.toContain('overflow-x-auto px-4 py-1');
     expect(html).toContain('overflow-x-auto px-0 py-1');
-    expect(html).toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
-    expect(html).not.toContain('h-7 !pl-2 !pr-1.5');
+    expect(html).toContain('h-7 !gap-1.5 !px-2');
+    expect(html).not.toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight-owner="hertzbeat-ui-toolbar-visual-weight"');
     expect(html).toContain('data-hz-topology-toolbar-secondary-visibility="assistive"');
@@ -723,6 +831,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-toolbar-control-strip="source-depth-group-reset"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-owner="hertzbeat-ui-toolbar-control-strip"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-display="contents"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
     expect(html).toContain('data-hz-topology-control="depth"');
     expect(html).toContain('data-hz-topology-control-depth-owner="hertzbeat-ui-toolbar-depth-control"');
@@ -767,7 +876,7 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-companion-rail-owner="hertzbeat-ui-companion-rail"');
     expect(html).toContain('data-topology-companion-rail-boundary-owner="hertzbeat-ui-companion-rail-boundary"');
     expect(html).toContain('data-topology-companion-rail-scope="legend-metrics-timeline-detail"');
-    expect(html).toContain('data-topology-companion-rail-visibility="hidden-from-layout"');
+    expect(html).toContain('data-topology-companion-rail-visibility="visible-side-rail"');
     expect(html).toContain('data-topology-companion-rail-priority="graph-first"');
     expect(html).toContain('data-hz-ui="topology-companion-rail"');
     expect(html).toContain('data-hz-topology-primitive="companion-rail"');
@@ -791,6 +900,15 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-companion-jump-list-scroll-scope-owner="hertzbeat-ui-companion-jump-list-scroll-scope"');
     expect(html).toContain('data-hz-topology-companion-jump-list-active-mode="contained-rail-scroll"');
     expect(html).toContain('data-hz-topology-companion-jump-list-active-mode-owner="hertzbeat-ui-companion-jump-list-active-mode"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-sync="manual-active-resets-scroll-active"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-sync-owner="hertzbeat-ui-companion-jump-list-selection-sync"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-scroll="active-section"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-scroll-owner="hertzbeat-ui-companion-jump-list-selection-scroll"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-url-policy="replace-active-section-hash"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-selection-url-policy-owner="hertzbeat-ui-companion-jump-list-selection-url-policy"');
+    expect(html).toContain('data-hz-topology-companion-jump-list-active-reset-key=');
+    expect(source).toContain("const topologyCompanionSelectionResetKey = topologyLocalSelection.source === 'none'");
+    expect(source).toContain('activeResetKey={topologyCompanionSelectionResetKey}');
     expect(html).toContain('data-hz-topology-companion-jump-item="view-mode"');
     expect(html).toContain('data-hz-topology-companion-jump-href="#topology-companion-view-mode"');
     expect(html).toContain('data-hz-topology-companion-jump-scroll-owner="hertzbeat-ui-companion-jump-scroll"');
@@ -831,7 +949,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-workbench-slot-kind="canvas"');
     expect(html).toContain('data-hz-topology-workbench-slot-kind="companion"');
     expect(html).toContain('data-hz-topology-workbench-slot-surface="content"');
-    expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness="none"');
+    expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness="sticky-with-companion"');
     expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness-owner="hertzbeat-ui-workbench-grid-canvas-stickiness"');
     expect(html).toContain('data-hz-ui="topology-canvas"');
     expect(html).toContain('data-hz-topology-primitive="canvas"');
@@ -880,12 +998,15 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-command-event="hz-topology-g6-viewport-command"');
     expect(html).toContain('data-hz-topology-g6-command-request="idle"');
     expect(html).toContain('data-hz-topology-g6-command-request-owner="hertzbeat-ui-g6-command-request"');
+    expect(html).toContain('data-hz-topology-g6-pan-selection-guard="drag-pan-suppresses-click-selection"');
+    expect(html).toContain('data-hz-topology-g6-pan-selection-guard-owner="hertzbeat-ui-g6-pan-selection-guard"');
     expect(html).toContain('data-hz-topology-g6-wheel-mode="manual-clamped-g6-zoom"');
     expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
     expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
     expect(html).toContain('data-hz-topology-g6-wheel-origin="pointer-clamped"');
-    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-2.2"');
+    expect(html).toContain('data-hz-topology-g6-wheel-zoom-bounds="0.18-1.35"');
     expect(html).toContain('data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"');
+    expect(html).toContain('data-hz-topology-g6-operator-zoom-tier="compact-readable"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts="plus-minus-zero-fit"');
     expect(html).toContain('data-hz-topology-g6-keyboard-shortcuts-owner="hertzbeat-ui-g6-keyboard"');
     expect(html).toContain('data-hz-topology-g6-keyboard-actions="zoom-in zoom-out reset-view fit-view"');
@@ -1000,6 +1121,8 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-legend-owner="hertzbeat-ui-legend"');
     expect(html).toContain('data-topology-g6-legend-owner="hertzbeat-ui-g6-legend-dock"');
     expect(html).toContain('data-hz-topology-g6-legend-dock="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-state="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-container="true"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-owner="hertzbeat-ui-g6-legend-dock"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-placement="canvas-bottom-left"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-occlusion="inside-canvas-low-interruption"');
@@ -1014,44 +1137,41 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
     expect(html).toContain('data-hz-topology-legend-title-owner="hertzbeat-ui-legend-title"');
     expect(html).toContain('data-hz-topology-legend-summary-owner="hertzbeat-ui-legend-summary"');
-	    expect(html).toContain('data-hz-topology-legend-section="status"');
-	    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
-	    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
-	    expect(html).toContain('data-hz-topology-legend-section="node-type"');
-	    expect(html).toContain('data-hz-topology-legend-section="interaction"');
-	    expect(html).not.toContain('data-hz-topology-legend-section="source-kind"');
-	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
-	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
-	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
-	    expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
-	    expect(html).toContain('data-hz-topology-legend-item="warning-node"');
-	    expect(html).toContain('data-hz-topology-legend-item="critical-node"');
-    expect(html).toContain('data-hz-topology-legend-item="selected-node"');
-    expect(html).toContain('data-hz-topology-legend-item="directional-edge"');
-    expect(html).toContain('data-hz-topology-legend-item="dimmed-edge"');
-	    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
-	    expect(html).toContain('data-hz-topology-legend-color="#f59e0b"');
-	    expect(html).toContain('data-hz-topology-legend-color="#ef4444"');
-	    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
-	    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
-	    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
-	    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
-    expect(html).toContain('data-hz-topology-legend-item-value-owner="hertzbeat-ui-legend-item-value"');
+    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
+    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
+    expect(html).toContain('data-hz-topology-legend-section="node-type"');
+    expect(html).not.toContain('data-hz-topology-legend-section="status"');
+    expect(html).not.toContain('data-hz-topology-legend-section="interaction"');
+    expect(html).not.toContain('data-hz-topology-legend-section="source-kind"');
+    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
+    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="server-cog"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
+    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+    expect(html).not.toContain('data-hz-topology-legend-item="healthy-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="warning-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="critical-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="selected-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="directional-edge"');
+    expect(html).not.toContain('data-hz-topology-legend-item="dimmed-edge"');
+    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
+    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
+    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
+    expect(html).not.toContain('data-hz-topology-legend-item-value-owner="hertzbeat-ui-legend-item-value"');
     expect(html).not.toContain('data-hz-topology-legend-pattern="dashed"');
     expect(html).toContain('data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"');
     expect(html).toContain('data-topology-detail-drawer-surface-owner="hertzbeat-ui-detail-surface"');
@@ -1151,7 +1271,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-action-link-copy-owner="hertzbeat-ui-action-link-copy"');
     expect(html).toContain('查看实体详情');
     expect(html).toContain('打开三信号');
-    expect(html).toContain('data-topology-node-select-href="/topology?');
+    expect(html).toContain('data-topology-node-select-mode="in-page-drawer"');
+    expect(html).toContain('data-topology-node-select-url-policy="preserve-current-url"');
+    expect(html).not.toContain('data-topology-node-select-href=');
     expect(html).toContain('data-topology-node-entity-href="/entities/service%3Acommerce%2Fcheckout?');
     expect(html).toContain('data-topology-context-link="entity"');
     expect(html).toContain('data-topology-context-link="metrics"');
@@ -1260,7 +1382,7 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-edge-id="relation-700"');
     expect(html).not.toContain('data-topology-node-id="svc-checkout"');
     expect(html).not.toContain('checkout-api');
-  }, 30000);
+  }, 60000);
 
   it('maps focused 1-hop routes into shared FocusTrail handoff metadata and scoped exit hrefs', async () => {
     const { default: TopologyPage } = await import('./topology-page');
@@ -1287,14 +1409,22 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-focus-trail-depth="1"');
     expect(html).toContain('data-hz-topology-focus-trail-entity="svc-checkout"');
     expect(html).toContain('data-hz-topology-focus-crumb-active="true"');
+    expect(html).toContain('data-topology-depth-state="1"');
+    expect(html).toMatch(
+      /<a(?=[^>]*data-hz-topology-focus-crumb="active-entity")[\s\S]*?<span[^>]*data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value">1 跳<\/span>/
+    );
+    expect(html).not.toMatch(
+      /<a(?=[^>]*data-hz-topology-focus-crumb="active-entity")[\s\S]*?<span[^>]*data-hz-topology-focus-crumb-value-owner="hertzbeat-ui-focus-trail-crumb-value">2 跳<\/span>/
+    );
     expect(html).toContain('data-hz-topology-focus-filter="source"');
     expect(html).toContain('data-topology-focus-filter-source="cmdb-manual-label"');
     expect(html).toMatch(
-      /<a(?=[^>]*data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit")(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*environment=prod)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*timeRange=last-30m)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*sourceKind=cmdb-manual-label)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*groupBy=source-kind)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*depth=2)(?=[^>]*data-hz-topol [...]
+      /<a(?=[^>]*data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit")(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*environment=prod)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*timeRange=last-30m)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*sourceKind=cmdb-manual-label)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*groupBy=source-kind)(?=[^>]*data-hz-topology-focus-exit-href="\/topology\?[^"]*depth=1)(?=[^>]*data-hz-topol [...]
     );
+    expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*depth=2/);
     expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*entityId=/);
     expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*serviceName=/);
-  }, 30000);
+  }, 60000);
 
   it('forwards groupBy route scope into the shared G6 grouping metadata and controls', async () => {
     const { default: TopologyPage } = await import('./topology-page');
@@ -1320,6 +1450,10 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-group-surface-owner="hertzbeat-ui-g6-group-surface"');
     expect(html).toContain('data-hz-topology-g6-group-surface-visibility="assistive"');
     expect(html).not.toContain('absolute left-3 top-12 flex');
+    expect(html).toContain('data-hz-topology-g6-filter-visible-node-count="2"');
+    expect(html).toContain('data-hz-topology-g6-filter-visible-edge-count="1"');
+    expect(html).toContain('2 个可见节点 · 1 条可见边');
+    expect(html).not.toContain('7 个可见节点 · 7 条可见边');
     expect(html).toContain('data-hz-topology-g6-group-item="otlp-trace-call"');
     expect(html).toContain('data-hz-topology-g6-group-item-action-owner="hertzbeat-ui-g6-group-item-action"');
     expect(html).toMatch(
@@ -1343,17 +1477,17 @@ describe('topology page', () => {
       /<a(?=[^>]*data-topology-view-mode="resource-dependency")(?=[^>]*href="\/topology\?[^"]*viewMode=resource-dependency)(?=[^>]*href="\/topology\?[^"]*groupBy=source-kind)[^>]*>/
     );
     expect(html).toMatch(
-      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-hz-topology-g6-metadata-role="selection-button")(?=[^>]*data-topology-node-select-href="\/topology\?[^"]*groupBy=source-kind)[^>]*>/
+      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-hz-topology-g6-metadata-role="selection-button")(?=[^>]*data-topology-node-select-mode="in-page-drawer")(?=[^>]*data-topology-node-select-url-policy="preserve-current-url")[^>]*>/
     );
     expect(html).toContain('data-hz-topology-g6-focus-entry="double-click-node"');
     expect(html).toContain('data-hz-topology-g6-focus-depth-target="1-hop"');
     expect(html).toMatch(
-      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-topology-node-select-href="\/topology\?[^"]*depth=2)(?=[^>]*data-topology-node-focus-href="\/topology\?[^"]*depth=1)[^>]*>/
+      /<button(?=[^>]*data-topology-node-id="svc-frontend")(?=[^>]*data-topology-node-select-mode="in-page-drawer")(?=[^>]*data-topology-node-focus-href="\/topology\?[^"]*depth=1)[^>]*>/
     );
     expect(html).toMatch(
       /data-topology-current-entity-panel="svc-checkout"[^>]*data-topology-current-entity-group-by="source-kind"[^>]*data-topology-current-entity-source-kind="otlp-trace-call"[^>]*data-topology-current-entity-view-mode="application"/
     );
-  }, 30000);
+  }, 60000);
 
   it('exposes focused API graph depth, relationship type, health, and evidence badges for browser verification', async () => {
     const { default: TopologyPage } = await import('./topology-page');
@@ -1435,7 +1569,7 @@ describe('topology page', () => {
     expect(html).toMatch(/data-topology-node-id="entity-503"[^>]+data-topology-node-health="critical"[^>]+data-topology-node-evidence-badges="entity-relation database-middleware-connection two-hop"/);
     expect(html).toMatch(/data-topology-edge-id="relation-701"[^>]+data-topology-edge-relationship-type="trace-call"[^>]+data-topology-edge-evidence-badges="entity-relation otlp-trace-call one-hop"/);
     expect(html).toMatch(/data-topology-edge-id="relation-702"[^>]+data-topology-edge-relationship-type="database-connection"[^>]+data-topology-edge-evidence-badges="entity-relation database-middleware-connection two-hop"/);
-  }, 30000);
+  }, 60000);
 
   it('exposes trace-derived service graph RED metrics on nodes, edges, and selected evidence', async () => {
     const { default: TopologyPage } = await import('./topology-page');
@@ -1549,6 +1683,21 @@ describe('topology page', () => {
     expect(html).toContain('7.25/s');
     expect(html).toContain('2.1%');
     expect(html).toContain('123 ms');
+    expect(html).toContain('data-hz-topology-legend-section="node-type"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-database"');
+    expect(html).not.toContain('data-hz-topology-legend-section="status"');
+    expect(html).not.toContain('data-hz-topology-legend-section="interaction"');
+    expect(html).not.toContain('data-hz-topology-legend-item="healthy-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="warning-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="critical-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="selected-node"');
+    expect(html).not.toContain('data-hz-topology-legend-item="directional-edge"');
+    expect(html).not.toContain('data-hz-topology-legend-item="dimmed-edge"');
+    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="server-cog"');
+    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
     expect(html).toContain('data-topology-metric-table-owner="hertzbeat-ui"');
     expect(html).toContain('data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"');
     expect(html).toContain('data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"');
@@ -1562,6 +1711,10 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-metric-table-header-owner="hertzbeat-ui-metric-table-header"');
     expect(html).toContain('data-hz-topology-metric-rows="1"');
     expect(html).toContain('data-hz-topology-metric-table-interaction="row-select-detail"');
+    expect(html).toContain('data-hz-topology-metric-table-live-selection-owner="hertzbeat-ui-metric-table-selection"');
+    expect(html).toContain(
+      'data-hz-topology-metric-table-live-selection-invariants="row-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"'
+    );
     expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="direct"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="inactive"');
@@ -1573,8 +1726,12 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-metric-table-filter-behavior="in-page-no-route-change"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter="all"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-metric-table-filtered-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control-owner="hertzbeat-ui-metric-table-filter-control"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-selection-policy="preserve-selected-edge"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
@@ -1585,6 +1742,9 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-edge-row-target-visible="true"');
     expect(html).toContain('data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"');
     expect(html).toContain('data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-owner="hertzbeat-ui-metric-table-row-selection"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-mode="table-row-click-drawer"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-edge-row="relation-701"');
     expect(html).toContain('data-hz-topology-edge-selected="true"');
     expect(html).toContain('data-hz-topology-metric-table-route-owner="hertzbeat-ui-metric-table-route"');
@@ -1614,26 +1774,262 @@ describe('topology page', () => {
     const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildLargeApiTopologyFixture()} />);
 
     expect(source).toContain('const topologyShouldShowRenderWindowMetricTable =');
+    expect(source).toContain('const topologyShouldRenderCompanionMetricTable = !topologyShouldShowRenderWindowMetricTable;');
     expect(source).toContain("topologyRenderWindowCompanion.mode === 'windowed'");
     expect(source).toContain("topologyRenderWindowCompanion.tableCompanion === 'required'");
+    expect(source).toContain('renderedEdgeCount: topologyG6RenderedEdgeCount');
+    expect(source).toContain('totalEdgeCount: topologyG6Graph.edges.length');
+    expect(source).toContain('const topologyG6HiddenEdgeCount = Math.max(0, topologyG6Graph.edges.length - topologyG6RenderedEdgeCount);');
     expect(html).toContain('data-hz-topology-g6-render-window-mode="windowed"');
     expect(html).toContain('data-hz-topology-g6-render-window-total-node-count="230"');
     expect(html).toContain('data-hz-topology-g6-render-window-hidden-node-count="30"');
+    expect(html).toContain('data-hz-topology-g6-render-window-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-g6-render-window-candidate-edge-count=');
+    expect(html).toContain(
+      'data-hz-topology-g6-render-window-rendered-edge-count-owner="hertzbeat-ui-g6-edge-density"'
+    );
+    expect(html).toContain('data-topology-g6-render-proof-owner="hertzbeat-topology-page-g6-proof"');
+    expect(html).toContain('data-topology-g6-render-window-mode="windowed"');
+    expect(html).toContain('data-topology-g6-render-window-total-node-count="230"');
+    expect(html).toContain('data-topology-g6-render-window-rendered-node-count="200"');
+    expect(html).toContain('data-topology-g6-render-window-total-edge-count="229"');
+    expect(html).toContain('data-topology-g6-render-window-rendered-edge-count="180"');
+    expect(html).toContain('data-topology-g6-render-window-hidden-edge-count="49"');
+    expect(html).toContain('data-topology-g6-summary-count-policy="windowed-rendered-vs-total"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-owner="hertzbeat-ui-g6-scale-performance"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-policy="windowed-interactive-budget"');
+    expect(html).toContain(
+      'data-hz-topology-g6-scale-performance-invariants="windowed-render pan-zoom-no-url-change no-remount no-refit viewport-preserved render-key-stable table-companion"'
+    );
+    expect(html).toContain('data-hz-topology-g6-scale-performance-rendered-node-budget="200"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-rendered-edge-budget="180"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-owner="hertzbeat-ui-g6-structure-key-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-policy="short-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-length=');
+    expect(html).not.toContain('scale-svc-000:service|');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-owner="hertzbeat-ui-g6-render-key-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-policy="short-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-length=');
+    expect(html).not.toContain('scale-svc-000::#');
+    expect(html).toContain('data-hz-topology-g6-viewport-fit-state="pending"');
+    expect(html).toContain('data-hz-topology-g6-viewport-fit-state-owner="hertzbeat-ui-g6-fit-settle-state"');
+    expect(html).toContain('data-hz-topology-g6-summary-count-policy="windowed-rendered-vs-total"');
+    expect(html).toContain('data-hz-topology-g6-summary-rendered-node-count="200"');
+    expect(html).toContain('data-hz-topology-g6-summary-total-node-count="230"');
+    expect(html).toContain('data-hz-topology-g6-summary-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-g6-summary-total-edge-count="229"');
+    expect(html).toContain('200/230 个节点已渲染 · 180/229 条边已渲染');
+    expect(html).not.toContain('230 个可见节点 · 229 条可见边');
     expect(html).toContain('data-topology-metric-table-placement="graph-bottom"');
     expect(html).toContain('data-topology-metric-table-visibility="render-window-companion"');
     expect(html).toContain('data-topology-metric-table-scope="edge-red-render-window"');
+    expect(html).toContain('data-topology-metric-table-dom-policy="single-interactive-table"');
+    expect(html).toContain('data-hz-topology-frame="workbench"');
+    expect(html).toContain('data-hz-topology-api-state="ready"');
+    expect(html).toContain('data-hz-topology-metric-table-root="true"');
+    expect(html).toContain('data-hz-topology-metric-table-total-rows="229"');
+    expect(html).not.toContain('data-topology-metric-table-scope="edge-red-companion"');
+    expect(html.match(/data-hz-ui="topology-metric-table"/g) ?? []).toHaveLength(1);
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-live-owner="hertzbeat-ui-g6-edge-density-affordance"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-invariants="table-scroll no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(source).toContain("edgeDensityDrilldownDetailLabel={t('topology.edge-density.hidden-in-table'");
+    expect(readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8')).toContain("'topology.edge-density.hidden-in-table'");
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-explanation-policy="render-window-not-missing-edges"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-detail-label=');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="230"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-hidden-node-count="30"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-total-edge-count="229"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-metric-table-edge-count-policy="canvas-rendered-vs-table-total"');
+    expect(html).toContain('data-hz-topology-metric-table-canvas-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-metric-table-table-edge-count="229"');
+    expect(html).toContain('data-hz-topology-metric-table-edge-summary-owner="hertzbeat-ui-metric-table-edge-summary"');
+    expect(html).toContain('画布 180 / 表格 229');
+    expect(readFileSync(resolve(process.cwd(), 'lib/i18n-runtime-messages.ts'), 'utf8')).toContain("'topology.metric-table.edge-summary'");
     expect(html).toContain('data-hz-topology-metric-table-render-window-table-companion="required"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="required"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-policy="windowed-dom-budget"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-budget="120"');
+    expect(html).toContain('data-hz-topology-metric-table-rendered-row-count="120"');
+    expect(html).toContain('data-hz-topology-metric-table-rendered-hidden-row-count="109"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-summary-owner="hertzbeat-ui-metric-table-row-render-summary"');
+    expect(html).toContain('显示 120 / 229 条关系');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
-    expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="hidden"');
+    expect(html).toContain('两端在画布');
+    expect(html).toContain('一端在画布');
+    expect(html).toContain('画布外');
+    expect(html).toContain('端点未知');
+    expect(html).not.toContain('半可见');
+    const renderedMetricRows = html.match(/data-hz-topology-edge-row="/g) ?? [];
+    const metricRowCountMatch = html.match(/data-hz-topology-metric-rows="(\d+)"/);
+    const metricRenderedRowCountMatch = html.match(/data-hz-topology-metric-table-rendered-row-count="(\d+)"/);
+    expect(metricRowCountMatch?.[1]).toBe('229');
+    expect(metricRenderedRowCountMatch?.[1]).toBe(String(renderedMetricRows.length));
     expect(html.indexOf('data-topology-g6-canvas-owner="hertzbeat-ui-g6-canvas"')).toBeLessThan(
       html.indexOf('data-topology-metric-table-placement="graph-bottom"')
     );
   }, 60000);
 
+  it('explains selected table edges that are only partly present in the current G6 render window', async () => {
+    const routeContext = {
+      environment: 'prod',
+      timeRange: 'last-1h',
+      sourceKind: 'otlp-trace-call',
+      viewMode: 'service-call',
+      groupBy: 'source-kind',
+      depth: '2',
+      edgeId: 'scale-svc-199--scale-svc-200'
+    };
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildLargeApiTopologyFixture()} />);
+
+    expect(html).toContain('data-topology-active-edge-id="scale-svc-199--scale-svc-200"');
+    expect(html).toContain('data-topology-edge-evidence-panel="scale-svc-199--scale-svc-200"');
+    expect(html).toContain('data-topology-edge-render-window-visibility="partial"');
+    expect(html).toContain('data-topology-edge-render-window-source-visible="true"');
+    expect(html).toContain('data-topology-edge-render-window-target-visible="false"');
+    expect(html).toContain('data-topology-edge-render-window-fact="partial"');
+    expect(html).toContain('data-topology-edge-render-window-fact-owner="hertzbeat-ui-detail-render-window-context"');
+    expect(html).toContain('画布窗口');
+    expect(html).toContain('一端在画布');
+  }, 60000);
+
+  it('does not turn an overview route into a focused graph when the API returns a default focus entity', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'cmdb-manual-label',
+          viewMode: 'application',
+          groupBy: 'source-kind',
+          depth: '2'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 501,
+          depth: 2,
+          sourceKinds: ['cmdb-manual-label'],
+          nodes: [
+            {
+              id: '501',
+              entityId: 501,
+              entityName: 'Payment API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['entity-relation', 'manual']
+            },
+            {
+              id: '502',
+              entityId: 502,
+              entityName: 'Orders DB',
+              entityType: 'database',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation', 'manual']
+            }
+          ],
+          edges: []
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-active-node-id="none"');
+    expect(html).toContain('data-hz-topology-focus-trail-mode="overview"');
+    expect(html).toContain('data-hz-topology-focus-trail-entity="all"');
+    expect(html).toContain('data-topology-focus-state="all"');
+    expect(html).not.toContain('data-hz-topology-focus-exit-owner="hertzbeat-ui-focus-trail-exit"');
+    expect(html).not.toContain('data-topology-focus-trail-exit-owner="hertzbeat-ui-focus-trail-exit"');
+    expect(html).not.toContain('data-hz-topology-g6-selected-node-id="entity-501"');
+    expect(html).not.toMatch(
+      /data-hz-topology-focus-crumb-active="true"[\s\S]*?<span[^>]*data-hz-topology-focus-crumb-label-owner="hertzbeat-ui-focus-trail-crumb-label">Payment API<\/span>/
+    );
+  }, 30000);
+
+  it('marks API-backed isolated nodes as a relationship evidence gap instead of implying G6 dropped edges', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          viewMode: 'application',
+          groupBy: 'source-kind',
+          depth: '2'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: null,
+          depth: 2,
+          sourceKinds: ['entity-relation'],
+          nodes: [
+            {
+              id: '642126742338816',
+              entityId: 642126742338816,
+              entityName: 'Payment API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['entity-relation']
+            },
+            {
+              id: '642126752570624',
+              entityId: 642126752570624,
+              entityName: 'Checkout API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation']
+            },
+            {
+              id: '642126762803200',
+              entityId: 642126762803200,
+              entityName: 'Orders DB',
+              entityType: 'database',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'healthy',
+              evidenceBadges: ['entity-relation']
+            }
+          ],
+          edges: []
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-relation-gap-state="nodes-without-edges"');
+    expect(html).toContain('data-topology-relation-gap-owner="hertzbeat-ui-relation-gap"');
+    expect(html).toContain('data-topology-relation-gap-node-count="3"');
+    expect(html).toContain('data-topology-relation-gap-edge-count="0"');
+    expect(html).toContain('data-topology-relation-gap-source-kind="all"');
+    expect(html).toContain('data-topology-relation-gap-api-source-kinds="entity-relation"');
+    expect(html).toContain('data-topology-relation-gap-view-mode="application"');
+    expect(html).toContain('data-topology-relation-gap-visual="canvas-annotation"');
+    expect(html).toContain('data-topology-relation-gap-canvas-policy="hide-isolated-node-only-graph"');
+    expect(html).toContain('data-hz-topology-g6-node-count="0"');
+    expect(html).toContain('data-hz-topology-g6-edge-count="0"');
+    expect(html).toContain('data-topology-relation-gap-action="otlp-trace-call"');
+    expect(html).toContain('data-topology-relation-gap-action-owner="hertzbeat-ui-action-link"');
+    expect(html).toContain('data-topology-relation-gap-action-source-kind="otlp-trace-call"');
+    expect(html).toContain('data-topology-relation-gap-action-view-mode="service-call"');
+    expect(html).toContain('sourceKind=otlp-trace-call');
+    expect(html).toContain('viewMode=service-call');
+    expect(html).toContain('relationType=trace-call');
+    expect(html).toContain('data-topology-empty-state="none"');
+    expect(html).toContain('未返回关系边');
+    expect(html).toContain('查看调用关系');
+  }, 60000);
+
   it('renders API-backed impact timeline evidence when topology returns change events', async () => {
     i18nState.locale = 'en-US';
     const { default: TopologyPage } = await import('./topology-page');
@@ -1740,7 +2136,8 @@ describe('topology page', () => {
           environment: 'prod',
           timeRange: 'last-1h',
           sourceKind: 'otlp-trace-call',
-          relationType: 'trace-call'
+          relationType: 'trace-call',
+          groupBy: 'source-kind'
         }}
         apiGraph={{
           apiBacked: true,
@@ -1761,6 +2158,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-primitive="empty-state"');
     expect(html).toContain('data-hz-topology-empty-boundary="canvas"');
     expect(html).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
+    expect(html).toContain('data-hz-topology-empty-boundary-visual="frameless-canvas"');
     expect(html).toContain('data-hz-topology-empty-kind="filtered-empty"');
     expect(html).toContain('data-hz-topology-empty-source="Greptime trace graph"');
     expect(html).toContain('data-hz-topology-empty-time-scope="last-1h"');
@@ -1772,7 +2170,36 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-empty-depth="2"');
     expect(html).toContain('data-hz-topology-empty-result-count="0"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace"');
+    expect(html).toContain('data-hz-topology-empty-copy-visibility="assistive"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-hz-topology-g6-minimap-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-hz-topology-g6-summary-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-hz-topology-g6-group-surface-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-hz-topology-g6-filter-control-surface-visibility="hidden-empty-graph"');
+    expect(html).toContain('data-topology-toolbar-group-control-visibility="hidden-empty-graph"');
+    expect(html).not.toContain('data-hz-topology-control-group-owner="hertzbeat-ui-toolbar-group-control"');
+    expect(html).not.toContain('data-hz-topology-control="group-by"');
+    expect(html).not.toContain('data-hz-topology-state-item="group"');
+    expect(html).not.toContain('data-topology-group-state=');
+    expect(html).not.toMatch(/data-hz-topology-focus-exit-href="[^"]*groupBy=source-kind/);
+    expect(html).not.toMatch(/<a(?=[^>]*data-hz-topology-control="reset-scope")(?=[^>]*href="[^"]*groupBy=source-kind)[^>]*>/);
+    expect(html).not.toContain('data-hz-topology-g6-toolbar-visible-actions="zoom-out zoom-in fit-view reset-view"');
+    expect(html).not.toContain('data-hz-topology-g6-action="zoom-out"');
+    expect(html).not.toContain('data-hz-topology-g6-action="fit-view"');
+    expect(html).not.toContain('data-hz-topology-g6-minimap="viewport-overview"');
+    expect(html).not.toContain('data-hz-topology-g6-summary-owner="hertzbeat-ui-g6-summary"');
+    expect(html).not.toContain('data-hz-topology-g6-group-surface-owner="hertzbeat-ui-g6-group-surface"');
+    expect(html).not.toContain('data-hz-topology-g6-filter-control-surface-owner="hertzbeat-ui-g6-filter-control-surface"');
+    expect(html).not.toContain('data-hz-topology-g6-filter-control="source-kind"');
+    expect(html).not.toContain('data-hz-ui="topology-companion-rail"');
+    expect(html).not.toContain('data-topology-companion-section=');
+    expect(html).not.toContain('data-topology-metric-table-owner="hertzbeat-ui"');
+    expect(html).not.toContain('data-hz-ui="topology-metric-table"');
+    expect(html).not.toContain('data-hz-ui="topology-detail-drawer"');
+    expect(html).not.toContain('data-topology-current-entity-panel=');
     expect(html).toContain('暂无 trace 调用拓扑证据');
+    expect(html).toContain('当前窗口无 trace 调用边。');
+    expect(html).not.toContain('Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。');
     expect(html).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
     expect(html).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
     expect(html).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
@@ -1781,11 +2208,44 @@ describe('topology page', () => {
     expect(html).not.toContain('data-topology-g6-legend-owner="hertzbeat-ui-g6-legend-dock"');
     expect(html).not.toContain('data-hz-ui="topology-legend"');
     expect(html).not.toContain('data-hz-topology-legend-section=');
+    expect(html).not.toContain('data-topology-fault-context="incoming-evidence"');
+    expect(html).not.toContain('data-hz-topology-evidence-list-kind="fault-context"');
+    expect(html).not.toContain('data-topology-group-panel="large-graph-grouping"');
+    expect(html).not.toContain('data-hz-ui="topology-group-panel"');
+    expect(html).not.toContain('data-hz-topology-group-panel-action="open-table"');
+    expect(html).not.toContain('data-hz-topology-focus-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('data-topology-focus-trail-hidden-count-owner="hertzbeat-ui-focus-trail-hidden-count"');
+    expect(html).not.toContain('0 hidden by scope');
     expect(html).not.toContain('data-topology-node-id="svc-checkout"');
     expect(html).not.toContain('checkout-api');
     expect(html).not.toContain('redis');
   }, 30000);
 
+  it('does not fall back to the default API graph for non-numeric focused entity ids', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          entityId: 'missing-topology-entity',
+          entityName: 'Missing',
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'cmdb-manual-label',
+          viewMode: 'application',
+          depth: '1'
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-data-source="api"');
+    expect(html).toContain('data-topology-api-focus-resolution="unresolvable-client-focus"');
+    expect(html).toContain('data-topology-api-focus-resolution-owner="topology-route-api-scope"');
+    expect(html).toContain('data-hz-topology-g6-node-count="0"');
+    expect(html).not.toContain('data-topology-node-id=');
+    expect(html).not.toContain('Payment API');
+    expect(html).not.toContain('Orders DB');
+  }, 30000);
+
   it('shows explicit missing trace-call evidence instead of a node-only trace graph', async () => {
     const { default: TopologyPage } = await import('./topology-page');
     const html = renderToStaticMarkup(
@@ -1838,35 +2298,52 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-empty-result-count="0"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace"');
     expect(html).toContain('暂无 trace 调用拓扑证据');
+    expect(html).toContain('当前窗口无 trace 调用边。');
+    expect(html).not.toContain('Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。');
     expect(html).toContain('data-hz-topology-g6-node-count="0"');
     expect(html).toContain('data-hz-topology-g6-edge-count="0"');
+    expect(html).not.toContain('data-hz-ui="topology-companion-rail"');
+    expect(html).not.toContain('data-topology-companion-section=');
+    expect(html).not.toContain('data-topology-metric-table-owner="hertzbeat-ui"');
+    expect(html).not.toContain('data-hz-ui="topology-metric-table"');
+    expect(html).not.toContain('data-hz-ui="topology-detail-drawer"');
+    expect(html).not.toContain('data-topology-current-entity-panel=');
   }, 15000);
 
   it('exposes the resolved topology API scope for Browser release-gate checks', async () => {
     const { default: TopologyPage } = await import('./topology-page');
-    const html = renderToStaticMarkup(
-      <TopologyPage
-        routeContext={{
-          environment: 'prod',
-          timeRange: 'last-1h',
-          sourceKind: 'otlp-trace-call',
-          viewMode: 'service-call',
-          depth: '2',
-          groupBy: 'source-kind'
-        }}
-        apiGraph={{
-          apiBacked: true,
-          focusEntityId: null,
-          depth: 2,
-          sourceKinds: ['otlp-trace-call'],
-          nodes: [],
-          edges: []
-        }}
-      />
-    );
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    let html = '';
+    try {
+      html = renderToStaticMarkup(
+        <TopologyPage
+          routeContext={{
+            environment: 'prod',
+            timeRange: 'last-1h',
+            sourceKind: 'otlp-trace-call',
+            viewMode: 'service-call',
+            depth: '2',
+            groupBy: 'source-kind'
+          }}
+          apiGraph={{
+            apiBacked: true,
+            focusEntityId: null,
+            depth: 2,
+            sourceKinds: ['otlp-trace-call'],
+            nodes: [],
+            edges: []
+          }}
+        />
+      );
+    } finally {
+      vi.useRealTimers();
+    }
 
     expect(html).toContain('data-topology-api-scope-owner="hertzbeat-ui-workbench-frame"');
-    expect(html).toContain('data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;sourceKind=otlp-trace-call&amp;relationType=trace-call"');
+    expect(html).toContain(
+      'data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;relationType=trace-call&amp;start=1780218000000&amp;end=1780221600000"'
+    );
     expect(html).toContain('data-topology-api-scope-source-kind="otlp-trace-call"');
     expect(html).toContain('data-topology-api-scope-relation-type="trace-call"');
     expect(html).toContain('data-topology-api-scope-view-mode="service-call"');
@@ -1874,6 +2351,130 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-empty-relation-type="trace-call"');
   }, 15000);
 
+  it('exposes a first-class local Greptime scale proof route without remembering a focused URL', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    let html = '';
+    try {
+      html = renderToStaticMarkup(
+        <TopologyPage
+          routeContext={{
+            environment: 'prod',
+            timeRange: 'last-7d',
+            sourceKind: 'otlp-trace-call',
+            viewMode: 'service-call',
+            depth: '2',
+            groupBy: 'source-kind',
+            scaleProof: 'greptime-real'
+          }}
+          apiGraph={{
+            apiBacked: true,
+            focusEntityId: 646562420231424,
+            depth: 2,
+            sourceKinds: ['otlp-trace-call'],
+            nodes: [],
+            edges: []
+          }}
+        />
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+
+    expect(html).toContain('data-topology-scale-proof="greptime-real"');
+    expect(html).toContain('data-topology-scale-proof-owner="topology-route-api-scope"');
+    expect(html).toContain('data-topology-scale-proof-focus-entity-id="646562420231424"');
+    expect(html).toContain('data-topology-scale-proof-api-policy="focused-real-greptime-scale-fixture"');
+    expect(html).toContain('data-topology-scale-proof-time-policy="fixed-seeded-greptime-window"');
+    expect(html).toContain('data-topology-scale-proof-window-start="1780344000000"');
+    expect(html).toContain('data-topology-scale-proof-window-end="1780352700000"');
+    expect(html).toContain(
+      'data-topology-api-request-path="/topology?focusEntityId=646562420231424&amp;depth=2&amp;environment=prod&amp;relationType=trace-call&amp;start=1780344000000&amp;end=1780352700000"'
+    );
+  }, 15000);
+
+  it('exposes the mixed star/mesh Greptime scale proof as a global short route', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    let html = '';
+    try {
+      html = renderToStaticMarkup(
+        <TopologyPage
+          routeContext={{
+            environment: 'prod',
+            timeRange: 'last-7d',
+            sourceKind: 'otlp-trace-call',
+            viewMode: 'service-call',
+            depth: '2',
+            groupBy: 'source-kind',
+            scaleProof: 'mixed-star-mesh'
+          }}
+          apiGraph={{
+            apiBacked: true,
+            focusEntityId: null,
+            depth: 2,
+            sourceKinds: ['otlp-trace-call'],
+            nodes: [],
+            edges: []
+          }}
+        />
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+
+    expect(html).toContain('data-topology-scale-proof="mixed-star-mesh"');
+    expect(html).toContain('data-topology-scale-proof-owner="topology-route-api-scope"');
+    expect(html).not.toContain('data-topology-scale-proof-focus-entity-id=');
+    expect(html).toContain('data-topology-scale-proof-api-policy="global-real-greptime-mixed-star-mesh-fixture"');
+    expect(html).toContain('data-topology-scale-proof-time-policy="fixed-seeded-greptime-window"');
+    expect(html).toContain('data-topology-scale-proof-window-start="1780344000000"');
+    expect(html).toContain('data-topology-scale-proof-window-end="1780352700000"');
+    expect(html).toContain(
+      'data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;relationType=trace-call&amp;start=1780344000000&amp;end=1780352700000"'
+    );
+  }, 15000);
+
+  it('does not let the mixed star/mesh proof drift out of the seeded Greptime window as time moves forward', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-06-02T04:30:00.000Z'));
+    let html = '';
+    try {
+      html = renderToStaticMarkup(
+        <TopologyPage
+          routeContext={{
+            environment: 'prod',
+            timeRange: 'last-7d',
+            sourceKind: 'otlp-trace-call',
+            viewMode: 'service-call',
+            depth: '2',
+            groupBy: 'source-kind',
+            scaleProof: 'mixed-star-mesh'
+          }}
+          apiGraph={{
+            apiBacked: true,
+            focusEntityId: null,
+            depth: 2,
+            sourceKinds: ['otlp-trace-call'],
+            nodes: [],
+            edges: []
+          }}
+        />
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+
+    expect(html).toContain('data-topology-scale-proof-time-policy="fixed-seeded-greptime-window"');
+    expect(html).toContain(
+      'data-topology-api-request-path="/topology?depth=2&amp;environment=prod&amp;relationType=trace-call&amp;start=1780344000000&amp;end=1780352700000"'
+    );
+    expect(html).not.toContain('start=1779737400000');
+  }, 15000);
+
   it('keeps the default topology route API-owned while the client API is pending or unavailable', async () => {
     const { default: TopologyPage } = await import('./topology-page');
     const pendingHtml = renderToStaticMarkup(<TopologyPage routeContext={{ environment: 'prod', timeRange: 'last-1h' }} />);
@@ -1901,8 +2502,28 @@ describe('topology page', () => {
         apiGraph={null}
       />
     );
+    const unauthorizedTraceCallHtml = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          environment: 'prod',
+          timeRange: 'last-1h',
+          sourceKind: 'otlp-trace-call',
+          relationType: 'trace-call'
+        }}
+        apiGraph={null}
+        apiGraphLoadError={{ status: 401, message: 'Topology API request failed: 401' }}
+      />
+    );
 
     expect(pendingHtml).toContain('data-topology-data-source="api"');
+    expect(pendingHtml).toContain('data-topology-api-settle-state="pending"');
+    expect(pendingHtml).toContain('data-topology-api-error-state="none"');
+    expect(pendingHtml).toContain('data-topology-api-error-status="none"');
+    expect(pendingHtml).toContain('data-topology-api-error-resolution="none"');
+    expect(pendingHtml).toContain('data-topology-api-settle-state-owner="hertzbeat-ui-workbench-frame"');
+    expect(pendingHtml).toContain('data-topology-relation-gap-state="api-pending"');
+    expect(pendingHtml).toContain('data-topology-relation-gap-visual="loading-state"');
+    expect(pendingHtml).toContain('data-topology-relation-gap-canvas-policy="loading-overlay"');
     expect(pendingHtml).toContain('data-topology-loading-state="api-pending"');
     expect(pendingHtml).toContain('data-topology-loading-state-owner="hertzbeat-ui-loading-state"');
     expect(pendingHtml).toContain('data-hz-ui="topology-loading-state"');
@@ -1915,6 +2536,9 @@ describe('topology page', () => {
     expect(pendingHtml).not.toContain('data-topology-empty-state="api-empty"');
     expect(pendingTraceCallHtml).toContain('data-topology-trace-call-state="pending"');
     expect(pendingTraceCallHtml).toContain('data-topology-trace-call-red-state="pending"');
+    expect(unavailableHtml).toContain('data-topology-api-settle-state="degraded"');
+    expect(unavailableHtml).toContain('data-topology-api-error-state="unknown"');
+    expect(unavailableHtml).toContain('data-topology-api-error-resolution="retry-or-check-api"');
 
     [pendingHtml, unavailableHtml].forEach(html => {
       expect(html).toContain('data-topology-data-source="api"');
@@ -1931,6 +2555,7 @@ describe('topology page', () => {
     expect(unavailableHtml).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
+    expect(unavailableHtml).toContain('data-hz-topology-empty-copy-visibility="assistive"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-kind="degraded"');
     expect(unavailableHtml).toContain('data-hz-topology-empty-source="拓扑 API 不可用"');
@@ -1942,6 +2567,13 @@ describe('topology page', () => {
     expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-source-kind="otlp-trace-call"');
     expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-relation-type="trace-call"');
     expect(traceCallUnavailableHtml).toContain('data-hz-topology-empty-evidence-sources="otlp-trace-call greptime trace unavailable"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-api-settle-state="degraded"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-api-error-state="unauthorized"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-api-error-status="401"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-api-error-resolution="login-required"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-trace-call-state="degraded"');
+    expect(unauthorizedTraceCallHtml).toContain('data-topology-empty-state="trace-call-degraded"');
+    expect(unauthorizedTraceCallHtml).not.toContain('data-topology-empty-state="trace-call-missing"');
   }, 30000);
 
   it('uses incoming entity context to seed filters, active topology node, and current signal links', async () => {
@@ -1966,6 +2598,14 @@ describe('topology page', () => {
     expect(html).toContain('last-30m');
     expect(html).toContain('timeRange=last-30m');
     expect(html).toContain('当前筛选');
+    expect(html).toContain('data-topology-view-scope="focused-adjacency"');
+    expect(html).toContain('data-topology-view-scope-owner="hertzbeat-ui-focus-scope-guide"');
+    expect(html).toContain('data-topology-view-scope-global-action="open-global-topology"');
+    expect(html).toContain('data-topology-view-scope-global-href="/topology?');
+    expect(html).toContain('当前为局部聚焦视角');
+    expect(html).toContain('查看全局拓扑');
+    expect(html).not.toMatch(/data-topology-view-scope-global-href="[^"]*entityId=/);
+    expect(html).not.toMatch(/data-topology-view-scope-global-href="[^"]*serviceName=/);
   }, 15000);
 
   it('uses view mode and source query params to narrow topology and expose alert-impact closure', async () => {
@@ -1975,9 +2615,21 @@ describe('topology page', () => {
       environment: 'prod',
       timeRange: 'last-1h'
     };
+    const alertImpactGraph = buildApiTopologyFixture();
+    alertImpactGraph.sourceKinds = [...alertImpactGraph.sourceKinds, 'alert-impact'];
+    alertImpactGraph.edges = alertImpactGraph.edges.map(edge =>
+      edge.sourceNodeId === 'svc-checkout' && edge.targetNodeId === 'res-orders-db'
+        ? {
+            ...edge,
+            relationType: 'alert impact',
+            relationSource: 'alert-impact',
+            evidenceBadges: ['alert-impact']
+          }
+        : edge
+    );
 
     const { default: TopologyPage } = await import('./topology-page');
-    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={buildApiTopologyFixture()} />);
+    const html = renderToStaticMarkup(<TopologyPage routeContext={routeContext} apiGraph={alertImpactGraph} />);
 
     expect(html).toContain('data-topology-active-view-mode="alert-impact"');
     expect(html).toContain('data-topology-active-source-kind="alert-impact"');
@@ -1985,8 +2637,11 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-control-source-kind-value="alert-impact"');
     expect(html).not.toContain('data-topology-source-active="alert-impact"');
     expect(html).toContain('data-topology-edge-focus="active-path"');
-    expect(html).toContain('data-topology-edge-focus="context-muted"');
-    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-focus="active"/);
+    expect(html).toContain('data-hz-topology-g6-edge-count="1"');
+    expect(html).toContain('data-topology-edge-source="alert-impact"');
+    expect(html).not.toContain('data-topology-edge-source="otlp-trace-call"');
+    expect(html).toContain('data-hz-topology-path-source-kind="alert-impact"');
+    expect(html).not.toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-focus="active"/);
     expect(html).toContain('data-topology-alert-impact-link="alert-center"');
     expect(html).toContain('data-topology-alert-impact-link-owner="hertzbeat-ui-action-link"');
     expect(html).toContain('data-hz-topology-action-link-label-owner="hertzbeat-ui-action-link-label"');
@@ -1995,12 +2650,80 @@ describe('topology page', () => {
     expect(html).toContain('source=topology');
     expect(html).toContain('viewMode=alert-impact');
     expect(html).toContain('sourceKind=alert-impact');
-    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-select-href="\/topology\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
+    expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-select-mode="in-page-drawer"/);
     expect(html).toMatch(/data-topology-node-id="svc-checkout"[^>]+data-topology-node-entity-href="\/entities\/service%3Acommerce%2Fcheckout\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-    expect(html).toMatch(/data-topology-context-link="entity"[^>]+href="\/entities\/service%3Acommerce%2Fcheckout\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-    expect(html).toMatch(/data-topology-context-link="metrics"[^>]+href="\/ingestion\/otlp\/metrics\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-    expect(html).toMatch(/data-topology-context-link="logs"[^>]+href="\/log\/manage\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
-    expect(html).toMatch(/data-topology-context-link="traces"[^>]+href="\/trace\/manage\?[^"]*viewMode=alert-impact[^"]*sourceKind=alert-impact/);
+    expect(html).not.toContain('data-topology-current-entity-panel="svc-checkout"');
+    expect(html).not.toContain('data-topology-context-link="entity"');
+    expect(html).not.toContain('data-topology-context-link="metrics"');
+    expect(html).not.toContain('data-topology-context-link="logs"');
+    expect(html).not.toContain('data-topology-context-link="traces"');
+  }, 15000);
+
+  it('does not render unrelated trace-call graph evidence for an explicit alert-impact source filter', async () => {
+    const { default: TopologyPage } = await import('./topology-page');
+    const html = renderToStaticMarkup(
+      <TopologyPage
+        routeContext={{
+          viewMode: 'alert-impact',
+          sourceKind: 'alert-impact',
+          environment: 'prod',
+          timeRange: 'last-1h'
+        }}
+        apiGraph={{
+          apiBacked: true,
+          focusEntityId: 'service:commerce/payment',
+          depth: 1,
+          sourceKinds: ['otlp-trace-call'],
+          nodes: [
+            {
+              id: 'svc-checkout',
+              entityId: 'service:commerce/checkout',
+              entityName: 'Checkout API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['otlp-trace-call']
+            },
+            {
+              id: 'svc-payment',
+              entityId: 'service:commerce/payment',
+              entityName: 'Payment API',
+              entityType: 'service',
+              namespace: 'commerce',
+              environment: 'prod',
+              health: 'warning',
+              evidenceBadges: ['otlp-trace-call']
+            }
+          ],
+          edges: [
+            {
+              sourceNodeId: 'svc-checkout',
+              targetNodeId: 'svc-payment',
+              relationType: 'trace-call',
+              relationSource: 'otlp-trace-call',
+              status: 'warning',
+              score: 42,
+              evidenceBadges: ['otlp-trace-call'],
+              redMetrics: {
+                requestRatePerSecond: 5,
+                errorRate: 0.02,
+                latencyP95Ms: 180
+              }
+            }
+          ]
+        }}
+      />
+    );
+
+    expect(html).toContain('data-topology-data-source="api"');
+    expect(html).toContain('data-topology-active-source-kind="alert-impact"');
+    expect(html).toContain('data-topology-empty-state="filtered-empty"');
+    expect(html).toContain('data-hz-topology-empty-source-kind="alert-impact"');
+    expect(html).toContain('data-hz-topology-empty-result-count="0"');
+    expect(html).toContain('data-hz-topology-g6-node-count="0"');
+    expect(html).toContain('data-hz-topology-g6-edge-count="0"');
+    expect(html).not.toContain('data-hz-topology-g6-edge-source="otlp-trace-call"');
   }, 15000);
 
   it('renders selected edge evidence and drilldown links with retained topology context', async () => {
@@ -2019,6 +2742,11 @@ describe('topology page', () => {
     expect(html).toContain('data-topology-edge-selected="true"');
     expect(html).toMatch(/data-topology-edge-id="svc-checkout--res-orders-db"[^>]+data-topology-edge-focus="active-path"/);
     expect(html).toContain('data-topology-edge-evidence-panel="svc-checkout--res-orders-db"');
+    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('data-topology-companion-rail-visibility="visible-side-rail"');
+    expect(html.indexOf('data-topology-edge-evidence-panel="svc-checkout--res-orders-db"')).toBeLessThan(
+      html.indexOf('data-topology-companion-section="view-mode"')
+    );
     expect(html).toContain('关系证据');
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
@@ -2056,6 +2784,29 @@ describe('topology page', () => {
 
     expect(html).toContain('data-topology-current-entity-panel="svc-checkout"');
     expect(html).toContain('data-topology-current-entity-neighbor-owner="hertzbeat-ui-detail-neighbor-evidence"');
+    expect(html).toContain('data-topology-current-entity-panel-hover-continuity="selected-node-survives-edge-hover"');
+    expect(html).toContain('data-topology-current-entity-panel-hover-invariants="selected-node-detail edge-hover-path-summary-temporary pointer-leave-clears-hover no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-topology-current-entity-panel-rail-scroll-continuity="selected-node-retakes-companion-first-viewport"');
+    expect(html).toContain('data-topology-current-entity-panel-rail-scroll-invariants="manual-active-resets-scroll-active active-section-scroll collapse-state-preserved no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-topology-current-entity-panel-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-topology-current-entity-panel-rail-fit-invariants="bounded-block internal-scroll no-page-scroll no-rail-overflow scan-first-viewport"');
+    expect(html).toContain('data-topology-current-entity-panel-signal-entry="header-dock"');
+    expect(html).toContain('data-topology-current-entity-panel-signal-entry-invariants="metrics-logs-traces-before-long-evidence sticky-with-node-context no-extra-navigation"');
+    expect(html).toContain('data-topology-current-entity-panel-scroll-reset="identity-change"');
+    expect(html).toContain('data-topology-current-entity-panel-scroll-reset-invariants="identity-change scroll-top-zero no-page-scroll no-rail-scroll no-url-change"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement="header-dock"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-sticky="top-with-header-context"');
+    expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('data-topology-companion-rail-visibility="visible-side-rail"');
+    expect(html.indexOf('data-topology-current-entity-panel="svc-checkout"')).toBeLessThan(
+      html.indexOf('data-topology-companion-section="view-mode"')
+    );
     expect(html).toContain('data-topology-current-entity-upstream-count="4"');
     expect(html).toContain('data-topology-current-entity-downstream-count="2"');
     expect(html).toContain('data-topology-current-entity-upstream-node-ids="app-commerce svc-frontend monitor-checkout k8s-checkout-workload"');
diff --git a/web-next/app/topology/page.tsx b/web-next/app/topology/page.tsx
index c30aab4f96..98460926b4 100644
--- a/web-next/app/topology/page.tsx
+++ b/web-next/app/topology/page.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
-import TopologyPage from './topology-page';
+import TopologyRouteClient from './topology-route-client';
+import TopologyRouteShell from './topology-route-shell';
 import { readTopologyRouteContext, type TopologySearchParams } from '../../lib/topology-surface/query-state';
 
 export default async function TopologyRoutePage({
@@ -9,5 +10,12 @@ export default async function TopologyRoutePage({
 } = {}) {
   const resolvedSearchParams = await searchParams;
   const routeContext = readTopologyRouteContext(resolvedSearchParams);
-  return <TopologyPage routeContext={routeContext} />;
+  return (
+    <>
+      <div id="topology-route-deferred-shell">
+        <TopologyRouteShell />
+      </div>
+      <TopologyRouteClient routeContext={routeContext} shellElementId="topology-route-deferred-shell" />
+    </>
+  );
 }
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index 20ec9f0449..94544929c0 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -33,15 +33,19 @@ import {
   type HzTopologyDetailDrawerAction,
   type HzTopologyDetailDrawerFact,
   type HzTopologyGroupPanelItem,
+  type HzTopologyLegendItem,
+  type HzTopologyLegendSection,
   type HzTopologyPathSummaryMetric,
   type HzTopologyHoverTooltipFact,
   type HzTopologyHoverTooltipMetric,
   type HzTopologyMetricRow,
+  type HzTopologyMetricRowWindowVisibility,
   type HzTopologyMetricTableRenderWindowFilter,
   type HzTopologyToolbarStateItem
-} from '@hertzbeat/ui';
+} from '@hertzbeat/ui/topology';
 import {
   HzTopologyG6Canvas,
+  HZ_TOPOLOGY_G6_EDGE_DENSITY_VISIBLE_EDGE_LIMIT,
   HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6RenderWindow,
@@ -51,7 +55,17 @@ import {
 } from '@hertzbeat/ui/topology-g6';
 import { useI18n } from '../../components/providers/i18n-provider';
 import { api } from '../../lib/api-facade';
-import { buildTopologyApiUrl, loadTopologyGraph, resolveTopologyRelationType } from '../../lib/topology-surface/controller';
+import {
+  buildTopologyApiUrl,
+  hasUnresolvableTopologyFocusEntity,
+  loadTopologyGraph,
+  resolveTopologyApiTimeoutMs,
+  resolveTopologyRelationType,
+  resolveTopologyScaleProofApiPolicy,
+  resolveTopologyScaleProofFocusEntityId,
+  resolveTopologyScaleProofTimeWindow,
+  shouldPreservePreviousTopologyGraphDuringLoad
+} from '../../lib/topology-surface/controller';
 import {
   buildTopologyServiceMapFromApiGraph,
   type EntityTopologyApiGraph,
@@ -64,6 +78,12 @@ import {
 
 type TopologyLocalSelectionSource = 'none' | 'node-click' | 'edge-click' | 'table-row-click';
 type TopologyLayoutMode = 'layered-service' | 'force' | 'grid-table';
+type TopologyApiLoadError = {
+  status?: number;
+  code?: number;
+  message?: string;
+};
+type TopologyApiErrorState = 'none' | 'unauthorized' | 'api-error' | 'unknown';
 
 function findNode(nodes: TopologyServiceNode[], id: string) {
   return nodes.find(node => node.id === id);
@@ -73,6 +93,27 @@ function findEdge(edges: TopologyServiceEdge[], id: string) {
   return edges.find(edge => edge.id === id);
 }
 
+function topologyEdgeMatchesCanvasView(edge: TopologyServiceEdge, viewMode: TopologyFilterContext['viewMode']) {
+  if (viewMode === 'application') return true;
+  if (viewMode === 'service-call') return edge.relationshipType === 'trace-call';
+  if (viewMode === 'alert-impact') return edge.alertImpact !== 'none' || edge.source === 'alert-impact';
+  return (
+    edge.relationshipType === 'template-dependency' ||
+    edge.relationshipType === 'k8s-ownership' ||
+    edge.relationshipType === 'database-connection' ||
+    edge.relationshipType === 'middleware-connection'
+  );
+}
+
+function topologyEdgeMatchesCanvasSource(edge: TopologyServiceEdge, sourceKind: TopologyFilterContext['sourceKind']) {
+  if (!sourceKind) return true;
+  return edge.source === sourceKind;
+}
+
+function topologyEdgeMatchesCanvasScope(edge: TopologyServiceEdge, filterContext: TopologyFilterContext) {
+  return topologyEdgeMatchesCanvasView(edge, filterContext.viewMode) && topologyEdgeMatchesCanvasSource(edge, filterContext.sourceKind);
+}
+
 function resolveTopologyG6EntityType(node: Pick<TopologyServiceNode, 'entityType' | 'id' | 'label' | 'source'>) {
   const inferredIcon = getHzTopologyG6NodeIcon(`${node.entityType} ${node.label} ${node.source} ${node.id}`);
   if (inferredIcon.kind === 'cache' || inferredIcon.kind === 'queue') return inferredIcon.kind;
@@ -345,8 +386,9 @@ function hasTraceRedMetrics(edge: TopologyServiceEdge) {
   ].some(value => typeof value === 'number' && Number.isFinite(value));
 }
 
-function pickTopologyDetailEdge(map: ReturnType<typeof buildTopologyServiceMapFromApiGraph>) {
-  return map.selectedEdge ?? map.edges.find(hasTraceRedMetrics) ?? map.edges[0];
+function pickTopologyDetailEdge(map: ReturnType<typeof buildTopologyServiceMapFromApiGraph>, edges: TopologyServiceEdge[] = map.edges) {
+  const selectedEdge = map.selectedEdge ? edges.find(edge => edge.id === map.selectedEdge?.id) : undefined;
+  return selectedEdge ?? edges.find(hasTraceRedMetrics) ?? edges[0];
 }
 
 function appendTopologySelectionParam(params: URLSearchParams, key: string, value: string | undefined) {
@@ -417,11 +459,76 @@ const TOPOLOGY_EDGE_FACT_IDS = [
   'last-seen'
 ];
 
+type TopologyEdgeRenderWindowContext = {
+  visibility: HzTopologyMetricRowWindowVisibility;
+  sourceVisible: 'true' | 'false' | 'unknown';
+  targetVisible: 'true' | 'false' | 'unknown';
+};
+
+function resolveTopologyEdgeEndpointRenderWindowVisibility(
+  nodeId: string | undefined,
+  renderedNodeIds: Set<string> | undefined
+): 'true' | 'false' | 'unknown' {
+  if (!nodeId || !renderedNodeIds) return 'unknown';
+  return renderedNodeIds.has(nodeId) ? 'true' : 'false';
+}
+
+function resolveTopologyEdgeRenderWindowVisibility(
+  sourceVisible: TopologyEdgeRenderWindowContext['sourceVisible'],
+  targetVisible: TopologyEdgeRenderWindowContext['targetVisible']
+): HzTopologyMetricRowWindowVisibility {
+  if (sourceVisible === 'unknown' || targetVisible === 'unknown') return 'unknown';
+  if (sourceVisible === 'true' && targetVisible === 'true') return 'visible';
+  if (sourceVisible === 'false' && targetVisible === 'false') return 'hidden';
+  return 'partial';
+}
+
+function buildTopologyEdgeRenderWindowContext(
+  edge: TopologyServiceEdge | undefined,
+  renderWindowMode: 'direct' | 'windowed',
+  renderedNodeIds: string[] | undefined
+): TopologyEdgeRenderWindowContext | undefined {
+  if (!edge || renderWindowMode !== 'windowed') return undefined;
+  const renderedNodeIdSet = renderedNodeIds?.length ? new Set(renderedNodeIds) : undefined;
+  const sourceVisible = resolveTopologyEdgeEndpointRenderWindowVisibility(edge.from, renderedNodeIdSet);
+  const targetVisible = resolveTopologyEdgeEndpointRenderWindowVisibility(edge.to, renderedNodeIdSet);
+  return {
+    visibility: resolveTopologyEdgeRenderWindowVisibility(sourceVisible, targetVisible),
+    sourceVisible,
+    targetVisible
+  };
+}
+
+function resolveTopologyEdgeRenderWindowLabel(
+  visibility: HzTopologyMetricRowWindowVisibility,
+  t: (key: string) => string
+) {
+  if (visibility === 'visible') return t('topology.metric-table.filter.visible');
+  if (visibility === 'partial') return t('topology.metric-table.filter.partial');
+  if (visibility === 'hidden') return t('topology.metric-table.filter.hidden');
+  return t('topology.metric-table.filter.unknown');
+}
+
 function buildTopologyEdgeDetailFacts(
   edge: TopologyServiceEdge,
-  t: (key: string) => string
+  t: (key: string) => string,
+  renderWindowContext?: TopologyEdgeRenderWindowContext
 ): HzTopologyDetailDrawerFact[] {
   return [
+    ...(renderWindowContext
+      ? [{
+          id: 'render-window-context',
+          label: t('topology.edge.render-window.label'),
+          value: resolveTopologyEdgeRenderWindowLabel(renderWindowContext.visibility, t),
+          tone: renderWindowContext.visibility === 'visible' ? 'info' as const : 'warning' as const,
+          factProps: {
+            'data-topology-edge-render-window-fact': renderWindowContext.visibility,
+            'data-topology-edge-render-window-fact-owner': 'hertzbeat-ui-detail-render-window-context',
+            'data-topology-edge-render-window-source-visible': renderWindowContext.sourceVisible,
+            'data-topology-edge-render-window-target-visible': renderWindowContext.targetVisible
+          } as React.HTMLAttributes<HTMLDivElement>
+        }]
+      : []),
     ...edge.evidence.rows.map((row, index) => ({
       id: TOPOLOGY_EDGE_FACT_IDS[index] ?? `evidence-${index + 1}`,
       label: row.label,
@@ -636,48 +743,105 @@ function buildApiOwnedEmptyGraph(routeContext?: TopologyRouteContext): EntityTop
   };
 }
 
-const TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;
-const DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;
+function normalizeTopologyApiLoadError(error: unknown): TopologyApiLoadError | null {
+  if (!error || typeof error !== 'object') return null;
+  const candidate = error as { status?: unknown; code?: unknown; message?: unknown };
+  const status = typeof candidate.status === 'number' ? candidate.status : undefined;
+  const code = typeof candidate.code === 'number' ? candidate.code : undefined;
+  const message = typeof candidate.message === 'string' ? candidate.message : undefined;
+  return { status, code, message };
+}
+
+function resolveTopologyApiErrorState(error: TopologyApiLoadError | null | undefined): TopologyApiErrorState {
+  if (!error) return 'none';
+  if (error.status === 401 || error.code === 401) return 'unauthorized';
+  if (error.status || error.code) return 'api-error';
+  return 'unknown';
+}
 
-function resolveTopologyApiTimeoutMs(routeContext?: TopologyRouteContext) {
-  if (routeContext?.sourceKind === 'otlp-trace-call' || routeContext?.viewMode === 'service-call') {
-    return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;
-  }
-  return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;
+function resolveTopologyApiErrorResolution(state: TopologyApiErrorState) {
+  if (state === 'none') return 'none';
+  if (state === 'unauthorized') return 'login-required';
+  return 'retry-or-check-api';
 }
 
 export default function TopologyPage({
   routeContext,
-  apiGraph
+  apiGraph,
+  apiGraphLoadError
 }: {
   routeContext?: TopologyRouteContext;
   apiGraph?: EntityTopologyApiGraph | null;
+  apiGraphLoadError?: TopologyApiLoadError | null;
 } = {}) {
   const topologyRouter = useRouter();
   const { t } = useI18n();
   const routeContextKey = React.useMemo(() => JSON.stringify(routeContext ?? {}), [routeContext]);
-  const [loadedApiGraph, setLoadedApiGraph] = React.useState<EntityTopologyApiGraph | null | undefined>(apiGraph);
-  const [topologyLayout] = React.useState<TopologyLayoutMode>('layered-service');
-  const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(undefined);
   const apiOwnedEmptyGraph = React.useMemo(() => buildApiOwnedEmptyGraph(routeContext), [routeContext]);
+  const topologyHasUnresolvableFocusEntity = React.useMemo(
+    () => hasUnresolvableTopologyFocusEntity(routeContext),
+    [routeContext]
+  );
+  const [loadedApiGraph, setLoadedApiGraph] = React.useState<EntityTopologyApiGraph | null | undefined>(() =>
+    apiGraph !== undefined ? apiGraph : topologyHasUnresolvableFocusEntity ? apiOwnedEmptyGraph : undefined
+  );
+  const [loadedApiGraphError, setLoadedApiGraphError] = React.useState<TopologyApiLoadError | null>(() =>
+    apiGraph === null ? (apiGraphLoadError ?? {}) : null
+  );
+  const [topologyLayout] = React.useState<TopologyLayoutMode>('layered-service');
+  const [topologySearchQuery, setTopologySearchQuery] = React.useState<string | undefined>(routeContext?.search);
+  const [topologyManualRefreshSequence, setTopologyManualRefreshSequence] = React.useState(0);
+
+  const handleTopologyRefresh = React.useCallback(() => {
+    setTopologyManualRefreshSequence(sequence => sequence + 1);
+  }, []);
 
   React.useEffect(() => {
-    if (apiGraph !== undefined) {
+    if (apiGraph !== undefined && topologyManualRefreshSequence === 0) {
       setLoadedApiGraph(apiGraph);
+      setLoadedApiGraphError(apiGraph === null ? (apiGraphLoadError ?? {}) : null);
+      return undefined;
+    }
+    if (topologyHasUnresolvableFocusEntity) {
+      setLoadedApiGraph(apiOwnedEmptyGraph);
+      setLoadedApiGraphError(null);
       return undefined;
     }
     let mounted = true;
-    void loadTopologyGraph(api.topology.request, routeContext ?? {}, { timeoutMs: resolveTopologyApiTimeoutMs(routeContext) })
+    const topologyShouldPreservePreviousGraphDuringLoad =
+      topologyManualRefreshSequence === 0 && shouldPreservePreviousTopologyGraphDuringLoad(routeContext ?? {});
+    if (!topologyShouldPreservePreviousGraphDuringLoad) {
+      setLoadedApiGraph(undefined);
+      setLoadedApiGraphError(null);
+    }
+    void loadTopologyGraph(api.topology.request, routeContext ?? {}, {
+      timeoutMs: resolveTopologyApiTimeoutMs(routeContext),
+      ...(topologyManualRefreshSequence > 0 ? { cacheBust: () => `manual-${topologyManualRefreshSequence}` } : {})
+    })
       .then(graph => {
-        if (mounted) setLoadedApiGraph(graph);
+        if (mounted) {
+          setLoadedApiGraph(graph);
+          setLoadedApiGraphError(null);
+        }
       })
-      .catch(() => {
-        if (mounted) setLoadedApiGraph(null);
+      .catch(error => {
+        if (mounted) {
+          setLoadedApiGraph(null);
+          setLoadedApiGraphError(normalizeTopologyApiLoadError(error) ?? {});
+        }
       });
     return () => {
       mounted = false;
     };
-  }, [apiGraph, apiOwnedEmptyGraph, routeContext, routeContextKey]);
+  }, [
+    apiGraph,
+    apiGraphLoadError,
+    apiOwnedEmptyGraph,
+    routeContext,
+    routeContextKey,
+    topologyHasUnresolvableFocusEntity,
+    topologyManualRefreshSequence
+  ]);
 
   const map = React.useMemo(
     () => buildTopologyServiceMapFromApiGraph(loadedApiGraph ?? apiOwnedEmptyGraph, routeContext, t),
@@ -694,13 +858,50 @@ export default function TopologyPage({
     loadedApiGraph !== null &&
     map.filterContext.sourceKind === 'otlp-trace-call' &&
     map.edges.length === 0;
+  const topologyHasExplicitCanvasScope = Boolean(routeContext?.sourceKind || routeContext?.viewMode || routeContext?.edgeId);
+  const topologyScopedCanvasEdges = React.useMemo(
+    () => (topologyHasExplicitCanvasScope && map.edges.length > 0 ? map.edges.filter(edge => topologyEdgeMatchesCanvasScope(edge, map.filterContext)) : map.edges),
+    [map.edges, map.filterContext, topologyHasExplicitCanvasScope]
+  );
+  const topologyFilteredEvidenceMissing =
+    map.dataSource === 'api' &&
+    !topologyIsPending &&
+    loadedApiGraph !== null &&
+    topologyHasExplicitCanvasScope &&
+    map.edges.length > 0 &&
+    topologyScopedCanvasEdges.length === 0;
+  const topologyRelationGap =
+    map.dataSource === 'api' &&
+    !topologyIsPending &&
+    loadedApiGraph !== null &&
+    map.nodes.length > 0 &&
+    map.edges.length === 0 &&
+    !topologyTraceCallMissingEdges &&
+    !topologyFilteredEvidenceMissing;
   const topologyCanvasNodes = React.useMemo(
-    () => (topologyTraceCallMissingEdges ? [] : map.nodes),
-    [map.nodes, topologyTraceCallMissingEdges]
+    () => {
+      if (topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing || topologyRelationGap) return [];
+      if (!topologyHasExplicitCanvasScope || map.edges.length === 0) return map.nodes;
+      const visibleNodeIds = new Set<string>();
+      topologyScopedCanvasEdges.forEach(edge => {
+        visibleNodeIds.add(edge.from);
+        visibleNodeIds.add(edge.to);
+      });
+      return map.nodes.filter(node => visibleNodeIds.has(node.id));
+    },
+    [
+      map.edges.length,
+      map.nodes,
+      topologyFilteredEvidenceMissing,
+      topologyHasExplicitCanvasScope,
+      topologyRelationGap,
+      topologyScopedCanvasEdges,
+      topologyTraceCallMissingEdges
+    ]
   );
   const topologyCanvasEdges = React.useMemo(
-    () => (topologyTraceCallMissingEdges ? [] : map.edges),
-    [map.edges, topologyTraceCallMissingEdges]
+    () => (topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing ? [] : topologyScopedCanvasEdges),
+    [topologyFilteredEvidenceMissing, topologyScopedCanvasEdges, topologyTraceCallMissingEdges]
   );
   const topologyTraceCallEdges = map.edges.filter(edge => edge.source === 'otlp-trace-call' || edge.relationshipType === 'trace-call');
   const topologyTraceCallRedEdgeCount = topologyTraceCallEdges.filter(edge =>
@@ -715,9 +916,11 @@ export default function TopologyPage({
     Boolean(edge.evidence.sampleTraceId || edge.evidence.sampleSpanId)
   ).length;
   const topologyTraceCallState =
-    topologyIsPending && topologyTraceCallScope
+    !topologyTraceCallScope
+      ? 'none'
+      : topologyIsPending
       ? 'pending'
-      : loadedApiGraph === null && topologyTraceCallScope
+      : loadedApiGraph === null
         ? 'degraded'
         : topologyTraceCallMissingEdges
           ? 'missing-edges'
@@ -734,9 +937,36 @@ export default function TopologyPage({
       : topologyTraceCallRedEdgeCount === topologyTraceCallEdges.length
         ? 'ready'
         : 'partial';
+  const topologyApiSettleState = topologyIsPending ? 'pending' : loadedApiGraph === null ? 'degraded' : 'ready';
+  const topologyApiErrorState = topologyIsPending || loadedApiGraph !== null
+    ? 'none'
+    : resolveTopologyApiErrorState(loadedApiGraphError);
+  const topologyApiErrorStatus = topologyApiErrorState === 'none'
+    ? 'none'
+    : loadedApiGraphError?.status ?? loadedApiGraphError?.code ?? 'unknown';
+  const topologyApiErrorResolution = resolveTopologyApiErrorResolution(topologyApiErrorState);
+  const topologyRelationGapState = topologyIsPending ? 'api-pending' : topologyRelationGap ? 'nodes-without-edges' : 'none';
+  const topologyRelationGapVisual = topologyIsPending ? 'loading-state' : topologyRelationGap ? 'canvas-annotation' : 'none';
+  const topologyRelationGapCanvasPolicy = topologyIsPending ? 'loading-overlay' : topologyRelationGap ? 'hide-isolated-node-only-graph' : 'render-graph';
+  const topologyHasRouteEntityFocus = Boolean(
+    routeContext?.entityId?.trim() ||
+    routeContext?.entityName?.trim() ||
+    routeContext?.serviceName?.trim() ||
+    routeContext?.monitorId?.trim() ||
+    routeContext?.monitorName?.trim() ||
+    routeContext?.traceId?.trim() ||
+    routeContext?.spanId?.trim()
+  );
+  const topologyCanShowApiFocus = routeContext === undefined || topologyHasRouteEntityFocus;
+  const topologyVisibleActiveNodeId = topologyCanShowApiFocus ? map.activeNodeId : undefined;
+  const topologyVisibleFocusQuery = topologyCanShowApiFocus ? topologyEffectiveSearchQuery : topologyToolbarSearchQuery;
   const topologyApiRequestPath = buildTopologyApiUrl(routeContext ?? {});
   const topologyApiRequestTimeoutMs = resolveTopologyApiTimeoutMs(routeContext);
   const topologyApiScopeRelationType = resolveTopologyRelationType(routeContext ?? {}) ?? 'all';
+  const topologyScaleProof = routeContext?.scaleProof?.trim();
+  const topologyScaleProofFocusEntityId = resolveTopologyScaleProofFocusEntityId(routeContext ?? {});
+  const topologyScaleProofApiPolicy = resolveTopologyScaleProofApiPolicy(routeContext ?? {});
+  const topologyScaleProofTimeWindow = resolveTopologyScaleProofTimeWindow(routeContext ?? {});
   const topologyG6Layout = topologyLayout === 'force' ? 'force' : 'layered-service';
   const topologyLayoutLabel =
     topologyLayout === 'force'
@@ -752,11 +982,10 @@ export default function TopologyPage({
       entityType: resolveTopologyG6EntityType(node),
       health: node.health,
       tone: node.tone,
-      focus: node.focus,
+      focus: topologyCanShowApiFocus ? node.focus : 'normal',
       source: node.source,
       evidenceBadges: node.evidenceBadges,
       redMetrics: node.redMetrics,
-      href: buildTopologyNodeSelectionHref(node, map, routeContext),
       focusHref: buildTopologyNodeSelectionHref(node, map, routeContext, { depth: 1 }),
       entityHref: node.links.entityHref
     })),
@@ -774,14 +1003,20 @@ export default function TopologyPage({
       redMetrics: edge.redMetrics,
       href: edge.drilldownHref
     }))
-  }), [map, routeContext, topologyCanvasEdges, topologyCanvasNodes]);
+  }), [map, routeContext, topologyCanShowApiFocus, topologyCanvasEdges, topologyCanvasNodes]);
+  const topologyHasRenderableGraph = topologyG6Graph.nodes.length > 0 || topologyG6Graph.edges.length > 0;
   React.useEffect(() => {
     setTopologyLocalSelection({ source: 'none' });
   }, [routeContextKey]);
   React.useEffect(() => {
-    setTopologySearchQuery(undefined);
-  }, [routeContextKey]);
-  const topologyRoutePrimaryNode = map.nodes.find(node => node.id === map.activeNodeId) ?? map.nodes.find(node => node.id === 'svc-checkout') ?? map.nodes[0];
+    setTopologySearchQuery(routeContext?.search);
+  }, [routeContext?.search, routeContextKey]);
+  const topologyRoutePrimaryNode =
+    topologyVisibleActiveNodeId
+      ? map.nodes.find(node => node.id === topologyVisibleActiveNodeId)
+      : topologyCanShowApiFocus
+        ? map.nodes.find(node => node.id === 'svc-checkout') ?? map.nodes[0]
+        : undefined;
   const topologyLocalSelectedNode = topologyLocalSelection.nodeId ? findNode(map.nodes, topologyLocalSelection.nodeId) : undefined;
   const primaryNode = topologyLocalSelectedNode ?? topologyRoutePrimaryNode;
   const activeViewModeLabel = map.viewModes.find(mode => mode.active)?.label;
@@ -795,12 +1030,15 @@ export default function TopologyPage({
     map.filterContext.groupBy === 'source-kind'
       ? Object.fromEntries(map.sources.map(source => [source.kind, withTopologyGroupByHref(source.href, 'source-kind')]))
       : {};
+  const topologyRouteDepth = Number(routeContext?.depth);
+  const topologyRouteDepthValue = Number.isFinite(topologyRouteDepth) ? Math.max(0, Math.floor(topologyRouteDepth)) : undefined;
+  const topologyScopedDepth = topologyRouteDepthValue ?? map.apiDepth;
   const topologyG6ResetParams = React.useMemo(() => {
     const params = new URLSearchParams();
     params.set('environment', map.filterContext.environment);
     params.set('timeRange', map.filterContext.timeRange);
     params.set('viewMode', map.filterContext.viewMode);
-    if (typeof map.apiDepth === 'number') params.set('depth', String(map.apiDepth));
+    if (typeof topologyScopedDepth === 'number') params.set('depth', String(topologyScopedDepth));
     appendTopologySelectionParam(params, 'start', routeContext?.start);
     appendTopologySelectionParam(params, 'end', routeContext?.end);
     appendTopologySelectionParam(params, 'refresh', routeContext?.refresh);
@@ -809,10 +1047,9 @@ export default function TopologyPage({
     appendTopologySelectionParam(params, 'relationType', routeContext?.relationType);
     appendTopologySelectionParam(params, 'hideInternal', routeContext?.hideInternal);
     if (map.filterContext.sourceKind) params.set('sourceKind', map.filterContext.sourceKind);
-    if (map.filterContext.groupBy !== 'none') params.set('groupBy', map.filterContext.groupBy);
+    if (topologyHasRenderableGraph && map.filterContext.groupBy !== 'none') params.set('groupBy', map.filterContext.groupBy);
     return params;
   }, [
-    map.apiDepth,
     map.filterContext.environment,
     map.filterContext.groupBy,
     map.filterContext.sourceKind,
@@ -824,16 +1061,19 @@ export default function TopologyPage({
     routeContext?.refresh,
     routeContext?.relationType,
     routeContext?.start,
-    routeContext?.tz
+    routeContext?.tz,
+    topologyHasRenderableGraph,
+    topologyScopedDepth
   ]);
   const topologyG6FilterResetParams = new URLSearchParams(topologyG6ResetParams);
   topologyG6FilterResetParams.delete('groupBy');
   const topologyG6ResetHref = `/topology?${topologyG6FilterResetParams.toString()}`;
+  const topologyGlobalScopeHref = `/topology?${topologyG6ResetParams.toString()}`;
   const topologyFocusExitParams = new URLSearchParams();
   topologyFocusExitParams.set('environment', map.filterContext.environment);
   topologyFocusExitParams.set('timeRange', map.filterContext.timeRange);
   topologyFocusExitParams.set('viewMode', map.filterContext.viewMode);
-  topologyFocusExitParams.set('depth', '2');
+  topologyFocusExitParams.set('depth', String(topologyScopedDepth ?? 2));
   appendTopologySelectionParam(topologyFocusExitParams, 'start', routeContext?.start);
   appendTopologySelectionParam(topologyFocusExitParams, 'end', routeContext?.end);
   appendTopologySelectionParam(topologyFocusExitParams, 'refresh', routeContext?.refresh);
@@ -843,13 +1083,13 @@ export default function TopologyPage({
   appendTopologySelectionParam(topologyFocusExitParams, 'hideInternal', routeContext?.hideInternal);
   appendTopologySelectionParam(topologyFocusExitParams, 'edgeId', map.selectedEdgeId ?? routeContext?.edgeId);
   if (map.filterContext.sourceKind) topologyFocusExitParams.set('sourceKind', map.filterContext.sourceKind);
-  if (map.filterContext.groupBy !== 'none') topologyFocusExitParams.set('groupBy', map.filterContext.groupBy);
+  if (topologyHasRenderableGraph && map.filterContext.groupBy !== 'none') topologyFocusExitParams.set('groupBy', map.filterContext.groupBy);
   const topologyFocusExitHref = `/topology?${topologyFocusExitParams.toString()}`;
   const topologyG6GroupParams = new URLSearchParams(topologyG6ResetParams);
   topologyG6GroupParams.set('groupBy', 'source-kind');
   const topologyG6SearchParams = new URLSearchParams(topologyG6ResetParams);
   if (topologyToolbarSearchQuery) topologyG6SearchParams.set('search', topologyToolbarSearchQuery);
-  if (map.filterContext.groupBy !== 'none') topologyG6SearchParams.set('groupBy', map.filterContext.groupBy);
+  if (topologyHasRenderableGraph && map.filterContext.groupBy !== 'none') topologyG6SearchParams.set('groupBy', map.filterContext.groupBy);
   const handleTopologyEnvironmentChange = React.useCallback(
     (value: string) => {
       navigateTopologyFocus(buildTopologyScopeHref(topologyG6ResetParams, { environment: value }), topologyRouter.push);
@@ -884,44 +1124,54 @@ export default function TopologyPage({
     },
     [topologyRouter.push]
   );
+  const handleTopologyGlobalScope = React.useCallback(
+    (event: React.MouseEvent<HTMLAnchorElement>) => {
+      event.preventDefault();
+      navigateTopologyFocus(topologyGlobalScopeHref, topologyRouter.push);
+    },
+    [topologyGlobalScopeHref, topologyRouter.push]
+  );
   const topologyToolbarSummaryItems = [
-    topologyEffectiveSearchQuery || t('topology.all-entities'),
+    topologyVisibleFocusQuery || t('topology.all-entities'),
     map.filterContext.environment,
     map.filterContext.timeRange,
     activeViewModeLabel,
     activeSourceLabel
   ].filter((item): item is string => Boolean(item));
-  const topologyFocusState = map.activeNodeId ?? (topologyEffectiveSearchQuery ? 'incoming-context' : 'all');
-  const routeFocusDepth = Number(routeContext?.depth);
-  const topologyFocusTrailDepth = Number.isFinite(routeFocusDepth) ? Math.max(0, Math.floor(routeFocusDepth)) : map.apiDepth ?? 2;
-  const topologyFocusTrailMode = primaryNode || topologyEffectiveSearchQuery || map.apiDepth === 1 ? 'focused' : 'overview';
+  const topologyFocusState = topologyVisibleActiveNodeId ?? (topologyVisibleFocusQuery ? 'incoming-context' : 'all');
+  const topologyFocusTrailDepth = topologyScopedDepth ?? 2;
+  const topologyFocusTrailMode = primaryNode || topologyVisibleFocusQuery || map.apiDepth === 1 ? 'focused' : 'overview';
   const topologyFocusTrailEntityId = primaryNode?.id ?? topologyFocusState;
+  const topologyShouldShowFocusExit = topologyFocusTrailMode === 'focused' && topologyFocusTrailEntityId !== 'all';
   const topologyToolbarStateItems: HzTopologyToolbarStateItem[] = [
     {
       id: 'focus',
       label: t('topology.state.focus'),
-      value: (primaryNode?.label ?? topologyEffectiveSearchQuery) || t('topology.all-entities'),
+      value: (primaryNode?.label ?? topologyVisibleFocusQuery) || t('topology.all-entities'),
       tone: primaryNode ? topologyNodeHealthTone(primaryNode) : 'neutral',
       'data-topology-focus-state': topologyFocusState
     },
     {
       id: 'depth',
       label: t('topology.state.depth'),
-      value: formatTopologyDepth(map.apiDepth, t),
-      'data-topology-depth-state': map.apiDepth ?? 'none'
-    },
-    {
+      value: formatTopologyDepth(topologyScopedDepth, t),
+      'data-topology-depth-state': topologyScopedDepth ?? 'none'
+    }
+  ];
+  if (topologyHasRenderableGraph) {
+    topologyToolbarStateItems.push({
       id: 'group',
       label: t('topology.state.group'),
       value: t(`topology.state.group.${map.filterContext.groupBy}`),
       'data-topology-group-state': map.filterContext.groupBy
-    }
-  ];
+    });
+  }
   const topologyApiNodeCount = loadedApiGraph?.nodes?.length ?? map.nodes.length;
-  const topologyHiddenCount = Math.max(0, topologyApiNodeCount - map.nodes.length);
+  const topologyHiddenCount = Math.max(0, topologyApiNodeCount - topologyCanvasNodes.length);
+  const topologyToolbarGroupControlVisibility = topologyHasRenderableGraph ? 'visible' : 'hidden-empty-graph';
   const topologyGroupPanelItems = buildTopologyGroupPanelItems({
-    nodes: map.nodes,
-    edges: map.edges,
+    nodes: topologyCanvasNodes,
+    edges: topologyCanvasEdges,
     activeSourceLabel,
     primaryNode,
     hiddenCount: topologyHiddenCount,
@@ -939,15 +1189,15 @@ export default function TopologyPage({
           id: 'active-entity',
           href: primaryNode.links.entityHref,
           label: primaryNode.label,
-          value: formatTopologyDepth(map.apiDepth, t),
+          value: formatTopologyDepth(topologyScopedDepth, t),
           active: true,
           'data-topology-focus-crumb': primaryNode.id
         }
       : {
           id: 'active-entity',
           href: '/topology',
-          label: topologyEffectiveSearchQuery || t('topology.all-entities'),
-          value: formatTopologyDepth(map.apiDepth, t),
+          label: topologyVisibleFocusQuery || t('topology.all-entities'),
+          value: formatTopologyDepth(topologyScopedDepth, t),
           active: true,
           'data-topology-focus-crumb': topologyFocusState
         }
@@ -978,7 +1228,7 @@ export default function TopologyPage({
       'data-topology-focus-filter-view': map.filterContext.viewMode
     }
   ];
-  const topologyMetricRows = React.useMemo(() => buildTopologyMetricRows(map.edges, map.nodes), [map.edges, map.nodes]);
+  const topologyMetricRows = React.useMemo(() => buildTopologyMetricRows(topologyCanvasEdges, map.nodes), [map.nodes, topologyCanvasEdges]);
   const [topologyMetricWindowFilter, setTopologyMetricWindowFilter] = React.useState<HzTopologyMetricTableRenderWindowFilter>('all');
   React.useEffect(() => {
     setTopologyMetricWindowFilter('all');
@@ -997,10 +1247,10 @@ export default function TopologyPage({
     setTopologyCompanionCollapsedSections(current => ({ ...current, [sectionId]: collapsed }));
   }, []);
   const topologyLocalSelectedEdge = topologyLocalSelection.edgeId ? findEdge(map.edges, topologyLocalSelection.edgeId) : undefined;
-  const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map);
+  const topologyDetailEdge = topologyLocalSelection.nodeId ? undefined : topologyLocalSelectedEdge ?? pickTopologyDetailEdge(map, topologyCanvasEdges);
   const topologyCanvasSelectedEdgeId = topologyLocalSelection.nodeId
     ? undefined
-    : topologyLocalSelectedEdge?.id ?? map.selectedEdgeId ?? topologyDetailEdge?.id;
+    : topologyLocalSelectedEdge?.id ?? map.selectedEdgeId;
   const topologyMetricSelectedRowId = topologyLocalSelection.nodeId
     ? undefined
     : map.selectedEdgeId ?? topologyDetailEdge?.id ?? topologyMetricRows[0]?.id;
@@ -1017,18 +1267,43 @@ export default function TopologyPage({
     const strategy = buildHzTopologyG6LargeGraphStrategy(topologyG6Graph);
     return buildHzTopologyG6RenderWindow(topologyG6Graph, strategy, { priorityNodeIds: topologyRenderWindowPriorityNodeIds });
   }, [topologyG6Graph, topologyRenderWindowPriorityNodeIds]);
+  const topologyG6RenderedEdgeCount = topologyRenderWindowCompanion.mode === 'windowed'
+    ? Math.min(topologyRenderWindowCompanion.renderedEdgeCount, HZ_TOPOLOGY_G6_EDGE_DENSITY_VISIBLE_EDGE_LIMIT)
+    : topologyG6Graph.edges.length;
+  const topologyG6HiddenEdgeCount = Math.max(0, topologyG6Graph.edges.length - topologyG6RenderedEdgeCount);
+  const topologyG6SummaryLabel = topologyRenderWindowCompanion.mode === 'windowed'
+    ? `${t('topology.g6.summary.rendered-node-count', {
+        rendered: topologyRenderWindowCompanion.renderedNodeCount,
+        total: topologyG6Graph.nodes.length
+      })} · ${t('topology.g6.summary.rendered-edge-count', {
+        rendered: topologyG6RenderedEdgeCount,
+        total: topologyG6Graph.edges.length
+      })}`
+    : `${t('topology.group-panel.node-count', { count: topologyG6Graph.nodes.length })} · ${t('topology.group-panel.edge-count', { count: topologyG6Graph.edges.length })}`;
+  const topologyG6SummaryCountPolicy = topologyRenderWindowCompanion.mode === 'windowed' ? 'windowed-rendered-vs-total' : 'direct-total';
   const topologyMetricRenderWindowCompanion = React.useMemo(() => ({
     mode: topologyRenderWindowCompanion.mode,
     totalNodeCount: topologyRenderWindowCompanion.totalNodeCount,
     renderedNodeCount: topologyRenderWindowCompanion.renderedNodeCount,
     hiddenNodeCount: topologyRenderWindowCompanion.hiddenNodeCount,
+    totalEdgeCount: topologyG6Graph.edges.length,
+    renderedEdgeCount: topologyG6RenderedEdgeCount,
     visibleNodeBudget: topologyRenderWindowCompanion.visibleNodeBudget,
     tableCompanion: topologyRenderWindowCompanion.tableCompanion,
     priorityNodeIds: topologyRenderWindowCompanion.priorityNodeIds,
     renderedNodeIds: topologyRenderWindowCompanion.graph.nodes.map(node => node.id)
-  }), [topologyRenderWindowCompanion]);
+  }), [topologyG6Graph.edges.length, topologyG6RenderedEdgeCount, topologyRenderWindowCompanion]);
+  const topologyDetailEdgeRenderWindowContext = React.useMemo(
+    () => buildTopologyEdgeRenderWindowContext(
+      topologyDetailEdge,
+      topologyMetricRenderWindowCompanion.mode,
+      topologyMetricRenderWindowCompanion.renderedNodeIds
+    ),
+    [topologyDetailEdge, topologyMetricRenderWindowCompanion.mode, topologyMetricRenderWindowCompanion.renderedNodeIds]
+  );
   const topologyShouldShowRenderWindowMetricTable =
     topologyRenderWindowCompanion.mode === 'windowed' || topologyRenderWindowCompanion.tableCompanion === 'required';
+  const topologyShouldRenderCompanionMetricTable = !topologyShouldShowRenderWindowMetricTable;
   const topologyMetricTableLabels = React.useMemo(() => ({
     edgeCount: t('topology.metric-table.edge-count', { count: topologyMetricRows.length }),
     requestRate: t('topology.metric-table.request-rate-unit'),
@@ -1040,13 +1315,30 @@ export default function TopologyPage({
     renderWindowFilterPartial: t('topology.metric-table.filter.partial'),
     renderWindowFilterHidden: t('topology.metric-table.filter.hidden'),
     renderWindowFilterUnknown: t('topology.metric-table.filter.unknown'),
+    renderWindowEdgeSummary: t('topology.metric-table.edge-summary', {
+      rendered: topologyG6RenderedEdgeCount,
+      total: topologyG6Graph.edges.length
+    }),
+    renderWindowRowSummary: (rendered: number, total: number) => t('topology.metric-table.row-render-summary', { rendered, total }),
+    renderWindowShowMore: (next: number, total: number) => t('topology.metric-table.row-render-show-more', { next, total }),
     rowAriaLabel: (row: HzTopologyMetricRow) => t('topology.metric-table.open-edge-aria', { edge: String(row.id) })
-  }), [t, topologyMetricRows.length]);
+  }), [t, topologyG6Graph.edges.length, topologyG6RenderedEdgeCount, topologyMetricRows.length]);
   const topologyHoveredDetailEdge = topologyG6HoveredEdgeId ? findEdge(map.edges, topologyG6HoveredEdgeId) : undefined;
   const topologyLiveHoverEdge = topologyHoveredDetailEdge;
-  const topologyInvestigationEdge = topologyHoveredDetailEdge ?? topologyDetailEdge;
+  const topologySelectedPathSummaryEdge = topologyCanvasSelectedEdgeId ? findEdge(map.edges, topologyCanvasSelectedEdgeId) : undefined;
+  const topologyInvestigationEdge = topologySelectedPathSummaryEdge ?? topologyHoveredDetailEdge ?? topologyDetailEdge;
   const topologyHoverFromNode = topologyInvestigationEdge ? findNode(map.nodes, topologyInvestigationEdge.from) : undefined;
   const topologyHoverToNode = topologyInvestigationEdge ? findNode(map.nodes, topologyInvestigationEdge.to) : undefined;
+  const topologyPathSummaryInteractionState = topologyCanvasSelectedEdgeId
+    ? 'selected'
+    : topologyG6HoveredEdgeId
+      ? 'hovered'
+      : 'preview';
+  const topologyPathSummaryTitle = topologyPathSummaryInteractionState === 'selected'
+    ? t('topology.path-summary.title.selected')
+    : topologyPathSummaryInteractionState === 'hovered'
+      ? t('topology.path-summary.title.hovered')
+      : t('topology.path-summary.title.preview');
   const topologyLiveHoverFromNode = topologyLiveHoverEdge ? findNode(map.nodes, topologyLiveHoverEdge.from) : undefined;
   const topologyLiveHoverToNode = topologyLiveHoverEdge ? findNode(map.nodes, topologyLiveHoverEdge.to) : undefined;
   const topologyCanvasDrawerMode: HzTopologyCanvasDrawerMode = topologyDetailEdge && primaryNode
@@ -1056,7 +1348,18 @@ export default function TopologyPage({
       : primaryNode
         ? 'node'
         : 'none';
+  const topologyShouldShowCompanionRail = Boolean(primaryNode || topologyDetailEdge);
+  const topologyWorkbenchLayout = topologyShouldShowCompanionRail ? 'canvas-companion' : 'canvas-only';
+  const topologyCompanionRailVisibility = topologyShouldShowCompanionRail ? 'visible-side-rail' : 'hidden-from-layout';
   const topologyCompanionJumpActiveId = topologyDetailEdge ? 'edge-detail' : primaryNode ? 'current-node' : 'view-mode';
+  const topologyCompanionJumpActiveResetKey = [
+    topologyCompanionJumpActiveId,
+    primaryNode?.id ?? 'none',
+    topologyDetailEdge?.id ?? 'none'
+  ].join(':');
+  const topologyCompanionSelectionResetKey = topologyLocalSelection.source === 'none'
+    ? undefined
+    : topologyCompanionJumpActiveResetKey;
   const topologyCompanionJumpItems = [
     {
       id: 'view-mode',
@@ -1070,12 +1373,16 @@ export default function TopologyPage({
       label: t('topology.companion.jump.legend'),
       active: topologyCompanionJumpActiveId === 'legend'
     },
-    {
-      id: 'edge-red',
-      href: '#topology-companion-edge-red',
-      label: t('topology.companion.jump.red'),
-      active: topologyCompanionJumpActiveId === 'edge-red'
-    },
+    ...(topologyShouldRenderCompanionMetricTable
+      ? [
+          {
+            id: 'edge-red',
+            href: '#topology-companion-edge-red',
+            label: t('topology.companion.jump.red'),
+            active: topologyCompanionJumpActiveId === 'edge-red'
+          }
+        ]
+      : []),
     ...(map.impactTimeline.length > 0
       ? [
           {
@@ -1117,8 +1424,29 @@ export default function TopologyPage({
   const topologyLoadingEvidenceSources = ['api', 'greptime', 'trace', 'relation'];
   const topologyTraceCallUnavailable = loadedApiGraph === null && topologyTraceCallScope;
   const topologyShouldShowEmptyState =
-    map.dataSource === 'api' && !topologyIsPending && (map.nodes.length === 0 || topologyTraceCallMissingEdges);
-  const topologyEmptyStateKind = loadedApiGraph === null ? 'degraded' : topologyTraceCallMissingEdges ? 'filtered-empty' : 'api-empty';
+    map.dataSource === 'api' && !topologyIsPending && (map.nodes.length === 0 || topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing);
+  const topologyRelationGapApiSourceKinds =
+    loadedApiGraph?.sourceKinds && loadedApiGraph.sourceKinds.length > 0
+      ? loadedApiGraph.sourceKinds.join(' ')
+      : 'none';
+  const topologyRelationGapTraceCallParams = new URLSearchParams(topologyG6ResetParams);
+  topologyRelationGapTraceCallParams.set('viewMode', 'service-call');
+  topologyRelationGapTraceCallParams.set('sourceKind', 'otlp-trace-call');
+  topologyRelationGapTraceCallParams.set('relationType', 'trace-call');
+  const topologyRelationGapTraceCallHref = `/topology?${topologyRelationGapTraceCallParams.toString()}`;
+  const handleTopologyRelationGapTraceCall = React.useCallback(
+    (event: React.MouseEvent<HTMLAnchorElement>) => {
+      event.preventDefault();
+      topologyRouter.push(topologyRelationGapTraceCallHref);
+    },
+    [topologyRelationGapTraceCallHref, topologyRouter]
+  );
+  const topologyEmptyStateKind =
+    loadedApiGraph === null
+      ? 'degraded'
+      : topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing
+        ? 'filtered-empty'
+        : 'api-empty';
   const topologyEmptyStateDataKind = topologyTraceCallUnavailable
     ? 'trace-call-degraded'
     : topologyTraceCallMissingEdges
@@ -1131,6 +1459,8 @@ export default function TopologyPage({
       ? ['api', 'unavailable']
       : topologyTraceCallMissingEdges
         ? ['otlp-trace-call', 'greptime', 'trace']
+        : topologyFilteredEvidenceMissing
+          ? [map.filterContext.sourceKind ?? map.filterContext.viewMode]
         : topologyEvidenceSources;
   const topologyEmptyStateTitle = topologyTraceCallMissingEdges
     ? t('topology.empty.trace-call.title')
@@ -1152,7 +1482,9 @@ export default function TopologyPage({
       ? t('topology.degraded.trace-call.source')
     : topologyEmptyStateKind === 'degraded'
       ? t('topology.degraded.api.source')
-      : 'API';
+      : topologyFilteredEvidenceMissing
+        ? (activeSourceLabel ?? activeViewModeLabel ?? 'API')
+        : 'API';
   const topologyLegendNodeTypeItems = React.useMemo(
     () => {
       const visibleNodeKinds = new Set(
@@ -1163,7 +1495,6 @@ export default function TopologyPage({
       return HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => visibleNodeKinds.has(icon.kind)).map(icon => ({
         id: `node-type-${icon.kind}`,
         label: t(`topology.legend.node-type.${icon.kind}`),
-        value: icon.iconName,
         iconSrc: icon.iconSrc,
         iconAlt: '',
         iconLibrary: icon.iconLibrary,
@@ -1174,80 +1505,18 @@ export default function TopologyPage({
     },
     [topologyG6Graph.nodes, t]
   );
-  const topologyHasVisibleNodes = topologyG6Graph.nodes.length > 0;
-  const topologyHasVisibleGraphEvidence = topologyHasVisibleNodes || topologyG6Graph.edges.length > 0;
   const topologyLegendSections = React.useMemo(
     () => {
-      const sections = [
+      const sections: HzTopologyLegendSection[] = [
         {
           id: 'node-type',
           label: t('topology.legend.node-type'),
           items: topologyLegendNodeTypeItems
         }
       ];
-      if (topologyHasVisibleNodes) {
-        sections.push({
-          id: 'status',
-          label: t('topology.legend.status'),
-          items: [
-            {
-              id: 'healthy-node',
-              label: t('topology.legend.status.healthy'),
-              color: '#22c55e',
-              visualSource: 'hertzbeat-status-token' as const,
-              value: t('topology.legend.status.healthy-value')
-            },
-            {
-              id: 'warning-node',
-              label: t('topology.legend.status.warning'),
-              color: '#f59e0b',
-              visualSource: 'hertzbeat-status-token' as const,
-              value: t('topology.legend.status.warning-value')
-            },
-            {
-              id: 'critical-node',
-              label: t('topology.legend.status.critical'),
-              color: '#ef4444',
-              visualSource: 'hertzbeat-status-token' as const,
-              value: t('topology.legend.status.critical-value')
-            }
-          ]
-        });
-      }
-      if (topologyHasVisibleGraphEvidence) {
-        sections.push({
-          id: 'interaction',
-          label: t('topology.legend.interaction'),
-          items: [
-            {
-              id: 'selected-node',
-              label: t('topology.legend.interaction.selected-node'),
-              color: '#e5edf8',
-              visualSource: 'hertzbeat-interaction-token' as const,
-              value: t('topology.legend.interaction.selected-node-value')
-            },
-            {
-              id: 'directional-edge',
-              label: t('topology.legend.interaction.directional-edge'),
-              color: '#94a3b8',
-              pattern: 'solid' as const,
-              visualSource: 'hertzbeat-edge-token' as const,
-              value: t('topology.legend.interaction.directional-edge-value')
-            },
-            {
-              id: 'dimmed-edge',
-              label: t('topology.legend.interaction.dimmed-edge'),
-              color: '#94a3b8',
-              pattern: 'muted' as const,
-              visualSource: 'hertzbeat-edge-token' as const,
-              value: t('topology.legend.interaction.dimmed-edge-value')
-            }
-          ]
-        });
-      }
       return sections;
     },
-    [t, topologyHasVisibleGraphEvidence, topologyHasVisibleNodes, topologyLegendNodeTypeItems]
+    [t, topologyLegendNodeTypeItems]
   );
   const topologyEdgeIds = React.useMemo(
     () => new Set(map.edges.map(edge => edge.id)),
@@ -1325,14 +1594,21 @@ export default function TopologyPage({
     <HzTopologyWorkbenchFrame
       as="main"
       data-topology-route="hertzbeat-entity-topology"
+      data-hz-topology-frame="workbench"
       data-topology-data-source={map.dataSource}
       data-topology-incoming-context={map.filterContext.hasIncomingContext ? 'entity-filter' : 'none'}
-      data-topology-active-node-id={map.activeNodeId ?? 'none'}
+      data-topology-active-node-id={topologyVisibleActiveNodeId ?? 'none'}
       data-topology-active-edge-id={map.selectedEdgeId ?? 'none'}
       data-topology-selection-behavior="in-page-drawer"
       data-topology-selection-source={topologyLocalSelection.source}
       data-topology-selection-node-id={topologyLocalSelection.nodeId ?? 'none'}
       data-topology-selection-edge-id={topologyLocalSelection.edgeId ?? 'none'}
+      data-topology-live-interaction-proof="wheel-hover-click-preserve-url-viewport"
+      data-topology-live-interaction-proof-owner="hertzbeat-ui-g6-live-interaction"
+      data-topology-normal-browser-giant-node-proof="compact-1x-no-fill"
+      data-topology-normal-browser-giant-node-proof-owner="hertzbeat-ui-g6-viewport"
+      data-topology-ordinary-inspection-proof="api-current-window-no-stale-empty-gap"
+      data-topology-ordinary-inspection-proof-owner="hertzbeat-ui-workbench-frame"
       data-topology-focus-navigation="explicit-soft-route"
       data-topology-focus-navigation-owner="hertzbeat-ui-g6-focus-entry"
       data-topology-focus-navigation-trigger="double-click-node"
@@ -1350,6 +1626,13 @@ export default function TopologyPage({
       data-topology-search-navigation-url-policy="preserve-current-url"
       data-topology-active-view-mode={map.filterContext.viewMode}
       data-topology-active-source-kind={map.filterContext.sourceKind ?? 'all'}
+      data-topology-api-settle-state={topologyApiSettleState}
+      data-hz-topology-api-state={topologyApiSettleState}
+      data-topology-api-settle-state-owner="hertzbeat-ui-workbench-frame"
+      data-topology-api-error-state={topologyApiErrorState}
+      data-topology-api-error-status={topologyApiErrorStatus}
+      data-topology-api-error-resolution={topologyApiErrorResolution}
+      data-topology-api-error-owner="hertzbeat-ui-workbench-frame"
       data-topology-trace-call-state={topologyTraceCallState}
       data-topology-trace-call-proof-owner="hertzbeat-ui-workbench-frame"
       data-topology-trace-call-edge-count={topologyTraceCallEdges.length}
@@ -1357,13 +1640,32 @@ export default function TopologyPage({
       data-topology-trace-call-window-edge-count={topologyTraceCallWindowEdgeCount}
       data-topology-trace-call-sample-edge-count={topologyTraceCallSampleEdgeCount}
       data-topology-trace-call-red-state={topologyTraceCallRedState}
+      data-topology-relation-gap-state={topologyRelationGapState}
+      data-topology-relation-gap-owner="hertzbeat-ui-relation-gap"
+      data-topology-relation-gap-node-count={map.nodes.length}
+      data-topology-relation-gap-edge-count={map.edges.length}
+      data-topology-relation-gap-source-kind={map.filterContext.sourceKind ?? 'all'}
+      data-topology-relation-gap-api-source-kinds={topologyRelationGapApiSourceKinds}
+      data-topology-relation-gap-view-mode={map.filterContext.viewMode}
+      data-topology-relation-gap-visual={topologyRelationGapVisual}
+      data-topology-relation-gap-canvas-policy={topologyRelationGapCanvasPolicy}
+      data-topology-empty-state={topologyShouldShowEmptyState ? topologyEmptyStateDataKind : 'none'}
       data-topology-api-depth={map.apiDepth ?? 'none'}
       data-topology-api-scope-owner="hertzbeat-ui-workbench-frame"
       data-topology-api-request-path={topologyApiRequestPath}
       data-topology-api-scope-source-kind={map.filterContext.sourceKind ?? 'all'}
       data-topology-api-scope-relation-type={topologyApiScopeRelationType}
       data-topology-api-scope-view-mode={map.filterContext.viewMode}
+      data-topology-api-focus-resolution={topologyHasUnresolvableFocusEntity ? 'unresolvable-client-focus' : 'api-compatible'}
+      data-topology-api-focus-resolution-owner="topology-route-api-scope"
       data-topology-api-timeout-ms={topologyApiRequestTimeoutMs}
+      data-topology-scale-proof={topologyScaleProof || undefined}
+      data-topology-scale-proof-owner={topologyScaleProof ? 'topology-route-api-scope' : undefined}
+      data-topology-scale-proof-focus-entity-id={topologyScaleProofFocusEntityId}
+      data-topology-scale-proof-api-policy={topologyScaleProofApiPolicy}
+      data-topology-scale-proof-time-policy={topologyScaleProofTimeWindow?.policy}
+      data-topology-scale-proof-window-start={topologyScaleProofTimeWindow?.start}
+      data-topology-scale-proof-window-end={topologyScaleProofTimeWindow?.end}
       data-topology-workbench-frame-owner="hertzbeat-ui-workbench-frame"
       data-topology-workbench-frame-boundary-owner="hertzbeat-ui-workbench-frame-boundary"
       density="compact"
@@ -1400,7 +1702,9 @@ export default function TopologyPage({
               {
                 id: 'refresh',
                 label: t('topology.refresh'),
-                'data-topology-refresh-action': 'refresh'
+                onClick: handleTopologyRefresh,
+                'data-topology-refresh-action': 'refresh',
+                'data-topology-refresh-action-behavior': 'in-page-api-reload'
               }
             ]}
           />
@@ -1418,25 +1722,46 @@ export default function TopologyPage({
         label={t('topology.focus-trail.label')}
         crumbs={topologyFocusTrailCrumbs}
         filters={topologyFocusTrailFilters}
-        hiddenCountLabel={t('topology.focus-trail.hidden-count', { count: topologyHiddenCount })}
+        hiddenCountLabel={topologyHiddenCount > 0 ? t('topology.focus-trail.hidden-count', { count: topologyHiddenCount }) : undefined}
         hiddenCountProps={{
           'data-topology-focus-trail-hidden-count-owner': 'hertzbeat-ui-focus-trail-hidden-count'
         } as React.HTMLAttributes<HTMLSpanElement>}
-        exitAction={{
-          href: topologyFocusExitHref,
-          label: t('topology.focus-trail.exit'),
-          onClick: handleTopologyFocusExit,
-          'data-topology-focus-trail-exit-owner': 'hertzbeat-ui-focus-trail-exit'
-        } as React.AnchorHTMLAttributes<HTMLAnchorElement> & { label: React.ReactNode }}
+        exitAction={
+          topologyShouldShowFocusExit
+            ? ({
+                href: topologyFocusExitHref,
+                label: t('topology.focus-trail.exit'),
+                onClick: handleTopologyFocusExit,
+                'data-topology-focus-trail-exit-owner': 'hertzbeat-ui-focus-trail-exit'
+              } as React.AnchorHTMLAttributes<HTMLAnchorElement> & { label: React.ReactNode })
+            : undefined
+        }
         boundary="none"
         density="graph-dock"
       />
 
+      {topologyHasRouteEntityFocus ? (
+        <HzTopologyActionLink
+          data-topology-view-scope="focused-adjacency"
+          data-topology-view-scope-owner="hertzbeat-ui-focus-scope-guide"
+          data-topology-view-scope-global-action="open-global-topology"
+          data-topology-view-scope-global-href={topologyGlobalScopeHref}
+          id="focused-adjacency-global-scope"
+          href={topologyGlobalScopeHref}
+          label={t('topology.view-scope.focused-adjacency')}
+          copy={t('topology.view-scope.focused-adjacency.copy')}
+          spacing="none"
+          onClick={handleTopologyGlobalScope}
+          className="mx-0 -mt-1 mb-1 w-full border-transparent bg-transparent px-0 py-0 text-[11px] text-[#9ca3b4] hover:border-transparent hover:bg-transparent"
+        />
+      ) : null}
+
       <HzTopologyToolbar
         data-topology-controls="hertzbeat-topology-controls"
         data-topology-controls-owner="hertzbeat-ui-toolbar"
         data-topology-toolbar-boundary-owner="hertzbeat-ui-toolbar-boundary"
         data-topology-toolbar-state-owner="hertzbeat-ui-toolbar-state"
+        data-topology-toolbar-group-control-visibility={topologyToolbarGroupControlVisibility}
         data-topology-filter-summary="incoming-context"
         density="graph-first"
         boundary="none"
@@ -1457,7 +1782,7 @@ export default function TopologyPage({
           ...map.sources.map(source => ({ value: source.kind, label: source.label }))
         ]}
         depthLabel={t('topology.state.depth')}
-        depthValue={String(map.apiDepth ?? 2)}
+        depthValue={String(topologyScopedDepth ?? 2)}
         depthOptions={[
           { value: '1', label: t('topology.state.depth.one-hop') },
           { value: '2', label: t('topology.state.depth.two-hop') },
@@ -1465,12 +1790,16 @@ export default function TopologyPage({
         ]}
         groupByLabel={t('topology.state.group')}
         groupByValue={map.filterContext.groupBy}
-        groupByOptions={[
-          { value: 'none', label: t('topology.state.group.none') },
-          { value: 'environment', label: t('topology.state.group.environment') },
-          { value: 'source-kind', label: t('topology.state.group.source-kind') },
-          { value: 'entity-type', label: t('topology.state.group.entity-type') }
-        ]}
+        groupByOptions={
+          topologyHasRenderableGraph
+            ? [
+                { value: 'none', label: t('topology.state.group.none') },
+                { value: 'environment', label: t('topology.state.group.environment') },
+                { value: 'source-kind', label: t('topology.state.group.source-kind') },
+                { value: 'entity-type', label: t('topology.state.group.entity-type') }
+              ]
+            : []
+        }
         resetLabel={t('topology.toolbar.reset')}
         resetHref="/topology"
         onEnvironmentChange={handleTopologyEnvironmentChange}
@@ -1485,7 +1814,7 @@ export default function TopologyPage({
         stateItems={topologyToolbarStateItems}
       />
 
-      <HzTopologyWorkbenchGrid data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid" layout="canvas-only">
+      <HzTopologyWorkbenchGrid data-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid" layout={topologyWorkbenchLayout}>
         <HzTopologyWorkbenchSlot
           data-topology-canvas-slot-owner="hertzbeat-ui-workbench-slot"
           kind="canvas"
@@ -1497,6 +1826,15 @@ export default function TopologyPage({
             data-topology-canvas-interaction-owner="hertzbeat-ui-canvas-interaction"
             data-topology-canvas-interaction-scope-owner="hertzbeat-ui-canvas-interaction-scope"
             data-topology-canvas-boundary-owner="hertzbeat-ui-canvas-boundary"
+            data-topology-g6-render-proof-owner="hertzbeat-topology-page-g6-proof"
+            data-topology-g6-render-window-mode={topologyRenderWindowCompanion.mode}
+            data-topology-g6-render-window-total-node-count={topologyRenderWindowCompanion.totalNodeCount}
+            data-topology-g6-render-window-rendered-node-count={topologyRenderWindowCompanion.renderedNodeCount}
+            data-topology-g6-render-window-hidden-node-count={topologyRenderWindowCompanion.hiddenNodeCount}
+            data-topology-g6-render-window-total-edge-count={topologyRenderWindowCompanion.totalEdgeCount}
+            data-topology-g6-render-window-rendered-edge-count={topologyG6RenderedEdgeCount}
+            data-topology-g6-render-window-hidden-edge-count={topologyG6HiddenEdgeCount}
+            data-topology-g6-summary-count-policy={topologyG6SummaryCountPolicy}
             layout={topologyLayout}
             interactionMode="inspect"
             interactionScope="hover-group"
@@ -1509,9 +1847,44 @@ export default function TopologyPage({
           <HzTopologyCanvasAnnotation
             data-topology-canvas-annotation-owner="hertzbeat-ui-canvas-annotation"
             title={topologyLayoutLabel}
-            copy={`${formatTopologyDepth(map.apiDepth, t)} · ${formatTimeRange(map.filterContext.timeRange, t)}`}
+            copy={`${formatTopologyDepth(topologyScopedDepth, t)} · ${formatTimeRange(map.filterContext.timeRange, t)}`}
             visibility="assistive"
           />
+          {topologyRelationGap ? (
+            <HzTopologyCanvasAnnotation
+              data-topology-relation-gap-state="nodes-without-edges"
+              data-topology-relation-gap-owner="hertzbeat-ui-relation-gap"
+              data-topology-relation-gap-node-count={map.nodes.length}
+              data-topology-relation-gap-edge-count={map.edges.length}
+              data-topology-relation-gap-source-kind={map.filterContext.sourceKind ?? 'all'}
+              data-topology-relation-gap-api-source-kinds={topologyRelationGapApiSourceKinds}
+              data-topology-relation-gap-view-mode={map.filterContext.viewMode}
+              data-topology-relation-gap-visual="canvas-annotation"
+              data-topology-relation-gap-canvas-policy="hide-isolated-node-only-graph"
+              title={t('topology.relation-gap.title')}
+              copy={t('topology.relation-gap.copy')}
+              placement="top-left"
+              visibility="visible"
+              className="border-transparent bg-[#08090c]/82"
+            />
+          ) : null}
+          {topologyRelationGap ? (
+            <HzTopologyActionLink
+              data-topology-relation-gap-action="otlp-trace-call"
+              data-topology-relation-gap-action-owner="hertzbeat-ui-action-link"
+              data-topology-relation-gap-action-source-kind="otlp-trace-call"
+              data-topology-relation-gap-action-view-mode="service-call"
+              data-topology-relation-gap-action-href={topologyRelationGapTraceCallHref}
+              id="relation-gap-trace-call"
+              href={topologyRelationGapTraceCallHref}
+              label={t('topology.relation-gap.action.trace-call')}
+              copy={t('topology.relation-gap.action.trace-call.copy')}
+              emphasis="primary"
+              spacing="none"
+              onClick={handleTopologyRelationGapTraceCall}
+              className="pointer-events-auto absolute left-3 top-[76px] z-10 w-[220px] border-[#252832] bg-[#0b0c0f]/92 px-2.5 py-1.5 shadow-[0_10px_28px_rgba(0,0,0,0.24)] backdrop-blur sm:left-4 sm:top-[80px]"
+            />
+          ) : null}
           <HzTopologyG6Canvas
             graph={topologyG6Graph}
             selectedNodeId={topologyTraceCallMissingEdges ? undefined : primaryNode?.id}
@@ -1573,12 +1946,18 @@ export default function TopologyPage({
             layout={topologyG6Layout}
             height="workbench"
             overlayMode="non-occluding"
-            summaryLabel={`${t('topology.group-panel.node-count', { count: map.nodes.length })} · ${t('topology.group-panel.edge-count', { count: map.edges.length })}`}
+            summaryLabel={topologyG6SummaryLabel}
+            nodeOnlyExplanationLabel={t('topology.node-only.explanation')}
             fitViewLabel={t('topology.view.fit')}
             resetViewLabel={t('topology.toolbar.reset')}
             zoomInLabel={t('topology.view.zoom-in')}
             zoomOutLabel={t('topology.view.zoom-out')}
             selectedFocusLabel={t('topology.view.focus-selected')}
+            edgeDensityDrilldownLabel={t('topology.edge-density.open-table')}
+            edgeDensityDrilldownDetailLabel={t('topology.edge-density.hidden-in-table', {
+              count: Math.max(0, topologyG6Graph.edges.length - topologyG6RenderedEdgeCount)
+            })}
+            edgeDensityDrilldownTargetId="topology-metric-table"
             onNodeSelect={handleTopologyG6NodeSelect}
             onNodeFocus={handleTopologyG6NodeFocus}
             onEdgeSelect={handleTopologyG6EdgeSelect}
@@ -1631,9 +2010,10 @@ export default function TopologyPage({
               relationType={topologyApiScopeRelationType}
               focusEntityId={routeContext?.entityId ?? 'none'}
               depth={map.apiDepth}
-              resultCount={topologyTraceCallMissingEdges ? 0 : map.nodes.length}
+              resultCount={topologyTraceCallMissingEdges || topologyFilteredEvidenceMissing ? 0 : map.nodes.length}
               evidenceSources={topologyEmptyStateEvidenceSources}
               kind={topologyEmptyStateKind}
+              copyVisibility="assistive"
               placement="canvas-center"
               boundary="canvas"
             />
@@ -1657,14 +2037,15 @@ export default function TopologyPage({
         ) : null}
           </HzTopologyCanvas>
         </HzTopologyWorkbenchSlot>
-        <HzTopologyWorkbenchSlot
-          data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"
-          data-topology-companion-rail-visibility="hidden-from-layout"
-          kind="companion"
-          surface="content"
-          className="hidden"
-          aria-hidden="true"
-        >
+        {topologyHasRenderableGraph ? (
+          <HzTopologyWorkbenchSlot
+            data-topology-companion-slot-owner="hertzbeat-ui-workbench-slot"
+            data-topology-companion-rail-visibility={topologyCompanionRailVisibility}
+            kind="companion"
+            surface="content"
+            className={topologyShouldShowCompanionRail ? undefined : 'hidden'}
+            aria-hidden={topologyShouldShowCompanionRail ? undefined : true}
+          >
         <HzTopologyCompanionRail
           data-topology-companion-rail-owner="hertzbeat-ui-companion-rail"
           data-topology-companion-rail-boundary-owner="hertzbeat-ui-companion-rail-boundary"
@@ -1681,8 +2062,115 @@ export default function TopologyPage({
             ariaLabel={t('topology.companion.jump.aria')}
             density="graph-first"
             activeMode="contained-rail-scroll"
+            activeResetKey={topologyCompanionSelectionResetKey}
             items={topologyCompanionJumpItems}
           />
+          {topologyDetailEdge ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="edge-detail"
+              data-topology-companion-section-collapsible="edge-detail"
+              sectionId="edge-detail"
+              anchorId="topology-companion-edge-detail"
+              density="graph-first"
+              collapsible
+              collapsed={topologyCompanionCollapsedSections['edge-detail'] ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-detail', collapsed)}
+            >
+              <HzTopologyDetailDrawer
+                data-topology-edge-evidence-panel={topologyDetailEdge.id}
+                data-topology-edge-evidence-window-owner="hertzbeat-ui-detail-drawer"
+                data-topology-edge-evidence-first-seen={topologyDetailEdge.evidence.firstSeen || 'none'}
+                data-topology-edge-evidence-last-seen={topologyDetailEdge.evidence.lastSeen || 'none'}
+                data-topology-edge-evidence-sample-trace-id={topologyDetailEdge.evidence.sampleTraceId || 'none'}
+                data-topology-edge-evidence-sample-span-id={topologyDetailEdge.evidence.sampleSpanId || 'none'}
+                data-topology-edge-render-window-visibility={topologyDetailEdgeRenderWindowContext?.visibility ?? 'direct'}
+                data-topology-edge-render-window-source-visible={topologyDetailEdgeRenderWindowContext?.sourceVisible ?? 'unknown'}
+                data-topology-edge-render-window-target-visible={topologyDetailEdgeRenderWindowContext?.targetVisible ?? 'unknown'}
+                data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"
+                data-topology-detail-drawer-surface-owner="hertzbeat-ui-detail-surface"
+                kind="edge"
+                density="graph-first"
+                subjectId={topologyDetailEdge.id}
+                sourceId={topologyDetailEdge.from}
+                targetId={topologyDetailEdge.to}
+                relationType={topologyDetailEdge.relationshipType}
+                sourceKind={topologyDetailEdge.source}
+                surface="framed"
+                eyebrow={t('topology.edge.evidence.title')}
+                title={topologyDetailEdge.evidence.title}
+                subtitle={`${topologyDetailEdge.evidence.sourceLabel} · ${topologyDetailEdge.evidence.collectedBy}`}
+                boundary={topologyDetailEdge.evidence.boundary}
+                boundaryProps={{
+                  'data-topology-edge-evidence-boundary': 'roadmap-boundary',
+                  'data-topology-detail-boundary-owner': 'hertzbeat-ui-detail-boundary'
+                } as React.HTMLAttributes<HTMLDivElement>}
+                facts={buildTopologyEdgeDetailFacts(topologyDetailEdge, t, topologyDetailEdgeRenderWindowContext)}
+                actions={buildTopologyEdgeDetailActions(topologyDetailEdge, t)}
+                signalActions={buildTopologyEdgeSignalActions(topologyDetailEdge, t)}
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
+          {primaryNode ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="current-node"
+              data-topology-companion-section-collapsible="current-node"
+              sectionId="current-node"
+              anchorId="topology-companion-current-node"
+              density="graph-first"
+              collapsible
+              collapsed={topologyCompanionCollapsedSections['current-node'] ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('current-node', collapsed)}
+            >
+              <HzTopologyDetailDrawer
+                data-topology-current-entity-panel={primaryNode.id}
+                data-topology-current-entity-panel-owner="hertzbeat-ui-detail-drawer"
+                data-topology-current-entity-panel-surface-owner="hertzbeat-ui-detail-surface"
+                data-topology-current-entity-panel-hover-continuity="selected-node-survives-edge-hover"
+                data-topology-current-entity-panel-hover-invariants="selected-node-detail edge-hover-path-summary-temporary pointer-leave-clears-hover no-url-change no-remount no-refit viewport-preserved render-key-stable"
+                data-topology-current-entity-panel-rail-scroll-continuity="selected-node-retakes-companion-first-viewport"
+                data-topology-current-entity-panel-rail-scroll-invariants="manual-active-resets-scroll-active active-section-scroll collapse-state-preserved no-url-change no-remount no-refit viewport-preserved render-key-stable"
+                data-topology-current-entity-panel-rail-fit="compact-side-rail"
+                data-topology-current-entity-panel-rail-fit-invariants="bounded-block internal-scroll no-page-scroll no-rail-overflow scan-first-viewport"
+                data-topology-current-entity-panel-signal-entry="header-dock"
+                data-topology-current-entity-panel-signal-entry-invariants="metrics-logs-traces-before-long-evidence sticky-with-node-context no-extra-navigation"
+                data-topology-current-entity-panel-scroll-reset="identity-change"
+                data-topology-current-entity-panel-scroll-reset-invariants="identity-change scroll-top-zero no-page-scroll no-rail-scroll no-url-change"
+                data-topology-current-entity-neighbor-owner="hertzbeat-ui-detail-neighbor-evidence"
+                data-topology-current-entity-upstream-count={currentNodeNeighborEvidence?.upstreamNodeIds.length ?? 0}
+                data-topology-current-entity-downstream-count={currentNodeNeighborEvidence?.downstreamNodeIds.length ?? 0}
+                data-topology-current-entity-upstream-node-ids={currentNodeNeighborEvidence?.upstreamNodeIds.join(' ') || 'none'}
+                data-topology-current-entity-downstream-node-ids={currentNodeNeighborEvidence?.downstreamNodeIds.join(' ') || 'none'}
+                data-topology-current-entity-group-by={map.filterContext.groupBy}
+                data-topology-current-entity-source-kind={map.filterContext.sourceKind ?? 'all'}
+                data-topology-current-entity-view-mode={map.filterContext.viewMode}
+                kind="node"
+                density="graph-first"
+                subjectId={primaryNode.id}
+                entityType={primaryNode.entityType}
+                sourceKind={primaryNode.source}
+                surface="framed"
+                eyebrow={t('topology.current-entity')}
+                title={primaryNode.label}
+                subtitle={`${primaryNode.entityId} · ${primaryNode.namespace} / ${primaryNode.environment}`}
+                boundary={t('topology.current-entity.boundary')}
+                boundaryProps={{
+                  'data-topology-current-entity-boundary-owner': 'hertzbeat-ui-detail-boundary'
+                } as React.HTMLAttributes<HTMLDivElement>}
+                facts={buildTopologyNodeDetailFacts(
+                  primaryNode,
+                  currentNodeNeighborEvidence ?? buildTopologyNodeNeighborEvidence(primaryNode, map.nodes, map.edges),
+                  t
+                )}
+                actions={buildTopologyNodeDetailActions(primaryNode, map.filterContext, map.apiDepth, t)}
+                signalActions={buildTopologyNodeSignalActions(primaryNode, map.filterContext, map.apiDepth, t)}
+                signalActionsLabel={t('topology.context-link.signals')}
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
           <HzTopologyCompanionSection
             data-topology-companion-section="view-mode"
             sectionId="view-mode"
@@ -1738,38 +2226,41 @@ export default function TopologyPage({
             />
           </HzTopologyCompanionSection>
 
-          <HzTopologyCompanionSection
-            data-topology-companion-section="edge-red"
-            data-topology-companion-section-collapsible="edge-red"
-            sectionId="edge-red"
-            anchorId="topology-companion-edge-red"
-            density="graph-first"
-            collapsible
-            collapsed={topologyCompanionCollapsedSections['edge-red'] ?? false}
-            collapseLabel={t('topology.companion.section.collapse')}
-            expandLabel={t('topology.companion.section.expand')}
-            onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-red', collapsed)}
-          >
-            <HzTopologyMetricTable
-              data-topology-metric-table-owner="hertzbeat-ui"
-              data-topology-metric-table-scope="edge-red-companion"
-              data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
-              data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
-              data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
-              data-topology-metric-table-filter-behavior="in-page-no-route-change"
-              title={t('topology.metric-table.title')}
+          {topologyShouldRenderCompanionMetricTable ? (
+            <HzTopologyCompanionSection
+              data-topology-companion-section="edge-red"
+              data-topology-companion-section-collapsible="edge-red"
+              sectionId="edge-red"
+              anchorId="topology-companion-edge-red"
               density="graph-first"
-              rows={topologyMetricRows}
-              selectedRowId={topologyMetricSelectedRowId}
-              renderWindowFilter={topologyMetricWindowFilter}
-              onRenderWindowFilterChange={setTopologyMetricWindowFilter}
-              renderWindowCompanion={topologyMetricRenderWindowCompanion}
-              emptyLabel={t('topology.metric-table.empty')}
-              labels={topologyMetricTableLabels}
-              onRowSelect={handleTopologyMetricRowSelect}
-              boundary="framed"
-            />
-          </HzTopologyCompanionSection>
+              collapsible
+              collapsed={topologyCompanionCollapsedSections['edge-red'] ?? false}
+              collapseLabel={t('topology.companion.section.collapse')}
+              expandLabel={t('topology.companion.section.expand')}
+              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-red', collapsed)}
+            >
+              <HzTopologyMetricTable
+                data-topology-metric-table-owner="hertzbeat-ui"
+                data-topology-metric-table-scope="edge-red-companion"
+                data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
+                data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
+                data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
+                data-topology-metric-table-filter-behavior="in-page-no-route-change"
+                title={t('topology.metric-table.title')}
+                density="graph-first"
+                rows={topologyMetricRows}
+                selectedRowId={topologyMetricSelectedRowId}
+                selectionSource={topologyLocalSelection.source}
+                renderWindowFilter={topologyMetricWindowFilter}
+                onRenderWindowFilterChange={setTopologyMetricWindowFilter}
+                renderWindowCompanion={topologyMetricRenderWindowCompanion}
+                emptyLabel={t('topology.metric-table.empty')}
+                labels={topologyMetricTableLabels}
+                onRowSelect={handleTopologyMetricRowSelect}
+                boundary="framed"
+              />
+            </HzTopologyCompanionSection>
+          ) : null}
 
           {map.impactTimeline.length > 0 ? (
             <HzTopologyCompanionSection
@@ -1807,102 +2298,9 @@ export default function TopologyPage({
             </HzTopologyCompanionSection>
           ) : null}
 
-          {topologyDetailEdge ? (
-            <HzTopologyCompanionSection
-              data-topology-companion-section="edge-detail"
-              data-topology-companion-section-collapsible="edge-detail"
-              sectionId="edge-detail"
-              anchorId="topology-companion-edge-detail"
-              density="graph-first"
-              collapsible
-              collapsed={topologyCompanionCollapsedSections['edge-detail'] ?? false}
-              collapseLabel={t('topology.companion.section.collapse')}
-              expandLabel={t('topology.companion.section.expand')}
-              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('edge-detail', collapsed)}
-            >
-              <HzTopologyDetailDrawer
-                data-topology-edge-evidence-panel={topologyDetailEdge.id}
-                data-topology-edge-evidence-window-owner="hertzbeat-ui-detail-drawer"
-                data-topology-edge-evidence-first-seen={topologyDetailEdge.evidence.firstSeen || 'none'}
-                data-topology-edge-evidence-last-seen={topologyDetailEdge.evidence.lastSeen || 'none'}
-                data-topology-edge-evidence-sample-trace-id={topologyDetailEdge.evidence.sampleTraceId || 'none'}
-                data-topology-edge-evidence-sample-span-id={topologyDetailEdge.evidence.sampleSpanId || 'none'}
-                data-topology-detail-drawer-owner="hertzbeat-ui-detail-drawer"
-                data-topology-detail-drawer-surface-owner="hertzbeat-ui-detail-surface"
-                kind="edge"
-                density="graph-first"
-                subjectId={topologyDetailEdge.id}
-                sourceId={topologyDetailEdge.from}
-                targetId={topologyDetailEdge.to}
-                relationType={topologyDetailEdge.relationshipType}
-                sourceKind={topologyDetailEdge.source}
-                surface="framed"
-                eyebrow={t('topology.edge.evidence.title')}
-                title={topologyDetailEdge.evidence.title}
-                subtitle={`${topologyDetailEdge.evidence.sourceLabel} · ${topologyDetailEdge.evidence.collectedBy}`}
-                boundary={topologyDetailEdge.evidence.boundary}
-                boundaryProps={{
-                  'data-topology-edge-evidence-boundary': 'roadmap-boundary',
-                  'data-topology-detail-boundary-owner': 'hertzbeat-ui-detail-boundary'
-                } as React.HTMLAttributes<HTMLDivElement>}
-                facts={buildTopologyEdgeDetailFacts(topologyDetailEdge, t)}
-                actions={buildTopologyEdgeDetailActions(topologyDetailEdge, t)}
-                signalActions={buildTopologyEdgeSignalActions(topologyDetailEdge, t)}
-              />
-            </HzTopologyCompanionSection>
-          ) : null}
-
-          {primaryNode ? (
-            <HzTopologyCompanionSection
-              data-topology-companion-section="current-node"
-              data-topology-companion-section-collapsible="current-node"
-              sectionId="current-node"
-              anchorId="topology-companion-current-node"
-              density="graph-first"
-              collapsible
-              collapsed={topologyCompanionCollapsedSections['current-node'] ?? false}
-              collapseLabel={t('topology.companion.section.collapse')}
-              expandLabel={t('topology.companion.section.expand')}
-              onCollapsedChange={collapsed => handleTopologyCompanionCollapsedChange('current-node', collapsed)}
-            >
-              <HzTopologyDetailDrawer
-                data-topology-current-entity-panel={primaryNode.id}
-                data-topology-current-entity-panel-owner="hertzbeat-ui-detail-drawer"
-                data-topology-current-entity-panel-surface-owner="hertzbeat-ui-detail-surface"
-                data-topology-current-entity-neighbor-owner="hertzbeat-ui-detail-neighbor-evidence"
-                data-topology-current-entity-upstream-count={currentNodeNeighborEvidence?.upstreamNodeIds.length ?? 0}
-                data-topology-current-entity-downstream-count={currentNodeNeighborEvidence?.downstreamNodeIds.length ?? 0}
-                data-topology-current-entity-upstream-node-ids={currentNodeNeighborEvidence?.upstreamNodeIds.join(' ') || 'none'}
-                data-topology-current-entity-downstream-node-ids={currentNodeNeighborEvidence?.downstreamNodeIds.join(' ') || 'none'}
-                data-topology-current-entity-group-by={map.filterContext.groupBy}
-                data-topology-current-entity-source-kind={map.filterContext.sourceKind ?? 'all'}
-                data-topology-current-entity-view-mode={map.filterContext.viewMode}
-                kind="node"
-                density="graph-first"
-                subjectId={primaryNode.id}
-                entityType={primaryNode.entityType}
-                sourceKind={primaryNode.source}
-                surface="framed"
-                eyebrow={t('topology.current-entity')}
-                title={primaryNode.label}
-                subtitle={`${primaryNode.entityId} · ${primaryNode.namespace} / ${primaryNode.environment}`}
-                boundary={t('topology.current-entity.boundary')}
-                boundaryProps={{
-                  'data-topology-current-entity-boundary-owner': 'hertzbeat-ui-detail-boundary'
-                } as React.HTMLAttributes<HTMLDivElement>}
-                facts={buildTopologyNodeDetailFacts(
-                  primaryNode,
-                  currentNodeNeighborEvidence ?? buildTopologyNodeNeighborEvidence(primaryNode, map.nodes, map.edges),
-                  t
-                )}
-                actions={buildTopologyNodeDetailActions(primaryNode, map.filterContext, map.apiDepth, t)}
-                signalActions={buildTopologyNodeSignalActions(primaryNode, map.filterContext, map.apiDepth, t)}
-                signalActionsLabel={t('topology.context-link.signals')}
-              />
-            </HzTopologyCompanionSection>
-          ) : null}
         </HzTopologyCompanionRail>
-        </HzTopologyWorkbenchSlot>
+          </HzTopologyWorkbenchSlot>
+        ) : null}
       </HzTopologyWorkbenchGrid>
 
       {topologyShouldShowRenderWindowMetricTable && topologyMetricRows.length > 0 ? (
@@ -1912,6 +2310,7 @@ export default function TopologyPage({
           data-topology-metric-table-placement="graph-bottom"
           data-topology-metric-table-visibility="render-window-companion"
           data-topology-metric-table-scope="edge-red-render-window"
+          data-topology-metric-table-dom-policy="single-interactive-table"
           data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
           data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
           data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
@@ -1920,6 +2319,7 @@ export default function TopologyPage({
           density="graph-first"
           rows={topologyMetricRows}
           selectedRowId={topologyMetricSelectedRowId}
+          selectionSource={topologyLocalSelection.source}
           renderWindowFilter={topologyMetricWindowFilter}
           onRenderWindowFilterChange={setTopologyMetricWindowFilter}
           renderWindowCompanion={topologyMetricRenderWindowCompanion}
@@ -1930,7 +2330,7 @@ export default function TopologyPage({
         />
       ) : null}
 
-      {map.faultContextRows.length > 0 ? (
+      {topologyHasRenderableGraph && map.faultContextRows.length > 0 ? (
         <HzTopologyEvidenceList
           data-topology-fault-context="incoming-evidence"
           data-topology-fault-context-owner="hertzbeat-ui-evidence-list"
@@ -1950,30 +2350,33 @@ export default function TopologyPage({
         />
       ) : null}
 
-      <HzTopologyGroupPanel
-        data-topology-group-panel="large-graph-grouping"
-        data-topology-group-panel-owner="hertzbeat-ui-group-panel"
-        data-topology-group-panel-boundary-owner="hertzbeat-ui-group-panel-boundary"
-        title={t('topology.group-panel.title')}
-        copy={t('topology.group-panel.copy')}
-        groupByLabel={t('topology.group-panel.group-by')}
-        boundary="framed"
-        items={topologyGroupPanelItems}
-        actions={[
-          { id: 'clear-group', href: '/topology', label: t('topology.group-panel.clear-group') },
-          { id: 'open-table', href: '#topology-metric-table', label: t('topology.group-panel.open-table') }
-        ]}
-      />
+      {topologyHasRenderableGraph ? (
+        <HzTopologyGroupPanel
+          data-topology-group-panel="large-graph-grouping"
+          data-topology-group-panel-owner="hertzbeat-ui-group-panel"
+          data-topology-group-panel-boundary-owner="hertzbeat-ui-group-panel-boundary"
+          title={t('topology.group-panel.title')}
+          copy={t('topology.group-panel.copy')}
+          groupByLabel={t('topology.group-panel.group-by')}
+          boundary="framed"
+          items={topologyGroupPanelItems}
+          actions={[
+            { id: 'clear-group', href: '/topology', label: t('topology.group-panel.clear-group') },
+            { id: 'open-table', href: '#topology-metric-table', label: t('topology.group-panel.open-table') }
+          ]}
+        />
+      ) : null}
 
       {topologyInvestigationEdge && topologyHoverFromNode && topologyHoverToNode ? (
         <HzTopologyPathSummary
           data-topology-path-summary="selected-edge-context"
           data-topology-path-summary-owner="hertzbeat-ui-path-summary"
           data-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"
-          title={t('topology.path-summary.title')}
+          title={topologyPathSummaryTitle}
           boundary="section"
-          selectedEdgeId={topologyInvestigationEdge.id}
-          hoveredEdgeId={topologyInvestigationEdge.id}
+          interactionState={topologyPathSummaryInteractionState}
+          selectedEdgeId={topologyCanvasSelectedEdgeId}
+          hoveredEdgeId={topologyG6HoveredEdgeId}
           sourceId={topologyHoverFromNode.id}
           targetId={topologyHoverToNode.id}
           relationType={topologyInvestigationEdge.relationshipType}
diff --git a/web-next/app/topology/topology-route-client.tsx b/web-next/app/topology/topology-route-client.tsx
new file mode 100644
index 0000000000..3ce7aa581a
--- /dev/null
+++ b/web-next/app/topology/topology-route-client.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import React from 'react';
+import dynamic from 'next/dynamic';
+import { preloadHzTopologyG6Runtime } from '@hertzbeat/ui/topology-g6-runtime';
+import TopologyRouteShell from './topology-route-shell';
+import { preloadTopologyGraph, resolveTopologyApiTimeoutMs } from '../../lib/topology-surface/controller';
+import type { TopologyRouteContext } from '../../lib/topology-surface/view-model';
+
+void preloadHzTopologyG6Runtime();
+
+const TopologyPage = dynamic(() => import('./topology-page'), {
+  ssr: false,
+  loading: () => <TopologyRouteShell />
+});
+
+export default function TopologyRouteClient({
+  routeContext,
+  shellElementId
+}: {
+  routeContext?: TopologyRouteContext;
+  shellElementId?: string;
+}) {
+  const routeContextKey = React.useMemo(() => JSON.stringify(routeContext ?? {}), [routeContext]);
+  const topologyScaleProof = routeContext?.scaleProof?.trim();
+
+  React.useEffect(() => {
+    if (routeContext?.scaleProof?.trim()) return;
+    void preloadTopologyGraph(routeContext ?? {}, { timeoutMs: resolveTopologyApiTimeoutMs(routeContext) });
+  }, [routeContext, routeContextKey]);
+
+  React.useEffect(() => {
+    if (!shellElementId) return;
+    document.getElementById(shellElementId)?.setAttribute('hidden', '');
+  }, [shellElementId]);
+
+  return (
+    <>
+      <span hidden data-topology-route-client-prefetch="topology-api-before-heavy-page" />
+      {topologyScaleProof ? <span hidden data-topology-route-client-scale-proof-prefetch="skipped" /> : null}
+      <TopologyPage routeContext={routeContext} />
+    </>
+  );
+}
diff --git a/web-next/app/topology/topology-route-shell.tsx b/web-next/app/topology/topology-route-shell.tsx
new file mode 100644
index 0000000000..d7690da3cd
--- /dev/null
+++ b/web-next/app/topology/topology-route-shell.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+export default function TopologyRouteShell() {
+  return (
+    <main
+      className="min-h-[calc(100vh-56px)] bg-[#08090c] px-4 py-6 text-[#f1f3f7]"
+      data-topology-route-shell="deferred-client-entry"
+      data-topology-route-shell-owner="next-dynamic-client-entry"
+    >
+      <section className="mx-auto flex max-w-[1440px] flex-col gap-3">
+        <div className="flex items-center justify-between gap-3">
+          <div>
+            <div className="h-3 w-14 rounded-[2px] bg-[#1b2230]" />
+            <div className="mt-3 h-6 w-28 rounded-[3px] bg-[#202838]" />
+          </div>
+          <div className="flex gap-2">
+            <div className="h-8 w-14 rounded-[3px] bg-[#121722]" />
+            <div className="h-8 w-20 rounded-[3px] bg-[#121722]" />
+          </div>
+        </div>
+        <div className="h-10 rounded-[3px] border border-[#1e2634] bg-[#0c1119]" />
+        <div className="h-[640px] rounded-[3px] border border-[#1e2634] bg-[#07090d]" />
+      </section>
+    </main>
+  );
+}
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index fe58142a55..876a2a4d2d 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2167,8 +2167,12 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
     expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
     expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
+    expect(html).toContain('data-hz-topology-toolbar-control-flow="single-grid-row"');
+    expect(html).toContain('data-hz-topology-toolbar-empty-offset="none"');
+    expect(html).toContain('[grid-template-columns:112px_minmax(260px,1fr)_148px_88px_132px_auto]');
     expect(html).toContain('data-hz-topology-toolbar-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-display="contents"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
     expect(html).toContain('data-hz-topology-toolbar-secondary-visibility="assistive"');
     expect(html).toContain('data-hz-topology-toolbar-boundary="none"');
@@ -2282,6 +2286,10 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-path-summary-boundary="section"');
     expect(html).toContain('data-hz-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"');
     expect(html).toContain('data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"');
+    expect(html).toContain('data-topology-path-summary-interaction-state="selected"');
+    expect(html).toContain('data-hz-topology-path-summary-interaction-state="selected"');
+    expect(html).toContain('data-topology-path-summary-selected-edge-id="checkout-orders"');
+    expect(html).toContain('data-topology-path-summary-hovered-edge-id="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-selected-edge="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-hovered-edge="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-source-id="svc-checkout"');
@@ -2372,6 +2380,13 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-node-icon-source="lucide-entity-type-catalog"');
     expect(html).toContain('data-hz-topology-g6-node-icon-library="lucide-react"');
     expect(html).toContain('data-hz-topology-g6-node-icon-no-handdrawn="true"');
+    expect(html).toContain('data-hz-topology-node-icon-kind="endpoint"');
+    expect(html).toContain('data-hz-topology-node-icon-name="route"');
+    expect(html).toContain('data-hz-topology-g6-no-handdrawn-icons="true"');
+    expect(html).toContain('data-hz-topology-g6-icon-source-proof="lucide-react:entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-node-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-node-icon-source="entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-node-icon-no-handdrawn="true"');
     expect(html).toContain('data-hz-topology-g6-engine="antv-g6"');
     expect(html).toContain('data-hz-topology-g6-interaction-owner="hertzbeat-ui-g6-interaction"');
     expect(html).toContain('data-hz-topology-g6-auto-fit="initial-only"');
@@ -2385,6 +2400,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-command-event="hz-topology-g6-viewport-command"');
     expect(html).toContain('data-hz-topology-g6-command-request="idle"');
     expect(html).toContain('data-hz-topology-g6-command-request-owner="hertzbeat-ui-g6-command-request"');
+    expect(html).toContain('data-hz-topology-g6-pan-selection-guard="drag-pan-suppresses-click-selection"');
+    expect(html).toContain('data-hz-topology-g6-pan-selection-guard-owner="hertzbeat-ui-g6-pan-selection-guard"');
     expect(html).toContain('data-hz-topology-g6-wheel-mode="manual-clamped-g6-zoom"');
     expect(html).toContain('data-hz-topology-g6-wheel-owner="hertzbeat-ui-g6-wheel"');
     expect(html).toContain('data-hz-topology-g6-wheel-listener-passive="false-control"');
@@ -2397,8 +2414,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-minimap="viewport-overview"');
     expect(html).toContain('data-hz-topology-g6-minimap-owner="hertzbeat-ui-g6-minimap"');
     expect(html).toContain('data-hz-topology-g6-minimap-visibility="visible"');
-    expect(html).toContain('data-hz-topology-g6-minimap-node-count="7"');
-    expect(html).toContain('data-hz-topology-g6-minimap-edge-count="6"');
+    expect(html).toContain('data-hz-topology-g6-minimap-node-count="8"');
+    expect(html).toContain('data-hz-topology-g6-minimap-edge-count="7"');
     expect(html).toContain('data-hz-topology-g6-minimap-viewport-source="initial"');
     expect(html).toContain('data-hz-topology-g6-node-selection="drawer-sync"');
     expect(html).toContain('data-hz-topology-g6-edge-selection="drawer-sync"');
@@ -2429,8 +2446,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-neighbor-role="upstream"');
     expect(html).toContain('data-hz-topology-g6-neighbor-role="downstream"');
     expect(html).toContain('data-hz-topology-g6-neighbor-role="outgoing"');
-    expect(html).toContain('data-hz-topology-g6-node-count="7"');
-    expect(html).toContain('data-hz-topology-g6-edge-count="6"');
+    expect(html).toContain('data-hz-topology-g6-node-count="8"');
+    expect(html).toContain('data-hz-topology-g6-edge-count="7"');
     expect(html).toContain('data-hz-topology-g6-action="fit-view"');
     expect(html).toContain('data-hz-topology-g6-filter-owner="hertzbeat-ui-g6-filter"');
     expect(html).toContain('data-hz-topology-g6-filter-environment="prod"');
@@ -2459,6 +2476,9 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-group-surface-visibility="assistive"');
     expect(html).toContain('data-hz-topology-g6-filter-control-surface-visibility="assistive"');
     expect(html).toContain('data-hz-topology-g6-toolbar-visibility="visible"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-node-only-relation-state="shared"');
+    expect(html).toContain('data-hz-topology-g6-relation-state="entities-without-relation-edges"');
+    expect(html).toContain('data-hz-topology-g6-node-only-explanation-owner="hertzbeat-ui-g6-node-only-explanation"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-scale-contract="50-200-500"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-scale-canvas="500-node-windowed-shared"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-scale-50="50"');
@@ -2468,6 +2488,11 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-strategy="grouped-table-companion"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-filtering="required"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-500-table-companion="required"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-scale-overflow="650"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-overflow-strategy="filter-first"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-large-graph-overflow-policy="filter-first-before-expanded-render"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-overflow-rendered="200"');
+    expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-overflow-hidden="450"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-render-window="500-node-windowed-shared"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-mode="windowed"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-render-window-rendered="200"');
@@ -2477,12 +2502,72 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-render-window-mode="windowed"');
     expect(html).toContain('data-hz-topology-g6-render-window-visible-node-budget="200"');
     expect(html).toContain('data-hz-topology-g6-render-window-rendered-node-count="200"');
+    expect(html).toContain('data-hz-topology-g6-render-window-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-g6-render-window-candidate-edge-count=');
+    expect(html).toContain(
+      'data-hz-topology-g6-render-window-rendered-edge-count-owner="hertzbeat-ui-g6-edge-density"'
+    );
     expect(html).toContain('data-hz-topology-g6-render-window-hidden-node-count="300"');
     expect(html).toContain('data-hz-topology-g6-render-window-table-companion="required"');
+    expect(html).toContain('data-hz-topology-g6-summary-count-policy="windowed-rendered-vs-total"');
+    expect(html).toContain('data-hz-topology-g6-summary-rendered-node-count="200"');
+    expect(html).toContain('data-hz-topology-g6-summary-total-node-count="500"');
+    expect(html).toContain('data-hz-topology-g6-summary-rendered-edge-count="180"');
+    expect(html).toContain('200/500 nodes rendered · 180/');
+    expect(html).not.toContain('>500 nodes ·');
+    expect(html).toContain('data-hz-topology-g6-edge-label-owner="hertzbeat-ui-g6-edge-label"');
+    expect(html).toContain('data-hz-topology-g6-edge-label-policy="hidden-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-edge-label-visible-count="0"');
+    expect(html).toContain('data-hz-topology-g6-node-label-owner="hertzbeat-ui-g6-node-label"');
+    expect(html).toContain('data-hz-topology-g6-node-label-policy="hub-only-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-node-label-metadata-policy="preserve-data-label"');
+    expect(html).toContain('data-hz-topology-g6-shape-profile-owner="hertzbeat-ui-g6-shape-profile"');
+    expect(html).toContain('data-hz-topology-g6-shape-profile-evidence="degree-derived"');
+    expect(html).toContain('data-hz-topology-g6-shape-profile=');
+    expect(html).toContain('data-hz-topology-g6-edge-density-owner="hertzbeat-ui-g6-edge-density"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-policy="reduced-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-max-visible="180"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-rendered-edge-count="180"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-owner="hertzbeat-ui-g6-edge-readability"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-policy="attenuated-large-graph"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-ranking-policy="red-priority-stable-render-order"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-stability="selection-hover-invariant"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-max-prominent-edge-count="72"');
+    expect(html).toContain('data-hz-topology-g6-edge-readability-minimum-opacity="0.2"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-owner="hertzbeat-ui-g6-edge-density-affordance"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-action="scroll-edge-table"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-target="topology-metric-table"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-live-owner="hertzbeat-ui-g6-edge-density-affordance"');
+    expect(html).toContain('data-hz-topology-g6-edge-density-affordance-invariants="table-scroll no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-hz-topology-g6-semantic-cluster-owner="hertzbeat-ui-g6-semantic-cluster"');
+    expect(html).toContain('data-hz-topology-g6-semantic-cluster-policy="hub-fanout-summary"');
+    expect(html).toContain('data-hz-topology-g6-semantic-cluster-item-count="4"');
+    expect(html).toContain('data-hz-topology-g6-semantic-cluster-item-role="hub"');
+    expect(html).toContain('data-hz-topology-g6-windowed-layout-owner="hertzbeat-ui-g6-windowed-layout"');
+    expect(html).toContain('data-hz-topology-g6-windowed-layout-policy="packed-lanes"');
+    expect(html).toContain('data-hz-topology-g6-windowed-layout-max-lane-rows="18"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-owner="hertzbeat-ui-g6-scale-performance"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-policy="windowed-interactive-budget"');
+    expect(html).toContain(
+      'data-hz-topology-g6-scale-performance-invariants="windowed-render pan-zoom-no-url-change no-remount no-refit viewport-preserved render-key-stable table-companion"'
+    );
+    expect(html).toContain('data-hz-topology-g6-scale-performance-rendered-node-budget="200"');
+    expect(html).toContain('data-hz-topology-g6-scale-performance-rendered-edge-budget="180"');
     expect(html).toContain('data-hz-topology-g6-mount-lifecycle-owner="hertzbeat-ui-g6-mount-lifecycle"');
     expect(html).toContain('data-hz-topology-g6-mount-lifecycle-policy="structure-layout-height-only"');
     expect(html).toContain('data-hz-topology-g6-mount-lifecycle-redraw-policy="setData-draw-preserve-viewport"');
     expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key=');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-owner="hertzbeat-ui-g6-structure-key-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-policy="short-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-structure-key-length=');
+    expect(html).not.toContain('scale-svc-000:service|');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-owner="hertzbeat-ui-g6-render-key-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-policy="short-fingerprint"');
+    expect(html).toContain('data-hz-topology-g6-mount-lifecycle-render-key-length=');
+    expect(html).not.toContain('scale-svc-000::#');
+    expect(html).toContain('data-hz-topology-g6-viewport-fit-state="pending"');
+    expect(html).toContain('data-hz-topology-g6-viewport-fit-state-owner="hertzbeat-ui-g6-fit-settle-state"');
     expect(html).toContain('data-hz-topology-g6-render-window-priority-node-count="1"');
     expect(html).toContain('data-hz-topology-g6-render-window-priority-node-ids="scale-svc-420"');
     expect(source).toContain('const [topologyG6ScaleLabSelection, setTopologyG6ScaleLabSelection]');
@@ -2503,7 +2588,20 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain(
       'data-hz-ui-lab-topology-g6-scale-browser-regression-invariants="no-url-change no-remount viewport-preserved table-companion"'
     );
+    expect(html).toContain('data-hz-topology-g6-live-interaction-owner="hertzbeat-ui-g6-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-live-interaction-invariants="no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-hz-topology-g6-edge-live-interaction-owner="hertzbeat-ui-g6-edge-live-interaction"');
+    expect(html).toContain('data-hz-topology-g6-edge-live-interaction-invariants="edge-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"');
+    expect(html).toContain('data-hz-topology-g6-selected-node-rendered=');
+    expect(html).toContain('data-hz-topology-g6-selected-edge-rendered=');
+    expect(html).toContain('data-hz-ui-lab-static-confirm-preview="non-blocking-inline"');
+    expect(source).toContain('uiLabStaticConfirmDialogPreviewClassName');
+    expect(source).toContain('data-hz-ui-lab-static-confirm-preview="non-blocking-inline"');
+    expect(source).not.toContain('<HzConfirmDialog\n                open\n                tone="critical"');
+    expect(source).not.toContain('<HzConfirmDialog\n                    open\n                    tone="critical"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
+    expect(html).toContain('data-hz-topology-metric-table-root="true"');
+    expect(html).toContain('data-hz-topology-metric-table-total-rows="3"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="500"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-node-count="200"');
@@ -2513,6 +2611,10 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-metric-table-visible-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-partial-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-row-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-owner="hertzbeat-ui-metric-table-hidden-row-proof"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof="available"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-filter="hidden"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-unknown-row-count="0"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter="all"');
@@ -2530,6 +2632,9 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-g6-search-query="Service 420"');
     expect(html).toContain('data-hz-topology-g6-search-match-count="1"');
     expect(html).toContain('data-hz-topology-g6-search-first-match="scale-svc-420"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-action-count="4"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-visible-actions="zoom-out zoom-in fit-view reset-view"');
+    expect(html).toContain('data-hz-topology-g6-toolbar-focus-actions-visibility="assistive"');
     expect(html).toContain('data-hz-topology-g6-action="focus-search-result"');
     expect(html).toContain('data-hz-topology-g6-selected-focus-owner="hertzbeat-ui-g6-selected-focus"');
     expect(html).toContain('data-hz-topology-g6-selected-focus-target="scale-svc-420"');
@@ -2633,6 +2738,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
     expect(html).toContain('data-hz-ui-lab-topology-g6-legend-dock="shared"');
     expect(html).toContain('data-hz-topology-g6-legend-dock="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-state="in-canvas"');
+    expect(html).toContain('data-hz-topology-g6-legend-dock-container="true"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-owner="hertzbeat-ui-g6-legend-dock"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-placement="canvas-bottom-left"');
     expect(html).toContain('data-hz-topology-g6-legend-dock-occlusion="inside-canvas-low-interruption"');
@@ -2696,41 +2803,41 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-legend-boundary="flush"');
     expect(html).toContain('data-hz-topology-legend-boundary-owner="hertzbeat-ui-legend-boundary"');
 	    expect(html).toContain('data-hz-topology-legend-header-owner="hertzbeat-ui-legend-header"');
-	    expect(html).toContain('data-hz-topology-legend-section="status"');
-	    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
-	    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
-	    expect(html).toContain('data-hz-topology-legend-section="node-type"');
-	    expect(html).toContain('data-hz-topology-legend-section="interaction"');
-	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-cache"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-queue"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-monitor"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-middleware"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-application"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
-	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
-	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="memory-stick"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="inbox"');
-	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
-	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
-	    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
-	    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
-	    expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
-	    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
-	    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
+    expect(html).not.toContain('data-hz-topology-legend-section="status"');
+    expect(html).toContain('data-hz-topology-legend-section-owner="hertzbeat-ui-legend-section"');
+    expect(html).toContain('data-hz-topology-legend-section-label-owner="hertzbeat-ui-legend-section-label"');
+    expect(html).toContain('data-hz-topology-legend-section="node-type"');
+    expect(html).not.toContain('data-hz-topology-legend-section="interaction"');
+    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-cache"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-queue"');
+    expect(html).toContain('data-hz-topology-legend-item="node-type-monitor"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-middleware"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-application"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
+    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
+    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
+    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="server-cog"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="route"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="memory-stick"');
+    expect(html).toContain('data-hz-topology-legend-icon-name="inbox"');
+    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
+    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
+    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
+    expect(html).not.toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+    expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
+    expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
+    expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
     expect(html).not.toContain('data-hz-topology-legend-pattern="dashed"');
     expect(html).toContain('data-hz-ui-lab-topology-detail-drawer="shared"');
     expect(html).toContain('data-hz-ui="topology-detail-drawer"');
@@ -2747,6 +2854,16 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-key="edge:api-warehouse:svc-api:db-warehouse:trace-call:otlp-trace-call:unknown"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement="header-dock"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement-owner="hertzbeat-ui-detail-signal-action-placement"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-sticky="top-with-header-context"');
     expect(html).toContain('data-hz-topology-detail-header-owner="hertzbeat-ui-detail-header"');
     expect(html).toContain('data-hz-topology-detail-eyebrow-owner="hertzbeat-ui-detail-eyebrow"');
     expect(html).toContain('data-hz-topology-detail-title-owner="hertzbeat-ui-detail-title"');
@@ -2782,6 +2899,13 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-key="node:svc-checkout:none:none:unknown:otlp-trace-call:service"');
     expect(html).toContain('data-hz-topology-detail-header-owner="hertzbeat-ui-detail-header"');
     expect(html).toContain('data-hz-topology-detail-eyebrow-owner="hertzbeat-ui-detail-eyebrow"');
     expect(html).toContain('data-hz-topology-detail-title-owner="hertzbeat-ui-detail-title"');
@@ -2834,9 +2958,20 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-metric-table-header-owner="hertzbeat-ui-metric-table-header"');
     expect(html).toContain('data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"');
     expect(html).toContain('data-hz-topology-metric-table-interaction="row-select-detail"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-selection-policy="preserve-selected-edge"');
+    expect(html).toContain('data-hz-topology-metric-table-live-selection-owner="hertzbeat-ui-metric-table-selection"');
+    expect(html).toContain(
+      'data-hz-topology-metric-table-live-selection-invariants="row-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"'
+    );
     expect(html).toContain('data-hz-ui-lab-topology-metric-table-scale-selection="edge-sync"');
     expect(html).toContain('data-hz-ui-lab-topology-metric-table-scale-selection-owner="hertzbeat-ui-metric-table-scale-selection"');
     expect(html).toContain('data-hz-topology-edge-row="scale-edge-420-421"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-owner="hertzbeat-ui-metric-table-row-selection"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-mode="table-row-click-drawer"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-edge-selected="true"');
     expect(html).toContain('data-hz-topology-metric-table-route-owner="hertzbeat-ui-metric-table-route"');
     expect(html).toContain('data-hz-topology-metric-table-badge-owner="hertzbeat-ui-metric-table-badge"');
@@ -2852,7 +2987,8 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-ui-lab-topology-empty-state="shared"');
     expect(html).toContain('data-hz-ui="topology-empty-state"');
     expect(html).toContain('data-hz-topology-primitive="empty-state"');
-    expect(html).toContain('data-hz-topology-empty-boundary="flush"');
+    expect(html).toContain('data-hz-topology-empty-boundary="canvas"');
+    expect(html).toContain('data-hz-topology-empty-boundary-visual="frameless-canvas"');
     expect(html).toContain('data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"');
     expect(html).toContain('data-hz-topology-empty-scope-owner="hertzbeat-ui-empty-scope"');
     expect(html).toContain('data-hz-topology-empty-environment="prod"');
@@ -2862,6 +2998,7 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-empty-depth="2"');
     expect(html).toContain('data-hz-topology-empty-result-count="0"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="trace relation monitor incident"');
+    expect(html).toContain('data-hz-topology-empty-copy-visibility="assistive"');
     expect(html).toContain('data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title"');
     expect(html).toContain('data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"');
     expect(html).toContain('data-hz-topology-empty-meta-owner="hertzbeat-ui-empty-meta"');
@@ -2872,6 +3009,7 @@ describe('HertzBeat UI lab page', () => {
     expect(html).toContain('data-hz-topology-empty-kind="degraded"');
     expect(html).toContain('data-hz-topology-empty-source="Topology API unavailable"');
     expect(html).toContain('data-hz-topology-empty-evidence-sources="api greptime trace relation"');
+    expect(html).toContain('data-hz-topology-empty-copy-visibility="assistive"');
     expect(html).toContain('Topology API unavailable');
     expect(html).toContain('data-hz-ui-lab-topology-loading-state="shared"');
     expect(html).toContain('data-hz-ui="topology-loading-state"');
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
index 4bf30d9a9c..fdea1c2085 100644
--- a/web-next/app/ui-lab/page.tsx
+++ b/web-next/app/ui-lab/page.tsx
@@ -204,6 +204,7 @@ import {
 } from '@hertzbeat/ui';
 import {
   HzTopologyG6Canvas,
+  HZ_TOPOLOGY_G6_EDGE_DENSITY_VISIBLE_EDGE_LIMIT,
   HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6RenderWindow,
@@ -358,6 +359,17 @@ const topologyG6LabGraph: HzTopologyG6GraphInput = {
       evidenceBadges: ['trace'],
       redMetrics: { requestRatePerSecond: 7.1, errorRate: 0.012, latencyP95Ms: 92 }
     },
+    {
+      id: 'endpoint-checkout',
+      label: '/api/checkout',
+      entityType: 'endpoint',
+      health: 'healthy',
+      tone: 'blue',
+      focus: 'normal',
+      source: 'otlp-trace-call',
+      evidenceBadges: ['endpoint'],
+      redMetrics: { requestRatePerSecond: 10.4, errorRate: 0.006, latencyP95Ms: 38 }
+    },
     {
       id: 'queue-events',
       label: 'Events Queue',
@@ -404,6 +416,18 @@ const topologyG6LabGraph: HzTopologyG6GraphInput = {
     }
   ],
   edges: [
+    {
+      id: 'web-endpoint',
+      from: 'svc-web',
+      to: 'endpoint-checkout',
+      label: 'HTTP route',
+      relationshipType: 'trace-call',
+      source: 'otlp-trace-call',
+      tone: 'blue',
+      focus: 'context-muted',
+      evidenceBadges: ['trace'],
+      redMetrics: { requestRatePerSecond: 10.4, errorRate: 0.006, latencyP95Ms: 38 }
+    },
     {
       id: 'web-checkout',
       from: 'svc-web',
@@ -478,6 +502,39 @@ const topologyG6LabGraph: HzTopologyG6GraphInput = {
   ]
 };
 
+const topologyG6NodeOnlyLabGraph: HzTopologyG6GraphInput = {
+  nodes: [
+    {
+      id: 'node-only-payment',
+      label: 'Payment API',
+      entityType: 'service',
+      health: 'warning',
+      tone: 'warning',
+      source: 'cmdb-manual-label',
+      evidenceBadges: ['cmdb']
+    },
+    {
+      id: 'node-only-checkout',
+      label: 'Checkout API',
+      entityType: 'service',
+      health: 'healthy',
+      tone: 'success',
+      source: 'cmdb-manual-label',
+      evidenceBadges: ['cmdb']
+    },
+    {
+      id: 'node-only-orders',
+      label: 'Orders DB',
+      entityType: 'database',
+      health: 'healthy',
+      tone: 'blue',
+      source: 'cmdb-manual-label',
+      evidenceBadges: ['cmdb']
+    }
+  ],
+  edges: []
+};
+
 const topologyG6ScaleLabGraph = buildHzTopologyG6ScaleFixture(50, {
   environment: 'prod',
   namespace: 'commerce',
@@ -488,14 +545,30 @@ const topologyG6ScaleLabGraph500 = buildHzTopologyG6ScaleFixture(500, {
   namespace: 'commerce',
   prefix: 'scale'
 });
+const topologyG6ScaleLabGraphOverflow = buildHzTopologyG6ScaleFixture(650, {
+  environment: 'prod',
+  namespace: 'commerce',
+  prefix: 'scale'
+});
 const topologyG6ScaleLabProfile50 = buildHzTopologyG6ScaleProfile(topologyG6ScaleLabGraph);
 const topologyG6ScaleLabProfile200 = buildHzTopologyG6ScaleProfile(buildHzTopologyG6ScaleFixture(200));
 const topologyG6ScaleLabProfile500 = buildHzTopologyG6ScaleProfile(topologyG6ScaleLabGraph500);
+const topologyG6ScaleLabProfileOverflow = buildHzTopologyG6ScaleProfile(topologyG6ScaleLabGraphOverflow);
 const topologyG6LargeGraphStrategy200 = buildHzTopologyG6LargeGraphStrategy(buildHzTopologyG6ScaleFixture(200));
 const topologyG6LargeGraphStrategy500 = buildHzTopologyG6LargeGraphStrategy(topologyG6ScaleLabGraph500);
+const topologyG6LargeGraphStrategyOverflow = buildHzTopologyG6LargeGraphStrategy(topologyG6ScaleLabGraphOverflow);
 const topologyG6ScaleLabRenderWindow500 = buildHzTopologyG6RenderWindow(topologyG6ScaleLabGraph500, topologyG6LargeGraphStrategy500, {
   priorityNodeIds: ['scale-svc-420']
 });
+const topologyG6ScaleLabRenderedEdgeCount500 = Math.min(
+  topologyG6ScaleLabRenderWindow500.renderedEdgeCount,
+  HZ_TOPOLOGY_G6_EDGE_DENSITY_VISIBLE_EDGE_LIMIT
+);
+const topologyG6ScaleLabSummary500 = `${topologyG6ScaleLabRenderWindow500.renderedNodeCount}/${topologyG6ScaleLabRenderWindow500.totalNodeCount} nodes rendered · ${topologyG6ScaleLabRenderedEdgeCount500}/${topologyG6ScaleLabRenderWindow500.totalEdgeCount} edges rendered`;
+const topologyG6ScaleLabRenderWindowOverflow = buildHzTopologyG6RenderWindow(
+  topologyG6ScaleLabGraphOverflow,
+  topologyG6LargeGraphStrategyOverflow
+);
 const topologyG6LabVisibleNodeKinds = new Set(
   topologyG6LabGraph.nodes.map(node => getHzTopologyG6NodeIcon(node.entityType).kind).filter(kind => kind !== 'unknown')
 );
@@ -1520,6 +1593,9 @@ function findTemplate(categories: HzTemplateCategory[], id: string) {
   return categories.flatMap(category => category.items).find(item => item.id === id) || categories[0]?.items[0];
 }
 
+const uiLabStaticConfirmDialogPreviewClassName =
+  'pointer-events-none relative inset-auto z-0 min-w-[360px] bg-transparent p-0';
+
 export default function HertzBeatUiLabPage() {
   const { t } = useI18n();
   const [activeTab, setActiveTab] = React.useState('monitors');
@@ -2189,6 +2265,7 @@ export default function HertzBeatUiLabPage() {
               </HzActionGroup>
               <HzConfirmDialog
                 open
+                className={uiLabStaticConfirmDialogPreviewClassName}
                 tone="critical"
                 kicker="Monitor center"
                 title="Please confirm whether to cancel monitor!"
@@ -2206,6 +2283,7 @@ export default function HertzBeatUiLabPage() {
                 data-monitor-row-response-confirm-target="checkout-api"
                 data-monitor-row-response-confirm-closable="angular-nz-closable-false"
                 data-monitor-row-response-confirm-ok="angular-nz-ok-danger-primary"
+                data-hz-ui-lab-static-confirm-preview="non-blocking-inline"
               >
                 <div data-monitor-row-response-confirm-body="angular-single-row-confirm">
                   <HzInlineFeedback
@@ -7014,7 +7092,7 @@ export default function HertzBeatUiLabPage() {
                 <HzTopologyLegend
                   data-hz-ui-lab-topology-g6-legend-dock="shared"
                   title="Legend"
-                  summaryLabel="In canvas"
+                  summaryLabel="Node types"
                   boundary="flush"
                   density="canvas-dock"
                   sections={[
@@ -7022,24 +7100,6 @@ export default function HertzBeatUiLabPage() {
                           id: 'node-type',
                           label: 'Node type',
                           items: topologyG6NodeTypeLegendItems
-                        },
-                        {
-                          id: 'status',
-                          label: 'Status',
-                          items: [
-                            { id: 'healthy-node', label: 'Healthy', color: '#22c55e', visualSource: 'hertzbeat-status-token', value: 'healthy' },
-                            { id: 'warning-node', label: 'Warning', color: '#f59e0b', visualSource: 'hertzbeat-status-token', value: 'warning' },
-                            { id: 'critical-node', label: 'Critical', color: '#ef4444', visualSource: 'hertzbeat-status-token', value: 'critical' }
-                          ]
-                        },
-                        {
-                          id: 'interaction',
-                          label: 'Interaction',
-                          items: [
-                            { id: 'selected-node', label: 'Selected', color: '#e5edf8', visualSource: 'hertzbeat-interaction-token', value: 'selected' },
-                            { id: 'directional-edge', label: 'Edge', color: '#94a3b8', pattern: 'solid', visualSource: 'hertzbeat-edge-token', value: 'direction' },
-                            { id: 'dimmed-edge', label: 'Dimmed', color: '#94a3b8', pattern: 'muted', visualSource: 'hertzbeat-edge-token', value: 'context' }
-                          ]
                         }
                       ]}
                     />
@@ -7059,15 +7119,49 @@ export default function HertzBeatUiLabPage() {
                   data-hz-ui-lab-topology-g6-non-occluding="shared"
                 />
               </HzTopologyCanvas>
+              <HzTopologyCanvas
+                data-hz-ui-lab-topology-g6-node-only-relation-state="shared"
+                layout="layered-service"
+                interactionMode="inspect"
+                interactionScope="hover-group"
+                hoverMode="neighbor-highlight"
+                drawerMode="node-edge"
+                focusDepth="2-hop"
+                minHeight="compact"
+                boundary="section"
+              >
+                <HzTopologyCanvasAnnotation
+                  title="Node-only relation state"
+                  copy="Entities are visible while the current filter has no relation edges."
+                  visibility="assistive"
+                />
+                <HzTopologyG6Canvas
+                  graph={topologyG6NodeOnlyLabGraph}
+                  layout="layered-service"
+                  height="compact"
+                  summaryLabel="3 nodes · 0 edges"
+                  fitViewLabel="Fit view"
+                  resetViewLabel="Reset view"
+                  zoomInLabel="Zoom in"
+                  zoomOutLabel="Zoom out"
+                />
+              </HzTopologyCanvas>
               <HzTopologyCanvas
                 data-hz-ui-lab-topology-g6-scale-contract="50-200-500"
                 data-hz-ui-lab-topology-g6-scale-50={topologyG6ScaleLabProfile50.nodeCount}
                 data-hz-ui-lab-topology-g6-scale-200={topologyG6ScaleLabProfile200.nodeCount}
                 data-hz-ui-lab-topology-g6-scale-500={topologyG6ScaleLabProfile500.nodeCount}
+                data-hz-ui-lab-topology-g6-scale-overflow={topologyG6ScaleLabProfileOverflow.nodeCount}
                 data-hz-ui-lab-topology-g6-large-graph-200-strategy={topologyG6LargeGraphStrategy200.strategy}
                 data-hz-ui-lab-topology-g6-large-graph-500-strategy={topologyG6LargeGraphStrategy500.strategy}
                 data-hz-ui-lab-topology-g6-large-graph-500-filtering={topologyG6LargeGraphStrategy500.filtering}
                 data-hz-ui-lab-topology-g6-large-graph-500-table-companion={topologyG6LargeGraphStrategy500.tableCompanion}
+                data-hz-ui-lab-topology-g6-large-graph-overflow-strategy={topologyG6LargeGraphStrategyOverflow.strategy}
+                data-hz-ui-lab-topology-g6-large-graph-overflow-policy="filter-first-before-expanded-render"
+                data-hz-ui-lab-topology-g6-render-window-overflow-rendered={
+                  topologyG6ScaleLabRenderWindowOverflow.renderedNodeCount
+                }
+                data-hz-ui-lab-topology-g6-render-window-overflow-hidden={topologyG6ScaleLabRenderWindowOverflow.hiddenNodeCount}
                 data-hz-ui-lab-topology-g6-render-window="500-node-windowed-shared"
                 data-hz-ui-lab-topology-g6-render-window-mode={topologyG6ScaleLabRenderWindow500.mode}
                 data-hz-ui-lab-topology-g6-render-window-rendered={topologyG6ScaleLabRenderWindow500.renderedNodeCount}
@@ -7134,7 +7228,7 @@ export default function HertzBeatUiLabPage() {
                       href: '/topology?environment=prod'
                     }
                   ]}
-                  summaryLabel={`${topologyG6ScaleLabProfile500.nodeCount} nodes · ${topologyG6ScaleLabProfile500.edgeCount} edges`}
+                  summaryLabel={topologyG6ScaleLabSummary500}
                   fitViewLabel="Fit view"
                   resetViewLabel="Reset view"
                   zoomInLabel="Zoom in"
@@ -7285,31 +7379,13 @@ export default function HertzBeatUiLabPage() {
                   <HzTopologyLegend
                     data-hz-ui-lab-topology-legend="shared"
                     title="Topology legend"
-                    summaryLabel="3 groups"
+                    summaryLabel="Node types"
                     boundary="flush"
                     sections={[
                       {
                         id: 'node-type',
                         label: 'Node type',
                         items: topologyG6NodeTypeLegendItems
-                      },
-                      {
-                        id: 'status',
-                        label: 'Status',
-                        items: [
-                          { id: 'healthy-node', label: 'Healthy', color: '#22c55e', visualSource: 'hertzbeat-status-token', value: 'healthy' },
-                          { id: 'warning-node', label: 'Warning', color: '#f59e0b', visualSource: 'hertzbeat-status-token', value: 'warning' },
-                          { id: 'critical-node', label: 'Critical', color: '#ef4444', visualSource: 'hertzbeat-status-token', value: 'critical' }
-                        ]
-                      },
-                      {
-                        id: 'interaction',
-                        label: 'Interaction',
-                        items: [
-                          { id: 'selected-node', label: 'Selected node', color: '#e5edf8', visualSource: 'hertzbeat-interaction-token', value: 'selected' },
-                          { id: 'directional-edge', label: 'Directional edge', color: '#94a3b8', pattern: 'solid', visualSource: 'hertzbeat-edge-token', value: 'arrow direction' },
-                          { id: 'dimmed-edge', label: 'Dimmed edge', color: '#94a3b8', pattern: 'muted', visualSource: 'hertzbeat-edge-token', value: 'outside selection' }
-                        ]
                       }
                     ]}
                   />
@@ -7475,7 +7551,8 @@ export default function HertzBeatUiLabPage() {
                   depth={2}
                   resultCount={0}
                   evidenceSources={['trace', 'relation', 'monitor', 'incident']}
-                  boundary="flush"
+                  copyVisibility="assistive"
+                  boundary="canvas"
                 />
                 <HzTopologyEmptyState
                   data-hz-ui-lab-topology-degraded-state="shared"
@@ -7491,6 +7568,7 @@ export default function HertzBeatUiLabPage() {
                   resultCount={0}
                   evidenceSources={['api', 'greptime', 'trace', 'relation']}
                   kind="degraded"
+                  copyVisibility="assistive"
                   boundary="flush"
                 />
                 <HzTopologyLoadingState
@@ -10866,6 +10944,7 @@ export default function HertzBeatUiLabPage() {
                   </HzConfirmDialog>
                   <HzConfirmDialog
                     open
+                    className={uiLabStaticConfirmDialogPreviewClassName}
                     tone="critical"
                     kicker="Monitor center"
                     title="Please confirm whether to cancel monitor in batches!"
@@ -10883,6 +10962,7 @@ export default function HertzBeatUiLabPage() {
                     data-monitor-batch-response-confirm-count="2"
                     data-monitor-batch-response-confirm-closable="angular-nz-closable-false"
                     data-monitor-batch-response-confirm-ok="angular-nz-ok-danger-primary"
+                    data-hz-ui-lab-static-confirm-preview="non-blocking-inline"
                   >
                     <div data-monitor-batch-response-confirm-body="angular-batch-confirm">
                       <HzInlineFeedback
diff --git a/web-next/components/pages/entity-detail-surface.test.tsx b/web-next/components/pages/entity-detail-surface.test.tsx
index 5aa19085de..df16bf361a 100644
--- a/web-next/components/pages/entity-detail-surface.test.tsx
+++ b/web-next/components/pages/entity-detail-surface.test.tsx
@@ -257,6 +257,41 @@ describe('EntityDetailSurface', () => {
     expect(html).not.toContain('data-drawer-section');
   }, 30000);
 
+  it('uses shared signal evidence when deciding whether the entity has investigation context', () => {
+    const html = renderToStaticMarkup(
+      <EntityDetailSurface
+        detail={{
+          entity: {
+            entity: {
+              id: 77,
+              name: 'payment-api',
+              displayName: 'Payment API',
+              type: 'service',
+              status: 'healthy'
+            }
+          },
+          evidenceSummary: {
+            downMonitorCount: 0
+          },
+          alertSummary: {
+            totalActiveAlerts: 0
+          },
+          signalEvidence: {
+            logSummary: { hintCount: 2 },
+            traceSummary: { recentTraceCount: 3 }
+          }
+        } as any}
+        actionError={null}
+        isPending={false}
+        onDelete={() => undefined}
+        onRefresh={() => undefined}
+      />
+    );
+
+    expect(html).toContain('Logs, traces, or monitor evidence are linked and ready for investigation.');
+    expect(html).not.toContain('Add owner, definition, and telemetry binding so this entity becomes usable.');
+  });
+
   it('renders entity context handoff links with inherited time and monitor context', () => {
     const html = renderToStaticMarkup(
       <EntityDetailSurface
diff --git a/web-next/components/pages/entity-detail-surface.tsx b/web-next/components/pages/entity-detail-surface.tsx
index 39211566d5..83e0211dc3 100644
--- a/web-next/components/pages/entity-detail-surface.tsx
+++ b/web-next/components/pages/entity-detail-surface.tsx
@@ -93,7 +93,9 @@ function resolveTitle(detail: EntityDetailDto) {
 function resolveSubtitle(detail: EntityDetailDto) {
   const activeAlerts = detail.alertSummary?.totalActiveAlerts ?? detail.activeAlerts?.length ?? 0;
   const downMonitors = detail.evidenceSummary?.downMonitorCount ?? 0;
-  const hasEvidence = (detail.logSummary?.hintCount ?? 0) > 0 || (detail.traceSummary?.recentTraceCount ?? 0) > 0 || (detail.boundMonitors?.length ?? 0) > 0;
+  const logSummary = detail.signalEvidence?.logSummary || detail.logSummary;
+  const traceSummary = detail.signalEvidence?.traceSummary || detail.traceSummary;
+  const hasEvidence = (logSummary?.hintCount ?? 0) > 0 || (traceSummary?.recentTraceCount ?? 0) > 0 || (detail.boundMonitors?.length ?? 0) > 0;
 
   if (activeAlerts > 0) {
     return translateEntityDetail('entities.detail.subtitle.active-alerts');
diff --git a/web-next/components/shell/app-frame.chrome.test.tsx b/web-next/components/shell/app-frame.chrome.test.tsx
index 1a0d752486..bff8b617d5 100644
--- a/web-next/components/shell/app-frame.chrome.test.tsx
+++ b/web-next/components/shell/app-frame.chrome.test.tsx
@@ -55,6 +55,7 @@ vi.mock('@/lib/api-client', () => ({
 
 vi.mock('@/lib/app-frame-state', () => ({
   isStandaloneRoute: () => false,
+  shouldLoadHeaderRealtime: () => false,
   shouldLoadHeaderState: () => false
 }));
 
@@ -86,6 +87,20 @@ const emptySetupState = {
 };
 
 describe('app frame chrome', () => {
+  it('loads header shell primitives from a lightweight UI subpath instead of the full UI barrel', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+
+    expect(source).toContain("from '@hertzbeat/ui/shell'");
+    expect(source).not.toContain("from '@hertzbeat/ui'");
+  });
+
+  it('keeps locale menu chrome on the lightweight shell UI subpath', () => {
+    const source = readFileSync(resolve(__dirname, 'locale-option-list.tsx'), 'utf8');
+
+    expect(source).toContain("from '@hertzbeat/ui/shell'");
+    expect(source).not.toContain("from '@hertzbeat/ui'");
+  });
+
   it('does not keep the startup baseline once live header setup state resolves empty', async () => {
     const { buildSetupSummary } = await import('./app-frame');
     const summary = buildSetupSummary(emptySetupState, setupT);
diff --git a/web-next/components/shell/app-frame.tsx b/web-next/components/shell/app-frame.tsx
index 6affb5ebb3..de372105e3 100644
--- a/web-next/components/shell/app-frame.tsx
+++ b/web-next/components/shell/app-frame.tsx
@@ -38,7 +38,7 @@ import {
   type HzAiChatScheduleDeleteStatus,
   type HzAiChatScheduleRow,
   type HzAiChatScheduleStatus
-} from '@hertzbeat/ui';
+} from '@hertzbeat/ui/shell';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { LocaleOptionList } from '@/components/shell/locale-option-list';
 import { AuthGate } from '@/components/shell/auth-gate';
@@ -46,7 +46,7 @@ import { AppSidebar } from '@/components/shell/app-sidebar';
 import { PlatformCopyrightFooter } from './platform-copyright-footer';
 import { Input } from '../ui/input';
 import { apiGet } from '@/lib/api-client';
-import { isStandaloneRoute, shouldLoadHeaderState } from '@/lib/app-frame-state';
+import { isStandaloneRoute, shouldLoadHeaderRealtime, shouldLoadHeaderState } from '@/lib/app-frame-state';
 import { buildAlertListUrl } from '@/lib/alert-manage/query-state';
 import {
   AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS,
@@ -1132,7 +1132,7 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
   }, [locale, pathname, standaloneRoute, t]);
 
   useEffect(() => {
-    if (!shouldLoadHeaderState(pathname)) {
+    if (!shouldLoadHeaderRealtime(pathname)) {
       setHeaderRealtimeStatus('idle');
       setHeaderRealtimeNotice(null);
       return undefined;
diff --git a/web-next/components/shell/auth-gate.tsx b/web-next/components/shell/auth-gate.tsx
index b7784eec8d..7d397f976f 100644
--- a/web-next/components/shell/auth-gate.tsx
+++ b/web-next/components/shell/auth-gate.tsx
@@ -1,12 +1,19 @@
 'use client';
 
 import { useEffect, useState } from 'react';
+import { usePathname } from 'next/navigation';
 import { buildLoginRedirectHref, buildLoginReturnTo } from '@/lib/passport-login/controller';
-import { readClientSessionState } from '@/lib/session-client';
+import { hasClientSessionMarker, readClientSessionState } from '@/lib/session-client';
+
+function shouldOptimisticallyRenderProtectedRoute(pathname: string) {
+  return pathname === '/topology' || pathname.startsWith('/topology/');
+}
 
 export function AuthGate({ children }: { children: React.ReactNode }) {
-  const [ready, setReady] = useState(false);
-  const [authed, setAuthed] = useState(false);
+  const pathname = usePathname();
+  const optimisticRender = shouldOptimisticallyRenderProtectedRoute(pathname) && hasClientSessionMarker();
+  const [ready, setReady] = useState(optimisticRender);
+  const [authed, setAuthed] = useState(optimisticRender);
 
   useEffect(() => {
     let cancelled = false;
diff --git a/web-next/components/shell/auth-recovery.chrome.test.ts b/web-next/components/shell/auth-recovery.chrome.test.ts
index ca241c55ff..b1cc6d2227 100644
--- a/web-next/components/shell/auth-recovery.chrome.test.ts
+++ b/web-next/components/shell/auth-recovery.chrome.test.ts
@@ -26,4 +26,18 @@ describe('auth recovery posture', () => {
     expect(authGateSource).not.toContain('Restoring your workspace');
     expect(authGateSource).not.toContain('max-w-lg');
   });
+
+  it('lets topology preheat protected content when a local session marker exists while still verifying the BFF session', () => {
+    const authGateSource = readFileSync(resolve(process.cwd(), 'components/shell/auth-gate.tsx'), 'utf8');
+    const sessionClientSource = readFileSync(resolve(process.cwd(), 'lib/session-client.ts'), 'utf8');
+
+    expect(authGateSource).toContain('shouldOptimisticallyRenderProtectedRoute(pathname)');
+    expect(authGateSource).toContain('hasClientSessionMarker()');
+    expect(authGateSource).toContain("pathname === '/topology' || pathname.startsWith('/topology/')");
+    expect(authGateSource).toContain('useState(optimisticRender)');
+    expect(authGateSource).toContain('readClientSessionState().then(session =>');
+    expect(authGateSource).toContain('window.location.href = buildLoginRedirectHref');
+    expect(sessionClientSource).toContain('export function hasClientSessionMarker()');
+    expect(sessionClientSource).toContain("trimmed.startsWith('hb_ui_session=')");
+  });
 });
diff --git a/web-next/components/shell/locale-option-list.tsx b/web-next/components/shell/locale-option-list.tsx
index 0b41b3dabb..7dd2a891b6 100644
--- a/web-next/components/shell/locale-option-list.tsx
+++ b/web-next/components/shell/locale-option-list.tsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import { HzLocaleMenuOption } from '@hertzbeat/ui';
+import { HzLocaleMenuOption } from '@hertzbeat/ui/shell';
 import { useI18n } from '../providers/i18n-provider';
 import { cn } from '../../lib/utils';
 import type { LOCALES, LocaleCode } from '../../lib/i18n';
diff --git a/web-next/lib/app-frame-state.test.ts b/web-next/lib/app-frame-state.test.ts
index 7ad9e6cf83..26c06ca2e6 100644
--- a/web-next/lib/app-frame-state.test.ts
+++ b/web-next/lib/app-frame-state.test.ts
@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest';
-import { isActiveRoute, isStandaloneRoute, shouldLoadHeaderState } from './app-frame-state';
+import { isActiveRoute, isStandaloneRoute, shouldLoadHeaderRealtime, shouldLoadHeaderState } from './app-frame-state';
 
 describe('app frame state', () => {
   it('recognizes standalone routes', () => {
@@ -35,4 +35,13 @@ describe('app frame state', () => {
     expect(shouldLoadHeaderState('/monitors/42')).toBe(false);
     expect(shouldLoadHeaderState('/monitors/42/edit')).toBe(false);
   });
+
+  it('does not let header state or SSE streams compete with topology first-canvas inspection', () => {
+    expect(shouldLoadHeaderState('/topology')).toBe(false);
+    expect(shouldLoadHeaderState('/topology?sourceKind=cmdb-manual-label')).toBe(false);
+    expect(shouldLoadHeaderRealtime('/topology')).toBe(false);
+    expect(shouldLoadHeaderRealtime('/topology?sourceKind=cmdb-manual-label')).toBe(false);
+    expect(shouldLoadHeaderState('/overview')).toBe(true);
+    expect(shouldLoadHeaderRealtime('/overview')).toBe(true);
+  });
 });
diff --git a/web-next/lib/app-frame-state.ts b/web-next/lib/app-frame-state.ts
index 4b56d1f1fa..1b543a1ba3 100644
--- a/web-next/lib/app-frame-state.ts
+++ b/web-next/lib/app-frame-state.ts
@@ -12,5 +12,11 @@ export function isStandaloneRoute(pathname: string) {
 export function shouldLoadHeaderState(pathname: string) {
   const pathOnly = pathname.split('?')[0] || pathname;
   const isMonitorCrudRoute = pathOnly === '/monitors' || pathOnly.startsWith('/monitors/');
-  return !isStandaloneRoute(pathOnly) && !isMonitorCrudRoute;
+  const isTopologyRoute = pathOnly === '/topology' || pathOnly.startsWith('/topology/');
+  return !isStandaloneRoute(pathOnly) && !isMonitorCrudRoute && !isTopologyRoute;
+}
+
+export function shouldLoadHeaderRealtime(pathname: string) {
+  const pathOnly = pathname.split('?')[0] || pathname;
+  return shouldLoadHeaderState(pathOnly);
 }
diff --git a/web-next/lib/entity-contract.test.ts b/web-next/lib/entity-contract.test.ts
index b18d606fae..8de7436da3 100644
--- a/web-next/lib/entity-contract.test.ts
+++ b/web-next/lib/entity-contract.test.ts
@@ -133,6 +133,36 @@ describe('HertzBeat unified entity contract', () => {
     });
   });
 
+  it('prefers shared signal evidence over stale entity detail signal fields', () => {
+    expect(
+      buildEntityContractFromDetail({
+        entity: {
+          entity: {
+            id: 8,
+            name: 'payment',
+            type: 'service'
+          }
+        },
+        evidenceSummary: {
+          logHintCount: 1,
+          lastEvidenceAt: 'stale-evidence'
+        },
+        logSummary: { hintCount: 1 },
+        traceSummary: { recentTraceCount: 1, recentErrorTraceCount: 0, latestObservedAt: 'stale-trace', active: false },
+        signalEvidence: {
+          logSummary: { hintCount: 6 },
+          traceSummary: { recentTraceCount: 9, recentErrorTraceCount: 3, latestObservedAt: 'shared-trace', active: true }
+        }
+      })
+    ).toMatchObject({
+      signals: {
+        logs: { present: true, count: 6 },
+        traces: { present: true, count: 9, errorCount: 3 }
+      },
+      lastSeen: 'shared-trace'
+    });
+  });
+
   it('normalizes OTLP bound entities and monitor objects as first-class entity sources', () => {
     expect(
       buildEntityContractFromOtlpBinding({
diff --git a/web-next/lib/entity-contract.ts b/web-next/lib/entity-contract.ts
index d0d64ebb8b..5398115fa2 100644
--- a/web-next/lib/entity-contract.ts
+++ b/web-next/lib/entity-contract.ts
@@ -69,6 +69,14 @@ function compactAttributes(values: Record<string, unknown>) {
   );
 }
 
+function detailLogSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'logSummary'>) {
+  return detail.signalEvidence?.logSummary || detail.logSummary;
+}
+
+function detailTraceSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'traceSummary'>) {
+  return detail.signalEvidence?.traceSummary || detail.traceSummary;
+}
+
 function normalizeSource(source: unknown): HertzBeatEntitySourceKind {
   const value = String(source || '').toLowerCase();
 
@@ -172,8 +180,10 @@ export function buildEntityContractFromDetail(detail: EntityDetailDto): HertzBea
   const monitorBinds = entityDto.monitorBinds ?? [];
   const relations = entityDto.relations ?? [];
   const activeAlertCount = detail.evidenceSummary?.activeAlertCount ?? detail.activeAlerts?.length ?? 0;
-  const traceCount = detail.traceSummary?.recentTraceCount ?? 0;
-  const logCount = detail.logSummary?.hintCount ?? detail.evidenceSummary?.logHintCount ?? 0;
+  const traceSummary = detailTraceSummary(detail);
+  const logSummary = detailLogSummary(detail);
+  const traceCount = traceSummary?.recentTraceCount ?? 0;
+  const logCount = logSummary?.hintCount ?? detail.evidenceSummary?.logHintCount ?? 0;
   const monitorCount = detail.monitorSummary?.totalBoundMonitors ?? monitorBinds.length;
 
   return {
@@ -195,9 +205,9 @@ export function buildEntityContractFromDetail(detail: EntityDetailDto): HertzBea
       metrics: { present: monitorCount > 0, count: monitorCount },
       logs: { present: logCount > 0, count: logCount },
       traces: {
-        present: Boolean(detail.traceSummary?.active || traceCount > 0),
+        present: Boolean(traceSummary?.active || traceCount > 0),
         count: traceCount,
-        errorCount: detail.traceSummary?.recentErrorTraceCount ?? 0
+        errorCount: traceSummary?.recentErrorTraceCount ?? 0
       },
       alerts: { present: activeAlertCount > 0, count: activeAlertCount }
     },
@@ -207,7 +217,7 @@ export function buildEntityContractFromDetail(detail: EntityDetailDto): HertzBea
       downMonitorCount: detail.evidenceSummary?.downMonitorCount,
       healthyMonitorCount: detail.evidenceSummary?.healthyMonitorCount
     },
-    lastSeen: detail.traceSummary?.latestObservedAt ?? detail.evidenceSummary?.lastEvidenceAt ?? null
+    lastSeen: traceSummary?.latestObservedAt ?? detail.evidenceSummary?.lastEvidenceAt ?? null
   };
 }
 
diff --git a/web-next/lib/entity-detail/view-model.test.ts b/web-next/lib/entity-detail/view-model.test.ts
index 5f1d94142c..e23536e30e 100644
--- a/web-next/lib/entity-detail/view-model.test.ts
+++ b/web-next/lib/entity-detail/view-model.test.ts
@@ -64,6 +64,50 @@ describe('entity detail view model', () => {
     ]);
   });
 
+  it('prefers the shared signal evidence bundle over scattered signal fields', () => {
+    const detail = {
+      evidenceSummary: { downMonitorCount: 0 },
+      monitorSummary: { totalBoundMonitors: 1 },
+      logSummary: { hintCount: 1, preferredQueryTitle: 'stale log context' },
+      traceSummary: { recentTraceCount: 1, recentErrorTraceCount: 0, latestTraceId: 'stale-trace' },
+      unifiedEvidenceSummary: {
+        activeSignalCount: 1,
+        metricsActive: true,
+        logsActive: false,
+        tracesActive: false,
+        metricEvidenceCount: 1,
+        logEvidenceCount: 0,
+        traceEvidenceCount: 0
+      },
+      signalEvidence: {
+        logSummary: { hintCount: 5, preferredQueryTitle: 'shared log context' },
+        traceSummary: { recentTraceCount: 8, recentErrorTraceCount: 2, latestTraceId: 'shared-trace' },
+        unifiedEvidenceSummary: {
+          activeSignalCount: 3,
+          metricsActive: true,
+          logsActive: true,
+          tracesActive: true,
+          metricEvidenceCount: 2,
+          logEvidenceCount: 5,
+          traceEvidenceCount: 8
+        }
+      }
+    } as any;
+
+    expect(buildSummaryRows(detail)).toEqual([
+      { title: 'Related metrics', copy: '1 bound monitors', meta: 'No abnormal monitors', tone: 'success' },
+      { title: 'Related logs', copy: '5 query hints available', meta: 'shared log context', tone: 'warning' },
+      { title: 'Related traces', copy: '8 recent traces', meta: '2 error traces', tone: 'danger' }
+    ]);
+    expect(buildUnifiedEvidenceRows(detail)[3]).toEqual({
+      title: 'Traces / RED',
+      copy: '8 recent traces',
+      meta: '2 error traces',
+      tone: 'danger'
+    });
+    expect(buildEntityContextHandoffLinks(detail, 'last-1h')[1].copy).toContain('traceId=shared-trace');
+  });
+
   it('builds the lightweight HertzBeat service health model without heavy SLO authoring', () => {
     expect(
       buildEntityHealthModel({
diff --git a/web-next/lib/entity-detail/view-model.ts b/web-next/lib/entity-detail/view-model.ts
index ce28773cac..bf7e0f55a8 100644
--- a/web-next/lib/entity-detail/view-model.ts
+++ b/web-next/lib/entity-detail/view-model.ts
@@ -12,6 +12,18 @@ export function translateEntityDetailViewModel(key: string, params?: Translation
   return interpolate(template, params);
 }
 
+function getSignalLogSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'logSummary'>) {
+  return detail.signalEvidence?.logSummary || detail.logSummary;
+}
+
+function getSignalTraceSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'traceSummary'>) {
+  return detail.signalEvidence?.traceSummary || detail.traceSummary;
+}
+
+function getSignalUnifiedEvidenceSummary(detail: Pick<EntityDetailDto, 'signalEvidence' | 'unifiedEvidenceSummary'>) {
+  return detail.signalEvidence?.unifiedEvidenceSummary || detail.unifiedEvidenceSummary;
+}
+
 type DetailRow = {
   title: string;
   copy: string;
@@ -164,14 +176,16 @@ function localizeActionText(
 }
 
 export function buildSummaryRows(
-  detail: Pick<EntityDetailDto, 'evidenceSummary' | 'monitorSummary' | 'logSummary' | 'traceSummary' | 'boundMonitors'>,
+  detail: Pick<EntityDetailDto, 'evidenceSummary' | 'monitorSummary' | 'logSummary' | 'traceSummary' | 'signalEvidence' | 'boundMonitors'>,
   t: EntityDetailViewModelTranslator = translateEntityDetailViewModel
 ) {
   const boundMonitorCount = detail.monitorSummary?.totalBoundMonitors ?? detail.boundMonitors?.length ?? 0;
   const downMonitorCount = detail.evidenceSummary?.downMonitorCount ?? 0;
-  const logHintCount = detail.logSummary?.hintCount ?? 0;
-  const traceCount = detail.traceSummary?.recentTraceCount ?? 0;
-  const errorTraceCount = detail.traceSummary?.recentErrorTraceCount ?? 0;
+  const logSummary = getSignalLogSummary(detail);
+  const traceSummary = getSignalTraceSummary(detail);
+  const logHintCount = logSummary?.hintCount ?? 0;
+  const traceCount = traceSummary?.recentTraceCount ?? 0;
+  const errorTraceCount = traceSummary?.recentErrorTraceCount ?? 0;
 
   return [
     {
@@ -189,7 +203,7 @@ export function buildSummaryRows(
         logHintCount > 0
           ? t('entities.detail.summary.logs.available', { count: formatCount(logHintCount, t('entities.detail.summary.logs.hints')) })
           : t('entities.detail.summary.logs.no-hints'),
-      meta: detail.logSummary?.preferredQueryTitle || detail.logSummary?.fallbackSearchTerm || '-',
+      meta: logSummary?.preferredQueryTitle || logSummary?.fallbackSearchTerm || '-',
       tone: logHintCount > 0 ? 'warning' : 'neutral'
     },
     {
@@ -293,6 +307,8 @@ export function buildEntityHealthModel(
   detail: EntityDetailDto,
   t: EntityDetailViewModelTranslator = translateEntityDetailViewModel
 ): DetailRow[] {
+  const logSummary = getSignalLogSummary(detail);
+  const traceSummary = getSignalTraceSummary(detail);
   const totalBoundMonitors = finiteNumber(
     detail.monitorSummary?.totalBoundMonitors ?? detail.boundMonitors?.length ?? detail.entity?.monitorBinds?.length,
     0
@@ -302,17 +318,17 @@ export function buildEntityHealthModel(
     0,
     Math.min(totalBoundMonitors, finiteNumber(detail.evidenceSummary?.healthyMonitorCount, totalBoundMonitors - downMonitorCount))
   );
-  const recentTraceCount = finiteNumber(detail.traceSummary?.recentTraceCount, 0);
-  const recentErrorTraceCount = finiteNumber(detail.traceSummary?.recentErrorTraceCount, 0);
+  const recentTraceCount = finiteNumber(traceSummary?.recentTraceCount, 0);
+  const recentErrorTraceCount = finiteNumber(traceSummary?.recentErrorTraceCount, 0);
   const activeAlertCount = finiteNumber(
     detail.alertSummary?.totalActiveAlerts ?? detail.activeAlerts?.length ?? detail.evidenceSummary?.activeAlertCount,
     0
   );
-  const logHintCount = finiteNumber(detail.logSummary?.hintCount ?? detail.evidenceSummary?.logHintCount, 0);
+  const logHintCount = finiteNumber(logSummary?.hintCount ?? detail.evidenceSummary?.logHintCount, 0);
   const availabilityRatio = totalBoundMonitors > 0 ? healthyMonitorCount / totalBoundMonitors : 0;
   const errorRate = recentTraceCount > 0 ? recentErrorTraceCount / recentTraceCount : 0;
   const recentAnomalyCount = downMonitorCount + recentErrorTraceCount + logHintCount;
-  const latency = formatLatency(detail.traceSummary, t);
+  const latency = formatLatency(traceSummary, t);
   const collectorEvidence = buildCollectorHealthEvidence({
     healthyMonitorCount,
     lastEvidenceLabel: detail.evidenceSummary?.lastEvidenceAt == null ? null : String(detail.evidenceSummary.lastEvidenceAt),
@@ -381,10 +397,10 @@ export function buildEntityHealthModel(
 }
 
 export function buildUnifiedEvidenceRows(
-  detail: Pick<EntityDetailDto, 'unifiedEvidenceSummary' | 'traceSummary'>,
+  detail: Pick<EntityDetailDto, 'unifiedEvidenceSummary' | 'traceSummary' | 'signalEvidence'>,
   t: EntityDetailViewModelTranslator = translateEntityDetailViewModel
 ): DetailRow[] {
-  const summary = detail.unifiedEvidenceSummary;
+  const summary = getSignalUnifiedEvidenceSummary(detail);
   if (!summary) {
     return [
       {
@@ -401,7 +417,7 @@ export function buildUnifiedEvidenceRows(
   const metricEvidenceCount = finiteNumber(summary.metricEvidenceCount, 0);
   const logEvidenceCount = finiteNumber(summary.logEvidenceCount, 0);
   const traceEvidenceCount = finiteNumber(summary.traceEvidenceCount, 0);
-  const traceErrorCount = finiteNumber(detail.traceSummary?.recentErrorTraceCount, 0);
+  const traceErrorCount = finiteNumber(getSignalTraceSummary(detail)?.recentErrorTraceCount, 0);
 
   return [
     {
@@ -573,7 +589,7 @@ function buildContextParams(
   const entityId = entity.id != null ? String(entity.id) : '';
   const entityName = entity.displayName || entity.name || '';
   const serviceName = normalizeContextText(routeContext.serviceName) || entity.name || entity.displayName || '';
-  const traceSummary = detail.traceSummary as (EntityDetailDto['traceSummary'] & { latestSpanId?: string | null }) | undefined;
+  const traceSummary = getSignalTraceSummary(detail);
   const timeRange = normalizeContextText(routeContext.timeRange) || 'last-1h';
   const params = new URLSearchParams();
 
diff --git a/web-next/lib/hertzbeat-2-gap-audit.test.ts b/web-next/lib/hertzbeat-2-gap-audit.test.ts
index b55c6c6bac..cc0b2f8175 100644
--- a/web-next/lib/hertzbeat-2-gap-audit.test.ts
+++ b/web-next/lib/hertzbeat-2-gap-audit.test.ts
@@ -86,7 +86,7 @@ describe('HertzBeat 2.0 release gap audit', () => {
         '/topology marks trace-call API degradation as Greptime trace graph unavailable instead of generic topology API empty evidence',
         'Topology edge signal drilldowns prefer the selected trace-call edge sample trace/span context and preserve it in returnTo links',
         'buildTopologyApiUrl forwards depth, relation type, hide-internal, and pagination scope into the topology API query',
-        'buildTopologyApiUrl defaults service-call and otlp-trace-call reads to relationType=trace-call so trace views do not fetch node-only relation graphs',
+        'buildTopologyApiUrl defaults service-call reads to relationType=trace-call while keeping application OTLP source filters on the mixed graph read',
         '/topology exposes Browser-readable topology API request scope metadata including resolved relationType=trace-call for service-call routes',
         '/topology passes resolved relationType=trace-call into shared loading and empty states for inferred service-call routes',
         'TopologyController and EntityTopologyQueryService accept backend relation type, hide-internal, and edge pagination scope for API-backed topology graphs',
@@ -165,6 +165,8 @@ describe('HertzBeat 2.0 release gap audit', () => {
         '/topology exposes node-click, edge-click, and table-row-click selection source markers so Browser can distinguish real in-page inspection from route initial state',
         '/topology clears fallback selected-edge context during node-click inspection so the G6 canvas and drawer do not retain stale edge state after ordinary node selection',
         '/topology Browser-verifies real G6 edge click opens the relationship drawer in-page without URL navigation, remount, zoom reset, or viewport drift',
+        '/topology durable Playwright smoke verifies real G6 node inspection after wheel zoom keeps URL, zoom, position, render key, and drawer state in-page',
+        '/topology durable Playwright smoke verifies compact service-call graphs open at 1x in a 2048px fresh browser viewport instead of filling the canvas with oversized nodes',
         '/topology Browser-verifies the final M5 route matrix across default, focused, service-call, and empty routes with API-owned graph or empty states and no static fallback',
         '/topology forwards topology search context into HzTopologyG6Canvas search focus metadata',
         '/topology reuses HzTopologyG6Canvas selected-node focus action for retained investigation context',
@@ -402,7 +404,8 @@ describe('HertzBeat 2.0 release gap audit', () => {
         'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java',
         'web-next/packages/hertzbeat-ui/src/index.tsx',
         'web-next/app/ui-lab/page.tsx',
-        'web-next/lib/topology-surface/controller.ts'
+        'web-next/lib/topology-surface/controller.ts',
+        'web-next/scripts/topology-g6-browser-smoke.spec.ts'
       ])
     });
     expect(audit.gaps.find(gap => gap.code === 'greptime-otlp-query-pushdown')).toMatchObject({
@@ -411,12 +414,43 @@ describe('HertzBeat 2.0 release gap audit', () => {
         'EntityTraceQueryServiceImpl builds entity trace summary/hints from storage-owned recent rows scoped by lookback time, workspace, and canonical entity identity',
         'GreptimeTraceQueryRepository owns service-to-service trace graph RED SQL aggregation with seed service source/target scope for topology',
         'GreptimeTraceQueryRepository owns grouped trace-list SQL with total_count, LIMIT, and OFFSET for paged list views',
+        'GreptimeTraceQueryRepository owns trace overview total/error/latest activity aggregate SQL with workspace, entity, service, environment, and internal-service filters',
+        'GreptimeTraceQueryRepository owns explicit trace-id overview total/error/latest aggregate SQL with trace id, workspace, entity, service, environment, and internal-service filters',
+        'GreptimeTraceQueryRepository owns entity trace summary total/error/latest/latest-trace aggregate SQL with workspace, entity, service, environment, and internal-service filters',
         'GreptimeTraceQueryRepository owns trace-id row SQL with time, service, environment, workspace, entity identity, and internal-service filters when route/detail context is present',
-        'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples'
+        'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples',
+        'GreptimeDbDataStorage owns workspace-scoped log list count/page SQL with search, noise filters, LIMIT/OFFSET, and timestamp-desc sorting',
+        'GreptimeDbDataStorage owns workspace-scoped log overview severity-bucket aggregate SQL with search and noise filters',
+        'GreptimeDbDataStorage owns workspace-scoped log trace-coverage and hourly-trend aggregate SQL with search and noise filters'
       ]),
       sourceFiles: expect.arrayContaining([
         'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java',
-        'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java'
+        'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java',
+        'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java',
+        'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java'
+      ])
+    });
+    expect(audit.gaps.find(gap => gap.code === 'entity-evidence-read-model')).toMatchObject({
+      backendApiStatus: 'partial',
+      releaseBlocking: true,
+      evidence: expect.arrayContaining([
+        'EntitySignalEvidenceBundle carries log summary, trace summary, metric evidence, log evidence, trace evidence, log hints, trace hints, unified evidence, and triage recommendation through one shared signal evidence response',
+        'EntityDetailDto exposes EntitySignalEvidenceBundle so entity detail clients can consume the shared evidence response directly',
+        'EntityDetailObservabilityReadModelService passes EntitySignalEvidenceBundle into EntityResponseHandoffReadModelService for response handoff assembly',
+        'EntityObservabilityGatewayImpl prefers EntityResponseHandoffsRequest.signalEvidence when deriving log and trace handoff context',
+        'Entity detail frontend view-model prefers EntitySignalEvidenceBundle for log summary, trace summary, unified RED evidence, and trace handoff IDs before falling back to scattered fields',
+        'EntityDetailSurface subtitle and buildEntityContractFromDetail now prefer EntitySignalEvidenceBundle for signal presence/count/last-seen context'
+      ]),
+      sourceFiles: expect.arrayContaining([
+        'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java',
+        'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java',
+        'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java',
+        'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java',
+        'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java',
+        'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java',
+        'web-next/components/pages/entity-detail-surface.tsx',
+        'web-next/lib/entity-contract.ts',
+        'web-next/lib/entity-detail/view-model.ts'
       ])
     });
 
diff --git a/web-next/lib/hertzbeat-2-gap-audit.ts b/web-next/lib/hertzbeat-2-gap-audit.ts
index f1493966a1..eaabc6cfbc 100644
--- a/web-next/lib/hertzbeat-2-gap-audit.ts
+++ b/web-next/lib/hertzbeat-2-gap-audit.ts
@@ -124,7 +124,7 @@ const functionalGaps: HertzBeat2Gap[] = [
       '/topology marks trace-call API degradation as Greptime trace graph unavailable instead of generic topology API empty evidence',
       'Topology edge signal drilldowns prefer the selected trace-call edge sample trace/span context and preserve it in returnTo links',
       'buildTopologyApiUrl forwards depth, relation type, hide-internal, and pagination scope into the topology API query',
-      'buildTopologyApiUrl defaults service-call and otlp-trace-call reads to relationType=trace-call so trace views do not fetch node-only relation graphs',
+      'buildTopologyApiUrl defaults service-call reads to relationType=trace-call while keeping application OTLP source filters on the mixed graph read',
       '/topology exposes Browser-readable topology API request scope metadata including resolved relationType=trace-call for service-call routes',
       '/topology passes resolved relationType=trace-call into shared loading and empty states for inferred service-call routes',
       'TopologyController and EntityTopologyQueryService accept backend relation type, hide-internal, and edge pagination scope for API-backed topology graphs',
@@ -189,6 +189,8 @@ const functionalGaps: HertzBeat2Gap[] = [
       '/topology exposes node-click, edge-click, and table-row-click selection source markers so Browser can distinguish real in-page inspection from route initial state',
       '/topology clears fallback selected-edge context during node-click inspection so the G6 canvas and drawer do not retain stale edge state after ordinary node selection',
       '/topology Browser-verifies real G6 edge click opens the relationship drawer in-page without URL navigation, remount, zoom reset, or viewport drift',
+      '/topology durable Playwright smoke verifies real G6 node inspection after wheel zoom keeps URL, zoom, position, render key, and drawer state in-page',
+      '/topology durable Playwright smoke verifies compact service-call graphs open at 1x in a 2048px fresh browser viewport instead of filling the canvas with oversized nodes',
       '/topology Browser-verifies the final M5 route matrix across default, focused, service-call, and empty routes with API-owned graph or empty states and no static fallback',
       '/topology forwards topology search context into HzTopologyG6Canvas search focus metadata',
       '/topology forwards environment, source, search, and group scope into HzTopologyG6Canvas filter-scope metadata',
@@ -448,9 +450,10 @@ const functionalGaps: HertzBeat2Gap[] = [
       'web-next/app/ui-lab/page.tsx',
       'web-next/lib/topology-surface/controller.ts',
       'web-next/lib/topology-surface/view-model.ts',
-      'web-next/app/topology/topology-page.tsx'
+      'web-next/app/topology/topology-page.tsx',
+      'web-next/scripts/topology-g6-browser-smoke.spec.ts'
     ],
-    nextStep: 'Move any remaining topology regression proof into durable browser fixtures and keep the live Greptime RED graph available for release smoke runs.'
+    nextStep: 'Keep the live Greptime RED graph and topology G6 browser smoke available for release regression runs while finishing the remaining cross-domain release gaps.'
   },
   {
     code: 'incident-workbench-domain',
@@ -492,7 +495,7 @@ const functionalGaps: HertzBeat2Gap[] = [
     page: '/trace/manage and /log/manage',
     backendApiStatus: 'partial',
     releaseBlocking: true,
-    currentState: 'Greptime trace and log reads exist, the topology trace-call path has timestamp, environment, seed service scope, and internal-service SQL predicates plus service graph RED aggregation, and trace list reads now pass time, service namespace, environment, workspace, canonical entity identity scope, grouping, total count, limit, and offset into the Greptime trace repository before Java fallback filtering. Trace-id list/detail reads now have a Greptime trace-row overload th [...]
+    currentState: 'Greptime trace and log reads exist, the topology trace-call path has timestamp, environment, seed service scope, and internal-service SQL predicates plus service graph RED aggregation, and trace list reads now pass time, service namespace, environment, workspace, canonical entity identity scope, grouping, total count, limit, and offset into the Greptime trace repository before Java fallback filtering. Trace overview now uses Greptime-side trace grouping for total/error [...]
     requiredClosure: [
       'Push time, workspace, entity, service, environment, trace id, pagination, and sorting into Greptime SQL.',
       'Promote frequently filtered OTLP attributes to queryable columns or tags where benchmarks require it.'
@@ -502,13 +505,21 @@ const functionalGaps: HertzBeat2Gap[] = [
       'GreptimeTraceQueryRepository owns service, service namespace, environment, workspace, entity identity, internal, and timestamp predicates for recent trace rows',
       'GreptimeTraceQueryRepository owns service-to-service trace graph RED SQL aggregation with seed service source/target scope for topology',
       'GreptimeTraceQueryRepository owns grouped trace-list SQL with total_count, LIMIT, and OFFSET for paged list views',
+      'GreptimeTraceQueryRepository owns trace overview total/error/latest activity aggregate SQL with workspace, entity, service, environment, and internal-service filters',
+      'GreptimeTraceQueryRepository owns explicit trace-id overview total/error/latest aggregate SQL with trace id, workspace, entity, service, environment, and internal-service filters',
+      'GreptimeTraceQueryRepository owns entity trace summary total/error/latest/latest-trace aggregate SQL with workspace, entity, service, environment, and internal-service filters',
       'GreptimeTraceQueryRepository owns trace-id row SQL with time, service, environment, workspace, entity identity, and internal-service filters when route/detail context is present',
       'EntityTraceQueryServiceImpl pushes canonical entity identity into trace detail row reads before Java span aggregation',
-      'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples'
+      'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples',
+      'GreptimeDbDataStorage owns workspace-scoped log list count/page SQL with search, noise filters, LIMIT/OFFSET, and timestamp-desc sorting',
+      'GreptimeDbDataStorage owns workspace-scoped log overview severity-bucket aggregate SQL with search and noise filters',
+      'GreptimeDbDataStorage owns workspace-scoped log trace-coverage and hourly-trend aggregate SQL with search and noise filters'
     ],
     sourceFiles: [
       'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java',
-      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java'
+      'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java',
+      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java',
+      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java'
     ],
     nextStep: 'Move remaining non-Greptime fallback semantics away from bounded Java samples where feasible, then re-run live trace list/detail smoke after the warehouse compile blocker is cleared.'
   },
@@ -520,21 +531,36 @@ const functionalGaps: HertzBeat2Gap[] = [
     route: '/entities/[entityId]',
     backendApiStatus: 'partial',
     releaseBlocking: true,
-    currentState: 'Entity relations and detail APIs exist, but topology, incident, and signal workspaces do not share one evidence contract.',
+    currentState: 'Entity relations and detail APIs exist, entity detail now builds and returns a shared signal evidence bundle for response handoffs instead of threading log, trace, and metric evidence as separate route-context fragments, and the entity detail frontend plus unified entity contract prefer that bundle when rendering signal summaries, RED evidence, page context, and log/trace handoff context. Topology, incident, and the remaining signal workspaces still need to consume the [...]
     requiredClosure: [
       'Return metrics, logs, traces, alerts, relations, and handoffs through one entity evidence read model.',
       'Make signal workspaces consume that contract instead of rebuilding route context locally.'
     ],
     evidence: [
       'EntityRelation has DAO and service coverage',
+      'EntitySignalEvidenceBundle carries log summary, trace summary, metric evidence, log evidence, trace evidence, log hints, trace hints, unified evidence, and triage recommendation through one shared signal evidence response',
+      'EntityDetailDto exposes EntitySignalEvidenceBundle so entity detail clients can consume the shared evidence response directly',
+      'EntityDetailObservabilityReadModelService passes EntitySignalEvidenceBundle into EntityResponseHandoffReadModelService for response handoff assembly',
+      'EntityObservabilityGatewayImpl prefers EntityResponseHandoffsRequest.signalEvidence when deriving log and trace handoff context',
+      'Entity detail frontend view-model prefers EntitySignalEvidenceBundle for log summary, trace summary, unified RED evidence, and trace handoff IDs before falling back to scattered fields',
+      'EntityDetailSurface subtitle and buildEntityContractFromDetail now prefer EntitySignalEvidenceBundle for signal presence/count/last-seen context',
       'signal workspaces still keep page-specific query and handoff builders'
     ],
     sourceFiles: [
       'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityRelation.java',
+      'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/entity/EntitySignalEvidenceBundle.java',
+      'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/dto/handoff/EntityResponseHandoffsRequest.java',
       'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/EntityController.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/EntityDetailDto.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelService.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityResponseHandoffReadModelService.java',
+      'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java',
+      'web-next/components/pages/entity-detail-surface.tsx',
+      'web-next/lib/entity-contract.ts',
+      'web-next/lib/entity-detail/view-model.ts',
       'web-next/lib/signal-route-context.ts'
     ],
-    nextStep: 'Define the shared evidence response and migrate one signal handoff at a time.'
+    nextStep: 'Migrate topology, incident, and the remaining signal workbench handoffs to consume EntitySignalEvidenceBundle instead of rebuilding page-local query context.'
   },
   {
     code: 'ui-i18n-reuse-sweep',
diff --git a/web-next/lib/i18n-runtime-messages.ts b/web-next/lib/i18n-runtime-messages.ts
index c1e4754789..90cc378250 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -908,6 +908,11 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.view.zoom-in': 'Zoom in',
     'topology.view.zoom-out': 'Zoom out',
     'topology.view.focus-selected': 'Focus selected node',
+    'topology.edge-density.open-table': 'Open edge table',
+    'topology.edge-density.hidden-in-table': '{{count}} more in table',
+    'topology.metric-table.edge-summary': 'Canvas {{rendered}} / table {{total}}',
+    'topology.metric-table.row-render-summary': 'Showing {{rendered}} of {{total}} rows',
+    'topology.metric-table.row-render-show-more': 'Show {{next}} of {{total}} rows',
     'topology.locate-entity': 'Locate entity',
     'topology.current-filter': 'Current filter',
     'topology.all-entities': 'All entities',
@@ -930,6 +935,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.toolbar.reset': 'Reset',
     'topology.focus-trail.label': 'Focused topology',
     'topology.focus-trail.exit': 'Exit focus',
+    'topology.view-scope.focused-adjacency': 'Focused local view',
+    'topology.view-scope.focused-adjacency.copy': 'Open global topology',
     'topology.focus-trail.hidden-count': '{{count}} hidden by scope',
     'topology.focus-trail.filter.environment': 'Environment',
     'topology.focus-trail.filter.time-range': 'Time range',
@@ -944,11 +951,16 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.group-panel.source-kind': 'Evidence source',
     'topology.group-panel.node-count': '{{count}} visible nodes',
     'topology.group-panel.edge-count': '{{count}} visible edges',
+    'topology.g6.summary.rendered-node-count': '{{rendered}}/{{total}} nodes rendered',
+    'topology.g6.summary.rendered-edge-count': '{{rendered}}/{{total}} edges rendered',
     'topology.group-panel.collapsed-count': '{{count}} collapsed',
     'topology.group-panel.active-scope': 'active scope',
     'topology.group-panel.clear-group': 'Clear group',
     'topology.group-panel.open-table': 'Open table',
     'topology.path-summary.title': 'Selected path',
+    'topology.path-summary.title.preview': 'Path preview',
+    'topology.path-summary.title.selected': 'Selected path',
+    'topology.path-summary.title.hovered': 'Hovered path',
     'topology.path-summary.source': 'Source',
     'topology.path-summary.target': 'Target',
     'topology.path-summary.relation': 'Relation',
@@ -960,8 +972,13 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.empty.api.title': 'No topology relationships found',
     'topology.empty.api.copy': 'The API returned no entity, monitor, or trace-call relationship evidence for the current filter.',
     'topology.empty.trace-call.title': 'No trace-call topology evidence',
-    'topology.empty.trace-call.copy': 'Greptime has no OTLP trace-call edges with RED metrics for the selected time and environment. Relation nodes are hidden so this view does not look complete without trace evidence.',
+    'topology.empty.trace-call.copy': 'No trace-call edges in this window.',
     'topology.empty.trace-call.source': 'Greptime trace graph',
+    'topology.relation-gap.title': 'No relationship edges returned',
+    'topology.relation-gap.copy': 'Nodes loaded, but this scope has no relation, trace-call, or ownership edges.',
+    'topology.relation-gap.action.trace-call': 'View call relationships',
+    'topology.relation-gap.action.trace-call.copy': 'Switch to OTLP trace-call evidence for this time range.',
+    'topology.node-only.explanation': 'Entities only · no relation edges',
     'topology.degraded.api.title': 'Topology API unavailable',
     'topology.degraded.api.copy': 'The topology graph is degraded because API, Greptime, trace, or relation evidence could not be loaded for the selected scope.',
     'topology.degraded.api.source': 'Topology API unavailable',
@@ -1038,6 +1055,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.edge.link.metrics': 'Metrics evidence',
     'topology.edge.link.logs': 'Logs evidence',
     'topology.edge.link.traces': 'Traces evidence',
+    'topology.edge.render-window.label': 'Canvas window',
     'topology.current-entity': 'Current entity',
     'topology.current-entity.signals': 'Signals: {{signals}}',
     'topology.current-entity.boundary': 'Signal handoffs keep the current entity, time range, environment, and topology scope.',
@@ -1058,10 +1076,10 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.metric-table.row-action': 'Inspect',
     'topology.metric-table.open-edge-aria': 'Open topology edge {{edge}}',
     'topology.metric-table.filter.all': 'All',
-    'topology.metric-table.filter.visible': 'Visible',
-    'topology.metric-table.filter.partial': 'Partial',
-    'topology.metric-table.filter.hidden': 'Hidden',
-    'topology.metric-table.filter.unknown': 'Unknown',
+    'topology.metric-table.filter.visible': 'Both endpoints on canvas',
+    'topology.metric-table.filter.partial': 'One endpoint on canvas',
+    'topology.metric-table.filter.hidden': 'Outside canvas',
+    'topology.metric-table.filter.unknown': 'Unknown endpoints',
     'topology.red.request-rate': 'Request rate',
     'topology.red.error-rate': 'Error rate',
     'topology.red.latency-p95': 'P95 latency',
@@ -4427,6 +4445,11 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.view.zoom-in': '放大',
     'topology.view.zoom-out': '缩小',
     'topology.view.focus-selected': '聚焦选中节点',
+    'topology.edge-density.open-table': '打开边表格',
+    'topology.edge-density.hidden-in-table': '其余 {{count}} 条在表格',
+    'topology.metric-table.edge-summary': '画布 {{rendered}} / 表格 {{total}}',
+    'topology.metric-table.row-render-summary': '显示 {{rendered}} / {{total}} 条关系',
+    'topology.metric-table.row-render-show-more': '显示 {{next}} / {{total}} 条关系',
     'topology.locate-entity': '定位实体',
     'topology.current-filter': '当前筛选',
     'topology.all-entities': '全部实体',
@@ -4449,6 +4472,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.toolbar.reset': '重置',
     'topology.focus-trail.label': '聚焦拓扑',
     'topology.focus-trail.exit': '退出聚焦',
+    'topology.view-scope.focused-adjacency': '当前为局部聚焦视角',
+    'topology.view-scope.focused-adjacency.copy': '查看全局拓扑',
     'topology.focus-trail.hidden-count': '当前范围隐藏 {{count}} 个',
     'topology.focus-trail.filter.environment': '环境',
     'topology.focus-trail.filter.time-range': '时间范围',
@@ -4463,11 +4488,16 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.group-panel.source-kind': '证据来源',
     'topology.group-panel.node-count': '{{count}} 个可见节点',
     'topology.group-panel.edge-count': '{{count}} 条可见边',
+    'topology.g6.summary.rendered-node-count': '{{rendered}}/{{total}} 个节点已渲染',
+    'topology.g6.summary.rendered-edge-count': '{{rendered}}/{{total}} 条边已渲染',
     'topology.group-panel.collapsed-count': '折叠 {{count}} 个',
     'topology.group-panel.active-scope': '当前范围',
     'topology.group-panel.clear-group': '清除分组',
     'topology.group-panel.open-table': '打开表格',
     'topology.path-summary.title': '选中路径',
+    'topology.path-summary.title.preview': '路径预览',
+    'topology.path-summary.title.selected': '选中路径',
+    'topology.path-summary.title.hovered': '悬停路径',
     'topology.path-summary.source': '起点',
     'topology.path-summary.target': '终点',
     'topology.path-summary.relation': '关系',
@@ -4479,8 +4509,13 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.empty.api.title': '未找到拓扑关系',
     'topology.empty.api.copy': '当前筛选下 API 没有返回实体、监控对象或链路调用关系证据。',
     'topology.empty.trace-call.title': '暂无 trace 调用拓扑证据',
-    'topology.empty.trace-call.copy': 'Greptime 在当前时间和环境下没有带 RED 指标的 OTLP trace 调用边。此视图会隐藏关系节点,避免把缺少 trace 证据的结果看成完整拓扑。',
+    'topology.empty.trace-call.copy': '当前窗口无 trace 调用边。',
     'topology.empty.trace-call.source': 'Greptime trace graph',
+    'topology.relation-gap.title': '未返回关系边',
+    'topology.relation-gap.copy': '节点已加载,但当前范围没有关系、调用或归属边证据。',
+    'topology.relation-gap.action.trace-call': '查看调用关系',
+    'topology.relation-gap.action.trace-call.copy': '切换到当前时间范围的 OTLP trace 调用证据。',
+    'topology.node-only.explanation': '仅实体 · 暂无关系边',
     'topology.degraded.api.title': '拓扑 API 不可用',
     'topology.degraded.api.copy': '当前范围无法加载 API、Greptime、链路或关系证据,拓扑图处于降级状态。',
     'topology.degraded.api.source': '拓扑 API 不可用',
@@ -4557,6 +4592,7 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.edge.link.metrics': '指标证据',
     'topology.edge.link.logs': '日志证据',
     'topology.edge.link.traces': '链路证据',
+    'topology.edge.render-window.label': '画布窗口',
     'topology.current-entity': '当前实体',
     'topology.current-entity.signals': '信号:{{signals}}',
     'topology.current-entity.boundary': '信号跳转会保留当前实体、时间范围、环境和拓扑范围。',
@@ -4577,10 +4613,10 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.metric-table.row-action': '检查',
     'topology.metric-table.open-edge-aria': '打开拓扑边 {{edge}}',
     'topology.metric-table.filter.all': '全部',
-    'topology.metric-table.filter.visible': '可见',
-    'topology.metric-table.filter.partial': '半可见',
-    'topology.metric-table.filter.hidden': '隐藏',
-    'topology.metric-table.filter.unknown': '未知',
+    'topology.metric-table.filter.visible': '两端在画布',
+    'topology.metric-table.filter.partial': '一端在画布',
+    'topology.metric-table.filter.hidden': '画布外',
+    'topology.metric-table.filter.unknown': '端点未知',
     'topology.red.request-rate': '请求速率',
     'topology.red.error-rate': '错误率',
     'topology.red.latency-p95': 'P95 延迟',
diff --git a/web-next/lib/session-client.ts b/web-next/lib/session-client.ts
index 5f5a12b81f..5f8eaa2042 100644
--- a/web-next/lib/session-client.ts
+++ b/web-next/lib/session-client.ts
@@ -64,6 +64,16 @@ export function clearClientSessionMarker() {
   document.cookie = 'hb_ui_session=; Max-Age=0; path=/; SameSite=Lax';
 }
 
+export function hasClientSessionMarker() {
+  if (typeof document === 'undefined') return false;
+  return document.cookie
+    .split(';')
+    .some(segment => {
+      const trimmed = segment.trim();
+      return trimmed.startsWith('hb_ui_session=') && trimmed !== 'hb_ui_session=';
+    });
+}
+
 export async function readClientSessionState(): Promise<ClientSessionState> {
   try {
     const response = await fetch('/api/account/session', {
diff --git a/web-next/lib/topology-surface/controller.test.ts b/web-next/lib/topology-surface/controller.test.ts
index b27b42478d..3a83254568 100644
--- a/web-next/lib/topology-surface/controller.test.ts
+++ b/web-next/lib/topology-surface/controller.test.ts
@@ -1,7 +1,138 @@
 import { describe, expect, it, vi } from 'vitest';
-import { buildTopologyApiUrl, loadTopologyGraph } from './controller';
+import {
+  CMDB_TOPOLOGY_API_TIMEOUT_MS,
+  buildTopologyApiUrl,
+  hasUnresolvableTopologyFocusEntity,
+  loadTopologyGraph,
+  shouldPreservePreviousTopologyGraphDuringLoad,
+  resolveTopologyApiTimeoutMs
+} from './controller';
 
 describe('topology API controller', () => {
+  it('resolves topology timeRange into concrete API start and end bounds', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          entityId: '501',
+          environment: 'prod',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          timeRange: 'last-1h'
+        })
+      ).toBe(
+        '/topology?focusEntityId=501&depth=2&environment=prod&relationType=trace-call&start=1780218000000&end=1780221600000'
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('does not apply timeRange bounds to CMDB manual-label topology reads', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          sourceKind: 'cmdb-manual-label',
+          viewMode: 'application',
+          timeRange: 'last-1h'
+        })
+      ).toBe('/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('applies timeRange bounds to all-source topology reads so mixed trace edges cannot leak from stale windows', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          viewMode: 'application',
+          timeRange: 'last-1h'
+        })
+      ).toBe('/topology?depth=2&environment=prod&start=1780218000000&end=1780221600000');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('keeps application OTLP source filters on the mixed topology read so edge endpoints stay available for G6 filtering', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'application',
+          timeRange: 'last-1h'
+        })
+      ).toBe('/topology?depth=2&environment=prod&start=1780218000000&end=1780221600000');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('uses the longer topology API timeout for CMDB manual-label reads so slow catalog queries are not aborted before retry', () => {
+    expect(
+      resolveTopologyApiTimeoutMs({
+        environment: 'prod',
+        sourceKind: 'cmdb-manual-label',
+        viewMode: 'application',
+        timeRange: 'last-1h'
+      })
+    ).toBe(CMDB_TOPOLOGY_API_TIMEOUT_MS);
+  });
+
+  it('retries a transient CMDB manual-label read once before degrading the topology surface', async () => {
+    const graph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['cmdb-manual-label'],
+      nodes: [
+        { id: '642126742338816', entityId: 642126742338816, entityName: 'Payment API', entityType: 'service' }
+      ],
+      edges: []
+    };
+    const apiGet = vi
+      .fn()
+      .mockRejectedValueOnce(new Error('transient proxy stall'))
+      .mockResolvedValueOnce(graph);
+
+    await expect(
+      loadTopologyGraph(apiGet, {
+        environment: 'prod',
+        sourceKind: 'cmdb-manual-label',
+        viewMode: 'application',
+        timeRange: 'last-1h'
+      })
+    ).resolves.toBe(graph);
+    expect(apiGet).toHaveBeenCalledTimes(2);
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label');
+    expect(apiGet).toHaveBeenNthCalledWith(2, '/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label');
+  });
+
+  it('does not retry trace-call topology reads because they already use an explicit long timeout', async () => {
+    const apiGet = vi.fn().mockRejectedValue(new Error('trace query unavailable'));
+
+    await expect(
+      loadTopologyGraph(apiGet, {
+        environment: 'prod',
+        sourceKind: 'otlp-trace-call',
+        viewMode: 'service-call',
+        timeRange: 'last-1h'
+      })
+    ).rejects.toThrow('trace query unavailable');
+    expect(apiGet).toHaveBeenCalledTimes(1);
+  });
+
   it('builds the focused topology API URL from numeric entity route context', async () => {
     const apiGet = vi.fn(async () => ({
       apiBacked: true,
@@ -26,14 +157,101 @@ describe('topology API controller', () => {
     });
 
     expect(apiGet).toHaveBeenCalledWith(
-      '/topology?focusEntityId=501&depth=1&environment=prod&sourceKind=otlp-trace-call&relationType=trace-call&hideInternal=true&pageIndex=2&pageSize=50&start=1710000000000&end=1710003600000'
+      '/topology?focusEntityId=501&depth=1&environment=prod&relationType=trace-call&hideInternal=true&pageIndex=2&pageSize=50&start=1710000000000&end=1710003600000'
     );
     expect(buildTopologyApiUrl({ entityId: 'service:commerce/checkout', environment: 'prod' })).toBe(
       '/topology?depth=2&environment=prod'
     );
   });
 
-  it('defaults service-call topology API reads to trace-call relation scope', async () => {
+  it('maps the local Greptime scale proof route to the seeded focused gateway API scope', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          timeRange: 'last-7d',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          scaleProof: 'greptime-real'
+        })
+      ).toBe(
+        '/topology?focusEntityId=646562420231424&depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000'
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('keeps the Greptime scale proof route on the seeded proof window after relative time moves forward', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-06-02T04:30:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          timeRange: 'last-7d',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          scaleProof: 'greptime-real'
+        })
+      ).toBe(
+        '/topology?focusEntityId=646562420231424&depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000'
+      );
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('keeps the mixed star/mesh scale proof on the global Greptime API scope', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-05-31T10:00:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          timeRange: 'last-7d',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          scaleProof: 'mixed-star-mesh'
+        })
+      ).toBe('/topology?depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('keeps the mixed star/mesh scale proof on the seeded proof window after relative time moves forward', async () => {
+    vi.useFakeTimers();
+    vi.setSystemTime(new Date('2026-06-02T04:30:00.000Z'));
+    try {
+      expect(
+        buildTopologyApiUrl({
+          environment: 'prod',
+          timeRange: 'last-7d',
+          sourceKind: 'otlp-trace-call',
+          viewMode: 'service-call',
+          depth: '2',
+          scaleProof: 'mixed-star-mesh'
+        })
+      ).toBe('/topology?depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000');
+    } finally {
+      vi.useRealTimers();
+    }
+  });
+
+  it('marks non-numeric focused entity ids as unresolvable for the numeric topology API', () => {
+    expect(hasUnresolvableTopologyFocusEntity({ entityId: 'service:commerce/checkout' })).toBe(true);
+    expect(hasUnresolvableTopologyFocusEntity({ entityId: 'missing-topology-entity' })).toBe(true);
+    expect(hasUnresolvableTopologyFocusEntity({ entityId: '501' })).toBe(false);
+    expect(hasUnresolvableTopologyFocusEntity({})).toBe(false);
+  });
+
+  it('defaults service-call topology API reads to trace-call relation scope without hiding persisted OTLP relation evidence', async () => {
     const apiGet = vi.fn(async () => ({
       apiBacked: true,
       focusEntityId: null,
@@ -50,7 +268,7 @@ describe('topology API controller', () => {
     });
 
     expect(apiGet).toHaveBeenCalledWith(
-      '/topology?depth=2&environment=prod&sourceKind=otlp-trace-call&relationType=trace-call'
+      '/topology?depth=2&environment=prod&relationType=trace-call'
     );
     expect(buildTopologyApiUrl({ environment: 'prod', viewMode: 'service-call' })).toBe(
       '/topology?depth=2&environment=prod&relationType=trace-call'
@@ -71,7 +289,7 @@ describe('topology API controller', () => {
 
       await rejection;
       expect(apiGet).toHaveBeenCalledWith(
-        '/topology?depth=2&sourceKind=otlp-trace-call&relationType=trace-call',
+        '/topology?depth=2&relationType=trace-call',
         expect.objectContaining({ signal: expect.any(AbortSignal) })
       );
     } finally {
@@ -116,7 +334,7 @@ describe('topology API controller', () => {
 
     expect(apiGet).toHaveBeenCalledTimes(1);
     expect(apiGet).toHaveBeenCalledWith(
-      '/topology?depth=2&sourceKind=otlp-trace-call&relationType=trace-call',
+      '/topology?depth=2&relationType=trace-call',
       expect.objectContaining({ signal: expect.any(AbortSignal) })
     );
 
@@ -132,4 +350,132 @@ describe('topology API controller', () => {
     await expect(first).resolves.toMatchObject({ apiBacked: true, sourceKinds: ['otlp-trace-call'] });
     await expect(second).resolves.toMatchObject({ apiBacked: true, sourceKinds: ['otlp-trace-call'] });
   });
+
+  it('reuses a recently completed topology prefetch when the heavy page mounts after the route-client request resolved', async () => {
+    const graph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['cmdb-manual-label'],
+      nodes: [],
+      edges: []
+    };
+    const apiGet = vi.fn(async () => graph);
+
+    await expect(
+      loadTopologyGraph(apiGet, { sourceKind: 'cmdb-manual-label', viewMode: 'application' }, { timeoutMs: 60000 })
+    ).resolves.toBe(graph);
+    await expect(
+      loadTopologyGraph(apiGet, { sourceKind: 'cmdb-manual-label', viewMode: 'application' }, { timeoutMs: 60000 })
+    ).resolves.toBe(graph);
+
+    expect(apiGet).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not reuse completed topology reads for scale proof routes because Greptime proof data can be reseeded in place', async () => {
+    const staleGraph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['otlp-trace-call'],
+      nodes: [{ id: 'stale-node' }],
+      edges: [{ id: 'stale-edge' }]
+    };
+    const freshGraph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['otlp-trace-call'],
+      nodes: [{ id: 'fresh-node-a' }, { id: 'fresh-node-b' }],
+      edges: [{ id: 'fresh-edge-a' }, { id: 'fresh-edge-b' }]
+    };
+    const apiGet = vi.fn()
+      .mockResolvedValueOnce(staleGraph)
+      .mockResolvedValueOnce(freshGraph);
+    const context = {
+      environment: 'prod',
+      sourceKind: 'otlp-trace-call',
+      viewMode: 'service-call',
+      depth: '2',
+      scaleProof: 'mixed-star-mesh'
+    };
+
+    await expect(loadTopologyGraph(apiGet, context, { timeoutMs: 60000 })).resolves.toBe(staleGraph);
+    await expect(loadTopologyGraph(apiGet, context, { timeoutMs: 60000 })).resolves.toBe(freshGraph);
+
+    expect(apiGet).toHaveBeenCalledTimes(2);
+  });
+
+  it('cache-busts volatile scale proof fetches without changing the clean topology API request path', async () => {
+    const graph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['otlp-trace-call'],
+      nodes: [],
+      edges: []
+    };
+    const apiGet = vi.fn().mockResolvedValue(graph);
+    const context = {
+      environment: 'prod',
+      viewMode: 'service-call',
+      sourceKind: 'otlp-trace-call',
+      depth: '2',
+      scaleProof: 'mixed-star-mesh'
+    };
+
+    expect(buildTopologyApiUrl(context)).toBe(
+      '/topology?depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000'
+    );
+
+    await expect(loadTopologyGraph(apiGet, context, { cacheBust: () => 'proof-1' })).resolves.toBe(graph);
+
+    expect(apiGet).toHaveBeenCalledWith(
+      '/topology?depth=2&environment=prod&relationType=trace-call&start=1780344000000&end=1780352700000&_hbTopologyCacheBust=proof-1'
+    );
+  });
+
+  it('bypasses completed topology cache when an explicit refresh cache-bust is provided', async () => {
+    const staleGraph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['entity-relation'],
+      nodes: [{ id: 'stale-node' }],
+      edges: []
+    };
+    const freshGraph = {
+      apiBacked: true,
+      focusEntityId: null,
+      depth: 2,
+      sourceKinds: ['entity-relation'],
+      nodes: [{ id: 'fresh-node' }],
+      edges: []
+    };
+    const apiGet = vi.fn()
+      .mockResolvedValueOnce(staleGraph)
+      .mockResolvedValueOnce(freshGraph);
+    const context = {
+      environment: 'prod',
+      viewMode: 'application',
+      sourceKind: 'cmdb-manual-label'
+    };
+
+    await expect(loadTopologyGraph(apiGet, context, { timeoutMs: 60000 })).resolves.toBe(staleGraph);
+    await expect(loadTopologyGraph(apiGet, context, { timeoutMs: 60000, cacheBust: () => 'manual-1' })).resolves.toBe(freshGraph);
+
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label', expect.any(Object));
+    expect(apiGet).toHaveBeenNthCalledWith(
+      2,
+      '/topology?depth=2&environment=prod&sourceKind=cmdb-manual-label&_hbTopologyCacheBust=manual-1',
+      expect.any(Object)
+    );
+  });
+
+  it('does not preserve the previous graph while volatile scale proof and refresh reads are loading', () => {
+    expect(shouldPreservePreviousTopologyGraphDuringLoad({ sourceKind: 'cmdb-manual-label', viewMode: 'application' })).toBe(true);
+    expect(shouldPreservePreviousTopologyGraphDuringLoad({ scaleProof: 'mixed-star-mesh' })).toBe(false);
+    expect(shouldPreservePreviousTopologyGraphDuringLoad({ sourceKind: 'otlp-trace-call', refresh: '30' })).toBe(false);
+    expect(shouldPreservePreviousTopologyGraphDuringLoad({ sourceKind: 'otlp-trace-call', live: 'true' })).toBe(false);
+  });
 });
diff --git a/web-next/lib/topology-surface/controller.ts b/web-next/lib/topology-surface/controller.ts
index b6f05e7565..60af006b3f 100644
--- a/web-next/lib/topology-surface/controller.ts
+++ b/web-next/lib/topology-surface/controller.ts
@@ -1,18 +1,71 @@
 import type { EntityTopologyApiGraph, TopologyRouteContext } from './view-model';
+import { resolveTimeContextBounds } from '../time-context';
 
 export type TopologyApiGet = <T>(path: string, init?: RequestInit) => Promise<T>;
 
 export type LoadTopologyGraphOptions = {
   timeoutMs?: number;
+  cacheBust?: () => string;
 };
 
+type TopologyApiMessagePayload<T> = {
+  code?: number;
+  msg?: string;
+  data?: T;
+};
+
+export const TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS = 60000;
+export const CMDB_TOPOLOGY_API_TIMEOUT_MS = 60000;
+export const DEFAULT_TOPOLOGY_API_TIMEOUT_MS = 30000;
+const COMPLETED_TOPOLOGY_PREFETCH_CACHE_MS = 60000;
+const TOPOLOGY_GREPTIME_REAL_SCALE_PROOF_FOCUS_ENTITY_ID = '646562420231424';
+const TOPOLOGY_GREPTIME_REAL_SCALE_PROOF_API_POLICY = 'focused-real-greptime-scale-fixture';
+const TOPOLOGY_MIXED_STAR_MESH_SCALE_PROOF_API_POLICY = 'global-real-greptime-mixed-star-mesh-fixture';
+const TOPOLOGY_SCALE_PROOF_SEEDED_WINDOW_START_MS = '1780344000000';
+const TOPOLOGY_SCALE_PROOF_SEEDED_WINDOW_END_MS = '1780352700000';
+
 const inFlightTopologyGraphReads = new Map<string, Promise<EntityTopologyApiGraph>>();
+const completedTopologyGraphReads = new Map<string, { graph: EntityTopologyApiGraph; expiresAt: number }>();
+let volatileTopologyRequestSequence = 0;
 
 function normalizeNumericId(value: string | undefined) {
   const trimmed = value?.trim();
   return trimmed && /^\d+$/.test(trimmed) ? trimmed : undefined;
 }
 
+export function resolveTopologyScaleProofFocusEntityId(context: TopologyRouteContext = {}) {
+  const proof = context.scaleProof?.trim();
+  if (proof === 'greptime-real') return TOPOLOGY_GREPTIME_REAL_SCALE_PROOF_FOCUS_ENTITY_ID;
+  return undefined;
+}
+
+export function resolveTopologyScaleProofApiPolicy(context: TopologyRouteContext = {}) {
+  const proof = context.scaleProof?.trim();
+  if (proof === 'greptime-real') return TOPOLOGY_GREPTIME_REAL_SCALE_PROOF_API_POLICY;
+  if (proof === 'mixed-star-mesh') return TOPOLOGY_MIXED_STAR_MESH_SCALE_PROOF_API_POLICY;
+  return undefined;
+}
+
+export function resolveTopologyScaleProofTimeWindow(context: TopologyRouteContext = {}) {
+  const proof = context.scaleProof?.trim();
+  if (proof !== 'greptime-real' && proof !== 'mixed-star-mesh') return undefined;
+  if (normalizeNumericId(context.start) || normalizeNumericId(context.end)) return undefined;
+  return {
+    start: TOPOLOGY_SCALE_PROOF_SEEDED_WINDOW_START_MS,
+    end: TOPOLOGY_SCALE_PROOF_SEEDED_WINDOW_END_MS,
+    policy: 'fixed-seeded-greptime-window'
+  } as const;
+}
+
+function resolveTopologyApiFocusEntityId(context: TopologyRouteContext) {
+  return normalizeNumericId(context.entityId) ?? resolveTopologyScaleProofFocusEntityId(context);
+}
+
+export function hasUnresolvableTopologyFocusEntity(context: TopologyRouteContext = {}) {
+  const trimmed = context.entityId?.trim();
+  return Boolean(trimmed && !normalizeNumericId(trimmed));
+}
+
 function normalizeBooleanText(value: string | undefined) {
   const trimmed = value?.trim().toLowerCase();
   return trimmed === 'true' || trimmed === 'false' ? trimmed : undefined;
@@ -23,62 +76,225 @@ function appendContextParam(params: URLSearchParams, key: string, value: string
   if (trimmed) params.set(key, trimmed);
 }
 
+function resolveTopologyApiTimeBounds(context: TopologyRouteContext) {
+  const explicitStart = normalizeNumericId(context.start);
+  const explicitEnd = normalizeNumericId(context.end);
+  if (explicitStart && explicitEnd) return { start: explicitStart, end: explicitEnd };
+  const scaleProofWindow = resolveTopologyScaleProofTimeWindow(context);
+  if (scaleProofWindow) {
+    return {
+      start: scaleProofWindow.start,
+      end: scaleProofWindow.end
+    };
+  }
+
+  const resolved = resolveTimeContextBounds({
+    timeRange: context.timeRange,
+    from: context.from,
+    to: context.to,
+    start: context.start,
+    end: context.end,
+    refresh: context.refresh,
+    live: context.live,
+    tz: context.tz,
+    timezone: context.timezone
+  });
+  return {
+    start: explicitStart ?? resolved?.start,
+    end: explicitEnd ?? resolved?.end
+  };
+}
+
 export function resolveTopologyRelationType(context: TopologyRouteContext) {
   const explicitRelationType = context.relationType?.trim();
   if (explicitRelationType) return explicitRelationType;
-  if (context.sourceKind === 'otlp-trace-call' || context.viewMode === 'service-call') return 'trace-call';
+  if (context.viewMode === 'service-call') return 'trace-call';
   return undefined;
 }
 
+export function resolveTopologyApiTimeoutMs(routeContext?: TopologyRouteContext) {
+  if (routeContext?.sourceKind === 'otlp-trace-call' || routeContext?.viewMode === 'service-call') {
+    return TRACE_CALL_TOPOLOGY_API_TIMEOUT_MS;
+  }
+  if (routeContext?.sourceKind === 'cmdb-manual-label') {
+    return CMDB_TOPOLOGY_API_TIMEOUT_MS;
+  }
+  return DEFAULT_TOPOLOGY_API_TIMEOUT_MS;
+}
+
+function shouldApplyTopologyApiTimeBounds(context: TopologyRouteContext, relationType: string | undefined) {
+  const hasTimeScope = Boolean(
+    context.timeRange?.trim() ||
+      context.from?.trim() ||
+      context.to?.trim() ||
+      context.start?.trim() ||
+      context.end?.trim() ||
+      context.live?.trim() ||
+      context.refresh?.trim()
+  );
+  return (
+    context.sourceKind === 'otlp-trace-call' ||
+    context.viewMode === 'service-call' ||
+    relationType === 'trace-call' ||
+    (!context.sourceKind && hasTimeScope)
+  );
+}
+
+function resolveTopologyApiRetryCount(context: TopologyRouteContext, relationType: string | undefined) {
+  if (context.sourceKind === 'cmdb-manual-label' && !shouldApplyTopologyApiTimeBounds(context, relationType)) return 1;
+  return 0;
+}
+
+function shouldUseCompletedTopologyGraphCache(context: TopologyRouteContext) {
+  if (context.scaleProof?.trim()) return false;
+  if (context.refresh?.trim()) return false;
+  if (context.live?.trim() === 'true') return false;
+  return true;
+}
+
+export function shouldPreservePreviousTopologyGraphDuringLoad(context: TopologyRouteContext = {}) {
+  return shouldUseCompletedTopologyGraphCache(context);
+}
+
+function shouldForwardTopologyApiSourceKind(context: TopologyRouteContext) {
+  const relationType = resolveTopologyRelationType(context);
+  if (context.sourceKind === 'otlp-trace-call' && relationType === 'trace-call') {
+    return false;
+  }
+  return !(
+    context.sourceKind === 'otlp-trace-call' &&
+    context.viewMode !== 'service-call' &&
+    !context.relationType?.trim()
+  );
+}
+
 export function buildTopologyApiUrl(context: TopologyRouteContext = {}) {
   const params = new URLSearchParams();
-  const focusEntityId = normalizeNumericId(context.entityId);
+  const focusEntityId = resolveTopologyApiFocusEntityId(context);
+  const relationType = resolveTopologyRelationType(context);
+  const timeBounds = shouldApplyTopologyApiTimeBounds(context, relationType)
+    ? resolveTopologyApiTimeBounds(context)
+    : {};
   if (focusEntityId) params.set('focusEntityId', focusEntityId);
   params.set('depth', normalizeNumericId(context.depth) ?? '2');
   appendContextParam(params, 'environment', context.environment);
-  appendContextParam(params, 'sourceKind', context.sourceKind);
-  appendContextParam(params, 'relationType', resolveTopologyRelationType(context));
+  appendContextParam(params, 'sourceKind', shouldForwardTopologyApiSourceKind(context) ? context.sourceKind : undefined);
+  appendContextParam(params, 'relationType', relationType);
   appendContextParam(params, 'hideInternal', normalizeBooleanText(context.hideInternal));
   appendContextParam(params, 'pageIndex', normalizeNumericId(context.pageIndex));
   appendContextParam(params, 'pageSize', normalizeNumericId(context.pageSize));
-  appendContextParam(params, 'start', normalizeNumericId(context.start));
-  appendContextParam(params, 'end', normalizeNumericId(context.end));
+  appendContextParam(params, 'start', timeBounds.start);
+  appendContextParam(params, 'end', timeBounds.end);
   return `/topology?${params.toString()}`;
 }
 
+function buildTopologyApiFetchUrl(url: string, useCompletedCache: boolean, explicitCacheBust: string | undefined) {
+  if (useCompletedCache) return url;
+  const [pathname, query = ''] = url.split('?');
+  const params = new URLSearchParams(query);
+  volatileTopologyRequestSequence += 1;
+  params.set('_hbTopologyCacheBust', explicitCacheBust || `${Date.now()}-${volatileTopologyRequestSequence}`);
+  return `${pathname}?${params.toString()}`;
+}
+
 export async function loadTopologyGraph(
   apiGet: TopologyApiGet,
   context: TopologyRouteContext = {},
   options: LoadTopologyGraphOptions = {}
 ) {
   const url = buildTopologyApiUrl(context);
+  const relationType = resolveTopologyRelationType(context);
+  const retryCount = resolveTopologyApiRetryCount(context, relationType);
   const timeoutMs = options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 0;
-  const inFlightKey = `${url}::${timeoutMs}`;
+  const explicitCacheBust = options.cacheBust?.().trim() || undefined;
+  const useCompletedCache = shouldUseCompletedTopologyGraphCache(context) && !explicitCacheBust;
+  const fetchUrl = buildTopologyApiFetchUrl(url, useCompletedCache, explicitCacheBust);
+  const inFlightKey = `${url}::${timeoutMs}::${retryCount}${explicitCacheBust ? `::${explicitCacheBust}` : ''}`;
   const inFlight = inFlightTopologyGraphReads.get(inFlightKey);
   if (inFlight) return inFlight;
+  if (useCompletedCache) {
+    const completed = completedTopologyGraphReads.get(inFlightKey);
+    if (completed) {
+      if (completed.expiresAt > Date.now()) return completed.graph;
+      completedTopologyGraphReads.delete(inFlightKey);
+    }
+  } else {
+    completedTopologyGraphReads.delete(inFlightKey);
+  }
 
   const request = (async () => {
-    if (!timeoutMs) return apiGet<EntityTopologyApiGraph>(url);
-
-    const controller = new AbortController();
-    const apiRequest = apiGet<EntityTopologyApiGraph>(url, { signal: controller.signal });
-    let timeoutId: ReturnType<typeof setTimeout> | undefined;
-    const timeout = new Promise<never>((_, reject) => {
-      timeoutId = setTimeout(() => {
-        controller.abort();
-        reject(new Error(`Topology API request timed out after ${timeoutMs}ms`));
-      }, timeoutMs);
-    });
-
-    try {
-      return await Promise.race([apiRequest, timeout]);
-    } finally {
-      if (timeoutId) clearTimeout(timeoutId);
+    const readOnce = () => {
+      if (!timeoutMs) return apiGet<EntityTopologyApiGraph>(fetchUrl);
+
+      const controller = new AbortController();
+      const apiRequest = apiGet<EntityTopologyApiGraph>(fetchUrl, { signal: controller.signal });
+      let timeoutId: ReturnType<typeof setTimeout> | undefined;
+      const timeout = new Promise<never>((_, reject) => {
+        timeoutId = setTimeout(() => {
+          controller.abort();
+          reject(new Error(`Topology API request timed out after ${timeoutMs}ms`));
+        }, timeoutMs);
+      });
+
+      return Promise.race([apiRequest, timeout]).finally(() => {
+        if (timeoutId) clearTimeout(timeoutId);
+      });
+    };
+
+    let lastError: unknown;
+    for (let attempt = 0; attempt <= retryCount; attempt += 1) {
+      try {
+        return await readOnce();
+      } catch (error) {
+        lastError = error;
+      }
+    }
+    throw lastError;
+  })().then(graph => {
+    if (useCompletedCache) {
+      completedTopologyGraphReads.set(inFlightKey, {
+        graph,
+        expiresAt: Date.now() + COMPLETED_TOPOLOGY_PREFETCH_CACHE_MS
+      });
+    } else {
+      completedTopologyGraphReads.delete(inFlightKey);
     }
-  })().finally(() => {
+    return graph;
+  }).finally(() => {
     inFlightTopologyGraphReads.delete(inFlightKey);
   });
 
   inFlightTopologyGraphReads.set(inFlightKey, request);
   return request;
 }
+
+async function fetchTopologyApiMessage<T>(path: string, init: RequestInit = {}) {
+  const locale = typeof window === 'undefined'
+    ? null
+    : window.localStorage.getItem('hb.lang') || window.localStorage.getItem('layout.lang');
+  const response = await fetch(path.startsWith('/api') ? path : `/api${path}`, {
+    ...init,
+    headers: {
+      ...(locale ? { 'Accept-Language': locale } : {}),
+      ...((init.headers as Record<string, string> | undefined) || {})
+    },
+    credentials: 'same-origin',
+    cache: 'no-store'
+  });
+  if (!response.ok) {
+    throw new Error(`Topology API request failed: ${response.status}`);
+  }
+  const payload = await response.json() as TopologyApiMessagePayload<T>;
+  if (payload.code !== 0) {
+    throw new Error(payload.msg || 'Topology API request failed');
+  }
+  return payload.data as T;
+}
+
+export function preloadTopologyGraph(
+  context: TopologyRouteContext = {},
+  options: LoadTopologyGraphOptions = {}
+) {
+  if (typeof window === 'undefined') return undefined;
+  return loadTopologyGraph(fetchTopologyApiMessage, context, options).catch(() => undefined);
+}
diff --git a/web-next/lib/topology-surface/query-state.test.ts b/web-next/lib/topology-surface/query-state.test.ts
index de13211c13..abf256ff66 100644
--- a/web-next/lib/topology-surface/query-state.test.ts
+++ b/web-next/lib/topology-surface/query-state.test.ts
@@ -24,6 +24,8 @@ describe('topology query state', () => {
         viewMode: ['resource-dependency', 'service-call'],
         sourceKind: ['database-middleware-connection', 'alert-impact'],
         groupBy: ['source-kind', 'environment'],
+        scaleProof: ['greptime-real', 'ignored-scale-proof'],
+        search: ['Service 420', 'ignored-search'],
         edgeId: ['svc-checkout--res-orders-db', 'ignored-edge'],
         returnTo: ['/alert?status=firing&returnLabel=告警']
       })
@@ -47,6 +49,8 @@ describe('topology query state', () => {
       viewMode: 'resource-dependency',
       sourceKind: 'database-middleware-connection',
       groupBy: 'source-kind',
+      scaleProof: 'greptime-real',
+      search: 'Service 420',
       edgeId: 'svc-checkout--res-orders-db',
       returnTo: '/alert?status=firing'
     });
diff --git a/web-next/lib/topology-surface/query-state.ts b/web-next/lib/topology-surface/query-state.ts
index 44813eed93..c1b200d8b9 100644
--- a/web-next/lib/topology-surface/query-state.ts
+++ b/web-next/lib/topology-surface/query-state.ts
@@ -19,6 +19,8 @@ export function readTopologyRouteContext(searchParams: TopologySearchParams = {}
     viewMode: firstParam(searchParams.viewMode),
     sourceKind: firstParam(searchParams.sourceKind),
     groupBy: firstParam(searchParams.groupBy),
+    scaleProof: firstParam(searchParams.scaleProof),
+    search: firstParam(searchParams.search),
     edgeId: firstParam(searchParams.edgeId)
   };
 }
diff --git a/web-next/lib/topology-surface/view-model.test.ts b/web-next/lib/topology-surface/view-model.test.ts
index ccfa092a9d..0ad883784a 100644
--- a/web-next/lib/topology-surface/view-model.test.ts
+++ b/web-next/lib/topology-surface/view-model.test.ts
@@ -670,7 +670,8 @@ describe('topology surface config', () => {
       focus: 'context-muted'
     });
     expect(model.nodes.find(node => node.id === 'svc-checkout')).toMatchObject({ focus: 'related' });
-    expect(model.nodes.find(node => node.id === 'svc-frontend')).toMatchObject({ focus: 'related' });
+    expect(model.nodes.find(node => node.id === 'res-orders-db')).toMatchObject({ focus: 'related' });
+    expect(model.nodes.find(node => node.id === 'svc-frontend')).toMatchObject({ focus: 'dimmed' });
     expect(model.nodes.find(node => node.id === 'k8s-checkout-workload')).toMatchObject({ focus: 'dimmed' });
   });
 
diff --git a/web-next/lib/topology-surface/view-model.ts b/web-next/lib/topology-surface/view-model.ts
index 6da093089e..093203563c 100644
--- a/web-next/lib/topology-surface/view-model.ts
+++ b/web-next/lib/topology-surface/view-model.ts
@@ -242,6 +242,8 @@ export type TopologyRouteContext = SignalRouteContext & {
   hideInternal?: string;
   pageIndex?: string;
   pageSize?: string;
+  scaleProof?: string;
+  search?: string;
 };
 
 type EntityTopologyApiRedMetrics = Partial<Record<keyof TopologyRedMetrics, number | string | null | undefined>>;
@@ -622,7 +624,7 @@ function edgeMatchesViewMode(edge: TopologyServiceEdgeSeed, viewMode: TopologyVi
 
 function edgeMatchesSource(edge: TopologyServiceEdgeSeed, sourceKind: TopologySourceKind | undefined) {
   if (!sourceKind) return true;
-  if (sourceKind === 'alert-impact') return edge.alertImpact !== 'none';
+  if (sourceKind === 'alert-impact') return edge.source === 'alert-impact' || (edge.source !== 'otlp-trace-call' && edge.alertImpact !== 'none');
   return edge.source === sourceKind;
 }
 
diff --git a/web-next/lib/types.ts b/web-next/lib/types.ts
index cfbb6e2c3f..675f19b66b 100644
--- a/web-next/lib/types.ts
+++ b/web-next/lib/types.ts
@@ -510,12 +510,25 @@ export interface EntityDetailDto {
   logSummary?: EntityLogSummary;
   traceSummary?: EntityTraceSummary;
   unifiedEvidenceSummary?: EntityUnifiedEvidenceSummary;
+  signalEvidence?: EntitySignalEvidenceBundle;
   boundMonitors?: Monitor[];
   activeAlerts?: unknown[];
   nextActions?: EntityNextAction[];
   noiseControlSummary?: EntityNoiseControlSummary;
 }
 
+export interface EntitySignalEvidenceBundle {
+  logSummary?: EntityLogSummary;
+  traceSummary?: EntityTraceSummary & { latestSpanId?: string | null };
+  metricEvidence?: unknown[];
+  logEvidence?: unknown[];
+  traceEvidence?: unknown[];
+  logQueryHints?: unknown[];
+  traceQueryHints?: unknown[];
+  unifiedEvidenceSummary?: EntityUnifiedEvidenceSummary;
+  triageRecommendation?: unknown;
+}
+
 export interface EntityUnifiedEvidenceSummary {
   activeSignalCount?: number;
   metricsActive?: boolean;
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index 4de18901b4..223bf42d3e 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -4497,6 +4497,8 @@ describe('@hertzbeat/ui', () => {
           totalNodeCount: 500,
           renderedNodeCount: 200,
           hiddenNodeCount: 300,
+          totalEdgeCount: 2,
+          renderedEdgeCount: 1,
           visibleNodeBudget: 200,
           tableCompanion: 'required',
           priorityNodeIds: ['payment-api'],
@@ -4508,6 +4510,7 @@ describe('@hertzbeat/ui', () => {
           errorRate: 'error ratio',
           latencyP95: 'p95 latency',
           rowAction: 'Inspect edge',
+          renderWindowEdgeSummary: 'Canvas renders 1 of 2 table edges',
           rowAriaLabel: row => `Inspect relation ${row.id}`
         }}
         rows={[
@@ -4549,6 +4552,7 @@ describe('@hertzbeat/ui', () => {
 
     expect(html).toContain('data-hz-ui="topology-metric-table"');
     expect(html).toContain('data-hz-topology-primitive="metric-table"');
+    expect(html).toContain('data-hz-topology-metric-table-root="true"');
     expect(html).toContain('data-hz-topology-metric-table-density="graph-first"');
     expect(html).toContain('data-hz-topology-metric-table-density-owner="hertzbeat-ui-metric-table-density"');
     expect(html).toContain('data-hz-topology-metric-table-visual-weight="low-interruption"');
@@ -4559,12 +4563,24 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-metric-table-title-owner="hertzbeat-ui-metric-table-title"');
     expect(html).toContain('data-hz-topology-metric-table-count-owner="hertzbeat-ui-metric-table-count"');
     expect(html).toContain('data-hz-topology-metric-rows="2"');
+    expect(html).toContain('data-hz-topology-metric-table-total-rows="2"');
     expect(html).toContain('data-hz-topology-metric-table-interaction="row-select-detail"');
+    expect(html).toContain('data-hz-topology-metric-table-live-selection-owner="hertzbeat-ui-metric-table-selection"');
+    expect(html).toContain(
+      'data-hz-topology-metric-table-live-selection-invariants="row-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"'
+    );
     expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="500"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-node-count="200"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-hidden-node-count="300"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-total-edge-count="2"');
+    expect(html).toContain('data-hz-topology-metric-table-render-window-rendered-edge-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-edge-count-policy="canvas-rendered-vs-table-total"');
+    expect(html).toContain('data-hz-topology-metric-table-canvas-rendered-edge-count="1"');
+    expect(html).toContain('data-hz-topology-metric-table-table-edge-count="2"');
+    expect(html).toContain('data-hz-topology-metric-table-edge-summary-owner="hertzbeat-ui-metric-table-edge-summary"');
+    expect(html).toContain('Canvas renders 1 of 2 table edges');
     expect(html).toContain('data-hz-topology-metric-table-render-window-visible-node-budget="200"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-visible-node-count="2"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="required"');
@@ -4575,16 +4591,24 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-metric-table-unknown-row-count="0"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-filter="all"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-metric-table-filtered-row-count="2"');
     expect(html).toContain('data-hz-topology-metric-table-filtered-out-row-count="0"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control-owner="hertzbeat-ui-metric-table-filter-control"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-selection-policy="preserve-selected-edge"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="all"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="visible"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
     expect(html).toContain('data-hz-topology-metric-table-filter-active="true"');
+    expect(html).toContain('data-hz-topology-metric-table-filter-control-active="true"');
     expect(html).toContain('data-hz-topology-metric-table-filter-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-owner="hertzbeat-ui-metric-table-row-selection"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-mode="table-row-click-drawer"');
+    expect(html).toContain('data-hz-topology-edge-row-selection-url-policy="preserve-current-url"');
     expect(html).toContain('data-hz-topology-edge-row="checkout-payment"');
     expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="visible"');
     expect(html).toContain('data-hz-topology-edge-row-source-node-id="checkout-api"');
@@ -4595,6 +4619,9 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="partial"');
     expect(html).toContain('data-hz-topology-edge-row-source-visible="true"');
     expect(html).toContain('data-hz-topology-edge-row-target-visible="false"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context="partial"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-source-visible="true"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-target-visible="false"');
     expect(html).toContain('data-hz-topology-edge-selected="true"');
     expect(html).toContain('aria-current="true"');
     expect(html).toContain('data-hz-topology-metric-table-endpoints-owner="hertzbeat-ui-metric-table-endpoints"');
@@ -4632,6 +4659,7 @@ describe('@hertzbeat/ui', () => {
     const html = renderToStaticMarkup(
       <HzTopologyMetricTable
         title="Topology edges"
+        selectedRowId="payment-db"
         renderWindowFilter="partial"
         renderWindowCompanion={{
           mode: 'windowed',
@@ -4642,6 +4670,9 @@ describe('@hertzbeat/ui', () => {
           tableCompanion: 'required',
           renderedNodeIds: ['checkout-api', 'payment-api']
         }}
+        labels={{
+          renderWindowFilterPartial: 'One endpoint on canvas'
+        }}
         rows={[
           {
             id: 'checkout-payment',
@@ -4680,10 +4711,81 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-metric-table-filtered-out-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-filter-control="partial"');
     expect(html).toMatch(/data-hz-topology-metric-table-filter-control="partial"[^>]+data-hz-topology-metric-table-filter-active="true"/);
+    expect(html).toMatch(/data-hz-topology-metric-table-filter-control="partial"[^>]+data-hz-topology-metric-table-filter-control-active="true"/);
     expect(html).toContain('data-hz-topology-edge-row="payment-db"');
+    expect(html).toContain('data-hz-topology-edge-selected="true"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-owner="hertzbeat-ui-metric-table-row-window-context"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context="partial"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-source-visible="true"');
+    expect(html).toContain('data-hz-topology-edge-row-window-context-target-visible="false"');
+    expect(html).toContain('One endpoint on canvas');
     expect(html).not.toContain('data-hz-topology-edge-row="checkout-payment"');
   });
 
+  it('budgets dense windowed topology metric table rows without losing counts or selected context', () => {
+    const rows = Array.from({ length: 150 }, (_, index) => ({
+      id: `edge-${index}`,
+      sourceNodeId: index % 2 === 0 ? `rendered-${index}` : `hidden-${index}`,
+      targetNodeId: `hidden-target-${index}`,
+      source: `source-${index}`,
+      target: `target-${index}`,
+      relationType: 'trace-call',
+      sourceKind: 'otlp-trace-call',
+      requestRatePerSecond: 1 + index,
+      requestCount: 10 + index,
+      errorRate: 0,
+      latencyP95Ms: 40 + index,
+      tone: 'neutral' as const
+    }));
+    const html = renderToStaticMarkup(
+      <HzTopologyMetricTable
+        title="Topology edges"
+        selectedRowId="edge-149"
+        rows={rows}
+        renderWindowCompanion={{
+          mode: 'windowed',
+          totalNodeCount: 500,
+          renderedNodeCount: 200,
+          hiddenNodeCount: 300,
+          totalEdgeCount: 150,
+          renderedEdgeCount: 80,
+          visibleNodeBudget: 200,
+          tableCompanion: 'required',
+          renderedNodeIds: rows.slice(0, 80).map(row => row.sourceNodeId ?? '')
+        }}
+      />
+    );
+
+    const renderedRows = html.match(/data-hz-topology-edge-row="/g) ?? [];
+    expect(html).toContain('data-hz-topology-metric-rows="150"');
+    expect(html).toContain('data-hz-topology-metric-table-filtered-row-count="150"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-policy="windowed-dom-budget"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-budget="120"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-page="1"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-next-count="150"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-can-show-more="true"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-count="70"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-owner="hertzbeat-ui-metric-table-hidden-row-proof"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof="available"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-filter="hidden"');
+    expect(html).toContain('data-hz-topology-metric-table-hidden-row-proof-count="70"');
+    expect(html).toContain('data-hz-topology-metric-table-rendered-row-count="121"');
+    expect(html).toContain('data-hz-topology-metric-table-rendered-hidden-row-count="29"');
+    expect(renderedRows).toHaveLength(121);
+    expect(html).toContain('data-hz-topology-edge-row="edge-0"');
+    expect(html).toContain('data-hz-topology-edge-row="edge-119"');
+    expect(html).not.toContain('data-hz-topology-edge-row="edge-120"');
+    expect(html).toContain('data-hz-topology-edge-row="edge-149"');
+    expect(html).toContain('data-hz-topology-edge-selected="true"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-summary-owner="hertzbeat-ui-metric-table-row-render-summary"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-action-owner="hertzbeat-ui-metric-table-row-render-action"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-action="show-more"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-action-url-policy="preserve-current-url"');
+    expect(html).toContain('data-hz-topology-metric-table-row-render-action-selection-policy="preserve-selected-edge"');
+    expect(html).toContain('Showing 121 of 150 rows');
+    expect(html).toContain('Show 150 of 150 rows');
+  });
+
   it('renders a shared topology companion rail for table, timeline, and drawer layout ownership', () => {
     const html = renderToStaticMarkup(
       <HzTopologyCompanionRail
@@ -4885,8 +4987,8 @@ describe('@hertzbeat/ui', () => {
           sourceSlot={<div data-demo-section="source">Sources</div>}
         />
         <HzTopologyWorkbenchGrid data-testid="topology-workbench-grid">
-          <section data-demo-section="canvas">Canvas</section>
-          <section data-demo-section="companion">Companion</section>
+          <HzTopologyWorkbenchSlot kind="canvas" data-demo-section="canvas">Canvas</HzTopologyWorkbenchSlot>
+          <HzTopologyWorkbenchSlot kind="companion" data-demo-section="companion">Companion</HzTopologyWorkbenchSlot>
         </HzTopologyWorkbenchGrid>
       </HzTopologyWorkbenchFrame>
     );
@@ -4924,6 +5026,9 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-primitive="workbench-grid"');
     expect(html).toContain('data-hz-topology-workbench-grid-owner="hertzbeat-ui-workbench-grid"');
     expect(html).toContain('data-hz-topology-workbench-grid-layout="canvas-companion"');
+    expect(html).toContain('lg:grid-cols-[minmax(0,1fr)_320px]');
+    expect(html).toContain('lg:sticky');
+    expect(html).toContain('lg:top-[64px]');
     expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness="sticky-with-companion"');
     expect(html).toContain('data-hz-topology-workbench-grid-canvas-stickiness-owner="hertzbeat-ui-workbench-grid-canvas-stickiness"');
     expect(html).toContain('data-demo-section="canvas"');
@@ -5131,6 +5236,7 @@ describe('@hertzbeat/ui', () => {
   });
 
   it('renders a compact topology legend for health, source, confidence, and stale evidence semantics', () => {
+    const source = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
     const html = renderToStaticMarkup(
       <HzTopologyLegend
         title="Topology legend"
@@ -5187,6 +5293,8 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-legend-pattern="dashed"');
     expect(html).toContain('Topology legend');
     expect(html).toContain('Current evidence');
+    expect(source).not.toContain("swatch?: 'line'");
+    expect(source).not.toContain('data-hz-topology-legend-swatch-owner');
     expect(html).not.toContain('rounded-[16px]');
     expect(html).not.toContain('rounded-[14px]');
     expect(html).not.toContain('rounded-[12px]');
@@ -5208,7 +5316,7 @@ describe('@hertzbeat/ui', () => {
 	                label: 'Service node',
 	                iconSrc: 'data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%3E%3C/svg%3E',
 	                iconLibrary: 'lucide-react',
-	                iconName: 'component',
+	                iconName: 'server-cog',
 	                iconSource: 'entity-type-catalog',
 	                visualSource: 'lucide-react'
 	              },
@@ -5241,7 +5349,7 @@ describe('@hertzbeat/ui', () => {
 	    expect(html).toContain('data-hz-topology-legend-source-label="lucide-react"');
 	    expect(html).toContain('data-hz-topology-legend-icon-owner="hertzbeat-ui-legend-source-icon"');
 	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="server-cog"');
 	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
 	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
 	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
@@ -5338,6 +5446,13 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
     expect(html).toContain('data-hz-topology-detail-fact-density="compressed"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-key="edge:checkout-orders:svc-checkout:db-orders:trace-call:otlp-trace-call:unknown"');
     expect(html).toContain('data-hz-topology-detail-surface="framed"');
     expect(html).toContain('data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"');
     expect(html).toContain('data-hz-topology-detail-identity-owner="hertzbeat-ui-detail-identity"');
@@ -5414,6 +5529,16 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-detail-density="graph-first"');
     expect(html).toContain('data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"');
     expect(html).toContain('data-hz-topology-detail-visual-weight="low-interruption"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit="compact-side-rail"');
+    expect(html).toContain('data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"');
+    expect(html).toContain('data-hz-topology-detail-rail-max-block="bounded-560px"');
+    expect(html).toContain('data-hz-topology-detail-overflow-policy="internal-scroll"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset="identity-change"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"');
+    expect(html).toContain('data-hz-topology-detail-scroll-reset-key="node:svc-checkout:none:none:unknown:otlp-trace-call:service"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement="header-dock"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-placement-owner="hertzbeat-ui-detail-signal-action-placement"');
+    expect(html).toContain('data-hz-topology-detail-signal-action-sticky="top-with-header-context"');
     expect(html).toContain('data-hz-topology-detail-surface="flush"');
     expect(html).toContain('data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"');
     expect(html).toContain('data-hz-topology-detail-identity-owner="hertzbeat-ui-detail-identity"');
@@ -5446,6 +5571,12 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-detail-signal-action="metrics"');
     expect(html).toContain('data-hz-topology-detail-signal-action="logs"');
     expect(html).toContain('data-hz-topology-detail-signal-action="traces"');
+    expect(html.indexOf('data-hz-topology-detail-actions="signals"')).toBeLessThan(
+      html.indexOf('data-hz-topology-detail-boundary="context"')
+    );
+    expect(html.indexOf('data-hz-topology-detail-actions="signals"')).toBeLessThan(
+      html.indexOf('data-hz-topology-detail-fact-group-owner="hertzbeat-ui-detail-fact-group"')
+    );
   });
 
   it('renders a compact topology filter strip for source and focus controls', () => {
@@ -5872,6 +6003,10 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-path-summary-boundary="section"');
     expect(html).toContain('data-hz-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"');
     expect(html).toContain('data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"');
+    expect(html).toContain('data-topology-path-summary-interaction-state="selected"');
+    expect(html).toContain('data-hz-topology-path-summary-interaction-state="selected"');
+    expect(html).toContain('data-topology-path-summary-selected-edge-id="checkout-orders"');
+    expect(html).toContain('data-topology-path-summary-hovered-edge-id="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-selected-edge="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-hovered-edge="checkout-orders"');
     expect(html).toContain('data-hz-topology-path-source-id="svc-checkout"');
@@ -6004,6 +6139,26 @@ describe('@hertzbeat/ui', () => {
     expect(html).not.toContain('rounded-[12px]');
   });
 
+  it('renders canvas topology empty states without an extra framed card surface', () => {
+    const html = renderToStaticMarkup(
+      <HzTopologyEmptyState
+        title="No topology evidence"
+        copy="No trace edges were found."
+        sourceLabel="Greptime trace graph"
+        timeScope="last-1h"
+        boundary="canvas"
+        placement="canvas-center"
+        copyVisibility="assistive"
+      />
+    );
+
+    expect(html).toContain('data-hz-topology-empty-boundary="canvas"');
+    expect(html).toContain('data-hz-topology-empty-boundary-visual="frameless-canvas"');
+    expect(html).toContain('bg-transparent');
+    expect(html).toContain('shadow-none');
+    expect(html).not.toContain('border border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] shadow-[0_16px_48px_rgba(0,0,0,0.28)]');
+  });
+
   it('renders a compact topology loading state owned by the shared UI lab primitive', () => {
     const html = renderToStaticMarkup(
       <HzTopologyLoadingState
@@ -6101,11 +6256,13 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-toolbar-select-padding="compact-flush"');
     expect(html).toContain('data-hz-topology-toolbar-row-separator="none"');
     expect(html).toContain('data-hz-topology-toolbar-control-gap="6px"');
-    expect(html).toContain('[grid-template-columns:132px_minmax(220px,1fr)_132px_auto]');
+    expect(html).toContain('data-hz-topology-toolbar-control-flow="single-grid-row"');
+    expect(html).toContain('data-hz-topology-toolbar-empty-offset="none"');
+    expect(html).toContain('[grid-template-columns:112px_minmax(260px,1fr)_148px_88px_132px_auto]');
     expect(html).not.toContain('overflow-x-auto px-4 py-1');
     expect(html).toContain('overflow-x-auto px-0 py-1');
-    expect(html).toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
-    expect(html).not.toContain('h-7 !pl-2 !pr-1.5');
+    expect(html).toContain('h-7 !gap-1.5 !px-2');
+    expect(html).not.toContain('h-7 !gap-1.5 !pl-1 !pr-1.5');
     expect(html).toContain('data-hz-topology-toolbar-action-policy="scope-controls-only"');
     expect(html).toContain('data-hz-topology-toolbar-canvas-action-policy="in-canvas-g6-toolbar"');
     expect(html).toContain('data-hz-topology-toolbar-chrome="frameless"');
@@ -6128,6 +6285,7 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-toolbar-control-strip="source-depth-group-reset"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-owner="hertzbeat-ui-toolbar-control-strip"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout="inline-overflow"');
+    expect(html).toContain('data-hz-topology-toolbar-control-strip-display="contents"');
     expect(html).toContain('data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"');
     expect(html).toContain('data-hz-topology-control="depth"');
     expect(html).toContain('data-hz-topology-control-depth-owner="hertzbeat-ui-toolbar-depth-control"');
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index 68ccfb32f6..5208a8eb90 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -10492,7 +10492,6 @@ export type HzTopologyLegendItem = {
   pattern?: 'solid' | 'dashed' | 'muted';
   color?: string;
   fill?: string;
-  swatch?: 'line';
   visualSource?: 'hertzbeat-status-token' | 'hertzbeat-interaction-token' | 'hertzbeat-edge-token' | 'lucide-react';
   iconSrc?: string;
   iconAlt?: string;
@@ -10781,6 +10780,7 @@ export type HzTopologyPathSummaryAction = Omit<React.AnchorHTMLAttributes<HTMLAn
 };
 
 export type HzTopologyPathSummaryBoundary = 'none' | 'section' | 'framed' | 'flush';
+export type HzTopologyPathSummaryInteractionState = 'preview' | 'hovered' | 'selected';
 
 export type HzTopologyPathSummaryProps = React.HTMLAttributes<HTMLElement> & {
   title: React.ReactNode;
@@ -10792,6 +10792,7 @@ export type HzTopologyPathSummaryProps = React.HTMLAttributes<HTMLElement> & {
   evidenceBadges?: string[];
   actions?: HzTopologyPathSummaryAction[];
   boundary?: HzTopologyPathSummaryBoundary;
+  interactionState?: HzTopologyPathSummaryInteractionState;
   selectedEdgeId?: string;
   hoveredEdgeId?: string;
   sourceId?: string;
@@ -11450,13 +11451,58 @@ export function HzTopologyDetailDrawer({
 }: HzTopologyDetailDrawerProps) {
   const { className: boundaryClassName, ...boundaryRestProps } = boundaryProps ?? {};
   const graphFirst = density === 'graph-first';
+  const scrollResetKey = `${kind}:${subjectId ?? 'none'}:${sourceId ?? 'none'}:${targetId ?? 'none'}:${relationType ?? 'unknown'}:${sourceKind ?? 'unknown'}:${entityType ?? 'unknown'}`;
+  const drawerRef = React.useRef<HTMLElement | null>(null);
+  const previousScrollResetKeyRef = React.useRef(scrollResetKey);
+
+  React.useEffect(() => {
+    const drawer = drawerRef.current;
+
+    if (!drawer || previousScrollResetKeyRef.current === scrollResetKey) {
+      return;
+    }
+
+    drawer.scrollTop = 0;
+    previousScrollResetKeyRef.current = scrollResetKey;
+  }, [scrollResetKey]);
+
+  const signalActionGroup =
+    signalActions.length > 0 ? (
+      <div
+        className={cn(
+          'grid',
+          graphFirst
+            ? 'sticky top-0 z-10 -mx-2 mt-2 gap-1 border-y border-[#252832] bg-[#0b0c0f] px-2 py-2'
+            : 'mt-3 gap-2'
+        )}
+        data-hz-topology-detail-actions="signals"
+        data-hz-topology-detail-signal-action-group-owner="hertzbeat-ui-detail-signal-action-group"
+        data-hz-topology-detail-signal-action-placement={graphFirst ? 'header-dock' : 'footer'}
+        data-hz-topology-detail-signal-action-placement-owner="hertzbeat-ui-detail-signal-action-placement"
+        data-hz-topology-detail-signal-action-sticky={graphFirst ? 'top-with-header-context' : 'none'}
+      >
+        {signalActionsLabel ? (
+          <div
+            className="truncate text-[10px] font-semibold uppercase tracking-[0.08em] text-[#727b8c]"
+            data-hz-topology-detail-signal-label="signals"
+            data-hz-topology-detail-signal-label-owner="hertzbeat-ui-detail-signal-label"
+          >
+            {signalActionsLabel}
+          </div>
+        ) : null}
+        {signalActions.map(action => (
+          <HzTopologyDetailDrawerActionLink key={action.id} action={action} attributeName="data-hz-topology-detail-signal-action" />
+        ))}
+      </div>
+    ) : null;
 
   return (
     <section
       {...props}
+      ref={drawerRef}
       className={cn(
         'min-w-0',
-        graphFirst ? 'bg-[#0b0c0f] p-2 text-[12px]' : 'bg-[#101217] p-3',
+        graphFirst ? 'hb-scrollbar max-h-[560px] overflow-y-auto overscroll-contain bg-[#0b0c0f] p-2 text-[12px]' : 'bg-[#101217] p-3',
         topologyDetailDrawerSurfaceClassName[surface],
         className
       )}
@@ -11467,6 +11513,13 @@ export function HzTopologyDetailDrawer({
       data-hz-topology-detail-density-owner="hertzbeat-ui-detail-density"
       data-hz-topology-detail-visual-weight={graphFirst ? 'low-interruption' : 'balanced'}
       data-hz-topology-detail-visual-weight-owner="hertzbeat-ui-detail-visual-weight"
+      data-hz-topology-detail-rail-fit={graphFirst ? 'compact-side-rail' : 'standard'}
+      data-hz-topology-detail-rail-fit-owner="hertzbeat-ui-detail-rail-fit"
+      data-hz-topology-detail-rail-max-block={graphFirst ? 'bounded-560px' : 'unbounded'}
+      data-hz-topology-detail-overflow-policy={graphFirst ? 'internal-scroll' : 'document-flow'}
+      data-hz-topology-detail-scroll-reset={graphFirst ? 'identity-change' : 'none'}
+      data-hz-topology-detail-scroll-reset-owner="hertzbeat-ui-detail-scroll-reset"
+      data-hz-topology-detail-scroll-reset-key={scrollResetKey}
       data-hz-topology-detail-surface={surface}
       data-hz-topology-detail-surface-owner="hertzbeat-ui-detail-surface"
       data-hz-topology-detail-identity-owner="hertzbeat-ui-detail-identity"
@@ -11499,6 +11552,7 @@ export function HzTopologyDetailDrawer({
           </div>
         ) : null}
       </div>
+      {graphFirst ? signalActionGroup : null}
       {boundary ? (
         <div
           {...boundaryRestProps}
@@ -11569,26 +11623,7 @@ export function HzTopologyDetailDrawer({
           ))}
         </div>
       ) : null}
-      {signalActions.length > 0 ? (
-        <div
-          className={cn('grid', graphFirst ? 'mt-2 gap-1' : 'mt-3 gap-2')}
-          data-hz-topology-detail-actions="signals"
-          data-hz-topology-detail-signal-action-group-owner="hertzbeat-ui-detail-signal-action-group"
-        >
-          {signalActionsLabel ? (
-            <div
-              className="truncate text-[10px] font-semibold uppercase tracking-[0.08em] text-[#727b8c]"
-              data-hz-topology-detail-signal-label="signals"
-              data-hz-topology-detail-signal-label-owner="hertzbeat-ui-detail-signal-label"
-            >
-              {signalActionsLabel}
-            </div>
-          ) : null}
-          {signalActions.map(action => (
-            <HzTopologyDetailDrawerActionLink key={action.id} action={action} attributeName="data-hz-topology-detail-signal-action" />
-          ))}
-        </div>
-      ) : null}
+      {!graphFirst ? signalActionGroup : null}
     </section>
   );
 }
@@ -12179,6 +12214,7 @@ export function HzTopologyPathSummary({
   evidenceBadges = [],
   actions = [],
   boundary = 'none',
+  interactionState,
   selectedEdgeId,
   hoveredEdgeId,
   sourceId,
@@ -12188,6 +12224,9 @@ export function HzTopologyPathSummary({
   className,
   ...props
 }: HzTopologyPathSummaryProps) {
+  const resolvedInteractionState: HzTopologyPathSummaryInteractionState =
+    interactionState ?? (selectedEdgeId ? 'selected' : hoveredEdgeId ? 'hovered' : 'preview');
+
   return (
     <section
       {...props}
@@ -12202,6 +12241,11 @@ export function HzTopologyPathSummary({
       data-hz-topology-path-summary-boundary={boundary}
       data-hz-topology-path-summary-boundary-owner="hertzbeat-ui-path-summary-boundary"
       data-hz-topology-path-interaction-owner="hertzbeat-ui-path-summary-interaction"
+      data-topology-path-summary-interaction-state={resolvedInteractionState}
+      data-hz-topology-path-summary-interaction-state={resolvedInteractionState}
+      data-hz-topology-path-summary-interaction-state-owner="hertzbeat-ui-path-summary-interaction-state"
+      data-topology-path-summary-selected-edge-id={selectedEdgeId ?? 'none'}
+      data-topology-path-summary-hovered-edge-id={hoveredEdgeId ?? 'none'}
       data-hz-topology-path-selected-edge={selectedEdgeId ?? 'none'}
       data-hz-topology-path-hovered-edge={hoveredEdgeId ?? 'none'}
       data-hz-topology-path-source-id={sourceId ?? 'none'}
@@ -12662,7 +12706,7 @@ export function HzTopologyWorkbenchGrid({
       {...props}
       className={cn(
         'grid min-h-[760px] bg-[#08090c]',
-        layout === 'canvas-companion' ? 'xl:grid-cols-[minmax(0,1fr)_320px]' : '',
+        layout === 'canvas-companion' ? 'lg:grid-cols-[minmax(0,1fr)_320px]' : '',
         className
       )}
       data-hz-ui="topology-workbench-grid"
@@ -12683,7 +12727,7 @@ const topologyWorkbenchSlotSurfaceClassName: Record<HzTopologyWorkbenchSlotSurfa
 };
 
 const topologyWorkbenchSlotKindClassName: Record<HzTopologyWorkbenchSlotKind, string> = {
-  canvas: 'min-w-0 xl:sticky xl:top-[64px] xl:self-start',
+  canvas: 'min-w-0 lg:sticky lg:top-[64px] lg:self-start',
   companion: 'min-w-0'
 };
 
@@ -12974,8 +13018,9 @@ export function HzTopologyToolbar({
   const hasScopeControls = depthOptions.length > 0 || hasLayoutControl || groupByOptions.length > 0 || Boolean(resetLabel);
   const secondaryVisibility = graphFirst ? 'assistive' : 'visible';
   const controlStripLayout = graphFirst ? 'inline-overflow' : 'stacked-grid';
+  const controlStripDisplay = graphFirst ? 'contents' : 'grid';
   const chrome = graphFirst && boundary === 'none' ? 'frameless' : 'surface';
-  const selectTriggerClassName = graphFirst ? 'h-7 !gap-1.5 !pl-1 !pr-1.5' : 'h-8';
+  const selectTriggerClassName = graphFirst ? 'h-7 !gap-1.5 !px-2' : 'h-8';
 
   return (
     <section
@@ -12984,7 +13029,7 @@ export function HzTopologyToolbar({
         'min-w-0',
         chrome === 'frameless' ? 'bg-transparent' : 'bg-[var(--hz-ui-surface)]',
         graphFirst
-          ? 'grid items-center gap-1.5 overflow-x-auto px-0 py-1 [grid-template-columns:132px_minmax(220px,1fr)_132px_auto]'
+          ? 'grid items-center gap-1.5 overflow-x-auto px-0 py-1 [grid-template-columns:112px_minmax(260px,1fr)_148px_88px_132px_auto]'
           : 'grid gap-2 px-4 py-2 lg:grid-cols-[180px_minmax(0,1fr)_104px_104px]',
         topologyToolbarBoundaryClassName[boundary],
         className
@@ -13003,6 +13048,8 @@ export function HzTopologyToolbar({
       data-hz-topology-toolbar-select-padding={graphFirst ? 'compact-flush' : 'default'}
       data-hz-topology-toolbar-row-separator={graphFirst ? 'none' : 'soft'}
       data-hz-topology-toolbar-control-gap={graphFirst ? '6px' : '8px'}
+      data-hz-topology-toolbar-control-flow={graphFirst ? 'single-grid-row' : 'stacked-grid'}
+      data-hz-topology-toolbar-empty-offset={graphFirst ? 'none' : undefined}
       data-hz-topology-toolbar-visual-weight={graphFirst ? 'low-interruption' : 'balanced'}
       data-hz-topology-toolbar-visual-weight-owner="hertzbeat-ui-toolbar-visual-weight"
       data-hz-topology-toolbar-secondary-visibility={secondaryVisibility}
@@ -13021,7 +13068,7 @@ export function HzTopologyToolbar({
         options={environmentOptions}
         value={environmentValue}
         onChange={event => onEnvironmentChange?.(event.currentTarget.value)}
-        className={graphFirst ? 'w-[132px] min-w-0' : undefined}
+        className={graphFirst ? 'w-[112px] min-w-0' : undefined}
         triggerClassName={selectTriggerClassName}
       />
       <HzInput
@@ -13044,7 +13091,7 @@ export function HzTopologyToolbar({
           options={sourceKindOptions}
           value={sourceKindValue}
           onChange={event => onSourceKindChange?.(event.currentTarget.value)}
-          className={graphFirst ? 'w-[132px] shrink-0' : undefined}
+          className={graphFirst ? 'w-[148px] shrink-0' : undefined}
           triggerClassName={selectTriggerClassName}
         />
       ) : null}
@@ -13075,13 +13122,14 @@ export function HzTopologyToolbar({
         <div
           className={cn(
             graphFirst
-              ? 'flex min-w-0 shrink-0 items-center justify-end gap-1.5'
+              ? 'contents'
               : 'grid min-w-0 border-t border-[var(--hz-ui-line-faint)] sm:grid-cols-2 lg:col-span-4',
             graphFirst ? '' : 'gap-2 pt-2 xl:grid-cols-[120px_160px_160px_auto]'
           )}
           data-hz-topology-toolbar-control-strip={graphFirst ? 'source-depth-group-reset' : 'depth-layout-group-reset'}
           data-hz-topology-toolbar-control-strip-owner="hertzbeat-ui-toolbar-control-strip"
           data-hz-topology-toolbar-control-strip-layout={controlStripLayout}
+          data-hz-topology-toolbar-control-strip-display={controlStripDisplay}
           data-hz-topology-toolbar-control-strip-layout-owner="hertzbeat-ui-toolbar-control-strip-layout"
         >
           {depthOptions.length > 0 ? (
@@ -13238,11 +13286,12 @@ export function HzTopologyToolbar({
 }
 
 export type HzTopologyEmptyStateBoundary = 'default' | 'flush' | 'canvas';
+export type HzTopologyEmptyStateCopyVisibility = 'visible' | 'assistive';
 
 const topologyEmptyStateBoundaryClassNames: Record<HzTopologyEmptyStateBoundary, string> = {
   default: 'border border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] shadow-[0_16px_48px_rgba(0,0,0,0.28)]',
   flush: 'border-y border-x-0 border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] shadow-[0_16px_48px_rgba(0,0,0,0.28)]',
-  canvas: 'border border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)] shadow-[0_16px_48px_rgba(0,0,0,0.28)]'
+  canvas: 'border-0 bg-transparent shadow-none'
 };
 
 export type HzTopologyEmptyStateProps = React.HTMLAttributes<HTMLElement> & {
@@ -13260,6 +13309,7 @@ export type HzTopologyEmptyStateProps = React.HTMLAttributes<HTMLElement> & {
   kind?: 'api-empty' | 'degraded' | 'filtered-empty';
   placement?: 'inline' | 'canvas-center';
   boundary?: HzTopologyEmptyStateBoundary;
+  copyVisibility?: HzTopologyEmptyStateCopyVisibility;
 };
 
 export function HzTopologyEmptyState({
@@ -13277,6 +13327,7 @@ export function HzTopologyEmptyState({
   kind = 'api-empty',
   placement = 'inline',
   boundary = 'default',
+  copyVisibility = 'visible',
   className,
   ...props
 }: HzTopologyEmptyStateProps) {
@@ -13298,6 +13349,7 @@ export function HzTopologyEmptyState({
       data-hz-topology-empty-kind={kind}
       data-hz-topology-empty-boundary={boundary}
       data-hz-topology-empty-boundary-owner="hertzbeat-ui-empty-boundary"
+      data-hz-topology-empty-boundary-visual={boundary === 'canvas' ? 'frameless-canvas' : 'surface'}
       data-hz-topology-empty-placement={placement}
       data-hz-topology-empty-source={sourceLabel}
       data-hz-topology-empty-time-scope={timeScope}
@@ -13309,11 +13361,17 @@ export function HzTopologyEmptyState({
       data-hz-topology-empty-depth={depth ?? 'unknown'}
       data-hz-topology-empty-result-count={typeof resultCount === 'number' ? resultCount : 'unknown'}
       data-hz-topology-empty-evidence-sources={normalizedEvidenceSources}
+      data-hz-topology-empty-copy-visibility={copyVisibility}
     >
       <div className="text-[13px] font-semibold text-[#f3f6fb]" data-hz-topology-empty-title-owner="hertzbeat-ui-empty-title">
         {title}
       </div>
-      <div className="mt-2 text-[12px] leading-5 text-[#8f99ab]" data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy">
+      <div
+        className={cn(
+          copyVisibility === 'assistive' ? 'sr-only' : 'mt-2 text-[12px] leading-5 text-[#8f99ab]'
+        )}
+        data-hz-topology-empty-copy-owner="hertzbeat-ui-empty-copy"
+      >
         {copy}
       </div>
       {sourceLabel || timeScope ? (
@@ -13474,6 +13532,7 @@ export type HzTopologyCompanionJumpListProps = React.HTMLAttributes<HTMLElement>
   items: HzTopologyCompanionJumpListItem[];
   density?: HzTopologyCompanionJumpListDensity;
   activeMode?: HzTopologyCompanionJumpListActiveMode;
+  activeResetKey?: string | number;
   ariaLabel?: string;
 };
 
@@ -13580,14 +13639,44 @@ export function HzTopologyCompanionJumpList({
   items,
   density = 'compact',
   activeMode = 'manual',
+  activeResetKey,
   ariaLabel = 'Topology companion sections',
   className,
   ...rest
 }: HzTopologyCompanionJumpListProps) {
   const jumpListRef = React.useRef<HTMLElement | null>(null);
   const manualActiveId = React.useMemo(() => items.find(item => item.active)?.id, [items]);
+  const manualActiveHref = items.find(item => item.id === manualActiveId)?.href;
   const [scrollActiveId, setScrollActiveId] = React.useState<string | undefined>();
   const resolvedActiveId = activeMode === 'contained-rail-scroll' ? (scrollActiveId ?? manualActiveId) : manualActiveId;
+  const didRunSelectionSyncRef = React.useRef(false);
+
+  React.useEffect(() => {
+    if (activeMode !== 'contained-rail-scroll') {
+      didRunSelectionSyncRef.current = false;
+      return;
+    }
+    const shouldSyncLocation = didRunSelectionSyncRef.current && activeResetKey !== undefined;
+    didRunSelectionSyncRef.current = true;
+    setScrollActiveId(undefined);
+    if (!manualActiveHref?.startsWith('#') || typeof document === 'undefined') return;
+    const jumpList = jumpListRef.current;
+    const rail = jumpList?.closest<HTMLElement>('[data-hz-ui="topology-companion-rail"][data-hz-topology-companion-scroll="contained"]');
+    const target = document.getElementById(manualActiveHref.slice(1));
+    if (!jumpList || !rail || !target || !rail.contains(target)) return;
+    const railRect = rail.getBoundingClientRect();
+    const targetRect = target.getBoundingClientRect();
+    const jumpListHeight = jumpList.getBoundingClientRect().height;
+    const targetTop = Math.max(0, rail.scrollTop + targetRect.top - railRect.top - jumpListHeight - 8);
+    if (typeof rail.scrollTo === 'function') {
+      rail.scrollTo({ top: targetTop, behavior: 'auto' });
+    } else {
+      rail.scrollTop = targetTop;
+    }
+    if (shouldSyncLocation && typeof window !== 'undefined' && window.history?.replaceState) {
+      window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${manualActiveHref}`);
+    }
+  }, [activeMode, activeResetKey, manualActiveHref, manualActiveId]);
 
   React.useEffect(() => {
     if (activeMode !== 'contained-rail-scroll' || typeof document === 'undefined') return undefined;
@@ -13636,7 +13725,6 @@ export function HzTopologyCompanionJumpList({
       if (nextActiveId) setScrollActiveId(current => (current === nextActiveId ? current : nextActiveId));
     };
 
-    updateActiveSection();
     rail.addEventListener('scroll', updateActiveSection, { passive: true });
     return () => rail.removeEventListener('scroll', updateActiveSection);
   }, [activeMode, items]);
@@ -13663,6 +13751,13 @@ export function HzTopologyCompanionJumpList({
       data-hz-topology-companion-jump-list-scroll-scope-owner="hertzbeat-ui-companion-jump-list-scroll-scope"
       data-hz-topology-companion-jump-list-active-mode={activeMode}
       data-hz-topology-companion-jump-list-active-mode-owner="hertzbeat-ui-companion-jump-list-active-mode"
+      data-hz-topology-companion-jump-list-selection-sync="manual-active-resets-scroll-active"
+      data-hz-topology-companion-jump-list-selection-sync-owner="hertzbeat-ui-companion-jump-list-selection-sync"
+      data-hz-topology-companion-jump-list-selection-scroll="active-section"
+      data-hz-topology-companion-jump-list-selection-scroll-owner="hertzbeat-ui-companion-jump-list-selection-scroll"
+      data-hz-topology-companion-jump-list-selection-url-policy="replace-active-section-hash"
+      data-hz-topology-companion-jump-list-selection-url-policy-owner="hertzbeat-ui-companion-jump-list-selection-url-policy"
+      data-hz-topology-companion-jump-list-active-reset-key={activeResetKey ?? manualActiveId ?? 'none'}
     >
       <div className="flex min-w-max items-center gap-1">
         {items.map(({ id, href, label, active = false, className: itemClassName, onClick, ...itemProps }) => {
@@ -13811,6 +13906,9 @@ export type HzTopologyMetricTableLabels = {
   renderWindowFilterPartial?: React.ReactNode;
   renderWindowFilterHidden?: React.ReactNode;
   renderWindowFilterUnknown?: React.ReactNode;
+  renderWindowEdgeSummary?: React.ReactNode;
+  renderWindowRowSummary?: (rendered: number, total: number) => React.ReactNode;
+  renderWindowShowMore?: (next: number, total: number) => React.ReactNode;
 };
 
 export type HzTopologyMetricTableRenderWindowCompanion = {
@@ -13818,6 +13916,8 @@ export type HzTopologyMetricTableRenderWindowCompanion = {
   totalNodeCount: number;
   renderedNodeCount: number;
   hiddenNodeCount: number;
+  totalEdgeCount?: number;
+  renderedEdgeCount?: number;
   visibleNodeBudget: number;
   tableCompanion?: 'optional' | 'recommended' | 'required';
   priorityNodeIds?: string[];
@@ -13828,6 +13928,7 @@ export type HzTopologyMetricTableBoundary = 'default' | 'framed' | 'flush';
 export type HzTopologyMetricTableDensity = 'compact' | 'graph-first';
 export type HzTopologyMetricRowWindowVisibility = 'visible' | 'partial' | 'hidden' | 'unknown';
 export type HzTopologyMetricTableRenderWindowFilter = 'all' | HzTopologyMetricRowWindowVisibility;
+const HZ_TOPOLOGY_METRIC_TABLE_ROW_RENDER_BUDGET = 120;
 
 const topologyMetricTableBoundaryClassName: Record<HzTopologyMetricTableBoundary, string> = {
   default: 'border-y border-[var(--hz-ui-line-soft)]',
@@ -13893,6 +13994,7 @@ export function HzTopologyMetricTable({
   title,
   rows,
   selectedRowId,
+  selectionSource = 'none',
   emptyLabel = 'No topology evidence',
   labels,
   renderWindowCompanion,
@@ -13907,6 +14009,7 @@ export function HzTopologyMetricTable({
   title: React.ReactNode;
   rows: HzTopologyMetricRow[];
   selectedRowId?: string;
+  selectionSource?: string;
   emptyLabel?: React.ReactNode;
   labels?: HzTopologyMetricTableLabels;
   renderWindowCompanion?: HzTopologyMetricTableRenderWindowCompanion;
@@ -13929,12 +14032,29 @@ export function HzTopologyMetricTable({
     : 'none';
   const renderedNodeIds = renderWindowCompanion?.renderedNodeIds;
   const renderedNodeIdSet = renderedNodeIds?.length ? new Set(renderedNodeIds) : undefined;
+  const tableEdgeCount = typeof renderWindowCompanion?.totalEdgeCount === 'number'
+    ? renderWindowCompanion.totalEdgeCount
+    : rows.length;
+  const canvasRenderedEdgeCount = typeof renderWindowCompanion?.renderedEdgeCount === 'number'
+    ? renderWindowCompanion.renderedEdgeCount
+    : tableEdgeCount;
+  const hasCanvasEdgeSummary =
+    renderWindowMode === 'windowed' &&
+    typeof renderWindowCompanion?.totalEdgeCount === 'number' &&
+    typeof renderWindowCompanion?.renderedEdgeCount === 'number';
+  const edgeCountPolicy = hasCanvasEdgeSummary ? 'canvas-rendered-vs-table-total' : 'row-window-visibility';
+  const renderWindowEdgeSummary =
+    labels?.renderWindowEdgeSummary ?? `${canvasRenderedEdgeCount}/${tableEdgeCount} rendered in canvas`;
   const rowsWithWindowVisibility = rows.map(row => {
     const sourceVisible = resolveTopologyMetricEndpointVisibility(row.sourceNodeId, renderWindowMode, renderedNodeIdSet);
     const targetVisible = resolveTopologyMetricEndpointVisibility(row.targetNodeId, renderWindowMode, renderedNodeIdSet);
     const rowWindowVisibility = resolveTopologyMetricRowWindowVisibility(sourceVisible, targetVisible);
     return { row, sourceVisible, targetVisible, rowWindowVisibility };
   });
+  const selectedRowWithWindowVisibility = selectedRowId
+    ? rowsWithWindowVisibility.find(({ row }) => row.id === selectedRowId)
+    : undefined;
+  const hasMetricTableSelection = Boolean(selectedRowId && selectionSource !== 'none');
   const renderWindowRowCounts = rowsWithWindowVisibility.reduce<Record<HzTopologyMetricRowWindowVisibility, number>>(
     (counts, row) => {
       counts[row.rowWindowVisibility] += 1;
@@ -13943,10 +14063,44 @@ export function HzTopologyMetricTable({
     { visible: 0, partial: 0, hidden: 0, unknown: 0 }
   );
   const activeRenderWindowFilter = normalizeTopologyMetricTableRenderWindowFilter(renderWindowFilter);
+  const [rowRenderPage, setRowRenderPage] = React.useState(1);
+  React.useEffect(() => {
+    setRowRenderPage(1);
+  }, [activeRenderWindowFilter, rows.length]);
   const filteredRowsWithWindowVisibility =
     activeRenderWindowFilter === 'all'
       ? rowsWithWindowVisibility
       : rowsWithWindowVisibility.filter(row => row.rowWindowVisibility === activeRenderWindowFilter);
+  const shouldBudgetRenderedRows =
+    renderWindowMode === 'windowed' &&
+    filteredRowsWithWindowVisibility.length > HZ_TOPOLOGY_METRIC_TABLE_ROW_RENDER_BUDGET;
+  const rowRenderBudget = shouldBudgetRenderedRows
+    ? Math.min(filteredRowsWithWindowVisibility.length, HZ_TOPOLOGY_METRIC_TABLE_ROW_RENDER_BUDGET * rowRenderPage)
+    : filteredRowsWithWindowVisibility.length;
+  const budgetedRowsWithWindowVisibility = shouldBudgetRenderedRows
+    ? filteredRowsWithWindowVisibility.slice(0, rowRenderBudget)
+    : filteredRowsWithWindowVisibility;
+  const selectedFilteredRowWithWindowVisibility = selectedRowId
+    ? filteredRowsWithWindowVisibility.find(({ row }) => row.id === selectedRowId)
+    : undefined;
+  const renderedRowsWithWindowVisibility =
+    shouldBudgetRenderedRows &&
+    selectedFilteredRowWithWindowVisibility &&
+    !budgetedRowsWithWindowVisibility.some(({ row }) => row.id === selectedFilteredRowWithWindowVisibility.row.id)
+      ? [...budgetedRowsWithWindowVisibility, selectedFilteredRowWithWindowVisibility]
+      : budgetedRowsWithWindowVisibility;
+  const hiddenRenderedRowCount = Math.max(0, filteredRowsWithWindowVisibility.length - renderedRowsWithWindowVisibility.length);
+  const nextRowRenderCount = shouldBudgetRenderedRows
+    ? Math.min(filteredRowsWithWindowVisibility.length, rowRenderBudget + HZ_TOPOLOGY_METRIC_TABLE_ROW_RENDER_BUDGET)
+    : filteredRowsWithWindowVisibility.length;
+  const canShowMoreRenderedRows = shouldBudgetRenderedRows && rowRenderBudget < filteredRowsWithWindowVisibility.length;
+  const rowRenderPolicy = shouldBudgetRenderedRows ? 'windowed-dom-budget' : 'all-filtered-rows';
+  const renderWindowRowSummary =
+    labels?.renderWindowRowSummary?.(renderedRowsWithWindowVisibility.length, filteredRowsWithWindowVisibility.length) ??
+    `Showing ${renderedRowsWithWindowVisibility.length} of ${filteredRowsWithWindowVisibility.length} rows`;
+  const renderWindowShowMore =
+    labels?.renderWindowShowMore?.(nextRowRenderCount, filteredRowsWithWindowVisibility.length) ??
+    `Show ${nextRowRenderCount} of ${filteredRowsWithWindowVisibility.length} rows`;
   const renderWindowFilterOptions: Array<{
     id: HzTopologyMetricTableRenderWindowFilter;
     label: React.ReactNode;
@@ -13969,6 +14123,7 @@ export function HzTopologyMetricTable({
       )}
       data-hz-ui="topology-metric-table"
       data-hz-topology-primitive="metric-table"
+      data-hz-topology-metric-table-root="true"
       data-hz-topology-metric-table-density={density}
       data-hz-topology-metric-table-density-owner="hertzbeat-ui-metric-table-density"
       data-hz-topology-metric-table-visual-weight={graphFirst ? 'low-interruption' : 'balanced'}
@@ -13977,12 +14132,33 @@ export function HzTopologyMetricTable({
       data-hz-topology-metric-table-boundary={boundary}
       data-hz-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
       data-hz-topology-metric-rows={rows.length}
+      data-hz-topology-metric-table-total-rows={rows.length}
       data-hz-topology-metric-table-interaction="row-select-detail"
+      data-hz-topology-metric-table-live-selection-owner="hertzbeat-ui-metric-table-selection"
+      data-hz-topology-metric-table-selected-edge-id={hasMetricTableSelection ? selectedRowId : 'none'}
+      data-hz-topology-metric-table-selection-source={hasMetricTableSelection ? selectionSource : 'none'}
+      data-hz-topology-metric-table-selected-row-render-window-visibility={
+        hasMetricTableSelection ? selectedRowWithWindowVisibility?.rowWindowVisibility ?? 'unknown' : 'none'
+      }
+      data-hz-topology-metric-table-selected-row-source-visible={
+        hasMetricTableSelection && selectedRowWithWindowVisibility ? String(selectedRowWithWindowVisibility.sourceVisible) : 'unknown'
+      }
+      data-hz-topology-metric-table-selected-row-target-visible={
+        hasMetricTableSelection && selectedRowWithWindowVisibility ? String(selectedRowWithWindowVisibility.targetVisible) : 'unknown'
+      }
+      data-hz-topology-metric-table-live-selection-invariants="row-click-drawer no-url-change no-remount no-refit viewport-preserved render-key-stable"
+      data-hz-topology-metric-table-filter-invariants="in-page no-url-change no-g6-remount viewport-preserved selection-preserved"
+      data-hz-topology-metric-table-filter-url-policy="preserve-current-url"
       data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"
       data-hz-topology-metric-table-render-window-mode={renderWindowMode}
       data-hz-topology-metric-table-render-window-total-node-count={renderWindowCompanion?.totalNodeCount ?? 0}
       data-hz-topology-metric-table-render-window-rendered-node-count={renderWindowCompanion?.renderedNodeCount ?? rows.length}
       data-hz-topology-metric-table-render-window-hidden-node-count={renderWindowCompanion?.hiddenNodeCount ?? 0}
+      data-hz-topology-metric-table-render-window-total-edge-count={tableEdgeCount}
+      data-hz-topology-metric-table-render-window-rendered-edge-count={canvasRenderedEdgeCount}
+      data-hz-topology-metric-table-edge-count-policy={edgeCountPolicy}
+      data-hz-topology-metric-table-canvas-rendered-edge-count={canvasRenderedEdgeCount}
+      data-hz-topology-metric-table-table-edge-count={tableEdgeCount}
       data-hz-topology-metric-table-render-window-visible-node-budget={renderWindowCompanion?.visibleNodeBudget ?? rows.length}
       data-hz-topology-metric-table-render-window-visible-node-count={renderedNodeIds?.length ?? (renderWindowMode === 'direct' ? renderWindowCompanion?.renderedNodeCount ?? 0 : 0)}
       data-hz-topology-metric-table-render-window-table-companion={renderWindowTableCompanion}
@@ -13991,11 +14167,23 @@ export function HzTopologyMetricTable({
       data-hz-topology-metric-table-visible-row-count={renderWindowRowCounts.visible}
       data-hz-topology-metric-table-partial-row-count={renderWindowRowCounts.partial}
       data-hz-topology-metric-table-hidden-row-count={renderWindowRowCounts.hidden}
+      data-hz-topology-metric-table-hidden-row-proof-owner="hertzbeat-ui-metric-table-hidden-row-proof"
+      data-hz-topology-metric-table-hidden-row-proof={renderWindowRowCounts.hidden > 0 ? 'available' : 'none'}
+      data-hz-topology-metric-table-hidden-row-proof-filter="hidden"
+      data-hz-topology-metric-table-hidden-row-proof-count={renderWindowRowCounts.hidden}
       data-hz-topology-metric-table-unknown-row-count={renderWindowRowCounts.unknown}
       data-hz-topology-metric-table-render-window-filter-owner="hertzbeat-ui-metric-table-render-window-filter"
       data-hz-topology-metric-table-render-window-filter={activeRenderWindowFilter}
       data-hz-topology-metric-table-filtered-row-count={filteredRowsWithWindowVisibility.length}
       data-hz-topology-metric-table-filtered-out-row-count={rows.length - filteredRowsWithWindowVisibility.length}
+      data-hz-topology-metric-table-row-render-policy={rowRenderPolicy}
+      data-hz-topology-metric-table-row-render-reset-policy="filter-change-resets-budget-preserve-selected-row"
+      data-hz-topology-metric-table-row-render-budget={rowRenderBudget}
+      data-hz-topology-metric-table-row-render-page={rowRenderPage}
+      data-hz-topology-metric-table-row-render-next-count={nextRowRenderCount}
+      data-hz-topology-metric-table-row-render-can-show-more={canShowMoreRenderedRows ? 'true' : 'false'}
+      data-hz-topology-metric-table-rendered-row-count={renderedRowsWithWindowVisibility.length}
+      data-hz-topology-metric-table-rendered-hidden-row-count={hiddenRenderedRowCount}
     >
       <header
         className={cn(
@@ -14024,6 +14212,18 @@ export function HzTopologyMetricTable({
         )}
         data-hz-topology-metric-table-filter-controls-owner="hertzbeat-ui-metric-table-filter-controls"
       >
+        {hasCanvasEdgeSummary ? (
+          <span
+            className={cn(
+              'inline-flex min-h-7 items-center border border-[var(--hz-ui-line-soft)] bg-[#08090c] font-mono text-[#8f99ab]',
+              graphFirst ? 'px-2 text-[10px]' : 'px-2.5 text-[11px]'
+            )}
+            data-hz-topology-metric-table-edge-summary-owner="hertzbeat-ui-metric-table-edge-summary"
+            data-hz-topology-metric-table-edge-summary-policy={edgeCountPolicy}
+          >
+            {renderWindowEdgeSummary}
+          </span>
+        ) : null}
         {renderWindowFilterOptions.map(option => {
           const active = option.id === activeRenderWindowFilter;
           return (
@@ -14042,7 +14242,11 @@ export function HzTopologyMetricTable({
               data-hz-topology-metric-table-filter-control-owner="hertzbeat-ui-metric-table-filter-control"
               data-hz-topology-metric-table-filter-control={option.id}
               data-hz-topology-metric-table-filter-active={active ? 'true' : 'false'}
+              data-hz-topology-metric-table-filter-control-active={active ? 'true' : 'false'}
               data-hz-topology-metric-table-filter-count={option.count}
+              data-hz-topology-metric-table-filter-control-url-policy="preserve-current-url"
+              data-hz-topology-metric-table-filter-control-selection-policy="preserve-selected-edge"
+              data-hz-topology-metric-table-filter-row-render-reset-policy="reset-row-budget-preserve-selection"
             >
               <span>{option.label}</span>
               <span className="text-[#727b8c]">{option.count}</span>
@@ -14055,11 +14259,21 @@ export function HzTopologyMetricTable({
           {emptyLabel}
         </div>
       ) : (
+        <>
         <div className="grid min-w-0 divide-y divide-[var(--hz-ui-line-faint)]">
-          {filteredRowsWithWindowVisibility.map(({ row, sourceVisible, targetVisible, rowWindowVisibility }) => {
+          {renderedRowsWithWindowVisibility.map(({ row, sourceVisible, targetVisible, rowWindowVisibility }) => {
             const tone = row.tone || (row.errorRate && row.errorRate > 0 ? 'warning' : 'neutral');
             const selected = selectedRowId === row.id;
             const rowActionLabel = labels?.rowAction;
+            const rowWindowContextLabel =
+              rowWindowVisibility === 'visible'
+                ? labels?.renderWindowFilterVisible ?? 'Visible'
+                : rowWindowVisibility === 'partial'
+                  ? labels?.renderWindowFilterPartial ?? 'Partial'
+                  : rowWindowVisibility === 'hidden'
+                    ? labels?.renderWindowFilterHidden ?? 'Hidden'
+                    : labels?.renderWindowFilterUnknown ?? 'Unknown';
+            const showRowWindowContext = renderWindowMode === 'windowed' && (selected || rowWindowVisibility !== 'visible');
             return (
               <button
                 key={row.id}
@@ -14100,6 +14314,9 @@ export function HzTopologyMetricTable({
                 data-hz-topology-error-count={row.errorCount}
                 data-hz-topology-latency-p95-ms={row.latencyP95Ms}
                 data-hz-topology-metric-table-row-owner="hertzbeat-ui-metric-table-row"
+                data-hz-topology-edge-row-selection-owner="hertzbeat-ui-metric-table-row-selection"
+                data-hz-topology-edge-row-selection-mode="table-row-click-drawer"
+                data-hz-topology-edge-row-selection-url-policy="preserve-current-url"
               >
                 <span className="min-w-0" data-hz-topology-metric-table-endpoints-owner="hertzbeat-ui-metric-table-endpoints">
                   <span
@@ -14127,6 +14344,17 @@ export function HzTopologyMetricTable({
                         {row.sourceKind}
                       </span>
                     ) : null}
+                    {showRowWindowContext ? (
+                      <span
+                        className="border-l border-[var(--hz-ui-line-soft)] pl-1.5 font-semibold text-[#dbe4f0]"
+                        data-hz-topology-edge-row-window-context-owner="hertzbeat-ui-metric-table-row-window-context"
+                        data-hz-topology-edge-row-window-context={rowWindowVisibility}
+                        data-hz-topology-edge-row-window-context-source-visible={sourceVisible}
+                        data-hz-topology-edge-row-window-context-target-visible={targetVisible}
+                      >
+                        {rowWindowContextLabel}
+                      </span>
+                    ) : null}
                     {row.evidenceBadges?.map((badge, index) => (
                       <span
                         key={index}
@@ -14184,6 +14412,35 @@ export function HzTopologyMetricTable({
             );
           })}
         </div>
+        {hiddenRenderedRowCount > 0 ? (
+          <div
+            className={cn(
+              'flex min-w-0 flex-wrap items-center justify-between gap-2 border-t border-[var(--hz-ui-line-faint)] font-mono text-[#8f99ab]',
+              graphFirst ? 'px-2 py-2 text-[10px]' : 'px-3 py-2.5 text-[11px]'
+            )}
+            data-hz-topology-metric-table-row-render-summary-owner="hertzbeat-ui-metric-table-row-render-summary"
+          >
+            <span>{renderWindowRowSummary}</span>
+            {canShowMoreRenderedRows ? (
+              <button
+                type="button"
+                className="inline-flex min-h-7 items-center border border-[var(--hz-ui-line-soft)] bg-[#0b0c0f] px-2 font-semibold text-[#dbe4f0] transition-colors hover:border-[var(--hz-ui-line)]"
+                onClick={() => setRowRenderPage(page => page + 1)}
+                data-hz-topology-metric-table-row-render-action-owner="hertzbeat-ui-metric-table-row-render-action"
+                data-hz-topology-metric-table-row-render-action="show-more"
+                data-hz-topology-metric-table-row-render-action-invariants="append-rows-only no-url-change no-g6-remount viewport-preserved selection-preserved"
+                data-hz-topology-metric-table-row-render-action-effect="append-row-budget"
+                data-hz-topology-metric-table-row-render-action-filter-reset-policy="reset-row-budget-on-filter-change"
+                data-hz-topology-metric-table-row-render-action-url-policy="preserve-current-url"
+                data-hz-topology-metric-table-row-render-action-selection-policy="preserve-selected-edge"
+                data-hz-topology-metric-table-row-render-action-next-count={nextRowRenderCount}
+              >
+                {renderWindowShowMore}
+              </button>
+            ) : null}
+          </div>
+        ) : null}
+        </>
       )}
     </section>
   );
diff --git a/web-next/packages/hertzbeat-ui/src/shell.tsx b/web-next/packages/hertzbeat-ui/src/shell.tsx
new file mode 100644
index 0000000000..9552474bbe
--- /dev/null
+++ b/web-next/packages/hertzbeat-ui/src/shell.tsx
@@ -0,0 +1,690 @@
+'use client';
+
+import * as React from 'react';
+import { X } from 'lucide-react';
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs));
+}
+
+const controlFocusClassName =
+  'focus-visible:border-[var(--hz-ui-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--hz-ui-active-soft)]';
+
+export type HzAiChatPreviewMessage = {
+  role: 'user' | 'assistant' | 'system';
+  label: React.ReactNode;
+  content: React.ReactNode;
+};
+
+export type HzAiChatConversationStatus = 'loading' | 'ready' | 'empty' | 'error';
+export type HzAiChatMessageStatus = 'idle' | 'loading' | 'ready' | 'empty' | 'error';
+export type HzAiChatNewConversationStatus = 'idle' | 'creating' | 'error';
+export type HzAiChatDeleteConversationStatus = 'idle' | 'confirming' | 'deleting' | 'error';
+export type HzAiChatSendStatus = 'idle' | 'sending' | 'error';
+export type HzAiChatConfigStatus = 'idle' | 'loading' | 'ready' | 'saving' | 'saved' | 'error';
+export type HzAiChatScheduleStatus = 'idle' | 'loading' | 'ready' | 'empty' | 'saving' | 'error';
+export type HzAiChatScheduleDeleteStatus = 'idle' | 'confirming' | 'deleting' | 'error';
+
+export type HzAiChatProviderOption = {
+  value: string;
+  label: React.ReactNode;
+  defaultBaseUrl?: string;
+  defaultModel?: string;
+};
+
+export type HzAiChatProviderConfigValue = {
+  code: string;
+  apiKey: string;
+  baseUrl: string;
+  model: string;
+};
+
+export type HzAiChatScheduleRow = {
+  id: string | number;
+  sopName: React.ReactNode;
+  cronExpression: string;
+  enabled: boolean;
+};
+
+export type HzAiChatScheduleSkillOption = {
+  value: string;
+  label: React.ReactNode;
+};
+
+export type HzAiChatScheduleDraft = {
+  sopName: string;
+  cronExpression: string;
+  enabled?: boolean;
+};
+
+export type HzAiChatConversationPreview = {
+  id: string | number;
+  title: React.ReactNode;
+  subtitle?: React.ReactNode;
+  active?: boolean;
+};
+
+export type HzAiChatModalSurfaceProps = React.HTMLAttributes<HTMLDivElement> & {
+  title: React.ReactNode;
+  subtitle?: React.ReactNode;
+  conversationsTitle: React.ReactNode;
+  newChatLabel: React.ReactNode;
+  newChatStatus?: HzAiChatNewConversationStatus;
+  deleteLabel?: string;
+  deleteConfirmLabel?: React.ReactNode;
+  deleteCancelLabel?: React.ReactNode;
+  deleteStatus?: HzAiChatDeleteConversationStatus;
+  deleteConversationId?: HzAiChatConversationPreview['id'] | null;
+  welcomeTitle: React.ReactNode;
+  welcomeDescription: React.ReactNode;
+  inputPlaceholder: string;
+  inputReadOnly?: boolean;
+  inputValue?: string;
+  inputHint?: React.ReactNode;
+  closeLabel: string;
+  sendLabel?: string;
+  sendStatus?: HzAiChatSendStatus;
+  streamingLabel?: React.ReactNode;
+  configOpen?: boolean;
+  configTitle?: React.ReactNode;
+  configDescription?: React.ReactNode;
+  configStatus?: HzAiChatConfigStatus;
+  configStatusLabel?: React.ReactNode;
+  configTriggerLabel?: React.ReactNode;
+  configProviderLabel?: React.ReactNode;
+  configProviderHelp?: React.ReactNode;
+  configApiKeyLabel?: React.ReactNode;
+  configApiKeyHelp?: React.ReactNode;
+  configBaseUrlLabel?: React.ReactNode;
+  configBaseUrlHelp?: React.ReactNode;
+  configModelLabel?: React.ReactNode;
+  configModelHelp?: React.ReactNode;
+  configResetLabel?: React.ReactNode;
+  configSaveLabel?: React.ReactNode;
+  configCancelLabel?: React.ReactNode;
+  configProviderOptions?: HzAiChatProviderOption[];
+  configValue?: HzAiChatProviderConfigValue;
+  scheduleOpen?: boolean;
+  scheduleStatus?: HzAiChatScheduleStatus;
+  scheduleStatusLabel?: React.ReactNode;
+  scheduleTriggerLabel?: React.ReactNode;
+  scheduleTitle?: React.ReactNode;
+  scheduleConfiguredTitle?: React.ReactNode;
+  scheduleCreateTitle?: React.ReactNode;
+  scheduleAddTitle?: React.ReactNode;
+  scheduleSkillLabel?: React.ReactNode;
+  scheduleSkillSelectLabel?: React.ReactNode;
+  scheduleSkillPlaceholder?: React.ReactNode;
+  scheduleCronLabel?: React.ReactNode;
+  scheduleCronHelp?: React.ReactNode;
+  scheduleCronCommonLabel?: React.ReactNode;
+  scheduleCronMondayLabel?: React.ReactNode;
+  scheduleStatusColumnLabel?: React.ReactNode;
+  scheduleActionLabel?: React.ReactNode;
+  scheduleEnabledLabel?: React.ReactNode;
+  scheduleDisabledLabel?: React.ReactNode;
+  scheduleEditLabel?: React.ReactNode;
+  scheduleDeleteLabel?: React.ReactNode;
+  scheduleDeleteConfirmLabel?: React.ReactNode;
+  scheduleDeleteCancelLabel?: React.ReactNode;
+  scheduleDeleteStatus?: HzAiChatScheduleDeleteStatus;
+  scheduleDeleteScheduleId?: HzAiChatScheduleRow['id'] | null;
+  scheduleSaveLabel?: React.ReactNode;
+  scheduleCancelLabel?: React.ReactNode;
+  scheduleCreateLabel?: React.ReactNode;
+  scheduleRows?: HzAiChatScheduleRow[];
+  scheduleSkills?: HzAiChatScheduleSkillOption[];
+  scheduleDraft?: HzAiChatScheduleDraft;
+  scheduleEditDraft?: (HzAiChatScheduleDraft & { id: string | number }) | null;
+  initialMessageLabel?: React.ReactNode;
+  initialMessage?: string;
+  previewMessages?: HzAiChatPreviewMessage[];
+  conversationMessages?: HzAiChatPreviewMessage[];
+  messageStatus?: HzAiChatMessageStatus;
+  messageStatusLabel?: React.ReactNode;
+  conversations?: HzAiChatConversationPreview[];
+  conversationStatus?: HzAiChatConversationStatus;
+  conversationStatusLabel?: React.ReactNode;
+  onNewConversation?: () => void;
+  onConversationSelect?: (id: HzAiChatConversationPreview['id']) => void;
+  onConversationDeleteRequest?: (id: HzAiChatConversationPreview['id']) => void;
+  onConversationDeleteCancel?: () => void;
+  onConversationDeleteConfirm?: (id: HzAiChatConversationPreview['id']) => void;
+  onInputChange?: (value: string) => void;
+  onSendMessage?: () => void;
+  onConfigOpen?: () => void;
+  onConfigClose?: () => void;
+  onConfigSave?: () => void;
+  onConfigResetDefaults?: () => void;
+  onConfigChange?: (value: HzAiChatProviderConfigValue) => void;
+  onScheduleOpen?: () => void;
+  onScheduleClose?: () => void;
+  onScheduleDraftChange?: (value: HzAiChatScheduleDraft) => void;
+  onScheduleCreate?: () => void;
+  onScheduleToggle?: (id: HzAiChatScheduleRow['id'], enabled: boolean) => void;
+  onScheduleEditStart?: (row: HzAiChatScheduleRow) => void;
+  onScheduleEditCancel?: () => void;
+  onScheduleEditChange?: (value: HzAiChatScheduleDraft & { id: string | number }) => void;
+  onScheduleUpdate?: () => void;
+  onScheduleDeleteRequest?: (id: HzAiChatScheduleRow['id']) => void;
+  onScheduleDeleteCancel?: () => void;
+  onScheduleDeleteConfirm?: () => void;
+  onClose?: () => void;
+};
+
+export const HzAiChatModalSurface = React.forwardRef<HTMLDivElement, HzAiChatModalSurfaceProps>(
+  (
+    {
+      className,
+      title,
+      subtitle,
+      conversationsTitle,
+      newChatLabel,
+      welcomeTitle,
+      welcomeDescription,
+      inputPlaceholder,
... 8847 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 22/50: feat(web-next): add M10 frontend convergence gate

Posted by zh...@apache.org.
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 <zq...@dromara.org>
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: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 41/50: fix(web-next): keep topology selection out of render window

Posted by zh...@apache.org.
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 69b98c9375dade8091d66d0e2e24588a784c8742
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 09:22:00 2026 +0800

    fix(web-next): keep topology selection out of render window
---
 web-next/app/topology/page.test.tsx     | 20 +++++++++++---------
 web-next/app/topology/topology-page.tsx | 19 +++++++------------
 2 files changed, 18 insertions(+), 21 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 9d41367f09..172759c94d 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -303,7 +303,8 @@ describe('topology page', () => {
     expect(source).toContain('overlayMode="non-occluding"');
     expect(source).toContain('const topologyLiveHoverEdge = topologyHoveredDetailEdge;');
     expect(source).toContain('topologyHoveredDetailEdge ?? topologyDetailEdge');
-    expect(source).toContain('searchQuery={topologyEffectiveSearchQuery}');
+    expect(source).toContain("const topologyG6SearchQuery = topologySearchQuery ?? '';");
+    expect(source).toContain('searchQuery={topologyG6SearchQuery}');
     expect(source).toContain('topologyNodeIds');
     expect(source).not.toContain('const topologyNodeHrefById');
     expect(source).toContain('topologyNodeFocusHrefById');
@@ -463,7 +464,7 @@ describe('topology page', () => {
     expect(source).toContain('searchValue={topologyEffectiveSearchQuery}');
     expect(source).toContain("layout={topologyLayout}");
     expect(source).toContain("layout={topologyG6Layout}");
-    expect(source).toContain('searchQuery={topologyEffectiveSearchQuery}');
+    expect(source).toContain('searchQuery={topologyG6SearchQuery}');
     expect(source).not.toContain('setLoadedApiGraph(undefined);');
   });
 
@@ -844,6 +845,7 @@ describe('topology page', () => {
     expect(html).toContain(
       'data-hz-topology-g6-render-window-priority-behavior="search-only-no-selection-reorder"'
     );
+    expect(html).toContain('data-hz-topology-g6-render-window-priority-node-ids="none"');
     expect(html).toContain('data-hz-topology-g6-minimap="viewport-overview"');
     expect(html).toContain('data-hz-topology-g6-minimap-owner="hertzbeat-ui-g6-minimap"');
     expect(html).toContain('data-hz-topology-g6-minimap-visibility="assistive"');
@@ -854,14 +856,14 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-node-selection="drawer-sync"');
     expect(html).toContain('data-hz-topology-g6-edge-selection="drawer-sync"');
     expect(html).toContain('data-hz-topology-g6-hover-owner="hertzbeat-ui-g6-hover"');
-    expect(html).toContain('data-hz-topology-g6-hover-source="none"');
+    expect(html).toContain('data-hz-topology-g6-hover-source="selection"');
     expect(html).toContain('data-hz-topology-g6-hovered-node="none"');
     expect(html).toContain('data-hz-topology-g6-hovered-edge="none"');
-    expect(html).toContain('data-hz-topology-g6-active-focus-source="search-node"');
+    expect(html).toContain('data-hz-topology-g6-active-focus-source="selection"');
     expect(html).toContain('data-hz-topology-g6-search-owner="hertzbeat-ui-g6-search"');
-    expect(html).toContain('data-hz-topology-g6-search-query="checkout-api"');
-    expect(html).toContain('data-hz-topology-g6-search-match-count="1"');
-    expect(html).toContain('data-hz-topology-g6-search-first-match="svc-checkout"');
+    expect(html).toContain('data-hz-topology-g6-search-query="none"');
+    expect(html).toContain('data-hz-topology-g6-search-match-count="0"');
+    expect(html).toContain('data-hz-topology-g6-search-first-match="none"');
     expect(html).toContain('data-hz-topology-g6-selected-focus-owner="hertzbeat-ui-g6-selected-focus"');
     expect(html).toContain('data-hz-topology-g6-selected-focus-target="svc-checkout"');
     expect(html).toContain('data-hz-topology-g6-selected-focus-status="ready"');
@@ -878,7 +880,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-g6-filter-owner="hertzbeat-ui-g6-filter"');
     expect(html).toContain('data-hz-topology-g6-filter-environment="prod"');
     expect(html).toContain('data-hz-topology-g6-filter-source-kind="all"');
-    expect(html).toContain('data-hz-topology-g6-filter-search-query="checkout-api"');
+    expect(html).toContain('data-hz-topology-g6-filter-search-query="none"');
     expect(html).toContain('data-hz-topology-g6-filter-group-by="none"');
     expect(html).toContain('data-hz-topology-g6-filter-visible-node-count="7"');
     expect(html).toContain('data-hz-topology-g6-filter-visible-edge-count="7"');
@@ -1509,7 +1511,7 @@ describe('topology page', () => {
     expect(html).toContain('data-hz-topology-metric-table-render-window-owner="hertzbeat-ui-metric-table-render-window"');
     expect(html).toContain('data-hz-topology-metric-table-render-window-mode="direct"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="inactive"');
-    expect(html).toContain('data-hz-topology-metric-table-priority-node-ids="entity-501 entity-502"');
+    expect(html).toContain('data-hz-topology-metric-table-priority-node-ids="none"');
     expect(html).toContain('data-hz-topology-metric-table-visible-row-count="1"');
     expect(html).toContain('data-hz-topology-metric-table-partial-row-count="0"');
     expect(html).toContain('data-hz-topology-metric-table-hidden-row-count="0"');
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index 81f7f9e5dd..334aea845e 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -684,6 +684,7 @@ export default function TopologyPage({
     [apiOwnedEmptyGraph, loadedApiGraph, routeContext, t]
   );
   const topologyEffectiveSearchQuery = topologySearchQuery ?? map.filterContext.search;
+  const topologyG6SearchQuery = topologySearchQuery ?? '';
   const topologyIsPending = loadedApiGraph === undefined;
   const topologyTraceCallScope = map.filterContext.sourceKind === 'otlp-trace-call' || map.filterContext.viewMode === 'service-call';
   const topologyTraceCallMissingEdges =
@@ -1003,19 +1004,13 @@ export default function TopologyPage({
     ? undefined
     : map.selectedEdgeId ?? topologyDetailEdge?.id ?? topologyMetricRows[0]?.id;
   const topologySearchPriorityNodeId = React.useMemo(() => {
-    const query = map.filterContext.search.trim().toLowerCase();
+    const query = (topologySearchQuery ?? '').trim().toLowerCase();
     if (!query) return undefined;
     return topologyG6Graph.nodes.find(node => node.label.toLowerCase().includes(query) || node.id.toLowerCase().includes(query))?.id;
-  }, [map.filterContext.search, topologyG6Graph.nodes]);
+  }, [topologyG6Graph.nodes, topologySearchQuery]);
   const topologyRenderWindowPriorityNodeIds = React.useMemo(
-    () =>
-      [
-        topologySearchPriorityNodeId,
-        topologyTraceCallMissingEdges ? undefined : primaryNode?.id,
-        topologyDetailEdge?.from,
-        topologyDetailEdge?.to
-      ].filter((nodeId): nodeId is string => Boolean(nodeId)),
-    [primaryNode?.id, topologyDetailEdge?.from, topologyDetailEdge?.to, topologySearchPriorityNodeId, topologyTraceCallMissingEdges]
+    () => [topologySearchPriorityNodeId].filter((nodeId): nodeId is string => Boolean(nodeId)),
+    [topologySearchPriorityNodeId]
   );
   const topologyRenderWindowCompanion = React.useMemo(() => {
     const strategy = buildHzTopologyG6LargeGraphStrategy(topologyG6Graph);
@@ -1497,12 +1492,12 @@ export default function TopologyPage({
             selectedEdgeId={topologyCanvasSelectedEdgeId}
             hoveredNodeId={topologyG6HoveredNodeId}
             hoveredEdgeId={topologyG6HoveredEdgeId}
-            searchQuery={topologyEffectiveSearchQuery}
+            searchQuery={topologyG6SearchQuery}
             filterScope={{
               environment: map.filterContext.environment,
               sourceKind: map.filterContext.sourceKind ?? 'all',
               groupBy: map.filterContext.groupBy,
-              searchQuery: topologyEffectiveSearchQuery
+              searchQuery: topologyG6SearchQuery
             }}
             filterControls={[
               {


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 32/50: fix(web-next): remove hand-drawn topology legend marks

Posted by zh...@apache.org.
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 81c4bd6576d785ff61ebf49c7408cecb9d0472f4
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 21:46:12 2026 +0800

    fix(web-next): remove hand-drawn topology legend marks
---
 web-next/packages/hertzbeat-ui/src/index.test.tsx | 11 +++++--
 web-next/packages/hertzbeat-ui/src/index.tsx      | 36 ++++++++++-------------
 2 files changed, 23 insertions(+), 24 deletions(-)

diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index 9b0d514fb5..a36e11afc9 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -5179,7 +5179,8 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-legend-section="confidence"');
     expect(html).toContain('data-hz-topology-legend-item="stale"');
     expect(html).toContain('data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"');
-    expect(html).toContain('data-hz-topology-legend-swatch-owner="hertzbeat-ui-legend-swatch"');
+    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
+    expect(html).toContain('data-hz-topology-legend-source-label="edge token"');
     expect(html).toContain('data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"');
     expect(html).toContain('data-hz-topology-legend-item-value-owner="hertzbeat-ui-legend-item-value"');
     expect(html).toContain('data-hz-topology-legend-tone="warning"');
@@ -5226,12 +5227,16 @@ describe('@hertzbeat/ui', () => {
     expect(html).toContain('data-hz-topology-legend-summary-visibility="hidden"');
     expect(html).toContain('data-hz-topology-legend-section="status"');
     expect(html).toContain('data-hz-topology-legend-section="interaction"');
-    expect(html).toContain('data-hz-topology-legend-swatch-shape="line"');
+    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
     expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
     expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-interaction-token"');
     expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-edge-token"');
+    expect(html).toContain('data-hz-topology-legend-source-label="status token"');
+    expect(html).toContain('data-hz-topology-legend-source-label="interaction token"');
+    expect(html).toContain('data-hz-topology-legend-source-label="edge token"');
     expect(html).toContain('data-hz-topology-legend-no-handdrawn-icon="true"');
-    expect(html).toContain('data-hz-topology-legend-swatch-no-handdrawn-icon="true"');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-owner=');
+    expect(html).not.toContain('data-hz-topology-legend-swatch-shape=');
     expect(html).not.toContain('data-hz-topology-legend-swatch-shape="node-ring"');
     expect(html).not.toContain('data-hz-topology-legend-swatch-shape="selected-ring"');
     expect(html).toContain('data-hz-topology-legend-color="#22c55e"');
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx b/web-next/packages/hertzbeat-ui/src/index.tsx
index e61dfd77b4..b3f3fd412a 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -10520,6 +10520,12 @@ const topologyLegendBoundaryClassName: Record<HzTopologyLegendBoundary, string>
   flush: 'border-y border-[var(--hz-ui-line-soft)] border-x-0'
 };
 
+const topologyLegendVisualSourceLabel: Record<NonNullable<HzTopologyLegendItem['visualSource']>, string> = {
+  'hertzbeat-status-token': 'status token',
+  'hertzbeat-interaction-token': 'interaction token',
+  'hertzbeat-edge-token': 'edge token'
+};
+
 export type HzTopologyHoverTooltipKind = 'node' | 'edge';
 export type HzTopologyHoverTooltipVisibility = 'preview' | 'hover';
 export type HzTopologyHoverTooltipTrigger = 'preview' | 'live-edge-hover';
@@ -11270,16 +11276,20 @@ export function HzTopologyLegend({
                 const pattern = item.pattern ?? 'solid';
                 const color = item.color ?? chartToneColor[tone].stroke;
                 const fill = item.fill ?? chartToneColor[tone].soft;
-                const swatch = item.swatch ?? 'line';
                 const visualSource =
                   item.visualSource ??
-                  (item.id.includes('edge') ? 'hertzbeat-edge-token' : item.id.includes('selected') ? 'hertzbeat-interaction-token' : 'hertzbeat-status-token');
+                  (item.id.includes('edge') || pattern !== 'solid'
+                    ? 'hertzbeat-edge-token'
+                    : item.id.includes('selected')
+                      ? 'hertzbeat-interaction-token'
+                      : 'hertzbeat-status-token');
+                const sourceLabel = topologyLegendVisualSourceLabel[visualSource];
                 return (
                   <div
                     key={item.id}
                     className={cn(
                       'grid min-w-0 items-center text-[11px]',
-                      isCanvasDock ? 'min-h-4 grid-cols-[18px_auto] gap-1' : 'min-h-5 grid-cols-[32px_minmax(0,1fr)_auto] gap-2'
+                      isCanvasDock ? 'min-h-4 grid-cols-[auto] gap-1' : 'min-h-5 grid-cols-[minmax(0,1fr)_auto] gap-2'
                     )}
                     data-hz-topology-legend-item={item.id}
                     data-hz-topology-legend-item-owner="hertzbeat-ui-legend-item"
@@ -11287,27 +11297,11 @@ export function HzTopologyLegend({
                     data-hz-topology-legend-pattern={pattern}
                     data-hz-topology-legend-color={color}
                     data-hz-topology-legend-fill={fill}
-                    data-hz-topology-legend-swatch-shape={swatch}
                     data-hz-topology-legend-visual-source={visualSource}
+                    data-hz-topology-legend-visual-mode="source-backed-text"
+                    data-hz-topology-legend-source-label={sourceLabel}
                     data-hz-topology-legend-no-handdrawn-icon="true"
                   >
-                    <span
-                      className={cn(
-                        'shrink-0',
-                        'h-px',
-                        isCanvasDock ? 'w-4' : 'w-7'
-                      )}
-                      style={{
-                        borderTop: pattern === 'dashed' ? `1px dashed ${color}` : undefined,
-                        backgroundColor: pattern === 'dashed' ? 'transparent' : color,
-                        opacity: pattern === 'muted' ? 0.45 : 1
-                      }}
-                      aria-hidden="true"
-                      data-hz-topology-legend-swatch-owner="hertzbeat-ui-legend-swatch"
-                      data-hz-topology-legend-swatch-shape={swatch}
-                      data-hz-topology-legend-swatch-source={visualSource}
-                      data-hz-topology-legend-swatch-no-handdrawn-icon="true"
-                    />
                     <span
                       className="min-w-0 truncate text-[#cbd3df]"
                       data-hz-topology-legend-item-label-owner="hertzbeat-ui-legend-item-label"


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 08/50: feat(web-next): restore shell utility parity

Posted by zh...@apache.org.
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 f114f1c6d113e8fd6a7c16916ce74d16f21b75d2
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 00:39:49 2026 +0800

    feat(web-next): restore shell utility parity
---
 .../components/shell/app-frame.chrome.test.tsx     |  479 +++++---
 web-next/components/shell/app-frame.tsx            | 1174 ++++++++++++++++++--
 web-next/components/shell/app-sidebar.test.tsx     |   66 +-
 web-next/components/shell/app-sidebar.tsx          |   21 +-
 web-next/components/shell/auth-gate.tsx            |   21 +-
 .../components/shell/locale-option-list.test.tsx   |   16 +-
 web-next/components/shell/locale-option-list.tsx   |   18 +-
 .../shell/platform-copyright-footer.test.tsx       |   10 +-
 .../components/shell/platform-copyright-footer.tsx |    1 -
 web-next/lib/ai-chat/conversations.test.ts         |  232 ++++
 web-next/lib/ai-chat/conversations.ts              |  336 ++++++
 web-next/lib/ai-chat/provider-config.test.ts       |  106 ++
 web-next/lib/ai-chat/provider-config.ts            |  111 ++
 web-next/lib/ai-chat/schedules.test.ts             |  155 +++
 web-next/lib/ai-chat/schedules.ts                  |  235 ++++
 web-next/lib/app-frame-state.test.ts               |    7 +
 web-next/lib/app-frame-state.ts                    |    4 +-
 web-next/lib/shell/about.test.ts                   |   69 ++
 web-next/lib/shell/about.ts                        |   44 +
 web-next/lib/shell/header-mute.test.ts             |   86 ++
 web-next/lib/shell/header-mute.ts                  |   85 ++
 web-next/lib/shell/header-realtime.test.ts         |   74 ++
 web-next/lib/shell/header-realtime.ts              |   85 ++
 web-next/lib/shell/locale-reload.test.ts           |   21 +
 web-next/lib/shell/locale-reload.ts                |   14 +
 web-next/lib/shell/sidebar.test.ts                 |   62 +-
 web-next/lib/shell/sidebar.ts                      |   36 +-
 27 files changed, 3258 insertions(+), 310 deletions(-)

diff --git a/web-next/components/shell/app-frame.chrome.test.tsx b/web-next/components/shell/app-frame.chrome.test.tsx
index 2f1829bb7b..1a0d752486 100644
--- a/web-next/components/shell/app-frame.chrome.test.tsx
+++ b/web-next/components/shell/app-frame.chrome.test.tsx
@@ -1,6 +1,9 @@
 import * as React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 
 vi.mock('next/image', () => ({
   default: ({ alt, priority: _priority, ...props }: React.ComponentProps<'img'> & { priority?: boolean }) => (
@@ -25,8 +28,8 @@ vi.mock('next/navigation', () => ({
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
-    t: (key: string) => `translated:${key}`,
-    locale: 'en-US',
+    t: createTranslatorMock({ locale: 'zh-CN' }),
+    locale: 'zh-CN',
     locales: ['en-US', 'zh-CN'],
     setLocale: vi.fn(async () => undefined)
   })
@@ -45,7 +48,9 @@ vi.mock('@/components/shell/app-sidebar', () => ({
 }));
 
 vi.mock('@/lib/api-client', () => ({
-  apiGet: vi.fn()
+  apiGet: vi.fn(),
+  apiMessageGet: vi.fn(),
+  apiMessagePost: vi.fn()
 }));
 
 vi.mock('@/lib/app-frame-state', () => ({
@@ -69,216 +74,112 @@ vi.mock('@/lib/utils', () => ({
   cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' ')
 }));
 
+const setupT = createTranslatorMock({ locale: 'zh-CN' });
+
+const emptySetupState = {
+  monitorTotal: 0,
+  collectorTotal: 0,
+  entityTotal: 0,
+  hasEvidenceLinked: false,
+  definitionCount: 0,
+  governanceCount: 0
+};
+
 describe('app frame chrome', () => {
   it('does not keep the startup baseline once live header setup state resolves empty', async () => {
     const { buildSetupSummary } = await import('./app-frame');
-    const summary = buildSetupSummary(
-      {
-        monitorTotal: 0,
-        collectorTotal: 0,
-        entityTotal: 0,
-        hasEvidenceLinked: false,
-        definitionCount: 0,
-        governanceCount: 0
-      },
-      (key: string, params?: Record<string, string | number | null | undefined>) =>
-        params?.percent != null ? `${key}:${params.percent}` : key
-    );
+    const summary = buildSetupSummary(emptySetupState, setupT);
 
     expect(summary.completedCount).toBe(0);
     expect(summary.progressPercent).toBe(0);
-    expect(summary.headline).toBe('layout.setup.progress.headline:0');
-  });
+    expect(summary.headline).toBe('你已完成 0% 的平台配置');
+  }, 15000);
 
   it('keeps the startup setup baseline on the entity create route when the live setup state is empty', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const summary = buildRouteSetupSummary(
-      '/entities/new',
-      {
-        monitorTotal: 0,
-        collectorTotal: 0,
-        entityTotal: 0,
-        hasEvidenceLinked: false,
-        definitionCount: 0,
-        governanceCount: 0
-      },
-      (key: string, params?: Record<string, string | number | null | undefined>) =>
-        params?.percent != null ? `${key}:${params.percent}` : key
-    );
+    const summary = buildRouteSetupSummary('/entities/new', emptySetupState, setupT);
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('layout.setup.progress.headline:83');
-  });
+    expect(summary.headline).toBe('你已完成 83% 的平台配置');
+  }, 15000);
 
   it('keeps the startup setup baseline on the entity import route when the live setup state is empty', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const summary = buildRouteSetupSummary(
-      '/entities/import',
-      {
-        monitorTotal: 0,
-        collectorTotal: 0,
-        entityTotal: 0,
-        hasEvidenceLinked: false,
-        definitionCount: 0,
-        governanceCount: 0
-      },
-      (key: string, params?: Record<string, string | number | null | undefined>) =>
-        params?.percent != null ? `${key}:${params.percent}` : key
-    );
+    const summary = buildRouteSetupSummary('/entities/import', emptySetupState, setupT);
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('layout.setup.progress.headline:83');
+    expect(summary.headline).toBe('你已完成 83% 的平台配置');
   });
 
   it('keeps the startup setup baseline on the entity discovery route when the live setup state is empty', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const summary = buildRouteSetupSummary(
-      '/entities/discovery',
-      {
-        monitorTotal: 0,
-        collectorTotal: 0,
-        entityTotal: 0,
-        hasEvidenceLinked: false,
-        definitionCount: 0,
-        governanceCount: 0
-      },
-      (key: string, params?: Record<string, string | number | null | undefined>) =>
-        params?.percent != null ? `${key}:${params.percent}` : key
-    );
+    const summary = buildRouteSetupSummary('/entities/discovery', emptySetupState, setupT);
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('layout.setup.progress.headline:83');
+    expect(summary.headline).toBe('你已完成 83% 的平台配置');
   });
 
   it('keeps the startup setup baseline on overview routes when the live setup state is empty', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const t = (key: string, params?: Record<string, string | number | null | undefined>) =>
-      params?.percent != null ? `${key}:${params.percent}` : key;
-    const emptyState = {
-      monitorTotal: 0,
-      collectorTotal: 0,
-      entityTotal: 0,
-      hasEvidenceLinked: false,
-      definitionCount: 0,
-      governanceCount: 0
-    };
 
     for (const pathname of ['/overview', '/dashboard']) {
-      const summary = buildRouteSetupSummary(pathname, emptyState, t);
+      const summary = buildRouteSetupSummary(pathname, emptySetupState, setupT);
 
       expect(summary.completedCount).toBe(5);
       expect(summary.progressPercent).toBe(83);
-      expect(summary.headline).toBe('layout.setup.progress.headline:83');
+      expect(summary.headline).toBe('你已完成 83% 的平台配置');
     }
   });
 
   it('keeps the startup setup baseline on dark-ops compatibility routes when the live setup state is empty', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const t = (key: string, params?: Record<string, string | number | null | undefined>) =>
-      params?.percent != null ? `${key}:${params.percent}` : key;
-    const emptyState = {
-      monitorTotal: 0,
-      collectorTotal: 0,
-      entityTotal: 0,
-      hasEvidenceLinked: false,
-      definitionCount: 0,
-      governanceCount: 0
-    };
 
     for (const pathname of ['/incidents', '/actions']) {
-      const summary = buildRouteSetupSummary(pathname, emptyState, t);
+      const summary = buildRouteSetupSummary(pathname, emptySetupState, setupT);
 
       expect(summary.completedCount).toBe(5);
       expect(summary.progressPercent).toBe(83);
-      expect(summary.headline).toBe('layout.setup.progress.headline:83');
+      expect(summary.headline).toBe('你已完成 83% 的平台配置');
     }
   });
 
   it('keeps the startup setup baseline on the log stream compatibility route when the live setup state is empty', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const summary = buildRouteSetupSummary(
-      '/log/stream',
-      {
-        monitorTotal: 0,
-        collectorTotal: 0,
-        entityTotal: 0,
-        hasEvidenceLinked: false,
-        definitionCount: 0,
-        governanceCount: 0
-      },
-      (key: string, params?: Record<string, string | number | null | undefined>) =>
-        params?.percent != null ? `${key}:${params.percent}` : key
-    );
+    const summary = buildRouteSetupSummary('/log/stream', emptySetupState, setupT);
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('layout.setup.progress.headline:83');
+    expect(summary.headline).toBe('你已完成 83% 的平台配置');
   });
 
   it('keeps the startup setup baseline on the OTLP intake route when the live setup state is empty', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const summary = buildRouteSetupSummary(
-      '/ingestion/otlp',
-      {
-        monitorTotal: 0,
-        collectorTotal: 0,
-        entityTotal: 0,
-        hasEvidenceLinked: false,
-        definitionCount: 0,
-        governanceCount: 0
-      },
-      (key: string, params?: Record<string, string | number | null | undefined>) =>
-        params?.percent != null ? `${key}:${params.percent}` : key
-    );
+    const summary = buildRouteSetupSummary('/ingestion/otlp', emptySetupState, setupT);
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('layout.setup.progress.headline:83');
+    expect(summary.headline).toBe('你已完成 83% 的平台配置');
   });
 
   it('keeps the startup setup baseline on entity definition routes when the live setup state is empty', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const summary = buildRouteSetupSummary(
-      '/entities/1/definition',
-      {
-        monitorTotal: 0,
-        collectorTotal: 0,
-        entityTotal: 0,
-        hasEvidenceLinked: false,
-        definitionCount: 0,
-        governanceCount: 0
-      },
-      (key: string, params?: Record<string, string | number | null | undefined>) =>
-        params?.percent != null ? `${key}:${params.percent}` : key
-    );
+    const summary = buildRouteSetupSummary('/entities/1/definition', emptySetupState, setupT);
 
     expect(summary.completedCount).toBe(5);
     expect(summary.progressPercent).toBe(83);
-    expect(summary.headline).toBe('layout.setup.progress.headline:83');
+    expect(summary.headline).toBe('你已完成 83% 的平台配置');
   });
 
   it('keeps the live empty setup state on the entity edit route', async () => {
     const { buildRouteSetupSummary } = await import('./app-frame');
-    const summary = buildRouteSetupSummary(
-      '/entities/1/edit',
-      {
-        monitorTotal: 0,
-        collectorTotal: 0,
-        entityTotal: 0,
-        hasEvidenceLinked: false,
-        definitionCount: 0,
-        governanceCount: 0
-      },
-      (key: string, params?: Record<string, string | number | null | undefined>) =>
-        params?.percent != null ? `${key}:${params.percent}` : key
-    );
+    const summary = buildRouteSetupSummary('/entities/1/edit', emptySetupState, setupT);
 
     expect(summary.completedCount).toBe(0);
     expect(summary.progressPercent).toBe(0);
-    expect(summary.headline).toBe('layout.setup.progress.headline:0');
+    expect(summary.headline).toBe('你已完成 0% 的平台配置');
   });
 
   it('renders shared header utility triggers with icon-only parity chrome on ops routes', async () => {
@@ -293,34 +194,308 @@ describe('app frame chrome', () => {
     expect(html).toContain('data-app-frame-icon-trigger="github"');
     expect(html).toContain('data-app-frame-icon-trigger="menu"');
     expect(html).toContain('data-app-frame-icon-trigger="notify"');
+    expect(html).toContain('data-app-frame-header-realtime-sse-contract="angular-alert-and-manager-sse"');
+    expect(html).toContain('data-app-frame-header-realtime-sse-owner="route-realtime-sse-contract"');
+    expect(html).toContain('data-app-frame-header-realtime-alert-source="/api/alert/sse/subscribe"');
+    expect(html).toContain('data-app-frame-header-realtime-manager-source="/api/manager/sse/subscribe"');
     expect(html).toContain('data-app-frame-icon-trigger="mute"');
+    expect(html).toContain('data-hz-header-icon-button-owner="hertzbeat-ui-header-icon-button"');
+    expect(html).toContain('data-hz-header-icon-button-density="angular-header-item"');
+    expect(html).toContain('data-app-frame-mute-state="muted"');
+    expect(html).toContain('data-app-frame-mute-save-lifecycle="angular-success-only-state-update"');
+    expect(html).toContain('data-app-frame-mute-save-lifecycle-owner="route-action-state-contract"');
     expect(html).toContain('data-app-frame-icon-trigger="lock"');
     expect(html).toContain('data-app-frame-icon-trigger="settings"');
+    expect(html).toContain('data-app-frame-locale-reload-contract="angular-load-use-layout-reload"');
+    expect(html).toContain('data-app-frame-locale-reload-owner="route-locale-reload-contract"');
     expect(html).toContain('data-app-frame-icon-trigger="user"');
+    expect(html).toContain('data-app-frame-user-name="admin"');
+    expect(html).toContain('data-app-frame-user-role="unknown"');
+    expect(html).toContain('data-app-frame-user-logout-lifecycle-contract="angular-clear-then-passport-login"');
+    expect(html).toContain('data-app-frame-user-logout-lifecycle-owner="route-session-contract"');
+    expect(html).toContain('data-app-frame-about-closable-contract="angular-nz-closable-false"');
+    expect(html).toContain('data-app-frame-about-cancel-contract="angular-on-cancel"');
     expect(html).toContain('data-app-frame-glyph="notify-bell"');
     expect(html).toContain('data-app-frame-glyph="mute-megaphone"');
     expect(html).toContain('data-app-frame-glyph="settings-gear"');
     expect(html).toContain('data-app-frame-user-avatar="angular-circle"');
-    expect(html).toContain('translated:layout.setup.title');
+    expect(html).toContain('开始使用');
     expect(html).toContain('data-app-frame-setup-progress="angular-reference"');
     expect(html).toContain('style="width:83%"');
-    expect(html).not.toContain('translated:common.setup');
+    expect(html).not.toContain('common.setup');
     expect(html).toContain('data-platform-footer="angular-footer"');
-    expect(html).toContain('class="relative min-w-0 self-start"');
+    expect(html).toContain('class="relative flex min-h-0 min-w-0 flex-col self-stretch"');
+    expect(html).toContain('data-platform-main-scroll="content-flow"');
+    expect(html).toContain('data-platform-footer-placement="flow-end-or-viewport-bottom"');
     expect(html).toContain('Apache HertzBeat™ v1.8.0');
-    expect(html).toContain('Licensed under the Apache License, Version 2.0');
-    expect(html).toContain('data-shell-help-launcher="angular-help"');
-    expect(html).toContain('class="sr-only">translated:common.help</span>');
+    expect(html).not.toContain('遵循 Apache License, Version 2.0 授权');
+    expect(html).not.toContain('Apache License, Version 2.0');
+    expect(html).toContain('data-shell-ai-chat-launcher="angular-ai-chat"');
+    expect(html).toContain('class="sr-only">打开 AI 助手</span>');
     expect(html).toContain('class="sr-only">GitHub</span>');
-    expect(html).toContain('class="sr-only">Menu</span>');
-    expect(html).toContain('class="sr-only">translated:common.notify</span>');
-    expect(html).toContain('class="sr-only">translated:common.mute</span>');
-    expect(html).toContain('class="sr-only">translated:common.lock</span>');
-    expect(html).toContain('class="sr-only">translated:menu.settings</span>');
-    expect(html).toContain('class="sr-only">admin</span>');
+    expect(html).toContain('class="sr-only">菜单</span>');
+    expect(html).toContain('class="sr-only">通知</span>');
+    expect(html).toContain('class="sr-only">取消静音</span>');
+    expect(html).toContain('class="sr-only">锁屏</span>');
+    expect(html).toContain('class="sr-only">设置</span>');
+    expect(html).toContain('class="sr-only">用户菜单:admin</span>');
+    expect(html).toContain('placeholder="询问助手"');
+    expect(html).toContain('aria-label="发送提示"');
     expect(html).toContain('overview-body');
   });
 
+  it('keeps the fullscreen settings action synchronized with the Angular header-fullscreen contract', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+
+    expect(source).toContain('HzHeaderMenuAction');
+    expect(source).toContain('const [fullscreenActive, setFullscreenActive] = useState(false)');
+    expect(source).toContain("const fullscreenLabel = fullscreenActive ? t('menu.fullscreen.exit') : t('menu.fullscreen')");
+    expect(source).toContain('document.fullscreenElement');
+    expect(source).toContain('document.addEventListener(\'fullscreenchange\', syncFullscreenStatus)');
+    expect(source).toContain('document.removeEventListener(\'fullscreenchange\', syncFullscreenStatus)');
+    expect(source).toContain('document.exitFullscreen()');
+    expect(source).toContain('document.documentElement.requestFullscreen()');
+    expect(source).toContain('data-app-frame-settings-fullscreen-action="angular-toggle"');
+    expect(source).toContain("state={fullscreenActive ? 'active' : 'inactive'}");
+    expect(source).toContain('data-app-frame-glyph="fullscreen-exit"');
+    expect(source).toContain('data-app-frame-glyph="fullscreen-enter"');
+  });
+
+  it('keeps the locale settings action on the Angular header-i18n reload contract', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+    const localeReloadSource = readFileSync(resolve(__dirname, '../../lib/shell/locale-reload.ts'), 'utf8');
+
+    expect(source).toContain('showHeaderLocaleReloadSpinner()');
+    expect(source).toContain('await setLocale(nextLocale);');
+    expect(source).toContain('window.location.reload();');
+    expect(source).toContain('data-app-frame-locale-reload-contract="angular-load-use-layout-reload"');
+    expect(source).toContain('data-app-frame-locale-reload-owner="route-locale-reload-contract"');
+    expect(localeReloadSource).toContain("HEADER_LOCALE_RELOAD_SPINNER_CLASS = 'page-loading ant-spin ant-spin-lg ant-spin-spinning'");
+    expect(localeReloadSource).toContain("HEADER_LOCALE_RELOAD_SPINNER_MARKER = 'angular-header-i18n-reload'");
+    expect(localeReloadSource).toContain('ant-spin-dot ant-spin-dot-spin');
+  });
+
+  it('keeps the AI chat launcher on the Angular modal path instead of redirecting to overview query state', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+
+    expect(source).toContain("data-app-frame-ai-chat-input=\"angular-header-ai-chat\"");
+    expect(source).toContain('data-app-frame-ai-chat-initial-message-contract="angular-open-modal-initial-message"');
+    expect(source).toContain('data-app-frame-ai-chat-initial-message-owner="route-ai-chat-modal-contract"');
+    expect(source).toContain('data-shell-ai-chat-launcher="angular-ai-chat"');
+    expect(source).toContain('data-app-frame-ai-chat-launcher-contract="angular-empty-modal"');
+    expect(source).toContain('data-app-frame-ai-chat-config-save-lifecycle-contract="angular-validate-save-close-refresh"');
+    expect(source).toContain('data-app-frame-ai-chat-config-save-lifecycle-owner="route-ai-chat-config-contract"');
+    expect(source).toContain('data-app-frame-ai-chat-conversation-action-lifecycle-contract="angular-create-select-delete-fallback"');
+    expect(source).toContain('data-app-frame-ai-chat-conversation-action-lifecycle-owner="route-ai-chat-conversation-contract"');
+    expect(source).toContain('data-app-frame-ai-chat-schedule-action-lifecycle-contract="angular-load-create-toggle-revert-confirm-update-delete"');
+    expect(source).toContain('data-app-frame-ai-chat-schedule-action-lifecycle-owner="route-ai-chat-schedule-contract"');
+    expect(source).toContain('data-app-frame-ai-chat-stream-history-lifecycle-contract="angular-push-user-placeholder-sse-skill-report-refresh"');
+    expect(source).toContain('data-app-frame-ai-chat-stream-history-lifecycle-owner="route-ai-chat-stream-contract"');
+    expect(source).toContain('data-app-frame-ai-chat-owner="hertzbeat-ui-ai-chat-modal"');
+    expect(source).toContain("data-app-frame-ai-chat-initial-message-contract={aiInitialPrompt ? 'angular-open-modal-initial-message' : 'angular-empty-modal'}");
+    expect(source).toContain('void loadAiProviderConfig(true)');
+    expect(source).toContain("setAiConfigStatusLabelKey('ai.chat.config.provider.required')");
+    expect(source).toContain("setAiConfigStatusLabelKey('ai.chat.config.api-key.required')");
+    expect(source).toContain("setAiConfigStatusLabelKey('ai.chat.config.base-url.required')");
+    expect(source).toContain("setAiConfigStatusLabelKey('ai.chat.config.model.required')");
+    expect(source).toContain('loadAiChatConversations()');
+    expect(source).toContain('loadAiChatConversationHistory(selectedAiConversationId)');
+    expect(source).toContain('createAiChatConversation()');
+    expect(source).toContain("setAiNewConversationStatus('creating')");
+    expect(source).toContain('setSelectedAiConversationId(next.conversation.id)');
+    expect(source).toContain('deleteAiChatConversation(conversationId)');
+    expect(source).toContain("setAiDeleteConversationStatus('confirming')");
+    expect(source).toContain("setAiDeleteConversationStatus('deleting')");
+    expect(source).toContain('const conversations = aiConversations.conversations.filter');
+    expect(source).toContain('await createNewAiConversation();');
+    expect(source).toContain('conversationStatus={aiConversations.status}');
+    expect(source).toContain('conversationStatusLabel={t(aiConversations.statusLabelKey)}');
+    expect(source).toContain('newChatStatus={aiNewConversationStatus}');
+    expect(source).toContain('onNewConversation={createNewAiConversation}');
+    expect(source).toContain('deleteStatus={aiDeleteConversationStatus}');
+    expect(source).toContain('deleteConversationId={aiDeleteConversationId}');
+    expect(source).toContain('onConversationDeleteConfirm={confirmDeleteAiConversation}');
+    expect(source).toContain('setAiDraftMessage(nextPrompt)');
+    expect(source).toContain('setAiLocalMessages(current =>');
+    expect(source).toContain('const nextContent = processAiChatStreamContent(`${lastMessage.content}${content}`);');
+    expect(source).toContain('content.substring(markerIndex + AI_CHAT_SKILL_REPORT_MARKER.length).replace(/^\\n+/, \'\')');
+    expect(source).toContain('appendOfflineAiAssistantResponse()');
+    expect(source).toContain('streamAiChatResponse(messageContent');
+    expect(source).toContain('onChunk: chunk => appendAiStreamChunk(chunk.content)');
+    expect(source).toContain('replaceLastStreamingAiAssistantMessage(current, t(');
+    expect(source).toContain("const AI_CHAT_SKILL_REPORT_MARKER = '[[SKILL_REPORT]]'");
+    expect(source).toContain('void refreshAiConversationAfterStream(streamConversationId)');
+    expect(source).toContain("content: t('ai.chat.offline.response')");
+    expect(source).toContain('loadAiChatProviderConfig()');
+    expect(source).toContain('saveAiChatProviderConfig(nextConfig)');
+    expect(source).toContain("if (result.status === 'saved')");
+    expect(source).toContain('setAiConfigOpen(false)');
+    expect(source).toContain('buildDefaultAiChatProviderConfig()');
+    expect(source).toContain('configOpen={aiConfigOpen}');
+    expect(source).toContain("configTriggerLabel={t('ai.chat.modify-api-key')}");
+    expect(source).toContain('configProviderOptions={AI_CHAT_PROVIDER_OPTIONS}');
+    expect(source).toContain('configValue={aiProviderConfig}');
+    expect(source).toContain('onConfigOpen={openAiProviderConfig}');
+    expect(source).toContain('onConfigSave={saveAiProviderConfig}');
+    expect(source).toContain('onConfigResetDefaults={resetAiProviderDefaults}');
+    expect(source).toContain('onConfigChange={changeAiProviderConfig}');
+    expect(source).toContain('loadAiChatSchedules(selectedAiConversationId)');
+    expect(source).toContain('loadAiChatScheduleSkills()');
+    expect(source).toContain('createAiChatSchedule(selectedAiConversationId, aiScheduleDraft)');
+    expect(source).toContain('setAiSchedules(current => current.map(row => String(row.id) === String(scheduleId) ? { ...row, enabled } : row));');
+    expect(source).toContain('toggleAiChatSchedule(scheduleId, enabled)');
+    expect(source).toContain('setAiSchedules(current => current.map(row => String(row.id) === String(scheduleId) ? { ...row, enabled: !enabled } : row));');
+    expect(source).toContain('updateAiChatSchedule(aiScheduleEditDraft.id, selectedAiConversationId, aiScheduleEditDraft)');
+    expect(source).toContain("setAiDeleteScheduleStatus('confirming')");
+    expect(source).toContain("setAiDeleteScheduleStatus('deleting')");
+    expect(source).toContain('deleteAiChatSchedule(scheduleId)');
+    expect(source).toContain('scheduleOpen={aiScheduleOpen}');
+    expect(source).toContain("scheduleTriggerLabel={t('ai.chat.schedule.button')}");
+    expect(source).toContain('scheduleRows={aiSchedules}');
+    expect(source).toContain('scheduleSkills={aiScheduleSkills}');
+    expect(source).toContain('scheduleDraft={aiScheduleDraft}');
+    expect(source).toContain('scheduleEditDraft={aiScheduleEditDraft}');
+    expect(source).toContain('onScheduleOpen={openAiSchedulePanel}');
+    expect(source).toContain('onScheduleCreate={createAiScheduleFromDraft}');
+    expect(source).toContain('onScheduleToggle={toggleAiSchedule}');
+    expect(source).toContain('onScheduleUpdate={updateAiScheduleFromDraft}');
+    expect(source).toContain('scheduleDeleteStatus={aiDeleteScheduleStatus}');
+    expect(source).toContain('scheduleDeleteScheduleId={aiDeleteScheduleId}');
+    expect(source).toContain('onScheduleDeleteRequest={requestDeleteAiSchedule}');
+    expect(source).toContain('onScheduleDeleteCancel={cancelDeleteAiSchedule}');
+    expect(source).toContain('onScheduleDeleteConfirm={confirmDeleteAiSchedule}');
+    expect(source).toContain('inputValue={aiDraftMessage}');
+    expect(source).toContain('onInputChange={setAiDraftMessage}');
+    expect(source).toContain('onSendMessage={sendAiMessage}');
+    expect(source).toContain('sendStatus={aiSendStatus}');
+    expect(source).toContain("streamingLabel={t('ai.chat.typing')}");
+    expect(source).toContain('messageStatus={aiMessageStatus}');
+    expect(source).toContain('messageStatusLabel={t(aiMessageStatusLabelKey)}');
+    expect(source).toContain('conversationMessages={aiConversationMessageRows}');
+    expect(source).toContain('const nextPrompt = initialPrompt.trim()');
+    expect(source).toContain('setAiInitialPrompt(nextPrompt)');
+    expect(source).toContain('setAiChatOpen(true)');
+    expect(source).toContain('openAiChatModal(query)');
+    expect(source).not.toContain('/overview?ai=');
+  });
+
+  it('keeps notification timestamp fallbacks localized', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+
+    expect(source).toContain('formatHeaderTime(item.activeAt, emptyValue)');
+    expect(source).toContain("const emptyValue = t('common.none');");
+    expect(source).not.toContain("if (!value) return '-'");
+  });
+
+  it('keeps the mute toggle action label synchronized for aria and screen readers', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+    const muteSource = readFileSync(resolve(__dirname, '../../lib/shell/header-mute.ts'), 'utf8');
+
+    expect(source).toContain(
+      "const notificationMuteLabel = notificationsMuted ? t('common.unmute') : t('common.mute');"
+    );
+    expect(source).toContain('loadHeaderMuteConfig()');
+    expect(source).toContain('saveHeaderMuteConfig(nextMuted)');
+    expect(muteSource).toContain("apiMessageGet<Partial<HeaderMuteConfig> | boolean>('/config/mute')");
+    expect(muteSource).toContain("apiMessagePost<unknown>('/config/mute', { mute: muted })");
+    expect(muteSource).toContain("status: 'error'");
+    expect(source).toContain("window.Notification.requestPermission()");
+    expect(source).toContain('<HzHeaderIconButton');
+    expect(source).toContain('label={notificationMuteLabel}');
+    expect(source).toContain("state={notificationsMuted ? 'active' : 'inactive'}");
+    expect(source).toContain("data-app-frame-mute-state={notificationsMuted ? 'muted' : 'audible'}");
+    expect(source).toContain('data-app-frame-mute-save-lifecycle="angular-success-only-state-update"');
+    expect(source).toContain('data-app-frame-mute-save-lifecycle-owner="route-action-state-contract"');
+    expect(source).toContain('<span>{notificationMuteLabel}</span>');
+    expect(source).not.toContain("notificationsMuted ? t('common.mute') : t('common.unmute')");
+    expect(source).not.toContain("window.localStorage.setItem(HEADER_MUTE_STORAGE_KEY");
+  });
+
+  it('keeps the header notification SSE, sound, and browser notification parity contract', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+    const realtimeSource = readFileSync(resolve(__dirname, '../../lib/shell/header-realtime.ts'), 'utf8');
+
+    expect(source).toContain('HzHeaderRealtimeNotice');
+    expect(source).toContain('HEADER_ALERT_SSE_URL');
+    expect(source).toContain('HEADER_DUAL_SSE_CONTRACT');
+    expect(source).toContain('HEADER_MANAGER_SSE_URL');
+    expect(source).toContain('HEADER_ALERT_EVENT_TYPE');
+    expect(source).toContain('HEADER_IMPORT_TASK_EVENT_TYPE');
+    expect(source).toContain('new window.EventSource(HEADER_ALERT_SSE_URL)');
+    expect(source).toContain('new window.EventSource(HEADER_MANAGER_SSE_URL)');
+    expect(source).toContain('buildHeaderNoticeFromAlert(alert');
+    expect(source).toContain('mergeHeaderNoticeEvent(current, notice)');
+    expect(source).toContain('resolveHeaderAlertSoundSrc(locale)');
+    expect(source).toContain("new window.Notification(t('alert.notify.title')");
+    expect(source).toContain('notifiedAlertIdsRef.current.has(alert.id)');
+    expect(source).toContain('data-app-frame-header-realtime-toast="angular-sse"');
+    expect(source).toContain('data-app-frame-header-realtime-sse-contract={HEADER_DUAL_SSE_CONTRACT}');
+    expect(source).toContain('data-app-frame-header-realtime-sse-owner="route-realtime-sse-contract"');
+    expect(source).toContain('data-app-frame-header-realtime-alert-source={HEADER_ALERT_SSE_URL}');
+    expect(source).toContain('data-app-frame-header-realtime-manager-source={HEADER_MANAGER_SSE_URL}');
+    expect(source).toContain('data-app-frame-header-realtime-notice-owner="hertzbeat-ui-header-realtime-notice"');
+    expect(realtimeSource).toContain("HEADER_ALERT_SSE_URL = '/api/alert/sse/subscribe'");
+    expect(realtimeSource).toContain("HEADER_MANAGER_SSE_URL = '/api/manager/sse/subscribe'");
+    expect(realtimeSource).toContain("HEADER_DUAL_SSE_CONTRACT = 'angular-alert-and-manager-sse'");
+    expect(realtimeSource).toContain("HEADER_ALERT_SOUND_CN = '/assets/audio/default-alert-CN.mp3'");
+    expect(realtimeSource).toContain("HEADER_ALERT_SOUND_EN = '/assets/audio/default-alert-EN.mp3'");
+  });
+
+  it('keeps the user menu about action on the Angular modal contract', () => {
+    const source = readFileSync(resolve(__dirname, 'app-frame.tsx'), 'utf8');
+    const aboutSource = readFileSync(resolve(__dirname, '../../lib/shell/about.ts'), 'utf8');
+    const sessionClientSource = readFileSync(resolve(__dirname, '../../lib/session-client.ts'), 'utf8');
+
+    expect(source).toContain('HzUserMenuAction');
+    expect(source).toContain('readClientSessionUserSnapshot()');
+    expect(source).toContain("const currentUserName = sessionUser?.name || 'admin'");
+    expect(source).toContain('data-app-frame-user-name={currentUserName}');
+    expect(source).toContain("data-app-frame-user-role={sessionUser?.role || 'unknown'}");
+    expect(source).toContain('data-app-frame-user-menu="angular-width-sm"');
+    expect(source).toContain('data-app-frame-user-action="setting"');
+    expect(source).toContain('data-app-frame-user-action="logout"');
+    expect(source).toContain('data-app-frame-user-logout-lifecycle="angular-clear-then-passport-login"');
+    expect(source).toContain('data-app-frame-user-logout-lifecycle-owner="route-session-contract"');
+    expect(source).toContain('data-app-frame-user-action="about"');
+    expect(source).toContain("t('menu.extras.setting')");
+    expect(source).toContain("t('menu.account.logout')");
+    expect(source).toContain("t('menu.extras.about')");
+    expect(source).toContain('data-app-frame-user-menu-icon="tool"');
+    expect(source).toContain('data-app-frame-user-menu-icon="logout"');
+    expect(source).toContain('data-app-frame-user-menu-icon="environment"');
+    expect(source).toContain('HzAboutModalSurface');
+    expect(source).toContain('data-app-frame-user-about-action="angular-modal"');
+    expect(source).toContain('data-app-frame-about-modal="angular-user-menu"');
+    expect(source).toContain('data-app-frame-about-closable-contract="angular-nz-closable-false"');
+    expect(source).toContain('data-app-frame-about-cancel-contract="angular-on-cancel"');
+    expect(source).toContain('openAboutModal');
+    expect(source).toContain('readAboutNotShowNextLogin()');
+    expect(source).toContain('writeAboutNotShowNextLogin(checked)');
+    expect(source).toContain('consumeAboutAutoShowAfterLogin()');
+    expect(source).toContain('ABOUT_AUTO_CLOSE_AFTER_LOGIN_MS = 20_000');
+    expect(source).toContain('setAboutOpen(true);');
+    expect(source).toContain('window.setTimeout(() => {');
+    expect(source).toContain('setAboutOpen(false);');
+    expect(source).toContain("releaseHref={`https://github.com/apache/hertzbeat/releases/tag/${PLATFORM_FOOTER_VERSION}`}");
+    expect(source).toContain("t('about.not-show-next-login')");
+    expect(source).toContain("t('about.point.6')");
+    expect(source).toContain("t('about.upgrade')");
+    expect(aboutSource).toContain("ABOUT_NOT_SHOW_NEXT_LOGIN_KEY = 'NOT_SHOW_ABOUT_NEXT_LOGIN'");
+    expect(aboutSource).toContain("ABOUT_AUTO_SHOW_AFTER_LOGIN_KEY = 'HB_ABOUT_AUTO_SHOW_AFTER_LOGIN'");
+    expect(aboutSource).toContain('markAboutAutoShowAfterLogin');
+    expect(aboutSource).toContain('consumeAboutAutoShowAfterLogin');
+    expect(sessionClientSource).toContain("HB_UI_SESSION_USER_KEY = 'HB_UI_SESSION_USER'");
+    expect(sessionClientSource).toContain('readClientSessionUserSnapshot');
+    expect(sessionClientSource).toContain('clearClientSessionUserSnapshot();');
+    expect(sessionClientSource).toContain('finally {');
+    expect(sessionClientSource).toContain('clearClientSessionMarker();');
+    expect(source).toContain('void clearClientSession().finally(() => {');
+    expect(source).toContain('window.location.href = buildLoginRedirectHref(undefined, process.env.NEXT_PUBLIC_LOGIN_PATH);');
+    expect(source).not.toContain('href="https://hertzbeat.apache.org/docs/"\\n                          target="_blank"');
+    expect(source).not.toContain("t('common.about')");
+    expect(source).not.toContain("t('common.logout')");
+  });
+
   it('lets OTLP metrics remove the app-frame padding shell instead of nesting another workbench layer', async () => {
     const { buildContentFrameClassName, buildContentFrameShellKind } = await import('./app-frame');
 
diff --git a/web-next/components/shell/app-frame.tsx b/web-next/components/shell/app-frame.tsx
index 6b4eb98b0e..6affb5ebb3 100644
--- a/web-next/components/shell/app-frame.tsx
+++ b/web-next/components/shell/app-frame.tsx
@@ -13,16 +13,32 @@ import {
   Github,
   Lock,
   LogOut,
+  MapPin,
   Megaphone,
   Menu,
   Sparkles,
   MessageSquare,
   Maximize2,
-  Info,
+  Minimize2,
+  Send,
   Settings,
-  Settings2,
-  Tags
+  Tags,
+  Wrench
 } from 'lucide-react';
+import {
+  HzAiChatModalSurface,
+  HzAboutModalSurface,
+  HzHeaderIconButton,
+  HzHeaderMenuAction,
+  HzHeaderRealtimeNotice,
+  HzUserMenuAction,
+  type HzAiChatConfigStatus,
+  type HzAiChatProviderConfigValue,
+  type HzAiChatScheduleDraft,
+  type HzAiChatScheduleDeleteStatus,
+  type HzAiChatScheduleRow,
+  type HzAiChatScheduleStatus
+} from '@hertzbeat/ui';
 import { useI18n } from '@/components/providers/i18n-provider';
 import { LocaleOptionList } from '@/components/shell/locale-option-list';
 import { AuthGate } from '@/components/shell/auth-gate';
@@ -32,7 +48,57 @@ import { Input } from '../ui/input';
 import { apiGet } from '@/lib/api-client';
 import { isStandaloneRoute, shouldLoadHeaderState } from '@/lib/app-frame-state';
 import { buildAlertListUrl } from '@/lib/alert-manage/query-state';
+import {
+  AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS,
+  AI_CHAT_MESSAGE_STATUS_LABEL_KEYS,
+  AI_CHAT_MESSAGE_ROLE_LABEL_KEYS,
+  createAiChatConversation,
+  deleteAiChatConversation,
+  loadAiChatConversationHistory,
+  loadAiChatConversations,
+  streamAiChatResponse,
+  type AiChatConversationMessage,
+  type AiChatConversationHistoryState,
+  type AiChatConversationListState
+} from '@/lib/ai-chat/conversations';
+import {
+  AI_CHAT_CONFIG_STATUS_LABEL_KEYS,
+  AI_CHAT_PROVIDER_OPTIONS,
+  applyAiChatProviderDefaults,
+  buildDefaultAiChatProviderConfig,
+  loadAiChatProviderConfig,
+  saveAiChatProviderConfig
+} from '@/lib/ai-chat/provider-config';
+import {
+  AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS,
+  createAiChatSchedule,
+  deleteAiChatSchedule,
+  loadAiChatScheduleSkills,
+  loadAiChatSchedules,
+  toggleAiChatSchedule,
+  updateAiChatSchedule
+} from '@/lib/ai-chat/schedules';
 import { buildLoginRedirectHref } from '@/lib/passport-login/controller';
+import { clearClientSession, readClientSessionUserSnapshot } from '@/lib/session-client';
+import { consumeAboutAutoShowAfterLogin, readAboutNotShowNextLogin, writeAboutNotShowNextLogin } from '@/lib/shell/about';
+import { loadHeaderMuteConfig, saveHeaderMuteConfig } from '@/lib/shell/header-mute';
+import { showHeaderLocaleReloadSpinner } from '@/lib/shell/locale-reload';
+import {
+  HEADER_ALERT_EVENT_TYPE,
+  HEADER_ALERT_SSE_URL,
+  HEADER_DUAL_SSE_CONTRACT,
+  HEADER_IMPORT_TASK_EVENT_TYPE,
+  HEADER_MANAGER_SSE_URL,
+  buildHeaderNoticeFromAlert,
+  buildManagerImportMessage,
+  mergeHeaderNoticeEvent,
+  parseHeaderSseJson,
+  resolveHeaderAlertSoundSrc,
+  type HeaderManagerImportEvent,
+  type HeaderNoticeEvent,
+  type HeaderRealtimeStatus
+} from '@/lib/shell/header-realtime';
+import { consumeWorkbenchLoad } from '@/lib/workbench-load-cache';
 import { bootstrapWorkbenchTheme } from '@/lib/workbench-theme';
 import type {
   AlertSummary,
@@ -45,11 +111,13 @@ import type {
 } from '@/lib/types';
 import { cn } from '@/lib/utils';
 
-const HEADER_MUTE_STORAGE_KEY = 'hb.header-mute';
 const PLATFORM_FOOTER_VERSION = 'v1.8.0';
 const ANGULAR_SETUP_BASELINE_COMPLETED = 5;
 const ANGULAR_SETUP_BASELINE_TOTAL = 6;
 const ANGULAR_SETUP_BASELINE_PERCENT = 83;
+const APP_FRAME_HEADER_STATE_CACHE_TTL_MS = 60_000;
+const AI_CHAT_SKILL_REPORT_MARKER = '[[SKILL_REPORT]]';
+const ABOUT_AUTO_CLOSE_AFTER_LOGIN_MS = 20_000;
 
 export type ContentFrameShellKind = 'standard' | 'flat-workbench';
 
@@ -104,23 +172,15 @@ type SetupSummaryData = {
   governanceCount: number;
 };
 
-type HeaderNotice = {
-  id: number;
+type HeaderNotice = HeaderNoticeEvent;
+
+type HeaderRealtimeNotice = {
+  status: Exclude<HeaderRealtimeStatus, 'connecting' | 'idle' | 'unsupported'> | 'live';
   title: string;
-  status: string;
-  activeAt?: number | null;
+  description?: string;
+  meta?: string;
 };
 
-function readHeaderMute() {
-  if (typeof window === 'undefined') return true;
-  return window.localStorage.getItem(HEADER_MUTE_STORAGE_KEY) !== 'false';
-}
-
-function writeHeaderMute(muted: boolean) {
-  if (typeof window === 'undefined') return;
-  window.localStorage.setItem(HEADER_MUTE_STORAGE_KEY, String(muted));
-}
-
 function buildEmptySetupSummary(t: (key: string, params?: Record<string, string | number | null | undefined>) => string): SetupSummary {
   return {
     loading: true,
@@ -136,11 +196,37 @@ function buildEmptySetupSummary(t: (key: string, params?: Record<string, string
   };
 }
 
-function formatHeaderTime(value?: number | null) {
-  if (!value) return '-';
+function formatHeaderTime(value: number | null | undefined, fallback: string) {
+  if (!value) return fallback;
   return new Date(value).toLocaleString();
 }
 
+function playHeaderAlertSound(locale: string, audioRef: React.MutableRefObject<HTMLAudioElement | null>) {
+  if (typeof window === 'undefined' || !window.Audio) return;
+  const audio = audioRef.current ?? new window.Audio();
+  audioRef.current = audio;
+  audio.src = resolveHeaderAlertSoundSrc(locale);
+  audio.load();
+  void audio.play().catch(() => undefined);
+}
+
+function showHeaderAlertNotification(
+  t: (key: string, params?: Record<string, string | number | null | undefined>) => string,
+  navigateToAlertCenter: () => void
+) {
+  if (typeof window === 'undefined' || !('Notification' in window)) return;
+  if (window.Notification.permission !== 'granted') return;
+  const notification = new window.Notification(t('alert.notify.title'), {
+    body: t('alert.notify.body'),
+    icon: '/assets/logo.svg'
+  });
+  notification.onclick = () => {
+    window.focus();
+    navigateToAlertCenter();
+    notification.close();
+  };
+}
+
 function resolveSetupActionHref(action: SetupAction) {
   switch (action) {
     case 'entities':
@@ -293,6 +379,55 @@ function shouldKeepAngularSetupBaseline(pathname: string, data: SetupSummaryData
   );
 }
 
+function processAiChatStreamContent(content: string) {
+  const markerIndex = content.indexOf(AI_CHAT_SKILL_REPORT_MARKER);
+  if (markerIndex === -1) return content;
+  return content.substring(markerIndex + AI_CHAT_SKILL_REPORT_MARKER.length).replace(/^\n+/, '');
+}
+
+function appendToLastStreamingAiAssistantMessage(messages: AiChatConversationMessage[], content: string): AiChatConversationMessage[] {
+  const nextMessages = [...messages];
+  const lastMessage = nextMessages[nextMessages.length - 1];
+  if (lastMessage?.role === 'assistant') {
+    const nextContent = processAiChatStreamContent(`${lastMessage.content}${content}`);
+    nextMessages[nextMessages.length - 1] = {
+      ...lastMessage,
+      content: nextContent
+    };
+    return nextMessages;
+  }
+
+  return [
+    ...nextMessages,
+    {
+      role: 'assistant',
+      labelKey: AI_CHAT_MESSAGE_ROLE_LABEL_KEYS.assistant,
+      content
+    }
+  ];
+}
+
+function replaceLastStreamingAiAssistantMessage(messages: AiChatConversationMessage[], content: string): AiChatConversationMessage[] {
+  const nextMessages = [...messages];
+  const lastMessage = nextMessages[nextMessages.length - 1];
+  if (lastMessage?.role === 'assistant') {
+    nextMessages[nextMessages.length - 1] = {
+      ...lastMessage,
+      content
+    };
+    return nextMessages;
+  }
+
+  return [
+    ...nextMessages,
+    {
+      role: 'assistant',
+      labelKey: AI_CHAT_MESSAGE_ROLE_LABEL_KEYS.assistant,
+      content
+    }
+  ];
+}
+
 export function buildRouteSetupSummary(
   pathname: string,
   data: SetupSummaryData,
@@ -307,26 +442,70 @@ export function buildRouteSetupSummary(
 export function AppFrame({ children }: { children: React.ReactNode }) {
   const router = useRouter();
   const { t, locale, locales, setLocale } = useI18n();
+  const [sessionUser] = useState(() => readClientSessionUserSnapshot());
+  const currentUserName = sessionUser?.name || 'admin';
+  const emptyValue = t('common.none');
   const pathname = usePathname();
   const [setupOpen, setSetupOpen] = useState(false);
   const [notifyOpen, setNotifyOpen] = useState(false);
   const [settingsOpen, setSettingsOpen] = useState(false);
   const [userOpen, setUserOpen] = useState(false);
+  const [aboutOpen, setAboutOpen] = useState(false);
+  const [aboutNotShowNextLogin, setAboutNotShowNextLogin] = useState(false);
   const [aiPrompt, setAiPrompt] = useState('');
+  const [aiChatOpen, setAiChatOpen] = useState(false);
+  const [aiInitialPrompt, setAiInitialPrompt] = useState('');
+  const [aiConversations, setAiConversations] = useState<AiChatConversationListState>({
+    status: 'loading',
+    conversations: [],
+    statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.loading
+  });
+  const [selectedAiConversationId, setSelectedAiConversationId] = useState<string | number | null>(null);
+  const [aiConversationHistory, setAiConversationHistory] = useState<AiChatConversationHistoryState>({
+    status: 'idle',
+    messages: [],
+    statusLabelKey: AI_CHAT_MESSAGE_STATUS_LABEL_KEYS.idle
+  });
+  const [aiDraftMessage, setAiDraftMessage] = useState('');
+  const [aiLocalMessages, setAiLocalMessages] = useState<AiChatConversationMessage[]>([]);
+  const [aiSendStatus, setAiSendStatus] = useState<'idle' | 'sending' | 'error'>('idle');
+  const [aiConfigOpen, setAiConfigOpen] = useState(false);
+  const [aiProviderConfig, setAiProviderConfig] = useState<HzAiChatProviderConfigValue>(() => buildDefaultAiChatProviderConfig());
+  const [aiConfigStatus, setAiConfigStatus] = useState<HzAiChatConfigStatus>('idle');
+  const [aiConfigStatusLabelKey, setAiConfigStatusLabelKey] = useState(AI_CHAT_CONFIG_STATUS_LABEL_KEYS.idle);
+  const [aiScheduleOpen, setAiScheduleOpen] = useState(false);
+  const [aiScheduleStatus, setAiScheduleStatus] = useState<HzAiChatScheduleStatus>('idle');
+  const [aiScheduleStatusLabelKey, setAiScheduleStatusLabelKey] = useState(AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.idle);
+  const [aiSchedules, setAiSchedules] = useState<HzAiChatScheduleRow[]>([]);
+  const [aiScheduleSkills, setAiScheduleSkills] = useState<Array<{ value: string; label: string }>>([]);
+  const [aiScheduleDraft, setAiScheduleDraft] = useState<HzAiChatScheduleDraft>({ sopName: '', cronExpression: '', enabled: true });
+  const [aiScheduleEditDraft, setAiScheduleEditDraft] = useState<(HzAiChatScheduleDraft & { id: string | number }) | null>(null);
+  const [aiDeleteScheduleId, setAiDeleteScheduleId] = useState<string | number | null>(null);
+  const [aiDeleteScheduleStatus, setAiDeleteScheduleStatus] = useState<HzAiChatScheduleDeleteStatus>('idle');
+  const [aiNewConversationStatus, setAiNewConversationStatus] = useState<'idle' | 'creating' | 'error'>('idle');
+  const [aiDeleteConversationId, setAiDeleteConversationId] = useState<string | number | null>(null);
+  const [aiDeleteConversationStatus, setAiDeleteConversationStatus] = useState<'idle' | 'confirming' | 'deleting' | 'error'>('idle');
   const [notificationsMuted, setNotificationsMuted] = useState(true);
+  const notificationMuteLabel = notificationsMuted ? t('common.unmute') : t('common.mute');
+  const [fullscreenActive, setFullscreenActive] = useState(false);
+  const fullscreenLabel = fullscreenActive ? t('menu.fullscreen.exit') : t('menu.fullscreen');
   const [alertSummary, setAlertSummary] = useState<AlertSummary | null>(null);
   const [alertNotices, setAlertNotices] = useState<HeaderNotice[]>([]);
+  const [headerRealtimeStatus, setHeaderRealtimeStatus] = useState<HeaderRealtimeStatus>('idle');
+  const [headerRealtimeNotice, setHeaderRealtimeNotice] = useState<HeaderRealtimeNotice | null>(null);
   const [setupSummary, setSetupSummary] = useState<SetupSummary>(() => buildEmptySetupSummary(t));
   const setupRef = useRef<HTMLDivElement | null>(null);
   const notifyRef = useRef<HTMLDivElement | null>(null);
   const settingsRef = useRef<HTMLDivElement | null>(null);
   const userRef = useRef<HTMLDivElement | null>(null);
+  const notifiedAlertIdsRef = useRef<Set<number>>(new Set());
+  const alertAudioRef = useRef<HTMLAudioElement | null>(null);
   const standaloneRoute = isStandaloneRoute(pathname);
 
   function logout() {
-    window.localStorage.removeItem('Authorization');
-    window.localStorage.removeItem('refresh-token');
-    window.location.href = buildLoginRedirectHref(undefined, process.env.NEXT_PUBLIC_LOGIN_PATH);
+    void clearClientSession().finally(() => {
+      window.location.href = buildLoginRedirectHref(undefined, process.env.NEXT_PUBLIC_LOGIN_PATH);
+    });
   }
 
   function toggleFullscreen() {
@@ -338,32 +517,514 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
     void document.documentElement.requestFullscreen();
   }
 
+  useEffect(() => {
+    if (typeof document === 'undefined') return undefined;
+
+    const syncFullscreenStatus = () => {
+      setFullscreenActive(Boolean(document.fullscreenElement));
+    };
+
+    syncFullscreenStatus();
+    document.addEventListener('fullscreenchange', syncFullscreenStatus);
+
+    return () => {
+      document.removeEventListener('fullscreenchange', syncFullscreenStatus);
+    };
+  }, []);
+
   function toggleMute() {
-    setNotificationsMuted(current => {
-      const next = !current;
-      writeHeaderMute(next);
-      return next;
+    const nextMuted = !notificationsMuted;
+
+    if (!nextMuted && typeof window !== 'undefined' && 'Notification' in window) {
+      void window.Notification.requestPermission();
+    }
+
+    void saveHeaderMuteConfig(nextMuted).then(result => {
+      if (result.status === 'saved') {
+        setNotificationsMuted(result.muted);
+      }
     });
   }
 
+  function openAboutModal() {
+    setUserOpen(false);
+    setAboutOpen(true);
+  }
+
+  function changeAboutNotShowNextLogin(checked: boolean) {
+    writeAboutNotShowNextLogin(checked);
+    setAboutNotShowNextLogin(checked);
+  }
+
   function openSetupAction(action: SetupAction) {
     setSetupOpen(false);
     router.push(resolveSetupActionHref(action));
   }
 
+  function openAiChatModal(initialPrompt = '') {
+    const nextPrompt = initialPrompt.trim();
+    setAiInitialPrompt(nextPrompt);
+    setAiDraftMessage(nextPrompt);
+    setAiChatOpen(true);
+  }
+
   function submitAiPrompt() {
     const query = aiPrompt.trim();
-    router.push(query ? `/overview?ai=${encodeURIComponent(query)}` : '/overview');
+    if (!query) return;
+    openAiChatModal(query);
+    setAiPrompt('');
+  }
+
+  async function loadAiProviderConfig(openOnError = false) {
+    setAiConfigStatus('loading');
+    setAiConfigStatusLabelKey(AI_CHAT_CONFIG_STATUS_LABEL_KEYS.loading);
+    const next = await loadAiChatProviderConfig();
+    setAiProviderConfig(next.config);
+    setAiConfigStatus(next.status);
+    setAiConfigStatusLabelKey(next.statusLabelKey);
+    if (openOnError && next.status === 'error') {
+      setAiConfigOpen(true);
+    }
+  }
+
+  function openAiProviderConfig() {
+    setAiConfigOpen(true);
+    void loadAiProviderConfig(false);
+  }
+
+  function changeAiProviderConfig(next: HzAiChatProviderConfigValue) {
+    setAiProviderConfig(current => {
+      const codeChanged = current.code !== next.code;
+      if (!codeChanged) {
+        return next;
+      }
+      return applyAiChatProviderDefaults(next);
+    });
+    setAiConfigStatus(current => current === 'error' ? 'ready' : current);
+    setAiConfigStatusLabelKey(AI_CHAT_CONFIG_STATUS_LABEL_KEYS.ready);
+  }
+
+  function resetAiProviderDefaults() {
+    setAiProviderConfig(current => ({
+      ...buildDefaultAiChatProviderConfig(current.code),
+      apiKey: current.apiKey
+    }));
+    setAiConfigStatus(current => current === 'error' ? 'ready' : current);
+    setAiConfigStatusLabelKey(AI_CHAT_CONFIG_STATUS_LABEL_KEYS.ready);
+  }
+
+  async function saveAiProviderConfig() {
+    const nextConfig = applyAiChatProviderDefaults(aiProviderConfig);
+    if (!nextConfig.code) {
+      setAiConfigStatus('error');
+      setAiConfigStatusLabelKey('ai.chat.config.provider.required');
+      return;
+    }
+    if (!nextConfig.apiKey.trim()) {
+      setAiConfigStatus('error');
+      setAiConfigStatusLabelKey('ai.chat.config.api-key.required');
+      return;
+    }
+    if (!nextConfig.baseUrl.trim()) {
+      setAiConfigStatus('error');
+      setAiConfigStatusLabelKey('ai.chat.config.base-url.required');
+      return;
+    }
+    if (!nextConfig.model.trim()) {
+      setAiConfigStatus('error');
+      setAiConfigStatusLabelKey('ai.chat.config.model.required');
+      return;
+    }
+
+    setAiConfigStatus('saving');
+    setAiConfigStatusLabelKey(AI_CHAT_CONFIG_STATUS_LABEL_KEYS.saving);
+    const result = await saveAiChatProviderConfig(nextConfig);
+    setAiConfigStatus(result.status);
+    setAiConfigStatusLabelKey(result.statusLabelKey);
+    if (result.status === 'saved') {
+      setAiProviderConfig(nextConfig);
+      setAiConfigOpen(false);
+      void loadAiChatConversations().then(next => {
+        setAiConversations(next);
+        setSelectedAiConversationId(next.conversations[0]?.id ?? null);
+      });
+    }
+  }
+
+  function openAiSchedulePanel() {
+    setAiScheduleOpen(true);
+    setAiScheduleEditDraft(null);
+    setAiDeleteScheduleId(null);
+    setAiDeleteScheduleStatus('idle');
+  }
+
+  async function createAiScheduleFromDraft() {
+    if (selectedAiConversationId === null || selectedAiConversationId === 0 || !aiScheduleDraft.sopName || !aiScheduleDraft.cronExpression) return;
+    setAiScheduleStatus('saving');
+    setAiScheduleStatusLabelKey(AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.saving);
+    const result = await createAiChatSchedule(selectedAiConversationId, aiScheduleDraft);
+    if (result.status === 'ready' && result.schedule) {
+      setAiSchedules(current => [...current, result.schedule!]);
+      setAiScheduleDraft({ sopName: '', cronExpression: '', enabled: true });
+      setAiScheduleStatus('ready');
+    } else {
+      setAiScheduleStatus('error');
+    }
+    setAiScheduleStatusLabelKey(result.statusLabelKey);
+  }
+
+  async function toggleAiSchedule(scheduleId: string | number, enabled: boolean) {
+    setAiScheduleStatus('saving');
+    setAiScheduleStatusLabelKey(AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.saving);
+    setAiSchedules(current => current.map(row => String(row.id) === String(scheduleId) ? { ...row, enabled } : row));
+    const result = await toggleAiChatSchedule(scheduleId, enabled);
+    if (result.status === 'ready' && result.schedule) {
+      setAiSchedules(current => current.map(row => String(row.id) === String(scheduleId) ? result.schedule! : row));
+      setAiScheduleStatus('ready');
+    } else {
+      setAiSchedules(current => current.map(row => String(row.id) === String(scheduleId) ? { ...row, enabled: !enabled } : row));
+      setAiScheduleStatus('error');
+    }
+    setAiScheduleStatusLabelKey(result.statusLabelKey);
+  }
+
+  function startAiScheduleEdit(row: HzAiChatScheduleRow) {
+    setAiScheduleEditDraft({
+      id: row.id,
+      sopName: String(row.sopName),
+      cronExpression: row.cronExpression,
+      enabled: row.enabled
+    });
+  }
+
+  async function updateAiScheduleFromDraft() {
+    if (!aiScheduleEditDraft || selectedAiConversationId === null || selectedAiConversationId === 0) return;
+    setAiScheduleStatus('saving');
+    setAiScheduleStatusLabelKey(AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.saving);
+    const result = await updateAiChatSchedule(aiScheduleEditDraft.id, selectedAiConversationId, aiScheduleEditDraft);
+    if (result.status === 'ready' && result.schedule) {
+      setAiSchedules(current => current.map(row => String(row.id) === String(aiScheduleEditDraft.id) ? result.schedule! : row));
+      setAiScheduleEditDraft(null);
+      setAiScheduleStatus('ready');
+    } else {
+      setAiScheduleStatus('error');
+    }
+    setAiScheduleStatusLabelKey(result.statusLabelKey);
+  }
+
+  function requestDeleteAiSchedule(scheduleId: string | number) {
+    setAiDeleteScheduleId(scheduleId);
+    setAiDeleteScheduleStatus('confirming');
+  }
+
+  function cancelDeleteAiSchedule() {
+    setAiDeleteScheduleId(null);
+    setAiDeleteScheduleStatus('idle');
+  }
+
+  async function confirmDeleteAiSchedule() {
+    if (aiDeleteScheduleId === null) return;
+    const scheduleId = aiDeleteScheduleId;
+    setAiScheduleStatus('saving');
+    setAiScheduleStatusLabelKey(AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.saving);
+    setAiDeleteScheduleStatus('deleting');
+    const result = await deleteAiChatSchedule(scheduleId);
+    if (result.status === 'ready') {
+      setAiSchedules(current => {
+        const next = current.filter(row => String(row.id) !== String(scheduleId));
+        setAiScheduleStatus(next.length > 0 ? 'ready' : 'empty');
+        return next;
+      });
+      setAiDeleteScheduleId(null);
+      setAiDeleteScheduleStatus('idle');
+    } else {
+      setAiScheduleStatus('error');
+      setAiDeleteScheduleStatus('error');
+    }
+    setAiScheduleStatusLabelKey(result.statusLabelKey);
+  }
+
+  async function createNewAiConversation() {
+    setAiNewConversationStatus('creating');
+    const next = await createAiChatConversation();
+    setAiNewConversationStatus(next.status === 'ready' ? 'idle' : 'error');
+    setAiConversations(current => ({
+      status: next.status === 'ready' ? 'ready' : 'error',
+      statusLabelKey: next.statusLabelKey,
+      conversations: [
+        next.conversation,
+        ...current.conversations.filter(conversation => String(conversation.id) !== String(next.conversation.id))
+      ]
+    }));
+    setSelectedAiConversationId(next.conversation.id);
+    setAiLocalMessages([]);
+    setAiSendStatus('idle');
+  }
+
+  function requestDeleteAiConversation(conversationId: string | number) {
+    setAiDeleteConversationId(conversationId);
+    setAiDeleteConversationStatus('confirming');
+  }
+
+  function cancelDeleteAiConversation() {
+    setAiDeleteConversationId(null);
+    setAiDeleteConversationStatus('idle');
+  }
+
+  async function confirmDeleteAiConversation(conversationId: string | number) {
+    setAiDeleteConversationId(conversationId);
+    setAiDeleteConversationStatus('deleting');
+    const next = await deleteAiChatConversation(conversationId);
+    if (next.status === 'error') {
+      setAiDeleteConversationStatus('error');
+      setAiConversations(current => ({
+        ...current,
+        status: 'error',
+        statusLabelKey: next.statusLabelKey
+      }));
+      return;
+    }
+
+    setAiDeleteConversationId(null);
+    setAiDeleteConversationStatus('idle');
+    const conversations = aiConversations.conversations.filter(conversation => String(conversation.id) !== String(conversationId));
+    if (conversations.length === 0) {
+      setAiConversations({
+        status: 'empty',
+        statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.empty,
+        conversations: []
+      });
+      setSelectedAiConversationId(null);
+      await createNewAiConversation();
+      return;
+    }
+
+    const nextSelected = String(selectedAiConversationId) === String(conversationId)
+      ? conversations[0].id
+      : selectedAiConversationId;
+    setAiConversations({
+      status: 'ready',
+      statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.ready,
+      conversations
+    });
+    setSelectedAiConversationId(nextSelected);
+    setAiLocalMessages([]);
+    setAiSendStatus('idle');
+  }
+
+  function appendOfflineAiAssistantResponse() {
+    globalThis.setTimeout(() => {
+      setAiLocalMessages(current => replaceLastStreamingAiAssistantMessage(current, t('ai.chat.offline.response')));
+      setAiSendStatus('idle');
+    }, 1000);
+  }
+
+  function appendAiStreamChunk(content: string) {
+    setAiLocalMessages(current => appendToLastStreamingAiAssistantMessage(current, content));
+  }
+
+  function failAiStreamResponse() {
+    setAiLocalMessages(current => replaceLastStreamingAiAssistantMessage(current, t('ai.chat.error.processing')));
+    setAiSendStatus('error');
+  }
+
+  async function refreshAiConversationAfterStream(conversationId: string | number | null) {
+    if (conversationId === null || conversationId === 0) return;
+    const next = await loadAiChatConversationHistory(conversationId);
+    if (next.status === 'ready' && next.messages.length > 0) {
+      setAiConversationHistory(next);
+      setAiLocalMessages([]);
+    }
+  }
+
+  function sendAiMessage() {
+    const messageContent = aiDraftMessage.trim();
+    if (!messageContent || aiSendStatus === 'sending') return;
+
+    setAiDraftMessage('');
+    setAiLocalMessages(current => [
+      ...current,
+      {
+        role: 'user',
+        labelKey: AI_CHAT_MESSAGE_ROLE_LABEL_KEYS.user,
+        content: messageContent
+      },
+      {
+        role: 'assistant',
+        labelKey: AI_CHAT_MESSAGE_ROLE_LABEL_KEYS.assistant,
+        content: ''
+      }
+    ]);
+    setAiSendStatus('sending');
+
+    if (
+      selectedAiConversationId === 0 ||
+      selectedAiConversationId === null ||
+      aiConversations.status === 'error' ||
+      aiConversationHistory.status === 'error'
+    ) {
+      appendOfflineAiAssistantResponse();
+      return;
+    }
+
+    const streamConversationId = selectedAiConversationId;
+    void streamAiChatResponse(messageContent, {
+      conversationId: streamConversationId,
+      onChunk: chunk => appendAiStreamChunk(chunk.content)
+    }).then(() => {
+      setAiSendStatus('idle');
+      void refreshAiConversationAfterStream(streamConversationId);
+    }).catch(() => {
+      failAiStreamResponse();
+    });
   }
 
   useEffect(() => {
-    setNotificationsMuted(readHeaderMute());
+    let mounted = true;
+
+    void loadHeaderMuteConfig().then(result => {
+      if (mounted) {
+        setNotificationsMuted(result.muted);
+      }
+    });
+
+    return () => {
+      mounted = false;
+    };
+  }, []);
+
+  useEffect(() => {
+    setAboutNotShowNextLogin(readAboutNotShowNextLogin());
+  }, []);
+
+  useEffect(() => {
+    if (!consumeAboutAutoShowAfterLogin()) return undefined;
+    if (readAboutNotShowNextLogin()) return undefined;
+
+    setAboutOpen(true);
+    const timeout = window.setTimeout(() => {
+      setAboutOpen(false);
+    }, ABOUT_AUTO_CLOSE_AFTER_LOGIN_MS);
+
+    return () => {
+      window.clearTimeout(timeout);
+    };
   }, []);
 
   useEffect(() => {
     bootstrapWorkbenchTheme();
   }, []);
 
+  useEffect(() => {
+    let mounted = true;
+
+    if (!aiChatOpen) return () => {
+      mounted = false;
+    };
+
+    setAiConversations({
+      status: 'loading',
+      conversations: [],
+      statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.loading
+    });
+    setSelectedAiConversationId(null);
+    setAiConversationHistory({
+      status: 'idle',
+      messages: [],
+      statusLabelKey: AI_CHAT_MESSAGE_STATUS_LABEL_KEYS.idle
+    });
+    setAiLocalMessages([]);
+    setAiSendStatus('idle');
+    setAiNewConversationStatus('idle');
+    setAiDeleteConversationId(null);
+    setAiDeleteConversationStatus('idle');
+    setAiConfigOpen(false);
+    setAiConfigStatus('idle');
+    setAiConfigStatusLabelKey(AI_CHAT_CONFIG_STATUS_LABEL_KEYS.idle);
+    setAiScheduleOpen(false);
+    setAiScheduleStatus('idle');
+    setAiScheduleStatusLabelKey(AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.idle);
+    setAiSchedules([]);
+    setAiScheduleEditDraft(null);
+    setAiDeleteScheduleId(null);
+    setAiDeleteScheduleStatus('idle');
+
+    void loadAiProviderConfig(true);
+
+    void loadAiChatConversations().then(next => {
+      if (mounted) {
+        setAiConversations(next);
+        setSelectedAiConversationId(next.conversations[0]?.id ?? null);
+      }
+    });
+
+    return () => {
+      mounted = false;
+    };
+  }, [aiChatOpen]);
+
+  useEffect(() => {
+    let mounted = true;
+
+    if (!aiChatOpen || selectedAiConversationId === null) return () => {
+      mounted = false;
+    };
+
+    setAiConversationHistory({
+      status: 'loading',
+      messages: [],
+      statusLabelKey: AI_CHAT_MESSAGE_STATUS_LABEL_KEYS.loading,
+      conversationId: selectedAiConversationId
+    });
+    setAiLocalMessages([]);
+    setAiSendStatus('idle');
+
+    void loadAiChatConversationHistory(selectedAiConversationId).then(next => {
+      if (mounted) {
+        setAiConversationHistory(next);
+      }
+    });
+
+    return () => {
+      mounted = false;
+    };
+  }, [aiChatOpen, selectedAiConversationId]);
+
+  useEffect(() => {
+    let mounted = true;
+
+    if (!aiChatOpen || !aiScheduleOpen) return () => {
+      mounted = false;
+    };
+
+    if (selectedAiConversationId === null || selectedAiConversationId === 0) {
+      setAiScheduleStatus('error');
+      setAiScheduleStatusLabelKey(AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.error);
+      setAiSchedules([]);
+      setAiScheduleSkills([]);
+      return () => {
+        mounted = false;
+      };
+    }
+
+    setAiScheduleStatus('loading');
+    setAiScheduleStatusLabelKey(AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.loading);
+    void Promise.all([
+      loadAiChatSchedules(selectedAiConversationId),
+      loadAiChatScheduleSkills()
+    ]).then(([schedules, skills]) => {
+      if (!mounted) return;
+      setAiSchedules(schedules.schedules);
+      setAiScheduleSkills(skills.skills.map(skill => ({ value: skill.value, label: String(skill.label) })));
+      setAiScheduleStatus(schedules.status);
+      setAiScheduleStatusLabelKey(schedules.statusLabelKey);
+    });
+
+    return () => {
+      mounted = false;
+    };
+  }, [aiChatOpen, aiScheduleOpen, selectedAiConversationId]);
+
   useEffect(() => {
     function handlePointerDown(event: MouseEvent) {
       const target = event.target as Node;
@@ -404,17 +1065,21 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
           governancePresets,
           summary,
           alerts,
-        ] = await Promise.all([
-          apiGet<PageResult<unknown>>('/monitors?pageIndex=0&pageSize=1').catch(() => ({ content: [], totalElements: 0, pageIndex: 0, pageSize: 1 })),
-          apiGet<PageResult<CollectorSummary>>('/collector?pageIndex=0&pageSize=1').catch(() => ({ content: [], totalElements: 0, pageIndex: 0, pageSize: 1 })),
-          apiGet<PageResult<EntitySummaryInfo>>('/entities?pageIndex=0&pageSize=40&sort=gmtUpdate&order=desc').catch(() => ({ content: [], totalElements: 0, pageIndex: 0, pageSize: 40 })),
-          apiGet<EntityDefinitionActivity[]>('/entities/definition-activities?limit=8').catch(() => []),
-          apiGet<EntityDiscoveryGovernancePreset[]>('/entities/discovery/governance-presets?limit=8').catch(() => []),
-          apiGet<AlertSummary>('/alerts/summary').catch(() => null),
-          apiGet<PageResult<SingleAlert>>(
-            buildAlertListUrl({ search: '', status: '', severity: '', entityId: '', entityName: '', returnTo: '' })
-          ).catch(() => ({ content: [], totalElements: 0, pageIndex: 0, pageSize: 8 })),
-        ]);
+        ] = await consumeWorkbenchLoad(
+          `app-frame:header-state:${locale}`,
+          () => Promise.all([
+            apiGet<PageResult<unknown>>('/monitors?pageIndex=0&pageSize=1').catch(() => ({ content: [], totalElements: 0, pageIndex: 0, pageSize: 1 })),
+            apiGet<PageResult<CollectorSummary>>('/collector?pageIndex=0&pageSize=1').catch(() => ({ content: [], totalElements: 0, pageIndex: 0, pageSize: 1 })),
+            apiGet<PageResult<EntitySummaryInfo>>('/entities?pageIndex=0&pageSize=40&sort=gmtUpdate&order=desc').catch(() => ({ content: [], totalElements: 0, pageIndex: 0, pageSize: 40 })),
+            apiGet<EntityDefinitionActivity[]>('/entities/definition-activities?limit=8').catch(() => []),
+            apiGet<EntityDiscoveryGovernancePreset[]>('/entities/discovery/governance-presets?limit=8').catch(() => []),
+            apiGet<AlertSummary>('/alerts/summary').catch(() => null),
+            apiGet<PageResult<SingleAlert>>(
+              buildAlertListUrl({ search: '', status: '', severity: '', entityId: '', entityName: '', returnTo: '' })
+            ).catch(() => ({ content: [], totalElements: 0, pageIndex: 0, pageSize: 8 })),
+          ]),
+          { settledTtlMs: APP_FRAME_HEADER_STATE_CACHE_TTL_MS }
+        );
 
         if (cancelled) return;
 
@@ -464,15 +1129,127 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
     return () => {
       cancelled = true;
     };
-  }, [pathname, standaloneRoute, t]);
+  }, [locale, pathname, standaloneRoute, t]);
+
+  useEffect(() => {
+    if (!shouldLoadHeaderState(pathname)) {
+      setHeaderRealtimeStatus('idle');
+      setHeaderRealtimeNotice(null);
+      return undefined;
+    }
+    if (typeof window === 'undefined' || !window.EventSource) {
+      setHeaderRealtimeStatus('unsupported');
+      return undefined;
+    }
+
+    let cancelled = false;
+    setHeaderRealtimeStatus('connecting');
+
+    const alertSource = new window.EventSource(HEADER_ALERT_SSE_URL);
+    const managerSource = new window.EventSource(HEADER_MANAGER_SSE_URL);
+
+    const markLive = () => {
+      if (!cancelled) setHeaderRealtimeStatus('live');
+    };
+    const markError = () => {
+      if (!cancelled) setHeaderRealtimeStatus('error');
+    };
+
+    alertSource.onopen = markLive;
+    managerSource.onopen = markLive;
+
+    alertSource.addEventListener(HEADER_ALERT_EVENT_TYPE, (event: MessageEvent) => {
+      if (cancelled) return;
+      const alert = parseHeaderSseJson<SingleAlert>(event.data);
+      if (!alert) return;
+
+      const notice = buildHeaderNoticeFromAlert(alert, t('dashboard.alerts.center-title'));
+      setHeaderRealtimeStatus('live');
+      setHeaderRealtimeNotice({
+        status: 'live',
+        title: notice.title,
+        description: `${notice.status} · ${formatHeaderTime(notice.activeAt, emptyValue)}`,
+        meta: HEADER_ALERT_EVENT_TYPE
+      });
+      setAlertNotices(current => mergeHeaderNoticeEvent(current, notice));
+      setAlertSummary(current => current
+        ? { ...current, total: Math.max(current.total, 1) }
+        : { total: 1, dealNum: 0, rate: 0, priorityWarningNum: 0, priorityCriticalNum: 0, priorityEmergencyNum: 0 });
+
+      if (!notificationsMuted && !notifiedAlertIdsRef.current.has(alert.id)) {
+        notifiedAlertIdsRef.current.add(alert.id);
+        playHeaderAlertSound(locale, alertAudioRef);
+        showHeaderAlertNotification(t, () => router.push('/alert'));
+      }
+    });
+
+    managerSource.addEventListener(HEADER_IMPORT_TASK_EVENT_TYPE, (event: MessageEvent) => {
+      if (cancelled) return;
+      const payload = parseHeaderSseJson<HeaderManagerImportEvent>(event.data);
+      if (!payload) return;
+      const message = buildManagerImportMessage(payload, t);
+      if (!message) return;
+      setHeaderRealtimeStatus(message.status === 'error' ? 'error' : 'live');
+      setHeaderRealtimeNotice(message);
+    });
+
+    alertSource.onerror = () => {
+      markError();
+      alertSource.close();
+    };
+    managerSource.onerror = () => {
+      markError();
+      managerSource.close();
+    };
+
+    return () => {
+      cancelled = true;
+      alertSource.close();
+      managerSource.close();
+    };
+  }, [emptyValue, locale, notificationsMuted, pathname, router, t]);
 
   if (standaloneRoute) {
     return <>{children}</>;
   }
 
+  const aiConversationRows = aiConversations.conversations.map(conversation => ({
+    ...conversation,
+    active: selectedAiConversationId === null
+      ? Boolean(conversation.active)
+      : String(conversation.id) === String(selectedAiConversationId)
+  }));
+  const aiConversationMessageRows = aiConversationHistory.messages.map(message => ({
+    role: message.role,
+    label: t(message.labelKey),
+    content: message.content
+  })).concat(aiLocalMessages.map(message => ({
+    role: message.role,
+    label: t(message.labelKey),
+    content: message.content
+  })));
+  const aiMessageStatus = aiLocalMessages.length > 0 ? 'ready' : aiConversationHistory.status;
+  const aiMessageStatusLabelKey = aiLocalMessages.length > 0
+    ? AI_CHAT_MESSAGE_STATUS_LABEL_KEYS.ready
+    : aiConversationHistory.statusLabelKey;
+
   return (
     <AuthGate>
       <div className="min-h-screen bg-[hsl(var(--background))] text-[hsl(var(--foreground))]">
+        {headerRealtimeNotice ? (
+          <div
+            className="fixed right-4 top-20 z-40 w-[320px]"
+            data-app-frame-header-realtime-toast="angular-sse"
+          >
+            <HzHeaderRealtimeNotice
+              status={headerRealtimeNotice.status}
+              title={headerRealtimeNotice.title}
+              description={headerRealtimeNotice.description}
+              meta={headerRealtimeNotice.meta}
+              data-app-frame-header-realtime-notice-owner="hertzbeat-ui-header-realtime-notice"
+            />
+          </div>
+        ) : null}
         <div className="grid min-h-screen lg:grid-cols-[164px_minmax(0,1fr)] lg:grid-rows-[64px_minmax(0,1fr)]">
           <header className="sticky top-0 z-20 col-span-full border-b border-[hsl(var(--border))] bg-[hsl(var(--background))]">
             <div className="grid h-16 lg:grid-cols-[164px_minmax(0,1fr)]">
@@ -494,22 +1271,22 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                 <button
                   type="button"
                   className="inline-flex h-8 w-8 items-center justify-center rounded-[2px] text-[hsl(var(--muted-foreground))] transition hover:bg-[hsl(var(--accent)/0.35)] hover:text-[hsl(var(--foreground))]"
-                  aria-label="Menu"
+                  aria-label={t('app.frame.utility.menu')}
                   data-app-frame-icon-trigger="menu"
                 >
                   <Menu size={17} />
-                  <span className="sr-only">Menu</span>
+                  <span className="sr-only">{t('app.frame.utility.menu')}</span>
                 </button>
                 <a
                   className="inline-flex h-8 w-8 items-center justify-center rounded-[2px] text-[hsl(var(--muted-foreground))] transition hover:bg-[hsl(var(--accent)/0.35)] hover:text-[hsl(var(--foreground))]"
                   href="https://github.com/apache/hertzbeat"
                   target="_blank"
                   rel="noreferrer"
-                  aria-label="GitHub"
+                  aria-label={t('app.frame.utility.github')}
                   data-app-frame-icon-trigger="github"
                 >
                   <Github size={17} />
-                  <span className="sr-only">GitHub</span>
+                  <span className="sr-only">{t('app.frame.utility.github')}</span>
                 </a>
 
                 <div className="relative hidden lg:block" ref={setupRef}>
@@ -622,7 +1399,12 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                 </div>
 
                 <div className="hidden min-w-[320px] max-w-[500px] flex-1 lg:block">
-                  <div className="flex h-8 items-stretch overflow-hidden rounded-[2px] border border-[hsl(var(--border))] bg-[hsl(var(--background))]">
+                  <div
+                    className="flex h-8 items-stretch overflow-hidden rounded-[2px] border border-[hsl(var(--border))] bg-[hsl(var(--background))]"
+                    data-app-frame-ai-chat-input="angular-header-ai-chat"
+                    data-app-frame-ai-chat-initial-message-contract="angular-open-modal-initial-message"
+                    data-app-frame-ai-chat-initial-message-owner="route-ai-chat-modal-contract"
+                  >
                     <Input
                       value={aiPrompt}
                       onChange={event => setAiPrompt(event.target.value)}
@@ -639,9 +1421,11 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                       type="button"
                       className="flex w-8 items-center justify-center border-l border-[hsl(var(--border))] text-[hsl(var(--muted-foreground))] transition hover:bg-[hsl(var(--accent)/0.2)] hover:text-[hsl(var(--foreground))]"
                       onClick={submitAiPrompt}
+                      disabled={!aiPrompt.trim()}
                       aria-label={t('ai.chat.submit')}
+                      data-app-frame-ai-chat-submit-state={aiPrompt.trim() ? 'ready' : 'empty'}
                     >
-                      <MessageSquare size={15} />
+                      {aiPrompt.trim() ? <Send size={15} /> : <MessageSquare size={15} />}
                     </button>
                   </div>
                 </div>
@@ -655,6 +1439,10 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                         aria-label={t('common.notify')}
                         onClick={() => setNotifyOpen(open => !open)}
                         data-app-frame-icon-trigger="notify"
+                        data-app-frame-header-realtime-sse-contract={HEADER_DUAL_SSE_CONTRACT}
+                        data-app-frame-header-realtime-sse-owner="route-realtime-sse-contract"
+                        data-app-frame-header-realtime-alert-source={HEADER_ALERT_SSE_URL}
+                        data-app-frame-header-realtime-manager-source={HEADER_MANAGER_SSE_URL}
                       >
                         <span className="relative inline-flex items-center">
                           <Bell size={16} data-app-frame-glyph="notify-bell" />
@@ -666,16 +1454,17 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                         </span>
                         <span className="sr-only">{t('common.notify')}</span>
                       </button>
-                      <button
-                        type="button"
-                        className="inline-flex h-8 w-8 items-center justify-center rounded-[2px] text-[hsl(var(--muted-foreground))] transition hover:bg-[hsl(var(--accent)/0.35)] hover:text-[hsl(var(--foreground))]"
+                      <HzHeaderIconButton
+                        label={notificationMuteLabel}
+                        state={notificationsMuted ? 'active' : 'inactive'}
                         onClick={toggleMute}
-                        aria-label={notificationsMuted ? t('common.unmute') : t('common.mute')}
                         data-app-frame-icon-trigger="mute"
+                        data-app-frame-mute-state={notificationsMuted ? 'muted' : 'audible'}
+                        data-app-frame-mute-save-lifecycle="angular-success-only-state-update"
+                        data-app-frame-mute-save-lifecycle-owner="route-action-state-contract"
                       >
                         <Megaphone size={16} data-app-frame-glyph="mute-megaphone" />
-                        <span className="sr-only">{notificationsMuted ? t('common.mute') : t('common.unmute')}</span>
-                      </button>
+                      </HzHeaderIconButton>
                     </div>
                     {notifyOpen ? (
                       <div className="absolute right-0 top-10 z-30 w-[320px] rounded-[4px] border border-[hsl(var(--border))] bg-[hsl(var(--card))] py-3 shadow-[0_10px_30px_rgba(0,0,0,.22)]">
@@ -694,9 +1483,19 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                             onClick={toggleMute}
                           >
                             <Megaphone size={15} />
-                            <span>{notificationsMuted ? t('common.unmute') : t('common.mute')}</span>
+                            <span>{notificationMuteLabel}</span>
                           </button>
                         </div>
+                        <div className="mt-3 px-4">
+                          <HzHeaderRealtimeNotice
+                            status={headerRealtimeStatus}
+                            title={t('dashboard.alerts.notifications-title')}
+                            description={headerRealtimeNotice?.description || t('dashboard.alerts.enter')}
+                            meta={headerRealtimeNotice?.meta || HEADER_ALERT_EVENT_TYPE}
+                            data-app-frame-header-realtime-notice-owner="hertzbeat-ui-header-realtime-notice"
+                            data-app-frame-header-realtime-status={headerRealtimeStatus}
+                          />
+                        </div>
                         <div className="mt-3 border-t border-[hsl(var(--border))]">
                           {alertNotices.length > 0 ? (
                             alertNotices.map(item => (
@@ -715,7 +1514,7 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                                 <span className="min-w-0">
                                   <span className="block truncate text-[12px] font-medium text-[hsl(var(--foreground))]">{item.title}</span>
                                   <span className="mt-1 block text-[11px] text-[hsl(var(--muted-foreground))]">
-                                    {item.status} · {formatHeaderTime(item.activeAt)}
+                                    {item.status} · {formatHeaderTime(item.activeAt, emptyValue)}
                                   </span>
                                 </span>
                               </button>
@@ -757,23 +1556,29 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                       aria-label={t('menu.settings')}
                       onClick={() => setSettingsOpen(open => !open)}
                       data-app-frame-icon-trigger="settings"
+                      data-app-frame-locale-reload-contract="angular-load-use-layout-reload"
+                      data-app-frame-locale-reload-owner="route-locale-reload-contract"
                     >
                       <Settings size={16} data-app-frame-glyph="settings-gear" />
                       <span className="sr-only">{t('menu.settings')}</span>
                     </button>
                     {settingsOpen ? (
                       <div className="absolute right-0 top-10 z-30 min-w-[220px] rounded-[4px] border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-1 shadow-[0_8px_24px_rgba(0,0,0,.22)]">
-                        <button
-                          type="button"
-                          className="flex w-full items-center gap-2 rounded-[2px] px-2.5 py-2 text-left text-[12px] text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent)/0.5)]"
+                        <HzHeaderMenuAction
+                          label={fullscreenLabel}
+                          state={fullscreenActive ? 'active' : 'inactive'}
                           onClick={() => {
                             setSettingsOpen(false);
                             toggleFullscreen();
                           }}
+                          data-app-frame-settings-fullscreen-action="angular-toggle"
                         >
-                          <Maximize2 size={14} />
-                          {t('common.fullscreen')}
-                        </button>
+                          {fullscreenActive ? (
+                            <Minimize2 size={14} data-app-frame-glyph="fullscreen-exit" />
+                          ) : (
+                            <Maximize2 size={14} data-app-frame-glyph="fullscreen-enter" />
+                          )}
+                        </HzHeaderMenuAction>
                         <Link
                           href="/setting/labels"
                           className="flex items-center gap-2 rounded-[2px] px-2.5 py-2 text-[12px] text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent)/0.5)]"
@@ -792,8 +1597,10 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                           inactiveItemClassName="text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent)/0.5)]"
                           activeIndicatorClassName="text-[11px] text-[hsl(var(--muted-foreground))]"
                           onSelect={async nextLocale => {
+                            showHeaderLocaleReloadSpinner();
                             await setLocale(nextLocale);
                             setSettingsOpen(false);
+                            window.location.reload();
                           }}
                         />
                       </div>
@@ -804,42 +1611,55 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
                       type="button"
                       className="inline-flex h-8 w-8 items-center justify-center rounded-[2px] text-[hsl(var(--muted-foreground))] transition hover:bg-[hsl(var(--accent)/0.35)] hover:text-[hsl(var(--foreground))]"
                       onClick={() => setUserOpen(open => !open)}
-                      aria-label="admin"
+                      aria-label={t('app.frame.utility.user', { user: currentUserName })}
                       data-app-frame-icon-trigger="user"
+                      data-app-frame-user-name={currentUserName}
+                      data-app-frame-user-role={sessionUser?.role || 'unknown'}
+                      data-app-frame-user-logout-lifecycle-contract="angular-clear-then-passport-login"
+                      data-app-frame-user-logout-lifecycle-owner="route-session-contract"
+                      data-app-frame-about-closable-contract="angular-nz-closable-false"
+                      data-app-frame-about-cancel-contract="angular-on-cancel"
                     >
                       <span
                         className="flex h-6 w-6 items-center justify-center rounded-full bg-[#d9dde5]"
                         data-app-frame-user-avatar="angular-circle"
                       />
-                      <span className="sr-only">admin</span>
+                      <span className="sr-only">{t('app.frame.utility.user', { user: currentUserName })}</span>
                     </button>
                     {userOpen ? (
-                      <div className="absolute right-0 top-10 z-30 min-w-[180px] rounded-[4px] border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-1 shadow-[0_8px_24px_rgba(0,0,0,.22)]">
-                        <Link
+                      <div
+                        className="absolute right-0 top-10 z-30 min-w-[180px] rounded-[4px] border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-1 shadow-[0_8px_24px_rgba(0,0,0,.22)]"
+                        data-app-frame-user-menu="angular-width-sm"
+                      >
+                        <HzUserMenuAction
+                          component={Link}
                           href="/setting/settings/config"
-                          className="flex items-center gap-2 rounded-[2px] px-2.5 py-2 text-[12px] text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent)/0.5)]"
+                          item="setting"
+                          label={t('menu.extras.setting')}
                           onClick={() => setUserOpen(false)}
+                          data-app-frame-user-action="setting"
                         >
-                          <Settings2 size={14} />
-                          {t('menu.settings')}
-                        </Link>
-                        <a
-                          href="https://hertzbeat.apache.org/docs/"
-                          target="_blank"
-                          rel="noreferrer"
-                          className="flex items-center gap-2 rounded-[2px] px-2.5 py-2 text-[12px] text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent)/0.5)]"
-                        >
-                          <Info size={14} />
-                          {t('common.about')}
-                        </a>
-                        <button
-                          type="button"
-                          className="flex w-full items-center gap-2 rounded-[2px] px-2.5 py-2 text-left text-[12px] text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent)/0.5)]"
+                          <Wrench size={14} data-app-frame-user-menu-icon="tool" />
+                        </HzUserMenuAction>
+                        <HzUserMenuAction
+                          item="logout"
+                          label={t('menu.account.logout')}
                           onClick={logout}
+                          data-app-frame-user-action="logout"
+                          data-app-frame-user-logout-lifecycle="angular-clear-then-passport-login"
+                          data-app-frame-user-logout-lifecycle-owner="route-session-contract"
                         >
-                          <LogOut size={14} />
-                          {t('common.logout')}
-                        </button>
+                          <LogOut size={14} data-app-frame-user-menu-icon="logout" />
+                        </HzUserMenuAction>
+                        <HzUserMenuAction
+                          item="about"
+                          label={t('menu.extras.about')}
+                          onClick={openAboutModal}
+                          data-app-frame-user-action="about"
+                          data-app-frame-user-about-action="angular-modal"
+                        >
+                          <MapPin size={14} data-app-frame-user-menu-icon="environment" />
+                        </HzUserMenuAction>
                       </div>
                     ) : null}
                   </div>
@@ -850,30 +1670,196 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
 
           <AppSidebar pathname={pathname} t={t} />
 
-          <main className="relative min-w-0 self-start">
+          <main
+            className="relative flex min-h-0 min-w-0 flex-col self-stretch"
+            data-platform-main-scroll="content-flow"
+          >
             <div data-app-frame-content-shell={buildContentFrameShellKind(pathname)} className={buildContentFrameClassName(pathname)}>
               {children}
             </div>
             <PlatformCopyrightFooter
-              className="border-t border-[var(--ops-border-color)] py-2.5 text-center"
+              className="mt-auto border-t border-[var(--ops-border-color)] py-2.5 text-center"
               data-platform-footer="angular-footer"
+              data-platform-footer-placement="flow-end-or-viewport-bottom"
               headlineClassName="text-[11px] font-semibold text-[var(--ops-text-secondary)]"
               innerClassName="space-y-0.5"
               lineClassName="text-[10px] leading-4 text-[var(--ops-text-tertiary)]"
               linkClassName="text-[var(--ops-text-secondary)]"
               version={PLATFORM_FOOTER_VERSION}
             />
-            <a
-              aria-label={t('common.help')}
+            <HzAboutModalSurface
+              open={aboutOpen}
+              title={t('about.title')}
+              points={[
+                t('about.point.1'),
+                t('about.point.2'),
+                t('about.point.3'),
+                t('about.point.4'),
+                t('about.point.5'),
+                t('about.point.6')
+              ]}
+              help={t('about.help')}
+              version={PLATFORM_FOOTER_VERSION}
+              releaseHref={`https://github.com/apache/hertzbeat/releases/tag/${PLATFORM_FOOTER_VERSION}`}
+              copyright={t('about.copyright', { year: new Date().getFullYear() })}
+              notShowLabel={t('about.not-show-next-login')}
+              notShowChecked={aboutNotShowNextLogin}
+              closeLabel={t('common.dialog.close')}
+              communityLinks={[
+                { href: 'https://github.com/apache/hertzbeat', label: t('about.github') },
+                { href: 'https://github.com/apache/hertzbeat/issues', label: t('about.issue') },
+                { href: 'https://github.com/apache/hertzbeat/pulls', label: t('about.pr') },
+                { href: 'https://discord.com/invite/Fb6M73htGr', label: t('about.discuss') },
+                { href: 'https://hertzbeat.apache.org/docs/', label: t('about.doc') },
+                { href: 'https://hertzbeat.apache.org/docs/start/upgrade', label: t('about.upgrade') },
+                { href: 'https://github.com/apache/hertzbeat', label: t('about.star') }
+              ]}
+              onClose={() => setAboutOpen(false)}
+              onNotShowChange={changeAboutNotShowNextLogin}
+              data-app-frame-about-modal="angular-user-menu"
+              data-app-frame-about-closable-contract="angular-nz-closable-false"
+              data-app-frame-about-cancel-contract="angular-on-cancel"
+            />
+            <button
+              type="button"
+              aria-label={t('ai.chat.launch')}
               className="fixed bottom-4 right-5 z-40 inline-flex h-12 w-12 items-center justify-center rounded-[6px] bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] shadow-[0_14px_38px_rgba(67,97,238,0.34)] transition hover:brightness-110"
-              data-shell-help-launcher="angular-help"
-              href="https://hertzbeat.apache.org/docs/"
-              rel="noreferrer"
-              target="_blank"
+              data-shell-ai-chat-launcher="angular-ai-chat"
+              data-app-frame-ai-chat-launcher-contract="angular-empty-modal"
+              data-app-frame-ai-chat-config-save-lifecycle-contract="angular-validate-save-close-refresh"
+              data-app-frame-ai-chat-config-save-lifecycle-owner="route-ai-chat-config-contract"
+              data-app-frame-ai-chat-conversation-action-lifecycle-contract="angular-create-select-delete-fallback"
+              data-app-frame-ai-chat-conversation-action-lifecycle-owner="route-ai-chat-conversation-contract"
+              data-app-frame-ai-chat-schedule-action-lifecycle-contract="angular-load-create-toggle-revert-confirm-update-delete"
+              data-app-frame-ai-chat-schedule-action-lifecycle-owner="route-ai-chat-schedule-contract"
+              data-app-frame-ai-chat-stream-history-lifecycle-contract="angular-push-user-placeholder-sse-skill-report-refresh"
+              data-app-frame-ai-chat-stream-history-lifecycle-owner="route-ai-chat-stream-contract"
+              onClick={() => openAiChatModal()}
             >
               <Bot size={28} strokeWidth={2.2} />
-              <span className="sr-only">{t('common.help')}</span>
-            </a>
+              <span className="sr-only">{t('ai.chat.launch')}</span>
+            </button>
+            {aiChatOpen ? (
+              <HzAiChatModalSurface
+                data-app-frame-ai-chat-owner="hertzbeat-ui-ai-chat-modal"
+                data-app-frame-ai-chat-initial-message-contract={aiInitialPrompt ? 'angular-open-modal-initial-message' : 'angular-empty-modal'}
+                data-app-frame-ai-chat-initial-message-owner="route-ai-chat-modal-contract"
+                data-app-frame-ai-chat-config-save-lifecycle-contract="angular-validate-save-close-refresh"
+                data-app-frame-ai-chat-config-save-lifecycle-owner="route-ai-chat-config-contract"
+                data-app-frame-ai-chat-conversation-action-lifecycle-contract="angular-create-select-delete-fallback"
+                data-app-frame-ai-chat-conversation-action-lifecycle-owner="route-ai-chat-conversation-contract"
+                data-app-frame-ai-chat-schedule-action-lifecycle-contract="angular-load-create-toggle-revert-confirm-update-delete"
+                data-app-frame-ai-chat-schedule-action-lifecycle-owner="route-ai-chat-schedule-contract"
+                data-app-frame-ai-chat-stream-history-lifecycle-contract="angular-push-user-placeholder-sse-skill-report-refresh"
+                data-app-frame-ai-chat-stream-history-lifecycle-owner="route-ai-chat-stream-contract"
+                title={t('ai.chat.title')}
+                subtitle={t('ai.chat.subtitle')}
+                conversationsTitle={t('ai.chat.conversations')}
+                newChatLabel={t('ai.chat.new-chat')}
+                newChatStatus={aiNewConversationStatus}
+                deleteLabel={t('ai.chat.conversation.delete.title')}
+                deleteConfirmLabel={t('ai.chat.conversation.delete.confirm')}
+                deleteCancelLabel={t('common.cancel')}
+                deleteStatus={aiDeleteConversationStatus}
+                deleteConversationId={aiDeleteConversationId}
+                welcomeTitle={t('ai.chat.welcome.title')}
+                welcomeDescription={t('ai.chat.welcome.description')}
+                inputPlaceholder={t('ai.chat.input.placeholder')}
+                inputValue={aiDraftMessage}
+                inputHint={t('ai.chat.input.hint')}
+                closeLabel={t('common.dialog.close')}
+                sendLabel={t('ai.chat.submit')}
+                sendStatus={aiSendStatus}
+                streamingLabel={t('ai.chat.typing')}
+                configOpen={aiConfigOpen}
+                configTitle={t('ai.chat.config.title')}
+                configDescription={t('ai.chat.config.required.content')}
+                configStatus={aiConfigStatus}
+                configStatusLabel={t(aiConfigStatusLabelKey)}
+                configTriggerLabel={t('ai.chat.modify-api-key')}
+                configProviderLabel={t('ai.chat.config.provider')}
+                configProviderHelp={t('ai.chat.config.provider.help')}
+                configApiKeyLabel={t('ai.chat.config.api-key')}
+                configApiKeyHelp={t('ai.chat.config.api-key.help')}
+                configBaseUrlLabel={t('ai.chat.config.base-url')}
+                configBaseUrlHelp={t('ai.chat.config.base-url.help')}
+                configModelLabel={t('ai.chat.config.model')}
+                configModelHelp={t('ai.chat.config.model.help')}
+                configResetLabel={t('ai.chat.config.reset')}
+                configSaveLabel={t('ai.chat.config.save')}
+                configCancelLabel={t('ai.chat.config.cancel')}
+                configProviderOptions={AI_CHAT_PROVIDER_OPTIONS}
+                configValue={aiProviderConfig}
+                scheduleOpen={aiScheduleOpen}
+                scheduleStatus={aiScheduleStatus}
+                scheduleStatusLabel={t(aiScheduleStatusLabelKey)}
+                scheduleTriggerLabel={t('ai.chat.schedule.button')}
+                scheduleTitle={t('ai.chat.schedule.title')}
+                scheduleConfiguredTitle={t('ai.chat.schedule.configured')}
+                scheduleCreateTitle={t('ai.chat.schedule.create')}
+                scheduleAddTitle={t('ai.chat.schedule.add')}
+                scheduleSkillLabel={t('ai.chat.schedule.skill')}
+                scheduleSkillSelectLabel={t('ai.chat.schedule.skill.select')}
+                scheduleSkillPlaceholder={t('ai.chat.schedule.skill.placeholder')}
+                scheduleCronLabel={t('ai.chat.schedule.cron.label')}
+                scheduleCronHelp={t('ai.chat.schedule.cron.help')}
+                scheduleCronCommonLabel={t('ai.chat.schedule.cron.common')}
+                scheduleCronMondayLabel={t('ai.chat.schedule.cron.monday')}
+                scheduleStatusColumnLabel={t('ai.chat.schedule.status')}
+                scheduleActionLabel={t('ai.chat.schedule.action')}
+                scheduleEnabledLabel={t('common.enabled')}
+                scheduleDisabledLabel={t('common.disabled')}
+                scheduleEditLabel={t('ai.chat.schedule.edit')}
+                scheduleDeleteLabel={t('common.button.delete')}
+                scheduleDeleteConfirmLabel={t('ai.chat.conversation.delete.confirm')}
+                scheduleDeleteCancelLabel={t('common.button.cancel')}
+                scheduleDeleteStatus={aiDeleteScheduleStatus}
+                scheduleDeleteScheduleId={aiDeleteScheduleId}
+                scheduleSaveLabel={t('common.button.save')}
+                scheduleCancelLabel={t('common.button.cancel')}
+                scheduleCreateLabel={t('ai.chat.schedule.create')}
+                scheduleRows={aiSchedules}
+                scheduleSkills={aiScheduleSkills}
+                scheduleDraft={aiScheduleDraft}
+                scheduleEditDraft={aiScheduleEditDraft}
+                initialMessageLabel={t('ai.chat.initial-message')}
+                initialMessage={aiInitialPrompt}
+                conversationStatus={aiConversations.status}
+                conversationStatusLabel={t(aiConversations.statusLabelKey)}
+                conversations={aiConversationRows}
+                onNewConversation={createNewAiConversation}
+                onConversationSelect={setSelectedAiConversationId}
+                onConversationDeleteRequest={requestDeleteAiConversation}
+                onConversationDeleteCancel={cancelDeleteAiConversation}
+                onConversationDeleteConfirm={confirmDeleteAiConversation}
+                messageStatus={aiMessageStatus}
+                messageStatusLabel={t(aiMessageStatusLabelKey)}
+                conversationMessages={aiConversationMessageRows}
+                onInputChange={setAiDraftMessage}
+                onSendMessage={sendAiMessage}
+                onConfigOpen={openAiProviderConfig}
+                onConfigClose={() => setAiConfigOpen(false)}
+                onConfigSave={saveAiProviderConfig}
+                onConfigResetDefaults={resetAiProviderDefaults}
+                onConfigChange={changeAiProviderConfig}
+                onScheduleOpen={openAiSchedulePanel}
+                onScheduleClose={() => setAiScheduleOpen(false)}
+                onScheduleDraftChange={setAiScheduleDraft}
+                onScheduleCreate={createAiScheduleFromDraft}
+                onScheduleToggle={toggleAiSchedule}
+                onScheduleEditStart={startAiScheduleEdit}
+                onScheduleEditCancel={() => setAiScheduleEditDraft(null)}
+                onScheduleEditChange={setAiScheduleEditDraft}
+                onScheduleUpdate={updateAiScheduleFromDraft}
+                onScheduleDeleteRequest={requestDeleteAiSchedule}
+                onScheduleDeleteCancel={cancelDeleteAiSchedule}
+                onScheduleDeleteConfirm={confirmDeleteAiSchedule}
+                previewMessages={[
+                  { role: 'assistant', label: t('ai.chat.assistant'), content: t('ai.chat.offline.response') }
+                ]}
+                onClose={() => setAiChatOpen(false)}
+              />
+            ) : null}
           </main>
         </div>
       </div>
diff --git a/web-next/components/shell/app-sidebar.test.tsx b/web-next/components/shell/app-sidebar.test.tsx
index d60356c30c..15a3ed4a7f 100644
--- a/web-next/components/shell/app-sidebar.test.tsx
+++ b/web-next/components/shell/app-sidebar.test.tsx
@@ -1,6 +1,7 @@
 import * as React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 import { AppSidebar } from './app-sidebar';
 
 vi.mock('next/link', () => ({
@@ -11,50 +12,73 @@ vi.mock('next/link', () => ({
   )
 }));
 
+const t = createTranslatorMock({ locale: 'zh-CN' });
+
 describe('AppSidebar', () => {
   it('renders translated navigation groups without the old operator footer card', () => {
-    const html = renderToStaticMarkup(
-      <AppSidebar pathname="/overview" t={key => `translated:${key}`} />
-    );
+    const html = renderToStaticMarkup(<AppSidebar pathname="/overview" t={t} />);
 
     expect(html).toContain('data-app-sidebar="true"');
     expect(html).toContain('data-app-sidebar-nav="true"');
     expect(html).toContain('data-shell-nav-item="overview-home"');
-    expect(html).toContain('translated:menu.section.ingestion');
-    expect(html).toContain('translated:menu.section.objects');
-    expect(html).toContain('translated:menu.section.observability');
-    expect(html).toContain('translated:menu.section.alerting');
-    expect(html).toContain('translated:menu.section.dashboards');
-    expect(html).toContain('translated:menu.section.settings');
-    expect(html).not.toContain('translated:menu.home');
-    expect(html).not.toContain('>translated:menu.monitor</div>');
+    expect(html).toContain('接入采集');
+    expect(html).toContain('对象资源');
+    expect(html).toContain('可观测排障');
+    expect(html).toContain('告警处置');
+    expect(html).toContain('仪表盘');
+    expect(html).toContain('平台设置');
     expect(html).toContain('data-shell-nav-item="settings-mcp-server"');
-    expect(html).toContain('translated:menu.advanced.mcp-server');
+    expect(html).toContain('MCP服务器');
     expect(html).toContain('data-shell-nav-item="help-center"');
-    expect(html).toContain('translated:menu.extras.help');
+    expect(html).toContain('帮助中心');
     expect(html).not.toContain('data-shell-nav-item="incidents"');
     expect(html).not.toContain('data-shell-nav-item="actions"');
     expect(html).not.toContain('data-shell-nav-item="status-public"');
     expect(html).toContain('href="/overview"');
     expect(html).not.toContain('data-app-sidebar-operator="true"');
-    expect(html).not.toContain('translated:common.operator');
-    expect(html).not.toContain('translated:common.session.authenticated');
+    expect(html).not.toContain('操作员');
+    expect(html).not.toContain('已认证会话');
   });
 
   it('flattens shared nav rows so the sidebar no longer renders every item as a bordered card with a boxed icon', () => {
-    const html = renderToStaticMarkup(
-      <AppSidebar pathname="/overview" t={key => `translated:${key}`} />
-    );
+    const html = renderToStaticMarkup(<AppSidebar pathname="/overview" t={t} />);
 
     expect(html).toContain('data-shell-nav-item="entities"');
     expect(html).not.toContain('rounded-[2px] border px-3 py-1.5');
     expect(html).not.toContain('rounded-[2px] border transition-colors');
   });
 
+  it('renders top-level nav rows with a left rail active treatment instead of filled card states', () => {
+    const html = renderToStaticMarkup(<AppSidebar pathname="/monitors/640360126405888" t={t} />);
+
+    expect(html).toContain('data-app-sidebar-visual="left-rail-primary-nav"');
+    expect(html).toContain('data-shell-nav-visual="left-rail"');
+    expect(html).toContain('data-shell-nav-item-active="true"');
+    expect(html).toContain('data-shell-nav-active-rail="true"');
+    expect(html).toContain('data-shell-nav-icon-key="monitor"');
+    expect(html).not.toContain('bg-[var(--ops-surface-raised)]');
+    expect(html).not.toContain('hover:bg-[var(--ops-surface-panel)]');
+  });
+
+  it('keeps visible primary menu icons specific to each operator workflow', () => {
+    const html = renderToStaticMarkup(<AppSidebar pathname="/monitors" t={t} />);
+
+    [
+      'data-shell-nav-icon-key="otlp"',
+      'data-shell-nav-icon-key="monitor"',
+      'data-shell-nav-icon-key="collector"',
+      'data-shell-nav-icon-key="monitor-template"',
+      'data-shell-nav-icon-key="entity-discovery"',
+      'data-shell-nav-icon-key="entity-definition"',
+      'data-shell-nav-icon-key="alert-group"',
+      'data-shell-nav-icon-key="alert-silence"',
+      'data-shell-nav-icon-key="plugins"',
+      'data-shell-nav-icon-key="help"'
+    ].forEach(marker => expect(html).toContain(marker));
+  });
+
   it('bounds the navigation scroll area so long menus do not create page-level footer whitespace', () => {
-    const html = renderToStaticMarkup(
-      <AppSidebar pathname="/alert/silence" t={key => `translated:${key}`} />
-    );
+    const html = renderToStaticMarkup(<AppSidebar pathname="/alert/silence" t={t} />);
 
     expect(html).toContain('data-app-sidebar-scroll-owner="bounded-sidebar-nav"');
     expect(html).toContain('flex h-full min-h-0 flex-col overflow-hidden');
diff --git a/web-next/components/shell/app-sidebar.tsx b/web-next/components/shell/app-sidebar.tsx
index ce0c82e007..47425fc572 100644
--- a/web-next/components/shell/app-sidebar.tsx
+++ b/web-next/components/shell/app-sidebar.tsx
@@ -13,6 +13,7 @@ export function AppSidebar({ pathname, t }: { pathname: string; t: Translator })
       className="flex h-full min-h-0 flex-col overflow-hidden border-b border-[var(--ops-border-color)] bg-[var(--ops-background)] py-0 lg:h-[calc(100vh-64px)] lg:border-b-0 lg:border-r"
       data-app-sidebar="true"
       data-app-sidebar-scroll-owner="bounded-sidebar-nav"
+      data-app-sidebar-visual="left-rail-primary-nav"
     >
       <nav className="mt-1 min-h-0 flex-1 space-y-1 overflow-y-auto overflow-x-hidden px-2 pb-2" data-app-sidebar-nav="true">
         {sections.map(section => (
@@ -26,14 +27,28 @@ export function AppSidebar({ pathname, t }: { pathname: string; t: Translator })
                   href={item.href}
                   key={item.key}
                   data-shell-nav-item={item.key}
+                  data-shell-nav-item-active={item.active ? 'true' : 'false'}
+                  data-shell-nav-visual="left-rail"
                   className={cn(
-                    'group flex items-center gap-2 rounded-[2px] px-3 py-1.5 transition-colors duration-150',
+                    'group grid min-h-8 grid-cols-[3px_18px_minmax(0,1fr)] items-center gap-2 px-0 pr-2 transition-colors duration-150',
                     item.active
-                      ? 'bg-[var(--ops-surface-raised)] text-[var(--ops-text-primary)]'
-                      : 'text-[var(--ops-text-secondary)] hover:bg-[var(--ops-surface-panel)] hover:text-[var(--ops-text-primary)]'
+                      ? 'text-[var(--ops-text-primary)]'
+                      : 'text-[var(--ops-text-secondary)] hover:text-[var(--ops-text-primary)]'
                   )}
                 >
                   <span
+                    aria-hidden="true"
+                    className={cn(
+                      'h-5 w-[2px] justify-self-start bg-transparent transition-colors',
+                      item.active
+                        ? 'bg-[var(--hz-ui-accent)]'
+                        : 'group-hover:bg-[var(--ops-border-strong)]'
+                    )}
+                    data-shell-nav-active-rail={item.active ? 'true' : 'false'}
+                  />
+                  <span
+                    data-shell-nav-icon-key={item.iconKey}
+                    data-shell-nav-icon-role="line-art"
                     className={cn(
                       'flex h-4 w-4 shrink-0 items-center justify-center transition-colors',
                       item.active
diff --git a/web-next/components/shell/auth-gate.tsx b/web-next/components/shell/auth-gate.tsx
index 25dbaad706..b7784eec8d 100644
--- a/web-next/components/shell/auth-gate.tsx
+++ b/web-next/components/shell/auth-gate.tsx
@@ -2,19 +2,26 @@
 
 import { useEffect, useState } from 'react';
 import { buildLoginRedirectHref, buildLoginReturnTo } from '@/lib/passport-login/controller';
+import { readClientSessionState } from '@/lib/session-client';
 
 export function AuthGate({ children }: { children: React.ReactNode }) {
   const [ready, setReady] = useState(false);
   const [authed, setAuthed] = useState(false);
 
   useEffect(() => {
-    const token = window.localStorage.getItem('Authorization');
-    setAuthed(!!token);
-    setReady(true);
-    if (!token) {
-      const returnTo = buildLoginReturnTo(window.location);
-      window.location.href = buildLoginRedirectHref(returnTo, process.env.NEXT_PUBLIC_LOGIN_PATH);
-    }
+    let cancelled = false;
+    readClientSessionState().then(session => {
+      if (cancelled) return;
+      setAuthed(session.authenticated);
+      setReady(true);
+      if (!session.authenticated) {
+        const returnTo = buildLoginReturnTo(window.location);
+        window.location.href = buildLoginRedirectHref(returnTo, process.env.NEXT_PUBLIC_LOGIN_PATH);
+      }
+    });
+    return () => {
+      cancelled = true;
+    };
   }, []);
 
   if (!ready || !authed) {
diff --git a/web-next/components/shell/locale-option-list.test.tsx b/web-next/components/shell/locale-option-list.test.tsx
index 6789d0554a..4b9c74df15 100644
--- a/web-next/components/shell/locale-option-list.test.tsx
+++ b/web-next/components/shell/locale-option-list.test.tsx
@@ -1,15 +1,10 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 import { LocaleOptionList } from './locale-option-list';
 
-const t = vi.fn((key: string) => {
-  const labels: Record<string, string> = {
-    'settings.system-config.locale.en_US': 'English(en_US)',
-    'settings.system-config.locale.zh_CN': '简体中文(zh_CN)'
-  };
-  return labels[key] || key;
-});
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 vi.mock('../providers/i18n-provider', () => ({
   useI18n: () => ({ t })
@@ -32,9 +27,14 @@ describe('LocaleOptionList', () => {
       />
     );
 
-    expect(html).toContain('English(en_US)');
+    expect(html).toContain('英语(en_US)');
     expect(html).toContain('简体中文(zh_CN)');
     expect(html).toContain('✓');
+    expect(html).toContain('data-hz-ui="locale-menu-option"');
+    expect(html).toContain('data-hz-locale-menu-option-owner="hertzbeat-ui-locale-menu-option"');
+    expect(html).toContain('data-hz-locale-menu-option-density="angular-header-locale-item"');
+    expect(html).toContain('data-hz-locale-menu-option-selected="true"');
+    expect(html).toContain('data-app-frame-locale-option="zh-CN"');
     expect(t).toHaveBeenCalledWith('settings.system-config.locale.en_US');
     expect(t).toHaveBeenCalledWith('settings.system-config.locale.zh_CN');
   });
diff --git a/web-next/components/shell/locale-option-list.tsx b/web-next/components/shell/locale-option-list.tsx
index 1bed8e0a44..0b41b3dabb 100644
--- a/web-next/components/shell/locale-option-list.tsx
+++ b/web-next/components/shell/locale-option-list.tsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import { HzLocaleMenuOption } from '@hertzbeat/ui';
 import { useI18n } from '../providers/i18n-provider';
 import { cn } from '../../lib/utils';
 import type { LOCALES, LocaleCode } from '../../lib/i18n';
@@ -29,22 +30,19 @@ export function LocaleOptionList({
   return (
     <div className={className}>
       {locales.map(option => (
-        <button
+        <HzLocaleMenuOption
           key={option.code}
-          type="button"
+          abbr={option.abbr}
+          label={t(option.labelKey)}
+          selected={locale === option.code}
           className={cn(
-            'flex w-full items-center gap-2 text-left transition',
             itemClassName,
             locale === option.code ? activeItemClassName : inactiveItemClassName
           )}
+          indicatorClassName={activeIndicatorClassName}
           onClick={() => void onSelect(option.code)}
-        >
-          <span className="inline-flex min-w-5 items-center justify-center text-[14px] leading-none">
-            {option.abbr}
-          </span>
-          <span className="flex-1">{t(option.labelKey)}</span>
-          {locale === option.code ? <span className={activeIndicatorClassName}>✓</span> : null}
-        </button>
+          data-app-frame-locale-option={option.code}
+        />
       ))}
     </div>
   );
diff --git a/web-next/components/shell/platform-copyright-footer.test.tsx b/web-next/components/shell/platform-copyright-footer.test.tsx
index ae93bfe908..cebb891ee4 100644
--- a/web-next/components/shell/platform-copyright-footer.test.tsx
+++ b/web-next/components/shell/platform-copyright-footer.test.tsx
@@ -5,10 +5,16 @@ import { PlatformCopyrightFooter } from './platform-copyright-footer';
 
 describe('PlatformCopyrightFooter', () => {
   it('renders the shared Apache HertzBeat copyright copy', () => {
-    const html = renderToStaticMarkup(<PlatformCopyrightFooter version="1.0.0" />);
+    const html = renderToStaticMarkup(
+      <PlatformCopyrightFooter
+        version="1.0.0"
+      />
+    );
 
     expect(html).toContain('Apache HertzBeat™ 1.0.0');
-    expect(html).toContain('Licensed under the Apache License, Version 2.0');
+    expect(html).not.toContain('Licensed under the Apache License, Version 2.0');
+    expect(html).not.toContain('Apache License, Version 2.0');
+    expect(html).not.toContain('授权');
     expect(html).toContain('https://hertzbeat.apache.org');
   });
 });
diff --git a/web-next/components/shell/platform-copyright-footer.tsx b/web-next/components/shell/platform-copyright-footer.tsx
index 1a43a2ca76..0d4dd8b5e2 100644
--- a/web-next/components/shell/platform-copyright-footer.tsx
+++ b/web-next/components/shell/platform-copyright-footer.tsx
@@ -33,7 +33,6 @@ export function PlatformCopyrightFooter({
             Apache HertzBeat™
           </a>
         </div>
-        <div className={lineClassName}>Licensed under the Apache License, Version 2.0</div>
       </div>
     </footer>
   );
diff --git a/web-next/lib/ai-chat/conversations.test.ts b/web-next/lib/ai-chat/conversations.test.ts
new file mode 100644
index 0000000000..6fbd5a3300
--- /dev/null
+++ b/web-next/lib/ai-chat/conversations.test.ts
@@ -0,0 +1,232 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+  createAiChatConversation,
+  deleteAiChatConversation,
+  loadAiChatConversationHistory,
+  loadAiChatConversations,
+  mapAiChatConversationHistory,
+  mapAiChatConversations,
+  parseAiChatStreamData,
+  streamAiChatResponse
+} from './conversations';
+
+vi.mock('@/lib/api-client', () => ({
+  apiMessageGet: vi.fn(),
+  apiMessageDelete: vi.fn(),
+  apiMessagePost: vi.fn()
+}));
+
+describe('AI chat conversations controller', () => {
+  afterEach(() => {
+    vi.unstubAllGlobals();
+  });
+
+  it('maps Angular chat conversation records into shared modal rows', () => {
+    const state = mapAiChatConversations([
+      { id: 7, title: 'Checkout incident review', gmtUpdate: '2026-05-24T10:00:00Z' },
+      { id: 8, title: 'Collector health', gmtCreated: '2026-05-24T09:00:00Z' }
+    ]);
+
+    expect(state.status).toBe('ready');
+    expect(state.statusLabelKey).toBe('ai.chat.conversation.ready');
+    expect(state.conversations).toHaveLength(2);
+    expect(state.conversations[0]).toMatchObject({
+      id: 7,
+      title: 'Checkout incident review',
+      active: true
+    });
+    expect(state.conversations[1]).toMatchObject({
+      id: 8,
+      title: 'Collector health',
+      active: false
+    });
+  });
+
+  it('keeps an explicit empty state when the conversation API returns no rows', () => {
+    const state = mapAiChatConversations([]);
+
+    expect(state).toEqual({
+      status: 'empty',
+      conversations: [],
+      statusLabelKey: 'ai.chat.conversation.empty'
+    });
+  });
+
+  it('degrades the shell instead of throwing when the BFF conversation read fails', async () => {
+    const { apiMessageGet } = await import('@/lib/api-client');
+    vi.mocked(apiMessageGet).mockRejectedValueOnce(new Error('401'));
+
+    await expect(loadAiChatConversations()).resolves.toEqual({
+      status: 'error',
+      conversations: [
+        {
+          id: 0,
+          title: 'Offline Conversation',
+          subtitle: 'API unavailable',
+          active: true
+        }
+      ],
+      statusLabelKey: 'ai.chat.conversation.error'
+    });
+  });
+
+  it('maps selected Angular conversation history into shared message rows', () => {
+    const state = mapAiChatConversationHistory({
+      id: 7,
+      title: 'Checkout incident review',
+      messages: [
+        { role: 'user', content: 'Why did checkout fail?' },
+        { role: 'assistant', content: 'The collector reported a timeout.' },
+        { role: 'system_push', content: 'SOP report attached.' },
+        { role: 'assistant', content: '   ' }
+      ]
+    });
+
+    expect(state.status).toBe('ready');
+    expect(state.statusLabelKey).toBe('ai.chat.history.ready');
+    expect(state.conversationId).toBe(7);
+    expect(state.messages).toEqual([
+      { role: 'user', labelKey: 'ai.chat.user', content: 'Why did checkout fail?' },
+      { role: 'assistant', labelKey: 'ai.chat.assistant', content: 'The collector reported a timeout.' },
+      { role: 'system', labelKey: 'ai.chat.system', content: 'SOP report attached.' }
+    ]);
+  });
+
+  it('keeps an explicit empty state when the selected conversation has no history', () => {
+    const state = mapAiChatConversationHistory({ id: 8, messages: [] });
+
+    expect(state).toEqual({
+      status: 'empty',
+      messages: [],
+      statusLabelKey: 'ai.chat.history.empty',
+      conversationId: 8
+    });
+  });
+
+  it('degrades selected conversation history instead of throwing when the detail read fails', async () => {
+    const { apiMessageGet } = await import('@/lib/api-client');
+    vi.mocked(apiMessageGet).mockRejectedValueOnce(new Error('500'));
+
+    await expect(loadAiChatConversationHistory(9)).resolves.toEqual({
+      status: 'error',
+      messages: [],
+      statusLabelKey: 'ai.chat.history.error',
+      conversationId: 9
+    });
+  });
+
+  it('creates a new conversation through the Angular POST endpoint and returns an active row', async () => {
+    const { apiMessagePost } = await import('@/lib/api-client');
+    vi.mocked(apiMessagePost).mockResolvedValueOnce({
+      id: 10,
+      title: 'New assistant conversation',
+      gmtUpdate: '2026-05-24T12:00:00Z',
+      messages: []
+    });
+
+    await expect(createAiChatConversation()).resolves.toMatchObject({
+      status: 'ready',
+      conversation: {
+        id: 10,
+        title: 'New assistant conversation',
+        active: true
+      },
+      statusLabelKey: 'ai.chat.conversation.ready'
+    });
+  });
+
+  it('falls back to an offline conversation when new conversation creation fails', async () => {
+    const { apiMessagePost } = await import('@/lib/api-client');
+    vi.mocked(apiMessagePost).mockRejectedValueOnce(new Error('503'));
+
+    await expect(createAiChatConversation()).resolves.toEqual({
+      status: 'error',
+      conversation: {
+        id: 0,
+        title: 'Offline Conversation',
+        subtitle: 'API unavailable',
+        active: true
+      },
+      statusLabelKey: 'ai.chat.conversation.error'
+    });
+  });
+
+  it('deletes a conversation through the Angular DELETE endpoint', async () => {
+    const { apiMessageDelete } = await import('@/lib/api-client');
+    vi.mocked(apiMessageDelete).mockResolvedValueOnce(undefined);
+
+    await expect(deleteAiChatConversation(10)).resolves.toEqual({
+      status: 'ready',
+      conversationId: 10,
+      statusLabelKey: 'ai.chat.conversation.ready'
+    });
+  });
+
+  it('reports a delete error without throwing when the DELETE endpoint fails', async () => {
+    const { apiMessageDelete } = await import('@/lib/api-client');
+    vi.mocked(apiMessageDelete).mockRejectedValueOnce(new Error('503'));
+
+    await expect(deleteAiChatConversation(10)).resolves.toEqual({
+      status: 'error',
+      conversationId: 10,
+      statusLabelKey: 'ai.chat.conversation.error'
+    });
+  });
+
+  it('parses Angular SSE stream chunks and raw fallback data', () => {
+    expect(parseAiChatStreamData('{"response":"hello","timestamp":"2026-05-24T12:00:00Z"}')).toMatchObject({
+      content: 'hello',
+      role: 'assistant',
+      gmtCreate: new Date('2026-05-24T12:00:00Z')
+    });
+    expect(parseAiChatStreamData('plain assistant text')).toMatchObject({
+      content: 'plain assistant text',
+      role: 'assistant'
+    });
+    expect(parseAiChatStreamData('[DONE]')).toBeNull();
+  });
+
+  it('streams chat through /api/chat/stream and emits assistant chunks', async () => {
+    const chunks: string[] = [];
+    const encoder = new TextEncoder();
+    const response = new Response(
+      new ReadableStream({
+        start(controller) {
+          controller.enqueue(encoder.encode('data: {"response":"Hel","timestamp":"2026-05-24T12:00:00Z"}\n'));
+          controller.enqueue(encoder.encode('data: {"response":"lo"}\n'));
+          controller.enqueue(encoder.encode('data: [DONE]\n'));
+          controller.close();
+        }
+      }),
+      { status: 200, headers: { 'Content-Type': 'text/event-stream' } }
+    );
+    const fetchMock = vi.fn().mockResolvedValue(response);
+    vi.stubGlobal('fetch', fetchMock);
+
+    await streamAiChatResponse('Summarize checkout errors', {
+      conversationId: 10,
+      onChunk: chunk => chunks.push(chunk.content)
+    });
+
+    expect(fetchMock).toHaveBeenCalledWith('/api/chat/stream', expect.objectContaining({
+      method: 'POST',
+      headers: expect.objectContaining({
+        Accept: 'text/event-stream',
+        'Cache-Control': 'no-cache',
+        'Content-Type': 'application/json'
+      }),
+      credentials: 'same-origin',
+      cache: 'no-store',
+      body: JSON.stringify({ message: 'Summarize checkout errors', conversationId: 10 })
+    }));
+    expect(chunks).toEqual(['Hel', 'lo']);
+  });
+
+  it('fails the stream when the SSE endpoint returns a non-ok response', async () => {
+    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 503 })));
+
+    await expect(streamAiChatResponse('hello', { conversationId: 10, onChunk: () => {} })).rejects.toThrow(
+      'AI chat stream failed with status 503'
+    );
+  });
+});
diff --git a/web-next/lib/ai-chat/conversations.ts b/web-next/lib/ai-chat/conversations.ts
new file mode 100644
index 0000000000..021887731c
--- /dev/null
+++ b/web-next/lib/ai-chat/conversations.ts
@@ -0,0 +1,336 @@
+import type { HzAiChatConversationPreview, HzAiChatConversationStatus, HzAiChatMessageStatus } from '@hertzbeat/ui';
+import { apiMessageDelete, apiMessageGet, apiMessagePost } from '@/lib/api-client';
+
+const AI_CHAT_FETCH_TIMEOUT_MS = 3500;
+
+type RawChatMessage = {
+  content?: string | null;
+  role?: 'user' | 'assistant' | 'system' | 'system_push' | string | null;
+  gmtCreate?: string | number | Date | null;
+  createTime?: string | number | Date | null;
+};
+
+type RawChatConversation = {
+  id?: string | number | null;
+  title?: string | null;
+  gmtUpdate?: string | number | Date | null;
+  gmtUpdated?: string | number | Date | null;
+  updateTime?: string | number | Date | null;
+  updatedAt?: string | number | Date | null;
+  gmtCreated?: string | number | Date | null;
+  createTime?: string | number | Date | null;
+  messages?: RawChatMessage[] | null;
+};
+
+export type AiChatConversationMessage = {
+  role: 'user' | 'assistant' | 'system';
+  labelKey: string;
+  content: string;
+};
+
+export type AiChatConversationListState = {
+  status: HzAiChatConversationStatus;
+  conversations: HzAiChatConversationPreview[];
+  statusLabelKey: string;
+};
+
+export type AiChatConversationHistoryState = {
+  status: HzAiChatMessageStatus;
+  messages: AiChatConversationMessage[];
+  statusLabelKey: string;
+  conversationId?: string | number;
+};
+
+export type AiChatConversationCreateState = {
+  status: 'ready' | 'error';
+  conversation: HzAiChatConversationPreview;
+  statusLabelKey: string;
+};
+
+export type AiChatConversationDeleteState = {
+  status: 'ready' | 'error';
+  conversationId: string | number;
+  statusLabelKey: string;
+};
+
+export type AiChatStreamChunk = {
+  content: string;
+  role: 'assistant';
+  gmtCreate: Date;
+};
+
+export type AiChatStreamOptions = {
+  conversationId?: string | number | null;
+  signal?: AbortSignal;
+  onChunk: (chunk: AiChatStreamChunk) => void;
+};
+
+export const AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS = {
+  loading: 'ai.chat.conversation.loading',
+  ready: 'ai.chat.conversation.ready',
+  empty: 'ai.chat.conversation.empty',
+  error: 'ai.chat.conversation.error'
+} as const satisfies Record<HzAiChatConversationStatus, string>;
+
+export const AI_CHAT_MESSAGE_STATUS_LABEL_KEYS = {
+  idle: 'ai.chat.history.idle',
+  loading: 'ai.chat.history.loading',
+  ready: 'ai.chat.history.ready',
+  empty: 'ai.chat.history.empty',
+  error: 'ai.chat.history.error'
+} as const satisfies Record<HzAiChatMessageStatus, string>;
+
+export const AI_CHAT_MESSAGE_ROLE_LABEL_KEYS = {
+  user: 'ai.chat.user',
+  assistant: 'ai.chat.assistant',
+  system: 'ai.chat.system'
+} as const satisfies Record<AiChatConversationMessage['role'], string>;
+
+export function parseAiChatStreamData(raw: string): AiChatStreamChunk | null {
+  const json = raw.trim();
+  if (!json || json === '[DONE]') return null;
+
+  try {
+    const data = JSON.parse(json) as { response?: unknown; timestamp?: string | number | Date | null };
+    if (data.response === undefined) return null;
+    return {
+      content: String(data.response || ''),
+      role: 'assistant',
+      gmtCreate: data.timestamp ? new Date(data.timestamp) : new Date()
+    };
+  } catch {
+    return {
+      content: json,
+      role: 'assistant',
+      gmtCreate: new Date()
+    };
+  }
+}
+
+async function apiMessageGetWithTimeout<T>(path: string): Promise<T> {
+  const controller = new AbortController();
+  const timeout = globalThis.setTimeout(() => controller.abort(), AI_CHAT_FETCH_TIMEOUT_MS);
+  try {
+    return await apiMessageGet<T>(path, { signal: controller.signal });
+  } finally {
+    globalThis.clearTimeout(timeout);
+  }
+}
+
+async function promiseWithAiChatTimeout<T>(promise: Promise<T>): Promise<T> {
+  let timeout: ReturnType<typeof globalThis.setTimeout> | undefined;
+  try {
+    return await Promise.race([
+      promise,
+      new Promise<T>((_, reject) => {
+        timeout = globalThis.setTimeout(() => {
+          reject(new Error('AI chat request timed out'));
+        }, AI_CHAT_FETCH_TIMEOUT_MS);
+      })
+    ]);
+  } finally {
+    if (timeout) {
+      globalThis.clearTimeout(timeout);
+    }
+  }
+}
+
+function formatConversationTime(value: RawChatConversation[keyof RawChatConversation]) {
+  if (!value) return undefined;
+  const date = value instanceof Date ? value : new Date(value as string | number);
+  if (Number.isNaN(date.getTime())) return undefined;
+  return date.toLocaleString();
+}
+
+export function buildOfflineAiChatConversation(): HzAiChatConversationPreview {
+  return {
+    id: 0,
+    title: 'Offline Conversation',
+    subtitle: 'API unavailable',
+    active: true
+  };
+}
+
+export function mapAiChatConversationPreview(conversation: RawChatConversation, index = 0): HzAiChatConversationPreview {
+  const updated =
+    conversation.gmtUpdate ??
+    conversation.gmtUpdated ??
+    conversation.updateTime ??
+    conversation.updatedAt ??
+    conversation.gmtCreated ??
+    conversation.createTime;
+  return {
+    id: conversation.id ?? `conversation-${index}`,
+    title: conversation.title?.trim() || `Conversation ${index + 1}`,
+    subtitle: formatConversationTime(updated),
+    active: index === 0
+  };
+}
+
+export function mapAiChatConversations(raw: RawChatConversation[] | null | undefined): AiChatConversationListState {
+  const rows = (raw || [])
+    .map((conversation, index): HzAiChatConversationPreview | null => mapAiChatConversationPreview(conversation, index))
+    .filter((conversation): conversation is HzAiChatConversationPreview => conversation !== null);
+
+  return {
+    status: rows.length > 0 ? 'ready' : 'empty',
+    conversations: rows,
+    statusLabelKey: rows.length > 0 ? AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.ready : AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.empty
+  };
+}
+
+function normalizeMessageRole(role: RawChatMessage['role']): AiChatConversationMessage['role'] {
+  if (role === 'user' || role === 'assistant') return role;
+  return 'system';
+}
+
+export function mapAiChatConversationHistory(
+  raw: RawChatConversation | null | undefined,
+  conversationId?: string | number
+): AiChatConversationHistoryState {
+  const messages = (raw?.messages || [])
+    .map(message => {
+      const content = message.content?.trim();
+      if (!content) return null;
+      const role = normalizeMessageRole(message.role);
+      return {
+        role,
+        labelKey: AI_CHAT_MESSAGE_ROLE_LABEL_KEYS[role],
+        content
+      };
+    })
+    .filter((message): message is AiChatConversationMessage => message !== null);
+
+  return {
+    status: messages.length > 0 ? 'ready' : 'empty',
+    messages,
+    statusLabelKey: messages.length > 0 ? AI_CHAT_MESSAGE_STATUS_LABEL_KEYS.ready : AI_CHAT_MESSAGE_STATUS_LABEL_KEYS.empty,
+    conversationId: raw?.id ?? conversationId
+  };
+}
+
+export async function loadAiChatConversations(): Promise<AiChatConversationListState> {
+  try {
+    return mapAiChatConversations(await apiMessageGetWithTimeout<RawChatConversation[]>('/chat/conversations'));
+  } catch {
+    return {
+      status: 'error',
+      conversations: [buildOfflineAiChatConversation()],
+      statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.error
+    };
+  }
+}
+
+export async function createAiChatConversation(): Promise<AiChatConversationCreateState> {
+  try {
+    return {
+      status: 'ready',
+      conversation: {
+        ...mapAiChatConversationPreview(
+          await promiseWithAiChatTimeout(apiMessagePost<RawChatConversation>('/chat/conversations', {})),
+          0
+        ),
+        active: true
+      },
+      statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.ready
+    };
+  } catch {
+    return {
+      status: 'error',
+      conversation: buildOfflineAiChatConversation(),
+      statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.error
+    };
+  }
+}
+
+export async function deleteAiChatConversation(conversationId: string | number): Promise<AiChatConversationDeleteState> {
+  try {
+    const encodedConversationId = encodeURIComponent(String(conversationId));
+    await promiseWithAiChatTimeout(apiMessageDelete<void>(`/chat/conversations/${encodedConversationId}`));
+    return {
+      status: 'ready',
+      conversationId,
+      statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.ready
+    };
+  } catch {
+    return {
+      status: 'error',
+      conversationId,
+      statusLabelKey: AI_CHAT_CONVERSATION_STATUS_LABEL_KEYS.error
+    };
+  }
+}
+
+export async function loadAiChatConversationHistory(conversationId: string | number): Promise<AiChatConversationHistoryState> {
+  try {
+    const encodedConversationId = encodeURIComponent(String(conversationId));
+    return mapAiChatConversationHistory(
+      await apiMessageGetWithTimeout<RawChatConversation>(`/chat/conversations/${encodedConversationId}`),
+      conversationId
+    );
+  } catch {
+    return {
+      status: 'error',
+      messages: [],
+      statusLabelKey: AI_CHAT_MESSAGE_STATUS_LABEL_KEYS.error,
+      conversationId
+    };
+  }
+}
+
+export async function streamAiChatResponse(message: string, options: AiChatStreamOptions): Promise<void> {
+  const requestBody: { message: string; conversationId?: string | number } = { message };
+  if (options.conversationId) {
+    requestBody.conversationId = options.conversationId;
+  }
+
+  const response = await fetch('/api/chat/stream', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      Accept: 'text/event-stream',
+      'Cache-Control': 'no-cache'
+    },
+    credentials: 'same-origin',
+    cache: 'no-store',
+    body: JSON.stringify(requestBody),
+    signal: options.signal
+  });
+
+  if (!response.ok) {
+    throw new Error(`AI chat stream failed with status ${response.status}`);
+  }
+
+  const reader = response.body?.getReader();
+  if (!reader) {
+    throw new Error('AI chat stream response body is unavailable');
+  }
+
+  const decoder = new TextDecoder();
+  let buffer = '';
+
+  for (;;) {
+    const { value, done } = await reader.read();
+    if (done) break;
+
+    buffer += decoder.decode(value, { stream: true });
+    const lines = buffer.split('\n');
+    buffer = lines.pop() || '';
+
+    for (const line of lines) {
+      const trimmedLine = line.trim();
+      if (!trimmedLine.startsWith('data:')) continue;
+      const chunk = parseAiChatStreamData(trimmedLine.substring(5));
+      if (chunk) {
+        options.onChunk(chunk);
+      }
+    }
+  }
+
+  if (buffer.trim().startsWith('data:')) {
+    const chunk = parseAiChatStreamData(buffer.trim().substring(5));
+    if (chunk) {
+      options.onChunk(chunk);
+    }
+  }
+}
diff --git a/web-next/lib/ai-chat/provider-config.test.ts b/web-next/lib/ai-chat/provider-config.test.ts
new file mode 100644
index 0000000000..6cdc645466
--- /dev/null
+++ b/web-next/lib/ai-chat/provider-config.test.ts
@@ -0,0 +1,106 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+  AI_CHAT_PROVIDER_OPTIONS,
+  applyAiChatProviderDefaults,
+  buildDefaultAiChatProviderConfig,
+  loadAiChatProviderConfig,
+  saveAiChatProviderConfig
+} from './provider-config';
+
+vi.mock('@/lib/api-client', () => ({
+  apiMessageGet: vi.fn(),
+  apiMessagePost: vi.fn()
+}));
+
+describe('AI chat provider configuration controller', () => {
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('keeps the Angular provider option defaults', () => {
+    expect(AI_CHAT_PROVIDER_OPTIONS).toEqual([
+      { value: 'openai', label: 'OpenAI', defaultBaseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4' },
+      { value: 'zai', label: 'ZAI', defaultBaseUrl: 'https://api.z.ai/api/paas/v4', defaultModel: 'glm-4.6' },
+      { value: 'zhipu', label: 'ZhiPu', defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4', defaultModel: 'glm-4.6' }
+    ]);
+    expect(buildDefaultAiChatProviderConfig('zhipu')).toEqual({
+      code: 'zhipu',
+      apiKey: '',
+      baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
+      model: 'glm-4.6'
+    });
+  });
+
+  it('fills missing base URL and model from the selected provider', () => {
+    expect(applyAiChatProviderDefaults({ code: 'zai', apiKey: 'sk-zai', baseUrl: '', model: '' })).toEqual({
+      code: 'zai',
+      apiKey: 'sk-zai',
+      baseUrl: 'https://api.z.ai/api/paas/v4',
+      model: 'glm-4.6'
+    });
+  });
+
+  it('loads /config/provider and normalizes missing fields for the shared config panel', async () => {
+    const { apiMessageGet } = await import('@/lib/api-client');
+    vi.mocked(apiMessageGet).mockResolvedValueOnce({
+      code: 'openai',
+      apiKey: 'sk-live',
+      baseUrl: '',
+      model: ''
+    });
+
+    await expect(loadAiChatProviderConfig()).resolves.toEqual({
+      status: 'ready',
+      config: {
+        code: 'openai',
+        apiKey: 'sk-live',
+        baseUrl: 'https://api.openai.com/v1',
+        model: 'gpt-4'
+      },
+      statusLabelKey: 'ai.chat.config.ready'
+    });
+    expect(apiMessageGet).toHaveBeenCalledWith('/config/provider');
+  });
+
+  it('opens the panel with default OpenAI values when /config/provider fails', async () => {
+    const { apiMessageGet } = await import('@/lib/api-client');
+    vi.mocked(apiMessageGet).mockRejectedValueOnce(new Error('503'));
+
+    await expect(loadAiChatProviderConfig()).resolves.toEqual({
+      status: 'error',
+      config: {
+        code: 'openai',
+        apiKey: '',
+        baseUrl: 'https://api.openai.com/v1',
+        model: 'gpt-4'
+      },
+      statusLabelKey: 'ai.chat.config.save.failed'
+    });
+  });
+
+  it('saves /config/provider with defaults applied', async () => {
+    const { apiMessagePost } = await import('@/lib/api-client');
+    vi.mocked(apiMessagePost).mockResolvedValueOnce({});
+
+    await expect(saveAiChatProviderConfig({ code: 'zhipu', apiKey: 'sk-zhipu', baseUrl: '', model: '' })).resolves.toEqual({
+      status: 'saved',
+      statusLabelKey: 'ai.chat.config.save.success'
+    });
+    expect(apiMessagePost).toHaveBeenCalledWith('/config/provider', {
+      code: 'zhipu',
+      apiKey: 'sk-zhipu',
+      baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
+      model: 'glm-4.6'
+    });
+  });
+
+  it('maps validation failures to the Angular validation copy', async () => {
+    const { apiMessagePost } = await import('@/lib/api-client');
+    vi.mocked(apiMessagePost).mockRejectedValueOnce(new Error('validation failed: invalid API key'));
+
+    await expect(saveAiChatProviderConfig(buildDefaultAiChatProviderConfig())).resolves.toEqual({
+      status: 'error',
+      statusLabelKey: 'ai.chat.config.validation.failed'
+    });
+  });
+});
diff --git a/web-next/lib/ai-chat/provider-config.ts b/web-next/lib/ai-chat/provider-config.ts
new file mode 100644
index 0000000000..b537b56c64
--- /dev/null
+++ b/web-next/lib/ai-chat/provider-config.ts
@@ -0,0 +1,111 @@
+import type { HzAiChatConfigStatus, HzAiChatProviderConfigValue, HzAiChatProviderOption } from '@hertzbeat/ui';
+import { apiMessageGet, apiMessagePost } from '@/lib/api-client';
+
+const AI_CHAT_CONFIG_TIMEOUT_MS = 3500;
+
+type RawModelProviderConfig = Partial<HzAiChatProviderConfigValue> & {
+  error?: string | null;
+  type?: string | null;
+};
+
+export type AiChatProviderConfigState = {
+  status: Extract<HzAiChatConfigStatus, 'ready' | 'error'>;
+  config: HzAiChatProviderConfigValue;
+  statusLabelKey: string;
+};
+
+export type AiChatProviderConfigSaveState = {
+  status: Extract<HzAiChatConfigStatus, 'saved' | 'error'>;
+  statusLabelKey: string;
+};
+
+export const AI_CHAT_PROVIDER_OPTIONS = [
+  { value: 'openai', label: 'OpenAI', defaultBaseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4' },
+  { value: 'zai', label: 'ZAI', defaultBaseUrl: 'https://api.z.ai/api/paas/v4', defaultModel: 'glm-4.6' },
+  { value: 'zhipu', label: 'ZhiPu', defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4', defaultModel: 'glm-4.6' }
+] as const satisfies HzAiChatProviderOption[];
+
+export const AI_CHAT_CONFIG_STATUS_LABEL_KEYS = {
+  idle: 'ai.chat.config.idle',
+  loading: 'ai.chat.config.loading',
+  ready: 'ai.chat.config.ready',
+  saving: 'ai.chat.config.saving',
+  saved: 'ai.chat.config.save.success',
+  error: 'ai.chat.config.save.failed',
+  validationFailed: 'ai.chat.config.validation.failed'
+} as const;
+
+function findProviderOption(code: string | null | undefined) {
+  return AI_CHAT_PROVIDER_OPTIONS.find(option => option.value === code) ?? AI_CHAT_PROVIDER_OPTIONS[0];
+}
+
+export function buildDefaultAiChatProviderConfig(code = 'openai'): HzAiChatProviderConfigValue {
+  const provider = findProviderOption(code);
+  return {
+    code: provider.value,
+    apiKey: '',
+    baseUrl: provider.defaultBaseUrl || '',
+    model: provider.defaultModel || ''
+  };
+}
+
+export function applyAiChatProviderDefaults(config: Partial<HzAiChatProviderConfigValue>): HzAiChatProviderConfigValue {
+  const provider = findProviderOption(config.code);
+  const defaults = buildDefaultAiChatProviderConfig(provider.value);
+  return {
+    code: provider.value,
+    apiKey: config.apiKey || '',
+    baseUrl: config.baseUrl || defaults.baseUrl,
+    model: config.model || defaults.model
+  };
+}
+
+async function promiseWithAiChatConfigTimeout<T>(promise: Promise<T>): Promise<T> {
+  let timeout: ReturnType<typeof globalThis.setTimeout> | undefined;
+  try {
+    return await Promise.race([
+      promise,
+      new Promise<T>((_, reject) => {
+        timeout = globalThis.setTimeout(() => reject(new Error('AI chat provider config request timed out')), AI_CHAT_CONFIG_TIMEOUT_MS);
+      })
+    ]);
+  } finally {
+    if (timeout) {
+      globalThis.clearTimeout(timeout);
+    }
+  }
+}
+
+export async function loadAiChatProviderConfig(): Promise<AiChatProviderConfigState> {
+  try {
+    return {
+      status: 'ready',
+      config: applyAiChatProviderDefaults(await promiseWithAiChatConfigTimeout(apiMessageGet<RawModelProviderConfig>('/config/provider'))),
+      statusLabelKey: AI_CHAT_CONFIG_STATUS_LABEL_KEYS.ready
+    };
+  } catch {
+    return {
+      status: 'error',
+      config: buildDefaultAiChatProviderConfig(),
+      statusLabelKey: AI_CHAT_CONFIG_STATUS_LABEL_KEYS.error
+    };
+  }
+}
+
+export async function saveAiChatProviderConfig(config: HzAiChatProviderConfigValue): Promise<AiChatProviderConfigSaveState> {
+  try {
+    await promiseWithAiChatConfigTimeout(apiMessagePost<unknown>('/config/provider', applyAiChatProviderDefaults(config)));
+    return {
+      status: 'saved',
+      statusLabelKey: AI_CHAT_CONFIG_STATUS_LABEL_KEYS.saved
+    };
+  } catch (error) {
+    const message = error instanceof Error ? error.message : '';
+    return {
+      status: 'error',
+      statusLabelKey: message.includes('validation failed')
+        ? AI_CHAT_CONFIG_STATUS_LABEL_KEYS.validationFailed
+        : AI_CHAT_CONFIG_STATUS_LABEL_KEYS.error
+    };
+  }
+}
diff --git a/web-next/lib/ai-chat/schedules.test.ts b/web-next/lib/ai-chat/schedules.test.ts
new file mode 100644
index 0000000000..4c89bafbed
--- /dev/null
+++ b/web-next/lib/ai-chat/schedules.test.ts
@@ -0,0 +1,155 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+  createAiChatSchedule,
+  deleteAiChatSchedule,
+  loadAiChatScheduleSkills,
+  loadAiChatSchedules,
+  mapAiChatScheduleSkills,
+  mapAiChatSchedules,
+  toggleAiChatSchedule,
+  updateAiChatSchedule
+} from './schedules';
+
+vi.mock('@/lib/api-client', () => ({
+  apiMessageGet: vi.fn(),
+  apiMessagePost: vi.fn(),
+  apiMessagePut: vi.fn(),
+  apiMessageDelete: vi.fn()
+}));
+
+describe('AI chat schedule controller', () => {
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('maps Angular SOP schedules into shared rows', () => {
+    expect(mapAiChatSchedules([
+      { id: 7, conversationId: 1, sopName: 'daily-health', cronExpression: '0 0 9 * * ?', enabled: true },
+      { id: 8, conversationId: 1, sopName: 'weekly-report', cronExpression: '0 0 9 ? * MON', enabled: false }
+    ])).toEqual({
+      status: 'ready',
+      schedules: [
+        { id: 7, sopName: 'daily-health', cronExpression: '0 0 9 * * ?', enabled: true },
+        { id: 8, sopName: 'weekly-report', cronExpression: '0 0 9 ? * MON', enabled: false }
+      ],
+      statusLabelKey: 'ai.chat.schedule.ready'
+    });
+  });
+
+  it('maps available skills into select options with descriptions', () => {
+    expect(mapAiChatScheduleSkills([
+      { name: 'daily-health', description: 'Daily health report' },
+      { name: 'weekly-report', description: '' },
+      { name: '   ', description: 'ignored' }
+    ])).toEqual({
+      status: 'ready',
+      skills: [
+        { value: 'daily-health', label: 'daily-health - Daily health report' },
+        { value: 'weekly-report', label: 'weekly-report' }
+      ],
+      statusLabelKey: 'ai.chat.schedule.ready'
+    });
+  });
+
+  it('loads schedules and skills from the Angular endpoints', async () => {
+    const { apiMessageGet } = await import('@/lib/api-client');
+    vi.mocked(apiMessageGet)
+      .mockResolvedValueOnce([{ id: 7, conversationId: 1, sopName: 'daily-health', cronExpression: '0 0 9 * * ?', enabled: true }])
+      .mockResolvedValueOnce([{ name: 'daily-health', description: 'Daily health report' }]);
+
+    await expect(loadAiChatSchedules(1)).resolves.toMatchObject({
+      status: 'ready',
+      schedules: [{ id: 7, sopName: 'daily-health', cronExpression: '0 0 9 * * ?', enabled: true }]
+    });
+    await expect(loadAiChatScheduleSkills()).resolves.toMatchObject({
+      status: 'ready',
+      skills: [{ value: 'daily-health', label: 'daily-health - Daily health report' }]
+    });
+    expect(apiMessageGet).toHaveBeenCalledWith('/ai/schedule/conversation/1');
+    expect(apiMessageGet).toHaveBeenCalledWith('/ai/schedule/skills');
+  });
+
+  it('creates a schedule through POST /ai/schedule', async () => {
+    const { apiMessagePost } = await import('@/lib/api-client');
+    vi.mocked(apiMessagePost).mockResolvedValueOnce({
+      id: 9,
+      conversationId: 1,
+      sopName: 'daily-health',
+      cronExpression: '0 0 9 * * ?',
+      enabled: true
+    });
+
+    await expect(createAiChatSchedule(1, { sopName: 'daily-health', cronExpression: '0 0 9 * * ?', enabled: true })).resolves.toEqual({
+      status: 'ready',
+      schedule: { id: 9, sopName: 'daily-health', cronExpression: '0 0 9 * * ?', enabled: true },
+      statusLabelKey: 'ai.chat.schedule.create.success'
+    });
+    expect(apiMessagePost).toHaveBeenCalledWith('/ai/schedule', {
+      conversationId: 1,
+      sopName: 'daily-health',
+      cronExpression: '0 0 9 * * ?',
+      enabled: true
+    });
+  });
+
+  it('updates, toggles, and deletes schedules through Angular endpoints', async () => {
+    const { apiMessagePut, apiMessageDelete } = await import('@/lib/api-client');
+    vi.mocked(apiMessagePut)
+      .mockResolvedValueOnce({ id: 9, conversationId: 1, sopName: 'daily-health', cronExpression: '0 15 9 * * ?', enabled: true })
+      .mockResolvedValueOnce({ id: 9, conversationId: 1, sopName: 'daily-health', cronExpression: '0 15 9 * * ?', enabled: false });
+    vi.mocked(apiMessageDelete).mockResolvedValueOnce(undefined);
+
+    await expect(updateAiChatSchedule(9, 1, { sopName: 'daily-health', cronExpression: '0 15 9 * * ?', enabled: true })).resolves.toMatchObject({
+      status: 'ready',
+      schedule: { id: 9, cronExpression: '0 15 9 * * ?', enabled: true },
+      statusLabelKey: 'ai.chat.schedule.update.success'
+    });
+    await expect(toggleAiChatSchedule(9, false)).resolves.toMatchObject({
+      status: 'ready',
+      schedule: { id: 9, enabled: false },
+      statusLabelKey: 'ai.chat.schedule.toggle.success'
+    });
+    await expect(deleteAiChatSchedule(9)).resolves.toEqual({
+      status: 'ready',
+      scheduleId: 9,
+      statusLabelKey: 'ai.chat.schedule.delete.success'
+    });
+
+    expect(apiMessagePut).toHaveBeenCalledWith('/ai/schedule/9', {
+      id: 9,
+      conversationId: 1,
+      sopName: 'daily-health',
+      cronExpression: '0 15 9 * * ?',
+      enabled: true
+    });
+    expect(apiMessagePut).toHaveBeenCalledWith('/ai/schedule/9/toggle?enabled=false', {});
+    expect(apiMessageDelete).toHaveBeenCalledWith('/ai/schedule/9');
+  });
+
+  it('returns localized failure keys instead of throwing', async () => {
+    const { apiMessageGet, apiMessagePost, apiMessagePut, apiMessageDelete } = await import('@/lib/api-client');
+    vi.mocked(apiMessageGet).mockRejectedValue(new Error('503'));
+    vi.mocked(apiMessagePost).mockRejectedValue(new Error('503'));
+    vi.mocked(apiMessagePut).mockRejectedValue(new Error('503'));
+    vi.mocked(apiMessageDelete).mockRejectedValue(new Error('503'));
+
+    await expect(loadAiChatSchedules(1)).resolves.toMatchObject({ status: 'error', statusLabelKey: 'ai.chat.schedule.error' });
+    await expect(loadAiChatScheduleSkills()).resolves.toMatchObject({ status: 'error', statusLabelKey: 'ai.chat.schedule.error' });
+    await expect(createAiChatSchedule(1, { sopName: 'daily-health', cronExpression: '0 0 9 * * ?' })).resolves.toMatchObject({
+      status: 'error',
+      statusLabelKey: 'ai.chat.schedule.create.failed'
+    });
+    await expect(updateAiChatSchedule(9, 1, { sopName: 'daily-health', cronExpression: '0 0 9 * * ?' })).resolves.toMatchObject({
+      status: 'error',
+      statusLabelKey: 'ai.chat.schedule.update.failed'
+    });
+    await expect(toggleAiChatSchedule(9, true)).resolves.toMatchObject({
+      status: 'error',
+      statusLabelKey: 'ai.chat.schedule.toggle.failed'
+    });
+    await expect(deleteAiChatSchedule(9)).resolves.toMatchObject({
+      status: 'error',
+      statusLabelKey: 'ai.chat.schedule.delete.failed'
+    });
+  });
+});
diff --git a/web-next/lib/ai-chat/schedules.ts b/web-next/lib/ai-chat/schedules.ts
new file mode 100644
index 0000000000..000ae725e3
--- /dev/null
+++ b/web-next/lib/ai-chat/schedules.ts
@@ -0,0 +1,235 @@
+import type { HzAiChatScheduleDraft, HzAiChatScheduleRow, HzAiChatScheduleSkillOption, HzAiChatScheduleStatus } from '@hertzbeat/ui';
+import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
+
+const AI_CHAT_SCHEDULE_TIMEOUT_MS = 3500;
+
+export type AiChatSopSchedule = {
+  id?: string | number;
+  conversationId: string | number;
+  sopName: string;
+  sopParams?: string;
+  cronExpression: string;
+  enabled: boolean;
+  lastRunTime?: string | number | Date | null;
+  nextRunTime?: string | number | Date | null;
+  gmtCreate?: string | number | Date | null;
+  gmtUpdate?: string | number | Date | null;
+};
+
+type RawSkillInfo = {
+  name?: string | null;
+  description?: string | null;
+};
+
+export type AiChatScheduleListState = {
+  status: Extract<HzAiChatScheduleStatus, 'ready' | 'empty' | 'error'>;
+  schedules: HzAiChatScheduleRow[];
+  statusLabelKey: string;
+};
+
+export type AiChatScheduleSkillState = {
+  status: Extract<HzAiChatScheduleStatus, 'ready' | 'empty' | 'error'>;
+  skills: HzAiChatScheduleSkillOption[];
+  statusLabelKey: string;
+};
+
+export type AiChatScheduleMutationState = {
+  status: Extract<HzAiChatScheduleStatus, 'ready' | 'error'>;
+  schedule?: HzAiChatScheduleRow;
+  scheduleId?: string | number;
+  statusLabelKey: string;
+};
+
+export const AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS = {
+  idle: 'ai.chat.schedule.idle',
+  loading: 'ai.chat.schedule.loading',
+  ready: 'ai.chat.schedule.ready',
+  empty: 'ai.chat.schedule.empty',
+  saving: 'ai.chat.schedule.saving',
+  error: 'ai.chat.schedule.error',
+  createSuccess: 'ai.chat.schedule.create.success',
+  createFailed: 'ai.chat.schedule.create.failed',
+  updateSuccess: 'ai.chat.schedule.update.success',
+  updateFailed: 'ai.chat.schedule.update.failed',
+  deleteSuccess: 'ai.chat.schedule.delete.success',
+  deleteFailed: 'ai.chat.schedule.delete.failed',
+  toggleSuccess: 'ai.chat.schedule.toggle.success',
+  toggleFailed: 'ai.chat.schedule.toggle.failed'
+} as const;
+
+async function promiseWithAiChatScheduleTimeout<T>(promise: Promise<T>): Promise<T> {
+  let timeout: ReturnType<typeof globalThis.setTimeout> | undefined;
+  try {
+    return await Promise.race([
+      promise,
+      new Promise<T>((_, reject) => {
+        timeout = globalThis.setTimeout(() => reject(new Error('AI chat schedule request timed out')), AI_CHAT_SCHEDULE_TIMEOUT_MS);
+      })
+    ]);
+  } finally {
+    if (timeout) {
+      globalThis.clearTimeout(timeout);
+    }
+  }
+}
+
+export function mapAiChatScheduleRow(schedule: AiChatSopSchedule, index = 0): HzAiChatScheduleRow {
+  return {
+    id: schedule.id ?? `schedule-${index}`,
+    sopName: schedule.sopName || `Schedule ${index + 1}`,
+    cronExpression: schedule.cronExpression || '',
+    enabled: Boolean(schedule.enabled)
+  };
+}
+
+export function mapAiChatSchedules(raw: AiChatSopSchedule[] | null | undefined): AiChatScheduleListState {
+  const schedules = (raw || []).map((schedule, index) => mapAiChatScheduleRow(schedule, index));
+  return {
+    status: schedules.length > 0 ? 'ready' : 'empty',
+    schedules,
+    statusLabelKey: schedules.length > 0 ? AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.ready : AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.empty
+  };
+}
+
+export function mapAiChatScheduleSkills(raw: RawSkillInfo[] | null | undefined): AiChatScheduleSkillState {
+  const skills = (raw || [])
+    .map(skill => {
+      const name = skill.name?.trim();
+      if (!name) return null;
+      const description = skill.description?.trim();
+      return {
+        value: name,
+        label: description ? `${name} - ${description}` : name
+      };
+    })
+    .filter((skill): skill is HzAiChatScheduleSkillOption => skill !== null);
+
+  return {
+    status: skills.length > 0 ? 'ready' : 'empty',
+    skills,
+    statusLabelKey: skills.length > 0 ? AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.ready : AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.empty
+  };
+}
+
+function buildSchedulePayload(conversationId: string | number, draft: HzAiChatScheduleDraft): AiChatSopSchedule {
+  return {
+    conversationId,
+    sopName: draft.sopName,
+    cronExpression: draft.cronExpression,
+    enabled: draft.enabled ?? true
+  };
+}
+
+export async function loadAiChatSchedules(conversationId: string | number): Promise<AiChatScheduleListState> {
+  try {
+    const encodedConversationId = encodeURIComponent(String(conversationId));
+    return mapAiChatSchedules(
+      await promiseWithAiChatScheduleTimeout(apiMessageGet<AiChatSopSchedule[]>(`/ai/schedule/conversation/${encodedConversationId}`))
+    );
+  } catch {
+    return {
+      status: 'error',
+      schedules: [],
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.error
+    };
+  }
+}
+
+export async function loadAiChatScheduleSkills(): Promise<AiChatScheduleSkillState> {
+  try {
+    return mapAiChatScheduleSkills(await promiseWithAiChatScheduleTimeout(apiMessageGet<RawSkillInfo[]>('/ai/schedule/skills')));
+  } catch {
+    return {
+      status: 'error',
+      skills: [],
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.error
+    };
+  }
+}
+
+export async function createAiChatSchedule(
+  conversationId: string | number,
+  draft: HzAiChatScheduleDraft
+): Promise<AiChatScheduleMutationState> {
+  try {
+    return {
+      status: 'ready',
+      schedule: mapAiChatScheduleRow(
+        await promiseWithAiChatScheduleTimeout(apiMessagePost<AiChatSopSchedule>('/ai/schedule', buildSchedulePayload(conversationId, draft)))
+      ),
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.createSuccess
+    };
+  } catch {
+    return {
+      status: 'error',
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.createFailed
+    };
+  }
+}
+
+export async function updateAiChatSchedule(
+  scheduleId: string | number,
+  conversationId: string | number,
+  draft: HzAiChatScheduleDraft
+): Promise<AiChatScheduleMutationState> {
+  try {
+    const encodedScheduleId = encodeURIComponent(String(scheduleId));
+    return {
+      status: 'ready',
+      schedule: mapAiChatScheduleRow(
+        await promiseWithAiChatScheduleTimeout(
+          apiMessagePut<AiChatSopSchedule>(`/ai/schedule/${encodedScheduleId}`, {
+            ...buildSchedulePayload(conversationId, draft),
+            id: scheduleId
+          })
+        )
+      ),
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.updateSuccess
+    };
+  } catch {
+    return {
+      status: 'error',
+      scheduleId,
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.updateFailed
+    };
+  }
+}
+
+export async function toggleAiChatSchedule(scheduleId: string | number, enabled: boolean): Promise<AiChatScheduleMutationState> {
+  try {
+    const encodedScheduleId = encodeURIComponent(String(scheduleId));
+    return {
+      status: 'ready',
+      schedule: mapAiChatScheduleRow(
+        await promiseWithAiChatScheduleTimeout(
+          apiMessagePut<AiChatSopSchedule>(`/ai/schedule/${encodedScheduleId}/toggle?enabled=${enabled}`, {})
+        )
+      ),
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.toggleSuccess
+    };
+  } catch {
+    return {
+      status: 'error',
+      scheduleId,
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.toggleFailed
+    };
+  }
+}
+
+export async function deleteAiChatSchedule(scheduleId: string | number): Promise<AiChatScheduleMutationState> {
+  try {
+    const encodedScheduleId = encodeURIComponent(String(scheduleId));
+    await promiseWithAiChatScheduleTimeout(apiMessageDelete<void>(`/ai/schedule/${encodedScheduleId}`));
+    return {
+      status: 'ready',
+      scheduleId,
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.deleteSuccess
+    };
+  } catch {
+    return {
+      status: 'error',
+      scheduleId,
+      statusLabelKey: AI_CHAT_SCHEDULE_STATUS_LABEL_KEYS.deleteFailed
+    };
+  }
+}
diff --git a/web-next/lib/app-frame-state.test.ts b/web-next/lib/app-frame-state.test.ts
index cd8ef55b16..7ad9e6cf83 100644
--- a/web-next/lib/app-frame-state.test.ts
+++ b/web-next/lib/app-frame-state.test.ts
@@ -28,4 +28,11 @@ describe('app frame state', () => {
     expect(shouldLoadHeaderState('/exception/404')).toBe(false);
     expect(shouldLoadHeaderState('/overview')).toBe(true);
   });
+
+  it('does not let app-frame setup polling compete with monitor CRUD interactions', () => {
+    expect(shouldLoadHeaderState('/monitors')).toBe(false);
+    expect(shouldLoadHeaderState('/monitors/new')).toBe(false);
+    expect(shouldLoadHeaderState('/monitors/42')).toBe(false);
+    expect(shouldLoadHeaderState('/monitors/42/edit')).toBe(false);
+  });
 });
diff --git a/web-next/lib/app-frame-state.ts b/web-next/lib/app-frame-state.ts
index 9fb74278cd..4b56d1f1fa 100644
--- a/web-next/lib/app-frame-state.ts
+++ b/web-next/lib/app-frame-state.ts
@@ -10,5 +10,7 @@ export function isStandaloneRoute(pathname: string) {
 }
 
 export function shouldLoadHeaderState(pathname: string) {
-  return !isStandaloneRoute(pathname);
+  const pathOnly = pathname.split('?')[0] || pathname;
+  const isMonitorCrudRoute = pathOnly === '/monitors' || pathOnly.startsWith('/monitors/');
+  return !isStandaloneRoute(pathOnly) && !isMonitorCrudRoute;
 }
diff --git a/web-next/lib/shell/about.test.ts b/web-next/lib/shell/about.test.ts
new file mode 100644
index 0000000000..b46c19c1e0
--- /dev/null
+++ b/web-next/lib/shell/about.test.ts
@@ -0,0 +1,69 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import {
+  ABOUT_AUTO_SHOW_AFTER_LOGIN_KEY,
+  ABOUT_NOT_SHOW_NEXT_LOGIN_KEY,
+  consumeAboutAutoShowAfterLogin,
+  markAboutAutoShowAfterLogin,
+  readAboutNotShowNextLogin,
+  writeAboutNotShowNextLogin
+} from './about';
+
+function installLocalStorageMock() {
+  const storage = new Map<string, string>();
+  const sessionStorage = {
+    getItem: (key: string) => storage.get(`session:${key}`) ?? null,
+    setItem: (key: string, value: string) => storage.set(`session:${key}`, value),
+    removeItem: (key: string) => storage.delete(`session:${key}`),
+    clear: () => {
+      for (const key of Array.from(storage.keys())) {
+        if (key.startsWith('session:')) storage.delete(key);
+      }
+    }
+  };
+  vi.stubGlobal('window', {
+    localStorage: {
+      getItem: (key: string) => storage.get(key) ?? null,
+      setItem: (key: string, value: string) => storage.set(key, value),
+      removeItem: (key: string) => storage.delete(key),
+      clear: () => storage.clear()
+    },
+    sessionStorage
+  } as any);
+  vi.stubGlobal('sessionStorage', sessionStorage);
+}
+
+describe('header about modal persistence', () => {
+  beforeEach(() => {
+    installLocalStorageMock();
+  });
+
+  afterEach(() => {
+    window.localStorage?.clear();
+    vi.unstubAllGlobals();
+  });
+
+  it('keeps the Angular not-show-next-login storage key and JSON value', () => {
+    expect(readAboutNotShowNextLogin()).toBe(false);
+    writeAboutNotShowNextLogin(true);
+    expect(window.localStorage.getItem(ABOUT_NOT_SHOW_NEXT_LOGIN_KEY)).toBe('true');
+    expect(readAboutNotShowNextLogin()).toBe(true);
+  });
+
+  it('marks and consumes the one-shot post-login auto-show signal', () => {
+    expect(consumeAboutAutoShowAfterLogin()).toBe(false);
+    markAboutAutoShowAfterLogin();
+    expect(window.sessionStorage.getItem(ABOUT_AUTO_SHOW_AFTER_LOGIN_KEY)).toBe('true');
+    expect(consumeAboutAutoShowAfterLogin()).toBe(true);
+    expect(window.sessionStorage.getItem(ABOUT_AUTO_SHOW_AFTER_LOGIN_KEY)).toBeNull();
+    expect(consumeAboutAutoShowAfterLogin()).toBe(false);
+  });
+
+  it('treats missing browser storage as a safe no-op', () => {
+    vi.stubGlobal('window', {} as any);
+
+    expect(readAboutNotShowNextLogin()).toBe(false);
+    expect(consumeAboutAutoShowAfterLogin()).toBe(false);
+    expect(() => writeAboutNotShowNextLogin(true)).not.toThrow();
+    expect(() => markAboutAutoShowAfterLogin()).not.toThrow();
+  });
+});
diff --git a/web-next/lib/shell/about.ts b/web-next/lib/shell/about.ts
new file mode 100644
index 0000000000..7f169c7fae
--- /dev/null
+++ b/web-next/lib/shell/about.ts
@@ -0,0 +1,44 @@
+export const ABOUT_NOT_SHOW_NEXT_LOGIN_KEY = 'NOT_SHOW_ABOUT_NEXT_LOGIN';
+export const ABOUT_AUTO_SHOW_AFTER_LOGIN_KEY = 'HB_ABOUT_AUTO_SHOW_AFTER_LOGIN';
+
+function getBrowserLocalStorage(): Storage | null {
+  if (typeof window === 'undefined' || !window.localStorage) return null;
+  return window.localStorage;
+}
+
+function getBrowserSessionStorage(): Storage | null {
+  if (typeof window === 'undefined' || !window.sessionStorage) return null;
+  return window.sessionStorage;
+}
+
+export function readAboutNotShowNextLogin(): boolean {
+  const localStorage = getBrowserLocalStorage();
+  if (!localStorage) return false;
+  const value = localStorage.getItem(ABOUT_NOT_SHOW_NEXT_LOGIN_KEY);
+  if (value === null) return false;
+  try {
+    return JSON.parse(value) === true;
+  } catch {
+    return value === 'true';
+  }
+}
+
+export function writeAboutNotShowNextLogin(value: boolean): void {
+  const localStorage = getBrowserLocalStorage();
+  if (!localStorage) return;
+  localStorage.setItem(ABOUT_NOT_SHOW_NEXT_LOGIN_KEY, JSON.stringify(value));
+}
+
+export function markAboutAutoShowAfterLogin(): void {
+  const sessionStorage = getBrowserSessionStorage();
+  if (!sessionStorage) return;
+  sessionStorage.setItem(ABOUT_AUTO_SHOW_AFTER_LOGIN_KEY, 'true');
+}
+
+export function consumeAboutAutoShowAfterLogin(): boolean {
+  const sessionStorage = getBrowserSessionStorage();
+  if (!sessionStorage) return false;
+  const shouldShow = sessionStorage.getItem(ABOUT_AUTO_SHOW_AFTER_LOGIN_KEY) === 'true';
+  sessionStorage.removeItem(ABOUT_AUTO_SHOW_AFTER_LOGIN_KEY);
+  return shouldShow;
+}
diff --git a/web-next/lib/shell/header-mute.test.ts b/web-next/lib/shell/header-mute.test.ts
new file mode 100644
index 0000000000..5a7bb80b4a
--- /dev/null
+++ b/web-next/lib/shell/header-mute.test.ts
@@ -0,0 +1,86 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import {
+  HEADER_MUTE_STORAGE_KEY,
+  loadHeaderMuteConfig,
+  normalizeHeaderMuteConfig,
+  readHeaderMuteStorage,
+  saveHeaderMuteConfig,
+  writeHeaderMuteStorage
+} from './header-mute';
+
+vi.mock('@/lib/api-client', () => ({
+  apiMessageGet: vi.fn(),
+  apiMessagePost: vi.fn()
+}));
+
+function installLocalStorageMock() {
+  const storage = new Map<string, string>();
+  vi.stubGlobal('window', {
+    localStorage: {
+      getItem: (key: string) => storage.get(key) ?? null,
+      setItem: (key: string, value: string) => storage.set(key, value),
+      clear: () => storage.clear()
+    }
+  });
+}
+
+describe('header mute controller', () => {
+  beforeEach(() => {
+    installLocalStorageMock();
+  });
+
+  afterEach(() => {
+    vi.clearAllMocks();
+    window.localStorage.clear();
+    vi.unstubAllGlobals();
+  });
+
+  it('normalizes the Angular /config/mute payload with mute defaulting to true', () => {
+    expect(normalizeHeaderMuteConfig({ mute: false })).toEqual({ mute: false });
+    expect(normalizeHeaderMuteConfig({})).toEqual({ mute: true });
+    expect(normalizeHeaderMuteConfig(true)).toEqual({ mute: true });
+  });
+
+  it('reads and writes the local fallback cache using the existing header key', () => {
+    expect(readHeaderMuteStorage()).toBe(true);
+    writeHeaderMuteStorage(false);
+    expect(window.localStorage.getItem(HEADER_MUTE_STORAGE_KEY)).toBe('false');
+    expect(readHeaderMuteStorage()).toBe(false);
+  });
+
+  it('loads /config/mute and mirrors the result into local storage', async () => {
+    const { apiMessageGet } = await import('@/lib/api-client');
+    vi.mocked(apiMessageGet).mockResolvedValueOnce({ mute: false });
+
+    await expect(loadHeaderMuteConfig()).resolves.toEqual({ muted: false, source: 'api' });
+    expect(apiMessageGet).toHaveBeenCalledWith('/config/mute');
+    expect(window.localStorage.getItem(HEADER_MUTE_STORAGE_KEY)).toBe('false');
+  });
+
+  it('falls back to local storage when /config/mute cannot be loaded', async () => {
+    const { apiMessageGet } = await import('@/lib/api-client');
+    writeHeaderMuteStorage(false);
+    vi.mocked(apiMessageGet).mockRejectedValueOnce(new Error('503'));
+
+    await expect(loadHeaderMuteConfig()).resolves.toEqual({ muted: false, source: 'storage' });
+  });
+
+  it('saves /config/mute before updating the local fallback cache', async () => {
+    const { apiMessagePost } = await import('@/lib/api-client');
+    vi.mocked(apiMessagePost).mockResolvedValueOnce({});
+
+    await expect(saveHeaderMuteConfig(true)).resolves.toEqual({ muted: true, status: 'saved' });
+    expect(apiMessagePost).toHaveBeenCalledWith('/config/mute', { mute: true });
+    expect(window.localStorage.getItem(HEADER_MUTE_STORAGE_KEY)).toBe('true');
+  });
+
+  it('keeps the Angular success-only update contract when /config/mute save fails', async () => {
+    const { apiMessagePost } = await import('@/lib/api-client');
+    writeHeaderMuteStorage(true);
+    vi.mocked(apiMessagePost).mockRejectedValueOnce(new Error('500'));
+
+    await expect(saveHeaderMuteConfig(false)).resolves.toEqual({ muted: false, status: 'error' });
+    expect(apiMessagePost).toHaveBeenCalledWith('/config/mute', { mute: false });
+    expect(window.localStorage.getItem(HEADER_MUTE_STORAGE_KEY)).toBe('true');
+  });
+});
diff --git a/web-next/lib/shell/header-mute.ts b/web-next/lib/shell/header-mute.ts
new file mode 100644
index 0000000000..3147133c18
--- /dev/null
+++ b/web-next/lib/shell/header-mute.ts
@@ -0,0 +1,85 @@
+import { apiMessageGet, apiMessagePost } from '@/lib/api-client';
+
+export const HEADER_MUTE_STORAGE_KEY = 'hb.header-mute';
+const HEADER_MUTE_TIMEOUT_MS = 3500;
+
+export type HeaderMuteConfig = {
+  mute: boolean;
+};
+
+export type HeaderMuteLoadState = {
+  muted: boolean;
+  source: 'api' | 'storage';
+};
+
+export type HeaderMuteSaveState = {
+  muted: boolean;
+  status: 'saved' | 'error';
+};
+
+export function normalizeHeaderMuteConfig(config: Partial<HeaderMuteConfig> | boolean | null | undefined): HeaderMuteConfig {
+  if (typeof config === 'boolean') {
+    return { mute: config };
+  }
+  return { mute: config?.mute !== false };
+}
+
+export function readHeaderMuteStorage(): boolean {
+  if (typeof window === 'undefined') return true;
+  return window.localStorage.getItem(HEADER_MUTE_STORAGE_KEY) !== 'false';
+}
+
+export function writeHeaderMuteStorage(muted: boolean): void {
+  if (typeof window === 'undefined') return;
+  window.localStorage.setItem(HEADER_MUTE_STORAGE_KEY, String(muted));
+}
+
+async function withHeaderMuteTimeout<T>(promise: Promise<T>): Promise<T> {
+  let timeout: ReturnType<typeof globalThis.setTimeout> | undefined;
+  try {
+    return await Promise.race([
+      promise,
+      new Promise<T>((_, reject) => {
+        timeout = globalThis.setTimeout(() => reject(new Error('Header mute request timed out')), HEADER_MUTE_TIMEOUT_MS);
+      })
+    ]);
+  } finally {
+    if (timeout) {
+      globalThis.clearTimeout(timeout);
+    }
+  }
+}
+
+export async function loadHeaderMuteConfig(): Promise<HeaderMuteLoadState> {
+  try {
+    const config = normalizeHeaderMuteConfig(
+      await withHeaderMuteTimeout(apiMessageGet<Partial<HeaderMuteConfig> | boolean>('/config/mute'))
+    );
+    writeHeaderMuteStorage(config.mute);
+    return {
+      muted: config.mute,
+      source: 'api'
+    };
+  } catch {
+    return {
+      muted: readHeaderMuteStorage(),
+      source: 'storage'
+    };
+  }
+}
+
+export async function saveHeaderMuteConfig(muted: boolean): Promise<HeaderMuteSaveState> {
+  try {
+    await withHeaderMuteTimeout(apiMessagePost<unknown>('/config/mute', { mute: muted }));
+    writeHeaderMuteStorage(muted);
+    return {
+      muted,
+      status: 'saved'
+    };
+  } catch {
+    return {
+      muted,
+      status: 'error'
+    };
+  }
+}
diff --git a/web-next/lib/shell/header-realtime.test.ts b/web-next/lib/shell/header-realtime.test.ts
new file mode 100644
index 0000000000..4e6f6bbaf8
--- /dev/null
+++ b/web-next/lib/shell/header-realtime.test.ts
@@ -0,0 +1,74 @@
+import { describe, expect, it } from 'vitest';
+import {
+  HEADER_ALERT_EVENT_TYPE,
+  HEADER_ALERT_SOUND_CN,
+  HEADER_ALERT_SOUND_EN,
+  HEADER_ALERT_SSE_URL,
+  HEADER_DUAL_SSE_CONTRACT,
+  HEADER_IMPORT_TASK_EVENT_TYPE,
+  HEADER_MANAGER_SSE_URL,
+  buildHeaderNoticeFromAlert,
+  buildManagerImportMessage,
+  mergeHeaderNoticeEvent,
+  parseHeaderSseJson,
+  resolveHeaderAlertSoundSrc
+} from './header-realtime';
+
+describe('header realtime notification contract', () => {
+  const t = (key: string, params?: Record<string, string | number | null | undefined>) => {
+    const suffix = params ? `:${JSON.stringify(params)}` : '';
+    return `${key}${suffix}`;
+  };
+
+  it('keeps the Angular SSE endpoints and event names', () => {
+    expect(HEADER_ALERT_SSE_URL).toBe('/api/alert/sse/subscribe');
+    expect(HEADER_MANAGER_SSE_URL).toBe('/api/manager/sse/subscribe');
+    expect(HEADER_ALERT_EVENT_TYPE).toBe('ALERT_EVENT');
+    expect(HEADER_IMPORT_TASK_EVENT_TYPE).toBe('IMPORT_TASK_EVENT');
+    expect(HEADER_DUAL_SSE_CONTRACT).toBe('angular-alert-and-manager-sse');
+  });
+
+  it('maps alert SSE payloads into dropdown notices and dedupes by alert id', () => {
+    const notice = buildHeaderNoticeFromAlert({
+      id: 42,
+      fingerprint: 'fp-42',
+      content: 'CPU load high',
+      status: 'firing',
+      activeAt: 1779557040000
+    }, 'Fallback alert');
+
+    expect(notice).toEqual({
+      id: 42,
+      title: 'CPU load high',
+      status: 'firing',
+      activeAt: 1779557040000
+    });
+    expect(mergeHeaderNoticeEvent([
+      { id: 1, title: 'old', status: 'firing', activeAt: 1 },
+      { id: 42, title: 'stale', status: 'firing', activeAt: 2 }
+    ], notice)).toEqual([
+      notice,
+      { id: 1, title: 'old', status: 'firing', activeAt: 1 }
+    ]);
+  });
+
+  it('keeps the localized alert sound assets from the Angular service', () => {
+    expect(resolveHeaderAlertSoundSrc('zh-CN')).toBe(HEADER_ALERT_SOUND_CN);
+    expect(resolveHeaderAlertSoundSrc('zh-TW')).toBe(HEADER_ALERT_SOUND_CN);
+    expect(resolveHeaderAlertSoundSrc('en-US')).toBe(HEADER_ALERT_SOUND_EN);
+  });
+
+  it('parses SSE json safely and maps manager import notifications', () => {
+    expect(parseHeaderSseJson('{bad')).toBeNull();
+    expect(parseHeaderSseJson<{ ok: boolean }>('{ "ok": true }')).toEqual({ ok: true });
+    expect(buildManagerImportMessage({ notifyLevel: 'SUCCESS', taskName: 'mysql' }, t)).toMatchObject({
+      status: 'live',
+      meta: 'IMPORT_TASK_EVENT'
+    });
+    expect(buildManagerImportMessage({ notifyLevel: 'ERROR', taskName: 'mysql', errMsg: 'invalid yaml' }, t)).toMatchObject({
+      status: 'error',
+      meta: 'IMPORT_TASK_EVENT'
+    });
+    expect(buildManagerImportMessage({ notifyLevel: 'UNKNOWN' }, t)).toBeNull();
+  });
+});
diff --git a/web-next/lib/shell/header-realtime.ts b/web-next/lib/shell/header-realtime.ts
new file mode 100644
index 0000000000..31a6968899
--- /dev/null
+++ b/web-next/lib/shell/header-realtime.ts
@@ -0,0 +1,85 @@
+import type { SingleAlert } from '@/lib/types';
+
+export const HEADER_ALERT_SSE_URL = '/api/alert/sse/subscribe';
+export const HEADER_MANAGER_SSE_URL = '/api/manager/sse/subscribe';
+export const HEADER_ALERT_EVENT_TYPE = 'ALERT_EVENT';
+export const HEADER_IMPORT_TASK_EVENT_TYPE = 'IMPORT_TASK_EVENT';
+export const HEADER_DUAL_SSE_CONTRACT = 'angular-alert-and-manager-sse';
+export const HEADER_ALERT_SOUND_CN = '/assets/audio/default-alert-CN.mp3';
+export const HEADER_ALERT_SOUND_EN = '/assets/audio/default-alert-EN.mp3';
+
+export type HeaderRealtimeStatus = 'connecting' | 'live' | 'idle' | 'error' | 'unsupported';
+
+export type HeaderNoticeEvent = {
+  id: number;
+  title: string;
+  status: string;
+  activeAt?: number | null;
+};
+
+export type HeaderManagerImportEvent = {
+  notifyLevel?: 'SUCCESS' | 'ERROR' | 'INFO' | string;
+  taskName?: string;
+  errMsg?: string;
+  progress?: number | string;
+};
+
+export function buildHeaderNoticeFromAlert(alert: SingleAlert, fallbackTitle: string): HeaderNoticeEvent {
+  return {
+    id: alert.id,
+    title: alert.content || fallbackTitle,
+    status: alert.status || 'firing',
+    activeAt: alert.activeAt ?? alert.gmtUpdate ?? null
+  };
+}
+
+export function mergeHeaderNoticeEvent(current: HeaderNoticeEvent[], event: HeaderNoticeEvent, limit = 5): HeaderNoticeEvent[] {
+  return [
+    event,
+    ...current.filter(item => item.id !== event.id)
+  ].slice(0, limit);
+}
+
+export function resolveHeaderAlertSoundSrc(locale: string): string {
+  return locale === 'zh-CN' || locale === 'zh-TW' ? HEADER_ALERT_SOUND_CN : HEADER_ALERT_SOUND_EN;
+}
+
+export function parseHeaderSseJson<T>(data: string): T | null {
+  try {
+    return JSON.parse(data) as T;
+  } catch {
+    return null;
+  }
+}
+
+export function buildManagerImportMessage(
+  event: HeaderManagerImportEvent,
+  t: (key: string, params?: Record<string, string | number | null | undefined>) => string
+) {
+  const taskName = event.taskName || t('common.unknown');
+  if (event.notifyLevel === 'SUCCESS') {
+    return {
+      status: 'live' as const,
+      title: t('common.notice'),
+      description: t('common.notify.import-success-detail', { taskName }),
+      meta: 'IMPORT_TASK_EVENT'
+    };
+  }
+  if (event.notifyLevel === 'ERROR') {
+    return {
+      status: 'error' as const,
+      title: t('common.notice'),
+      description: t('common.notify.import-fail-detail', { taskName, errMsg: event.errMsg || t('common.failed') }),
+      meta: 'IMPORT_TASK_EVENT'
+    };
+  }
+  if (event.notifyLevel === 'INFO') {
+    return {
+      status: 'live' as const,
+      title: t('common.notice'),
+      description: t('common.notify.import-progress', { taskName, progress: event.progress ?? 0 }),
+      meta: 'IMPORT_TASK_EVENT'
+    };
+  }
+  return null;
+}
diff --git a/web-next/lib/shell/locale-reload.test.ts b/web-next/lib/shell/locale-reload.test.ts
new file mode 100644
index 0000000000..32b9b989b6
--- /dev/null
+++ b/web-next/lib/shell/locale-reload.test.ts
@@ -0,0 +1,21 @@
+// @vitest-environment jsdom
+
+import { afterEach, describe, expect, it } from 'vitest';
+import { HEADER_LOCALE_RELOAD_SPINNER_CLASS, HEADER_LOCALE_RELOAD_SPINNER_MARKER, showHeaderLocaleReloadSpinner } from './locale-reload';
+
+describe('header locale reload spinner', () => {
+  afterEach(() => {
+    document.body.innerHTML = '';
+  });
+
+  it('matches the Angular header-i18n page-loading spinner contract and dedupes repeats', () => {
+    showHeaderLocaleReloadSpinner();
+    showHeaderLocaleReloadSpinner();
+
+    const spinners = document.querySelectorAll(`[data-app-frame-locale-reload="${HEADER_LOCALE_RELOAD_SPINNER_MARKER}"]`);
+
+    expect(spinners).toHaveLength(1);
+    expect(spinners[0]?.getAttribute('class')).toBe(HEADER_LOCALE_RELOAD_SPINNER_CLASS);
+    expect(spinners[0]?.innerHTML).toContain('ant-spin-dot ant-spin-dot-spin');
+  });
+});
diff --git a/web-next/lib/shell/locale-reload.ts b/web-next/lib/shell/locale-reload.ts
new file mode 100644
index 0000000000..f8959f9cd2
--- /dev/null
+++ b/web-next/lib/shell/locale-reload.ts
@@ -0,0 +1,14 @@
+export const HEADER_LOCALE_RELOAD_SPINNER_CLASS = 'page-loading ant-spin ant-spin-lg ant-spin-spinning';
+export const HEADER_LOCALE_RELOAD_SPINNER_MARKER = 'angular-header-i18n-reload';
+
+export function showHeaderLocaleReloadSpinner(doc: Document = document) {
+  if (doc.querySelector(`[data-app-frame-locale-reload="${HEADER_LOCALE_RELOAD_SPINNER_MARKER}"]`)) {
+    return;
+  }
+
+  const spinEl = doc.createElement('div');
+  spinEl.setAttribute('class', HEADER_LOCALE_RELOAD_SPINNER_CLASS);
+  spinEl.setAttribute('data-app-frame-locale-reload', HEADER_LOCALE_RELOAD_SPINNER_MARKER);
+  spinEl.innerHTML = '<span class="ant-spin-dot ant-spin-dot-spin"><i></i><i></i><i></i><i></i></span>';
+  doc.body.appendChild(spinEl);
+}
diff --git a/web-next/lib/shell/sidebar.test.ts b/web-next/lib/shell/sidebar.test.ts
index c12f9135ab..ffdccd10f2 100644
--- a/web-next/lib/shell/sidebar.test.ts
+++ b/web-next/lib/shell/sidebar.test.ts
@@ -1,13 +1,34 @@
-import { Compass, LayoutDashboard, Settings2 } from 'lucide-react';
-import { describe, expect, it, vi } from 'vitest';
+import {
+  BellOff,
+  Compass,
+  FileCode2,
+  GitMerge,
+  LayoutDashboard,
+  Plug,
+  RadioReceiver,
+  Send,
+  Settings2,
+  Webhook
+} from 'lucide-react';
+import { describe, expect, it } from 'vitest';
 import { buildShellSidebarSections, resolveShellNavIcon } from './sidebar';
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+import { createTranslatorMock } from '../../test/i18n-test-helper';
 
-const t = vi.fn((key: string) => `translated:${key}`);
+const t = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('shell sidebar helpers', () => {
   it('maps configured icons and falls back safely', () => {
     expect(resolveShellNavIcon('overview')).toBe(LayoutDashboard);
     expect(resolveShellNavIcon('settings')).toBe(Settings2);
+    expect(resolveShellNavIcon('collector')).toBe(RadioReceiver);
+    expect(resolveShellNavIcon('monitor-template')).toBe(FileCode2);
+    expect(resolveShellNavIcon('alert-integration')).toBe(Webhook);
+    expect(resolveShellNavIcon('alert-group')).toBe(GitMerge);
+    expect(resolveShellNavIcon('alert-silence')).toBe(BellOff);
+    expect(resolveShellNavIcon('alert-notice')).toBe(Send);
+    expect(resolveShellNavIcon('plugins')).toBe(Plug);
     expect(resolveShellNavIcon('missing')).toBe(Compass);
     expect(resolveShellNavIcon()).toBe(Compass);
   });
@@ -26,21 +47,46 @@ describe('shell sidebar helpers', () => {
       'dashboards',
       'settings'
     ]);
-    expect(ingestion?.title).toBe('translated:menu.section.ingestion');
+    expect(ingestion?.title).toBe('接入采集');
     expect(ingestion?.items.map(item => item.href)).toEqual([
       '/ingestion/otlp',
       '/monitors',
       '/setting/collector',
       '/setting/define'
     ]);
-    expect(alerting?.title).toBe('translated:menu.section.alerting');
+    expect(ingestion?.items.map(item => item.iconKey)).toEqual([
+      'otlp',
+      'monitor',
+      'collector',
+      'monitor-template'
+    ]);
+    expect(alerting?.title).toBe('告警处置');
     expect(alerting?.items.some(item => item.href === '/alert' && item.active)).toBe(true);
     expect(alerting?.items.some(item => item.href === '/alert/setting' && item.active)).toBe(true);
-    expect(alerting?.items.find(item => item.href === '/alert/setting')?.title).toBe('translated:menu.alert.setting');
+    expect(alerting?.items.find(item => item.href === '/alert/setting')?.title).toBe('阈值规则');
+    expect(alerting?.items.map(item => item.iconKey)).toEqual([
+      'alert',
+      'alert-setting',
+      'alert-integration',
+      'alert-group',
+      'alert-inhibit',
+      'alert-silence',
+      'alert-notice',
+      'alert-bulletin'
+    ]);
     expect(alerting?.items.some(item => item.href === '/incidents')).toBe(false);
     expect(alerting?.items.some(item => item.href === '/actions')).toBe(false);
-    expect(settings?.items.find(item => item.key === 'settings-mcp-server')?.title).toBe('translated:menu.advanced.mcp-server');
-    expect(settings?.items.find(item => item.key === 'help-center')?.title).toBe('translated:menu.extras.help');
+    expect(settings?.items.find(item => item.key === 'settings-mcp-server')?.title).toBe('MCP服务器');
+    expect(settings?.items.find(item => item.key === 'help-center')?.title).toBe('帮助中心');
     expect(settings?.items.some(item => item.href === '/status')).toBe(false);
   });
+
+  it('does not fall back to catalog English labels for visible sidebar text', () => {
+    const source = readFileSync(resolve(process.cwd(), 'lib/shell/sidebar.ts'), 'utf8');
+
+    expect(source).toContain('title: t(section.titleKey)');
+    expect(source).toContain('title: t(item.labelKey)');
+    expect(source).not.toContain(': section.title');
+    expect(source).not.toContain(': item.label');
+  });
 });
diff --git a/web-next/lib/shell/sidebar.ts b/web-next/lib/shell/sidebar.ts
index b768bfc096..c2c0a2b7d1 100644
--- a/web-next/lib/shell/sidebar.ts
+++ b/web-next/lib/shell/sidebar.ts
@@ -1,20 +1,34 @@
 import {
   Activity,
+  BellDot,
+  BellOff,
   BellRing,
   Boxes,
   ChevronRight,
   CircleGauge,
+  CircleHelp,
   Compass,
+  FileCode2,
+  FileCog,
+  GitMerge,
   LayoutDashboard,
+  ListTree,
   Logs,
   Network,
   Orbit,
+  Plug,
+  RadioReceiver,
   type LucideIcon,
   RadioTower,
+  Search,
+  Send,
+  ServerCog,
   Settings2,
+  ShieldOff,
   Siren,
   Tags,
-  Waypoints
+  Waypoints,
+  Webhook
 } from 'lucide-react';
 import { isActiveRoute } from '../app-frame-state';
 import { navSections } from '../nav';
@@ -26,6 +40,7 @@ export type ShellSidebarItem = {
   href: string;
   title: string;
   active: boolean;
+  iconKey: string;
   Icon: LucideIcon;
 };
 
@@ -39,9 +54,13 @@ const shellNavIconMap: Record<string, LucideIcon> = {
   overview: LayoutDashboard,
   bulletin: BellRing,
   entities: Boxes,
+  'entity-discovery': Search,
+  'entity-definition': ListTree,
   trace: Waypoints,
   log: Logs,
   monitor: CircleGauge,
+  collector: RadioReceiver,
+  'monitor-template': FileCode2,
   otlp: RadioTower,
   'otlp-metrics': Activity,
   incidents: Siren,
@@ -50,7 +69,17 @@ const shellNavIconMap: Record<string, LucideIcon> = {
   explorer: Compass,
   alert: BellRing,
   'alert-setting': Settings2,
+  'alert-integration': Webhook,
+  'alert-group': GitMerge,
+  'alert-inhibit': ShieldOff,
+  'alert-silence': BellOff,
+  'alert-notice': Send,
   settings: Settings2,
+  'mcp-server': ServerCog,
+  'settings-config': FileCog,
+  plugins: Plug,
+  help: CircleHelp,
+  'alert-bulletin': BellDot,
   token: Tags,
   status: Network
 };
@@ -63,12 +92,13 @@ export function resolveShellNavIcon(icon?: string) {
 export function buildShellSidebarSections(pathname: string, t: Translator): ShellSidebarSection[] {
   return navSections.map(section => ({
     key: section.key,
-    title: section.titleKey ? t(section.titleKey) : section.title,
+    title: t(section.titleKey),
     items: section.items.map(item => ({
       key: item.key,
       href: item.href,
-      title: item.labelKey ? t(item.labelKey) : item.label,
+      title: t(item.labelKey),
       active: isActiveRoute(pathname, item.href),
+      iconKey: item.icon,
       Icon: resolveShellNavIcon(item.icon)
     }))
   }));


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 48/50: chore(frontend): retire angular web app

Posted by zh...@apache.org.
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 fc5c12431e8f2c60599e0bbc369521b5ac87ed06
Author: Logic <zq...@dromara.org>
AuthorDate: Wed Jun 3 11:39:29 2026 +0800

    chore(frontend): retire angular web app
---
 .github/labeler.yml                                |     2 +-
 .github/workflows/backend-build-test.yml           |     9 +-
 .github/workflows/frontend-build-test.yml          |    23 +-
 .github/workflows/nightly-build.yml                |    10 +-
 CONTRIBUTING.md                                    |    48 +-
 README.md                                          |     6 +-
 README_CN.md                                       |     6 +-
 README_JP.md                                       |     6 +-
 script/assembly/server/assembly-docker.xml         |     6 -
 script/assembly/server/assembly-linux-amd64.xml    |     6 -
 script/assembly/server/assembly-linux-arm64.xml    |     6 -
 script/assembly/server/assembly-macos-amd64.xml    |     6 -
 script/assembly/server/assembly-macos-arm64.xml    |     6 -
 script/assembly/server/assembly-windows-64.xml     |     6 -
 script/assembly/server/assembly.xml                |     6 -
 script/dev/start-mixed-frontend.sh                 |    17 +-
 script/release/release-win.ps1                     |    13 +-
 web-app/.browserslistrc                            |    17 -
 web-app/.editorconfig                              |    16 -
 web-app/.eslintignore                              |    34 -
 web-app/.eslintrc.js                               |   132 -
 web-app/.gitignore                                 |    46 -
 web-app/.prettierignore                            |    18 -
 web-app/.prettierrc.js                             |    13 -
 web-app/.stylelintrc                               |    37 -
 web-app/README.md                                  |    30 -
 web-app/angular.json                               |   181 -
 web-app/karma.conf.js                              |    44 -
 web-app/ng-alain.json                              |    13 -
 web-app/package.json                               |   147 -
 web-app/pnpm-lock.yaml                             | 16592 ---------
 web-app/proxy.conf.json                            |   158 -
 web-app/src/app/app.component.ts                   |    52 -
 web-app/src/app/app.module.ts                      |    82 -
 web-app/src/app/core/core.module.ts                |    13 -
 web-app/src/app/core/guard/detect-auth-guard.ts    |    34 -
 web-app/src/app/core/i18n/i18n.service.spec.ts     |    91 -
 web-app/src/app/core/i18n/i18n.service.ts          |   152 -
 web-app/src/app/core/index.ts                      |     4 -
 .../app/core/interceptor/default.interceptor.ts    |   203 -
 web-app/src/app/core/module-import-guard.ts        |     6 -
 .../ops-workspace/ops-workspace.facade.spec.ts     |    70 -
 .../app/core/ops-workspace/ops-workspace.facade.ts |   174 -
 .../app/core/ops-workspace/ops-workspace.types.ts  |    75 -
 .../src/app/core/startup/startup.service.spec.ts   |   121 -
 web-app/src/app/core/startup/startup.service.ts    |   129 -
 web-app/src/app/global-config.module.ts            |    32 -
 web-app/src/app/icons-provider.module.ts           |    12 -
 .../src/app/layout/basic/basic-layout.util.spec.ts |     7 -
 web-app/src/app/layout/basic/basic-layout.util.ts  |     3 -
 web-app/src/app/layout/basic/basic.component.less  |   731 -
 .../src/app/layout/basic/basic.component.spec.ts   |   147 -
 web-app/src/app/layout/basic/basic.component.ts    |   215 -
 .../setting-drawer-i18n.directive.spec.ts          |   165 -
 .../directives/setting-drawer-i18n.directive.ts    |   377 -
 .../layout/basic/widgets/chat-input.component.ts   |    94 -
 .../layout/basic/widgets/fullscreen.component.ts   |    29 -
 .../src/app/layout/basic/widgets/i18n.component.ts |    62 -
 .../basic/widgets/main-nav.component.spec.ts       |   126 -
 .../app/layout/basic/widgets/main-nav.component.ts |   160 -
 .../app/layout/basic/widgets/notify.component.ts   |   358 -
 .../basic/widgets/setup-guide.component.less       |   317 -
 .../basic/widgets/setup-guide.component.spec.ts    |    57 -
 .../layout/basic/widgets/setup-guide.component.ts  |   430 -
 .../src/app/layout/basic/widgets/user.component.ts |   151 -
 web-app/src/app/layout/blank/blank.component.ts    |    10 -
 web-app/src/app/layout/layout.module.ts            |    95 -
 .../app/layout/passport/passport.component.html    |    58 -
 .../app/layout/passport/passport.component.less    |   172 -
 .../src/app/layout/passport/passport.component.ts  |    23 -
 web-app/src/app/pojo/AlertDefine.ts                |    41 -
 web-app/src/app/pojo/AlertDefineBind.ts            |    29 -
 web-app/src/app/pojo/AlertGroupConverge.ts         |    35 -
 web-app/src/app/pojo/AlertInhibit.ts               |    31 -
 web-app/src/app/pojo/AlertSilence.ts               |    35 -
 web-app/src/app/pojo/AlibabaSmsConfig.ts           |    25 -
 web-app/src/app/pojo/AppCount.ts                   |    27 -
 web-app/src/app/pojo/AwsSmsConfig.ts               |    24 -
 web-app/src/app/pojo/BulletinDefine.ts             |    28 -
 web-app/src/app/pojo/Collector.ts                  |    32 -
 web-app/src/app/pojo/CollectorSummary.ts           |    26 -
 web-app/src/app/pojo/EmailNoticeSender.ts          |    33 -
 web-app/src/app/pojo/Entity.ts                     |   105 -
 web-app/src/app/pojo/EntityCatalogSuggestions.ts   |    31 -
 web-app/src/app/pojo/EntityDefinition.ts           |    25 -
 .../app/pojo/EntityDefinitionWorkspaceActivity.ts  |    33 -
 .../app/pojo/EntityDefinitionWorkspaceResume.ts    |    34 -
 .../app/pojo/EntityDefinitionWorkspaceTemplate.ts  |    34 -
 web-app/src/app/pojo/EntityDetail.ts               |   325 -
 web-app/src/app/pojo/EntityDiscoveryGovernance.ts  |    65 -
 web-app/src/app/pojo/EntityIdentity.ts             |    29 -
 web-app/src/app/pojo/EntityMonitorBind.ts          |    29 -
 .../src/app/pojo/EntityMonitorBindingCandidate.ts  |    28 -
 web-app/src/app/pojo/EntityRelation.ts             |    31 -
 web-app/src/app/pojo/EntityStatus.ts               |    29 -
 web-app/src/app/pojo/EntitySummary.ts              |    39 -
 web-app/src/app/pojo/Fields.ts                     |    22 -
 web-app/src/app/pojo/GrafanaDashboard.ts           |    26 -
 web-app/src/app/pojo/GroupAlert.ts                 |    35 -
 web-app/src/app/pojo/Label.ts                      |    31 -
 web-app/src/app/pojo/LogEntry.ts                   |    39 -
 web-app/src/app/pojo/Message.ts                    |    24 -
 web-app/src/app/pojo/ModelProviderConfig.ts        |    55 -
 web-app/src/app/pojo/Monitor.ts                    |    46 -
 web-app/src/app/pojo/Mute.ts                       |    22 -
 web-app/src/app/pojo/NoticeReceiver.ts             |    59 -
 web-app/src/app/pojo/NoticeRule.ts                 |    38 -
 web-app/src/app/pojo/NoticeTemplate.ts             |    32 -
 web-app/src/app/pojo/ObjectStore.ts                |    49 -
 web-app/src/app/pojo/OtlpIngestion.ts              |   159 -
 web-app/src/app/pojo/Page.ts                       |    30 -
 web-app/src/app/pojo/Param.ts                      |    26 -
 web-app/src/app/pojo/ParamDefine.ts                |    39 -
 web-app/src/app/pojo/Plugin.ts                     |    28 -
 web-app/src/app/pojo/PluginItem.ts                 |    22 -
 web-app/src/app/pojo/SingleAlert.ts                |    35 -
 web-app/src/app/pojo/SmsNoticeSender.ts            |    42 -
 web-app/src/app/pojo/SmslocalSmsConfig.ts          |    22 -
 web-app/src/app/pojo/StatusPageComponent.ts        |    36 -
 web-app/src/app/pojo/StatusPageComponentStatus.ts  |    26 -
 web-app/src/app/pojo/StatusPageHistory.ts          |    33 -
 web-app/src/app/pojo/StatusPageIncident.ts         |    40 -
 web-app/src/app/pojo/StatusPageIncidentContent.ts  |    30 -
 web-app/src/app/pojo/StatusPageOrg.ts              |    34 -
 web-app/src/app/pojo/SystemConfig.ts               |    24 -
 web-app/src/app/pojo/TemplateConfig.ts             |    22 -
 web-app/src/app/pojo/TencentSmsConfig.ts           |    26 -
 web-app/src/app/pojo/Trace.ts                      |    94 -
 web-app/src/app/pojo/TwilioSmsConfig.ts            |    24 -
 web-app/src/app/pojo/UniSmsConfig.ts               |    26 -
 web-app/src/app/pojo/enums/sms-type.enum.ts        |    32 -
 web-app/src/app/routes/SafePipe.ts                 |    31 -
 .../alert/alert-center/alert-center.component.html |   659 -
 .../alert/alert-center/alert-center.component.less |   802 -
 .../alert-center/alert-center.component.spec.ts    |   200 -
 .../alert/alert-center/alert-center.component.ts   |  1201 -
 .../alert-group-converge.component.html            |   260 -
 .../alert-group-converge.component.less            |    62 -
 .../alert-group-converge.component.spec.ts         |    41 -
 .../alert-group/alert-group-converge.component.ts  |   314 -
 .../alert-inhibit/alert-inhibit.component.html     |   289 -
 .../alert-inhibit/alert-inhibit.component.less     |   166 -
 .../alert-inhibit/alert-inhibit.component.spec.ts  |    41 -
 .../alert/alert-inhibit/alert-inhibit.component.ts |   635 -
 .../alert-integration.component.html               |    55 -
 .../alert-integration.component.less               |   110 -
 .../alert-integration.component.spec.ts            |    80 -
 .../alert-integration.component.ts                 |   159 -
 .../alert-notice-receiver.component.html           |   609 -
 .../alert-notice-receiver.component.less           |     0
 .../alert-notice-receiver.component.spec.ts        |    41 -
 .../alert-notice-receiver.component.ts             |   295 -
 .../alert-notice-rule.component.html               |   240 -
 .../alert-notice-rule.component.less               |     0
 .../alert-notice-rule.component.spec.ts            |    41 -
 .../alert-notice-rule.component.ts                 |   461 -
 .../alert-notice-template.component.html           |   259 -
 .../alert-notice-template.component.less           |     9 -
 .../alert-notice-template.component.spec.ts        |    41 -
 .../alert-notice-template.component.ts             |   243 -
 .../alert/alert-notice/alert-notice.component.html |    34 -
 .../alert-notice/alert-notice.component.spec.ts    |    43 -
 .../alert/alert-notice/alert-notice.component.ts   |    27 -
 .../src/app/routes/alert/alert-routing.module.ts   |    46 -
 .../alert-setting/alert-setting.component.html     |  1255 -
 .../alert-setting/alert-setting.component.less     |   424 -
 .../alert-setting/alert-setting.component.spec.ts  |   114 -
 .../alert/alert-setting/alert-setting.component.ts |  1947 -
 .../alert-silence/alert-silence.component.html     |   291 -
 .../alert-silence/alert-silence.component.less     |   139 -
 .../alert-silence/alert-silence.component.spec.ts  |    41 -
 .../alert/alert-silence/alert-silence.component.ts |   640 -
 web-app/src/app/routes/alert/alert.module.ts       |    90 -
 web-app/src/app/routes/dashboard/ColorTheme.ts     |   142 -
 .../routes/dashboard/dashboard-routing.module.ts   |    12 -
 .../app/routes/dashboard/dashboard.component.html  |   222 -
 .../app/routes/dashboard/dashboard.component.less  |   797 -
 .../routes/dashboard/dashboard.component.spec.ts   |   344 -
 .../app/routes/dashboard/dashboard.component.ts    |   561 -
 .../src/app/routes/dashboard/dashboard.module.ts   |    30 -
 .../entity/entity-definition-activity.util.ts      |    54 -
 .../entity/entity-definition-preview.util.ts       |   162 -
 .../entity/entity-definition-request.util.ts       |    86 -
 .../routes/entity/entity-definition-resume.util.ts |    86 -
 .../entity/entity-definition-template.util.ts      |    64 -
 .../entity-definition-workspace.component.html     |   783 -
 .../entity-definition-workspace.component.less     |   730 -
 .../entity-definition-workspace.component.spec.ts  |   193 -
 .../entity-definition-workspace.component.ts       |  3988 ---
 .../entity-detail/entity-detail.component.html     |  1814 -
 .../entity-detail/entity-detail.component.less     |  2778 --
 .../entity-detail/entity-detail.component.spec.ts  |   650 -
 .../entity-detail/entity-detail.component.ts       |  6476 ----
 .../entity/entity-discovery-activity.util.ts       |    65 -
 .../routes/entity/entity-discovery-preset.util.ts  |   115 -
 .../entity-discovery.component.html                |  1030 -
 .../entity-discovery.component.less                |   680 -
 .../entity-discovery.component.spec.ts             |   265 -
 .../entity-discovery/entity-discovery.component.ts |  3573 --
 .../entity-editor/entity-editor.component.html     |  2140 --
 .../entity-editor/entity-editor.component.less     |  1738 -
 .../entity-editor/entity-editor.component.spec.ts  |   136 -
 .../entity-editor/entity-editor.component.ts       |  4844 ---
 .../routes/entity/entity-governance-recipe.util.ts |   207 -
 .../entity/entity-list/entity-list.component.html  |   688 -
 .../entity/entity-list/entity-list.component.less  |  2463 --
 .../entity-list/entity-list.component.spec.ts      |   193 -
 .../entity/entity-list/entity-list.component.ts    |  3065 --
 .../app/routes/entity/entity-log-handoff.util.ts   |   103 -
 .../app/routes/entity/entity-otel-identity.util.ts |   305 -
 .../routes/entity/entity-rail-label.util.spec.ts   |     9 -
 .../app/routes/entity/entity-rail-label.util.ts    |    10 -
 .../src/app/routes/entity/entity-routing.module.ts |    44 -
 web-app/src/app/routes/entity/entity.module.ts     |    71 -
 .../routes/exception/exception-routing.module.ts   |    16 -
 .../routes/exception/exception.component.spec.ts   |    43 -
 .../app/routes/exception/exception.component.ts    |    23 -
 .../src/app/routes/exception/exception.module.ts   |    15 -
 .../routes/ingestion/ingestion-routing.module.ts   |    36 -
 .../src/app/routes/ingestion/ingestion.module.ts   |    32 -
 .../otlp-center/otlp-center.component.html         |   225 -
 .../otlp-center/otlp-center.component.less         |   652 -
 .../otlp-center/otlp-center.component.spec.ts      |   303 -
 .../ingestion/otlp-center/otlp-center.component.ts |   596 -
 .../otlp-metrics-console.component.html            |   160 -
 .../otlp-metrics-console.component.less            |   262 -
 .../otlp-metrics-console.component.spec.ts         |   177 -
 .../otlp-metrics-console.component.ts              |   411 -
 .../monitor-data-chart.component.html              |    63 -
 .../monitor-data-chart.component.less              |    95 -
 .../monitor-data-chart.component.spec.ts           |   109 -
 .../monitor-data-chart.component.ts                |   394 -
 .../monitor-data-chart.worker.ts                   |   218 -
 .../monitor-data-table.component.html              |   273 -
 .../monitor-data-table.component.less              |   359 -
 .../monitor-data-table.component.spec.ts           |   218 -
 .../monitor-data-table.component.ts                |   263 -
 .../monitor-detail/monitor-detail.component.html   |   296 -
 .../monitor-detail/monitor-detail.component.less   |   410 -
 .../monitor-detail.component.spec.ts               |   625 -
 .../monitor-detail/monitor-detail.component.ts     |   831 -
 .../monitor-edit/monitor-edit.component.html       |    60 -
 .../monitor-edit/monitor-edit.component.spec.ts    |   322 -
 .../monitor/monitor-edit/monitor-edit.component.ts |   511 -
 .../monitor-form/monitor-form.component.html       |   351 -
 .../monitor-form/monitor-form.component.less       |   317 -
 .../monitor-form/monitor-form.component.spec.ts    |   125 -
 .../monitor/monitor-form/monitor-form.component.ts |   347 -
 .../monitor-list/monitor-list.component.html       |   469 -
 .../monitor-list/monitor-list.component.less       |   608 -
 .../monitor-list/monitor-list.component.spec.ts    |   326 -
 .../monitor/monitor-list/monitor-list.component.ts |  1563 -
 .../monitor/monitor-new/monitor-new.component.html |    60 -
 .../monitor-new/monitor-new.component.spec.ts      |   256 -
 .../monitor/monitor-new/monitor-new.component.ts   |   430 -
 .../app/routes/monitor/monitor-routing.module.ts   |    40 -
 web-app/src/app/routes/monitor/monitor.module.ts   |    87 -
 .../monitor-route-state-panel.component.html       |    29 -
 .../monitor-route-state-panel.component.less       |    95 -
 .../monitor-route-state-panel.component.spec.ts    |    68 -
 .../monitor-route-state-panel.component.ts         |    42 -
 .../monitor/shared/monitor-route-state.type.ts     |    20 -
 .../ops-placeholder-page.component.less            |    38 -
 .../ops-placeholder-page.component.ts              |    95 -
 .../app/routes/passport/lock/lock.component.html   |    40 -
 .../app/routes/passport/lock/lock.component.less   |    20 -
 .../src/app/routes/passport/lock/lock.component.ts |    52 -
 .../app/routes/passport/login/login.component.html |    81 -
 .../app/routes/passport/login/login.component.less |    59 -
 .../routes/passport/login/login.component.spec.ts  |    93 -
 .../app/routes/passport/login/login.component.ts   |   175 -
 .../app/routes/passport/passport-routing.module.ts |    23 -
 web-app/src/app/routes/passport/passport.module.ts |    14 -
 .../src/app/routes/routes-routing.module.spec.ts   |    56 -
 web-app/src/app/routes/routes-routing.module.ts    |    91 -
 web-app/src/app/routes/routes.module.ts            |     8 -
 .../setting/collector/collector.component.html     |   286 -
 .../setting/collector/collector.component.spec.ts  |    43 -
 .../setting/collector/collector.component.ts       |   357 -
 .../routes/setting/define/define.component.html    |   126 -
 .../routes/setting/define/define.component.less    |    38 -
 .../routes/setting/define/define.component.spec.ts |    83 -
 .../app/routes/setting/define/define.component.ts  |   298 -
 .../app/routes/setting/label/label.component.html  |   135 -
 .../app/routes/setting/label/label.component.less  |   113 -
 .../routes/setting/label/label.component.spec.ts   |    43 -
 .../app/routes/setting/label/label.component.ts    |   218 -
 .../routes/setting/plugins/plugin.component.html   |   200 -
 .../setting/plugins/plugin.component.spec.ts       |    43 -
 .../app/routes/setting/plugins/plugin.component.ts |   340 -
 .../app/routes/setting/setting-routing.module.ts   |    74 -
 web-app/src/app/routes/setting/setting.module.ts   |    89 -
 .../message-server/message-server.component.html   |   432 -
 .../message-server/message-server.component.less   |     0
 .../message-server.component.spec.ts               |    41 -
 .../message-server/message-server.component.ts     |   238 -
 .../object-store/object-store.component.html       |   116 -
 .../object-store/object-store.component.less       |     0
 .../object-store/object-store.component.spec.ts    |    41 -
 .../object-store/object-store.component.ts         |   122 -
 .../setting/settings/settings.component.html       |    40 -
 .../setting/settings/settings.component.less       |    92 -
 .../setting/settings/settings.component.spec.ts    |    80 -
 .../routes/setting/settings/settings.component.ts  |   111 -
 .../system-config/system-config.component.html     |    78 -
 .../system-config/system-config.component.less     |     0
 .../system-config/system-config.component.spec.ts  |    75 -
 .../system-config/system-config.component.ts       |   128 -
 .../token-management.component.html                |   169 -
 .../token-management.component.less                |   149 -
 .../token-management.component.spec.ts             |    64 -
 .../token-management/token-management.component.ts |   201 -
 .../routes/setting/status/status.component.html    |   570 -
 .../routes/setting/status/status.component.less    |     0
 .../routes/setting/status/status.component.spec.ts |    43 -
 .../app/routes/setting/status/status.component.ts  |   520 -
 web-app/src/app/service/ai-chat.service.ts         |   246 -
 .../src/app/service/alert-define.service.spec.ts   |    35 -
 web-app/src/app/service/alert-define.service.ts    |   114 -
 .../src/app/service/alert-group.service.spec.ts    |    35 -
 web-app/src/app/service/alert-group.service.ts     |    77 -
 .../src/app/service/alert-inhibit.service.spec.ts  |    35 -
 web-app/src/app/service/alert-inhibit.service.ts   |    77 -
 .../src/app/service/alert-silence.service.spec.ts  |    35 -
 web-app/src/app/service/alert-silence.service.ts   |    77 -
 web-app/src/app/service/alert-sound.service.ts     |    46 -
 web-app/src/app/service/alert.service.spec.ts      |    35 -
 web-app/src/app/service/alert.service.ts           |   131 -
 web-app/src/app/service/app-define.service.spec.ts |    35 -
 web-app/src/app/service/app-define.service.ts      |   118 -
 web-app/src/app/service/auth.service.spec.ts       |    35 -
 web-app/src/app/service/auth.service.ts            |    60 -
 web-app/src/app/service/collector.service.spec.ts  |    35 -
 web-app/src/app/service/collector.service.ts       |    92 -
 .../src/app/service/dashboard-layout.service.ts    |    68 -
 web-app/src/app/service/entity.service.ts          |   293 -
 web-app/src/app/service/general-config.service.ts  |    58 -
 web-app/src/app/service/label.service.spec.ts      |    35 -
 web-app/src/app/service/label.service.ts           |    78 -
 .../src/app/service/local-storage.service.spec.ts  |    35 -
 web-app/src/app/service/local-storage.service.ts   |    64 -
 web-app/src/app/service/log.service.ts             |   147 -
 .../src/app/service/memory-storage.service.spec.ts |    35 -
 web-app/src/app/service/memory-storage.service.ts  |    42 -
 web-app/src/app/service/monitor.service.spec.ts    |    35 -
 web-app/src/app/service/monitor.service.ts         |   215 -
 .../app/service/notice-receiver.service.spec.ts    |    39 -
 web-app/src/app/service/notice-receiver.service.ts |    75 -
 .../src/app/service/notice-rule.service.spec.ts    |    35 -
 web-app/src/app/service/notice-rule.service.ts     |    65 -
 .../app/service/notice-template.service.spec.ts    |    35 -
 web-app/src/app/service/notice-template.service.ts |    75 -
 .../src/app/service/otlp-ingestion.service.spec.ts |    65 -
 web-app/src/app/service/otlp-ingestion.service.ts  |    66 -
 web-app/src/app/service/plugin.service.spec.ts     |    35 -
 web-app/src/app/service/plugin.service.ts          |    87 -
 .../src/app/service/status-page.service.spec.ts    |    35 -
 web-app/src/app/service/status-page.service.ts     |   105 -
 web-app/src/app/service/theme.service.spec.ts      |    37 -
 web-app/src/app/service/theme.service.ts           |    94 -
 .../activity-timeline.component.html               |    19 -
 .../activity-timeline.component.less               |    77 -
 .../activity-timeline.component.ts                 |    24 -
 .../shared/components/ai-chat/ai-chat.module.ts    |    67 -
 .../shared/components/ai-chat/chat.component.html  |   416 -
 .../shared/components/ai-chat/chat.component.less  |   820 -
 .../shared/components/ai-chat/chat.component.ts    |   854 -
 .../code-block/code-block.component.html           |    26 -
 .../code-block/code-block.component.less           |   240 -
 .../components/code-block/code-block.component.ts  |   193 -
 .../configurable-field.component.html              |    43 -
 .../configurable-field.component.less              |    31 -
 .../configurable-field.component.spec.ts           |    43 -
 .../configurable-field.component.ts                |   115 -
 .../form-field/form-field.component.html           |   229 -
 .../form-field/form-field.component.less           |     0
 .../form-field/form-field.component.spec.ts        |   111 -
 .../components/form-field/form-field.component.ts  |   143 -
 .../help-message-show.component.html               |    72 -
 .../help-message-show.component.less               |   209 -
 .../help-message-show.component.spec.ts            |    41 -
 .../help-message-show.component.ts                 |    95 -
 .../label-selector/label-selector.component.html   |    84 -
 .../label-selector/label-selector.component.less   |    17 -
 .../label-selector.component.spec.ts               |    41 -
 .../label-selector/label-selector.component.ts     |   218 -
 .../monitor-select-list.component.html             |    47 -
 .../monitor-select-list.component.less             |    31 -
 .../monitor-select-list.component.spec.ts          |    43 -
 .../monitor-select-list.component.ts               |    54 -
 .../monitor-select-menu.component.html             |    54 -
 .../monitor-select-menu.component.less             |   154 -
 .../monitor-select-menu.component.spec.ts          |    43 -
 .../monitor-select-menu.component.ts               |    75 -
 .../multi-func-input.component.html                |    70 -
 .../multi-func-input.component.less                |    27 -
 .../multi-func-input.component.spec.ts             |    43 -
 .../multi-func-input/multi-func-input.component.ts |    97 -
 .../ops-context-bar/ops-context-bar.component.html |    81 -
 .../ops-context-bar/ops-context-bar.component.less |   202 -
 .../ops-context-bar.component.spec.ts              |   113 -
 .../ops-context-bar/ops-context-bar.component.ts   |   129 -
 .../page-shell/page-shell.component.html           |    48 -
 .../page-shell/page-shell.component.less           |   134 -
 .../page-shell/page-shell.component.spec.ts        |    92 -
 .../components/page-shell/page-shell.component.ts  |    44 -
 .../platform-chart-card.component.html             |    12 -
 .../platform-chart-card.component.less             |    27 -
 .../platform-chart-card.component.spec.ts          |    21 -
 .../platform-chart-card.component.ts               |    17 -
 .../platform-column-header-row.component.html      |     1 -
 .../platform-column-header-row.component.less      |     8 -
 .../platform-column-header-row.component.spec.ts   |    27 -
 .../platform-column-header-row.component.ts        |    14 -
 .../platform-context-chip-bar.component.html       |    12 -
 .../platform-context-chip-bar.component.less       |    94 -
 .../platform-context-chip-bar.component.spec.ts    |    44 -
 .../platform-context-chip-bar.component.ts         |    20 -
 .../platform-copyright-footer.component.html       |    10 -
 .../platform-copyright-footer.component.less       |    33 -
 .../platform-copyright-footer.component.spec.ts    |    24 -
 .../platform-copyright-footer.component.ts         |    16 -
 .../platform-drawer-action-links.component.html    |    15 -
 .../platform-drawer-action-links.component.less    |    16 -
 .../platform-drawer-action-links.component.spec.ts |    29 -
 .../platform-drawer-action-links.component.ts      |    27 -
 .../platform-drawer-attribute-list.component.html  |     6 -
 .../platform-drawer-attribute-list.component.less  |    58 -
 ...latform-drawer-attribute-list.component.spec.ts |    35 -
 .../platform-drawer-attribute-list.component.ts    |    19 -
 .../platform-drawer-callout-card.component.html    |    18 -
 .../platform-drawer-callout-card.component.less    |    74 -
 .../platform-drawer-callout-card.component.spec.ts |    44 -
 .../platform-drawer-callout-card.component.ts      |    18 -
 .../platform-drawer-code-preview.component.html    |     9 -
 .../platform-drawer-code-preview.component.less    |    11 -
 .../platform-drawer-code-preview.component.spec.ts |    41 -
 .../platform-drawer-code-preview.component.ts      |    17 -
 .../platform-drawer-facts.component.html           |     9 -
 .../platform-drawer-facts.component.less           |    42 -
 .../platform-drawer-facts.component.spec.ts        |    28 -
 .../platform-drawer-facts.component.ts             |    21 -
 .../platform-drawer-pill-row.component.html        |    20 -
 .../platform-drawer-pill-row.component.less        |    37 -
 .../platform-drawer-pill-row.component.spec.ts     |    49 -
 .../platform-drawer-pill-row.component.ts          |    27 -
 .../platform-drawer-preview-list.component.html    |     6 -
 .../platform-drawer-preview-list.component.less    |    38 -
 .../platform-drawer-preview-list.component.spec.ts |    30 -
 .../platform-drawer-preview-list.component.ts      |    18 -
 .../platform-drawer-section-list.component.html    |    33 -
 .../platform-drawer-section-list.component.less    |    55 -
 .../platform-drawer-section-list.component.spec.ts |    66 -
 .../platform-drawer-section-list.component.ts      |    37 -
 .../platform-drawer-section.component.html         |    12 -
 .../platform-drawer-section.component.less         |    40 -
 .../platform-drawer-section.component.spec.ts      |    31 -
 .../platform-drawer-section.component.ts           |    13 -
 .../platform-drawer-shell.component.html           |    29 -
 .../platform-drawer-shell.component.less           |    62 -
 .../platform-drawer-shell.component.spec.ts        |    64 -
 .../platform-drawer-shell.component.ts             |    27 -
 .../platform-drawer-toolbar.component.html         |    16 -
 .../platform-drawer-toolbar.component.less         |    38 -
 .../platform-drawer-toolbar.component.spec.ts      |    39 -
 .../platform-drawer-toolbar.component.ts           |    19 -
 .../platform-facts-strip.component.html            |    13 -
 .../platform-facts-strip.component.less            |    78 -
 .../platform-facts-strip.component.spec.ts         |    30 -
 .../platform-facts-strip.component.ts              |    20 -
 .../platform-key-value-grid.component.html         |     6 -
 .../platform-key-value-grid.component.less         |    85 -
 .../platform-key-value-grid.component.spec.ts      |    31 -
 .../platform-key-value-grid.component.ts           |    19 -
 .../platform-panel-header.component.html           |    11 -
 .../platform-panel-header.component.less           |    62 -
 .../platform-panel-header.component.spec.ts        |    35 -
 .../platform-panel-header.component.ts             |    15 -
 .../platform-rail-nav.component.html               |    36 -
 .../platform-rail-nav.component.less               |   124 -
 .../platform-rail-nav.component.spec.ts            |    55 -
 .../platform-rail-nav.component.ts                 |    53 -
 .../platform-section-header.component.html         |    12 -
 .../platform-section-header.component.less         |    51 -
 .../platform-section-header.component.spec.ts      |    35 -
 .../platform-section-header.component.ts           |    16 -
 .../platform-stage-insight-list.component.html     |     6 -
 .../platform-stage-insight-list.component.less     |    47 -
 .../platform-stage-insight-list.component.spec.ts  |    33 -
 .../platform-stage-insight-list.component.ts       |    18 -
 .../platform-stage-meta-header.component.html      |    25 -
 .../platform-stage-meta-header.component.less      |   102 -
 .../platform-stage-meta-header.component.spec.ts   |    44 -
 .../platform-stage-meta-header.component.ts        |    22 -
 .../platform-stage-section.component.html          |    21 -
 .../platform-stage-section.component.less          |    67 -
 .../platform-stage-section.component.spec.ts       |    39 -
 .../platform-stage-section.component.ts            |    21 -
 .../platform-summary-metric-grid.component.html    |    29 -
 .../platform-summary-metric-grid.component.less    |   128 -
 .../platform-summary-metric-grid.component.spec.ts |    43 -
 .../platform-summary-metric-grid.component.ts      |    32 -
 .../platform-support-action-bar.component.html     |    15 -
 .../platform-support-action-bar.component.less     |    26 -
 .../platform-support-action-bar.component.spec.ts  |    36 -
 .../platform-support-action-bar.component.ts       |    28 -
 .../platform-support-link-list.component.html      |    15 -
 .../platform-support-link-list.component.less      |    37 -
 .../platform-support-link-list.component.spec.ts   |    46 -
 .../platform-support-link-list.component.ts        |    29 -
 .../platform-support-panel.component.html          |    13 -
 .../platform-support-panel.component.less          |    56 -
 .../platform-support-panel.component.spec.ts       |    35 -
 .../platform-support-panel.component.ts            |    16 -
 ...platform-trace-waterfall-preview.component.html |    69 -
 ...platform-trace-waterfall-preview.component.less |   301 -
 ...tform-trace-waterfall-preview.component.spec.ts |    66 -
 .../platform-trace-waterfall-preview.component.ts  |   181 -
 .../right-drawer/right-drawer.component.html       |    48 -
 .../right-drawer/right-drawer.component.less       |   101 -
 .../right-drawer/right-drawer.component.spec.ts    |    70 -
 .../right-drawer/right-drawer.component.ts         |    67 -
 .../sql-editor/sql-editor.component.html           |    28 -
 .../sql-editor/sql-editor.component.less           |    68 -
 .../components/sql-editor/sql-editor.component.ts  |   438 -
 .../components/sql-editor/sql-editor.module.ts     |    13 -
 .../summary-card/summary-card.component.html       |    11 -
 .../summary-card/summary-card.component.less       |    61 -
 .../summary-card/summary-card.component.ts         |    19 -
 .../components/toolbar/toolbar.component.html      |    39 -
 .../components/toolbar/toolbar.component.less      |    20 -
 .../components/toolbar/toolbar.component.spec.ts   |    43 -
 .../shared/components/toolbar/toolbar.component.ts |    41 -
 .../workspace-guidance-panel.component.html        |    40 -
 .../workspace-guidance-panel.component.less        |    26 -
 .../workspace-guidance-panel.component.spec.ts     |    61 -
 .../workspace-guidance-panel.component.ts          |    90 -
 .../workspace-shell/workspace-shell.component.html |    84 -
 .../workspace-shell/workspace-shell.component.less |   156 -
 .../workspace-shell.component.spec.ts              |   142 -
 .../workspace-shell/workspace-shell.component.ts   |    96 -
 web-app/src/app/shared/constants.ts                |    22 -
 .../directives/markdown-mermaid.directive.spec.ts  |    46 -
 .../directives/markdown-mermaid.directive.ts       |   152 -
 web-app/src/app/shared/index.ts                    |     2 -
 .../app/shared/json-schema/json-schema.module.ts   |    18 -
 .../src/app/shared/json-schema/test/test.widget.ts |    20 -
 .../observability-console-context.spec.ts          |    91 -
 .../observability/observability-console-context.ts |   139 -
 .../observability/observability-theme.spec.ts      |   111 -
 .../shared/observability/observability-theme.ts    |   528 -
 .../src/app/shared/pipe/elapsed-time.pipe.spec.ts  |    47 -
 web-app/src/app/shared/pipe/elapsed-time.pipe.ts   |    55 -
 web-app/src/app/shared/pipe/i18n-else.pipe.spec.ts |    37 -
 web-app/src/app/shared/pipe/i18n-else.pipe.ts      |    37 -
 web-app/src/app/shared/pipe/timezone.pipe.spec.ts  |    27 -
 web-app/src/app/shared/pipe/timezone.pipe.ts       |    39 -
 web-app/src/app/shared/services/ai-bot.service.ts  |   183 -
 .../app/shared/services/ai-chat-modal.service.ts   |    79 -
 web-app/src/app/shared/shared-delon.module.ts      |     6 -
 web-app/src/app/shared/shared-zorro.module.ts      |    47 -
 web-app/src/app/shared/shared.module.ts            |   178 -
 .../src/app/shared/utils/code-navigation.util.ts   |    76 -
 web-app/src/app/shared/utils/common-util.ts        |   177 -
 web-app/src/assets/.gitkeep                        |     0
 web-app/src/assets/app-data.json                   |   340 -
 web-app/src/assets/audio/default-alert-CN.mp3      |   Bin 12096 -> 0 bytes
 web-app/src/assets/audio/default-alert-EN.mp3      |   Bin 11664 -> 0 bytes
 web-app/src/assets/bg.png                          |   Bin 154582 -> 0 bytes
 web-app/src/assets/brand.svg                       |     6 -
 web-app/src/assets/brand_white.svg                 |     6 -
 web-app/src/assets/color.less                      |  3461 --
 web-app/src/assets/dev/trace-demo-bootstrap.html   |    70 -
 .../src/assets/doc/log-integration/otlp.en-US.md   |   108 -
 .../src/assets/doc/log-integration/otlp.zh-CN.md   |   108 -
 web-app/src/assets/i18n/en-US.json                 |  3509 --
 web-app/src/assets/i18n/ja-JP.json                 |  2358 --
 web-app/src/assets/i18n/pt-BR.json                 |  2466 --
 web-app/src/assets/i18n/zh-CN.json                 |  3533 --
 web-app/src/assets/i18n/zh-TW.json                 |  2358 --
 web-app/src/assets/img/avatar.svg                  |     1 -
 web-app/src/assets/img/empty-full.svg              |    21 -
 .../src/assets/img/integration/alibabacloud.svg    |    15 -
 web-app/src/assets/img/integration/huaweicloud.svg |     1 -
 web-app/src/assets/img/integration/otlp.svg        |     6 -
 web-app/src/assets/img/integration/prometheus.svg  |     1 -
 web-app/src/assets/img/integration/skywalking.svg  |     6 -
 web-app/src/assets/img/integration/tencent.svg     |     1 -
 web-app/src/assets/img/integration/uptime-kuma.svg |     9 -
 web-app/src/assets/img/integration/volcengine.svg  |    33 -
 web-app/src/assets/img/integration/zabbix.svg      |    35 -
 web-app/src/assets/img/notification.svg            |     1 -
 web-app/src/assets/logo.svg                        |     4 -
 web-app/src/assets/logo_white.svg                  |     4 -
 web-app/src/assets/style.compact.css               |    20 -
 web-app/src/assets/style.dark.css                  | 35387 -------------------
 web-app/src/environments/environment.prod.ts       |    10 -
 web-app/src/environments/environment.ts            |    22 -
 web-app/src/favicon.ico                            |   Bin 2540 -> 0 bytes
 web-app/src/index.html                             |    35 -
 web-app/src/main.ts                                |    42 -
 web-app/src/polyfills.ts                           |    65 -
 web-app/src/style-icons-auto.ts                    |   106 -
 web-app/src/style-icons.ts                         |     5 -
 web-app/src/styles.less                            |   438 -
 web-app/src/styles/entity-workspace.less           |   280 -
 web-app/src/styles/index.less                      |   275 -
 web-app/src/styles/observability-workbench.less    |   113 -
 web-app/src/styles/platform-drawer.less            |   119 -
 web-app/src/styles/platform-shell.less             |    82 -
 web-app/src/styles/theme-vars.less                 |    64 -
 web-app/src/styles/theme.less                      |   439 -
 web-app/src/test.ts                                |   132 -
 web-app/src/typings.d.ts                           |     5 -
 web-app/tsconfig.app.json                          |    15 -
 web-app/tsconfig.json                              |    41 -
 web-app/tsconfig.spec.json                         |    18 -
 .../doc/alert-integration/alertmanager.en-US.md    |     0
 .../doc/alert-integration/alertmanager.ja-JP.md    |     0
 .../doc/alert-integration/alertmanager.pt-BR.md    |     0
 .../doc/alert-integration/alertmanager.zh-CN.md    |     0
 .../doc/alert-integration/alertmanager.zh-TW.md    |     0
 .../alert-integration/alibabacloud-sls.en-US.md    |     0
 .../alert-integration/alibabacloud-sls.zh-CN.md    |     0
 .../alert-integration/alibabacloud-sls.zh-TW.md    |     0
 .../doc/alert-integration/huaweicloud-ces.en-US.md |     0
 .../doc/alert-integration/huaweicloud-ces.zh-CN.md |     0
 .../doc/alert-integration/huaweicloud-ces.zh-TW.md |     0
 .../doc/alert-integration/prometheus.en-US.md      |     0
 .../doc/alert-integration/prometheus.ja-JP.md      |     0
 .../doc/alert-integration/prometheus.pt-BR.md      |     0
 .../doc/alert-integration/prometheus.zh-CN.md      |     0
 .../doc/alert-integration/prometheus.zh-TW.md      |     0
 .../doc/alert-integration/skywalking.en-US.md      |     0
 .../doc/alert-integration/skywalking.ja-JP.md      |     0
 .../doc/alert-integration/skywalking.pt-BR.md      |     0
 .../doc/alert-integration/skywalking.zh-CN.md      |     0
 .../doc/alert-integration/skywalking.zh-TW.md      |     0
 .../assets/doc/alert-integration/tencent.en-US.md  |     0
 .../assets/doc/alert-integration/tencent.ja-JP.md  |     0
 .../assets/doc/alert-integration/tencent.pt-BR.md  |     0
 .../assets/doc/alert-integration/tencent.zh-CN.md  |     0
 .../assets/doc/alert-integration/tencent.zh-TW.md  |     0
 .../doc/alert-integration/uptime-kuma.en-US.md     |     0
 .../doc/alert-integration/uptime-kuma.zh-CN.md     |     0
 .../doc/alert-integration/uptime-kuma.zh-TW.md     |     0
 .../doc/alert-integration/volcengine.en-US.md      |     0
 .../doc/alert-integration/volcengine.ja-JP.md      |     0
 .../doc/alert-integration/volcengine.pt-BR.md      |     0
 .../doc/alert-integration/volcengine.zh-CN.md      |     0
 .../doc/alert-integration/volcengine.zh-TW.md      |     0
 .../assets/doc/alert-integration/webhook.en-US.md  |     0
 .../assets/doc/alert-integration/webhook.ja-JP.md  |     0
 .../assets/doc/alert-integration/webhook.pt-BR.md  |     0
 .../assets/doc/alert-integration/webhook.zh-CN.md  |     0
 .../assets/doc/alert-integration/webhook.zh-TW.md  |     0
 .../assets/doc/alert-integration/zabbix.en-US.md   |     0
 .../assets/doc/alert-integration/zabbix.zh-CN.md   |     0
 .../assets/doc/alert-integration/zabbix.zh-TW.md   |     0
 659 files changed, 66 insertions(+), 172474 deletions(-)

diff --git a/.github/labeler.yml b/.github/labeler.yml
index 9c2f45c690..36039b1f6a 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -79,7 +79,7 @@ home:
 webapp:
   - changed-files:
       - any-glob-to-any-file:
-          - "web-app/**/*"
+          - "web-next/**/*"
 
 e2e:
   - changed-files:
diff --git a/.github/workflows/backend-build-test.yml b/.github/workflows/backend-build-test.yml
index 34917e8e50..7872fcf1b8 100644
--- a/.github/workflows/backend-build-test.yml
+++ b/.github/workflows/backend-build-test.yml
@@ -22,19 +22,19 @@ name: Backend CI
 
 on:
   push:
-    branches: [ master, dev, action* ]
+    branches: [ master, dev, action*, '2.*' ]
     paths-ignore:
       - '**.md'
       - 'home/**'
-      - 'web-app/**'
+      - 'web-next/**'
       - 'script/**'
       - 'material/**'
   pull_request:
-    branches: [ master, dev ]
+    branches: [ master, dev, '2.*' ]
     paths-ignore:
       - '**.md'
       - 'home/**'
-      - 'web-app/**'
+      - 'web-next/**'
       - 'script/**'
       - 'material/**'
 
@@ -80,4 +80,3 @@ jobs:
           path: |
             e2e/logs/
             e2e/report/
-
diff --git a/.github/workflows/frontend-build-test.yml b/.github/workflows/frontend-build-test.yml
index 55b57c95de..63b5cdaeda 100644
--- a/.github/workflows/frontend-build-test.yml
+++ b/.github/workflows/frontend-build-test.yml
@@ -22,18 +22,14 @@ name: Frontend CI
 
 on:
   push:
-    branches: [ master, dev, 'action*', 'codex/**', 'feature/**', 'release/**' ]
+    branches: [ master, dev, 'action*', 'codex/**', 'feature/**', 'release/**', '2.*' ]
     paths:
-      - 'web-app/**'
       - 'web-next/**'
-      - 'web-app/src/assets/i18n/**'
       - '.github/workflows/frontend-build-test.yml'
   pull_request:
-    branches: [ master, dev, 'codex/**', 'feature/**', 'release/**' ]
+    branches: [ master, dev, 'codex/**', 'feature/**', 'release/**', '2.*' ]
     paths:
-      - 'web-app/**'
       - 'web-next/**'
-      - 'web-app/src/assets/i18n/**'
       - '.github/workflows/frontend-build-test.yml'
 
 jobs:
@@ -51,22 +47,23 @@ jobs:
         filters: |
           web_next:
             - 'web-next/**'
-            - 'web-app/src/assets/i18n/**'
             - '.github/workflows/frontend-build-test.yml'
 
   build:
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v4
-    - uses: pnpm/action-setup@v4
+    - uses: actions/setup-node@v4
       with:
-        version: 10
+        node-version: 22
+        cache: npm
+        cache-dependency-path: web-next/package-lock.json
     - name: Install
-      working-directory: web-app
-      run: pnpm install
+      working-directory: web-next
+      run: npm ci
     - name: EsLint Test
-      working-directory: web-app
-      run: pnpm lint:ts
+      working-directory: web-next
+      run: npm run lint
 
   web-next-contract:
     needs: web-next-scope
diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml
index 7b1847ef49..df03780758 100644
--- a/.github/workflows/nightly-build.yml
+++ b/.github/workflows/nightly-build.yml
@@ -31,15 +31,11 @@ jobs:
     steps:
     - uses: actions/checkout@v4
     - uses: ./script/ci/github-actions/setup-deps
-    - uses: pnpm/action-setup@v4
-      with:
-        version: 10
-        
     - name: Build the Frontend
       run: |
-        cd web-app
-        pnpm install
-        pnpm package
+        cd web-next
+        npm ci
+        npm run build
         
     - name: Build the Backend
       run: |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1a1738267c..aa458e9f9b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,7 +27,7 @@ Even small corrections to typos are very welcome :)
 ### Getting HertzBeat up and running   
 
 > To get HertzBeat code running on your development tools, and able to debug with breakpoints.
-> This is a front-end and back-end separation project. To start the local code, the back-end [startup](hertzbeat-startup) and the front-end [web-app](web-app) must be started separately.  
+> This is a front-end and back-end separation project. To start the local code, the back-end [startup](hertzbeat-startup) and the front-end [web-next](web-next) must be started separately.
 
 
 #### Backend start
@@ -44,17 +44,15 @@ Even small corrections to typos are very welcome :)
 
 #### Frontend start
 
-1. Need `Node pnpm` Environment, Make sure `Node.js >= 18`
+1. Need `Node.js >= 22` and `npm`
 
-2. Cd to the `web-app` directory: `cd web-app`
+2. Cd to the `web-next` directory: `cd web-next`
 
-3. Install pnpm if not existed `npm install -g pnpm`
+3. Install dependencies: `npm ci`
 
-4. Install Dependencies: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
+4. After the local backend is started, start the local frontend in the `web-next` directory: `npm run dev`
 
-5. After the local backend is started, start the local frontend in the web-app directory: `pnpm start`
-
-6. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
+5. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
 
 
 ### Find tasks   
@@ -89,6 +87,16 @@ Of course, if you have a good idea, you can also propose it directly on GitHub D
 
 Please note that the title of the PR needs to conform to our spec, and write the necessary description in the PR to facilitate code review by Committers and other contributors.   
 
+### AI-assisted contributions
+
+AI-assisted code is welcome when it is reviewed and owned by a human contributor. Please follow [AI-Assisted Development Guidelines](AGENTS.md):
+
+1. Treat AI output as a draft and keep each PR narrowly scoped.
+2. Do not include proprietary, private, license-incompatible, or secret material from AI prompts or outputs.
+3. Do not invent product behavior, API responses, topology data, metrics, health states, screenshots, or release claims.
+4. Write or update the smallest useful test/contract first for risky changes, then include the validation commands and results in the PR description.
+5. If AI helped, add a short PR note: `AI assistance`, `Human validation`, and `Risk notes`.
+
 ### Wait for the code to be merged   
 
 After submitting the PR, the Committee or the community's friends will review the code you submitted (Code Review), and will propose some modification suggestions or conduct some discussions. Please pay attention to your PR in time.  
@@ -168,7 +176,7 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
 ### 让 HertzBeat 运行起来   
 
 > 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。   
-> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。
+> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-next](web-next)分别启动生效。
 
 #### 后端启动
 
@@ -185,17 +193,15 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
 
 #### 前端启动
 
-1. 需要 `nodejs pnpm` 环境, Make sure `Node.js >= 18`
+1. 需要 `Node.js >= 22` 和 `npm`
 
-2. 进入 `web-app` 目录: `cd web-app`
+2. 进入 `web-next` 目录: `cd web-next`
 
-3. 安装pnpm: `npm install -g pnpm`
+3. 安装依赖: `npm ci`
 
-4. 在前端工程目录 `web-app` 下执行: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
+4. 待本地后端启动后,在 `web-next` 目录下启动本地前端: `npm run dev`
 
-5. 待本地后端启动后,在web-app目录下启动本地前端 `pnpm start`
-
-6. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
+5. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
 
 ### 寻找任务  
 
@@ -229,6 +235,16 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
 
 请注意 PR 的标题需要符合我们的规范,并且在 PR 中写上必要的说明,来方便 Committer 和其他贡献者进行代码审查。 
 
+### AI 辅助贡献
+
+欢迎使用 AI 辅助写代码,但 PR 必须由人类贡献者负责、审查和验证。请遵循 [AI-Assisted Development Guidelines](AGENTS.md):
+
+1. 把 AI 输出当作草稿,每个 PR 保持清晰、收敛的范围。
+2. 不要提交来自 AI 提示词或输出中的私有、专有、许可证不兼容内容,也不要包含密钥或凭据。
+3. 不要编造产品行为、API 响应、拓扑数据、指标、健康状态、截图或发布结论。
+4. 对高风险或用户可见改动,先写或更新最小测试/合约,再在 PR 描述中写明验证命令和结果。
+5. 如果使用了 AI,请在 PR 描述中简短说明:`AI assistance`、`Human validation`、`Risk notes`。
+
 ### 等待PR代码被合并  
 
 在提交了 PR 后,Committer 或者社区的小伙伴们会对您提交的代码进行审查(Code Review),会提出一些修改建议,或者是进行一些讨论,请及时关注您的PR。 
diff --git a/README.md b/README.md
index 3ac9a8b8a8..c844851fa5 100644
--- a/README.md
+++ b/README.md
@@ -158,9 +158,9 @@ Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apach
 
 ##### 3:Start via source code        
 
-1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-app`.  
+1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-next`.
 2. Backend:need `maven3+`, `java21`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `hertzbeat-startup` service. 
-3. Web:need `nodejs npm angular-cli` environment, Run `ng serve --open` in `web-app` directory after backend startup.  
+3. Web:need `Node.js >= 22` and `npm`, run `npm ci && npm run dev` in `web-next` after backend startup.
 4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`  
 
 Detailed steps refer to [CONTRIBUTING](CONTRIBUTING.md)        
@@ -589,7 +589,7 @@ WeChat Public : Search ID `usthecom`.
 HertzBeat is built on so many great open source projects, thanks to them!
 
 - `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...`
-- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
+- `TypeScript Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor Docusaurus ...`
 
 ## Landscape
 
diff --git a/README_CN.md b/README_CN.md
index 7393ace701..66db01fd42 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -152,9 +152,9 @@
 
 ##### 方式三:本地代码启动
 
-1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-app`
+1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-next`
 2. 后端:需要 `maven3+`, `java21` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。
-3. 前端:需要 `nodejs npm angular-cli`环境,待本地后端启动后,在 `web-app` 目录下启动 `ng serve --open`
+3. 前端:需要 `Node.js >= 22` 和 `npm` 环境,待本地后端启动后,在 `web-next` 目录下执行 `npm ci && npm run dev`
 4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat`
 
 详细步骤参考 [参与贡献之本地代码启动](CONTRIBUTING.md)
@@ -584,7 +584,7 @@ Thanks these wonderful people, welcome to join us:
 HertzBeat is built on so many great open source projects, thanks to them!
 
 - `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...`
-- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
+- `TypeScript Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor Docusaurus ...`
 
 
 ## Landscape
diff --git a/README_JP.md b/README_JP.md
index 13c49de6a4..9c75d7b9f0 100644
--- a/README_JP.md
+++ b/README_JP.md
@@ -155,9 +155,9 @@
 
 ##### 方式3:ローカルの実行
 
-1. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
+1. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-next`を起動する必要があります。
 2. バックエンド:`maven3+`、`Java21`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`hertzbeat-startup` を起動します。
-3. フロントエンド:`nodejs npm angular-cli`の環境は必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `ng serve --open` というコマンドを実行します。
+3. フロントエンド:`Node.js >= 22` と `npm` の環境が必要です。ローカルのバックエンドが立ち上がったら、`web-next` ディレクトリで `npm ci && npm run dev` を実行します。
 4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
 
 詳細ステップ [貢献ガイド](CONTRIBUTING.md)
@@ -587,7 +587,7 @@ WeChat公式アカウント : `usthecom`を検索.
 HertzBeat is built on so many great open source projects, thanks to them!
 
 - `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...`
-- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
+- `TypeScript Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor Docusaurus ...`
 
 
 ## Landscape
diff --git a/script/assembly/server/assembly-docker.xml b/script/assembly/server/assembly-docker.xml
index 14d5dbccb8..8091431e78 100644
--- a/script/assembly/server/assembly-docker.xml
+++ b/script/assembly/server/assembly-docker.xml
@@ -77,12 +77,6 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
             </includes>
         </fileSet>
 
-        <!-- Pack the static resources of the web app into the dist directory-->
-        <fileSet>
-            <directory>../web-app/dist</directory>
-            <outputDirectory>dist</outputDirectory>
-        </fileSet>
-
         <!-- external lib -->
         <fileSet>
             <directory>../script/ext-lib</directory>
diff --git a/script/assembly/server/assembly-linux-amd64.xml b/script/assembly/server/assembly-linux-amd64.xml
index be317b21f2..1f7834e895 100644
--- a/script/assembly/server/assembly-linux-amd64.xml
+++ b/script/assembly/server/assembly-linux-amd64.xml
@@ -78,12 +78,6 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
             </includes>
         </fileSet>
 
-        <!-- Pack the static resources of the web app into the directory-->
-        <fileSet>
-            <directory>../web-app/dist</directory>
-            <outputDirectory>dist</outputDirectory>
-        </fileSet>
-
         <!-- copy jdk -->
         <fileSet>
             <directory>../jdk/linux-amd64</directory>
diff --git a/script/assembly/server/assembly-linux-arm64.xml b/script/assembly/server/assembly-linux-arm64.xml
index 8d6801e3d6..a0c15199cb 100644
--- a/script/assembly/server/assembly-linux-arm64.xml
+++ b/script/assembly/server/assembly-linux-arm64.xml
@@ -78,12 +78,6 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
             </includes>
         </fileSet>
 
-        <!-- Pack the static resources of the web app into the directory-->
-        <fileSet>
-            <directory>../web-app/dist</directory>
-            <outputDirectory>dist</outputDirectory>
-        </fileSet>
-
         <!-- copy jdk -->
         <fileSet>
             <directory>../jdk/linux-arm64</directory>
diff --git a/script/assembly/server/assembly-macos-amd64.xml b/script/assembly/server/assembly-macos-amd64.xml
index 152b67b198..70ba379a49 100644
--- a/script/assembly/server/assembly-macos-amd64.xml
+++ b/script/assembly/server/assembly-macos-amd64.xml
@@ -78,12 +78,6 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
             </includes>
         </fileSet>
 
-        <!-- Pack the static resources of the web app into the directory-->
-        <fileSet>
-            <directory>../web-app/dist</directory>
-            <outputDirectory>dist</outputDirectory>
-        </fileSet>
-        
         <!-- copy jdk -->
         <fileSet>
             <directory>../jdk/macos-amd64</directory>
diff --git a/script/assembly/server/assembly-macos-arm64.xml b/script/assembly/server/assembly-macos-arm64.xml
index 385bfebb9b..907a0e9f20 100644
--- a/script/assembly/server/assembly-macos-arm64.xml
+++ b/script/assembly/server/assembly-macos-arm64.xml
@@ -77,12 +77,6 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
             </includes>
         </fileSet>
 
-        <!-- Pack the static resources of the web app into the directory-->
-        <fileSet>
-            <directory>../web-app/dist</directory>
-            <outputDirectory>dist</outputDirectory>
-        </fileSet>
-        
         <!-- copy jdk -->
         <fileSet>
             <directory>../jdk/macos-arm64</directory>
diff --git a/script/assembly/server/assembly-windows-64.xml b/script/assembly/server/assembly-windows-64.xml
index 94a42b2eb1..2695b7706d 100644
--- a/script/assembly/server/assembly-windows-64.xml
+++ b/script/assembly/server/assembly-windows-64.xml
@@ -78,12 +78,6 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
             </includes>
         </fileSet>
 
-        <!-- Pack the static resources of the web app into the directory-->
-        <fileSet>
-            <directory>../web-app/dist</directory>
-            <outputDirectory>dist</outputDirectory>
-        </fileSet>
-        
         <!-- copy jdk -->
         <fileSet>
             <directory>../jdk/windows-64</directory>
diff --git a/script/assembly/server/assembly.xml b/script/assembly/server/assembly.xml
index 60aad0f744..1326ae0762 100644
--- a/script/assembly/server/assembly.xml
+++ b/script/assembly/server/assembly.xml
@@ -77,12 +77,6 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
             </includes>
         </fileSet>
 
-        <!-- Pack the static resources of the web app into the dist directory-->
-        <fileSet>
-            <directory>../web-app/dist</directory>
-            <outputDirectory>dist</outputDirectory>
-        </fileSet>
-
         <!-- external lib -->
         <fileSet>
             <directory>../script/ext-lib</directory>
diff --git a/script/dev/start-mixed-frontend.sh b/script/dev/start-mixed-frontend.sh
index 1b7bf17ecb..f4b73e864a 100755
--- a/script/dev/start-mixed-frontend.sh
+++ b/script/dev/start-mixed-frontend.sh
@@ -2,11 +2,9 @@
 set -euo pipefail
 
 ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
-WEB_APP_DIR="$ROOT_DIR/web-app"
 WEB_NEXT_DIR="$ROOT_DIR/web-next"
 BACKEND_ORIGIN="${BACKEND_ORIGIN:-http://127.0.0.1:1157}"
 NEXT_PORT="${NEXT_PORT:-4200}"
-ANGULAR_PORT="${ANGULAR_PORT:-4201}"
 NEXT_DIST_DIR="${NEXT_DIST_DIR:-.next}"
 
 cleanup_pid() {
@@ -23,24 +21,15 @@ cleanup_pid() {
 }
 
 cleanup_pid /tmp/hb-next-dev.pid
-cleanup_pid /tmp/hb-angular-dev.pid
 
 cd "$WEB_NEXT_DIR"
 rm -rf "$NEXT_DIST_DIR"
-echo "[mixed-frontend] starting Next.js on :$NEXT_PORT (BACKEND_ORIGIN=$BACKEND_ORIGIN, NEXT_DIST_DIR=$NEXT_DIST_DIR)"
+echo "[web-next] starting Next.js on :$NEXT_PORT (BACKEND_ORIGIN=$BACKEND_ORIGIN, NEXT_DIST_DIR=$NEXT_DIST_DIR)"
 BACKEND_ORIGIN="$BACKEND_ORIGIN" NEXT_DIST_DIR="$NEXT_DIST_DIR" ./node_modules/.bin/next dev -p "$NEXT_PORT" > /tmp/hb-next-dev.log 2>&1 &
 NEXT_PID=$!
 
-cd "$WEB_APP_DIR"
-echo "[mixed-frontend] starting Angular on :$ANGULAR_PORT with proxy.conf.json"
-npm run start -- --port "$ANGULAR_PORT" > /tmp/hb-angular-dev.log 2>&1 &
-ANGULAR_PID=$!
-
 echo "$NEXT_PID" > /tmp/hb-next-dev.pid
-echo "$ANGULAR_PID" > /tmp/hb-angular-dev.pid
 
-echo "[mixed-frontend] Next.js  : http://127.0.0.1:$NEXT_PORT"
-echo "[mixed-frontend] Angular  : http://127.0.0.1:$ANGULAR_PORT"
-echo "[mixed-frontend] proxied via Angular dev server: /trace/manage /log/manage /monitors /ingestion/otlp /ingestion/otlp/metrics"
-echo "[mixed-frontend] logs: /tmp/hb-next-dev.log /tmp/hb-angular-dev.log"
+echo "[web-next] Next.js: http://127.0.0.1:$NEXT_PORT"
+echo "[web-next] logs: /tmp/hb-next-dev.log"
 wait
diff --git a/script/release/release-win.ps1 b/script/release/release-win.ps1
index de9242dae6..1595717b67 100644
--- a/script/release/release-win.ps1
+++ b/script/release/release-win.ps1
@@ -31,17 +31,12 @@ Write-Host "Cleaning up the 'dist' directory..."
 Remove-Item -Recurse -Force $distPath\* -ErrorAction SilentlyContinue
 New-Item -ItemType Directory -Path $distPath -Force
 
-# Clean up the 'web-app/dist' directory
-Write-Host "Cleaning up the 'web-app/dist' directory..."
-$webAppDistPath = "$projectPath\web-app\dist"
-Remove-Item -Recurse -Force $webAppDistPath\* -ErrorAction SilentlyContinue
-New-Item -ItemType Directory -Path $webAppDistPath -Force
 Push-Location -Path $projectPath
 
-Write-Host "Installing dependencies and packaging web-app..."
-Push-Location -Path "web-app"
-yarn install
-yarn package
+Write-Host "Installing dependencies and building web-next..."
+Push-Location -Path "web-next"
+npm ci
+npm run build
 Pop-Location
 
 Write-Host "Building project with Maven..."
diff --git a/web-app/.browserslistrc b/web-app/.browserslistrc
deleted file mode 100644
index 40b5201552..0000000000
--- a/web-app/.browserslistrc
+++ /dev/null
@@ -1,17 +0,0 @@
-# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
-# For additional information regarding the format and rule options, please see:
-# https://github.com/browserslist/browserslist#queries
-
-# For the full list of supported browsers by the Angular framework, please see:
-# https://angular.io/guide/browser-support
-
-# You can see what browsers were selected by your queries by running:
-#   npx browserslist
-
-last 1 Chrome version
-last 1 Firefox version
-last 2 Edge major versions
-# last 2 Safari major versions
-last 2 iOS major versions
-Firefox ESR
-not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
diff --git a/web-app/.editorconfig b/web-app/.editorconfig
deleted file mode 100644
index 59d9a3a3e7..0000000000
--- a/web-app/.editorconfig
+++ /dev/null
@@ -1,16 +0,0 @@
-# Editor configuration, see https://editorconfig.org
-root = true
-
-[*]
-charset = utf-8
-indent_style = space
-indent_size = 2
-insert_final_newline = true
-trim_trailing_whitespace = true
-
-[*.ts]
-quote_type = single
-
-[*.md]
-max_line_length = off
-trim_trailing_whitespace = false
diff --git a/web-app/.eslintignore b/web-app/.eslintignore
deleted file mode 100644
index 42da9d3f2b..0000000000
--- a/web-app/.eslintignore
+++ /dev/null
@@ -1,34 +0,0 @@
-_cli-tpl/
-dist/
-coverage/
-
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# Dependency directories
-node_modules/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-.cache/
-
-# yarn v2
-.yarn
diff --git a/web-app/.eslintrc.js b/web-app/.eslintrc.js
deleted file mode 100644
index 4edcdce465..0000000000
--- a/web-app/.eslintrc.js
+++ /dev/null
@@ -1,132 +0,0 @@
-const prettierConfig = require('./.prettierrc.js');
-
-module.exports = {
-  root: true,
-  parserOptions: { ecmaVersion: 2021 },
-  overrides: [
-    {
-      files: ['*.ts'],
-      parser: '@typescript-eslint/parser',
-      parserOptions: {
-        tsconfigRootDir: __dirname,
-        project: ['tsconfig.json'],
-        createDefaultProgram: true
-      },
-      plugins: ['@typescript-eslint', 'jsdoc', 'import'],
-      extends: [
-        'plugin:@angular-eslint/recommended',
-        'plugin:@angular-eslint/template/process-inline-templates',
-        'plugin:prettier/recommended'
-      ],
-      rules: {
-        'prettier/prettier': ['error', prettierConfig],
-        'jsdoc/tag-lines': [
-          'error',
-          'any',
-          {
-            startLines: 1,
-          },
-        ],
-        '@angular-eslint/component-class-suffix': [
-          'error',
-          {
-            suffixes: ['Directive', 'Component', 'Base', 'Widget']
-          }
-        ],
-        '@angular-eslint/directive-class-suffix': [
-          'error',
-          {
-            suffixes: ['Directive', 'Component', 'Base', 'Widget']
-          }
-        ],
-        '@angular-eslint/component-selector': [
-          'off',
-          {
-            type: ['element', 'attribute'],
-            prefix: ['app', 'test'],
-            style: 'kebab-case'
-          }
-        ],
-        '@angular-eslint/directive-selector': [
-          'off',
-          {
-            type: 'attribute',
-            prefix: ['app']
-          }
-        ],
-        '@angular-eslint/no-attribute-decorator': 'error',
-        '@angular-eslint/no-conflicting-lifecycle': 'off',
-        '@angular-eslint/no-forward-ref': 'off',
-        '@angular-eslint/no-host-metadata-property': 'off',
-        '@angular-eslint/no-lifecycle-call': 'off',
-        '@angular-eslint/no-pipe-impure': 'error',
-        '@angular-eslint/prefer-output-readonly': 'error',
-        '@angular-eslint/use-component-selector': 'off',
-        '@angular-eslint/use-component-view-encapsulation': 'off',
-        '@angular-eslint/no-input-rename': 'off',
-        '@angular-eslint/no-output-native': 'off',
-        '@typescript-eslint/array-type': [
-          'error',
-          {
-            default: 'array-simple'
-          }
-        ],
-        '@typescript-eslint/ban-types': [
-          'off',
-          {
-            types: {
-              String: {
-                message: 'Use string instead.'
-              },
-              Number: {
-                message: 'Use number instead.'
-              },
-              Boolean: {
-                message: 'Use boolean instead.'
-              },
-              Function: {
-                message: 'Use specific callable interface instead.'
-              }
-            }
-          }
-        ],
-        'import/no-duplicates': 'error',
-        'import/no-unused-modules': 'error',
-        'import/no-unassigned-import': 'error',
-        'import/order': [
-          'error',
-          {
-            alphabetize: { order: 'asc', caseInsensitive: false },
-            'newlines-between': 'always',
-            groups: ['external', 'internal', ['parent', 'sibling', 'index']],
-            pathGroups: [],
-            pathGroupsExcludedImportTypes: []
-          }
-        ],
-        '@typescript-eslint/no-this-alias': 'error',
-        '@typescript-eslint/member-ordering': 'off',
-        'no-irregular-whitespace': 'error',
-        'no-multiple-empty-lines': 'error',
-        'no-sparse-arrays': 'error',
-        'prefer-object-spread': 'error',
-        'prefer-template': 'error',
-        'prefer-const': 'off',
-        'max-len': 'off'
-      }
-    },
-    {
-      files: ['*.html'],
-      extends: ['plugin:@angular-eslint/template/recommended'],
-      rules: {}
-    },
-    {
-      files: ['*.html'],
-      excludedFiles: ['*inline-template-*.component.html'],
-      extends: ['plugin:prettier/recommended'],
-      rules: {
-        'prettier/prettier': ['error', { parser: 'angular' }],
-        '@angular-eslint/template/eqeqeq': 'off'
-      }
-    }
-  ]
-};
diff --git a/web-app/.gitignore b/web-app/.gitignore
deleted file mode 100644
index 2808cce893..0000000000
--- a/web-app/.gitignore
+++ /dev/null
@@ -1,46 +0,0 @@
-# See http://help.github.com/ignore-files/ for more about ignoring files.
-
-# compiled output
-/dist
-/tmp
-/out-tsc
-# Only exists if Bazel was run
-/bazel-out
-
-# dependencies
-/node_modules
-.angular/*
-
-# profiling files
-chrome-profiler-events*.json
-
-# IDEs and editors
-/.idea
-.project
-.classpath
-.c9/
-*.launch
-.settings/
-*.sublime-workspace
-
-# IDE - VSCode
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-.history/*
-
-# misc
-/.sass-cache
-/connect.lock
-/coverage
-/libpeerconnection.log
-npm-debug.log
-yarn-error.log
-testem.log
-/typings
-
-# System Files
-.DS_Store
-Thumbs.db
diff --git a/web-app/.prettierignore b/web-app/.prettierignore
deleted file mode 100644
index d250447cc9..0000000000
--- a/web-app/.prettierignore
+++ /dev/null
@@ -1,18 +0,0 @@
-# add files you wish to ignore here
-**/*.md
-**/*.svg
-**/test.ts
-
-.stylelintrc
-.prettierrc
-
-src/assets/*
-src/index.html
-node_modules/
-.vscode/
-coverage/
-dist/
-package.json
-tslint.json
-
-_cli-tpl/**/*
diff --git a/web-app/.prettierrc.js b/web-app/.prettierrc.js
deleted file mode 100644
index 26ba9009b8..0000000000
--- a/web-app/.prettierrc.js
+++ /dev/null
@@ -1,13 +0,0 @@
-module.exports = {
-  singleQuote: true,
-  useTabs: false,
-  printWidth: 140,
-  tabWidth: 2,
-  semi: true,
-  htmlWhitespaceSensitivity: 'strict',
-  arrowParens: 'avoid',
-  bracketSpacing: true,
-  proseWrap: 'preserve',
-  trailingComma: 'none',
-  endOfLine: 'lf'
-};
diff --git a/web-app/.stylelintrc b/web-app/.stylelintrc
deleted file mode 100644
index ce01946ae7..0000000000
--- a/web-app/.stylelintrc
+++ /dev/null
@@ -1,37 +0,0 @@
-{
-  "extends": [
-    "stylelint-config-standard",
-    "stylelint-config-rational-order",
-    "stylelint-config-prettier"
-  ],
-  "plugins": [
-    "stylelint-order",
-    "stylelint-declaration-block-no-ignored-properties"
-  ],
-  "rules": {
-    "no-descending-specificity": null,
-    "plugin/declaration-block-no-ignored-properties": true,
-    "selector-type-no-unknown": [
-      true,
-      {
-        "ignoreTypes": [
-          "/^g2-/",
-          "/^nz-/",
-          "/^app-/"
-        ]
-      }
-    ],
-    "selector-pseudo-element-no-unknown": [
-      true,
-      {
-        "ignorePseudoElements": [
-          "ng-deep"
-        ]
-      }
-    ],
-    "import-notation": "string"
-  },
-  "ignoreFiles": [
-    "src/assets/**/*"
-  ]
-}
\ No newline at end of file
diff --git a/web-app/README.md b/web-app/README.md
deleted file mode 100644
index a882a22460..0000000000
--- a/web-app/README.md
+++ /dev/null
@@ -1,30 +0,0 @@
-## Hertzbeat Web-App        
-
-> [!NOTE]
->
-> HertzBeat Web-App is a fork to [ng-alain](https://github.com/ng-alain/ng-alain/). Check [LICENSE](/LICENSE) and [license-ng-alain.txt](/material/licenses/frontend/LICENSE-ng-alain.txt) for more details.
-
-
-### Quickly Start   
-
-1. Need `Node Pnpm` Environment, Make sure `Node.js >= 18`
-2. Install yarn if not existed `npm install -g pnpm`
-3. Execute `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
-4. Start After Backend Server Available : `pnpm start`
-
-
-### Build HertzBeat Install Package    
-
-1. Execute command in web-app  
-
-    ```pnpm package```
-
-2. Execute command in root  
-
-    ```mvn clean install```
-
-    The HertzBeat install package will at `manager/target/hertzbeat-{version}.tar.gz`     
-
-3. Execute command in collector  
-
-    ```mvn clean package -Pcluster```
diff --git a/web-app/angular.json b/web-app/angular.json
deleted file mode 100644
index 149fdd4823..0000000000
--- a/web-app/angular.json
+++ /dev/null
@@ -1,181 +0,0 @@
-{
-  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
-  "version": 1,
-  "newProjectRoot": "projects",
-  "projects": {
-    "hertzbeat-web-app": {
-      "projectType": "application",
-      "root": "",
-      "sourceRoot": "src",
-      "prefix": "app",
-      "schematics": {
-        "@schematics/angular:component": {
-          "style": "less"
-        },
-        "@schematics/angular:application": {
-          "strict": true
-        }
-      },
-      "architect": {
-        "build": {
-          "builder": "@angular-devkit/build-angular:browser",
-          "options": {
-            "preserveSymlinks": true,
-            "outputPath": "dist",
-            "index": "src/index.html",
-            "main": "src/main.ts",
-            "tsConfig": "tsconfig.app.json",
-            "polyfills": "src/polyfills.ts",
-            "assets": [
-              "src/favicon.ico",
-              "src/assets",
-              {
-                "glob": "**/*",
-                "input": "src/assets/audio",
-                "output": "/assets/audio"
-              },
-              {
-                "glob": "**/*",
-                "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
-                "output": "/assets/"
-              },
-              {
-                "glob": "**/*",
-                "input": "node_modules/monaco-editor/min/vs",
-                "output": "/assets/vs/"
-              }
-            ],
-            "styles": [
-              "src/styles.less",
-              "node_modules/slick-carousel/slick/slick.scss",
-              "node_modules/slick-carousel/slick/slick-theme.scss"
-            ],
-            "scripts": [
-              "node_modules/jquery/dist/jquery.min.js",
-              "node_modules/slick-carousel/slick/slick.min.js"
-            ],
-            "allowedCommonJsDependencies": [
-              "ajv",
-              "ajv-formats",
-              "mockjs",
-              "date-fns",
-              "file-saver",
-              "extend"
-            ],
-            "stylePreprocessorOptions": {
-              "includePaths": [
-                "node_modules/"
-              ]
-            }
-          },
-          "configurations": {
-            "production": {
-              "extractLicenses": false,
-              "fileReplacements": [
-                {
-                  "replace": "src/environments/environment.ts",
-                  "with": "src/environments/environment.prod.ts"
-                }
-              ],
-              "outputHashing": "all",
-              "budgets": [
-                {
-                  "type": "initial",
-                  "maximumWarning": "2mb",
-                  "maximumError": "6mb"
-                },
-                {
-                  "type": "anyComponentStyle",
-                  "maximumWarning": "50kb",
-                  "maximumError": "100kb"
-                }
-              ]
-            },
-            "development": {
-              "buildOptimizer": false,
-              "optimization": false,
-              "vendorChunk": true,
-              "extractLicenses": false,
-              "sourceMap": true,
-              "namedChunks": true
-            }
-          },
-          "defaultConfiguration": "production"
-        },
-        "serve": {
-          "builder": "@angular-devkit/build-angular:dev-server",
-          "options": {
-            "proxyConfig": "proxy.conf.json",
-            "buildTarget": "hertzbeat-web-app:build"
-          },
-          "configurations": {
-            "production": {
-              "buildTarget": "hertzbeat-web-app:build:production"
-            },
-            "development": {
-              "buildTarget": "hertzbeat-web-app:build:development"
-            }
-          },
-          "defaultConfiguration": "development"
-        },
-        "extract-i18n": {
-          "builder": "@angular-devkit/build-angular:extract-i18n",
-          "options": {
-            "buildTarget": "hertzbeat-web-app:build"
-          }
-        },
-        "test": {
-          "builder": "@angular-devkit/build-angular:karma",
-          "options": {
-            "main": "src/test.ts",
-            "polyfills": "src/polyfills.ts",
-            "karmaConfig": "karma.conf.js",
-            "tsConfig": "tsconfig.spec.json",
-            "scripts": [],
-            "styles": [],
-            "assets": [
-              "src/assets"
-            ]
-          }
-        },
-        "lint": {
-          "builder": "@angular-eslint/builder:lint",
-          "options": {
-            "lintFilePatterns": [
-              "src/**/*.ts",
-              "src/**/*.html"
-            ]
-          }
-        },
-        "e2e": {
-          "builder": "@angular-devkit/build-angular:protractor",
-          "options": {
-            "protractorConfig": "e2e/protractor.conf.js",
-            "devServerTarget": "hertzbeat-web-app:serve"
-          },
-          "configurations": {
-            "production": {
-              "devServerTarget": "hertzbeat-web-app:serve:production"
-            }
-          }
-        }
-      }
-    }
-  },
-  "cli": {
-    "packageManager": "yarn",
-    "schematicCollections": [
-      "@schematics/angular",
-      "hertzbeat-web-app"
-    ],
-    "analytics": false
-  },
-  "schematics": {
-    "@angular-eslint/schematics:application": {
-      "setParserOptionsProject": true
-    },
-    "@angular-eslint/schematics:library": {
-      "setParserOptionsProject": true
-    }
-  }
-}
diff --git a/web-app/karma.conf.js b/web-app/karma.conf.js
deleted file mode 100644
index 78f65e57f0..0000000000
--- a/web-app/karma.conf.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// Karma configuration file, see link for more information
-// https://karma-runner.github.io/1.0/config/configuration-file.html
-
-module.exports = function (config) {
-  config.set({
-    basePath: '',
-    frameworks: ['jasmine', '@angular-devkit/build-angular'],
-    plugins: [
-      require('karma-jasmine'),
-      require('karma-chrome-launcher'),
-      require('karma-jasmine-html-reporter'),
-      require('karma-coverage'),
-      require('@angular-devkit/build-angular/plugins/karma')
-    ],
-    client: {
-      jasmine: {
-        // you can add configuration options for Jasmine here
-        // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
-        // for example, you can disable the random execution with `random: false`
-        // or set a specific seed with `seed: 4321`
-      },
-      clearContext: false // leave Jasmine Spec Runner output visible in browser
-    },
-    jasmineHtmlReporter: {
-      suppressAll: true // removes the duplicated traces
-    },
-    coverageReporter: {
-      dir: require('path').join(__dirname, './coverage/web-app'),
-      subdir: '.',
-      reporters: [
-        { type: 'html' },
-        { type: 'text-summary' }
-      ]
-    },
-    reporters: ['progress', 'kjhtml'],
-    port: 9876,
-    colors: true,
-    logLevel: config.LOG_INFO,
-    autoWatch: true,
-    browsers: ['Chrome'],
-    singleRun: false,
-    restartOnFileChange: true
-  });
-};
diff --git a/web-app/ng-alain.json b/web-app/ng-alain.json
deleted file mode 100644
index ff78a68b9a..0000000000
--- a/web-app/ng-alain.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "$schema": "./node_modules/ng-alain/schema.json",
-  "theme": {
-    "list": [
-      {
-        "theme": "dark"
-      },
-      {
-        "theme": "compact"
-      }
-    ]
-  }
-}
diff --git a/web-app/package.json b/web-app/package.json
deleted file mode 100644
index d750e6bcb3..0000000000
--- a/web-app/package.json
+++ /dev/null
@@ -1,147 +0,0 @@
-{
-  "name": "hertzbeat-web-app",
-  "version": "1.0.0",
-  "description": "A real-time monitoring tool with custom-monitor and agentLess.",
-  "author": "hertzbeat community",
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/apache/hertzbeat.git"
-  },
-  "bugs": {
-    "url": "https://github.com/apache/hertzbeat/issues"
-  },
-  "homepage": "https://hertzbeat.apache.org",
-  "license": "Apache2.0",
-  "keywords": [
-    "monitor",
-    "uptime",
-    "monitoring",
-    "angular"
-  ],
-  "scripts": {
-    "ng": "ng",
-    "start": "ng serve --proxy-config proxy.conf.json",
-    "build": "npm run ng-high-memory build",
-    "watch": "ng build --watch --configuration development",
-    "test": "ng test",
-    "ng-high-memory": "node --max_old_space_size=8000 ./node_modules/@angular/cli/bin/ng",
-    "hmr": "ng s -o --hmr",
-    "analyze": "npm run ng-high-memory build -- --source-map",
-    "analyze:view": "source-map-explorer dist/**/*.js",
-    "test-coverage": "ng test --code-coverage --watch=false",
-    "color-less": "ng-alain-plugin-theme -t=colorLess",
-    "theme": "ng-alain-plugin-theme -t=themeCss",
-    "icon": "ng g ng-alain:plugin icon",
-    "lint": "npm run lint:fix && npm run lint:style",
-    "lint:ts": "ng lint",
-    "lint:fix": "ng lint --fix",
-    "lint:style": "stylelint \"src/**/*.less\" --syntax less --fix",
-    "package": "ng build --configuration production"
-  },
-  "private": true,
-  "dependencies": {
-    "@angular/animations": "17.3.10",
-    "@angular/cdk": "17.3.10",
-    "@angular/common": "17.3.10",
-    "@angular/compiler": "17.3.10",
-    "@angular/core": "17.3.10",
-    "@angular/forms": "17.3.10",
-    "@angular/platform-browser": "17.3.10",
-    "@angular/platform-browser-dynamic": "17.3.10",
-    "@angular/router": "17.3.10",
-    "@ant-design/colors": "^7.2.1",
-    "@ant-design/fast-color": "^3.0.0",
-    "@ant-design/icons-angular": "19.0.0",
-    "@ctrl/tinycolor": "^4.2.0",
-    "@delon/abc": "17.0.5",
-    "@delon/acl": "17.3.1",
-    "@delon/auth": "17.3.1",
-    "@delon/cache": "17.3.1",
-    "@delon/form": "17.3.1",
-    "@delon/mock": "17.3.1",
-    "@delon/theme": "17.3.1",
-    "@delon/util": "17.3.1",
-    "@kerwin612/ngx-query-builder": "0.6.4",
-    "ajv": "8.12.0",
-    "ajv-formats": "2.1.1",
-    "angular-tag-cloud-module": "17.0.1",
-    "date-fns": "2.30.0",
-    "@babel/runtime": "7.28.4",
-    "echarts": "5.4.3",
-    "extend": "^3.0.2",
-    "fast-deep-equal": "^3.1.3",
-    "isutf8": "^4.0.1",
-    "jquery": "3.7.1",
-    "json-schema-traverse": "^1.0.0",
-    "lodash-es": "^4.17.21",
-    "marked": "15.0.6",
-    "monaco-editor": "0.36.1",
-    "ng-in-viewport": "^16.1.0",
-    "ng-zorro-antd": "17.4.1",
-    "ngx-color-picker": "16.0.0",
-    "ngx-echarts": "17.2.0",
-    "ngx-markdown": "19.0.0",
-    "ngx-slick-carousel": "17.0.0",
-    "rxjs": "7.8.1",
-    "screenfull": "6.0.2",
-    "slick-carousel": "1.8.1",
-    "tslib": "2.6.2",
-    "uri-js": "^4.4.1",
-    "zone.js": "0.14.5",
-    "zrender": "^6.0.0"
-  },
-  "devDependencies": {
-    "@angular-devkit/build-angular": "17.3.8",
-    "@angular-eslint/builder": "17.5.2",
-    "@angular-eslint/eslint-plugin": "17.5.2",
-    "@angular-eslint/eslint-plugin-template": "17.5.2",
-    "@angular-eslint/schematics": "17.5.2",
-    "@angular-eslint/template-parser": "17.5.2",
-    "@angular/cli": "17.3.8",
-    "@angular/compiler-cli": "17.3.10",
-    "@angular/language-service": "17.3.10",
-    "@delon/testing": "17.3.1",
-    "@types/jasmine": "5.1.4",
-    "@types/jasminewd2": "2.0.13",
-    "@types/node": "20.12.11",
-    "@typescript-eslint/eslint-plugin": "7.8.0",
-    "@typescript-eslint/parser": "7.8.0",
-    "eslint": "8.56.0",
-    "eslint-config-prettier": "8.6.0",
-    "eslint-plugin-deprecation": "^3.0.0",
-    "eslint-plugin-import": "2.26.0",
-    "eslint-plugin-jsdoc": "48.2.5",
-    "eslint-plugin-prefer-arrow": "1.2.3",
-    "eslint-plugin-prettier": "4.2.1",
-    "husky": "7.0.4",
-    "jasmine-core": "4.3.0",
-    "jasmine-spec-reporter": "7.0.0",
-    "karma": "6.4.2",
-    "karma-chrome-launcher": "3.1.1",
-    "karma-coverage": "2.2.1",
-    "karma-jasmine": "5.1.0",
-    "karma-jasmine-html-reporter": "2.0.0",
-    "lint-staged": "13.3.0",
-    "ng-alain": "17.3.1",
-    "node-fetch": "2.7.0",
-    "prettier": "2.8.8",
-    "protractor": "7.0.0",
-    "source-map-explorer": "2.5.3",
-    "stylelint": "14.16.1",
-    "stylelint-config-prettier": "9.0.5",
-    "stylelint-config-rational-order": "0.1.2",
-    "stylelint-config-standard": "28.0.0",
-    "stylelint-declaration-block-no-ignored-properties": "2.7.0",
-    "stylelint-order": "5.0.0",
-    "ts-node": "10.9.1",
-    "typescript": "5.3.3"
-  },
-  "lint-staged": {
-    "(src)/**/*.{html,ts}": [
-      "eslint --fix"
-    ],
-    "(src)/**/*.less": [
-      "npm run lint:style"
-    ]
-  }
-}
diff --git a/web-app/pnpm-lock.yaml b/web-app/pnpm-lock.yaml
deleted file mode 100644
index 4b394e39ad..0000000000
--- a/web-app/pnpm-lock.yaml
+++ /dev/null
@@ -1,16592 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
-  autoInstallPeers: true
-  excludeLinksFromLockfile: false
-
-importers:
-
-  .:
-    dependencies:
-      '@angular/animations':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))
-      '@angular/cdk':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1)
-      '@angular/common':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1)
-      '@angular/compiler':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))
-      '@angular/core':
-        specifier: 17.3.10
-        version: 17.3.10(rxjs@7.8.1)(zone.js@0.14.5)
-      '@angular/forms':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/platform-browser@17.3.10(@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(rxjs@7.8.1)
-      '@angular/platform-browser':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))
-      '@angular/platform-browser-dynamic':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/platform-browser@17.3.10(@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))
-      '@angular/router':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/platform-browser@17.3.10(@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(rxjs@7.8.1)
-      '@ant-design/colors':
-        specifier: ^7.2.1
-        version: 7.2.1
-      '@ant-design/fast-color':
-        specifier: ^3.0.0
-        version: 3.0.0
-      '@ant-design/icons-angular':
-        specifier: 19.0.0
-        version: 19.0.0(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/platform-browser@17.3.10(@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(rxjs@7.8.1)
-      '@babel/runtime':
-        specifier: 7.28.4
-        version: 7.28.4
-      '@ctrl/tinycolor':
-        specifier: ^4.2.0
-        version: 4.2.0
-      '@delon/abc':
-        specifier: 17.0.5
-        version: 17.0.5(3a74f0e17f1ee83cf0178f42d12d00d9)
-      '@delon/acl':
-        specifier: 17.3.1
-        version: 17.3.1
-      '@delon/auth':
-        specifier: 17.3.1
-        version: 17.3.1
-      '@delon/cache':
-        specifier: 17.3.1
-        version: 17.3.1
-      '@delon/form':
-        specifier: 17.3.1
-        version: 17.3.1(3a74f0e17f1ee83cf0178f42d12d00d9)
-      '@delon/mock':
-        specifier: 17.3.1
-        version: 17.3.1
-      '@delon/theme':
-        specifier: 17.3.1
-        version: 17.3.1(3a74f0e17f1ee83cf0178f42d12d00d9)
-      '@delon/util':
-        specifier: 17.3.1
-        version: 17.3.1
-      '@kerwin612/ngx-query-builder':
-        specifier: 0.6.4
-        version: 0.6.4(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))
-      ajv:
-        specifier: 8.12.0
-        version: 8.12.0
-      ajv-formats:
-        specifier: 2.1.1
-        version: 2.1.1(ajv@8.12.0)
-      angular-tag-cloud-module:
-        specifier: 17.0.1
-        version: 17.0.1(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))
-      date-fns:
-        specifier: 2.30.0
-        version: 2.30.0
-      echarts:
-        specifier: 5.4.3
-        version: 5.4.3
-      extend:
-        specifier: ^3.0.2
-        version: 3.0.2
-      fast-deep-equal:
-        specifier: ^3.1.3
-        version: 3.1.3
-      isutf8:
-        specifier: ^4.0.1
-        version: 4.0.1
-      jquery:
-        specifier: 3.7.1
-        version: 3.7.1
-      json-schema-traverse:
-        specifier: ^1.0.0
-        version: 1.0.0
-      lodash-es:
-        specifier: ^4.17.21
-        version: 4.17.22
-      marked:
-        specifier: 15.0.6
-        version: 15.0.6
-      monaco-editor:
-        specifier: 0.36.1
-        version: 0.36.1
-      ng-in-viewport:
-        specifier: ^16.1.0
-        version: 16.1.0(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))
-      ng-zorro-antd:
-        specifier: 17.4.1
-        version: 17.4.1(3a74f0e17f1ee83cf0178f42d12d00d9)
-      ngx-color-picker:
-        specifier: 16.0.0
-        version: 16.0.0(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/forms@17.3.10(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/platform-browser@17.3.10(@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7. [...]
-      ngx-echarts:
-        specifier: 17.2.0
-        version: 17.2.0(echarts@5.4.3)
-      ngx-markdown:
-        specifier: 19.0.0
-        version: 19.0.0(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/platform-browser@17.3.10(@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(marked@15.0.6)(rxjs@7.8.1)(zone.js@0.14.5)
-      ngx-slick-carousel:
-        specifier: 17.0.0
-        version: 17.0.0(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/forms@17.3.10(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(@angular/platform-browser@17.3.10(@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7. [...]
-      rxjs:
-        specifier: 7.8.1
-        version: 7.8.1
-      screenfull:
-        specifier: 6.0.2
-        version: 6.0.2
-      slick-carousel:
-        specifier: 1.8.1
-        version: 1.8.1(jquery@3.7.1)
-      tslib:
-        specifier: 2.6.2
-        version: 2.6.2
-      uri-js:
-        specifier: ^4.4.1
-        version: 4.4.1
-      zone.js:
-        specifier: 0.14.5
-        version: 0.14.5
-      zrender:
-        specifier: ^6.0.0
-        version: 6.0.0
-    devDependencies:
-      '@angular-devkit/build-angular':
-        specifier: 17.3.8
-        version: 17.3.8(@angular/compiler-cli@17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3))(@types/express@4.17.25)(@types/node@20.12.11)(chokidar@3.6.0)(karma@6.4.2)(protractor@7.0.0)(sugarss@2.0.0)(typescript@5.3.3)
-      '@angular-eslint/builder':
-        specifier: 17.5.2
-        version: 17.5.2(eslint@8.56.0)(typescript@5.3.3)
-      '@angular-eslint/eslint-plugin':
-        specifier: 17.5.2
-        version: 17.5.2(eslint@8.56.0)(typescript@5.3.3)
-      '@angular-eslint/eslint-plugin-template':
-        specifier: 17.5.2
-        version: 17.5.2(eslint@8.56.0)(typescript@5.3.3)
-      '@angular-eslint/schematics':
-        specifier: 17.5.2
-        version: 17.5.2(@angular/cli@17.3.8(chokidar@3.6.0))(eslint@8.56.0)(typescript@5.3.3)
-      '@angular-eslint/template-parser':
-        specifier: 17.5.2
-        version: 17.5.2(eslint@8.56.0)(typescript@5.3.3)
-      '@angular/cli':
-        specifier: 17.3.8
-        version: 17.3.8(chokidar@3.6.0)
-      '@angular/compiler-cli':
-        specifier: 17.3.10
-        version: 17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3)
-      '@angular/language-service':
-        specifier: 17.3.10
-        version: 17.3.10
-      '@delon/testing':
-        specifier: 17.3.1
-        version: 17.3.1
-      '@types/jasmine':
-        specifier: 5.1.4
-        version: 5.1.4
-      '@types/jasminewd2':
-        specifier: 2.0.13
-        version: 2.0.13
-      '@types/node':
-        specifier: 20.12.11
-        version: 20.12.11
-      '@typescript-eslint/eslint-plugin':
-        specifier: 7.8.0
-        version: 7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
-      '@typescript-eslint/parser':
-        specifier: 7.8.0
-        version: 7.8.0(eslint@8.56.0)(typescript@5.3.3)
-      eslint:
-        specifier: 8.56.0
-        version: 8.56.0
-      eslint-config-prettier:
-        specifier: 8.6.0
-        version: 8.6.0(eslint@8.56.0)
-      eslint-plugin-deprecation:
-        specifier: ^3.0.0
-        version: 3.0.0(eslint@8.56.0)(typescript@5.3.3)
-      eslint-plugin-import:
-        specifier: 2.26.0
-        version: 2.26.0(@typescript-eslint/parser@7.8.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)
-      eslint-plugin-jsdoc:
-        specifier: 48.2.5
-        version: 48.2.5(eslint@8.56.0)
-      eslint-plugin-prefer-arrow:
-        specifier: 1.2.3
-        version: 1.2.3(eslint@8.56.0)
-      eslint-plugin-prettier:
-        specifier: 4.2.1
-        version: 4.2.1(eslint-config-prettier@8.6.0(eslint@8.56.0))(eslint@8.56.0)(prettier@2.8.8)
-      husky:
-        specifier: 7.0.4
-        version: 7.0.4
-      jasmine-core:
-        specifier: 4.3.0
-        version: 4.3.0
-      jasmine-spec-reporter:
-        specifier: 7.0.0
-        version: 7.0.0
-      karma:
-        specifier: 6.4.2
-        version: 6.4.2
-      karma-chrome-launcher:
-        specifier: 3.1.1
-        version: 3.1.1
-      karma-coverage:
-        specifier: 2.2.1
-        version: 2.2.1
-      karma-jasmine:
-        specifier: 5.1.0
-        version: 5.1.0(karma@6.4.2)
-      karma-jasmine-html-reporter:
-        specifier: 2.0.0
-        version: 2.0.0(jasmine-core@4.3.0)(karma-jasmine@5.1.0(karma@6.4.2))(karma@6.4.2)
-      lint-staged:
-        specifier: 13.3.0
-        version: 13.3.0(enquirer@2.3.6)
-      ng-alain:
-        specifier: 17.3.1
-        version: 17.3.1(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(encoding@0.1.13)(rxjs@7.8.1)
-      node-fetch:
-        specifier: 2.7.0
-        version: 2.7.0(encoding@0.1.13)
-      prettier:
-        specifier: 2.8.8
-        version: 2.8.8
-      protractor:
-        specifier: 7.0.0
-        version: 7.0.0
-      source-map-explorer:
-        specifier: 2.5.3
-        version: 2.5.3
-      stylelint:
-        specifier: 14.16.1
-        version: 14.16.1
-      stylelint-config-prettier:
-        specifier: 9.0.5
-        version: 9.0.5(stylelint@14.16.1)
-      stylelint-config-rational-order:
-        specifier: 0.1.2
-        version: 0.1.2
-      stylelint-config-standard:
-        specifier: 28.0.0
-        version: 28.0.0(stylelint@14.16.1)
-      stylelint-declaration-block-no-ignored-properties:
-        specifier: 2.7.0
-        version: 2.7.0(stylelint@14.16.1)
-      stylelint-order:
-        specifier: 5.0.0
-        version: 5.0.0(stylelint@14.16.1)
-      ts-node:
-        specifier: 10.9.1
-        version: 10.9.1(@types/node@20.12.11)(typescript@5.3.3)
-      typescript:
-        specifier: 5.3.3
-        version: 5.3.3
-
-packages:
-
-  '@ampproject/remapping@2.3.0':
-    resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
-    engines: {node: '>=6.0.0'}
-
-  '@angular-devkit/architect@0.1703.8':
-    resolution: {integrity: sha512-lKxwG4/QABXZvJpqeSIn/kAwnY6MM9HdHZUV+o5o3UiTi+vO8rZApG4CCaITH3Bxebm7Nam7Xbk8RuukC5rq6g==}
-    engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
-
-  '@angular-devkit/build-angular@17.3.8':
-    resolution: {integrity: sha512-ixsdXggWaFRP7Jvxd0AMukImnePuGflT9Yy7NJ9/y0cL/k//S/3RnkQv5i411KzN+7D4RIbNkRGGTYeqH24zlg==}
-    engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
-    peerDependencies:
-      '@angular/compiler-cli': ^17.0.0
-      '@angular/localize': ^17.0.0
-      '@angular/platform-server': ^17.0.0
-      '@angular/service-worker': ^17.0.0
-      '@web/test-runner': ^0.18.0
-      browser-sync: ^3.0.2
-      jest: ^29.5.0
-      jest-environment-jsdom: ^29.5.0
-      karma: ^6.3.0
-      ng-packagr: ^17.0.0
-      protractor: ^7.0.0
-      tailwindcss: ^2.0.0 || ^3.0.0
-      typescript: '>=5.2 <5.5'
-    peerDependenciesMeta:
-      '@angular/localize':
-        optional: true
-      '@angular/platform-server':
-        optional: true
-      '@angular/service-worker':
-        optional: true
-      '@web/test-runner':
-        optional: true
-      browser-sync:
-        optional: true
-      jest:
-        optional: true
-      jest-environment-jsdom:
-        optional: true
-      karma:
-        optional: true
-      ng-packagr:
-        optional: true
-      protractor:
-        optional: true
-      tailwindcss:
-        optional: true
-
-  '@angular-devkit/build-webpack@0.1703.8':
-    resolution: {integrity: sha512-9u6fl8VVOxcLOEMzrUeaybSvi9hSLSRucHnybneYrabsgreDo32tuy/4G8p6YAHQjpWEj9jvF9Um13ertdni5Q==}
-    engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
-    peerDependencies:
-      webpack: ^5.30.0
-      webpack-dev-server: ^4.0.0
-
-  '@angular-devkit/core@17.3.8':
-    resolution: {integrity: sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q==}
-    engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
-    peerDependencies:
-      chokidar: ^3.5.2
-    peerDependenciesMeta:
-      chokidar:
-        optional: true
-
-  '@angular-devkit/schematics@17.3.8':
-    resolution: {integrity: sha512-QRVEYpIfgkprNHc916JlPuNbLzOgrm9DZalHasnLUz4P6g7pR21olb8YCyM2OTJjombNhya9ZpckcADU5Qyvlg==}
-    engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
-
-  '@angular-eslint/builder@17.5.2':
-    resolution: {integrity: sha512-bvegxJuocWeHdvISPfCXeLQPSjrMCEVzxXPg16JJKksKWSeRA1JnbXnfnb7eoLdq1+riMHKqbH6Fs4rORAvUiA==}
-    peerDependencies:
-      eslint: ^7.20.0 || ^8.0.0
-      typescript: '*'
-
-  '@angular-eslint/bundled-angular-compiler@17.5.2':
-    resolution: {integrity: sha512-K4hVnMyI98faMJmsA4EOBkD0tapDjWV5gy0j/wJ2uSL46d3JgZPZNJSO1zStf/b3kT4gLOlQ/ulWFiUf1DxgIw==}
-
-  '@angular-eslint/eslint-plugin-template@17.5.2':
-    resolution: {integrity: sha512-6sxVwrJ7yElZxcjxHSA0Ujs29bCD/cMubd9n6TDFI9e3v+ktpoMW4Nv/TCHv0OuYatIOZ7bcJxi38cAO8Vhfhw==}
-    peerDependencies:
-      eslint: ^7.20.0 || ^8.0.0
-      typescript: '*'
-
-  '@angular-eslint/eslint-plugin@17.5.2':
-    resolution: {integrity: sha512-kzPALKyT5XIEbgoNmY/hEZWdMWKTX56Pap9fVLJSC/b+Nd+MXc7TNly2s0XoC0Ru1/kMiVzbmSGPheu/rw+9aA==}
-    peerDependencies:
-      eslint: ^7.20.0 || ^8.0.0
-      typescript: '*'
-
-  '@angular-eslint/schematics@17.5.2':
-    resolution: {integrity: sha512-HcvqrBEJfYMTc+fZ6YdRkb+9YcXsy2XSv59Yhd0bBqZ4ZdM4QuuVFxWlLNvhV6TF1DtO24CzpN4OyE+AO5EWBA==}
-    peerDependencies:
-      '@angular/cli': '>= 17.0.0 < 18.0.0'
-
-  '@angular-eslint/template-parser@17.5.2':
-    resolution: {integrity: sha512-46emLElmnIUzW0bpEpSf0u05ofRVUwlfttDOMLedhi700peUKbB9Y6iyz3GzAtQCMklBbJC9nR87LQRH9aSlog==}
-    peerDependencies:
-      eslint: ^7.20.0 || ^8.0.0
-      typescript: '*'
-
-  '@angular-eslint/utils@17.5.2':
-    resolution: {integrity: sha512-bTMPFqtoetBJsYR/xqREEOCy/CdsKGf2gZkRdH73gG6pOpskWt8J/PbRcMZsC349paV4HFixByVm89inqA0TNg==}
-    peerDependencies:
-      eslint: ^7.20.0 || ^8.0.0
-      typescript: '*'
-
-  '@angular/animations@17.3.10':
-    resolution: {integrity: sha512-9fR5snTuG4aM2K54TG/6DXcKXMDKZMovZhjQOxO8l68/oqn6fKrHs8DLzckFs0XGRZ+2OyURH8WggFm1Z828rA==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    peerDependencies:
-      '@angular/core': 17.3.10
-
-  '@angular/cdk@17.3.10':
-    resolution: {integrity: sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==}
-    peerDependencies:
-      '@angular/common': ^17.0.0 || ^18.0.0
-      '@angular/core': ^17.0.0 || ^18.0.0
-      rxjs: ^6.5.3 || ^7.4.0
-
-  '@angular/cli@17.3.8':
-    resolution: {integrity: sha512-X5ZOQ6ZTKVHjhIsfl32ZRqbs+FUoeHLbT7x4fh2Os/8ObDDwrUcCJPqxe2b2RB5E2d0vepYigknHeLE7gwzlNQ==}
-    engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
-    hasBin: true
-
-  '@angular/common@17.3.10':
-    resolution: {integrity: sha512-6SfD21M3LujymmZsZQIxAsV8Bj5u6He6ImZ+p2rr7FAhFxpVJyKldK8LCmJcFsBD4srpQcxEZ0iDxXvg+0ihAw==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    peerDependencies:
-      '@angular/core': 17.3.10
-      rxjs: ^6.5.3 || ^7.4.0
-
-  '@angular/compiler-cli@17.3.10':
-    resolution: {integrity: sha512-85SBphqRj3szac3FbeYgEZ+I6WaAlo5h7JX06BdjOLLiaoIwlFhLeAuG+jVekseV+95grFUxIsCMphWHi2e6hQ==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    hasBin: true
-    peerDependencies:
-      '@angular/compiler': 17.3.10
-      typescript: '>=5.2 <5.5'
-
-  '@angular/compiler@17.3.10':
-    resolution: {integrity: sha512-6Ce4siHyF0fCZBDm/cz+blJByGDu1/hbPkQVGmk5HGZTmCUeKkgyjoM6bZr7ssAsyGDRwxBh2SGHO4Ce31vuPA==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    peerDependencies:
-      '@angular/core': 17.3.10
-    peerDependenciesMeta:
-      '@angular/core':
-        optional: true
-
-  '@angular/core@17.3.10':
-    resolution: {integrity: sha512-ocEKu7X0yFCOvgJn1uZy76qjhsjKvULrO1k/BuIX0nwhp61DTGYTvCqKmwCBLM8/gvcKYH5vMKMHoQKtiSGE0A==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    peerDependencies:
-      rxjs: ^6.5.3 || ^7.4.0
-      zone.js: ~0.14.0
-
-  '@angular/forms@17.3.10':
-    resolution: {integrity: sha512-0VZWSXDi2M3DAGJlpdV3lo73Yo/73GPRqmfTOrvIoUIenFg5Dz6oNGzvt/1aRkRn6HKccjix6iMpH91EN65pWA==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    peerDependencies:
-      '@angular/common': 17.3.10
-      '@angular/core': 17.3.10
-      '@angular/platform-browser': 17.3.10
-      rxjs: ^6.5.3 || ^7.4.0
-
-  '@angular/language-service@17.3.10':
-    resolution: {integrity: sha512-6y0yEnjuKGCnH+YxmZZUC/KEb2ZuB5z7y0AOj4PwOladMWSwHv71x1rz5MokBVBf7ZTeN2w89f9jSWBzSz+fPw==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-
-  '@angular/platform-browser-dynamic@17.3.10':
-    resolution: {integrity: sha512-TW6G4+isdHM2ssQTRTobeAKtR2516pJ25BSwRb+9+Jw/ZAEYOOi+KQyofIFYQccaUjb3+LpjRcaZbtZ9m/Ispg==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    peerDependencies:
-      '@angular/common': 17.3.10
-      '@angular/compiler': 17.3.10
-      '@angular/core': 17.3.10
-      '@angular/platform-browser': 17.3.10
-
-  '@angular/platform-browser@17.3.10':
-    resolution: {integrity: sha512-LEhBDOKm2A7nRmZqsafVp6OinRDG1OYZBSqjnT1jZ+f0CRRFIXz6aJ0TMPoU6vq9SLRJ7vrGD9P/eBf2hW00NQ==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    peerDependencies:
-      '@angular/animations': 17.3.10
-      '@angular/common': 17.3.10
-      '@angular/core': 17.3.10
-    peerDependenciesMeta:
-      '@angular/animations':
-        optional: true
-
-  '@angular/router@17.3.10':
-    resolution: {integrity: sha512-HlZlR9BOLoEKGOSMjmL5EfYL7F7PeDifbFi0dYWNcrG8zFrVKFklB1cuBdJhfPZgYhDEoGms/EToD71tg5wliA==}
-    engines: {node: ^18.13.0 || >=20.9.0}
-    peerDependencies:
-      '@angular/common': 17.3.10
-      '@angular/core': 17.3.10
-      '@angular/platform-browser': 17.3.10
-      rxjs: ^6.5.3 || ^7.4.0
-
-  '@ant-design/colors@7.2.1':
-    resolution: {integrity: sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==}
-
-  '@ant-design/fast-color@2.0.6':
-    resolution: {integrity: sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==}
-    engines: {node: '>=8.x'}
-
-  '@ant-design/fast-color@3.0.0':
-    resolution: {integrity: sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==}
-    engines: {node: '>=8.x'}
-
-  '@ant-design/icons-angular@17.0.0':
-    resolution: {integrity: sha512-MNEh3UbkSl6gkdb5MQRNHEuWI1DnU1dME9zSymnWCipEXN7MB0mcYHSfyYTqKL1j45ftp6l1UnsLvhokRYyhXA==}
-    peerDependencies:
-      '@angular/common': ^17.0.1
-      '@angular/core': ^17.0.1
-      '@angular/platform-browser': ^17.0.1
-      rxjs: ^6.4.0 || ^7.4.0
-
-  '@ant-design/icons-angular@19.0.0':
-    resolution: {integrity: sha512-bBWFA1cTZwLAFTgpozkeNIHX1nXyZuiUaRzTcAfFEt85eW1X3ypMcBfS/XEVVVzkdTw5Td+E1vwzgfuUlKiYSA==}
-    peerDependencies:
-      '@angular/common': ^19.0.0
-      '@angular/core': ^19.0.0
-      '@angular/platform-browser': ^19.0.0
-      rxjs: ^6.5.3 || ^7.4.0
-
-  '@antfu/install-pkg@1.1.0':
-    resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
-
-  '@babel/code-frame@7.28.6':
-    resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/compat-data@7.28.6':
-    resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/core@7.23.9':
-    resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/core@7.24.0':
-    resolution: {integrity: sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/core@7.28.6':
-    resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/generator@7.23.6':
-    resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/generator@7.28.6':
-    resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-annotate-as-pure@7.22.5':
-    resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-annotate-as-pure@7.27.3':
-    resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-compilation-targets@7.28.6':
-    resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-create-class-features-plugin@7.28.6':
-    resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-create-regexp-features-plugin@7.28.5':
-    resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-define-polyfill-provider@0.5.0':
-    resolution: {integrity: sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
-  '@babel/helper-define-polyfill-provider@0.6.5':
-    resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
-  '@babel/helper-environment-visitor@7.24.7':
-    resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-globals@7.28.0':
-    resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-member-expression-to-functions@7.28.5':
-    resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-module-imports@7.28.6':
-    resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-module-transforms@7.28.6':
-    resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-optimise-call-expression@7.27.1':
-    resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-plugin-utils@7.28.6':
-    resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-remap-async-to-generator@7.27.1':
-    resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-replace-supers@7.28.6':
-    resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
-    resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-split-export-declaration@7.22.6':
-    resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-string-parser@7.27.1':
-    resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-validator-identifier@7.28.5':
-    resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-validator-option@7.27.1':
-    resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-wrap-function@7.28.6':
-    resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helpers@7.28.6':
-    resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/parser@7.28.6':
-    resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==}
-    engines: {node: '>=6.0.0'}
-    hasBin: true
-
-  '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1':
-    resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1':
-    resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.13.0
-
-  '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6':
-    resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
-    resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-async-generators@7.8.4':
-    resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-class-properties@7.12.13':
-    resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-class-static-block@7.14.5':
-    resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-dynamic-import@7.8.3':
-    resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-export-namespace-from@7.8.3':
-    resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-import-assertions@7.28.6':
-    resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-import-attributes@7.28.6':
-    resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-import-meta@7.10.4':
-    resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-json-strings@7.8.3':
-    resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-logical-assignment-operators@7.10.4':
-    resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3':
-    resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-numeric-separator@7.10.4':
-    resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-object-rest-spread@7.8.3':
-    resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-optional-catch-binding@7.8.3':
-    resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-optional-chaining@7.8.3':
-    resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-private-property-in-object@7.14.5':
-    resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-top-level-await@7.14.5':
-    resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-unicode-sets-regex@7.18.6':
-    resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/plugin-transform-arrow-functions@7.27.1':
-    resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-async-generator-functions@7.23.9':
-    resolution: {integrity: sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-async-to-generator@7.23.3':
-    resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-block-scoped-functions@7.27.1':
-    resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-block-scoping@7.28.6':
-    resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-class-properties@7.28.6':
-    resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-class-static-block@7.28.6':
-    resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.12.0
-
-  '@babel/plugin-transform-classes@7.28.6':
-    resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-computed-properties@7.28.6':
-    resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-destructuring@7.28.5':
-    resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-dotall-regex@7.28.6':
-    resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-duplicate-keys@7.27.1':
-    resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-dynamic-import@7.27.1':
-    resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-exponentiation-operator@7.28.6':
-    resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-export-namespace-from@7.27.1':
-    resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-for-of@7.27.1':
-    resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-function-name@7.27.1':
-    resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-json-strings@7.28.6':
-    resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-literals@7.27.1':
-    resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-logical-assignment-operators@7.28.6':
-    resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-member-expression-literals@7.27.1':
-    resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-modules-amd@7.27.1':
-    resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-modules-commonjs@7.28.6':
-    resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-modules-systemjs@7.28.5':
-    resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-modules-umd@7.27.1':
-    resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-named-capturing-groups-regex@7.27.1':
-    resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/plugin-transform-new-target@7.27.1':
-    resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-nullish-coalescing-operator@7.28.6':
-    resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-numeric-separator@7.28.6':
-    resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-object-rest-spread@7.28.6':
-    resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-object-super@7.27.1':
-    resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-optional-catch-binding@7.28.6':
-    resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-optional-chaining@7.28.6':
-    resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-parameters@7.27.7':
-    resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-private-methods@7.28.6':
-    resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-private-property-in-object@7.28.6':
-    resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-property-literals@7.27.1':
-    resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-regenerator@7.28.6':
-    resolution: {integrity: sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-reserved-words@7.27.1':
-    resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-runtime@7.24.0':
-    resolution: {integrity: sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-shorthand-properties@7.27.1':
-    resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-spread@7.28.6':
-    resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-sticky-regex@7.27.1':
-    resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-template-literals@7.27.1':
-    resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-typeof-symbol@7.27.1':
-    resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-unicode-escapes@7.27.1':
-    resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-unicode-property-regex@7.28.6':
-    resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-unicode-regex@7.27.1':
-    resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-unicode-sets-regex@7.28.6':
-    resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/preset-env@7.24.0':
-    resolution: {integrity: sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/preset-modules@0.1.6-no-external-plugins':
-    resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
-
-  '@babel/runtime@7.24.0':
-    resolution: {integrity: sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/runtime@7.28.4':
-    resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/template@7.28.6':
-    resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/traverse@7.28.6':
-    resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/types@7.28.6':
-    resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
-    engines: {node: '>=6.9.0'}
-
-  '@braintree/sanitize-url@7.1.1':
-    resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
-
-  '@chevrotain/cst-dts-gen@11.0.3':
-    resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==}
-
-  '@chevrotain/gast@11.0.3':
-    resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==}
-
-  '@chevrotain/regexp-to-ast@11.0.3':
-    resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==}
-
-  '@chevrotain/types@11.0.3':
-    resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==}
-
-  '@chevrotain/utils@11.0.3':
-    resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
-
-  '@colors/colors@1.5.0':
-    resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
-    engines: {node: '>=0.1.90'}
-
-  '@cspotcode/source-map-support@0.8.1':
-    resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
-    engines: {node: '>=12'}
-
-  '@csstools/selector-specificity@2.2.0':
-    resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==}
-    engines: {node: ^14 || ^16 || >=18}
-    peerDependencies:
-      postcss-selector-parser: ^6.0.10
-
-  '@ctrl/tinycolor@4.2.0':
-    resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==}
-    engines: {node: '>=14'}
-
-  '@delon/abc@17.0.5':
-    resolution: {integrity: sha512-TdQMYFZ3dBC85eH/7vZGBRsL12uP0zD5lkYEdFhCiMSnnuIdQDosufv3oScFU7yHKriDc87IdXcA+wYobzlbag==}
-
-  '@delon/abc@17.3.1':
-    resolution: {integrity: sha512-Gh0IwSQVFA5cgrKvGMNEupj6He2ywk3luHz9ORRa4a81+SZpvvrebIba38f9Y5ZqPovUh4PLvrK2AfqGFpPfAQ==}
-
-  '@delon/acl@17.3.1':
-    resolution: {integrity: sha512-iiw7eCwvkG5TTJMBKAD6mHDNIYgpGLiut7nKOinybyhwWWjev5okJceZyAI1QAZx0xYSfA9BPTfkq4vrjO0HIA==}
-
-  '@delon/auth@17.3.1':
-    resolution: {integrity: sha512-rOhelX3Bcydyr9RfCvDTqBTrKJpWO+ufQto5SS6tMRG3Snsp1tfLMau9oNkwq1lwS60SvlFq6ipxG6KTuWkb8A==}
-
-  '@delon/cache@17.3.1':
-    resolution: {integrity: sha512-vsrWHH+ndWQ9vk04GazeKTQj0+EHk5c9LlSGtRHgrD7lLwwtWAOnrr6YOp0Z0UAOAR6WFGZmDGq37gWlZu9GxA==}
-
-  '@delon/form@17.3.1':
-    resolution: {integrity: sha512-cCP0HLsiQjexy4nHfu8i86X/CT7mFMIdvkDsURXXDsSygiN56IdTFkzItEIcSVveL9tEK9zay0xiLNlOykG4RQ==}
-
-  '@delon/mock@17.3.1':
-    resolution: {integrity: sha512-MAR+jc6cncpRtZ/z7l14gz+SGKs7uVkoGcrCHIzC36Sxs2q4PudxyfxvDVPGqAlkR2TDbSdjVPbO1QfZg6gdMw==}
-
-  '@delon/testing@17.3.1':
-    resolution: {integrity: sha512-TrYlHWNHQClFPMDragmYTz39iP+K4aLd4szUSuEBax/OB9Q3ejX18GQoqe0ox0A7kawVaMsyTNRIuJT1Xe4zpQ==}
-
-  '@delon/theme@17.3.1':
-    resolution: {integrity: sha512-0pSuy13uhVfL/jOlIgY+onmZ89KYk9/NmtdRcuZ+nWfnlKh7Zj1BOtn6bShikbkGEzI3+EIzhOZAwgsMoBLSxg==}
-
-  '@delon/util@17.3.1':
-    resolution: {integrity: sha512-4dXkfWNu4jE6e9mFYCOy1Fnr1DJubHGsWniFPg7zPRY44EC1IfQWNzKe2GB6WHxLZBU7I6UUeg0lBAYXrG+ctg==}
-
-  '@discoveryjs/json-ext@0.5.7':
-    resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
-    engines: {node: '>=10.0.0'}
-
-  '@emnapi/core@1.8.1':
-    resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
-
-  '@emnapi/runtime@1.8.1':
-    resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
-
-  '@emnapi/wasi-threads@1.1.0':
-    resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
-
-  '@es-joy/jsdoccomment@0.43.1':
-    resolution: {integrity: sha512-I238eDtOolvCuvtxrnqtlBaw0BwdQuYqK7eA6XIonicMdOOOb75mqdIzkGDUbS04+1Di007rgm9snFRNeVrOog==}
-    engines: {node: '>=16'}
-
-  '@esbuild/aix-ppc64@0.19.12':
-    resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
-    engines: {node: '>=12'}
-    cpu: [ppc64]
-    os: [aix]
-
-  '@esbuild/aix-ppc64@0.20.1':
-    resolution: {integrity: sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==}
-    engines: {node: '>=12'}
-    cpu: [ppc64]
-    os: [aix]
-
-  '@esbuild/android-arm64@0.19.12':
-    resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [android]
-
-  '@esbuild/android-arm64@0.20.1':
-    resolution: {integrity: sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [android]
-
-  '@esbuild/android-arm@0.19.12':
-    resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [android]
-
-  '@esbuild/android-arm@0.20.1':
-    resolution: {integrity: sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [android]
-
-  '@esbuild/android-x64@0.19.12':
-    resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [android]
-
-  '@esbuild/android-x64@0.20.1':
-    resolution: {integrity: sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [android]
-
-  '@esbuild/darwin-arm64@0.19.12':
-    resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [darwin]
-
-  '@esbuild/darwin-arm64@0.20.1':
-    resolution: {integrity: sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [darwin]
-
-  '@esbuild/darwin-x64@0.19.12':
-    resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [darwin]
-
-  '@esbuild/darwin-x64@0.20.1':
-    resolution: {integrity: sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [darwin]
-
-  '@esbuild/freebsd-arm64@0.19.12':
-    resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [freebsd]
-
-  '@esbuild/freebsd-arm64@0.20.1':
-    resolution: {integrity: sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [freebsd]
-
-  '@esbuild/freebsd-x64@0.19.12':
-    resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [freebsd]
-
-  '@esbuild/freebsd-x64@0.20.1':
-    resolution: {integrity: sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [freebsd]
-
-  '@esbuild/linux-arm64@0.19.12':
-    resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [linux]
-
-  '@esbuild/linux-arm64@0.20.1':
-    resolution: {integrity: sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [linux]
-
-  '@esbuild/linux-arm@0.19.12':
-    resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [linux]
-
-  '@esbuild/linux-arm@0.20.1':
-    resolution: {integrity: sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [linux]
-
-  '@esbuild/linux-ia32@0.19.12':
-    resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [linux]
-
-  '@esbuild/linux-ia32@0.20.1':
-    resolution: {integrity: sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [linux]
-
-  '@esbuild/linux-loong64@0.19.12':
-    resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
-    engines: {node: '>=12'}
-    cpu: [loong64]
-    os: [linux]
-
-  '@esbuild/linux-loong64@0.20.1':
-    resolution: {integrity: sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==}
-    engines: {node: '>=12'}
-    cpu: [loong64]
-    os: [linux]
-
-  '@esbuild/linux-mips64el@0.19.12':
-    resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
-    engines: {node: '>=12'}
-    cpu: [mips64el]
-    os: [linux]
-
-  '@esbuild/linux-mips64el@0.20.1':
-    resolution: {integrity: sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==}
-    engines: {node: '>=12'}
-    cpu: [mips64el]
-    os: [linux]
-
-  '@esbuild/linux-ppc64@0.19.12':
-    resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
-    engines: {node: '>=12'}
-    cpu: [ppc64]
-    os: [linux]
-
-  '@esbuild/linux-ppc64@0.20.1':
-    resolution: {integrity: sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==}
-    engines: {node: '>=12'}
-    cpu: [ppc64]
-    os: [linux]
-
-  '@esbuild/linux-riscv64@0.19.12':
-    resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
-    engines: {node: '>=12'}
-    cpu: [riscv64]
-    os: [linux]
-
-  '@esbuild/linux-riscv64@0.20.1':
-    resolution: {integrity: sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==}
-    engines: {node: '>=12'}
-    cpu: [riscv64]
-    os: [linux]
-
-  '@esbuild/linux-s390x@0.19.12':
-    resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
-    engines: {node: '>=12'}
-    cpu: [s390x]
-    os: [linux]
-
-  '@esbuild/linux-s390x@0.20.1':
-    resolution: {integrity: sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==}
-    engines: {node: '>=12'}
-    cpu: [s390x]
-    os: [linux]
-
-  '@esbuild/linux-x64@0.19.12':
-    resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [linux]
-
-  '@esbuild/linux-x64@0.20.1':
-    resolution: {integrity: sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [linux]
-
-  '@esbuild/netbsd-x64@0.19.12':
-    resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [netbsd]
-
-  '@esbuild/netbsd-x64@0.20.1':
-    resolution: {integrity: sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [netbsd]
-
-  '@esbuild/openbsd-x64@0.19.12':
-    resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [openbsd]
-
-  '@esbuild/openbsd-x64@0.20.1':
-    resolution: {integrity: sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [openbsd]
-
-  '@esbuild/sunos-x64@0.19.12':
-    resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [sunos]
-
-  '@esbuild/sunos-x64@0.20.1':
-    resolution: {integrity: sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [sunos]
-
-  '@esbuild/win32-arm64@0.19.12':
-    resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [win32]
-
-  '@esbuild/win32-arm64@0.20.1':
-    resolution: {integrity: sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [win32]
-
-  '@esbuild/win32-ia32@0.19.12':
-    resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [win32]
-
-  '@esbuild/win32-ia32@0.20.1':
-    resolution: {integrity: sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [win32]
-
-  '@esbuild/win32-x64@0.19.12':
-    resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [win32]
-
-  '@esbuild/win32-x64@0.20.1':
-    resolution: {integrity: sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [win32]
-
-  '@eslint-community/eslint-utils@4.9.1':
-    resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    peerDependencies:
-      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
-
-  '@eslint-community/regexpp@4.12.2':
-    resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
-    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
-
-  '@eslint/eslintrc@2.1.4':
-    resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
-  '@eslint/js@8.56.0':
-    resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
-  '@exodus/schemasafe@1.3.0':
-    resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==}
-
-  '@github/hotkey@2.3.1':
-    resolution: {integrity: sha512-+aEUEKAsJnnZElLKW5vdXJ9o7g0PXDGN9w54kDm4BorHyQiUVXRLcaDbBS41T62iyO+tqlCO1NuQrYbrbS8Z2Q==}
-
-  '@humanwhocodes/config-array@0.11.14':
-    resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
-    engines: {node: '>=10.10.0'}
-    deprecated: Use @eslint/config-array instead
-
-  '@humanwhocodes/module-importer@1.0.1':
-    resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
-    engines: {node: '>=12.22'}
-
-  '@humanwhocodes/object-schema@2.0.3':
-    resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
-    deprecated: Use @eslint/object-schema instead
-
-  '@iconify/types@2.0.0':
-    resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
-
-  '@iconify/utils@3.1.0':
-    resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
-
-  '@isaacs/balanced-match@4.0.1':
-    resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
-    engines: {node: 20 || >=22}
-
-  '@isaacs/brace-expansion@5.0.0':
-    resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
-    engines: {node: 20 || >=22}
-
-  '@isaacs/cliui@8.0.2':
-    resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
-    engines: {node: '>=12'}
-
-  '@istanbuljs/load-nyc-config@1.1.0':
-    resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
-    engines: {node: '>=8'}
-
-  '@istanbuljs/schema@0.1.3':
-    resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
-    engines: {node: '>=8'}
-
-  '@jest/schemas@29.6.3':
-    resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
-  '@jridgewell/gen-mapping@0.3.13':
-    resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
-
-  '@jridgewell/remapping@2.3.5':
-    resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
-
-  '@jridgewell/resolve-uri@3.1.2':
-    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
-    engines: {node: '>=6.0.0'}
-
-  '@jridgewell/source-map@0.3.11':
-    resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
-
-  '@jridgewell/sourcemap-codec@1.5.5':
-    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
-
-  '@jridgewell/trace-mapping@0.3.31':
-    resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
-
-  '@jridgewell/trace-mapping@0.3.9':
-    resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
-
-  '@kerwin612/ngx-query-builder@0.6.4':
-    resolution: {integrity: sha512-LZ+X6EDuufOSW+AyT88cB9tqkJfTLD3RV9pSbg7ixeDENWlloeqCbRAPMaQNJfKBgdnJSDuZtI7aPVV6ryg06g==}
-    peerDependencies:
-      '@angular/common': ^18.1.0
-      '@angular/core': ^18.1.0
-
-  '@leichtgewicht/ip-codec@2.0.5':
-    resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
-
-  '@ljharb/through@2.3.14':
-    resolution: {integrity: sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==}
-    engines: {node: '>= 0.4'}
-
-  '@mermaid-js/parser@0.6.3':
-    resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==}
-
-  '@mrmlnc/readdir-enhanced@2.2.1':
-    resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==}
-    engines: {node: '>=4'}
-
-  '@napi-rs/wasm-runtime@0.2.4':
-    resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==}
-
-  '@ngtools/webpack@17.3.8':
-    resolution: {integrity: sha512-CjSVVa/9fzMpEDQP01SC4colKCbZwj7vUq0H2bivp8jVsmd21x9Fu0gDBH0Y9NdfAIm4eGZvmiZKMII3vIOaYQ==}
-    engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
-    peerDependencies:
-      '@angular/compiler-cli': ^17.0.0
-      typescript: '>=5.2 <5.5'
-      webpack: ^5.54.0
-
-  '@nodelib/fs.scandir@2.1.5':
-    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
-    engines: {node: '>= 8'}
-
-  '@nodelib/fs.stat@1.1.3':
-    resolution: {integrity: sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==}
-    engines: {node: '>= 6'}
-
-  '@nodelib/fs.stat@2.0.5':
-    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
-    engines: {node: '>= 8'}
-
-  '@nodelib/fs.walk@1.2.8':
-    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
-    engines: {node: '>= 8'}
-
-  '@npmcli/agent@2.2.2':
-    resolution: {integrity: sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@npmcli/fs@3.1.1':
-    resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  '@npmcli/git@5.0.8':
-    resolution: {integrity: sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@npmcli/installed-package-contents@2.1.0':
-    resolution: {integrity: sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-    hasBin: true
-
-  '@npmcli/node-gyp@3.0.0':
-    resolution: {integrity: sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  '@npmcli/package-json@5.2.1':
-    resolution: {integrity: sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@npmcli/promise-spawn@7.0.2':
-    resolution: {integrity: sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@npmcli/redact@1.1.0':
-    resolution: {integrity: sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@npmcli/run-script@7.0.4':
-    resolution: {integrity: sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@nrwl/devkit@19.8.14':
-    resolution: {integrity: sha512-Oud7BPhFNqE3/YStULn/gHyuGSw2QyxUaHXJApr+DybmYtUms7hQ+cWnY1IY+hRpdtU9ldlg8UYx+VslpS9YNQ==}
-
-  '@nrwl/tao@19.8.14':
-    resolution: {integrity: sha512-zBeYzzwg43T/Z8ZtLblv0fcKuqJULttqYDekSLILThXp3UOMSerEvruhUgwddCY1jUssfLscz8vacMKISv5X4w==}
-    hasBin: true
-
-  '@nx/devkit@19.8.14':
-    resolution: {integrity: sha512-A8dCGttbuqgg9P56VTb0ElD2vM5nc5g0aLnX5PSXo4SkFXwd8DV5GgwJKWB1GO9hYyEtbj4gKek0KxnCtdav4g==}
-    peerDependencies:
-      nx: '>= 19 <= 21'
-
-  '@nx/nx-darwin-arm64@19.8.14':
-    resolution: {integrity: sha512-bZUFf23gAzuwVw71dR8rngye5aCR8Z/ouIo+KayjqB0LWWoi3WzO73s4S69ljftYt4n6z9wvD+Trbb1BKm2fPg==}
-    engines: {node: '>= 10'}
-    cpu: [arm64]
-    os: [darwin]
-
-  '@nx/nx-darwin-x64@19.8.14':
-    resolution: {integrity: sha512-UXXVea8icFG/3rFwpbLYsD6O4wlyJ1STQfOdhGK1Hyuga70AUUdrjVm7HzigAQP/Sb2Nzd7155YXHzfpRPDFYA==}
-    engines: {node: '>= 10'}
-    cpu: [x64]
-    os: [darwin]
-
-  '@nx/nx-freebsd-x64@19.8.14':
-    resolution: {integrity: sha512-TK2xuXn+BI6hxGaRK1HRUPWeF/nOtezKSqM+6rbippfCzjES/crmp9l5nbI764MMthtUmykCyWvhEfkDca6kbA==}
-    engines: {node: '>= 10'}
-    cpu: [x64]
-    os: [freebsd]
-
-  '@nx/nx-linux-arm-gnueabihf@19.8.14':
-    resolution: {integrity: sha512-33rptyRraqaeQ2Kq6pcZKQqgnYY/7zcGH8fHXgKK7XzKk+7QuPViq+jMEUZP5E3UzZPkIYhsfmZcZqhNRvepJQ==}
-    engines: {node: '>= 10'}
-    cpu: [arm]
-    os: [linux]
-
-  '@nx/nx-linux-arm64-gnu@19.8.14':
-    resolution: {integrity: sha512-2E70qMKOhh7Fp4JGcRbRLvFKq0+ANVdAgSzH47plxOLygIeVAfIXRSuQbCI0EUFa5Sy6hImLaoRSB2GdgKihAw==}
-    engines: {node: '>= 10'}
-    cpu: [arm64]
-    os: [linux]
-
-  '@nx/nx-linux-arm64-musl@19.8.14':
-    resolution: {integrity: sha512-ltty/PDWqkYgu/6Ye65d7v5nh3D6e0n3SacoKRs2Vtfz5oHYRUkSKizKIhEVfRNuHn3d9j8ve1fdcCN4SDPUBQ==}
-    engines: {node: '>= 10'}
-    cpu: [arm64]
-    os: [linux]
-
-  '@nx/nx-linux-x64-gnu@19.8.14':
-    resolution: {integrity: sha512-JzE3BuO9RCBVdgai18CCze6KUzG0AozE0TtYFxRokfSC05NU3nUhd/o62UsOl7s6Bqt/9nwrW7JC8pNDiCi9OQ==}
-    engines: {node: '>= 10'}
-    cpu: [x64]
-    os: [linux]
-
-  '@nx/nx-linux-x64-musl@19.8.14':
-    resolution: {integrity: sha512-2rPvDOQLb7Wd6YiU88FMBiLtYco0dVXF99IJBRGAWv+WTI7MNr47OyK2ze+JOsbYY1d8aOGUvckUvCCZvZKEfg==}
-    engines: {node: '>= 10'}
-    cpu: [x64]
-    os: [linux]
-
-  '@nx/nx-win32-arm64-msvc@19.8.14':
-    resolution: {integrity: sha512-JxW+YPS+EjhUsLw9C6wtk9pQTG3psyFwxhab8y/dgk2s4AOTLyIm0XxgcCJVvB6i4uv+s1g0QXRwp6+q3IR6hg==}
-    engines: {node: '>= 10'}
-    cpu: [arm64]
-    os: [win32]
-
-  '@nx/nx-win32-x64-msvc@19.8.14':
-    resolution: {integrity: sha512-RxiPlBWPcGSf9TzIIy62iKRdRhokXMDUsPub9DL2VdVyTMXPZQR25aY/PJeasJN1EQU74hg097LK2wSHi+vzOQ==}
-    engines: {node: '>= 10'}
-    cpu: [x64]
-    os: [win32]
-
-  '@pkgjs/parseargs@0.11.0':
-    resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
-    engines: {node: '>=14'}
-
-  '@rollup/rollup-android-arm-eabi@4.55.1':
-    resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==}
-    cpu: [arm]
-    os: [android]
-
-  '@rollup/rollup-android-arm64@4.55.1':
-    resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==}
-    cpu: [arm64]
-    os: [android]
-
-  '@rollup/rollup-darwin-arm64@4.55.1':
-    resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==}
-    cpu: [arm64]
-    os: [darwin]
-
-  '@rollup/rollup-darwin-x64@4.55.1':
-    resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==}
-    cpu: [x64]
-    os: [darwin]
-
-  '@rollup/rollup-freebsd-arm64@4.55.1':
-    resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==}
-    cpu: [arm64]
-    os: [freebsd]
-
-  '@rollup/rollup-freebsd-x64@4.55.1':
-    resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==}
-    cpu: [x64]
-    os: [freebsd]
-
-  '@rollup/rollup-linux-arm-gnueabihf@4.55.1':
-    resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
-    cpu: [arm]
-    os: [linux]
-
-  '@rollup/rollup-linux-arm-musleabihf@4.55.1':
-    resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
-    cpu: [arm]
-    os: [linux]
-
-  '@rollup/rollup-linux-arm64-gnu@4.55.1':
-    resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
-    cpu: [arm64]
-    os: [linux]
-
-  '@rollup/rollup-linux-arm64-musl@4.55.1':
-    resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
-    cpu: [arm64]
-    os: [linux]
-
-  '@rollup/rollup-linux-loong64-gnu@4.55.1':
-    resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
-    cpu: [loong64]
-    os: [linux]
-
-  '@rollup/rollup-linux-loong64-musl@4.55.1':
-    resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
-    cpu: [loong64]
-    os: [linux]
-
-  '@rollup/rollup-linux-ppc64-gnu@4.55.1':
-    resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
-    cpu: [ppc64]
-    os: [linux]
-
-  '@rollup/rollup-linux-ppc64-musl@4.55.1':
-    resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
-    cpu: [ppc64]
-    os: [linux]
-
-  '@rollup/rollup-linux-riscv64-gnu@4.55.1':
-    resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
-    cpu: [riscv64]
-    os: [linux]
-
-  '@rollup/rollup-linux-riscv64-musl@4.55.1':
-    resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
-    cpu: [riscv64]
-    os: [linux]
-
-  '@rollup/rollup-linux-s390x-gnu@4.55.1':
-    resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
-    cpu: [s390x]
-    os: [linux]
-
-  '@rollup/rollup-linux-x64-gnu@4.55.1':
-    resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
-    cpu: [x64]
-    os: [linux]
-
-  '@rollup/rollup-linux-x64-musl@4.55.1':
-    resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
-    cpu: [x64]
-    os: [linux]
-
-  '@rollup/rollup-openbsd-x64@4.55.1':
-    resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
-    cpu: [x64]
-    os: [openbsd]
-
-  '@rollup/rollup-openharmony-arm64@4.55.1':
-    resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==}
-    cpu: [arm64]
-    os: [openharmony]
-
-  '@rollup/rollup-win32-arm64-msvc@4.55.1':
-    resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==}
-    cpu: [arm64]
-    os: [win32]
-
-  '@rollup/rollup-win32-ia32-msvc@4.55.1':
-    resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==}
-    cpu: [ia32]
-    os: [win32]
-
-  '@rollup/rollup-win32-x64-gnu@4.55.1':
-    resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==}
-    cpu: [x64]
-    os: [win32]
-
-  '@rollup/rollup-win32-x64-msvc@4.55.1':
-    resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==}
-    cpu: [x64]
-    os: [win32]
-
-  '@schematics/angular@17.3.8':
-    resolution: {integrity: sha512-2g4OmSyE9YGq50Uj7fNI26P/TSAFJ7ZuirwTF2O7Xc4XRQ29/tYIIqhezpNlTb6rlYblcQuMcUZBrMfWJHcqJw==}
-    engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
-
-  '@sigstore/bundle@2.3.2':
-    resolution: {integrity: sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@sigstore/core@1.1.0':
-    resolution: {integrity: sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@sigstore/protobuf-specs@0.3.3':
-    resolution: {integrity: sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==}
-    engines: {node: ^18.17.0 || >=20.5.0}
-
-  '@sigstore/sign@2.3.2':
-    resolution: {integrity: sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@sigstore/tuf@2.3.4':
-    resolution: {integrity: sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@sigstore/verify@1.2.1':
-    resolution: {integrity: sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@sinclair/typebox@0.27.8':
-    resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
-
-  '@socket.io/component-emitter@3.1.2':
-    resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
-
-  '@tsconfig/node10@1.0.12':
-    resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==}
-
-  '@tsconfig/node12@1.0.11':
-    resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
-
-  '@tsconfig/node14@1.0.3':
-    resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
-
-  '@tsconfig/node16@1.0.4':
-    resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
-
-  '@tufjs/canonical-json@2.0.0':
-    resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@tufjs/models@2.0.1':
-    resolution: {integrity: sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  '@tybys/wasm-util@0.9.0':
-    resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
-
-  '@types/body-parser@1.19.6':
-    resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
-
-  '@types/bonjour@3.5.13':
-    resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==}
-
-  '@types/connect-history-api-fallback@1.5.4':
-    resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==}
-
-  '@types/connect@3.4.38':
-    resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
-
-  '@types/cors@2.8.19':
-    resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
-
-  '@types/d3-array@3.2.2':
-    resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
-
-  '@types/d3-axis@3.0.6':
-    resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
-
-  '@types/d3-brush@3.0.6':
-    resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
-
-  '@types/d3-chord@3.0.6':
-    resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
-
-  '@types/d3-color@3.1.3':
-    resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
-
-  '@types/d3-contour@3.0.6':
-    resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
-
-  '@types/d3-delaunay@6.0.4':
-    resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
-
-  '@types/d3-dispatch@3.0.7':
-    resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
-
-  '@types/d3-drag@3.0.7':
-    resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
-
-  '@types/d3-dsv@3.0.7':
-    resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
-
-  '@types/d3-ease@3.0.2':
-    resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
-
-  '@types/d3-fetch@3.0.7':
-    resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
-
-  '@types/d3-force@3.0.10':
-    resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
-
-  '@types/d3-format@3.0.4':
-    resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
-
-  '@types/d3-geo@3.1.0':
-    resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
-
-  '@types/d3-hierarchy@3.1.7':
-    resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
-
-  '@types/d3-interpolate@3.0.4':
-    resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
-
-  '@types/d3-path@3.1.1':
-    resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
-
-  '@types/d3-polygon@3.0.2':
-    resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
-
-  '@types/d3-quadtree@3.0.6':
-    resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
-
-  '@types/d3-random@3.0.3':
-    resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
-
-  '@types/d3-scale-chromatic@3.1.0':
-    resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
-
-  '@types/d3-scale@4.0.9':
-    resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
-
-  '@types/d3-selection@3.0.11':
-    resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
-
-  '@types/d3-shape@3.1.8':
-    resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
-
-  '@types/d3-time-format@4.0.3':
-    resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
-
-  '@types/d3-time@3.0.4':
-    resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
-
-  '@types/d3-timer@3.0.2':
-    resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
-
-  '@types/d3-transition@3.0.9':
-    resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
-
-  '@types/d3-zoom@3.0.8':
-    resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
-
-  '@types/d3@7.4.3':
-    resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
-
-  '@types/eslint-scope@3.7.7':
-    resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
-
-  '@types/eslint@8.56.12':
-    resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==}
-
-  '@types/eslint@9.6.1':
-    resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
-
-  '@types/estree@1.0.8':
-    resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
-
-  '@types/express-serve-static-core@4.19.8':
-    resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==}
-
-  '@types/express-serve-static-core@5.1.1':
-    resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
-
-  '@types/express@4.17.25':
-    resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==}
-
-  '@types/geojson@7946.0.16':
-    resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
-
-  '@types/glob@7.2.0':
-    resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
-
-  '@types/http-errors@2.0.5':
-    resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
-
-  '@types/http-proxy@1.17.17':
-    resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==}
-
-  '@types/jasmine@5.1.4':
-    resolution: {integrity: sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==}
-
-  '@types/jasminewd2@2.0.13':
-    resolution: {integrity: sha512-aJ3wj8tXMpBrzQ5ghIaqMisD8C3FIrcO6sDKHqFbuqAsI7yOxj0fA7MrRCPLZHIVUjERIwsMmGn/vB0UQ9u0Hg==}
-
-  '@types/json-schema@7.0.15':
-    resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
-
-  '@types/json5@0.0.29':
-    resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
-
-  '@types/mime@1.3.5':
-    resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
-
-  '@types/minimatch@6.0.0':
-    resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
-    deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
-
-  '@types/minimist@1.2.5':
-    resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
-
-  '@types/mockjs@1.0.10':
-    resolution: {integrity: sha512-SXgrhajHG7boLv6oU93CcmdDm0HYRiceuz6b+7z+/2lCJPTWDv0V5YiwFHT2ejE4bQqgSXQiVPQYPWv7LGsK1g==}
-
-  '@types/node-forge@1.3.14':
-    resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
-
-  '@types/node@20.12.11':
-    resolution: {integrity: sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==}
-
-  '@types/normalize-package-data@2.4.4':
-    resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
-
-  '@types/parse-json@4.0.2':
-    resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
-
-  '@types/q@0.0.32':
-    resolution: {integrity: sha512-qYi3YV9inU/REEfxwVcGZzbS3KG/Xs90lv0Pr+lDtuVjBPGd1A+eciXzVSaRvLify132BfcvhvEjeVahrUl0Ug==}
-
-  '@types/qs@6.14.0':
-    resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
-
-  '@types/range-parser@1.2.7':
-    resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
-
-  '@types/retry@0.12.0':
-    resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
-
-  '@types/selenium-webdriver@3.0.26':
-    resolution: {integrity: sha512-dyIGFKXfUFiwkMfNGn1+F6b80ZjR3uSYv1j6xVJSDlft5waZ2cwkHW4e7zNzvq7hiEackcgvBpmnXZrI1GltPg==}
-
-  '@types/semver@7.7.1':
-    resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
-
-  '@types/send@0.17.6':
-    resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==}
-
-  '@types/send@1.2.1':
-    resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
-
-  '@types/serve-index@1.9.4':
-    resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==}
-
-  '@types/serve-static@1.15.10':
-    resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==}
-
-  '@types/sockjs@0.3.36':
-    resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==}
-
-  '@types/swagger-schema-official@2.0.22':
-    resolution: {integrity: sha512-7yQiX6MWSFSvc/1wW5smJMZTZ4fHOd+hqLr3qr/HONDxHEa2bnYAsOcGBOEqFIjd4yetwMOdEDdeW+udRAQnHA==}
-
-  '@types/swagger-schema-official@2.0.25':
-    resolution: {integrity: sha512-T92Xav+Gf/Ik1uPW581nA+JftmjWPgskw/WBf4TJzxRG/SJ+DfNnNE+WuZ4mrXuzflQMqMkm1LSYjzYW7MB1Cg==}
-
-  '@types/trusted-types@2.0.7':
-    resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
-
-  '@types/unist@2.0.11':
-    resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
-
-  '@types/unist@3.0.3':
-    resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
-
-  '@types/vfile-message@2.0.0':
-    resolution: {integrity: sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==}
-    deprecated: This is a stub types definition. vfile-message provides its own type definitions, so you do not need this installed.
-
-  '@types/vfile@3.0.2':
-    resolution: {integrity: sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==}
-
-  '@types/ws@8.18.1':
-    resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
-
-  '@typescript-eslint/eslint-plugin@7.8.0':
-    resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      '@typescript-eslint/parser': ^7.0.0
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
-  '@typescript-eslint/parser@7.8.0':
-    resolution: {integrity: sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
-  '@typescript-eslint/scope-manager@7.11.0':
-    resolution: {integrity: sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@typescript-eslint/scope-manager@7.18.0':
-    resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@typescript-eslint/scope-manager@7.8.0':
-    resolution: {integrity: sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@typescript-eslint/type-utils@7.11.0':
-    resolution: {integrity: sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
-  '@typescript-eslint/type-utils@7.8.0':
-    resolution: {integrity: sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
-  '@typescript-eslint/types@7.11.0':
-    resolution: {integrity: sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@typescript-eslint/types@7.18.0':
-    resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@typescript-eslint/types@7.8.0':
-    resolution: {integrity: sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@typescript-eslint/typescript-estree@7.11.0':
-    resolution: {integrity: sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
-  '@typescript-eslint/typescript-estree@7.18.0':
-    resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
-  '@typescript-eslint/typescript-estree@7.8.0':
-    resolution: {integrity: sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
-  '@typescript-eslint/utils@7.11.0':
-    resolution: {integrity: sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      eslint: ^8.56.0
-
-  '@typescript-eslint/utils@7.18.0':
-    resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      eslint: ^8.56.0
-
-  '@typescript-eslint/utils@7.8.0':
-    resolution: {integrity: sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-    peerDependencies:
-      eslint: ^8.56.0
-
-  '@typescript-eslint/visitor-keys@7.11.0':
-    resolution: {integrity: sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@typescript-eslint/visitor-keys@7.18.0':
-    resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@typescript-eslint/visitor-keys@7.8.0':
-    resolution: {integrity: sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==}
-    engines: {node: ^18.18.0 || >=20.0.0}
-
-  '@ungap/structured-clone@1.3.0':
-    resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
-
-  '@vitejs/plugin-basic-ssl@1.1.0':
-    resolution: {integrity: sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==}
-    engines: {node: '>=14.6.0'}
-    peerDependencies:
-      vite: ^3.0.0 || ^4.0.0 || ^5.0.0
-
-  '@webassemblyjs/ast@1.14.1':
-    resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
-
-  '@webassemblyjs/floating-point-hex-parser@1.13.2':
-    resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==}
-
-  '@webassemblyjs/helper-api-error@1.13.2':
-    resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==}
-
-  '@webassemblyjs/helper-buffer@1.14.1':
-    resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==}
-
-  '@webassemblyjs/helper-numbers@1.13.2':
-    resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==}
-
-  '@webassemblyjs/helper-wasm-bytecode@1.13.2':
-    resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==}
-
-  '@webassemblyjs/helper-wasm-section@1.14.1':
-    resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==}
-
-  '@webassemblyjs/ieee754@1.13.2':
-    resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==}
-
-  '@webassemblyjs/leb128@1.13.2':
-    resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==}
-
-  '@webassemblyjs/utf8@1.13.2':
-    resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==}
-
-  '@webassemblyjs/wasm-edit@1.14.1':
-    resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==}
-
-  '@webassemblyjs/wasm-gen@1.14.1':
-    resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==}
-
-  '@webassemblyjs/wasm-opt@1.14.1':
-    resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==}
-
-  '@webassemblyjs/wasm-parser@1.14.1':
-    resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==}
-
-  '@webassemblyjs/wast-printer@1.14.1':
-    resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
-
-  '@xtuc/ieee754@1.2.0':
-    resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
-
-  '@xtuc/long@4.2.2':
-    resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
-
-  '@yarnpkg/lockfile@1.1.0':
-    resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
-
-  '@yarnpkg/parsers@3.0.0-rc.46':
-    resolution: {integrity: sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==}
-    engines: {node: '>=14.15.0'}
-
-  '@zkochan/js-yaml@0.0.7':
-    resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==}
-    hasBin: true
-
-  abbrev@2.0.0:
-    resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  accepts@1.3.8:
-    resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
-    engines: {node: '>= 0.6'}
-
-  acorn-import-assertions@1.9.0:
-    resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
-    deprecated: package has been renamed to acorn-import-attributes
-    peerDependencies:
-      acorn: ^8
-
-  acorn-jsx@5.3.2:
-    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
-    peerDependencies:
-      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
-
-  acorn-walk@8.3.4:
-    resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
-    engines: {node: '>=0.4.0'}
-
-  acorn@8.15.0:
-    resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
-    engines: {node: '>=0.4.0'}
-    hasBin: true
-
-  adjust-sourcemap-loader@4.0.0:
-    resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==}
-    engines: {node: '>=8.9'}
-
-  adler-32@1.3.1:
-    resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
-    engines: {node: '>=0.8'}
-
-  adm-zip@0.5.16:
-    resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
-    engines: {node: '>=12.0'}
-
-  agent-base@4.3.0:
-    resolution: {integrity: sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==}
-    engines: {node: '>= 4.0.0'}
-
-  agent-base@7.1.4:
-    resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
-    engines: {node: '>= 14'}
-
-  aggregate-error@3.1.0:
-    resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
-    engines: {node: '>=8'}
-
-  ajv-formats@2.1.1:
-    resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
-    peerDependencies:
-      ajv: ^8.0.0
-    peerDependenciesMeta:
-      ajv:
-        optional: true
-
-  ajv-keywords@3.5.2:
-    resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
-    peerDependencies:
-      ajv: ^6.9.1
-
-  ajv-keywords@5.1.0:
-    resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==}
-    peerDependencies:
-      ajv: ^8.8.2
-
-  ajv@6.12.6:
-    resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
-
-  ajv@8.12.0:
-    resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
-
-  angular-tag-cloud-module@17.0.1:
-    resolution: {integrity: sha512-9r34krgnLhfZZWRis0Sl4VPKm0x9MGkw7bBBPSKfd5R4URizBfn2Wac0NXn1dtmavJwfqqSyq/oFJFpPS8/IUg==}
-    peerDependencies:
-      '@angular/common': ^17.0.0 || ^18.0.0-0
-      '@angular/core': ^17.0.0 || ^18.0.0-0
-
-  ansi-colors@4.1.3:
-    resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
-    engines: {node: '>=6'}
-
-  ansi-escapes@4.3.2:
-    resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
-    engines: {node: '>=8'}
-
-  ansi-escapes@5.0.0:
-    resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==}
-    engines: {node: '>=12'}
-
-  ansi-html-community@0.0.8:
-    resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==}
-    engines: {'0': node >= 0.8.0}
-    hasBin: true
-
-  ansi-regex@2.1.1:
-    resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
-    engines: {node: '>=0.10.0'}
-
-  ansi-regex@4.1.1:
-    resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
-    engines: {node: '>=6'}
-
-  ansi-regex@5.0.1:
-    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
-    engines: {node: '>=8'}
-
-  ansi-regex@6.2.2:
-    resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
-    engines: {node: '>=12'}
-
-  ansi-styles@2.2.1:
-    resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
-    engines: {node: '>=0.10.0'}
-
-  ansi-styles@3.2.1:
-    resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
-    engines: {node: '>=4'}
-
-  ansi-styles@4.3.0:
-    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
-    engines: {node: '>=8'}
-
-  ansi-styles@5.2.0:
-    resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
-    engines: {node: '>=10'}
-
-  ansi-styles@6.2.3:
-    resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
-    engines: {node: '>=12'}
-
-  anymatch@3.1.3:
-    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
-    engines: {node: '>= 8'}
-
-  are-docs-informative@0.0.2:
-    resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
-    engines: {node: '>=14'}
-
-  arg@4.1.3:
-    resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
-
-  argparse@1.0.10:
-    resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
-
-  argparse@2.0.1:
-    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
-
-  aria-query@5.3.0:
-    resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
-
-  arr-diff@4.0.0:
-    resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==}
-    engines: {node: '>=0.10.0'}
-
-  arr-flatten@1.1.0:
-    resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==}
-    engines: {node: '>=0.10.0'}
-
-  arr-union@3.1.0:
-    resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==}
-    engines: {node: '>=0.10.0'}
-
-  array-buffer-byte-length@1.0.2:
-    resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
-    engines: {node: '>= 0.4'}
-
-  array-find-index@1.0.2:
-    resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==}
-    engines: {node: '>=0.10.0'}
-
-  array-flatten@1.1.1:
-    resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
-
-  array-includes@3.1.9:
-    resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
-    engines: {node: '>= 0.4'}
-
-  array-union@1.0.2:
-    resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==}
-    engines: {node: '>=0.10.0'}
-
-  array-union@2.1.0:
-    resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
-    engines: {node: '>=8'}
-
-  array-uniq@1.0.3:
-    resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==}
-    engines: {node: '>=0.10.0'}
-
-  array-unique@0.3.2:
-    resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==}
-    engines: {node: '>=0.10.0'}
-
-  array.prototype.flat@1.3.3:
-    resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==}
-    engines: {node: '>= 0.4'}
-
-  arraybuffer.prototype.slice@1.0.4:
-    resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
-    engines: {node: '>= 0.4'}
-
-  arrify@1.0.1:
-    resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
-    engines: {node: '>=0.10.0'}
-
-  asn1@0.2.6:
-    resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
-
-  assert-plus@1.0.0:
-    resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
-    engines: {node: '>=0.8'}
-
-  assign-symbols@1.0.0:
-    resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==}
-    engines: {node: '>=0.10.0'}
-
-  astral-regex@1.0.0:
-    resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==}
-    engines: {node: '>=4'}
-
-  astral-regex@2.0.0:
-    resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
-    engines: {node: '>=8'}
-
-  async-function@1.0.0:
-    resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
-    engines: {node: '>= 0.4'}
-
-  async@3.2.6:
-    resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
-
-  asynckit@0.4.0:
-    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
-
-  atob@2.1.2:
-    resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
-    engines: {node: '>= 4.5.0'}
-    hasBin: true
-
-  autoprefixer@10.4.18:
-    resolution: {integrity: sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==}
-    engines: {node: ^10 || ^12 || >=14}
-    hasBin: true
-    peerDependencies:
-      postcss: ^8.1.0
-
-  autoprefixer@9.8.8:
-    resolution: {integrity: sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==}
-    hasBin: true
-
-  available-typed-arrays@1.0.7:
-    resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
-    engines: {node: '>= 0.4'}
-
-  aws-sign2@0.7.0:
-    resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
-
-  aws4@1.13.2:
-    resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==}
-
-  axios@1.13.2:
-    resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
-
-  axobject-query@4.0.0:
-    resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==}
-
-  babel-loader@9.1.3:
-    resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==}
-    engines: {node: '>= 14.15.0'}
-    peerDependencies:
-      '@babel/core': ^7.12.0
-      webpack: '>=5'
-
-  babel-plugin-istanbul@6.1.1:
-    resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==}
-    engines: {node: '>=8'}
-
-  babel-plugin-polyfill-corejs2@0.4.14:
-    resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
-  babel-plugin-polyfill-corejs3@0.9.0:
-    resolution: {integrity: sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
-  babel-plugin-polyfill-regenerator@0.5.5:
-    resolution: {integrity: sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
-  bail@1.0.5:
-    resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==}
-
-  balanced-match@1.0.2:
-    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
-
-  balanced-match@2.0.0:
-    resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
-
-  base64-js@1.5.1:
-    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
-
-  base64id@2.0.0:
-    resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
-    engines: {node: ^4.5.0 || >= 5.9}
-
-  base@0.11.2:
-    resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
-    engines: {node: '>=0.10.0'}
-
-  baseline-browser-mapping@2.9.15:
-    resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==}
-    hasBin: true
-
-  batch@0.6.1:
-    resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==}
-
-  bcrypt-pbkdf@1.0.2:
-    resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
-
-  big.js@5.2.2:
-    resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
-
-  binary-extensions@2.3.0:
-    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
-    engines: {node: '>=8'}
-
-  bl@4.1.0:
-    resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
-
-  blocking-proxy@1.0.1:
-    resolution: {integrity: sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==}
-    engines: {node: '>=6.9.x'}
-    hasBin: true
-
-  body-parser@1.20.4:
-    resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
-    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
-
-  bonjour-service@1.3.0:
-    resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==}
-
-  boolbase@1.0.0:
-    resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
-
-  brace-expansion@1.1.12:
-    resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
-
-  brace-expansion@2.0.2:
-    resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
-
-  braces@2.3.2:
-    resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==}
-    engines: {node: '>=0.10.0'}
-
-  braces@3.0.3:
-    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
-    engines: {node: '>=8'}
-
-  browserslist@4.28.1:
-    resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
-    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
-    hasBin: true
-
-  browserstack@1.6.1:
-    resolution: {integrity: sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==}
-
-  btoa@1.2.1:
-    resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
-    engines: {node: '>= 0.4.0'}
-    hasBin: true
-
-  buffer-from@1.1.2:
-    resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
-
-  buffer@5.7.1:
-    resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
-
-  builtin-modules@3.3.0:
-    resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
-    engines: {node: '>=6'}
-
-  bytes@3.1.2:
-    resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
-    engines: {node: '>= 0.8'}
-
-  cacache@18.0.4:
-    resolution: {integrity: sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  cache-base@1.0.1:
-    resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==}
-    engines: {node: '>=0.10.0'}
-
-  call-bind-apply-helpers@1.0.2:
-    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
-    engines: {node: '>= 0.4'}
-
-  call-bind@1.0.8:
-    resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
-    engines: {node: '>= 0.4'}
-
-  call-bound@1.0.4:
-    resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
-    engines: {node: '>= 0.4'}
-
-  call-me-maybe@1.0.2:
-    resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
-
-  caller-callsite@2.0.0:
-    resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==}
-    engines: {node: '>=4'}
-
-  caller-path@2.0.0:
-    resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==}
-    engines: {node: '>=4'}
-
-  callsites@2.0.0:
-    resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==}
-    engines: {node: '>=4'}
-
-  callsites@3.1.0:
-    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
-    engines: {node: '>=6'}
-
-  camelcase-keys@4.2.0:
-    resolution: {integrity: sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==}
-    engines: {node: '>=4'}
-
-  camelcase-keys@6.2.2:
-    resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
-    engines: {node: '>=8'}
-
-  camelcase@4.1.0:
-    resolution: {integrity: sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==}
-    engines: {node: '>=4'}
-
-  camelcase@5.3.1:
-    resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
-    engines: {node: '>=6'}
-
-  caniuse-lite@1.0.30001764:
-    resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==}
-
-  caseless@0.12.0:
-    resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
-
-  ccount@1.1.0:
-    resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==}
-
-  cfb@1.2.2:
-    resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
-    engines: {node: '>=0.8'}
-
-  chalk@1.1.3:
-    resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
-    engines: {node: '>=0.10.0'}
-
-  chalk@2.4.2:
-    resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
-    engines: {node: '>=4'}
-
-  chalk@4.1.2:
-    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
-    engines: {node: '>=10'}
-
-  chalk@5.3.0:
-    resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
-    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
-
-  chalk@5.6.2:
-    resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
-    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
-
-  character-entities-html4@1.1.4:
-    resolution: {integrity: sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==}
-
-  character-entities-legacy@1.1.4:
-    resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
-
-  character-entities@1.2.4:
-    resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==}
-
-  character-reference-invalid@1.1.4:
-    resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
-
-  chardet@0.7.0:
-    resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
-
-  chevrotain-allstar@0.3.1:
-    resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==}
-    peerDependencies:
-      chevrotain: ^11.0.0
-
-  chevrotain@11.0.3:
-    resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==}
-
-  chokidar@3.6.0:
-    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
-    engines: {node: '>= 8.10.0'}
-
-  chownr@2.0.0:
-    resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
-    engines: {node: '>=10'}
-
-  chrome-trace-event@1.0.4:
-    resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
-    engines: {node: '>=6.0'}
-
-  class-utils@0.3.6:
-    resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
-    engines: {node: '>=0.10.0'}
-
-  clean-css@5.3.3:
-    resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
-    engines: {node: '>= 10.0'}
-
-  clean-stack@2.2.0:
-    resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
-    engines: {node: '>=6'}
-
-  cli-cursor@3.1.0:
-    resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
-    engines: {node: '>=8'}
-
-  cli-cursor@4.0.0:
-    resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  cli-spinners@2.6.1:
-    resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==}
-    engines: {node: '>=6'}
-
-  cli-spinners@2.9.2:
-    resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
-    engines: {node: '>=6'}
-
-  cli-truncate@3.1.0:
-    resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  cli-width@4.1.0:
-    resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
-    engines: {node: '>= 12'}
-
-  clipboard@2.0.11:
-    resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
-
-  cliui@6.0.0:
-    resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
-
-  cliui@7.0.4:
-    resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
-
-  cliui@8.0.1:
-    resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
-    engines: {node: '>=12'}
-
-  clone-deep@4.0.1:
-    resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==}
-    engines: {node: '>=6'}
-
-  clone-regexp@1.0.1:
-    resolution: {integrity: sha512-Fcij9IwRW27XedRIJnSOEupS7RVcXtObJXbcUOX93UCLqqOdRpkvzKywOOSizmEK/Is3S/RHX9dLdfo6R1Q1mw==}
-    engines: {node: '>=0.10.0'}
-
-  clone@1.0.4:
-    resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
-    engines: {node: '>=0.8'}
-
-  codepage@1.15.0:
-    resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
-    engines: {node: '>=0.8'}
-
-  collapse-white-space@1.0.6:
-    resolution: {integrity: sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==}
-
-  collection-visit@1.0.0:
-    resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
-    engines: {node: '>=0.10.0'}
-
-  color-convert@1.9.3:
-    resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
-
-  color-convert@2.0.1:
-    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
-    engines: {node: '>=7.0.0'}
-
-  color-name@1.1.3:
-    resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
-
-  color-name@1.1.4:
-    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
-
-  colord@2.9.3:
-    resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
-
-  colorette@2.0.20:
-    resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
-
-  colors@1.4.0:
-    resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
-    engines: {node: '>=0.1.90'}
-
-  combined-stream@1.0.8:
-    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
-    engines: {node: '>= 0.8'}
-
-  commander@11.0.0:
-    resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==}
-    engines: {node: '>=16'}
-
-  commander@14.0.2:
-    resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
-    engines: {node: '>=20'}
-
-  commander@2.20.3:
-    resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
-
-  commander@7.2.0:
-    resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
-    engines: {node: '>= 10'}
-
-  commander@8.3.0:
-    resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
-    engines: {node: '>= 12'}
-
-  comment-parser@1.4.1:
-    resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
-    engines: {node: '>= 12.0.0'}
-
-  common-path-prefix@3.0.0:
-    resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==}
-
-  component-emitter@1.3.1:
-    resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
-
-  compressible@2.0.18:
-    resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
-    engines: {node: '>= 0.6'}
-
-  compression@1.8.1:
-    resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
-    engines: {node: '>= 0.8.0'}
-
-  concat-map@0.0.1:
-    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
-
-  confbox@0.1.8:
-    resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
-
-  connect-history-api-fallback@2.0.0:
-    resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==}
-    engines: {node: '>=0.8'}
-
-  connect@3.7.0:
-    resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
-    engines: {node: '>= 0.10.0'}
-
-  content-disposition@0.5.4:
-    resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
-    engines: {node: '>= 0.6'}
-
-  content-type@1.0.5:
-    resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
-    engines: {node: '>= 0.6'}
-
-  convert-source-map@1.9.0:
-    resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
-
-  convert-source-map@2.0.0:
-    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
-
-  cookie-signature@1.0.7:
-    resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
-
-  cookie@0.7.2:
-    resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
-    engines: {node: '>= 0.6'}
-
-  copy-anything@2.0.6:
-    resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
-
-  copy-descriptor@0.1.1:
-    resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==}
-    engines: {node: '>=0.10.0'}
-
-  copy-webpack-plugin@11.0.0:
-    resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==}
-    engines: {node: '>= 14.15.0'}
-    peerDependencies:
-      webpack: ^5.1.0
-
-  core-js-compat@3.47.0:
-    resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==}
-
-  core-js@3.47.0:
-    resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
-
-  core-util-is@1.0.2:
-    resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
-
-  core-util-is@1.0.3:
-    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
-
-  cors@2.8.5:
-    resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
-    engines: {node: '>= 0.10'}
-
-  cose-base@1.0.3:
-    resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
-
-  cose-base@2.2.0:
-    resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
-
-  cosmiconfig@5.2.1:
-    resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==}
-    engines: {node: '>=4'}
-
-  cosmiconfig@7.0.1:
-    resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==}
-    engines: {node: '>=10'}
-
-  cosmiconfig@7.1.0:
-    resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
-    engines: {node: '>=10'}
-
-  cosmiconfig@9.0.0:
-    resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
-    engines: {node: '>=14'}
-    peerDependencies:
-      typescript: '>=4.9.5'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
-  crc-32@1.2.2:
-    resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
-    engines: {node: '>=0.8'}
-    hasBin: true
-
-  create-require@1.1.1:
-    resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
-
-  critters@0.0.22:
-    resolution: {integrity: sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==}
-
-  cross-spawn@7.0.6:
-    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
-    engines: {node: '>= 8'}
-
-  css-functions-list@3.2.3:
-    resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
-    engines: {node: '>=12 || >=16'}
-
-  css-loader@6.10.0:
-    resolution: {integrity: sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==}
-    engines: {node: '>= 12.13.0'}
-    peerDependencies:
-      '@rspack/core': 0.x || 1.x
-      webpack: ^5.0.0
-    peerDependenciesMeta:
-      '@rspack/core':
-        optional: true
-      webpack:
-        optional: true
-
-  css-select@5.2.2:
-    resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
-
-  css-what@6.2.2:
-    resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
-    engines: {node: '>= 6'}
-
-  cssesc@3.0.0:
-    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
-    engines: {node: '>=4'}
-    hasBin: true
-
-  currently-unhandled@0.4.1:
-    resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
-    engines: {node: '>=0.10.0'}
-
-  custom-event-polyfill@1.0.7:
-    resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==}
-
-  custom-event@1.0.1:
-    resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==}
-
-  cytoscape-cose-bilkent@4.1.0:
-    resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
-    peerDependencies:
-      cytoscape: ^3.2.0
-
-  cytoscape-fcose@2.2.0:
-    resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==}
-    peerDependencies:
-      cytoscape: ^3.2.0
-
-  cytoscape@3.33.1:
-    resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==}
-    engines: {node: '>=0.10'}
-
-  d3-array@2.12.1:
-    resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
-
-  d3-array@3.2.4:
-    resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
-    engines: {node: '>=12'}
-
-  d3-axis@3.0.0:
-    resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
-    engines: {node: '>=12'}
-
-  d3-brush@3.0.0:
-    resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
-    engines: {node: '>=12'}
-
-  d3-chord@3.0.1:
-    resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
-    engines: {node: '>=12'}
-
-  d3-color@3.1.0:
-    resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
-    engines: {node: '>=12'}
-
-  d3-contour@4.0.2:
-    resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
-    engines: {node: '>=12'}
-
-  d3-delaunay@6.0.4:
-    resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
-    engines: {node: '>=12'}
-
-  d3-dispatch@3.0.1:
-    resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
-    engines: {node: '>=12'}
-
-  d3-drag@3.0.0:
-    resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
-    engines: {node: '>=12'}
-
-  d3-dsv@3.0.1:
-    resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
-    engines: {node: '>=12'}
-    hasBin: true
-
-  d3-ease@3.0.1:
-    resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
-    engines: {node: '>=12'}
-
-  d3-fetch@3.0.1:
-    resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
-    engines: {node: '>=12'}
-
-  d3-force@3.0.0:
-    resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
-    engines: {node: '>=12'}
-
-  d3-format@3.1.2:
-    resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
-    engines: {node: '>=12'}
-
-  d3-geo@3.1.1:
-    resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
-    engines: {node: '>=12'}
-
-  d3-hierarchy@3.1.2:
-    resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
-    engines: {node: '>=12'}
-
-  d3-interpolate@3.0.1:
-    resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
-    engines: {node: '>=12'}
-
-  d3-path@1.0.9:
-    resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
-
-  d3-path@3.1.0:
-    resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
-    engines: {node: '>=12'}
-
-  d3-polygon@3.0.1:
-    resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
-    engines: {node: '>=12'}
-
-  d3-quadtree@3.0.1:
-    resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
-    engines: {node: '>=12'}
-
-  d3-random@3.0.1:
-    resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
-    engines: {node: '>=12'}
-
-  d3-sankey@0.12.3:
-    resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==}
-
-  d3-scale-chromatic@3.1.0:
-    resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
-    engines: {node: '>=12'}
-
-  d3-scale@4.0.2:
-    resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
-    engines: {node: '>=12'}
-
-  d3-selection@3.0.0:
-    resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
-    engines: {node: '>=12'}
-
-  d3-shape@1.3.7:
-    resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==}
-
-  d3-shape@3.2.0:
-    resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
-    engines: {node: '>=12'}
-
-  d3-time-format@4.1.0:
-    resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
-    engines: {node: '>=12'}
-
-  d3-time@3.1.0:
-    resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
-    engines: {node: '>=12'}
-
-  d3-timer@3.0.1:
-    resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
-    engines: {node: '>=12'}
-
-  d3-transition@3.0.1:
-    resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
-    engines: {node: '>=12'}
-    peerDependencies:
-      d3-selection: 2 - 3
-
-  d3-zoom@3.0.0:
-    resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
-    engines: {node: '>=12'}
-
-  d3@7.9.0:
-    resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
-    engines: {node: '>=12'}
-
-  dagre-d3-es@7.0.13:
-    resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==}
-
-  dashdash@1.14.1:
-    resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
-    engines: {node: '>=0.10'}
-
-  data-uri-to-buffer@4.0.1:
-    resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
-    engines: {node: '>= 12'}
-
-  data-view-buffer@1.0.2:
-    resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
-    engines: {node: '>= 0.4'}
-
-  data-view-byte-length@1.0.2:
-    resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==}
-    engines: {node: '>= 0.4'}
-
-  data-view-byte-offset@1.0.1:
-    resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
-    engines: {node: '>= 0.4'}
-
-  date-fns@2.30.0:
-    resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
-    engines: {node: '>=0.11'}
-
-  date-format@4.0.14:
-    resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
-    engines: {node: '>=4.0'}
-
-  dayjs@1.11.19:
-    resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
-
-  debug@2.6.9:
-    resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
-  debug@3.2.7:
-    resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
-  debug@4.3.4:
-    resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
-    engines: {node: '>=6.0'}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
-  debug@4.4.3:
-    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
-    engines: {node: '>=6.0'}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
-  decamelize-keys@1.1.1:
-    resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
-    engines: {node: '>=0.10.0'}
-
-  decamelize@1.2.0:
-    resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
-    engines: {node: '>=0.10.0'}
-
-  decode-uri-component@0.2.2:
-    resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
-    engines: {node: '>=0.10'}
-
-  deep-is@0.1.4:
-    resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
-
-  default-gateway@6.0.3:
-    resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==}
-    engines: {node: '>= 10'}
-
-  defaults@1.0.4:
-    resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
-
-  define-data-property@1.1.4:
-    resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
-    engines: {node: '>= 0.4'}
-
-  define-lazy-prop@2.0.0:
-    resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
-    engines: {node: '>=8'}
-
-  define-properties@1.2.1:
-    resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
-    engines: {node: '>= 0.4'}
-
-  define-property@0.2.5:
-    resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==}
-    engines: {node: '>=0.10.0'}
-
-  define-property@1.0.0:
-    resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==}
-    engines: {node: '>=0.10.0'}
-
-  define-property@2.0.2:
-    resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==}
-    engines: {node: '>=0.10.0'}
-
-  del@2.2.2:
-    resolution: {integrity: sha512-Z4fzpbIRjOu7lO5jCETSWoqUDVe0IPOlfugBsF6suen2LKDlVb4QZpKEM9P+buNJ4KI1eN7I083w/pbKUpsrWQ==}
-    engines: {node: '>=0.10.0'}
-
-  delaunator@5.0.1:
-    resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
-
-  delayed-stream@1.0.0:
-    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
-    engines: {node: '>=0.4.0'}
-
-  delegate@3.2.0:
-    resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
-
-  depd@1.1.2:
-    resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
-    engines: {node: '>= 0.6'}
-
-  depd@2.0.0:
-    resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
-    engines: {node: '>= 0.8'}
-
-  dequal@2.0.3:
-    resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
-    engines: {node: '>=6'}
-
-  destroy@1.2.0:
-    resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
-    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
-
-  detect-node@2.1.0:
-    resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
-
-  di@0.0.1:
-    resolution: {integrity: sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==}
-
-  didyoumean@1.2.2:
-    resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
-
-  diff-sequences@29.6.3:
-    resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
-  diff@4.0.2:
-    resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
-    engines: {node: '>=0.3.1'}
-
-  dir-glob@2.2.2:
-    resolution: {integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==}
-    engines: {node: '>=4'}
-
-  dir-glob@3.0.1:
-    resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
-    engines: {node: '>=8'}
-
-  dns-packet@5.6.1:
-    resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
-    engines: {node: '>=6'}
-
-  doctrine@2.1.0:
-    resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
-    engines: {node: '>=0.10.0'}
-
-  doctrine@3.0.0:
-    resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
-    engines: {node: '>=6.0.0'}
-
-  dom-serialize@2.2.1:
-    resolution: {integrity: sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==}
-
-  dom-serializer@0.2.2:
-    resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
-
-  dom-serializer@2.0.0:
-    resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
-
-  domelementtype@1.3.1:
-    resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==}
-
-  domelementtype@2.3.0:
-    resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
-
-  domhandler@2.4.2:
-    resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==}
-
-  domhandler@5.0.3:
-    resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
-    engines: {node: '>= 4'}
-
-  dompurify@3.3.1:
-    resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
-
-  domutils@1.7.0:
-    resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
-
-  domutils@3.2.2:
-    resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
-
-  dot-prop@5.3.0:
-    resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
-    engines: {node: '>=8'}
-
-  dotenv-expand@11.0.7:
-    resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==}
-    engines: {node: '>=12'}
-
-  dotenv@16.4.7:
-    resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
-    engines: {node: '>=12'}
-
-  dunder-proto@1.0.1:
-    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
-    engines: {node: '>= 0.4'}
-
-  duplexer@0.1.2:
-    resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
-
-  eastasianwidth@0.2.0:
-    resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
-
-  ecc-jsbn@0.1.2:
-    resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
-
-  echarts@5.4.3:
-    resolution: {integrity: sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==}
-
-  ee-first@1.1.1:
-    resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
-
-  ejs@3.1.10:
-    resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
-    engines: {node: '>=0.10.0'}
-    hasBin: true
-
-  electron-to-chromium@1.5.267:
-    resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
-
-  emoji-regex@7.0.3:
-    resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==}
-
-  emoji-regex@8.0.0:
-    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
-
-  emoji-regex@9.2.2:
-    resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
-
-  emoji-toolkit@9.0.1:
-    resolution: {integrity: sha512-sMMNqKNLVHXJfIKoPbrRJwtYuysVNC9GlKetr72zE3SSVbHqoeDLWVrxP0uM0AE0qvdl3hbUk+tJhhwXZrDHaw==}
-
-  emojis-list@3.0.0:
-    resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
-    engines: {node: '>= 4'}
-
-  encodeurl@1.0.2:
-    resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
-    engines: {node: '>= 0.8'}
-
-  encodeurl@2.0.0:
-    resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
-    engines: {node: '>= 0.8'}
-
-  encoding@0.1.13:
-    resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
-
-  end-of-stream@1.4.5:
-    resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
-
-  engine.io-parser@5.2.3:
-    resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
-    engines: {node: '>=10.0.0'}
-
-  engine.io@6.6.5:
-    resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==}
-    engines: {node: '>=10.2.0'}
-
-  enhanced-resolve@5.18.4:
-    resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
-    engines: {node: '>=10.13.0'}
-
-  enquirer@2.3.6:
-    resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
-    engines: {node: '>=8.6'}
-
-  ent@2.2.2:
-    resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==}
-    engines: {node: '>= 0.4'}
-
-  entities@1.1.2:
-    resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==}
-
-  entities@2.2.0:
-    resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
-
-  entities@4.5.0:
-    resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
-    engines: {node: '>=0.12'}
-
-  entities@6.0.1:
-    resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
-    engines: {node: '>=0.12'}
-
-  env-paths@2.2.1:
-    resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
-    engines: {node: '>=6'}
-
-  err-code@2.0.3:
-    resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
-
-  errno@0.1.8:
-    resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
-    hasBin: true
-
-  error-ex@1.3.4:
-    resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
-
-  es-abstract@1.24.1:
-    resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==}
-    engines: {node: '>= 0.4'}
-
-  es-define-property@1.0.1:
-    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
-    engines: {node: '>= 0.4'}
-
-  es-errors@1.3.0:
-    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
-    engines: {node: '>= 0.4'}
-
-  es-module-lexer@1.7.0:
-    resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
-
-  es-object-atoms@1.1.1:
-    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
-    engines: {node: '>= 0.4'}
-
-  es-set-tostringtag@2.1.0:
-    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
-    engines: {node: '>= 0.4'}
-
-  es-shim-unscopables@1.1.0:
-    resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==}
-    engines: {node: '>= 0.4'}
-
-  es-to-primitive@1.3.0:
-    resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
-    engines: {node: '>= 0.4'}
-
-  es6-promise@3.3.1:
-    resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
-
-  es6-promise@4.2.8:
-    resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
-
-  es6-promisify@5.0.0:
-    resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==}
-
-  esbuild-wasm@0.20.1:
-    resolution: {integrity: sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==}
-    engines: {node: '>=12'}
-    hasBin: true
-
-  esbuild@0.19.12:
-    resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
-    engines: {node: '>=12'}
-    hasBin: true
-
-  esbuild@0.20.1:
-    resolution: {integrity: sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==}
-    engines: {node: '>=12'}
-    hasBin: true
-
-  escalade@3.2.0:
-    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
-    engines: {node: '>=6'}
-
-  escape-html@1.0.3:
-    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
-
-  escape-string-regexp@1.0.5:
-    resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
-    engines: {node: '>=0.8.0'}
-
-  escape-string-regexp@4.0.0:
-    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
-    engines: {node: '>=10'}
-
-  eslint-config-prettier@8.6.0:
-    resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==}
-    hasBin: true
-    peerDependencies:
-      eslint: '>=7.0.0'
-
-  eslint-import-resolver-node@0.3.9:
-    resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
-
-  eslint-module-utils@2.12.1:
-    resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==}
-    engines: {node: '>=4'}
-    peerDependencies:
-      '@typescript-eslint/parser': '*'
-      eslint: '*'
-      eslint-import-resolver-node: '*'
-      eslint-import-resolver-typescript: '*'
-      eslint-import-resolver-webpack: '*'
-    peerDependenciesMeta:
-      '@typescript-eslint/parser':
-        optional: true
-      eslint:
-        optional: true
-      eslint-import-resolver-node:
-        optional: true
-      eslint-import-resolver-typescript:
-        optional: true
-      eslint-import-resolver-webpack:
-        optional: true
-
-  eslint-plugin-deprecation@3.0.0:
-    resolution: {integrity: sha512-JuVLdNg/uf0Adjg2tpTyYoYaMbwQNn/c78P1HcccokvhtRphgnRjZDKmhlxbxYptppex03zO76f97DD/yQHv7A==}
-    peerDependencies:
-      eslint: ^8.0.0
-      typescript: ^4.2.4 || ^5.0.0
-
-  eslint-plugin-import@2.26.0:
-    resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
-    engines: {node: '>=4'}
-    peerDependencies:
-      '@typescript-eslint/parser': '*'
-      eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
-    peerDependenciesMeta:
-      '@typescript-eslint/parser':
-        optional: true
-
-  eslint-plugin-jsdoc@48.2.5:
-    resolution: {integrity: sha512-ZeTfKV474W1N9niWfawpwsXGu+ZoMXu4417eBROX31d7ZuOk8zyG66SO77DpJ2+A9Wa2scw/jRqBPnnQo7VbcQ==}
-    engines: {node: '>=18'}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
-
-  eslint-plugin-prefer-arrow@1.2.3:
-    resolution: {integrity: sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==}
-    peerDependencies:
-      eslint: '>=2.0.0'
-
-  eslint-plugin-prettier@4.2.1:
-    resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
-    engines: {node: '>=12.0.0'}
-    peerDependencies:
-      eslint: '>=7.28.0'
-      eslint-config-prettier: '*'
-      prettier: '>=2.0.0'
-    peerDependenciesMeta:
-      eslint-config-prettier:
-        optional: true
-
-  eslint-scope@5.1.1:
-    resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
-    engines: {node: '>=8.0.0'}
-
-  eslint-scope@7.2.2:
-    resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
-  eslint-scope@8.4.0:
-    resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
-    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
-  eslint-visitor-keys@3.4.3:
-    resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
-  eslint@8.56.0:
-    resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
-    hasBin: true
-
-  espree@9.6.1:
-    resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
-  esprima@4.0.1:
-    resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
-    engines: {node: '>=4'}
-    hasBin: true
-
-  esquery@1.7.0:
-    resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
-    engines: {node: '>=0.10'}
-
-  esrecurse@4.3.0:
-    resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
-    engines: {node: '>=4.0'}
-
-  estraverse@4.3.0:
-    resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==}
-    engines: {node: '>=4.0'}
-
-  estraverse@5.3.0:
-    resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
-    engines: {node: '>=4.0'}
-
-  esutils@2.0.3:
-    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
-    engines: {node: '>=0.10.0'}
-
-  eta@2.2.0:
-    resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==}
-    engines: {node: '>=6.0.0'}
-
-  etag@1.8.1:
-    resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
-    engines: {node: '>= 0.6'}
-
-  eventemitter3@4.0.7:
-    resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
-
-  eventemitter3@5.0.1:
-    resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
-
-  events@3.3.0:
-    resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
-    engines: {node: '>=0.8.x'}
-
-  execa@5.1.1:
-    resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
-    engines: {node: '>=10'}
-
-  execa@7.2.0:
-    resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==}
-    engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
-
-  execall@1.0.0:
-    resolution: {integrity: sha512-/J0Q8CvOvlAdpvhfkD/WnTQ4H1eU0exze2nFGPj/RSC7jpQ0NkKe2r28T5eMkhEEs+fzepMZNy1kVRKNlC04nQ==}
-    engines: {node: '>=0.10.0'}
-
-  exit@0.1.2:
-    resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
-    engines: {node: '>= 0.8.0'}
-
-  expand-brackets@2.1.4:
-    resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
-    engines: {node: '>=0.10.0'}
-
-  exponential-backoff@3.1.3:
-    resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
-
-  express@4.22.1:
-    resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
-    engines: {node: '>= 0.10.0'}
-
-  extend-shallow@2.0.1:
-    resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
-    engines: {node: '>=0.10.0'}
-
-  extend-shallow@3.0.2:
-    resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==}
-    engines: {node: '>=0.10.0'}
-
-  extend@3.0.2:
-    resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
-
-  external-editor@3.1.0:
-    resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
-    engines: {node: '>=4'}
-
-  extglob@2.0.4:
-    resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==}
-    engines: {node: '>=0.10.0'}
-
-  extsprintf@1.3.0:
-    resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
-    engines: {'0': node >=0.6.0}
-
-  fast-deep-equal@3.1.3:
-    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
-
-  fast-diff@1.3.0:
-    resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
-
-  fast-glob@2.2.7:
-    resolution: {integrity: sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==}
-    engines: {node: '>=4.0.0'}
-
-  fast-glob@3.3.2:
-    resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
-    engines: {node: '>=8.6.0'}
-
-  fast-glob@3.3.3:
-    resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
-    engines: {node: '>=8.6.0'}
-
-  fast-json-stable-stringify@2.1.0:
-    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
-
-  fast-levenshtein@2.0.6:
-    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
-
-  fast-safe-stringify@2.1.1:
-    resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
-
-  fastest-levenshtein@1.0.16:
-    resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
-    engines: {node: '>= 4.9.1'}
-
-  fastq@1.20.1:
-    resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
-
-  faye-websocket@0.11.4:
-    resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
-    engines: {node: '>=0.8.0'}
-
-  fetch-blob@3.2.0:
-    resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
-    engines: {node: ^12.20 || >= 14.13}
-
-  figures@3.2.0:
-    resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
-    engines: {node: '>=8'}
-
-  file-entry-cache@4.0.0:
-    resolution: {integrity: sha512-AVSwsnbV8vH/UVbvgEhf3saVQXORNv0ZzSkvkhQIaia5Tia+JhGTaa/ePUSVoPHQyGayQNmYfkzFi3WZV5zcpA==}
-    engines: {node: '>=4'}
-
-  file-entry-cache@6.0.1:
-    resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
-    engines: {node: ^10.12.0 || >=12.0.0}
-
-  file-saver@2.0.5:
-    resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
-
-  filelist@1.0.4:
-    resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
-
-  fill-range@4.0.0:
-    resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==}
-    engines: {node: '>=0.10.0'}
-
-  fill-range@7.1.1:
-    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
-    engines: {node: '>=8'}
-
-  finalhandler@1.1.2:
-    resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
-    engines: {node: '>= 0.8'}
-
-  finalhandler@1.3.2:
-    resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
-    engines: {node: '>= 0.8'}
-
-  find-cache-dir@4.0.0:
-    resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==}
-    engines: {node: '>=14.16'}
-
-  find-up@2.1.0:
-    resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==}
-    engines: {node: '>=4'}
-
-  find-up@4.1.0:
-    resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
-    engines: {node: '>=8'}
-
-  find-up@5.0.0:
-    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
-    engines: {node: '>=10'}
-
-  find-up@6.3.0:
-    resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  flat-cache@2.0.1:
-    resolution: {integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==}
-    engines: {node: '>=4'}
-
-  flat-cache@3.2.0:
-    resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
-    engines: {node: ^10.12.0 || >=12.0.0}
-
-  flat@5.0.2:
-    resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
-    hasBin: true
-
-  flatted@2.0.2:
-    resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==}
-
-  flatted@3.3.3:
-    resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
-
-  follow-redirects@1.15.11:
-    resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
-    engines: {node: '>=4.0'}
-    peerDependencies:
-      debug: '*'
-    peerDependenciesMeta:
-      debug:
-        optional: true
-
-  for-each@0.3.5:
-    resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
-    engines: {node: '>= 0.4'}
-
-  for-in@1.0.2:
-    resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==}
-    engines: {node: '>=0.10.0'}
-
-  foreground-child@3.3.1:
-    resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
-    engines: {node: '>=14'}
-
-  forever-agent@0.6.1:
-    resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
-
-  form-data@2.3.3:
-    resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
-    engines: {node: '>= 0.12'}
-
-  form-data@4.0.5:
-    resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
-    engines: {node: '>= 6'}
-
-  formdata-polyfill@4.0.10:
-    resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
-    engines: {node: '>=12.20.0'}
-
-  forwarded@0.2.0:
-    resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
-    engines: {node: '>= 0.6'}
-
-  frac@1.1.2:
-    resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
-    engines: {node: '>=0.8'}
-
-  fraction.js@4.3.7:
-    resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
-
-  fragment-cache@0.2.1:
-    resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
-    engines: {node: '>=0.10.0'}
-
-  fresh@0.5.2:
-    resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
-    engines: {node: '>= 0.6'}
-
-  front-matter@4.0.2:
-    resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==}
-
-  fs-constants@1.0.0:
-    resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
-
-  fs-extra@8.1.0:
-    resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
-    engines: {node: '>=6 <7 || >=8'}
-
-  fs-minipass@2.1.0:
-    resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
-    engines: {node: '>= 8'}
-
-  fs-minipass@3.0.3:
-    resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  fs-monkey@1.1.0:
-    resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==}
-
-  fs.realpath@1.0.0:
-    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
-
-  fsevents@2.3.3:
-    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
-    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
-    os: [darwin]
-
-  function-bind@1.1.2:
-    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
-
-  function.prototype.name@1.1.8:
-    resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
-    engines: {node: '>= 0.4'}
-
-  functions-have-names@1.2.3:
-    resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
-
-  generator-function@2.0.1:
-    resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
-    engines: {node: '>= 0.4'}
-
-  gensync@1.0.0-beta.2:
-    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
-    engines: {node: '>=6.9.0'}
-
-  get-caller-file@2.0.5:
-    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
-    engines: {node: 6.* || 8.* || >= 10.*}
-
-  get-intrinsic@1.3.0:
-    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
-    engines: {node: '>= 0.4'}
-
-  get-package-type@0.1.0:
-    resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
-    engines: {node: '>=8.0.0'}
-
-  get-proto@1.0.1:
-    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
-    engines: {node: '>= 0.4'}
-
-  get-stdin@6.0.0:
-    resolution: {integrity: sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==}
-    engines: {node: '>=4'}
-
-  get-stream@6.0.1:
-    resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
-    engines: {node: '>=10'}
-
-  get-symbol-description@1.1.0:
-    resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
-    engines: {node: '>= 0.4'}
-
-  get-value@2.0.6:
-    resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==}
-    engines: {node: '>=0.10.0'}
-
-  getpass@0.1.7:
-    resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
-
-  glob-parent@3.1.0:
-    resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==}
-
-  glob-parent@5.1.2:
-    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
-    engines: {node: '>= 6'}
-
-  glob-parent@6.0.2:
-    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
-    engines: {node: '>=10.13.0'}
-
-  glob-to-regexp@0.3.0:
-    resolution: {integrity: sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==}
-
-  glob-to-regexp@0.4.1:
-    resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
-
-  glob@10.5.0:
-    resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
-    hasBin: true
-
-  glob@7.2.3:
-    resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
-    deprecated: Glob versions prior to v9 are no longer supported
-
-  global-modules@2.0.0:
-    resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
-    engines: {node: '>=6'}
-
-  global-prefix@3.0.0:
-    resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==}
-    engines: {node: '>=6'}
-
-  globals@13.24.0:
-    resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
-    engines: {node: '>=8'}
-
-  globalthis@1.0.4:
-    resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
-    engines: {node: '>= 0.4'}
-
-  globby@11.1.0:
-    resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
-    engines: {node: '>=10'}
-
-  globby@13.2.2:
-    resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  globby@5.0.0:
-    resolution: {integrity: sha512-HJRTIH2EeH44ka+LWig+EqT2ONSYpVlNfx6pyd592/VF1TbfljJ7elwie7oSwcViLGqOdWocSdu2txwBF9bjmQ==}
-    engines: {node: '>=0.10.0'}
-
-  globby@9.2.0:
-    resolution: {integrity: sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==}
-    engines: {node: '>=6'}
-
-  globjoin@0.1.4:
-    resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
-
-  gonzales-pe@4.3.0:
-    resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==}
-    engines: {node: '>=0.6.0'}
-    hasBin: true
-
-  good-listener@1.2.2:
-    resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
-
-  gopd@1.2.0:
-    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
-    engines: {node: '>= 0.4'}
-
-  graceful-fs@4.2.11:
-    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
-
-  graphemer@1.4.0:
-    resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
-
-  gzip-size@6.0.0:
-    resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
-    engines: {node: '>=10'}
-
-  hachure-fill@0.5.2:
-    resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
-
-  handle-thing@2.0.1:
-    resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
-
-  har-schema@2.0.0:
-    resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
-    engines: {node: '>=4'}
-
-  har-validator@5.1.5:
-    resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==}
-    engines: {node: '>=6'}
-    deprecated: this library is no longer supported
-
-  hard-rejection@2.1.0:
-    resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
-    engines: {node: '>=6'}
-
-  has-ansi@2.0.0:
-    resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
-    engines: {node: '>=0.10.0'}
-
-  has-bigints@1.1.0:
-    resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
-    engines: {node: '>= 0.4'}
-
-  has-flag@3.0.0:
-    resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
-    engines: {node: '>=4'}
-
-  has-flag@4.0.0:
-    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
-    engines: {node: '>=8'}
-
-  has-property-descriptors@1.0.2:
-    resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
-
-  has-proto@1.2.0:
-    resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
-    engines: {node: '>= 0.4'}
-
-  has-symbols@1.1.0:
-    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
-    engines: {node: '>= 0.4'}
-
-  has-tostringtag@1.0.2:
-    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
-    engines: {node: '>= 0.4'}
-
-  has-value@0.3.1:
-    resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==}
-    engines: {node: '>=0.10.0'}
-
-  has-value@1.0.0:
-    resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==}
-    engines: {node: '>=0.10.0'}
-
-  has-values@0.1.4:
-    resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==}
-    engines: {node: '>=0.10.0'}
-
-  has-values@1.0.0:
-    resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==}
-    engines: {node: '>=0.10.0'}
-
-  has@1.0.4:
-    resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==}
-    engines: {node: '>= 0.4.0'}
-
-  hasown@2.0.2:
-    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
-    engines: {node: '>= 0.4'}
-
-  hosted-git-info@2.8.9:
-    resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
-
-  hosted-git-info@4.1.0:
-    resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
-    engines: {node: '>=10'}
-
-  hosted-git-info@7.0.2:
-    resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  hpack.js@2.1.6:
-    resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==}
-
-  html-entities@2.6.0:
-    resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
-
-  html-escaper@2.0.2:
-    resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
-
-  html-tags@2.0.0:
-    resolution: {integrity: sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==}
-    engines: {node: '>=4'}
-
-  html-tags@3.3.1:
-    resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
-    engines: {node: '>=8'}
-
-  htmlparser2@3.10.1:
-    resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
-
-  htmlparser2@8.0.2:
-    resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
-
-  http-cache-semantics@4.2.0:
-    resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
-
-  http-deceiver@1.2.7:
-    resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==}
-
-  http-errors@1.6.3:
-    resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==}
-    engines: {node: '>= 0.6'}
-
-  http-errors@2.0.1:
-    resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
-    engines: {node: '>= 0.8'}
-
-  http-parser-js@0.5.10:
-    resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
-
-  http-proxy-agent@7.0.2:
-    resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
-    engines: {node: '>= 14'}
-
-  http-proxy-middleware@2.0.6:
-    resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==}
-    engines: {node: '>=12.0.0'}
-    peerDependencies:
-      '@types/express': ^4.17.13
-    peerDependenciesMeta:
-      '@types/express':
-        optional: true
-
-  http-proxy@1.18.1:
-    resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
-    engines: {node: '>=8.0.0'}
-
-  http-signature@1.2.0:
-    resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
-    engines: {node: '>=0.8', npm: '>=1.3.7'}
-
-  http2-client@1.3.5:
-    resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==}
-
-  https-proxy-agent@2.2.4:
-    resolution: {integrity: sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==}
-    engines: {node: '>= 4.5.0'}
-
-  https-proxy-agent@7.0.4:
-    resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
-    engines: {node: '>= 14'}
-
-  https-proxy-agent@7.0.6:
-    resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
-    engines: {node: '>= 14'}
-
-  human-signals@2.1.0:
-    resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
-    engines: {node: '>=10.17.0'}
-
-  human-signals@4.3.1:
-    resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==}
-    engines: {node: '>=14.18.0'}
-
-  husky@7.0.4:
-    resolution: {integrity: sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==}
-    engines: {node: '>=12'}
-    hasBin: true
-
-  iconv-lite@0.4.24:
-    resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
-    engines: {node: '>=0.10.0'}
-
-  iconv-lite@0.6.3:
-    resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
-    engines: {node: '>=0.10.0'}
-
-  icss-utils@5.1.0:
-    resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
-  ieee754@1.2.1:
-    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
-
-  ignore-walk@6.0.5:
-    resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  ignore@4.0.6:
-    resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
-    engines: {node: '>= 4'}
-
-  ignore@5.3.1:
-    resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
-    engines: {node: '>= 4'}
-
-  ignore@5.3.2:
-    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
-    engines: {node: '>= 4'}
-
-  image-size@0.5.5:
-    resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==}
-    engines: {node: '>=0.10.0'}
-    hasBin: true
-
-  immediate@3.0.6:
-    resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
-
-  immutable@4.3.7:
-    resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==}
-
-  import-fresh@2.0.0:
-    resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}
-    engines: {node: '>=4'}
-
-  import-fresh@3.3.1:
-    resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
-    engines: {node: '>=6'}
-
-  import-lazy@3.1.0:
-    resolution: {integrity: sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==}
-    engines: {node: '>=6'}
-
-  import-lazy@4.0.0:
-    resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
-    engines: {node: '>=8'}
-
-  imurmurhash@0.1.4:
-    resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
-    engines: {node: '>=0.8.19'}
-
-  indent-string@3.2.0:
-    resolution: {integrity: sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==}
-    engines: {node: '>=4'}
-
-  indent-string@4.0.0:
-    resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
-    engines: {node: '>=8'}
-
-  indexes-of@1.0.1:
-    resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==}
-
-  inflight@1.0.6:
-    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
-    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
-
-  inherits@2.0.3:
-    resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==}
-
-  inherits@2.0.4:
-    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
-
-  ini@1.3.8:
-    resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
-
-  ini@4.1.2:
-    resolution: {integrity: sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  ini@4.1.3:
-    resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  inquirer@9.2.15:
-    resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==}
-    engines: {node: '>=18'}
-
-  internal-slot@1.1.0:
-    resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
-    engines: {node: '>= 0.4'}
-
-  internmap@1.0.1:
-    resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
-
-  internmap@2.0.3:
-    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
-    engines: {node: '>=12'}
-
-  ip-address@10.1.0:
-    resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
-    engines: {node: '>= 12'}
-
-  ipaddr.js@1.9.1:
-    resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
-    engines: {node: '>= 0.10'}
-
-  ipaddr.js@2.3.0:
-    resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
-    engines: {node: '>= 10'}
-
-  is-accessor-descriptor@1.0.1:
-    resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==}
-    engines: {node: '>= 0.10'}
-
-  is-alphabetical@1.0.4:
-    resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
-
-  is-alphanumeric@1.0.0:
-    resolution: {integrity: sha512-ZmRL7++ZkcMOfDuWZuMJyIVLr2keE1o/DeNWh1EmgqGhUcV+9BIVsx0BcSBOHTZqzjs4+dISzr2KAeBEWGgXeA==}
-    engines: {node: '>=0.10.0'}
-
-  is-alphanumerical@1.0.4:
-    resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==}
-
-  is-array-buffer@3.0.5:
-    resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
-    engines: {node: '>= 0.4'}
-
-  is-arrayish@0.2.1:
-    resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
-
-  is-async-function@2.1.1:
-    resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
-    engines: {node: '>= 0.4'}
-
-  is-bigint@1.1.0:
-    resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
-    engines: {node: '>= 0.4'}
-
-  is-binary-path@2.1.0:
-    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
-    engines: {node: '>=8'}
-
-  is-boolean-object@1.2.2:
-    resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
-    engines: {node: '>= 0.4'}
-
-  is-buffer@1.1.6:
-    resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
-
-  is-buffer@2.0.5:
-    resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
-    engines: {node: '>=4'}
-
-  is-builtin-module@3.2.1:
-    resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
-    engines: {node: '>=6'}
-
-  is-callable@1.2.7:
-    resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
-    engines: {node: '>= 0.4'}
-
-  is-core-module@2.16.1:
-    resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
-    engines: {node: '>= 0.4'}
-
-  is-data-descriptor@1.0.1:
-    resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==}
-    engines: {node: '>= 0.4'}
-
-  is-data-view@1.0.2:
-    resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==}
-    engines: {node: '>= 0.4'}
-
-  is-date-object@1.1.0:
-    resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
-    engines: {node: '>= 0.4'}
-
-  is-decimal@1.0.4:
-    resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==}
-
-  is-descriptor@0.1.7:
-    resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==}
-    engines: {node: '>= 0.4'}
-
-  is-descriptor@1.0.3:
-    resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==}
-    engines: {node: '>= 0.4'}
-
-  is-directory@0.3.1:
-    resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==}
-    engines: {node: '>=0.10.0'}
-
-  is-docker@2.2.1:
-    resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
-    engines: {node: '>=8'}
-    hasBin: true
-
-  is-extendable@0.1.1:
-    resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
-    engines: {node: '>=0.10.0'}
-
-  is-extendable@1.0.1:
-    resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==}
-    engines: {node: '>=0.10.0'}
-
-  is-extglob@2.1.1:
-    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
-    engines: {node: '>=0.10.0'}
-
-  is-finalizationregistry@1.1.1:
-    resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
-    engines: {node: '>= 0.4'}
-
-  is-fullwidth-code-point@2.0.0:
-    resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
-    engines: {node: '>=4'}
-
-  is-fullwidth-code-point@3.0.0:
-    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
-    engines: {node: '>=8'}
-
-  is-fullwidth-code-point@4.0.0:
-    resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
-    engines: {node: '>=12'}
-
-  is-generator-function@1.1.2:
-    resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
-    engines: {node: '>= 0.4'}
-
-  is-glob@3.1.0:
-    resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==}
-    engines: {node: '>=0.10.0'}
-
-  is-glob@4.0.3:
-    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
-    engines: {node: '>=0.10.0'}
-
-  is-hexadecimal@1.0.4:
-    resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
-
-  is-interactive@1.0.0:
-    resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
-    engines: {node: '>=8'}
-
-  is-lambda@1.0.1:
-    resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
-
-  is-map@2.0.3:
-    resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
-    engines: {node: '>= 0.4'}
-
-  is-negative-zero@2.0.3:
-    resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
-    engines: {node: '>= 0.4'}
-
-  is-number-object@1.1.1:
-    resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
-    engines: {node: '>= 0.4'}
-
-  is-number@3.0.0:
-    resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==}
-    engines: {node: '>=0.10.0'}
-
-  is-number@7.0.0:
-    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
-    engines: {node: '>=0.12.0'}
-
-  is-obj@2.0.0:
-    resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
-    engines: {node: '>=8'}
-
-  is-path-cwd@1.0.0:
-    resolution: {integrity: sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw==}
-    engines: {node: '>=0.10.0'}
-
-  is-path-in-cwd@1.0.1:
-    resolution: {integrity: sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==}
-    engines: {node: '>=0.10.0'}
-
-  is-path-inside@1.0.1:
-    resolution: {integrity: sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==}
-    engines: {node: '>=0.10.0'}
-
-  is-path-inside@3.0.3:
-    resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
-    engines: {node: '>=8'}
-
-  is-plain-obj@1.1.0:
-    resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
-    engines: {node: '>=0.10.0'}
-
-  is-plain-obj@3.0.0:
-    resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==}
-    engines: {node: '>=10'}
-
-  is-plain-object@2.0.4:
-    resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
-    engines: {node: '>=0.10.0'}
-
-  is-plain-object@5.0.0:
-    resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
-    engines: {node: '>=0.10.0'}
-
-  is-regex@1.2.1:
-    resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
-    engines: {node: '>= 0.4'}
-
-  is-regexp@1.0.0:
-    resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==}
-    engines: {node: '>=0.10.0'}
-
-  is-set@2.0.3:
-    resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
-    engines: {node: '>= 0.4'}
-
-  is-shared-array-buffer@1.0.4:
-    resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
-    engines: {node: '>= 0.4'}
-
-  is-stream@2.0.1:
-    resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
-    engines: {node: '>=8'}
-
-  is-stream@3.0.0:
-    resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  is-string@1.1.1:
-    resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
-    engines: {node: '>= 0.4'}
-
-  is-supported-regexp-flag@1.0.1:
-    resolution: {integrity: sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ==}
-    engines: {node: '>=0.10.0'}
-
-  is-symbol@1.1.1:
-    resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
-    engines: {node: '>= 0.4'}
-
-  is-typed-array@1.1.15:
-    resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
-    engines: {node: '>= 0.4'}
-
-  is-typedarray@1.0.0:
-    resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
-
-  is-unicode-supported@0.1.0:
-    resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
-    engines: {node: '>=10'}
-
-  is-weakmap@2.0.2:
-    resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
-    engines: {node: '>= 0.4'}
-
-  is-weakref@1.1.1:
-    resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
-    engines: {node: '>= 0.4'}
-
-  is-weakset@2.0.4:
-    resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
-    engines: {node: '>= 0.4'}
-
-  is-what@3.14.1:
-    resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
-
-  is-whitespace-character@1.0.4:
-    resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
-
-  is-windows@1.0.2:
-    resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
-    engines: {node: '>=0.10.0'}
-
-  is-word-character@1.0.4:
-    resolution: {integrity: sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==}
-
-  is-wsl@2.2.0:
-    resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
-    engines: {node: '>=8'}
-
-  isarray@1.0.0:
-    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
-
-  isarray@2.0.5:
-    resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
-
-  isbinaryfile@4.0.10:
-    resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
-    engines: {node: '>= 8.0.0'}
-
-  isexe@2.0.0:
-    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
-
-  isexe@3.1.1:
-    resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
-    engines: {node: '>=16'}
-
-  isobject@2.1.0:
-    resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
-    engines: {node: '>=0.10.0'}
-
-  isobject@3.0.1:
-    resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
-    engines: {node: '>=0.10.0'}
-
-  isstream@0.1.2:
-    resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
-
-  istanbul-lib-coverage@3.2.2:
-    resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
-    engines: {node: '>=8'}
-
-  istanbul-lib-instrument@5.2.1:
-    resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
-    engines: {node: '>=8'}
-
-  istanbul-lib-report@3.0.1:
-    resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
-    engines: {node: '>=10'}
-
-  istanbul-lib-source-maps@4.0.1:
-    resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
-    engines: {node: '>=10'}
-
-  istanbul-reports@3.2.0:
-    resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
-    engines: {node: '>=8'}
-
-  isutf8@4.0.1:
-    resolution: {integrity: sha512-1pk2/2pE+G48eETnp4uOLxQ9WUCxD7oVauYwhFEAGREJPDxEO7iX9qstylrCcx3lNWa1RCS2DxGTxrHdWqS7/w==}
-    engines: {node: '>= 12'}
-
-  jackspeak@3.4.3:
-    resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
-
-  jake@10.9.4:
-    resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
-    engines: {node: '>=10'}
-    hasBin: true
-
-  jasmine-core@2.8.0:
-    resolution: {integrity: sha512-SNkOkS+/jMZvLhuSx1fjhcNWUC/KG6oVyFUGkSBEr9n1axSNduWU8GlI7suaHXr4yxjet6KjrUZxUTE5WzzWwQ==}
-
-  jasmine-core@4.3.0:
-    resolution: {integrity: sha512-qybtBUesniQdW6n+QIHMng2vDOHscIC/dEXjW+JzO9+LoAZMb03RCUC5xFOv/btSKPm1xL42fn+RjlU4oB42Lg==}
-
-  jasmine-spec-reporter@7.0.0:
-    resolution: {integrity: sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg==}
-
-  jasmine@2.8.0:
-    resolution: {integrity: sha512-KbdGQTf5jbZgltoHs31XGiChAPumMSY64OZMWLNYnEnMfG5uwGBhffePwuskexjT+/Jea/gU3qAU8344hNohSw==}
-    hasBin: true
-
-  jasminewd2@2.2.0:
-    resolution: {integrity: sha512-Rn0nZe4rfDhzA63Al3ZGh0E+JTmM6ESZYXJGKuqKGZObsAB9fwXPD03GjtIEvJBDOhN94T5MzbwZSqzFHSQPzg==}
-    engines: {node: '>= 6.9.x'}
-
-  jest-diff@29.7.0:
-    resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
-  jest-get-type@29.6.3:
-    resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
-  jest-worker@27.5.1:
-    resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
-    engines: {node: '>= 10.13.0'}
-
-  jiti@1.21.7:
-    resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
-    hasBin: true
-
-  jquery@3.7.1:
-    resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==}
-
-  js-tokens@4.0.0:
-    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
-
-  js-yaml@3.14.2:
-    resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
-    hasBin: true
-
-  js-yaml@4.1.0:
-    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
-    hasBin: true
-
-  js-yaml@4.1.1:
-    resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
-    hasBin: true
-
-  jsbn@0.1.1:
-    resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
-
-  jsdoc-type-pratt-parser@4.0.0:
-    resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==}
-    engines: {node: '>=12.0.0'}
-
-  jsesc@2.5.2:
-    resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
-    engines: {node: '>=4'}
-    hasBin: true
-
-  jsesc@3.1.0:
-    resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
-    engines: {node: '>=6'}
-    hasBin: true
-
-  json-buffer@3.0.1:
-    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
-
-  json-parse-better-errors@1.0.2:
-    resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
-
-  json-parse-even-better-errors@2.3.1:
-    resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
-
-  json-parse-even-better-errors@3.0.2:
-    resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  json-schema-traverse@0.4.1:
-    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
-
-  json-schema-traverse@1.0.0:
-    resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
-
-  json-schema@0.4.0:
-    resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
-
-  json-stable-stringify-without-jsonify@1.0.1:
-    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
-
-  json-stringify-safe@5.0.1:
-    resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
-
-  json5@1.0.2:
-    resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
-    hasBin: true
-
-  json5@2.2.3:
-    resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
-    engines: {node: '>=6'}
-    hasBin: true
-
-  jsonc-parser@3.2.0:
-    resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
-
-  jsonc-parser@3.2.1:
-    resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
-
-  jsonc-parser@3.3.1:
-    resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
-
-  jsonfile@4.0.0:
-    resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
-
-  jsonparse@1.3.1:
-    resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
-    engines: {'0': node >= 0.2.0}
-
-  jsprim@1.4.2:
-    resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
-    engines: {node: '>=0.6.0'}
-
-  jszip@3.10.1:
-    resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
-
-  karma-chrome-launcher@3.1.1:
-    resolution: {integrity: sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==}
-
-  karma-coverage@2.2.1:
-    resolution: {integrity: sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==}
-    engines: {node: '>=10.0.0'}
-
-  karma-jasmine-html-reporter@2.0.0:
-    resolution: {integrity: sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==}
-    peerDependencies:
-      jasmine-core: ^4.0.0
-      karma: ^6.0.0
-      karma-jasmine: ^5.0.0
-
-  karma-jasmine@5.1.0:
-    resolution: {integrity: sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==}
-    engines: {node: '>=12'}
-    peerDependencies:
-      karma: ^6.0.0
-
-  karma-source-map-support@1.4.0:
-    resolution: {integrity: sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==}
-
-  karma@6.4.2:
-    resolution: {integrity: sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==}
-    engines: {node: '>= 10'}
-    hasBin: true
-
-  katex@0.16.27:
-    resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==}
-    hasBin: true
-
-  keyv@4.5.4:
-    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
-
-  khroma@2.1.0:
-    resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
-
-  kind-of@3.2.2:
-    resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
-    engines: {node: '>=0.10.0'}
-
-  kind-of@4.0.0:
-    resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==}
-    engines: {node: '>=0.10.0'}
-
-  kind-of@6.0.3:
-    resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
-    engines: {node: '>=0.10.0'}
-
-  klona@2.0.6:
-    resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
-    engines: {node: '>= 8'}
-
-  known-css-properties@0.11.0:
-    resolution: {integrity: sha512-bEZlJzXo5V/ApNNa5z375mJC6Nrz4vG43UgcSCrg2OHC+yuB6j0iDSrY7RQ/+PRofFB03wNIIt9iXIVLr4wc7w==}
-
-  known-css-properties@0.26.0:
-    resolution: {integrity: sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==}
-
-  langium@3.3.1:
-    resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==}
-    engines: {node: '>=16.0.0'}
-
-  launch-editor@2.12.0:
-    resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==}
-
-  layout-base@1.0.2:
-    resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
-
-  layout-base@2.0.1:
-    resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
-
-  less-loader@11.1.0:
-    resolution: {integrity: sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==}
-    engines: {node: '>= 14.15.0'}
-    peerDependencies:
-      less: ^3.5.0 || ^4.0.0
-      webpack: ^5.0.0
-
-  less-plugin-clean-css@1.6.0:
-    resolution: {integrity: sha512-jwXX6WlXT57OVCXa5oBJBaJq1b4s1BOKeEEoAL2UTeEitogQWfTcBbLT/vow9pl0N0MXV8Mb4KyhTGG0YbEKyQ==}
-    engines: {node: '>=0.10'}
-
-  less-vars-to-js@1.3.0:
-    resolution: {integrity: sha512-xeiLLn/IMCGtdyCkYQnW8UuzoW2oYMCKg9boZRaGI58fLz5r90bNJDlqGzmVt/1Uqk75/DxIVtQSNCMkE5fRZQ==}
-    engines: {node: '>=8'}
-
-  less@4.2.0:
-    resolution: {integrity: sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==}
-    engines: {node: '>=6'}
-    hasBin: true
-
-  less@4.5.1:
-    resolution: {integrity: sha512-UKgI3/KON4u6ngSsnDADsUERqhZknsVZbnuzlRZXLQCmfC/MDld42fTydUE9B+Mla1AL6SJ/Pp6SlEFi/AVGfw==}
-    engines: {node: '>=14'}
-    hasBin: true
-
-  leven@2.1.0:
-    resolution: {integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==}
-    engines: {node: '>=0.10.0'}
-
-  levn@0.4.1:
-    resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
-    engines: {node: '>= 0.8.0'}
-
-  license-webpack-plugin@4.0.2:
-    resolution: {integrity: sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==}
-    peerDependencies:
-      webpack: '*'
-    peerDependenciesMeta:
-      webpack:
-        optional: true
-
-  lie@3.3.0:
-    resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
-
-  lilconfig@2.1.0:
-    resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
-    engines: {node: '>=10'}
-
-  lines-and-columns@1.2.4:
-    resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
-
-  lines-and-columns@2.0.3:
-    resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  lint-staged@13.3.0:
-    resolution: {integrity: sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-    hasBin: true
-
-  listr2@6.6.1:
-    resolution: {integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==}
-    engines: {node: '>=16.0.0'}
-    peerDependencies:
-      enquirer: '>= 2.3.0 < 3'
-    peerDependenciesMeta:
-      enquirer:
-        optional: true
-
-  load-json-file@4.0.0:
-    resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
-    engines: {node: '>=4'}
-
-  loader-runner@4.3.1:
-    resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
-    engines: {node: '>=6.11.5'}
-
-  loader-utils@2.0.4:
-    resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==}
-    engines: {node: '>=8.9.0'}
-
-  loader-utils@3.2.1:
-    resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==}
-    engines: {node: '>= 12.13.0'}
-
-  loadjs@4.3.0:
-    resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==}
-
-  locate-path@2.0.0:
-    resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==}
-    engines: {node: '>=4'}
-
-  locate-path@5.0.0:
-    resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
-    engines: {node: '>=8'}
-
-  locate-path@6.0.0:
-    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
-    engines: {node: '>=10'}
-
-  locate-path@7.2.0:
-    resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  lodash-es@4.17.21:
-    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
-
-  lodash-es@4.17.22:
-    resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==}
-
-  lodash.debounce@4.0.8:
-    resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
-
-  lodash.merge@4.6.2:
-    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
-
-  lodash.truncate@4.4.2:
-    resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
-
-  lodash@4.17.21:
-    resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
-
-  log-symbols@2.2.0:
-    resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}
-    engines: {node: '>=4'}
-
-  log-symbols@4.1.0:
-    resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
-    engines: {node: '>=10'}
-
-  log-update@5.0.1:
-    resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  log4js@6.9.1:
-    resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==}
-    engines: {node: '>=8.0'}
-
-  longest-streak@2.0.4:
-    resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==}
-
-  loud-rejection@1.6.0:
-    resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==}
-    engines: {node: '>=0.10.0'}
-
-  lru-cache@10.4.3:
-    resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
-
-  lru-cache@5.1.1:
-    resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
-
-  lru-cache@6.0.0:
-    resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
-    engines: {node: '>=10'}
-
-  magic-string@0.30.8:
-    resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
-    engines: {node: '>=12'}
-
-  make-dir@2.1.0:
-    resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
-    engines: {node: '>=6'}
-
-  make-dir@3.1.0:
-    resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
-    engines: {node: '>=8'}
-
-  make-dir@4.0.0:
-    resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
-    engines: {node: '>=10'}
-
-  make-error@1.3.6:
-    resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
-
-  make-fetch-happen@13.0.1:
-    resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  map-cache@0.2.2:
-    resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
-    engines: {node: '>=0.10.0'}
-
-  map-obj@1.0.1:
-    resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
-    engines: {node: '>=0.10.0'}
-
-  map-obj@2.0.0:
-    resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==}
-    engines: {node: '>=4'}
-
-  map-obj@4.3.0:
-    resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
-    engines: {node: '>=8'}
-
-  map-visit@1.0.0:
-    resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
-    engines: {node: '>=0.10.0'}
-
-  markdown-escapes@1.0.4:
-    resolution: {integrity: sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==}
-
-  markdown-table@1.1.3:
-    resolution: {integrity: sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==}
-
-  marked@15.0.6:
-    resolution: {integrity: sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==}
-    engines: {node: '>= 18'}
-    hasBin: true
-
-  marked@16.4.2:
-    resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==}
-    engines: {node: '>= 20'}
-    hasBin: true
-
-  math-intrinsics@1.1.0:
-    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
-    engines: {node: '>= 0.4'}
-
-  mathml-tag-names@2.1.3:
-    resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
-
-  mdast-util-compact@1.0.4:
-    resolution: {integrity: sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==}
-
-  media-typer@0.3.0:
-    resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
-    engines: {node: '>= 0.6'}
-
-  memfs@3.5.3:
-    resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
-    engines: {node: '>= 4.0.0'}
-
-  meow@5.0.0:
-    resolution: {integrity: sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==}
-    engines: {node: '>=6'}
-
-  meow@9.0.0:
-    resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==}
-    engines: {node: '>=10'}
-
-  merge-descriptors@1.0.3:
-    resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
-
-  merge-stream@2.0.0:
-    resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
-
-  merge2@1.4.1:
-    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
-    engines: {node: '>= 8'}
-
-  mermaid@11.12.2:
-    resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==}
-
-  methods@1.1.2:
-    resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
-    engines: {node: '>= 0.6'}
-
-  micromatch@3.1.10:
-    resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==}
-    engines: {node: '>=0.10.0'}
-
-  micromatch@4.0.5:
-    resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
-    engines: {node: '>=8.6'}
-
-  micromatch@4.0.8:
-    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
-    engines: {node: '>=8.6'}
-
-  mime-db@1.52.0:
-    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
-    engines: {node: '>= 0.6'}
-
-  mime-db@1.54.0:
-    resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
-    engines: {node: '>= 0.6'}
-
-  mime-types@2.1.35:
-    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
-    engines: {node: '>= 0.6'}
-
-  mime@1.6.0:
-    resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
-    engines: {node: '>=4'}
-    hasBin: true
-
-  mime@2.6.0:
-    resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
-    engines: {node: '>=4.0.0'}
-    hasBin: true
-
-  mimic-fn@2.1.0:
-    resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
-    engines: {node: '>=6'}
-
-  mimic-fn@4.0.0:
-    resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
-    engines: {node: '>=12'}
-
-  min-indent@1.0.1:
-    resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
-    engines: {node: '>=4'}
-
-  mini-css-extract-plugin@2.8.1:
-    resolution: {integrity: sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==}
-    engines: {node: '>= 12.13.0'}
-    peerDependencies:
-      webpack: ^5.0.0
-
-  minimalistic-assert@1.0.1:
-    resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
-
-  minimatch@10.1.1:
-    resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
-    engines: {node: 20 || >=22}
-
-  minimatch@3.1.2:
-    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
-
-  minimatch@5.1.6:
-    resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
-    engines: {node: '>=10'}
-
-  minimatch@9.0.3:
-    resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
-    engines: {node: '>=16 || 14 >=14.17'}
-
-  minimatch@9.0.5:
-    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
-    engines: {node: '>=16 || 14 >=14.17'}
-
-  minimist-options@3.0.2:
-    resolution: {integrity: sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==}
-    engines: {node: '>= 4'}
-
-  minimist-options@4.1.0:
-    resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
-    engines: {node: '>= 6'}
-
-  minimist@1.2.8:
-    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
-
-  minipass-collect@2.0.1:
-    resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
-    engines: {node: '>=16 || 14 >=14.17'}
-
-  minipass-fetch@3.0.5:
-    resolution: {integrity: sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  minipass-flush@1.0.5:
-    resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
-    engines: {node: '>= 8'}
-
-  minipass-json-stream@1.0.2:
-    resolution: {integrity: sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==}
-
-  minipass-pipeline@1.2.4:
-    resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
-    engines: {node: '>=8'}
-
-  minipass-sized@1.0.3:
-    resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
-    engines: {node: '>=8'}
-
-  minipass@3.3.6:
-    resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
-    engines: {node: '>=8'}
-
-  minipass@5.0.0:
-    resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
-    engines: {node: '>=8'}
-
-  minipass@7.1.2:
-    resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
-    engines: {node: '>=16 || 14 >=14.17'}
-
-  minizlib@2.1.2:
-    resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
-    engines: {node: '>= 8'}
-
-  mixin-deep@1.3.2:
-    resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
-    engines: {node: '>=0.10.0'}
-
-  mkdirp@0.5.6:
-    resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
-    hasBin: true
-
-  mkdirp@1.0.4:
-    resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
-    engines: {node: '>=10'}
-    hasBin: true
-
-  mlly@1.8.0:
-    resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
-
-  mockjs@1.1.0:
-    resolution: {integrity: sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==}
-    hasBin: true
-
-  monaco-editor@0.36.1:
-    resolution: {integrity: sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg==}
-
-  mrmime@2.0.0:
-    resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
-    engines: {node: '>=10'}
-
-  ms@2.0.0:
-    resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
-
-  ms@2.1.2:
-    resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
-
-  ms@2.1.3:
-    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
-
-  multicast-dns@7.2.5:
-    resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==}
-    hasBin: true
-
-  mute-stream@1.0.0:
-    resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  nanoid@3.3.11:
-    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
-    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
-    hasBin: true
-
-  nanoid@3.3.4:
-    resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
-    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
-    hasBin: true
-
-  nanomatch@1.2.13:
-    resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
-    engines: {node: '>=0.10.0'}
-
-  natural-compare@1.4.0:
-    resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
-
-  needle@3.3.1:
-    resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==}
-    engines: {node: '>= 4.4.x'}
-    hasBin: true
-
-  negotiator@0.6.3:
-    resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
-    engines: {node: '>= 0.6'}
-
-  negotiator@0.6.4:
-    resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
-    engines: {node: '>= 0.6'}
-
-  neo-async@2.6.2:
-    resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
-
-  ng-alain-plugin-theme@16.0.2:
-    resolution: {integrity: sha512-8PsBnym4+qigDkQBbUbNjpCPpZC2vfStPORIpz6k8qV+2BkfJOxxqgGxqGey8p/f5JJ+SKFU+szFyGikdb6New==}
-    hasBin: true
-
-  ng-alain-sts@0.0.2:
-    resolution: {integrity: sha512-ZvNLFY75lGCuXIQZKKnKQ9bopKh3FLnSY7xUtvS1ga4m+hLiex2P7dat5bpJyEogA9l8s5Utrj+lNZjQ34Ed0Q==}
-    hasBin: true
-
-  ng-alain@17.3.1:
-    resolution: {integrity: sha512-C+JthG3QYijeEcXq4iQzfpvdcueZndccbqQwzXyCvyNPPS1Y9gP/u0+EYRaU3AKRR6L1Nem0IXVlChH0xzfIRg==}
-
-  ng-in-viewport@16.1.0:
-    resolution: {integrity: sha512-HWFiLgcZ2tHSFnjbMUsWV95laY14JQAIjdM1c7VPMS3KvXDVlEgrVItPqzZz8Jbj+Vpsl0kge+ZxIHwiTul3BA==}
-    peerDependencies:
-      '@angular/common': ^14.0.0 || ^15.0.0 || ^16.0.0 || >=17.0.0
-      '@angular/core': ^14.0.0 || ^15.0.0 || ^16.0.0 || >=17.0.0
-
-  ng-zorro-antd@17.4.1:
-    resolution: {integrity: sha512-KsMYFlRBFPK5FPPoaYIS/uzQ4400WFMLcA9/1LmYZcSgmeINlCvgfiitNcOVLsRyQYlpdIQyOo9z5Ue2SS9ypg==}
-    peerDependencies:
-      '@angular/animations': ^17.0.0
-      '@angular/common': ^17.0.0
-      '@angular/core': ^17.0.0
-      '@angular/forms': ^17.0.0
-      '@angular/platform-browser': ^17.0.0
-      '@angular/router': ^17.0.0
-
-  ngx-color-picker@16.0.0:
-    resolution: {integrity: sha512-Dk2FvcbebD6STZSVzkI5oFHOlTrrNC5bOHh+YVaFgaWuWrVUdVIJm68ocUvTgr/qxTEJjrfcnRnS4wi7BJ2hKg==}
-    peerDependencies:
-      '@angular/common': '>=9.0.0'
-      '@angular/core': '>=9.0.0'
-      '@angular/forms': '>=9.0.0'
-
-  ngx-countdown@17.1.1:
-    resolution: {integrity: sha512-ItZokeKc3QAYxLIZ96eD2mI6IZ5oEmKzFLIx94wjrLfZER1XRxUu+agHc+CnNmWEOmPWNn4ZtA5AUSawAcWZNQ==}
-
-  ngx-echarts@17.2.0:
-    resolution: {integrity: sha512-i3XDE9d53zmJH4bp8RQ/271oPlhBkczO1M3VtWk8nCXdxQq9qx8UckjWEQ7oV1AbSDLGK5sRiFu5EaY5hvdWPA==}
-    peerDependencies:
-      echarts: '>=5.0.0'
-
-  ngx-markdown@19.0.0:
-    resolution: {integrity: sha512-/UDTYxK2sbG9LjeuPfqErCg9gbT1O64Rnqvs9qgvK70X//gEVCMStNUi1zYIqw/SLRk19Rk48DZMgPiFRbgb1Q==}
-    peerDependencies:
-      '@angular/common': ^19.0.0
-      '@angular/core': ^19.0.0
-      '@angular/platform-browser': ^19.0.0
-      marked: ^15.0.0
-      rxjs: ^6.5.3 || ^7.4.0
-      zone.js: ~0.15.0
-
-  ngx-slick-carousel@17.0.0:
-    resolution: {integrity: sha512-4GS13vjbfEUzQR6St9tq9ms69ODSxkPR0HhXJ97YhDlOlbirk9wHxnSKNneutitlKIt9G/MCrIV4u0L3wiC8rQ==}
-    peerDependencies:
-      '@angular/common': ^17.0.0
-      '@angular/core': ^17.0.0
-      '@angular/forms': ^17.0.0
-      rxjs: ^7.0.0
-      zone.js: '>=0.10.2'
-
-  nice-napi@1.0.2:
-    resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==}
-    os: ['!win32']
-
-  node-addon-api@3.2.1:
-    resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==}
-
-  node-domexception@1.0.0:
-    resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
-    engines: {node: '>=10.5.0'}
-    deprecated: Use your platform's native DOMException instead
-
-  node-emoji@1.11.0:
-    resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
-
-  node-fetch-h2@2.3.0:
-    resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==}
-    engines: {node: 4.x || >=6.0.0}
-
-  node-fetch@2.7.0:
-    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
-    engines: {node: 4.x || >=6.0.0}
-    peerDependencies:
-      encoding: ^0.1.0
-    peerDependenciesMeta:
-      encoding:
-        optional: true
-
-  node-fetch@3.3.2:
-    resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  node-forge@1.3.3:
-    resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
-    engines: {node: '>= 6.13.0'}
-
-  node-gyp-build@4.8.4:
-    resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
-    hasBin: true
-
-  node-gyp@10.3.1:
-    resolution: {integrity: sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-    hasBin: true
-
-  node-machine-id@1.1.12:
-    resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
-
-  node-readfiles@0.2.0:
-    resolution: {integrity: sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==}
-
-  node-releases@2.0.27:
-    resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
-
-  nopt@7.2.1:
-    resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-    hasBin: true
-
-  normalize-package-data@2.5.0:
-    resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
-
-  normalize-package-data@3.0.3:
-    resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==}
-    engines: {node: '>=10'}
-
-  normalize-package-data@6.0.2:
-    resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  normalize-path@3.0.0:
-    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
-    engines: {node: '>=0.10.0'}
-
-  normalize-range@0.1.2:
-    resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
-    engines: {node: '>=0.10.0'}
-
-  normalize-selector@0.2.0:
-    resolution: {integrity: sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw==}
-
-  npm-bundled@3.0.1:
-    resolution: {integrity: sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  npm-install-checks@6.3.0:
-    resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  npm-normalize-package-bin@3.0.1:
-    resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  npm-package-arg@11.0.1:
-    resolution: {integrity: sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  npm-packlist@8.0.2:
-    resolution: {integrity: sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  npm-pick-manifest@9.0.0:
-    resolution: {integrity: sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  npm-registry-fetch@16.2.1:
-    resolution: {integrity: sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  npm-run-path@4.0.1:
-    resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
-    engines: {node: '>=8'}
-
-  npm-run-path@5.3.0:
-    resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  nth-check@2.1.1:
-    resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
-
-  num2fraction@1.2.2:
-    resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==}
-
-  nx@19.8.14:
-    resolution: {integrity: sha512-yprBOWV16eQntz5h5SShYHMVeN50fUb6yHfzsqNiFneCJeyVjyJ585m+2TuVbE11vT1amU0xCjHcSGfJBBnm8g==}
-    hasBin: true
-    peerDependencies:
-      '@swc-node/register': ^1.8.0
-      '@swc/core': ^1.3.85
-    peerDependenciesMeta:
-      '@swc-node/register':
-        optional: true
-      '@swc/core':
-        optional: true
-
-  oas-kit-common@1.0.8:
-    resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==}
-
-  oas-linter@3.2.2:
-    resolution: {integrity: sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==}
-
-  oas-resolver@2.5.6:
-    resolution: {integrity: sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==}
-    hasBin: true
-
-  oas-schema-walker@1.1.5:
-    resolution: {integrity: sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==}
-
-  oas-validator@5.0.8:
-    resolution: {integrity: sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==}
-
-  oauth-sign@0.9.0:
-    resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
-
-  object-assign@4.1.1:
-    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
-    engines: {node: '>=0.10.0'}
-
-  object-copy@0.1.0:
-    resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==}
-    engines: {node: '>=0.10.0'}
-
-  object-inspect@1.13.4:
-    resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
-    engines: {node: '>= 0.4'}
-
-  object-keys@1.1.1:
-    resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
-    engines: {node: '>= 0.4'}
-
-  object-visit@1.0.1:
-    resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==}
-    engines: {node: '>=0.10.0'}
-
-  object.assign@4.1.7:
-    resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
-    engines: {node: '>= 0.4'}
-
-  object.pick@1.3.0:
-    resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==}
-    engines: {node: '>=0.10.0'}
-
-  object.values@1.2.1:
-    resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
-    engines: {node: '>= 0.4'}
-
-  obuf@1.1.2:
-    resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
-
-  on-finished@2.3.0:
-    resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
-    engines: {node: '>= 0.8'}
-
-  on-finished@2.4.1:
-    resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
-    engines: {node: '>= 0.8'}
-
-  on-headers@1.1.0:
-    resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
-    engines: {node: '>= 0.8'}
-
-  once@1.4.0:
-    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
-
-  onetime@5.1.2:
-    resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
-    engines: {node: '>=6'}
-
-  onetime@6.0.0:
-    resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
-    engines: {node: '>=12'}
-
-  open@7.4.2:
-    resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
-    engines: {node: '>=8'}
-
-  open@8.4.2:
-    resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
-    engines: {node: '>=12'}
-
-  optionator@0.9.4:
-    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
-    engines: {node: '>= 0.8.0'}
-
-  ora@5.3.0:
-    resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==}
-    engines: {node: '>=10'}
-
-  ora@5.4.1:
-    resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
-    engines: {node: '>=10'}
-
-  os-tmpdir@1.0.2:
-    resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
-    engines: {node: '>=0.10.0'}
-
-  own-keys@1.0.1:
-    resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
-    engines: {node: '>= 0.4'}
-
-  p-limit@1.3.0:
-    resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==}
-    engines: {node: '>=4'}
-
-  p-limit@2.3.0:
-    resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
-    engines: {node: '>=6'}
-
-  p-limit@3.1.0:
-    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
-    engines: {node: '>=10'}
-
-  p-limit@4.0.0:
-    resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  p-locate@2.0.0:
-    resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==}
-    engines: {node: '>=4'}
-
-  p-locate@4.1.0:
-    resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
-    engines: {node: '>=8'}
-
-  p-locate@5.0.0:
-    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
-    engines: {node: '>=10'}
-
-  p-locate@6.0.0:
-    resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  p-map@4.0.0:
-    resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
-    engines: {node: '>=10'}
-
-  p-retry@4.6.2:
-    resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
-    engines: {node: '>=8'}
-
-  p-try@1.0.0:
-    resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==}
-    engines: {node: '>=4'}
-
-  p-try@2.2.0:
-    resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
-    engines: {node: '>=6'}
-
-  package-json-from-dist@1.0.1:
-    resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
-
-  package-manager-detector@1.6.0:
-    resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
-
-  pacote@17.0.6:
-    resolution: {integrity: sha512-cJKrW21VRE8vVTRskJo78c/RCvwJCn1f4qgfxL4w77SOWrTCRcmfkYHlHtS0gqpgjv3zhXflRtgsrUCX5xwNnQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-    hasBin: true
-
-  pako@1.0.11:
-    resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
-
-  parent-module@1.0.1:
-    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
-    engines: {node: '>=6'}
-
-  parse-entities@1.2.2:
-    resolution: {integrity: sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==}
-
-  parse-json@4.0.0:
-    resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
-    engines: {node: '>=4'}
-
-  parse-json@5.2.0:
-    resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
-    engines: {node: '>=8'}
-
-  parse-node-version@1.0.1:
-    resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
-    engines: {node: '>= 0.10'}
-
-  parse5-html-rewriting-stream@7.0.0:
-    resolution: {integrity: sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==}
-
-  parse5-sax-parser@7.0.0:
-    resolution: {integrity: sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==}
-
-  parse5@7.3.0:
-    resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
-
-  parseurl@1.3.3:
-    resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
-    engines: {node: '>= 0.8'}
-
-  pascalcase@0.1.1:
-    resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
-    engines: {node: '>=0.10.0'}
-
-  path-data-parser@0.1.0:
-    resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==}
-
-  path-dirname@1.0.2:
-    resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}
-
-  path-exists@3.0.0:
-    resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
-    engines: {node: '>=4'}
-
-  path-exists@4.0.0:
-    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
-    engines: {node: '>=8'}
-
-  path-exists@5.0.0:
-    resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  path-is-absolute@1.0.1:
-    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
-    engines: {node: '>=0.10.0'}
-
-  path-is-inside@1.0.2:
-    resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
-
-  path-key@3.1.1:
-    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
-    engines: {node: '>=8'}
-
-  path-key@4.0.0:
-    resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
-    engines: {node: '>=12'}
-
-  path-parse@1.0.7:
-    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
-
-  path-scurry@1.11.1:
-    resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
-    engines: {node: '>=16 || 14 >=14.18'}
-
-  path-to-regexp@0.1.12:
-    resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
-
-  path-type@3.0.0:
-    resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
-    engines: {node: '>=4'}
-
-  path-type@4.0.0:
-    resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
-    engines: {node: '>=8'}
-
-  pathe@2.0.3:
-    resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
-
-  performance-now@2.1.0:
-    resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
-
-  picocolors@0.2.1:
-    resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==}
-
-  picocolors@1.1.1:
-    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
-
-  picomatch@2.3.1:
-    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
-    engines: {node: '>=8.6'}
-
-  picomatch@4.0.1:
-    resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==}
-    engines: {node: '>=12'}
-
-  pidtree@0.6.0:
-    resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
-    engines: {node: '>=0.10'}
-    hasBin: true
-
-  pify@2.3.0:
-    resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
-    engines: {node: '>=0.10.0'}
-
-  pify@3.0.0:
-    resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
-    engines: {node: '>=4'}
-
-  pify@4.0.1:
-    resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
-    engines: {node: '>=6'}
-
-  pinkie-promise@2.0.1:
-    resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
-    engines: {node: '>=0.10.0'}
-
-  pinkie@2.0.4:
-    resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
-    engines: {node: '>=0.10.0'}
-
-  piscina@4.4.0:
-    resolution: {integrity: sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==}
-
-  pkg-dir@7.0.0:
-    resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==}
-    engines: {node: '>=14.16'}
-
-  pkg-types@1.3.1:
-    resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
-
-  plyr@3.8.4:
-    resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==}
-
-  points-on-curve@0.2.0:
-    resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
-
-  points-on-path@0.2.1:
-    resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
-
-  posix-character-classes@0.1.1:
-    resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
-    engines: {node: '>=0.10.0'}
-
-  possible-typed-array-names@1.1.0:
-    resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
-    engines: {node: '>= 0.4'}
-
-  postcss-html@0.36.0:
-    resolution: {integrity: sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==}
-    peerDependencies:
-      postcss: '>=5.0.0'
-      postcss-syntax: '>=0.36.0'
-
-  postcss-jsx@0.36.4:
-    resolution: {integrity: sha512-jwO/7qWUvYuWYnpOb0+4bIIgJt7003pgU3P6nETBLaOyBXuTD55ho21xnals5nBrlpTIFodyd3/jBi6UO3dHvA==}
-    peerDependencies:
-      postcss: '>=5.0.0'
-      postcss-syntax: '>=0.36.0'
-
-  postcss-less@3.1.4:
-    resolution: {integrity: sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==}
-    engines: {node: '>=6.14.4'}
-
-  postcss-less@6.0.0:
-    resolution: {integrity: sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==}
-    engines: {node: '>=12'}
-    peerDependencies:
-      postcss: ^8.3.5
-
-  postcss-loader@8.1.1:
-    resolution: {integrity: sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==}
-    engines: {node: '>= 18.12.0'}
-    peerDependencies:
-      '@rspack/core': 0.x || 1.x
-      postcss: ^7.0.0 || ^8.0.1
-      webpack: ^5.0.0
-    peerDependenciesMeta:
-      '@rspack/core':
-        optional: true
-      webpack:
-        optional: true
-
-  postcss-markdown@0.36.0:
-    resolution: {integrity: sha512-rl7fs1r/LNSB2bWRhyZ+lM/0bwKv9fhl38/06gF6mKMo/NPnp55+K1dSTosSVjFZc0e1ppBlu+WT91ba0PMBfQ==}
-    peerDependencies:
-      postcss: '>=5.0.0'
-      postcss-syntax: '>=0.36.0'
-
-  postcss-media-query-parser@0.2.3:
-    resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
-
-  postcss-modules-extract-imports@3.1.0:
-    resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
-  postcss-modules-local-by-default@4.2.0:
-    resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
-  postcss-modules-scope@3.2.1:
-    resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
-  postcss-modules-values@4.0.0:
-    resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
-  postcss-reporter@6.0.1:
-    resolution: {integrity: sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==}
-    engines: {node: '>=6'}
-
-  postcss-resolve-nested-selector@0.1.6:
-    resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==}
-
-  postcss-safe-parser@4.0.2:
-    resolution: {integrity: sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==}
-    engines: {node: '>=6.0.0'}
-
-  postcss-safe-parser@6.0.0:
-    resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==}
-    engines: {node: '>=12.0'}
-    peerDependencies:
-      postcss: ^8.3.3
-
-  postcss-sass@0.3.5:
-    resolution: {integrity: sha512-B5z2Kob4xBxFjcufFnhQ2HqJQ2y/Zs/ic5EZbCywCkxKd756Q40cIQ/veRDwSrw1BF6+4wUgmpm0sBASqVi65A==}
-
-  postcss-scss@2.1.1:
-    resolution: {integrity: sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==}
-    engines: {node: '>=6.0.0'}
-
-  postcss-selector-parser@3.1.2:
-    resolution: {integrity: sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==}
-    engines: {node: '>=8'}
-
-  postcss-selector-parser@6.1.2:
-    resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
-    engines: {node: '>=4'}
-
-  postcss-selector-parser@7.1.1:
-    resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
-    engines: {node: '>=4'}
-
-  postcss-sorting@4.1.0:
-    resolution: {integrity: sha512-r4T2oQd1giURJdHQ/RMb72dKZCuLOdWx2B/XhXN1Y1ZdnwXsKH896Qz6vD4tFy9xSjpKNYhlZoJmWyhH/7JUQw==}
-    engines: {node: '>=6.14.3'}
-
-  postcss-sorting@7.0.1:
-    resolution: {integrity: sha512-iLBFYz6VRYyLJEJsBJ8M3TCqNcckVzz4wFounSc5Oez35ogE/X+aoC5fFu103Ot7NyvjU3/xqIXn93Gp3kJk4g==}
-    peerDependencies:
-      postcss: ^8.3.9
-
-  postcss-syntax@0.36.2:
-    resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==}
-    peerDependencies:
-      postcss: '>=5.0.0'
-      postcss-html: '*'
-      postcss-jsx: '*'
-      postcss-less: '*'
-      postcss-markdown: '*'
-      postcss-scss: '*'
-    peerDependenciesMeta:
-      postcss-html:
-        optional: true
-      postcss-jsx:
-        optional: true
-      postcss-less:
-        optional: true
-      postcss-markdown:
-        optional: true
-      postcss-scss:
-        optional: true
-
-  postcss-value-parser@3.3.1:
-    resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==}
-
-  postcss-value-parser@4.2.0:
-    resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
-
-  postcss@7.0.39:
-    resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==}
-    engines: {node: '>=6.0.0'}
-
-  postcss@8.4.35:
-    resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==}
-    engines: {node: ^10 || ^12 || >=14}
-
-  postcss@8.5.6:
-    resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
-    engines: {node: ^10 || ^12 || >=14}
-
-  prelude-ls@1.2.1:
-    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
-    engines: {node: '>= 0.8.0'}
-
-  prettier-linter-helpers@1.0.1:
-    resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==}
-    engines: {node: '>=6.0.0'}
-
-  prettier@2.7.1:
-    resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==}
-    engines: {node: '>=10.13.0'}
-    hasBin: true
-
-  prettier@2.8.8:
-    resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
-    engines: {node: '>=10.13.0'}
-    hasBin: true
-
-  pretty-format@29.7.0:
-    resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
-  prismjs@1.30.0:
-    resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
-    engines: {node: '>=6'}
-
-  proc-log@3.0.0:
-    resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  proc-log@4.2.0:
-    resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  process-nextick-args@2.0.1:
-    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
-
-  promise-inflight@1.0.1:
-    resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
-    peerDependencies:
-      bluebird: '*'
-    peerDependenciesMeta:
-      bluebird:
-        optional: true
-
-  promise-retry@2.0.1:
-    resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
-    engines: {node: '>=10'}
-
-  protractor@7.0.0:
-    resolution: {integrity: sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==}
-    engines: {node: '>=10.13.x'}
-    deprecated: We have news to share - Protractor is deprecated and will reach end-of-life by Summer 2023. To learn more and find out about other options please refer to this post on the Angular blog. Thank you for using and contributing to Protractor. https://goo.gle/state-of-e2e-in-angular
-    hasBin: true
-
-  proxy-addr@2.0.7:
-    resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
-    engines: {node: '>= 0.10'}
-
-  proxy-from-env@1.1.0:
-    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
-
-  prr@1.0.1:
-    resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
-
-  psl@1.15.0:
-    resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
-
-  punycode@1.4.1:
-    resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
-
-  punycode@2.3.1:
-    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
-    engines: {node: '>=6'}
-
-  q@1.4.1:
-    resolution: {integrity: sha512-/CdEdaw49VZVmyIDGUQKDDT53c7qBkO6g5CefWz91Ae+l4+cRtcDYwMTXh6me4O8TMldeGHG3N2Bl84V78Ywbg==}
-    engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
-    deprecated: |-
-      You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
-
-      (For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
-
-  qjobs@1.2.0:
-    resolution: {integrity: sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==}
-    engines: {node: '>=0.9'}
-
-  qs@6.14.1:
-    resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
-    engines: {node: '>=0.6'}
-
-  qs@6.5.3:
-    resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
-    engines: {node: '>=0.6'}
-
-  queue-microtask@1.2.3:
-    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
-
-  quick-lru@1.1.0:
-    resolution: {integrity: sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==}
-    engines: {node: '>=4'}
-
-  quick-lru@4.0.1:
-    resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
-    engines: {node: '>=8'}
-
-  randombytes@2.1.0:
-    resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
-
-  range-parser@1.2.1:
-    resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
-    engines: {node: '>= 0.6'}
-
-  rangetouch@2.0.1:
-    resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==}
-
-  raw-body@2.5.3:
-    resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
-    engines: {node: '>= 0.8'}
-
-  react-is@18.3.1:
-    resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
-
-  read-package-json-fast@3.0.2:
-    resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  read-package-json@7.0.1:
-    resolution: {integrity: sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-    deprecated: This package is no longer supported. Please use @npmcli/package-json instead.
-
-  read-pkg-up@3.0.0:
-    resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==}
-    engines: {node: '>=4'}
-
-  read-pkg-up@7.0.1:
-    resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
-    engines: {node: '>=8'}
-
-  read-pkg@3.0.0:
-    resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
-    engines: {node: '>=4'}
-
-  read-pkg@5.2.0:
-    resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
-    engines: {node: '>=8'}
-
-  readable-stream@2.3.8:
-    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
-
-  readable-stream@3.6.2:
-    resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
-    engines: {node: '>= 6'}
-
-  readdirp@3.6.0:
-    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
-    engines: {node: '>=8.10.0'}
-
-  redent@2.0.0:
-    resolution: {integrity: sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==}
-    engines: {node: '>=4'}
-
-  redent@3.0.0:
-    resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
-    engines: {node: '>=8'}
-
-  reflect-metadata@0.2.2:
-    resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
-
-  reflect.getprototypeof@1.0.10:
-    resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
-    engines: {node: '>= 0.4'}
-
-  reftools@1.1.9:
-    resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==}
-
-  regenerate-unicode-properties@10.2.2:
-    resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
-    engines: {node: '>=4'}
-
-  regenerate@1.4.2:
-    resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
-
-  regenerator-runtime@0.14.1:
-    resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
-
-  regex-not@1.0.2:
-    resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==}
-    engines: {node: '>=0.10.0'}
-
-  regex-parser@2.3.1:
-    resolution: {integrity: sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==}
-
-  regexp.prototype.flags@1.5.4:
-    resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
-    engines: {node: '>= 0.4'}
-
-  regexpu-core@6.4.0:
-    resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
-    engines: {node: '>=4'}
-
-  regjsgen@0.8.0:
-    resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==}
-
-  regjsparser@0.13.0:
-    resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
-    hasBin: true
-
-  remark-parse@6.0.3:
-    resolution: {integrity: sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==}
-
-  remark-stringify@6.0.4:
-    resolution: {integrity: sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==}
-
-  remark@10.0.1:
-    resolution: {integrity: sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==}
-
-  repeat-element@1.1.4:
-    resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==}
-    engines: {node: '>=0.10.0'}
-
-  repeat-string@1.6.1:
-    resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
-    engines: {node: '>=0.10'}
-
-  replace-ext@1.0.0:
-    resolution: {integrity: sha512-vuNYXC7gG7IeVNBC1xUllqCcZKRbJoSPOBhnTEcAIiKCsbuef6zO3F0Rve3isPMMoNoQRWjQwbAgAjHUHniyEA==}
-    engines: {node: '>= 0.10'}
-
-  request@2.88.2:
-    resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
-    engines: {node: '>= 6'}
-    deprecated: request has been deprecated, see https://github.com/request/request/issues/3142
-
-  require-directory@2.1.1:
-    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
-    engines: {node: '>=0.10.0'}
-
-  require-from-string@2.0.2:
-    resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
-    engines: {node: '>=0.10.0'}
-
-  require-main-filename@2.0.0:
-    resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
-
-  requires-port@1.0.0:
-    resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
-
-  resolve-from@3.0.0:
-    resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==}
-    engines: {node: '>=4'}
-
-  resolve-from@4.0.0:
-    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
-    engines: {node: '>=4'}
-
-  resolve-from@5.0.0:
-    resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
-    engines: {node: '>=8'}
-
-  resolve-url-loader@5.0.0:
-    resolution: {integrity: sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==}
-    engines: {node: '>=12'}
-
-  resolve-url@0.2.1:
-    resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
-    deprecated: https://github.com/lydell/resolve-url#deprecated
-
-  resolve@1.22.11:
-    resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
-    engines: {node: '>= 0.4'}
-    hasBin: true
-
-  resolve@1.22.8:
-    resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
-    hasBin: true
-
-  restore-cursor@3.1.0:
-    resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
-    engines: {node: '>=8'}
-
-  restore-cursor@4.0.0:
-    resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  ret@0.1.15:
-    resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==}
-    engines: {node: '>=0.12'}
-
-  retry@0.12.0:
-    resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
-    engines: {node: '>= 4'}
-
-  retry@0.13.1:
-    resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
-    engines: {node: '>= 4'}
-
-  reusify@1.1.0:
-    resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
-    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
-
-  rfdc@1.4.1:
-    resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
-
-  rimraf@2.6.3:
-    resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
-    deprecated: Rimraf versions prior to v4 are no longer supported
-    hasBin: true
-
-  rimraf@2.7.1:
-    resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
-    deprecated: Rimraf versions prior to v4 are no longer supported
-    hasBin: true
-
-  rimraf@3.0.2:
-    resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
-    deprecated: Rimraf versions prior to v4 are no longer supported
-    hasBin: true
-
-  robust-predicates@3.0.2:
-    resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
-
-  rollup@4.55.1:
-    resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==}
-    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
-    hasBin: true
-
-  roughjs@4.6.6:
-    resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
-
-  run-async@3.0.0:
-    resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
-    engines: {node: '>=0.12.0'}
-
-  run-parallel@1.2.0:
-    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
-
-  rw@1.3.3:
-    resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
-
-  rxjs@7.8.1:
-    resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
-
-  safe-array-concat@1.1.3:
-    resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
-    engines: {node: '>=0.4'}
-
-  safe-buffer@5.1.2:
-    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
-
-  safe-buffer@5.2.1:
-    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
-
-  safe-push-apply@1.0.0:
-    resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
-    engines: {node: '>= 0.4'}
-
-  safe-regex-test@1.1.0:
-    resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
-    engines: {node: '>= 0.4'}
-
-  safe-regex@1.1.0:
-    resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==}
-
-  safer-buffer@2.1.2:
-    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
-
-  sass-loader@14.1.1:
-    resolution: {integrity: sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==}
-    engines: {node: '>= 18.12.0'}
-    peerDependencies:
-      '@rspack/core': 0.x || 1.x
-      node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
-      sass: ^1.3.0
-      sass-embedded: '*'
-      webpack: ^5.0.0
-    peerDependenciesMeta:
-      '@rspack/core':
-        optional: true
-      node-sass:
-        optional: true
-      sass:
-        optional: true
-      sass-embedded:
-        optional: true
-      webpack:
-        optional: true
-
-  sass@1.71.1:
-    resolution: {integrity: sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==}
-    engines: {node: '>=14.0.0'}
-    hasBin: true
-
-  saucelabs@1.5.0:
-    resolution: {integrity: sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==}
-
-  sax@1.4.4:
-    resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
-    engines: {node: '>=11.0.0'}
-
-  schema-utils@3.3.0:
-    resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
-    engines: {node: '>= 10.13.0'}
-
-  schema-utils@4.3.3:
-    resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
-    engines: {node: '>= 10.13.0'}
-
-  screenfull@6.0.2:
-    resolution: {integrity: sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==}
-    engines: {node: ^14.13.1 || >=16.0.0}
-
-  select-hose@2.0.0:
-    resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==}
-
-  select@1.1.2:
-    resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
-
-  selenium-webdriver@3.6.0:
-    resolution: {integrity: sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==}
-    engines: {node: '>= 6.9.0'}
-
-  selfsigned@2.4.1:
-    resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
-    engines: {node: '>=10'}
-
-  semver@5.7.2:
-    resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
-    hasBin: true
-
-  semver@6.3.1:
-    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
-    hasBin: true
-
-  semver@7.6.0:
-    resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
-    engines: {node: '>=10'}
-    hasBin: true
-
-  semver@7.7.3:
-    resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
-    engines: {node: '>=10'}
-    hasBin: true
-
-  send@0.19.2:
-    resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
-    engines: {node: '>= 0.8.0'}
-
-  serialize-javascript@6.0.2:
-    resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
-
-  serve-index@1.9.1:
-    resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==}
-    engines: {node: '>= 0.8.0'}
-
-  serve-static@1.16.3:
-    resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
-    engines: {node: '>= 0.8.0'}
-
-  set-blocking@2.0.0:
-    resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
-
-  set-function-length@1.2.2:
-    resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
-    engines: {node: '>= 0.4'}
-
-  set-function-name@2.0.2:
-    resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
-    engines: {node: '>= 0.4'}
-
-  set-proto@1.0.0:
-    resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
-    engines: {node: '>= 0.4'}
-
-  set-value@2.0.1:
-    resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
-    engines: {node: '>=0.10.0'}
-
-  setimmediate@1.0.5:
-    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
-
-  setprototypeof@1.1.0:
-    resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
-
-  setprototypeof@1.2.0:
-    resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
-
-  shallow-clone@3.0.1:
-    resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==}
-    engines: {node: '>=8'}
-
-  shebang-command@2.0.0:
-    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
-    engines: {node: '>=8'}
-
-  shebang-regex@3.0.0:
-    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
-    engines: {node: '>=8'}
-
-  shell-quote@1.8.3:
-    resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
-    engines: {node: '>= 0.4'}
-
-  should-equal@2.0.0:
-    resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==}
-
-  should-format@3.0.3:
-    resolution: {integrity: sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==}
-
-  should-type-adaptors@1.1.0:
-    resolution: {integrity: sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==}
-
-  should-type@1.4.0:
-    resolution: {integrity: sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==}
-
-  should-util@1.0.1:
-    resolution: {integrity: sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==}
-
-  should@13.2.3:
-    resolution: {integrity: sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==}
-
-  side-channel-list@1.0.0:
-    resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
-    engines: {node: '>= 0.4'}
-
-  side-channel-map@1.0.1:
-    resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
-    engines: {node: '>= 0.4'}
-
-  side-channel-weakmap@1.0.2:
-    resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
-    engines: {node: '>= 0.4'}
-
-  side-channel@1.1.0:
-    resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
-    engines: {node: '>= 0.4'}
-
-  signal-exit@3.0.7:
-    resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
-
-  signal-exit@4.1.0:
-    resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
-    engines: {node: '>=14'}
-
-  sigstore@2.3.1:
-    resolution: {integrity: sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  slash@2.0.0:
-    resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
-    engines: {node: '>=6'}
-
-  slash@3.0.0:
-    resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
-    engines: {node: '>=8'}
-
-  slash@4.0.0:
-    resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
-    engines: {node: '>=12'}
-
-  slice-ansi@2.1.0:
-    resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==}
-    engines: {node: '>=6'}
-
-  slice-ansi@4.0.0:
-    resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
-    engines: {node: '>=10'}
-
-  slice-ansi@5.0.0:
-    resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
-    engines: {node: '>=12'}
-
-  slick-carousel@1.8.1:
-    resolution: {integrity: sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==}
-    peerDependencies:
-      jquery: '>=1.8.0'
-
-  smart-buffer@4.2.0:
-    resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
-    engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
-
-  snapdragon-node@2.1.1:
-    resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==}
-    engines: {node: '>=0.10.0'}
-
-  snapdragon-util@3.0.1:
-    resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==}
-    engines: {node: '>=0.10.0'}
-
-  snapdragon@0.8.2:
-    resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==}
-    engines: {node: '>=0.10.0'}
-
-  socket.io-adapter@2.5.6:
-    resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==}
-
-  socket.io-parser@4.2.5:
-    resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==}
-    engines: {node: '>=10.0.0'}
-
-  socket.io@4.8.3:
-    resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==}
-    engines: {node: '>=10.2.0'}
-
-  sockjs@0.3.24:
-    resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==}
-
-  socks-proxy-agent@8.0.5:
-    resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
-    engines: {node: '>= 14'}
-
-  socks@2.8.7:
-    resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
-    engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
-
-  source-map-explorer@2.5.3:
-    resolution: {integrity: sha512-qfUGs7UHsOBE5p/lGfQdaAj/5U/GWYBw2imEpD6UQNkqElYonkow8t+HBL1qqIl3CuGZx7n8/CQo4x1HwSHhsg==}
-    engines: {node: '>=12'}
-    hasBin: true
-
-  source-map-js@1.2.1:
-    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
-    engines: {node: '>=0.10.0'}
-
-  source-map-loader@5.0.0:
-    resolution: {integrity: sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==}
-    engines: {node: '>= 18.12.0'}
-    peerDependencies:
-      webpack: ^5.72.1
-
-  source-map-resolve@0.5.3:
-    resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==}
-    deprecated: See https://github.com/lydell/source-map-resolve#deprecated
-
-  source-map-support@0.4.18:
-    resolution: {integrity: sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==}
-
-  source-map-support@0.5.21:
-    resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
-
-  source-map-url@0.4.1:
-    resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==}
-    deprecated: See https://github.com/lydell/source-map-url#deprecated
-
-  source-map@0.5.7:
-    resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
-    engines: {node: '>=0.10.0'}
-
-  source-map@0.6.1:
-    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
-    engines: {node: '>=0.10.0'}
-
-  source-map@0.7.4:
-    resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
-    engines: {node: '>= 8'}
-
-  source-map@0.7.6:
-    resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
-    engines: {node: '>= 12'}
-
-  spdx-correct@3.2.0:
-    resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
-
-  spdx-exceptions@2.5.0:
-    resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
-
-  spdx-expression-parse@3.0.1:
-    resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
-
-  spdx-expression-parse@4.0.0:
-    resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==}
-
-  spdx-license-ids@3.0.22:
-    resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==}
-
-  spdy-transport@3.0.0:
-    resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==}
-
-  spdy@4.0.2:
-    resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==}
-    engines: {node: '>=6.0.0'}
-
-  specificity@0.4.1:
-    resolution: {integrity: sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==}
-    hasBin: true
-
-  split-string@3.1.0:
-    resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
-    engines: {node: '>=0.10.0'}
-
-  sprintf-js@1.0.3:
-    resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
-
-  ssf@0.11.2:
-    resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
-    engines: {node: '>=0.8'}
-
-  sshpk@1.18.0:
-    resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
-    engines: {node: '>=0.10.0'}
-    hasBin: true
-
-  ssri@10.0.6:
-    resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  state-toggle@1.0.3:
-    resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==}
-
-  static-extend@0.1.2:
-    resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
-    engines: {node: '>=0.10.0'}
-
-  statuses@1.5.0:
-    resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
-    engines: {node: '>= 0.6'}
-
-  statuses@2.0.2:
-    resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
-    engines: {node: '>= 0.8'}
-
-  stop-iteration-iterator@1.1.0:
-    resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
-    engines: {node: '>= 0.4'}
-
-  streamroller@3.1.5:
-    resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==}
-    engines: {node: '>=8.0'}
-
-  string-argv@0.3.2:
-    resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
-    engines: {node: '>=0.6.19'}
-
-  string-width@3.1.0:
-    resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==}
-    engines: {node: '>=6'}
-
-  string-width@4.2.3:
-    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
-    engines: {node: '>=8'}
-
-  string-width@5.1.2:
-    resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
-    engines: {node: '>=12'}
-
-  string.prototype.trim@1.2.10:
-    resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
-    engines: {node: '>= 0.4'}
-
-  string.prototype.trimend@1.0.9:
-    resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==}
-    engines: {node: '>= 0.4'}
-
-  string.prototype.trimstart@1.0.8:
-    resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
-    engines: {node: '>= 0.4'}
-
-  string_decoder@1.1.1:
-    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
-
-  string_decoder@1.3.0:
-    resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
-
-  stringify-entities@1.3.2:
-    resolution: {integrity: sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==}
-
-  strip-ansi@3.0.1:
-    resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
-    engines: {node: '>=0.10.0'}
-
-  strip-ansi@5.2.0:
-    resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
-    engines: {node: '>=6'}
-
-  strip-ansi@6.0.1:
-    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
-    engines: {node: '>=8'}
-
-  strip-ansi@7.1.2:
-    resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
-    engines: {node: '>=12'}
-
-  strip-bom@3.0.0:
-    resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
-    engines: {node: '>=4'}
-
-  strip-final-newline@2.0.0:
-    resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
-    engines: {node: '>=6'}
-
-  strip-final-newline@3.0.0:
-    resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
-    engines: {node: '>=12'}
-
-  strip-indent@2.0.0:
-    resolution: {integrity: sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==}
-    engines: {node: '>=4'}
-
-  strip-indent@3.0.0:
-    resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
-    engines: {node: '>=8'}
-
-  strip-json-comments@2.0.1:
-    resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
-    engines: {node: '>=0.10.0'}
-
-  strip-json-comments@3.1.1:
-    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
-    engines: {node: '>=8'}
-
-  strong-log-transformer@2.1.0:
-    resolution: {integrity: sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==}
-    engines: {node: '>=4'}
-    hasBin: true
-
-  style-search@0.1.0:
-    resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==}
-
-  stylelint-config-prettier@9.0.5:
-    resolution: {integrity: sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==}
-    engines: {node: '>= 12'}
-    hasBin: true
-    peerDependencies:
-      stylelint: '>= 11.x < 15'
-
-  stylelint-config-rational-order@0.1.2:
-    resolution: {integrity: sha512-Qo7ZQaihCwTqijfZg4sbdQQHtugOX/B1/fYh018EiDZHW+lkqH9uHOnsDwDPGZrYJuB6CoyI7MZh2ecw2dOkew==}
-
-  stylelint-config-recommended@9.0.0:
-    resolution: {integrity: sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==}
-    peerDependencies:
-      stylelint: ^14.10.0
-
-  stylelint-config-standard@28.0.0:
-    resolution: {integrity: sha512-q/StuowDdDmFCravzGHAwgS9pjX0bdOQUEBBDIkIWsQuYGgYz/xsO8CM6eepmIQ1fc5bKdDVimlJZ6MoOUcJ5Q==}
-    peerDependencies:
-      stylelint: ^14.11.0
-
-  stylelint-declaration-block-no-ignored-properties@2.7.0:
-    resolution: {integrity: sha512-44SpI9+9Oc1ICuwwRfwS/3npQ2jPobDSTnwWdNgZGryGqQCp17CgEIWjCv1BgUOSzND3RqywNCNLKvO1AOxbfg==}
-    engines: {node: '>=6'}
-    peerDependencies:
-      stylelint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
-
-  stylelint-order@2.2.1:
-    resolution: {integrity: sha512-019KBV9j8qp1MfBjJuotse6MgaZqGVtXMc91GU9MsS9Feb+jYUvUU3Z8XiClqPdqJZQ0ryXQJGg3U3PcEjXwfg==}
-    engines: {node: '>=6'}
-    peerDependencies:
-      stylelint: ^9.10.1 || ^10.0.0
-
-  stylelint-order@5.0.0:
-    resolution: {integrity: sha512-OWQ7pmicXufDw5BlRqzdz3fkGKJPgLyDwD1rFY3AIEfIH/LQY38Vu/85v8/up0I+VPiuGRwbc2Hg3zLAsJaiyw==}
-    peerDependencies:
-      stylelint: ^14.0.0
-
-  stylelint@14.16.1:
-    resolution: {integrity: sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-    hasBin: true
-
-  stylelint@9.10.1:
-    resolution: {integrity: sha512-9UiHxZhOAHEgeQ7oLGwrwoDR8vclBKlSX7r4fH0iuu0SfPwFaLkb1c7Q2j1cqg9P7IDXeAV2TvQML/fRQzGBBQ==}
-    engines: {node: '>=6'}
-    hasBin: true
-
-  stylis@4.3.6:
-    resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
-
-  sugarss@2.0.0:
-    resolution: {integrity: sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==}
-
-  supports-color@2.0.0:
-    resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
-    engines: {node: '>=0.8.0'}
-
-  supports-color@5.5.0:
-    resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
-    engines: {node: '>=4'}
-
-  supports-color@7.2.0:
-    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
-    engines: {node: '>=8'}
-
-  supports-color@8.1.1:
-    resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
-    engines: {node: '>=10'}
-
-  supports-hyperlinks@2.3.0:
-    resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
-    engines: {node: '>=8'}
-
-  supports-preserve-symlinks-flag@1.0.0:
-    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
-    engines: {node: '>= 0.4'}
-
-  svg-tags@1.0.0:
-    resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
-
-  swagger-schema-official@2.0.0-bab6bed:
-    resolution: {integrity: sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA==}
-
-  swagger-typescript-api@12.0.4:
-    resolution: {integrity: sha512-04ZxlJzu3g15TupfPhS0Yk0jzV/MM23WU4uuOl2vSi4yHrxEwnkIsoBkP084ec61q4vr2FHcI3DKxC+Mt1u10Q==}
-    hasBin: true
-
-  swagger2openapi@7.0.8:
-    resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==}
-    hasBin: true
-
-  symbol-observable@4.0.0:
-    resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
-    engines: {node: '>=0.10'}
-
-  table@5.4.6:
-    resolution: {integrity: sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==}
-    engines: {node: '>=6.0.0'}
-
-  table@6.9.0:
-    resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
-    engines: {node: '>=10.0.0'}
-
-  tapable@2.3.0:
-    resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
-    engines: {node: '>=6'}
-
-  tar-stream@2.2.0:
-    resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
-    engines: {node: '>=6'}
-
-  tar@6.2.1:
-    resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
-    engines: {node: '>=10'}
-
-  temp@0.9.4:
-    resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==}
-    engines: {node: '>=6.0.0'}
-
-  terser-webpack-plugin@5.3.16:
-    resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==}
-    engines: {node: '>= 10.13.0'}
-    peerDependencies:
-      '@swc/core': '*'
-      esbuild: '*'
-      uglify-js: '*'
-      webpack: ^5.1.0
-    peerDependenciesMeta:
-      '@swc/core':
-        optional: true
-      esbuild:
-        optional: true
-      uglify-js:
-        optional: true
-
-  terser@5.29.1:
-    resolution: {integrity: sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==}
-    engines: {node: '>=10'}
-    hasBin: true
-
-  terser@5.46.0:
-    resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==}
-    engines: {node: '>=10'}
-    hasBin: true
-
-  test-exclude@6.0.0:
-    resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
-    engines: {node: '>=8'}
-
-  text-table@0.2.0:
-    resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
-
-  through@2.3.8:
-    resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
-
-  thunky@1.1.0:
-    resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
-
-  tiny-emitter@2.1.0:
-    resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
-
-  tinyexec@1.0.2:
-    resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
-    engines: {node: '>=18'}
-
-  tmp@0.0.30:
-    resolution: {integrity: sha512-HXdTB7lvMwcb55XFfrTM8CPr/IYREk4hVBFaQ4b/6nInrluSL86hfHm7vu0luYKCfyBZp2trCjpc8caC3vVM3w==}
-    engines: {node: '>=0.4.0'}
-
-  tmp@0.0.33:
-    resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
-    engines: {node: '>=0.6.0'}
-
-  tmp@0.2.3:
-    resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
-    engines: {node: '>=14.14'}
-
-  tmp@0.2.5:
-    resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
-    engines: {node: '>=14.14'}
-
-  to-object-path@0.3.0:
-    resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
-    engines: {node: '>=0.10.0'}
-
-  to-regex-range@2.1.1:
-    resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==}
-    engines: {node: '>=0.10.0'}
-
-  to-regex-range@5.0.1:
-    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
-    engines: {node: '>=8.0'}
-
-  to-regex@3.0.2:
-    resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==}
-    engines: {node: '>=0.10.0'}
-
-  toidentifier@1.0.1:
-    resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
-    engines: {node: '>=0.6'}
-
-  tough-cookie@2.5.0:
-    resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
-    engines: {node: '>=0.8'}
-
-  tr46@0.0.3:
-    resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
-
-  tree-kill@1.2.2:
-    resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
-    hasBin: true
-
-  trim-newlines@2.0.0:
-    resolution: {integrity: sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==}
-    engines: {node: '>=4'}
-
-  trim-newlines@3.0.1:
-    resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
-    engines: {node: '>=8'}
-
-  trim-trailing-lines@1.1.4:
-    resolution: {integrity: sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==}
-
-  trim@0.0.1:
-    resolution: {integrity: sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==}
-    deprecated: Use String.prototype.trim() instead
-
-  trough@1.0.5:
-    resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==}
-
-  ts-api-utils@1.4.3:
-    resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
-    engines: {node: '>=16'}
-    peerDependencies:
-      typescript: '>=4.2.0'
-
-  ts-dedent@2.2.0:
-    resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
-    engines: {node: '>=6.10'}
-
-  ts-node@10.9.1:
-    resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
-    hasBin: true
-    peerDependencies:
-      '@swc/core': '>=1.2.50'
-      '@swc/wasm': '>=1.2.50'
-      '@types/node': '*'
-      typescript: '>=2.7'
-    peerDependenciesMeta:
-      '@swc/core':
-        optional: true
-      '@swc/wasm':
-        optional: true
-
-  tsconfig-paths@3.15.0:
-    resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
-
-  tsconfig-paths@4.2.0:
-    resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
-    engines: {node: '>=6'}
-
-  tslib@2.3.0:
-    resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
-
-  tslib@2.6.2:
-    resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
-
-  tuf-js@2.2.1:
-    resolution: {integrity: sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==}
-    engines: {node: ^16.14.0 || >=18.0.0}
-
-  tunnel-agent@0.6.0:
-    resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
-
-  tweetnacl@0.14.5:
-    resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
-
-  type-check@0.4.0:
-    resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
-    engines: {node: '>= 0.8.0'}
-
-  type-fest@0.18.1:
-    resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==}
-    engines: {node: '>=10'}
-
-  type-fest@0.20.2:
-    resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
-    engines: {node: '>=10'}
-
-  type-fest@0.21.3:
-    resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
-    engines: {node: '>=10'}
-
-  type-fest@0.6.0:
-    resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
-    engines: {node: '>=8'}
-
-  type-fest@0.8.1:
-    resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
-    engines: {node: '>=8'}
-
-  type-fest@1.4.0:
-    resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
-    engines: {node: '>=10'}
-
-  type-is@1.6.18:
-    resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
-    engines: {node: '>= 0.6'}
-
-  typed-array-buffer@1.0.3:
-    resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
-    engines: {node: '>= 0.4'}
-
-  typed-array-byte-length@1.0.3:
-    resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==}
-    engines: {node: '>= 0.4'}
-
-  typed-array-byte-offset@1.0.4:
-    resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==}
-    engines: {node: '>= 0.4'}
-
-  typed-array-length@1.0.7:
-    resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
-    engines: {node: '>= 0.4'}
-
-  typed-assert@1.0.9:
-    resolution: {integrity: sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==}
-
-  typescript@4.8.4:
-    resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==}
-    engines: {node: '>=4.2.0'}
-    hasBin: true
-
-  typescript@5.3.3:
-    resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
-    engines: {node: '>=14.17'}
-    hasBin: true
-
-  ua-parser-js@0.7.41:
-    resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==}
-    hasBin: true
-
-  ufo@1.6.3:
-    resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
-
-  unbox-primitive@1.1.0:
-    resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
-    engines: {node: '>= 0.4'}
-
-  undici-types@5.26.5:
-    resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
-
-  undici@6.11.1:
-    resolution: {integrity: sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==}
-    engines: {node: '>=18.0'}
-
-  unherit@1.1.3:
-    resolution: {integrity: sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==}
-
-  unicode-canonical-property-names-ecmascript@2.0.1:
-    resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
-    engines: {node: '>=4'}
-
-  unicode-match-property-ecmascript@2.0.0:
-    resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
-    engines: {node: '>=4'}
-
-  unicode-match-property-value-ecmascript@2.2.1:
-    resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
-    engines: {node: '>=4'}
-
-  unicode-property-aliases-ecmascript@2.2.0:
-    resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
-    engines: {node: '>=4'}
-
-  unified@7.1.0:
-    resolution: {integrity: sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==}
-
-  union-value@1.0.1:
-    resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==}
-    engines: {node: '>=0.10.0'}
-
-  uniq@1.0.1:
-    resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==}
-
-  unique-filename@3.0.0:
-    resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  unique-slug@4.0.0:
-    resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  unist-util-find-all-after@1.0.5:
-    resolution: {integrity: sha512-lWgIc3rrTMTlK1Y0hEuL+k+ApzFk78h+lsaa2gHf63Gp5Ww+mt11huDniuaoq1H+XMK2lIIjjPkncxXcDp3QDw==}
-
-  unist-util-is@3.0.0:
-    resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==}
-
-  unist-util-remove-position@1.1.4:
-    resolution: {integrity: sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==}
-
-  unist-util-stringify-position@1.1.2:
-    resolution: {integrity: sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==}
-
-  unist-util-stringify-position@4.0.0:
-    resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
-
-  unist-util-visit-parents@2.1.2:
-    resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==}
-
-  unist-util-visit@1.4.1:
-    resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==}
-
-  universalify@0.1.2:
-    resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
-    engines: {node: '>= 4.0.0'}
-
-  unpipe@1.0.0:
-    resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
-    engines: {node: '>= 0.8'}
-
-  unset-value@1.0.0:
-    resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
-    engines: {node: '>=0.10.0'}
-
-  update-browserslist-db@1.2.3:
-    resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
-    hasBin: true
-    peerDependencies:
-      browserslist: '>= 4.21.0'
-
-  uri-js@4.4.1:
-    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
-
-  urix@0.1.0:
-    resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==}
-    deprecated: Please see https://github.com/lydell/urix#deprecated
-
-  url-polyfill@1.1.14:
-    resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==}
-
-  use@3.1.1:
-    resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
-    engines: {node: '>=0.10.0'}
-
-  util-deprecate@1.0.2:
-    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
-
-  utils-merge@1.0.1:
-    resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
-    engines: {node: '>= 0.4.0'}
-
-  uuid@11.1.0:
-    resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
-    hasBin: true
-
-  uuid@3.4.0:
-    resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
-    deprecated: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
-    hasBin: true
-
-  uuid@8.3.2:
-    resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
-    hasBin: true
-
-  v8-compile-cache-lib@3.0.1:
-    resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
-
-  v8-compile-cache@2.4.0:
-    resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==}
-
-  validate-npm-package-license@3.0.4:
-    resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
-
-  validate-npm-package-name@5.0.1:
-    resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
-    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
-  vary@1.1.2:
-    resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
-    engines: {node: '>= 0.8'}
-
-  verror@1.10.0:
-    resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
-    engines: {'0': node >=0.6.0}
-
-  vfile-location@2.0.6:
-    resolution: {integrity: sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==}
-
-  vfile-message@1.1.1:
-    resolution: {integrity: sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==}
-
-  vfile-message@4.0.3:
-    resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
-
-  vfile@3.0.1:
-    resolution: {integrity: sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==}
-
-  vite@5.1.7:
-    resolution: {integrity: sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==}
-    engines: {node: ^18.0.0 || >=20.0.0}
-    hasBin: true
-    peerDependencies:
-      '@types/node': ^18.0.0 || >=20.0.0
-      less: '*'
-      lightningcss: ^1.21.0
-      sass: '*'
-      stylus: '*'
-      sugarss: '*'
-      terser: ^5.4.0
-    peerDependenciesMeta:
-      '@types/node':
-        optional: true
-      less:
-        optional: true
-      lightningcss:
-        optional: true
-      sass:
-        optional: true
-      stylus:
-        optional: true
-      sugarss:
-        optional: true
-      terser:
-        optional: true
-
-  void-elements@2.0.1:
-    resolution: {integrity: sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==}
-    engines: {node: '>=0.10.0'}
-
-  vscode-jsonrpc@8.2.0:
-    resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
-    engines: {node: '>=14.0.0'}
-
-  vscode-languageserver-protocol@3.17.5:
-    resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
-
-  vscode-languageserver-textdocument@1.0.12:
-    resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
-
-  vscode-languageserver-types@3.17.5:
-    resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
-
-  vscode-languageserver@9.0.1:
-    resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
-    hasBin: true
-
-  vscode-uri@3.0.8:
-    resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
-
-  watchpack@2.4.0:
-    resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
-    engines: {node: '>=10.13.0'}
-
-  wbuf@1.7.3:
-    resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==}
-
-  wcwidth@1.0.1:
-    resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
-
-  web-streams-polyfill@3.3.3:
-    resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
-    engines: {node: '>= 8'}
-
-  webdriver-js-extender@2.1.0:
-    resolution: {integrity: sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==}
-    engines: {node: '>=6.9.x'}
-
-  webdriver-manager@12.1.9:
-    resolution: {integrity: sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==}
-    engines: {node: '>=6.9.x'}
-    hasBin: true
-
-  webidl-conversions@3.0.1:
-    resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
-
-  webpack-dev-middleware@5.3.4:
-    resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==}
-    engines: {node: '>= 12.13.0'}
-    peerDependencies:
-      webpack: ^4.0.0 || ^5.0.0
-
-  webpack-dev-middleware@6.1.2:
-    resolution: {integrity: sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==}
-    engines: {node: '>= 14.15.0'}
-    peerDependencies:
-      webpack: ^5.0.0
-    peerDependenciesMeta:
-      webpack:
-        optional: true
-
-  webpack-dev-server@4.15.1:
-    resolution: {integrity: sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==}
-    engines: {node: '>= 12.13.0'}
-    hasBin: true
-    peerDependencies:
-      webpack: ^4.37.0 || ^5.0.0
-      webpack-cli: '*'
-    peerDependenciesMeta:
-      webpack:
-        optional: true
-      webpack-cli:
-        optional: true
-
-  webpack-merge@5.10.0:
-    resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==}
-    engines: {node: '>=10.0.0'}
-
-  webpack-sources@3.3.3:
-    resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
-    engines: {node: '>=10.13.0'}
-
-  webpack-subresource-integrity@5.1.0:
-    resolution: {integrity: sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==}
-    engines: {node: '>= 12'}
-    peerDependencies:
-      html-webpack-plugin: '>= 5.0.0-beta.1 < 6'
-      webpack: ^5.12.0
-    peerDependenciesMeta:
-      html-webpack-plugin:
-        optional: true
-
-  webpack@5.90.3:
-    resolution: {integrity: sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==}
-    engines: {node: '>=10.13.0'}
-    hasBin: true
-    peerDependencies:
-      webpack-cli: '*'
-    peerDependenciesMeta:
-      webpack-cli:
-        optional: true
-
-  websocket-driver@0.7.4:
-    resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
-    engines: {node: '>=0.8.0'}
-
-  websocket-extensions@0.1.4:
-    resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
-    engines: {node: '>=0.8.0'}
-
-  whatwg-url@5.0.0:
-    resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
-
-  which-boxed-primitive@1.1.1:
-    resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
-    engines: {node: '>= 0.4'}
-
-  which-builtin-type@1.2.1:
-    resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
-    engines: {node: '>= 0.4'}
-
-  which-collection@1.0.2:
-    resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
-    engines: {node: '>= 0.4'}
-
-  which-module@2.0.1:
-    resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
-
-  which-typed-array@1.1.20:
-    resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
-    engines: {node: '>= 0.4'}
-
-  which@1.3.1:
-    resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
-    hasBin: true
-
-  which@2.0.2:
-    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
-    engines: {node: '>= 8'}
-    hasBin: true
-
-  which@4.0.0:
-    resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
-    engines: {node: ^16.13.0 || >=18.0.0}
-    hasBin: true
-
-  wildcard@2.0.1:
-    resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==}
-
-  wmf@1.0.2:
-    resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
-    engines: {node: '>=0.8'}
-
-  word-wrap@1.2.5:
-    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
-    engines: {node: '>=0.10.0'}
-
-  word@0.3.0:
-    resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
-    engines: {node: '>=0.8'}
-
-  wrap-ansi@6.2.0:
-    resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
-    engines: {node: '>=8'}
-
-  wrap-ansi@7.0.0:
-    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
-    engines: {node: '>=10'}
-
-  wrap-ansi@8.1.0:
-    resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
-    engines: {node: '>=12'}
-
-  wrappy@1.0.2:
-    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
-
-  write-file-atomic@4.0.2:
-    resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
-    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
-
-  write@1.0.3:
-    resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==}
-    engines: {node: '>=4'}
-
-  ws@8.18.3:
-    resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
-    engines: {node: '>=10.0.0'}
-    peerDependencies:
-      bufferutil: ^4.0.1
-      utf-8-validate: '>=5.0.2'
-    peerDependenciesMeta:
-      bufferutil:
-        optional: true
-      utf-8-validate:
-        optional: true
-
-  ws@8.19.0:
-    resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
-    engines: {node: '>=10.0.0'}
-    peerDependencies:
-      bufferutil: ^4.0.1
-      utf-8-validate: '>=5.0.2'
-    peerDependenciesMeta:
-      bufferutil:
-        optional: true
-      utf-8-validate:
-        optional: true
-
-  x-is-string@0.1.0:
-    resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==}
-
-  xlsx@0.18.5:
-    resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
-    engines: {node: '>=0.8'}
-    hasBin: true
-
-  xml2js@0.4.23:
-    resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
-    engines: {node: '>=4.0.0'}
-
-  xmlbuilder@11.0.1:
-    resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
-    engines: {node: '>=4.0'}
-
-  xtend@4.0.2:
-    resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
-    engines: {node: '>=0.4'}
-
-  y18n@4.0.3:
-    resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
-
-  y18n@5.0.8:
-    resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
-    engines: {node: '>=10'}
-
-  yallist@3.1.1:
-    resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
-
-  yallist@4.0.0:
-    resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
-
-  yaml@1.10.2:
-    resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
-    engines: {node: '>= 6'}
-
-  yaml@2.3.1:
-    resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==}
-    engines: {node: '>= 14'}
-
-  yargs-parser@10.1.0:
-    resolution: {integrity: sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==}
-
-  yargs-parser@18.1.3:
-    resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
-    engines: {node: '>=6'}
-
-  yargs-parser@20.2.9:
-    resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
-    engines: {node: '>=10'}
-
-  yargs-parser@21.1.1:
-    resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
-    engines: {node: '>=12'}
-
-  yargs@15.4.1:
-    resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
-    engines: {node: '>=8'}
-
-  yargs@16.2.0:
-    resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
-    engines: {node: '>=10'}
-
-  yargs@17.7.2:
-    resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
-    engines: {node: '>=12'}
-
-  yn@3.1.1:
-    resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
-    engines: {node: '>=6'}
-
-  yocto-queue@0.1.0:
-    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
-    engines: {node: '>=10'}
-
-  yocto-queue@1.2.2:
-    resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
-    engines: {node: '>=12.20'}
-
-  zone.js@0.14.5:
-    resolution: {integrity: sha512-9XYWZzY6PhHOSdkYryNcMm7L8EK7a4q+GbTvxbIA2a9lMdRUpGuyaYvLDcg8D6bdn+JomSsbPcilVKg6SmUx6w==}
-
-  zrender@5.4.4:
-    resolution: {integrity: sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==}
-
-  zrender@6.0.0:
-    resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
-
-snapshots:
-
-  '@ampproject/remapping@2.3.0':
-    dependencies:
-      '@jridgewell/gen-mapping': 0.3.13
-      '@jridgewell/trace-mapping': 0.3.31
-
-  '@angular-devkit/architect@0.1703.8(chokidar@3.6.0)':
-    dependencies:
-      '@angular-devkit/core': 17.3.8(chokidar@3.6.0)
-      rxjs: 7.8.1
-    transitivePeerDependencies:
-      - chokidar
-
-  '@angular-devkit/build-angular@17.3.8(@angular/compiler-cli@17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3))(@types/express@4.17.25)(@types/node@20.12.11)(chokidar@3.6.0)(karma@6.4.2)(protractor@7.0.0)(sugarss@2.0.0)(typescript@5.3.3)':
-    dependencies:
-      '@ampproject/remapping': 2.3.0
-      '@angular-devkit/architect': 0.1703.8(chokidar@3.6.0)
-      '@angular-devkit/build-webpack': 0.1703.8(chokidar@3.6.0)(webpack-dev-server@4.15.1(webpack@5.90.3))(webpack@5.90.3)
-      '@angular-devkit/core': 17.3.8(chokidar@3.6.0)
-      '@angular/compiler-cli': 17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3)
-      '@babel/core': 7.24.0
-      '@babel/generator': 7.23.6
-      '@babel/helper-annotate-as-pure': 7.22.5
-      '@babel/helper-split-export-declaration': 7.22.6
-      '@babel/plugin-transform-async-generator-functions': 7.23.9(@babel/core@7.24.0)
-      '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-runtime': 7.24.0(@babel/core@7.24.0)
-      '@babel/preset-env': 7.24.0(@babel/core@7.24.0)
-      '@babel/runtime': 7.24.0
-      '@discoveryjs/json-ext': 0.5.7
-      '@ngtools/webpack': 17.3.8(@angular/compiler-cli@17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3))(typescript@5.3.3)(webpack@5.90.3)
-      '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.1.7(@types/node@20.12.11)(less@4.2.0)(sass@1.71.1)(sugarss@2.0.0)(terser@5.29.1))
-      ansi-colors: 4.1.3
-      autoprefixer: 10.4.18(postcss@8.4.35)
-      babel-loader: 9.1.3(@babel/core@7.24.0)(webpack@5.90.3)
-      babel-plugin-istanbul: 6.1.1
-      browserslist: 4.28.1
-      copy-webpack-plugin: 11.0.0(webpack@5.90.3)
-      critters: 0.0.22
-      css-loader: 6.10.0(webpack@5.90.3)
-      esbuild-wasm: 0.20.1
-      fast-glob: 3.3.2
-      http-proxy-middleware: 2.0.6(@types/express@4.17.25)
-      https-proxy-agent: 7.0.4
-      inquirer: 9.2.15
-      jsonc-parser: 3.2.1
-      karma-source-map-support: 1.4.0
-      less: 4.2.0
-      less-loader: 11.1.0(less@4.2.0)(webpack@5.90.3)
-      license-webpack-plugin: 4.0.2(webpack@5.90.3)
-      loader-utils: 3.2.1
-      magic-string: 0.30.8
-      mini-css-extract-plugin: 2.8.1(webpack@5.90.3)
-      mrmime: 2.0.0
-      open: 8.4.2
-      ora: 5.4.1
-      parse5-html-rewriting-stream: 7.0.0
-      picomatch: 4.0.1
-      piscina: 4.4.0
-      postcss: 8.4.35
-      postcss-loader: 8.1.1(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3)
-      resolve-url-loader: 5.0.0
-      rxjs: 7.8.1
-      sass: 1.71.1
-      sass-loader: 14.1.1(sass@1.71.1)(webpack@5.90.3)
-      semver: 7.6.0
-      source-map-loader: 5.0.0(webpack@5.90.3)
-      source-map-support: 0.5.21
-      terser: 5.29.1
-      tree-kill: 1.2.2
-      tslib: 2.6.2
-      typescript: 5.3.3
-      undici: 6.11.1
-      vite: 5.1.7(@types/node@20.12.11)(less@4.2.0)(sass@1.71.1)(sugarss@2.0.0)(terser@5.29.1)
-      watchpack: 2.4.0
-      webpack: 5.90.3(esbuild@0.20.1)
-      webpack-dev-middleware: 6.1.2(webpack@5.90.3)
-      webpack-dev-server: 4.15.1(webpack@5.90.3)
-      webpack-merge: 5.10.0
-      webpack-subresource-integrity: 5.1.0(webpack@5.90.3)
-    optionalDependencies:
-      esbuild: 0.20.1
-      karma: 6.4.2
-      protractor: 7.0.0
-    transitivePeerDependencies:
-      - '@rspack/core'
-      - '@swc/core'
-      - '@types/express'
-      - '@types/node'
-      - bufferutil
-      - chokidar
-      - debug
-      - html-webpack-plugin
-      - lightningcss
-      - node-sass
-      - sass-embedded
-      - stylus
-      - sugarss
-      - supports-color
-      - uglify-js
-      - utf-8-validate
-      - webpack-cli
-
-  '@angular-devkit/build-webpack@0.1703.8(chokidar@3.6.0)(webpack-dev-server@4.15.1(webpack@5.90.3))(webpack@5.90.3)':
-    dependencies:
-      '@angular-devkit/architect': 0.1703.8(chokidar@3.6.0)
-      rxjs: 7.8.1
-      webpack: 5.90.3(esbuild@0.20.1)
-      webpack-dev-server: 4.15.1(webpack@5.90.3)
-    transitivePeerDependencies:
-      - chokidar
-
-  '@angular-devkit/core@17.3.8(chokidar@3.6.0)':
-    dependencies:
-      ajv: 8.12.0
-      ajv-formats: 2.1.1(ajv@8.12.0)
-      jsonc-parser: 3.2.1
-      picomatch: 4.0.1
-      rxjs: 7.8.1
-      source-map: 0.7.4
-    optionalDependencies:
-      chokidar: 3.6.0
-
-  '@angular-devkit/schematics@17.3.8(chokidar@3.6.0)':
-    dependencies:
-      '@angular-devkit/core': 17.3.8(chokidar@3.6.0)
-      jsonc-parser: 3.2.1
-      magic-string: 0.30.8
-      ora: 5.4.1
-      rxjs: 7.8.1
-    transitivePeerDependencies:
-      - chokidar
-
-  '@angular-eslint/builder@17.5.2(eslint@8.56.0)(typescript@5.3.3)':
-    dependencies:
-      '@nx/devkit': 19.8.14(nx@19.8.14)
-      eslint: 8.56.0
-      nx: 19.8.14
-      typescript: 5.3.3
-    transitivePeerDependencies:
-      - '@swc-node/register'
-      - '@swc/core'
-      - debug
-
-  '@angular-eslint/bundled-angular-compiler@17.5.2': {}
-
-  '@angular-eslint/eslint-plugin-template@17.5.2(eslint@8.56.0)(typescript@5.3.3)':
-    dependencies:
-      '@angular-eslint/bundled-angular-compiler': 17.5.2
-      '@angular-eslint/utils': 17.5.2(eslint@8.56.0)(typescript@5.3.3)
-      '@typescript-eslint/type-utils': 7.11.0(eslint@8.56.0)(typescript@5.3.3)
-      '@typescript-eslint/utils': 7.11.0(eslint@8.56.0)(typescript@5.3.3)
-      aria-query: 5.3.0
-      axobject-query: 4.0.0
-      eslint: 8.56.0
-      typescript: 5.3.3
-    transitivePeerDependencies:
-      - supports-color
-
-  '@angular-eslint/eslint-plugin@17.5.2(eslint@8.56.0)(typescript@5.3.3)':
-    dependencies:
-      '@angular-eslint/bundled-angular-compiler': 17.5.2
-      '@angular-eslint/utils': 17.5.2(eslint@8.56.0)(typescript@5.3.3)
-      '@typescript-eslint/utils': 7.11.0(eslint@8.56.0)(typescript@5.3.3)
-      eslint: 8.56.0
-      typescript: 5.3.3
-    transitivePeerDependencies:
-      - supports-color
-
-  '@angular-eslint/schematics@17.5.2(@angular/cli@17.3.8(chokidar@3.6.0))(eslint@8.56.0)(typescript@5.3.3)':
-    dependencies:
-      '@angular-eslint/eslint-plugin': 17.5.2(eslint@8.56.0)(typescript@5.3.3)
-      '@angular-eslint/eslint-plugin-template': 17.5.2(eslint@8.56.0)(typescript@5.3.3)
-      '@angular/cli': 17.3.8(chokidar@3.6.0)
-      '@nx/devkit': 19.8.14(nx@19.8.14)
-      ignore: 5.3.1
-      nx: 19.8.14
-      strip-json-comments: 3.1.1
-      tmp: 0.2.3
-    transitivePeerDependencies:
-      - '@swc-node/register'
-      - '@swc/core'
-      - debug
-      - eslint
-      - supports-color
-      - typescript
-
-  '@angular-eslint/template-parser@17.5.2(eslint@8.56.0)(typescript@5.3.3)':
-    dependencies:
-      '@angular-eslint/bundled-angular-compiler': 17.5.2
-      eslint: 8.56.0
-      eslint-scope: 8.4.0
-      typescript: 5.3.3
-
-  '@angular-eslint/utils@17.5.2(eslint@8.56.0)(typescript@5.3.3)':
-    dependencies:
-      '@angular-eslint/bundled-angular-compiler': 17.5.2
-      '@typescript-eslint/utils': 7.11.0(eslint@8.56.0)(typescript@5.3.3)
-      eslint: 8.56.0
-      typescript: 5.3.3
-    transitivePeerDependencies:
-      - supports-color
-
-  '@angular/animations@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))':
-    dependencies:
-      '@angular/core': 17.3.10(rxjs@7.8.1)(zone.js@0.14.5)
-      tslib: 2.6.2
-
-  '@angular/cdk@17.3.10(@angular/common@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1))(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1)':
-    dependencies:
-      '@angular/common': 17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5))(rxjs@7.8.1)
-      '@angular/core': 17.3.10(rxjs@7.8.1)(zone.js@0.14.5)
-      rxjs: 7.8.1
-      tslib: 2.6.2
-    optionalDependencies:
-      parse5: 7.3.0
-
-  '@angular/cli@17.3.8(chokidar@3.6.0)':
-    dependencies:
-      '@angular-devkit/architect': 0.1703.8(chokidar@3.6.0)
-      '@angular-devkit/core': 17.3.8(chokidar@3.6.0)
-      '@angular-devkit/schematics': 17.3.8(chokidar@3.6.0)
-      '@schematics/angular': 17.3.8(chokidar@3.6.0)
-      '@yarnpkg/lockfile': 1.1.0
-      ansi-colors: 4.1.3
-      ini: 4.1.2
-      inquirer: 9.2.15
-      jsonc-parser: 3.2.1
-      npm-package-arg: 11.0.1
-      npm-pick-manifest: 9.0.0
-      open: 8.4.2
... 167265 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 28/50: test(web-next): add HertzBeat 2.0 gap and ecosystem gates

Posted by zh...@apache.org.
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 7e2b3582e16cde7bd028c1299e848c9a8efdd368
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 15:18:47 2026 +0800

    test(web-next): add HertzBeat 2.0 gap and ecosystem gates
---
 web-next/docs/parity/entity-workbench-brief.md     |   51 +
 .../parity/legacy-frontend-full-parity-brief.md    |  323 ++
 .../docs/parity/monitor-editor-collector-brief.md  |   17 +
 web-next/docs/parity/release-readiness-brief.md    |   11 +
 web-next/docs/parity/session-security-brief.md     | 1067 +++++++
 web-next/docs/parity/time-context-visual-brief.md  | 3278 ++++++++++++++++++++
 .../docs/parity/topology-product-ux-baseline.md    |  100 +
 web-next/lib/ecosystem-alignment.test.ts           |  177 ++
 web-next/lib/ecosystem-alignment.ts                |  494 +++
 web-next/lib/hertzbeat-2-gap-audit.test.ts         |  541 ++++
 web-next/lib/hertzbeat-2-gap-audit.ts              |  708 +++++
 11 files changed, 6767 insertions(+)

diff --git a/web-next/docs/parity/entity-workbench-brief.md b/web-next/docs/parity/entity-workbench-brief.md
new file mode 100644
index 0000000000..1aa84994e8
--- /dev/null
+++ b/web-next/docs/parity/entity-workbench-brief.md
@@ -0,0 +1,51 @@
+# Entity Workbench Brief
+
+Local-only parity note for milestone 7.
+
+## 2026-05-13 Entity-Centered Workbench Boundary
+
+- Hierarchy: entity detail should lead with resource identity, inherited time/source context, and real evidence totals, then branch into metrics, logs, traces, alerts, topology, monitor binding, and definition actions. Cross-signal cards must be handoffs from the entity evidence model, not standalone demo panels.
+- Density: keep the cold operator surface compact and scannable. Prefer evidence rows, links, and counters over explanatory banners, marketing copy, large empty cards, or repeated hero blocks.
+- Anti-AI-slop: never show fabricated zero panels, synthetic health, guessed RED/USE, fake topology, or placeholder trace/log/alert evidence to make the page feel complete. Missing evidence must stay explicit and actionable.
+- Operator workflow clarity: an operator should see which entity is being investigated, which time/source context is inherited, what evidence exists, and where each drilldown will land before leaving the page.
+- Context visibility: entity id/name, service, namespace, environment, time range, monitor/source context, trace/span context, alert state, topology target, and monitor/template binding must remain visible in data attributes, URLs, or read-model rows for contract tests and browser smoke.
+
+## 2026-05-13 Explicit Entity Evidence Boundary
+
+- Hierarchy: when ingestion already carries `hertzbeat.entity_id`, entity detail should trust that explicit resource attribution as real evidence even if canonical OTEL identity fields are incomplete.
+- Density: explicit-ID evidence should flow into the same compact RED/USE counters and rows; do not add a separate diagnostic panel just to explain the fallback.
+- Anti-AI-slop: do not fabricate service/namespace/environment values from the entity id. Keep missing canonical context blank/unknown while preserving the signal count.
+- Operator workflow clarity: the entity workbench should not appear empty after the ingestion layer has resolved an entity id; real metrics/logs/traces must remain drillable.
+- Context visibility: explicit `hertzbeat.entity_id` and `hertzbeat_workspace_id`/`hertzbeat.workspace_id` must be preserved in resource/read-model maps so tests can distinguish real entity attribution from guessed identity matching.
+
+## 2026-05-13 OTLP Candidate Entity Boundary
+
+- Hierarchy: OTLP intake should distinguish confirmed bound entities from unbound telemetry identities, then route unbound identities toward entity discovery instead of making operators infer candidates from raw samples.
+- Density: show a short, capped candidate list with service, namespace/environment, signal coverage, and primary identity; do not create a large second dashboard.
+- Anti-AI-slop: candidates are suggestions, not entities. Do not invent health, owners, topology, monitor binds, or generated descriptions before an operator confirms them.
+- Operator workflow clarity: a newly reporting service with real OTLP evidence should have an obvious "candidate entity" handoff even when no HertzBeat entity exists yet.
+- Context visibility: candidate rows must preserve canonical identities and signal names in backend DTOs/data attributes so contracts can verify that candidates are derived from telemetry, not from static examples.
+
+## 2026-05-13 Discovery Confirmation Boundary
+
+- Hierarchy: `/entities/discovery` must preserve OTLP candidate identity context from the intake handoff before any monitor-search result, so the operator knows which telemetry identity is being confirmed.
+- Density: keep the handoff as a compact strip above search/policy/table controls; avoid a second hero, wizard, or explanatory right rail.
+- Anti-AI-slop: candidate handoff may prefill search and carry identity context, but must not create an entity, assign ownership, infer health, or claim topology until the operator confirms.
+- Operator workflow clarity: the page should offer explicit search/confirm/create-draft actions for the candidate while leaving normal discovery search intact.
+- Context visibility: `identityKey`, `identityValue`, `serviceName`, `serviceNamespace`, and `environment` from the URL must survive in data attributes and action URLs for tests and browser smoke.
+
+## 2026-05-13 Candidate Draft Boundary
+
+- Hierarchy: `/entities/new?source=otlp-candidate...` should open a prefilled entity draft from the confirmed OTLP candidate identity, then require the operator to save before catalog state changes.
+- Density: reuse the existing cold entity composer and telemetry handoff panel; do not add a wizard, modal, or duplicate discovery hero for this narrow handoff.
+- Anti-AI-slop: the draft may seed type, name, display name, namespace, environment, source, and identities, but must not fabricate owner, monitor binds, health, topology, or relations.
+- Operator workflow clarity: the draft should make it obvious that the source is telemetry discovery and that identity evidence is present even when no traditional monitor bind exists.
+- Context visibility: `source=otlp-candidate`, `identityKey`, `identityValue`, `serviceName`, `serviceNamespace`, and `environment` must survive the route seed, cache key, draft payload, attribution panel, and return link.
+
+## 2026-05-13 Evidence Handoff Boundary
+
+- Hierarchy: entity detail should expose alert, topology, and runbook handoffs only after the unified evidence/read model has shown real entity context; handoffs are investigation exits, not standalone product cards.
+- Density: use a compact action cluster with evidence labels and counts. Avoid a new hero, right rail, or repeated overview panel for these narrow operator exits.
+- Anti-AI-slop: do not show active alert counts, topology targets, or runbook links unless the backend/entity model provides evidence. Missing evidence must not be padded with fake zero states.
+- Operator workflow clarity: each handoff should name why it appears and where it will land so the operator can jump from an entity to alert evidence, topology context, or response guidance without losing investigation state.
+- Context visibility: entity id/name, active alert count/status, topology relation target, runbook URL/title, time range, and source context must survive in data attributes or URLs for contract tests and browser smoke.
diff --git a/web-next/docs/parity/legacy-frontend-full-parity-brief.md b/web-next/docs/parity/legacy-frontend-full-parity-brief.md
new file mode 100644
index 0000000000..c1880bd99a
--- /dev/null
+++ b/web-next/docs/parity/legacy-frontend-full-parity-brief.md
@@ -0,0 +1,323 @@
+# Legacy Frontend Full Parity Brief
+
+Local-only parity note for the post-M9 frontend parity correction.
+
+## 2026-05-13 M10 Legacy Frontend Functional Parity Boundary
+
+- Hierarchy: route ownership is only the first gate. Release readiness now requires old Angular operator functions to be proven in the Next frontend with the same navigation, read, mutation, bulk-action, SSE/live, and context handoff behavior.
+- Density: keep the audit as a compact operator matrix. Do not add marketing copy, explanatory hero sections, duplicate navigation groups, or decorative placeholder cards while closing missing legacy functions.
+- Anti-AI-slop: a route cannot be counted complete because it renders a page, redirect, static shell, fixture row, or placeholder. Candidate pages must prove real API read/mutation behavior and visible operator controls against the old Angular capability.
+- Operator workflow clarity: each legacy workflow needs an explicit old source, Next destination, expected controls, minimum browser/API smoke, and release status. `hold` and `placeholder` routes remain blockers until either implemented or deliberately cut by product decision.
+- Context visibility: route path, query parameters, selected ids, entity/monitor return context, time range, SSE/live source, mutation endpoint, and success/error feedback must stay visible in tests, data attributes, or browser smoke evidence.
+
+## Initial Audit Findings
+
+- Route-level coverage is broad but not sufficient: Angular core route families are present in Next or aliases, yet `web-next/lib/nav.ts` still marks 4 primary routes as `hold` and 3 primary routes as `placeholder`.
+- Current `hold` primary routes: `/log/manage`, `/trace/manage`, `/monitors/[monitorId]`, `/passport/login`.
+- Current `placeholder` primary routes: `/incidents`, `/actions`, `/explorer`.
+- Angular placeholders for incidents/actions/topology/explorer were intentionally shell-only, but Next still must not count them as complete product functions unless a later product decision says the shell is enough.
+- The old Angular shell includes server-backed alert/manager SSE notification behavior, server mute config, AI chat modal, about modal with "do not show again", settings drawer, fullscreen, locale, lock, and account actions. The Next shell has analogous chrome, but some behavior is downgraded to polling, local-only mute, route query handoff, or external documentation link and needs explicit parity decisions.
+- Monitor detail remains a known high-risk parity area: Angular has realtime/history/favorite tab rhythm, metric row drilldowns, chart/table switching, quick time presets, refresh controls, and favorite metric behavior. The Next route is still `hold`.
+- Monitor management, alert center, alert rules, notification rules/templates/receivers, public status management, collectors, templates, plugins, labels, system config, message server, object store, token management, and entity workbench all need action-level parity evidence, not just a route test.
+
+## M10 Gate Shape
+
+1. Build a legacy function inventory from Angular routes/components/services and Next routes/controllers/surfaces.
+2. Mark each old workflow as `covered`, `needs-browser-proof`, `hold`, `placeholder`, or `missing`.
+3. Write the failing parity contract before changing product code.
+4. Close one missing or hold slice per pass with the smallest implementation and a focused browser/API smoke.
+5. Only mark M10 complete when no old Angular operator workflow is untriaged, no primary route remains `hold` without product approval, and no placeholder is counted as feature-complete.
+
+## 2026-05-13 Contract Baseline
+
+- `web-next/lib/legacy-frontend-parity.test.ts` is the first M10 contract.
+- `buildLegacyFrontendParityAudit()` records the route catalog count, primary hold routes, primary placeholder routes, and legacy operator areas.
+- `validateLegacyFrontendParityGate()` intentionally returns an invalid gate while hold/placeholder routes and unproven legacy areas remain.
+- This contract is a release blocker inventory, not a claim that the listed areas are implemented.
+
+## 2026-05-13 YML Definition Workspace Plan
+
+- Hierarchy: `/setting/define` must be treated as the old monitor-template YML definition workspace, not as alert-rule authoring. The primary old sources are `web-app/src/app/routes/setting/define/define.component.ts`, `define.component.html`, `web-app/src/app/service/app-define.service.ts`, and backend `/api/apps` plus `/api/config/template/{app}` contracts.
+- Density: preserve the old operator rhythm: left categorized monitor-template tree, compact toolbar, single YAML editor, diff editor only while editing an existing template, and no extra datasource/status/preview panels unless a later product decision asks for them.
+- Anti-AI-slop: do not synthesize a monitor-template editor from alert definitions, datasource status, PromQL previews, or fixture rows. A passing route shell is not enough; the contract must prove monitor-template YML load/save/delete/hide behavior against the old endpoints.
+- Operator workflow clarity: first red contract should prove the current Next implementation is wrong by expecting `/apps/hierarchy`, `/apps/{app}/define/yml`, `/apps/define/yml` POST/PUT, `/apps/{app}/define/yml` DELETE, and `/config/template/{app}` hide/show ownership, with `prometheus` and `__system__` hidden from the menu.
+- Context visibility: keep selected `app` query state, app label, category, hide state, read-only vs edit mode, original YAML, edited YAML, save-apply success/error, delete success/error, and startup config reload evidence visible in tests or browser smoke.
+- Planned first slice: replace the current alert-definition backing model for `/setting/define` with a monitor-template YML backing model and surface contract. Do not broaden into monitor detail, OTLP, alert-rule authoring, or generic settings parity until this slice validates green.
+
+## 2026-05-13 YML Definition Workspace Slice Result
+
+- Completed first M10 YML slice: `/setting/define` now uses the old monitor-template YML API contract instead of alert-definition APIs.
+- Covered behavior: `/apps/hierarchy` category menu with `prometheus` and `__system__` filtering, `/apps/{app}/define/yml` selected YAML loading, `/apps/define/yml` POST/PUT save/apply ownership, `/apps/{app}/define/yml` delete ownership, `/config/template/{app}` hide/show ownership, selected `app` query cache key, `app-*.yml` label, and a single YAML editor without alert preview or datasource panels.
+- Remaining risk: this slice proves the backing contract and visible shell, but it does not yet prove full live mutation success against every template storage backend or old Angular diff-editor parity during complex edits. Keep M10 open for the next old-frontend workflow gap.
+
+## 2026-05-13 YML Existing-Template Diff Edit Plan
+
+- Hierarchy: old Angular switches existing templates into `nzEditorMode="diff"` with `nzOriginalText=originalCode`; Next must expose the same operator boundary as original YAML versus edited YAML when `selectedApp` is present and editing is active.
+- Density: keep the edit surface compact. The old workflow is a two-pane code comparison inside the same template workspace, not a new review page, modal, preview panel, or explanatory guide.
+- Anti-AI-slop: do not fake diff parity by only toggling read-only state on one editor. The contract must prove both original and current YAML are visible with stable data markers so browser smoke can distinguish rollback context from the editable buffer.
+- Operator workflow clarity: Cancel must still reset the current editor to `originalYaml`, Save must apply the edited buffer, and the monitor-template app label/file label must remain visible while the two YAML panes are shown.
+- Context visibility: expose `monitor-template-yaml-original`, `monitor-template-yaml`, and a `monitor-template-diff` shell only for existing-template edit mode; new-template drafts continue to use the normal single editor because old Angular does not show diff mode for `currentApp == null`.
+
+## 2026-05-13 YML Save Guard Plan
+
+- Hierarchy: old Angular gates `define.save-apply` behind `!loading && code != originalCode`; Next must preserve that mutation boundary instead of treating edit mode alone as permission to save.
+- Density: unchanged edit mode should show Cancel, Delete, file context, and the diff panes, but should not spend toolbar space on a no-op Save action.
+- Anti-AI-slop: do not simulate mutation readiness by showing a Save button that cannot produce a real changed YAML payload. The UI contract must compare `editorValue` and `originalYaml`.
+- Operator workflow clarity: Save appears only after the current buffer differs from original YAML; Cancel remains available to reset the current buffer; Delete/hide keep their existing app-level ownership.
+- Context visibility: expose the save guard through component tests and browser smoke by verifying unchanged existing-template edit mode has no `保存并应用` action while changed buffers still expose it.
+
+## 2026-05-13 YML Mutation Confirmation Plan
+
+- Hierarchy: old Angular uses `modal.confirm` for both `onSaveAndApply()` and `onDeleteDefineYml()` before the YML mutation runs; Next must restore that safety boundary for monitor-template save/apply and delete.
+- Density: confirmation should reuse the compact shared cold confirm dialog, not add inline warnings, extra cards, or a multi-step wizard to the dense YML workspace.
+- Anti-AI-slop: do not call `onSave` or `onDelete` directly from toolbar buttons and then merely show a message. The contract must prove the first click opens confirmation, and only the confirm action invokes the mutation handler.
+- Operator workflow clarity: Save confirmation uses the old `define.save-apply.confirm` copy; Delete confirmation includes the selected app label like old `define.delete.confirm`; both keep OK/Cancel labels visible.
+- Context visibility: expose `data-setting-define-save-confirm` and `data-setting-define-delete-confirm` wrappers so browser smoke can verify closed/open dialog state without relying on copy alone.
+
+## 2026-05-13 YML Template Visibility Confirmation Plan
+
+- Hierarchy: old Angular renders each template visibility action inside the monitor-template tree prefix and gates it with `nz-popconfirm`; Next must keep hide/show as a per-template menu action, not a toolbar-level batch setting or direct mutation.
+- Density: reuse the same compact cold confirmation layer already used for save/delete so the dense template list does not gain extra warning rows, helper cards, or duplicated state panels.
+- Anti-AI-slop: do not treat a hide/show icon click as proof of parity if it immediately calls `/config/template/{app}`. The contract must prove the first click opens confirmation and the API callback is invoked only from the confirm action.
+- Operator workflow clarity: hidden templates use the old `define.hide-true.confirm` copy before being displayed; visible templates use `define.hide-false.confirm` before being hidden. Labels remain `显示` / `隐藏` in the row action.
+- Context visibility: expose a single `data-setting-define-template-visibility-confirm` state wrapper plus selected template app/action data so browser smoke can distinguish closed/open confirmation and verify the target app without mutating the wrong template.
+
+## 2026-05-13 YML Edit Toolbar State Plan
+
+- Hierarchy: old Angular renders the existing-template edit action only when `currentApp != null && !isEditing`; once the operator enters edit mode the toolbar should switch to rollback/mutation actions instead of keeping a second edit affordance.
+- Density: edit mode should keep the toolbar compact with Cancel, conditional Save, Delete, file context, and theme toggle. Leaving Edit visible beside Cancel adds a duplicate no-op control to the dense YML workspace.
+- Anti-AI-slop: do not count the diff editor as old-frontend parity if the surrounding command row still exposes controls the old workflow intentionally hid. The contract must prove edit mode suppresses the Edit button, not just that diff panes render.
+- Operator workflow clarity: entering edit mode should make the next safe choices obvious: Cancel rolls back to original YAML, Save appears only for changed YAML, Delete remains app-scoped, and Edit is not actionable until edit mode exits.
+- Context visibility: component and browser smoke should verify the existing-template edit state by checking the diff shell, Cancel action, and absence of the Edit action while `selectedApp` remains visible.
+
+## 2026-05-13 YML Empty Save Guard Plan
+
+- Hierarchy: old Angular checks `code == undefined || code === '' || code == null` inside `onSaveAndApply()` before opening `modal.confirm`; Next must keep the empty-template warning in front of the save confirmation and API mutation.
+- Density: the guard should reuse the existing compact feedback line in the YML editor shell instead of adding a new validation panel, wizard, or persistent warning card.
+- Anti-AI-slop: do not treat the save confirmation as sufficient safety for an empty YAML buffer. The contract must prove an empty changed buffer does not open the confirm dialog and does not call the save handler.
+- Operator workflow clarity: an empty buffer still counts as changed, so the Save action can be visible, but clicking it must produce the old `define.save-apply.no-code` warning and leave the operator in edit/diff context.
+- Context visibility: expose the closed save-confirm wrapper, no-code warning copy, selected app/file label, and unchanged diff shell in component and browser smoke so the validation path is distinguishable from save/apply confirmation.
+
+## 2026-05-13 YML Save Apply Feedback Plan
+
+- Hierarchy: old Angular `saveAndApply()` treats a successful YML save as an applied monitor-template change, reloads menu/template context, triggers startup config reload, and shows `common.notify.apply-success`; Next must preserve that operator meaning instead of downgrading it to a generic definition-saved message.
+- Density: keep success feedback in the existing compact editor message line. Do not add a second notification rail, status card, or release-style freshness chrome to the YML workspace.
+- Anti-AI-slop: do not claim save/apply parity if the visible feedback says only "definition saved" while the action label, confirmation copy, and old workflow all mean "apply". The contract must bind the save success path to `common.notify.apply-success`.
+- Operator workflow clarity: after a successful confirmed save, editing exits, the cache reloads, original YAML catches up to the saved buffer, and the visible feedback reads as apply success; failure still uses the existing save failure path.
+- Context visibility: page contract and browser smoke should distinguish save/apply success feedback from hide/show apply feedback by proving the `saveTemplateDefine` success branch itself sets `common.notify.apply-success`.
+
+## 2026-05-13 YML New Draft Toolbar Plan
+
+- Hierarchy: old Angular shows the `define.new` entry only from an existing selected template and shows Cancel only for `isEditing && currentApp != null`; once the operator is already in a new-template draft, the command row should not keep duplicate New or existing-template rollback actions.
+- Density: the new draft command row stays sparse: Save appears only when the draft has changed, while app-scoped file, Edit, Cancel, and Delete controls stay out of the draft context.
+- Anti-AI-slop: do not count the new-template workspace as old-frontend parity if it exposes generic edit-mode controls that the old monitor-template YML workflow intentionally hid. The contract must distinguish a new draft from an existing-template edit.
+- Operator workflow clarity: a new draft is committed by Save/Apply or abandoned by selecting an existing template from the menu; Cancel remains the rollback action for existing-template diff edits only.
+- Context visibility: component and browser smoke should prove `selectedApp=null` with `isEditing=true` renders the normal single `monitor-template-yaml` editor, no diff shell, no monitor file link, no duplicate New, no Cancel, and no Delete.
+
+## 2026-05-13 YML New Draft Placeholder/Dirty-State Plan
+
+- Hierarchy: old Angular seeds new monitor-template drafts from the localized `define.new.code` help comment, then appends five blank lines to the editable buffer while keeping `originalCode` as the bare help comment. Next must not replace that with a synthesized `app: custom` skeleton.
+- Density: keep the editor as the only place where the draft guidance appears. Do not add a guide card, example panel, or generated boilerplate that could be mistaken for a valid template.
+- Anti-AI-slop: do not fabricate a YML template structure just because the editor is empty. The old workflow asks the operator to write real monitor-template YML and links the extension-point docs; fake fields like `metrics: availability` are worse than a blank draft.
+- Operator workflow clarity: because the editable buffer intentionally differs from `originalYaml`, Save/Apply is visible immediately for a new draft, matching old `code != originalCode`; empty-save protection and confirmation still apply before mutation.
+- Context visibility: contracts and browser smoke should prove the draft contains the old localized help comment, not the generated skeleton, and that `selectedApp=null` new mode exposes Save while keeping app-scoped controls hidden.
+
+## 2026-05-13 YML Mutation Startup Reload Plan
+
+- Hierarchy: old Angular calls `startUpSvc.loadConfigResourceViaHttp()` after successful save/apply, delete, and template hide/show mutations; Next must preserve the same post-mutation startup context refresh instead of only updating the local editor cache.
+- Density: keep this as invisible runtime behavior. Do not add a reload button, status rail, or explanatory copy to the compact YML workspace.
+- Anti-AI-slop: do not treat the local `/setting/define` reloadVersion as full parity if the wider app startup/menu context never re-reads system locale and monitor-template hierarchy after a YML mutation.
+- Operator workflow clarity: after each successful mutation, the app should refresh startup context from `/config/system` and `/apps/hierarchy?lang=...` so monitor-template visibility and menu availability converge with the backend without requiring a browser refresh.
+- Context visibility: controller/page contracts should prove the reload helper owns `/config/system` plus `/apps/hierarchy`, and browser smoke should intercept a mutation and observe the startup reload requests while preventing real template writes.
+
+## 2026-05-13 YML New Draft Save Continuation Plan
+
+- Hierarchy: old Angular `saveAndApply()` only reloads existing-template content when `currentApp != null`; for a new YML draft it keeps `currentApp == null`, leaves the edited code buffer in place, and does not promote `originalCode` to the saved buffer.
+- Density: keep the new draft in the same editor without introducing a completion panel, redirect, wizard, or explanatory success state. The operator can keep refining the draft or select the newly-added template from the menu after menu reload.
+- Anti-AI-slop: do not freeze the new-template editor just because the save succeeded. Hiding Save and making the draft read-only gives a false sense that the route now owns a concrete `app` when old Angular still treats it as an unselected draft.
+- Operator workflow clarity: existing-template save should still exit edit mode and catch `originalYaml` up to the backend content; new-template save should keep `selectedApp=null`, preserve the previous draft `originalYaml` boundary, keep edit mode active, and continue showing Save because the editable buffer still differs from the localized starter comment.
+- Context visibility: page contracts and browser smoke should distinguish new save from existing save by proving the saved new draft remains editable with `保存并应用` visible, while existing-template save retains the previous apply-success behavior.
+
+## 2026-05-13 YML Query Ownership Plan
+
+- Hierarchy: old Angular `/setting/define` route state is only the selected monitor-template `app` query param plus the in-page menu search. Alert-rule pagination/search belongs to the alert setting route, not the monitor-template YML route.
+- Density: keep route state small and transparent. Do not carry alert-rule list pagination, sort, or encoded alert search payloads in the YML define package.
+- Anti-AI-slop: do not leave `/alert/defines` helpers under `lib/setting-define` and call that release parity. That artifact came from the suspect alert-rule define implementation and makes the YML package look connected to the wrong backend domain.
+- Operator workflow clarity: `/setting/define?app=mysql` should hydrate `currentApp=mysql`; `/setting/define` should hydrate the old new-template draft. Multi-value route params should use the first app value like other compatibility readers.
+- Context visibility: contracts should prove `lib/setting-define/query-state.ts` owns only monitor-template route selection and contains no `/alert/defines`, while alert setting keeps its own alert-list URL builder.
+
+## 2026-05-13 YML Menu Order Plan
+
+- Hierarchy: old Angular builds the define menu by iterating `/apps/hierarchy` in backend order, appending each template to its first-seen category, and filtering only `prometheus` plus `__system__`; Next must not alphabetize categories or template rows after normalization.
+- Density: the menu should stay a dense backend-owned catalog, not become a client-curated taxonomy with reordered sections that operators cannot map back to template definition order.
+- Anti-AI-slop: do not make the list look cleaner by sorting labels if that changes legacy navigation muscle memory or hides backend ordering defects that the old workspace exposed directly.
+- Operator workflow clarity: search may filter matching rows, but the relative order of matched categories and rows must remain the same as the original hierarchy response.
+- Context visibility: controller and view-model contracts should prove first-seen group order and in-group item order survive filtering, while filtered hidden/system templates are still excluded.
+
+## 2026-05-13 YML Failure Feedback Plan
+
+- Hierarchy: old Angular save/apply failures call `notify.error(common.notify.apply-fail, message.msg)`, so the operator sees both the action-level failure title and the backend explanation. Next must not collapse that into only the raw backend message.
+- Density: keep feedback in the existing compact editor message line; do not add a toast rail, alert card, or second message surface to the YML workspace.
+- Anti-AI-slop: do not claim mutation parity if the failed save says only `schema invalid` without the old action context `应用失败`. The contract must bind the failed save path to the same apply-fail title used by old Angular.
+- Operator workflow clarity: failed save/apply keeps the operator in the same edit/diff context, preserves the changed YAML, and displays the failure title plus backend detail so the next repair step is obvious.
+- Context visibility: page contracts and browser smoke should prove the failed `/apps/define/yml` path includes both localized `common.notify.apply-fail` and the backend detail, with no startup reload or success message.
+
+## 2026-05-13 YML Delete Failure Feedback Plan
+
+- Hierarchy: old Angular delete failures call `notify.error(common.notify.delete-fail, message.msg)`, so the monitor-template delete path needs its own delete-failure title plus backend detail instead of the save/apply title or a raw exception.
+- Density: keep the failure in the existing compact editor message line. Do not add a new destructive-action panel, persistent warning card, or secondary notification area to the dense YML workspace.
+- Anti-AI-slop: do not claim delete parity if the failed delete says only `template is still in use` or renders the untranslated `common.notify.delete-fail` key. The contract must prove the localized delete-failure context is visible.
+- Operator workflow clarity: failed delete keeps the selected app, current editor buffer, and edit/read state intact, and must not reset to a new draft, emit delete success, or reload startup/menu context.
+- Context visibility: page contract and browser smoke should prove failed `DELETE /apps/{app}/define/yml` includes both `common.notify.delete-fail` and backend detail, with no post-failure startup reload or success message.
+
+## 2026-05-13 YML Visibility Failure Feedback Plan
+
+- Hierarchy: old Angular `updateAppTemplateConfig()` failures call `notify.error(common.notify.apply-fail, message.msg)` for the per-template hide/show config path, so Next must treat visibility changes as apply mutations and keep the action-level apply-failure title.
+- Density: keep failed hide/show feedback in the same compact editor message line. Do not add a per-row error rail, warning card, or menu-level status column to the dense template list.
+- Anti-AI-slop: do not claim hide/show parity if failed `/config/template/{app}` shows only `visibility locked` or another raw backend string without `应用失败`; that loses the old operator context that this was an apply-style configuration mutation.
+- Operator workflow clarity: failed visibility changes must keep the current selected template and editor context unchanged, keep the row in place, and avoid success feedback or startup/menu reload.
+- Context visibility: page contract and browser smoke should prove failed `PUT /config/template/{app}` includes both localized `common.notify.apply-fail` and backend detail, with no post-failure `/config/system` or hierarchy reload.
+
+## 2026-05-13 YML Menu Search Filter Plan
+
+- Hierarchy: old Angular `app-monitor-select-list` filters monitor-template sections only by each child template's visible `label`, while category headers remain grouping context for the matched children. Next must not let raw `category` or `value` fields create matches that the old menu would not show.
+- Density: keep the existing compact search row and dense grouped list. Do not add extra filter chips, generated helper text, or a second search surface to explain mismatched raw IDs.
+- Anti-AI-slop: do not overbroaden search just because raw template fields are available. Searching `database` should not surface every database category row unless a visible template label itself contains `database`.
+- Operator workflow clarity: operators search by the same names they see in the list, and filtered results preserve the original backend/category order with only matching child rows visible.
+- Context visibility: view-model and browser contracts should prove a label match keeps the row, while raw category/value-only terms collapse the list to the existing empty state without mutating selection or editor context.
+
+## 2026-05-13 YML Category Label Plan
+
+- Hierarchy: old Angular `renderCategoryName(category)` renders menu section headers through `menu.monitor.<category>` and falls back to uppercase only when the translation key is missing. Next must keep those localized category headers instead of exposing raw backend taxonomy keys like `DB`, `OS`, or `CACHE`.
+- Density: translate the existing compact group headers in place. Do not add a taxonomy legend, helper copy, or a separate category column to compensate for raw IDs.
+- Anti-AI-slop: do not hardcode a local category map inside the YML surface. Category copy belongs to the same i18n keys used by the legacy menu and must fall back cleanly for unknown custom categories.
+- Operator workflow clarity: translated section labels should help operators scan by domain while row search still matches only visible template labels, matching the old shared selector behavior.
+- Context visibility: view-model and browser contracts should prove known categories render through `menu.monitor.*`, unknown categories keep uppercase fallback, and live `/setting/define` shows localized section context without changing selection or mutation flows.
+
+## 2026-05-13 YML Search Empty-State Plan
+
+- Hierarchy: old Angular `app-monitor-select-list` renders zero menu rows when a search has no matching child labels; it does not tell the operator to create a new monitor-template type while templates already exist.
+- Density: keep a compact empty state in the same list slot, but use search-miss copy instead of the global no-template CTA. Do not add a second panel, explanation card, or extra search controls.
+- Anti-AI-slop: do not reuse "please create a template" copy for a filtered catalog. A search miss is not a data-empty state and should not push operators toward template creation.
+- Operator workflow clarity: when the list has templates and the filter is too narrow, tell the operator to adjust the search while keeping the selected editor, route, and mutation controls unchanged.
+- Context visibility: component and browser contracts should prove search misses show the search-empty copy, do not show the create-template empty copy, and selecting/editing context remains intact.
+
+## 2026-05-14 Monitor History Chart Surface Plan
+
+- Hierarchy: old Angular history renders one chart card per numeric metric; Next should keep that direct chart-card scan, but the card shell, title/action density, and footer evidence should be owned by `@hertzbeat/ui` instead of each monitor page hand-writing Tailwind chrome.
+- Density: preserve the 460px chart affordance and native ECharts dataZoom controls for history. Do not replace the real chart with a decorative mini SVG or add nested panels around it.
+- Anti-AI-slop: do not mark a page-local `section` as "shared-timeseries" while the visual shell is still bespoke business-page markup. Shared visual ownership must be visible through a reusable UI primitive contract.
+- Operator workflow clarity: metric selection, local zoom observation, explicit "apply as query time", loading/error/empty states, and sample-count evidence must stay in the same chart slot.
+- Context visibility: component contracts should prove monitor history chart cards render through a `@hertzbeat/ui` chart surface while preserving Angular chart-card markers, ECharts dataZoom handoff, selected state, and localized sample counts.
+
+## 2026-05-14 Monitor Realtime Card Surface Plan
+
+- Hierarchy: old Angular realtime detail renders the basic monitor table followed by metric table cards in the same direct two-column scan. Next should preserve that order and density while moving the repeated card surface/title/action structure into `@hertzbeat/ui`.
+- Density: keep 400px realtime card affordance, compact table rows, icon-only edit action, and neutral selected state. Do not add nested panels, summary strips, source badges, or large card chrome.
+- Anti-AI-slop: do not let every monitor detail sub-card own another hand-written `section/header/body` Tailwind shell. Shared surface ownership should be visible through one reusable workbench-surface primitive.
+- Operator workflow clarity: clicking a metric title/table still selects the metric, loading/error states remain inline, and the basic card edit action remains in the same header slot.
+- Context visibility: component contracts should prove realtime basic and metric cards render through `@hertzbeat/ui` while preserving Angular markers, table-card layout, edit href, selected metric state, and localized empty/loading copy.
+
+## 2026-05-15 Monitor Realtime Metric Card Ownership Slice
+
+- Hierarchy: the next monitor-detail migration slice focuses on the repeated realtime metric table card, not the whole detail page. The real page may own metric data loading/selection, but card chrome, title affordance, table row density, inline loading/error/empty states, and `/ui-lab` demo parity belong to `@hertzbeat/ui`.
+- Density: preserve the old two-column 400px scan and compact metric table rows; avoid new badges, descriptions, or nested sections that would make realtime feel like an OTLP explorer.
+- Anti-AI-slop: the page must not keep local `monitor-workbench-metric-table` or metric-card table row markup after the primitive exists. Source contracts should prove the shell moved into the UI package, while render tests keep legacy selectors visible for browser smoke.
+- Operator workflow clarity: selecting a metric by title or table body remains a single-click realtime action. Loading, backend error, and empty-table copy stay inline within the same card slot.
+- Context visibility: `/ui-lab` must expose the primitive with a stable shared marker before `MonitorDetailSections` consumes it, and the real page must expose `data-monitor-detail-metric-card-owner="hertzbeat-ui-metric-card"`.
+
+## 2026-05-15 Monitor Realtime Basic Card Ownership Slice
+
+- Hierarchy: this slice completes the realtime two-column card ownership by moving the Basic summary card shell, heading, and edit affordance into `@hertzbeat/ui`; the monitor page still owns the summary facts and labels as business content.
+- Density: keep the same 400px first-card footprint, no extra summary strip, no source badge, and the existing icon-only edit action. Do not add copy that tries to explain what a monitor is.
+- Anti-AI-slop: the page must not keep a bespoke `MonitorBasicPlainSurface` function that assembles `HzWorkbenchSurface` props, action markup, and Angular card markers locally once the primitive exists.
+- Operator workflow clarity: the Basic card remains the first realtime card before metric cards, and the edit icon keeps the same `href`, `aria-label`, and compact hit target.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-basic-card="shared"`, and monitor detail must expose `data-monitor-basic-owner="hertzbeat-ui-basic-card"` while preserving the old Angular markers for browser smoke.
+
+## 2026-05-15 Monitor Manage Pagination Ownership Slice
+
+- Hierarchy: the monitor list footer owns pagination semantics, but the page-size selector, previous/next button rhythm, and compact footer chrome must live in `@hertzbeat/ui` so monitor pages do not hand-write another control strip.
+- Density: keep the simple Angular-style footer: page summary on the left, page size plus previous/next on the right. Do not add total metric cards, a second filter rail, icon decoration, or verbose paging explanations.
+- Anti-AI-slop: the real page must not keep the bespoke `flex flex-wrap items-center justify-between ...` footer after the shared primitive exists. The shared component must use `HzSelect` so the dropdown behavior and upward placement stay aligned with the lab.
+- Operator workflow clarity: changing page size resets page index to 0, previous/next keep the existing query-state handoff, and disabled states remain obvious without adding extra copy.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-pagination="shared"`, and `/monitors` must expose `data-monitor-manage-pagination-owner="hertzbeat-ui-pagination-bar"` while keeping the existing page-size selector and navigation actions functional.
+
+## 2026-05-15 Monitor Manage Delete Confirmation Ownership Slice
+
+- Hierarchy: destructive monitor deletion is an operator confirmation workflow, so the modal chrome, footer buttons, focusable close affordance, and danger tone should be owned by `@hertzbeat/ui`; the monitor page owns only delete copy, selected count, and mutation handler.
+- Density: keep one compact confirmation dialog. Do not add a right rail, JSON preview, card stack, or extra explanatory panel. The selected-count evidence can stay as a single bordered line inside the dialog body.
+- Anti-AI-slop: the monitor page must not keep importing `OverlayDialog` or hand-writing modal footers once the shared confirmation primitive exists. Confirmation UI should be visible in `/ui-lab` before real-page reuse.
+- Operator workflow clarity: the batch toolbar still opens confirmation only after a valid selection, Cancel closes without mutation, Confirm calls the existing delete handler, and disabled state remains tied to selected count/pending action.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-delete-confirm="shared"`, and `/monitors` must expose `data-monitor-delete-confirm-owner="hertzbeat-ui-confirm-dialog"` while preserving the old smoke selector `data-monitors-delete-confirm="cold-modal"`.
+
+## 2026-05-15 Monitor Editor Section Ownership Slice
+
+- Hierarchy: new/edit monitor forms can still own field values and validation, but the repeated section chrome for base params, scrape params, labels, annotations, Grafana, and payload facts belongs in `@hertzbeat/ui` so the editor page does not drift into local card styling.
+- Density: keep the editor sections as thin, square-edged bands with compact heading/copy rhythm. Do not add icon cards, nested panels, or marketing descriptions; the section body should simply host the real form controls.
+- Anti-AI-slop: `MonitorEditorSurface` must not import `SurfaceSection` or reassemble rounded workbench sections locally once the shared monitor-editor section primitive exists.
+- Operator workflow clarity: the section primitive preserves the old form order and does not change submit/detect/cancel behavior. This slice is visual ownership only.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-editor-section="shared"`, and monitor new/edit must expose `data-monitor-editor-section-owner="hertzbeat-ui-editor-section"` through every editor section.
+
+## 2026-05-15 Monitor Editor Key-Value Rows Ownership Slice
+
+- Hierarchy: labels and annotations remain monitor business data, but their repeated key/value row chrome, add/remove action rhythm, and compact input pairing belong in `@hertzbeat/ui` instead of the monitor editor page.
+- Density: keep two fields plus one remove action per row and one add action underneath. Do not add icons, row descriptions, nested cards, or helper copy that cannot apply to custom metadata.
+- Anti-AI-slop: `MonitorEditorSurface` must not keep a page-local `KeyValueEditor` function or import the legacy page `Button` to assemble metadata rows once the shared primitive exists.
+- Operator workflow clarity: empty-row fallback, remove-last-row behavior, and label/annotation validation stay unchanged while the visual row structure moves to the shared component.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-key-value-editor="shared"`, and monitor new/edit must expose `data-monitor-editor-key-value-owner="hertzbeat-ui-key-value-editor"` on label and annotation row groups.
+
+## 2026-05-15 Monitor Editor Field Controls Ownership Slice
+
+- Hierarchy: monitor editor still owns values, validation, and API payloads; the compact label/control pairing for text inputs, select menus, radio-backed params, and schedule/base fields belongs in `@hertzbeat/ui`.
+- Density: keep one terse label and one control per field. Do not add helper prose, icons, cards, or nested surfaces around normal monitor form controls.
+- Anti-AI-slop: `MonitorEditorSurface` must not import legacy `Input`/`Select` or maintain a page-local `fieldLabelClassName` once shared `HzField`, `HzInput`, and `HzSelect` exist.
+- Operator workflow clarity: app, scrape, name, collector, schedule, radio, text, number, boolean, and code-like param changes keep the same state update behavior while their field shell moves to the shared UI layer.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-field-controls="shared"`, and monitor new/edit must expose `data-monitor-editor-field-owner="hertzbeat-ui-field"` on shared field rows.
+
+## 2026-05-15 Monitor Editor Textarea Ownership Slice
+
+- Hierarchy: freeform monitor descriptions remain monitor business content; textarea chrome, focus treatment, density, and resize behavior belong in `@hertzbeat/ui` so side-panel fields do not drift from the shared editor rhythm.
+- Density: keep the description editor as one compact full-width field with no icon, explanatory helper paragraph, nested card, or oversized padding beyond the section copy already present.
+- Anti-AI-slop: `MonitorEditorSurface` must not import the legacy `Textarea` component or expose `data-cold-textarea-owner` for monitor description once `HzTextarea` exists.
+- Operator workflow clarity: the description value and `onChange` handler stay unchanged; this slice only changes the visual primitive and ownership marker.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-textarea="shared"`, and monitor new/edit must expose `data-monitor-editor-textarea-owner="hertzbeat-ui-textarea"` on the description control.
+
+## 2026-05-15 Monitor Editor Checkbox Ownership Slice
+
+- Hierarchy: boolean monitor params and Grafana enablement remain monitor business choices, but checkbox chrome, focus state, label rhythm, and checked-state visuals belong in `@hertzbeat/ui`.
+- Density: keep each checkbox as one compact row with label text only. Do not add icons, helper prose, nested cards, or explanatory copy around boolean toggles.
+- Anti-AI-slop: `MonitorEditorSurface` must not import the legacy `Checkbox` component or expose `data-cold-checkbox-owner` once the shared `HzCheckbox` exists.
+- Operator workflow clarity: boolean param and Grafana `checked`/`onChange` behavior stays unchanged; this slice only moves the visual primitive and ownership markers.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-checkbox="shared"`, and monitor new/edit must expose `data-monitor-editor-checkbox-owner="hertzbeat-ui-checkbox"` on shared checkbox controls.
+
+## 2026-05-15 Monitor Editor Number Stepper Ownership Slice
+
+- Hierarchy: interval schedules and numeric monitor params remain monitor business values, but stepper chrome, increment/decrement affordances, clamping, and input focus treatment belong in `@hertzbeat/ui`.
+- Density: keep the number stepper as one compact square-edged control with icon-only step buttons and no helper prose, nested cards, or page-local border styling.
+- Anti-AI-slop: `MonitorEditorSurface` must not import the legacy `NumberStepper` component or expose `data-cold-number-stepper-owner` once the shared `HzNumberStepper` exists.
+- Operator workflow clarity: interval and numeric param `value`/`onValueChange`, min/max/step, and text-input behavior stay unchanged; this slice only moves the visual primitive and ownership markers.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-number-stepper="shared"`, and monitor new/edit must expose `data-monitor-editor-number-stepper-owner="hertzbeat-ui-number-stepper"` on shared number controls.
+
+## 2026-05-15 Monitor Editor Code Editor Frame Ownership Slice
+
+- Hierarchy: JSON/YAML-like monitor param values and Grafana template text remain monitor business payloads, but the editor frame chrome, density, and ownership markers belong in `@hertzbeat/ui`. The CodeMirror runtime can remain an inner child until a full editor primitive is promoted.
+- Density: keep one compact framed editor with no helper prose, nested card, duplicate description, or page-local border wrapper around custom monitor values.
+- Anti-AI-slop: `MonitorEditorSurface` must not render code editor instances without an explicit shared owner marker once the frame primitive exists.
+- Operator workflow clarity: param editor and Grafana template `value`, `onChange`, `language`, `name`, and `minHeight` behavior stay unchanged; this slice only moves the visual shell and ownership marker.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-code-editor-frame="shared"`, and monitor new/edit must expose `data-monitor-editor-code-editor-owner="hertzbeat-ui-code-editor-frame"` around code editor instances.
+
+## 2026-05-15 Monitor Editor CodeMirror Runtime Ownership Slice
+
+- Hierarchy: structured monitor param editing and the Prometheus Grafana template editor should now consume a full `HzCodeEditor` from `@hertzbeat/ui`; the monitor editor page keeps only business values and API draft updates.
+- Density: keep the same compact CodeMirror editor, line wrapping, hidden form value, and frame body without introducing a second wrapper, extra title, or helper text.
+- Anti-AI-slop: `MonitorEditorSurface` must not import `../ui/cold-code-editor` or render `<ColdCodeEditor>` once the shared runtime primitive exists.
+- Operator workflow clarity: `value`, `onChange`, `language`, `name`, `minHeight`, placeholder, readonly, and accessibility label behavior stay unchanged while the runtime ownership moves.
+- Context visibility: `/ui-lab` must expose `data-hz-ui-lab-monitor-code-editor="shared"`, and monitor new/edit must expose `data-monitor-editor-code-editor-owner="hertzbeat-ui-code-editor"` with `data-hz-code-editor-runtime="codemirror"`.
+
+## 2026-05-14 Monitor Manage List Table Plan
+
+- Hierarchy: old Angular monitor center renders a dense monitor result list with page-level selection, per-row checkbox, name/detail navigation, status, instance/scrape/app metadata, labels, and updated time. Next should expose that as a reusable `@hertzbeat/ui` data table, not as a page-local evidence/card list.
+- Density: keep the compact filter toolbar, batch action strip, and simple pagination footer. Do not add card grids, nested detail cards, oversized row chrome, or marketing summaries.
+- Anti-AI-slop: do not call the monitor list "table parity" while routing rows through a generic `SelectableEvidenceList` abstraction that hides monitor-specific columns in page-local JSX blobs.
+- Operator workflow clarity: clicking a row still opens monitor detail, row checkbox selection still drives batch actions, and status/app/scrape/update evidence remains visible in the same scan.
+- Context visibility: page contracts should prove `/monitors` renders `HzDataTable` with monitor-row owner markers, keeps row selection and detail handoff selectors, and removes the page-local selectable evidence list for the main result list.
diff --git a/web-next/docs/parity/monitor-editor-collector-brief.md b/web-next/docs/parity/monitor-editor-collector-brief.md
new file mode 100644
index 0000000000..7c811c90cc
--- /dev/null
+++ b/web-next/docs/parity/monitor-editor-collector-brief.md
@@ -0,0 +1,17 @@
+# Monitor Editor Collector Brief
+
+Local design judgment for the monitor edit collector regression.
+
+- Hierarchy: an empty collector value and the built-in `main-default-collector` both mean system default dispatch; selectable pinned collectors should only show real user/edge collector names.
+- Density: keep the existing compact form and payload preview; do not add explanatory banners for a parity label fix.
+- Anti-AI-slop: do not render default dispatch as `None`, and do not fake save success while backend collection probe work is still blocking.
+- Operator workflow clarity: `OK` saves configuration and returns to the monitor list; `Detect` remains the explicit synchronous probe action.
+- Context visibility: the collector select, payload preview, save payload, and backend update path are covered by contracts so the operator sees the same default-dispatch meaning as Angular.
+
+## 2026-05-13 Monitor Type Picker Boundary
+
+- Hierarchy: the default monitor center `New monitor` action must open the old Angular-style grouped monitor type picker instead of inferring a type from the currently selected row; direct `/monitors/new?app=...` is reserved for an explicit app filter or picker selection.
+- Density: use a compact searchable modal with grouped type rows, not a permanent second-level rail that makes the monitor center or YML definition workspace feel longer than the task.
+- Anti-AI-slop: do not silently default to `mysql`, `website`, or the first row when the operator asked to create a monitor from the general center.
+- Operator workflow clarity: selection is a deliberate first step; after the operator chooses a type, navigate to the normal monitor form with that `app`.
+- Context visibility: the picker keeps category labels visible, excludes `__system__`, and preserves return/filter context only after an explicit type choice.
diff --git a/web-next/docs/parity/release-readiness-brief.md b/web-next/docs/parity/release-readiness-brief.md
new file mode 100644
index 0000000000..050520ce7a
--- /dev/null
+++ b/web-next/docs/parity/release-readiness-brief.md
@@ -0,0 +1,11 @@
+# HertzBeat 2.0 Release Readiness Brief
+
+Local parity brief for M8 deployment/release pipeline work. Keep this local unless the user asks to commit or publish docs.
+
+## 2026-05-13 Release Gateway And Checklist Boundary
+
+- Hierarchy: release ingress must preserve the same operator entry hierarchy available in dev: overview, monitors, entities, OTLP ingestion, logs, traces, alerts, topology, settings, and static Next assets route to `web-next`; API and legacy fallbacks route to the Spring backend.
+- Density: gateway rules should stay compact and path-prefix based. Avoid per-page sprawl when a route family can be represented by one prefix.
+- Anti-AI-slop: do not invent Helm, Kustomize, SaaS packaging, hosted control-plane assumptions, or vanity release docs. Validate only concrete local artifacts: Dockerfile, compose, nginx gateway, CI workflow, package scripts, and executable release gates.
+- Operator clarity: a private deployment should open the same signed-in operator routes after promotion or rollback; compose config checks must validate release and rollback versions without pulling images or starting containers.
+- Context visibility: release version, rollback version, backend image, web-next image, gateway route ownership, and budget/checklist gate names must remain visible in machine-readable scripts and tests.
diff --git a/web-next/docs/parity/session-security-brief.md b/web-next/docs/parity/session-security-brief.md
new file mode 100644
index 0000000000..1d5a6b1e15
--- /dev/null
+++ b/web-next/docs/parity/session-security-brief.md
@@ -0,0 +1,1067 @@
+# Session Security Boundary Brief
+
+Local-only parity note for milestone 4.
+
+## 2026-05-10 Session BFF Route Ownership Fix
+
+- Hierarchy: browser-facing `/api/*` traffic must enter the Next BFF route layer first, where HttpOnly UI cookies are converted into backend `Authorization`; `next.config.mjs` must not rewrite `/api/:path*` directly to the backend.
+- Density: do not add login prompts, auth banners, duplicate session cards, or token copy into the workbench; keep the authenticated workflow silent when authorized calls succeed.
+- Anti-AI-slop: never expose access or refresh tokens back to browser localStorage or visible UI to patch missing auth context.
+- Operator workflow clarity: after successful login, protected pages should load normal data through same-origin BFF cookies, and only genuinely unauthenticated sessions should redirect to `/passport/login`.
+- Context visibility: session cookies, refresh cookie fallback, BFF proxy handoff, backend Authorization header, and 401/403 recovery stay covered by session security contracts and live smoke.
+
+## 2026-05-10 Old Monitor Delete Row Cleanup Boundary
+
+- Hierarchy: old monitor deletion should keep service-discovery expansion, cleanup sequencing, scheduler cancellation, and event publication in `MonitorServiceImpl`, while accepted monitor row lookup plus delete live behind an old-monitor catalog write-model boundary.
+- Density: do not add UI copy, cleanup summaries, duplicate delete controls, route aliases, helper banners, synthetic rows, or response fields while moving raw row deletion ownership.
+- Anti-AI-slop: never infer deleted monitor rows from entity, telemetry, alert evidence, topology, labels, or selection display state; delete only persisted rows accepted from the expanded old monitor id set.
+- Operator workflow clarity: deletion must keep existing no-op semantics, cleanup order, scheduler cancellation, and delete-event emission for the same persisted monitor rows.
+- Context visibility: submitted ids, expanded monitor ids, accepted monitor rows, row-delete boundary, cleanup ids, scheduler cancellation, and events stay visible through source and behavior contracts.
+
+## 2026-05-10 Old Monitor Row Save Boundary
+
+- Hierarchy: old monitor create/edit should keep validation, scheduling, dashboard, collector-bind, and parameter orchestration in `MonitorServiceImpl`, while the final monitor row save lives behind an old-monitor catalog write-model boundary.
+- Density: do not add UI copy, save summaries, duplicate forms, route aliases, helper banners, synthetic monitor rows, or new response fields while moving raw monitor-row persistence.
+- Anti-AI-slop: never synthesize monitor rows from entity, telemetry, topology, alert evidence, or rendered form state; only persist the submitted and scheduled old monitor row.
+- Operator workflow clarity: create/edit must keep monitor-id assignment timing, job-id assignment, dashboard side effects, rollback cancellation, and existing error behavior.
+- Context visibility: submitted monitor row, generated monitor id, scheduler job id, dashboard action, parameter save, and final row-save boundary stay visible through source and behavior contracts.
+
+## 2026-05-10 Old Monitor Template Job-Id Write Boundary
+
+- Hierarchy: old monitor template refresh keeps app-definition assembly and scheduler update orchestration in `MonitorServiceImpl`, while the persisted monitor job-id save belongs behind the old-monitor write-model boundary.
+- Density: do not add UI copy, refresh summaries, duplicate template refresh controls, route aliases, synthetic monitor rows, or new response fields while moving job-id persistence ownership.
+- Anti-AI-slop: never invent job ids from entity, telemetry, topology, alert evidence, or displayed scheduler state; only persist the scheduler-returned job id on an actual monitor row.
+- Operator workflow clarity: template refresh should keep existing active-monitor filtering, collector lookup, param defaults, per-monitor error isolation, and continue-on-error semantics.
+- Context visibility: app name, active monitor rows, prior job id, scheduler-returned job id, collector evidence, and final persistence boundary stay visible through source and behavior contracts.
+
+## 2026-05-10 Old Monitor Manage Status Write Boundary
+
+- Hierarchy: old monitor pause/resume selection and persisted status/job-id saves should sit behind a write-model boundary, while `MonitorServiceImpl` keeps scheduling side effects and legacy service API orchestration.
+- Density: do not add alternate pause/resume panels, duplicate counters, synthetic monitor rows, generated health text, or new route wrappers while moving persistence ownership.
+- Anti-AI-slop: pause/resume can only act on persisted monitor rows expanded from explicit service-discovery binds; never infer target monitors from telemetry labels, entity hints, alert evidence, topology context, or generated examples.
+- Operator workflow clarity: existing pause/resume callers keep the same accepted ids, service-discovery expansion, cancellation, job creation, detect attempt, event publication, and no-op semantics.
+- Context visibility: submitted ids, expanded monitor ids, selected persisted rows, status transitions, scheduler calls, and final save boundary stay visible through source and behavior contracts.
+
+## 2026-05-10 Old Monitor Catalog Query Boundary
+
+- Hierarchy: old monitor catalog lookups and app status counts should sit behind a query boundary, while `MonitorServiceImpl` keeps route-compatible orchestration and job refresh semantics.
+- Density: do not add UI copy, count explanations, extra filters, helper banners, route aliases, or response fields while moving raw monitor catalog reads.
+- Anti-AI-slop: never infer monitor rows or app counts from entity, telemetry, alert, collector, or rendered navigation state; use only persisted monitor rows and counts.
+- Operator workflow clarity: app monitor lists, app count summaries, single monitor lookup, and template refresh should continue using the same submitted app/id evidence without changing legacy APIs.
+- Context visibility: monitor id, app name, raw app status rows, active monitor rows, collector evidence, and scheduler refresh remain visible through service/source contracts.
+
+## 2026-05-10 Old Monitor Param Query Boundary
+
+- Hierarchy: old monitor detail, resume, template refresh, and copy flows keep their orchestration roles, while persisted submitted-parameter reads move behind a dedicated old-monitor query boundary.
+- Density: do not add UI copy, parameter previews, helper banners, route aliases, response fields, or duplicate read affordances while moving raw parameter queries.
+- Anti-AI-slop: never synthesize parameter values from entity, telemetry, collector, labels, or dashboard context; only persisted monitor parameters may feed legacy jobs and DTOs.
+- Operator workflow clarity: detail, resume, template refresh, and copy should continue using the same monitor ids and parameter evidence without changing legacy route behavior or job config semantics.
+- Context visibility: monitor id, persisted parameter rows, runtime configmap construction, copied params, scheduler update, and DTO evidence remain visible through service/source contracts.
+
+## 2026-05-10 Old Monitor Param Delete Cleanup Boundary
+
+- Hierarchy: old monitor deletion keeps accepted monitor-row selection, scheduler cancellation, and event publishing, while monitor-owned parameter cleanup lives behind the old-monitor parameter write-model boundary.
+- Density: do not add UI copy, cleanup summaries, helper banners, duplicate delete controls, or response fields while moving raw parameter deletes.
+- Anti-AI-slop: never infer parameter cleanup ids from entity, telemetry, collector, label, or selection display state; delete params only for monitor rows accepted for deletion.
+- Operator workflow clarity: parent service-discovery deletes should continue cleaning params for the final expanded monitor set without changing legacy delete behavior or response shape.
+- Context visibility: submitted ids, expanded monitor ids, accepted monitor rows, parameter cleanup ids, scheduler cancellation, and delete events remain visible through service/source contracts.
+
+## 2026-05-10 Old Monitor Param Save Boundary
+
+- Hierarchy: old monitor create/edit keeps job assembly, monitor persistence, and rollback sequencing, while submitted parameter row persistence lives behind a dedicated old-monitor write-model boundary.
+- Density: do not add UI copy, parameter summaries, helper banners, duplicate form hints, or response fields while moving raw parameter saves.
+- Anti-AI-slop: never synthesize parameter rows from entity, telemetry, collector, labels, or rendered forms; only submitted monitor params may be persisted.
+- Operator workflow clarity: create/edit should keep existing parameter normalization and monitor-id assignment timing, then delegate the final submitted-param save without changing legacy errors or rollback behavior.
+- Context visibility: submitted params, monitor id assignment, scheduler job config, monitor save, parameter save, and rollback cancellation remain visible through service/source contracts without changing route or response shape.
+
+## 2026-05-10 Old Monitor Label Write Boundary
+
+- Hierarchy: old monitor create/edit keeps monitor assembly, validation, scheduling, and persistence sequencing, while shared label row creation lives behind a dedicated old-monitor write-model boundary.
+- Density: do not add UI copy, label summaries, duplicate label settings links, helper banners, or response fields while moving raw label persistence.
+- Anti-AI-slop: never synthesize labels from entity, alert, telemetry, collector, or display context; only submitted monitor labels may create persisted label rows.
+- Operator workflow clarity: create/edit should continue normalizing absent labels to an empty map, then persist only genuinely new submitted labels before monitor save.
+- Context visibility: submitted labels, normalized empty labels, new-label detection, and label persistence remain visible through service/source contracts without changing route or response shape.
+
+## 2026-05-10 Old Monitor Service-Discovery Null Child Boundary
+
+- Hierarchy: old monitor service-discovery expansion should keep submitted parent ids and valid child monitor ids, while suppressing incomplete bind rows before monitor lookup.
+- Density: do not add UI warnings, response fields, cleanup summaries, helper banners, duplicate routes, or confirmation affordances for dirty bind rows.
+- Anti-AI-slop: never invent replacement child ids from entity, telemetry, alert, host, or labels when a persisted monitor-bind row has no child monitor id.
+- Operator workflow clarity: delete, pause, and resume should remain deterministic even when legacy service-discovery bind evidence is partially missing.
+- Context visibility: submitted ids, valid expanded child ids, ignored null child ids, monitor lookup ids, and downstream side effects remain visible through service contracts without changing route or response shape.
+
+## 2026-05-10 Old Monitor Service-Discovery Expansion Helper Boundary
+
+- Hierarchy: old monitor delete, pause, and resume should share one service-discovery child expansion helper before acting on monitor rows.
+- Density: do not add UI copy, response fields, helper banners, duplicate routes, or confirmation affordances while reducing duplicated service code.
+- Anti-AI-slop: never infer child monitor ids from entity, telemetry, alert, host, or selection context; only expand from persisted monitor-bind evidence.
+- Operator workflow clarity: automation clients get consistent parent-plus-child behavior across delete, pause, and resume, while submitted ids remain request evidence.
+- Context visibility: submitted ids, expanded ids, monitor row lookup, cleanup, status changes, and scheduler effects remain visible through existing service/source contracts without changing route or response shape.
+
+## 2026-05-10 Old Monitor Manage Expanded Id Immutability Boundary
+
+- Hierarchy: old monitor pause/resume should expand service-discovery child monitors into a local accepted action set, leaving submitted request ids as input evidence.
+- Density: do not add UI copy, response fields, helper banners, duplicate manage routes, or new confirmation affordances while tightening the service boundary.
+- Anti-AI-slop: never infer child monitor ids from entity, telemetry, alert, host, or selection context; only use monitor-bind expansion returned by the monitor service boundary.
+- Operator workflow clarity: automation clients may pass immutable id sets, and pause/resume must still apply to parent plus child monitors without mutating caller-owned request evidence.
+- Context visibility: submitted ids, expanded monitor ids, status changes, scheduler side effects, and saved rows remain visible through service contracts without changing route or response shape.
+
+## 2026-05-10 Old Monitor Deletion Expanded Param Cleanup Boundary
+
+- Hierarchy: old monitor deletion should expand service-discovery child monitors once, then use that accepted deletion set for monitor rows and all monitor-owned cleanup rows.
+- Density: do not add UI copy, cleanup summaries, response fields, helper banners, duplicate delete routes, or confirmation affordances while tightening cleanup.
+- Anti-AI-slop: never infer child monitor ids from entity, telemetry, alerts, host labels, or displayed selections; only use the monitor-bind expansion returned by the monitor service boundary.
+- Operator workflow clarity: deleting a parent monitor should delete params for every actual monitor row removed, so re-created monitors do not inherit stale parameter evidence.
+- Context visibility: submitted ids, expanded monitor ids, param cleanup ids, and downstream delete side effects remain visible through the service contract without changing route or response shape.
+
+## 2026-05-10 Old Monitor Deletion Entity-Bind Cleanup Boundary
+
+- Hierarchy: old monitor deletion owns monitor, param, alert-bind, collector-bind, favorite, scheduler, and delete-event cleanup, while entity monitor-bind rows stay behind the entity monitor-bind write boundary.
+- Density: do not add UI copy, cleanup summaries, extra response fields, duplicate delete actions, or confirmation affordances while moving cleanup ownership.
+- Anti-AI-slop: never infer entity bind cleanup from host, entity detail, telemetry, alert, or selection context; delete only the monitor ids confirmed by the monitor service.
+- Operator workflow clarity: deleting old monitors should remove entity bind evidence through the entity boundary so entity catalog read models never keep stale monitor evidence after a real deletion.
+- Context visibility: monitor ids, downstream entity-bind cleanup call, and raw DAO ownership remain visible through source and service contracts without changing monitor delete response shape.
+
+## 2026-05-10 Old Monitor Manage Route Authorization Boundary
+
+- Hierarchy: exact `/api/monitors/manage` method rules should sit beside the old monitor compatibility routes, before broad `/api/monitors/**` wildcards.
+- Density: do not add UI affordances, helper copy, duplicate navigation entries, or new endpoint surfaces; this is a shipped config contract only.
+- Anti-AI-slop: never rely on broad wildcard permissions to imply mutation safety; exact method rules should name the supported legacy GET, safe POST/PUT aliases, and DELETE pause path.
+- Operator workflow clarity: legacy GET enable keeps its old readable method behavior, while POST/PUT enable and DELETE pause remain visible as mutation methods for automation clients.
+- Context visibility: route path, HTTP method, role set, and ordering relative to broad wildcard rules remain visible in shipped Sureness config tests.
+
+## 2026-05-10 Old Monitor Manage Mutation Compatibility Boundary
+
+- Hierarchy: legacy `GET /api/monitors/manage` remains for existing clients, while explicit POST/PUT mutation aliases should map to the same enable boundary instead of introducing new workflow branches.
+- Density: do not add response fields, helper banners, duplicate manage rows, or UI copy; the compatibility change stays at the route contract level.
+- Anti-AI-slop: never infer monitor ids from entity, telemetry, alert, host, or selection state; the route must act only on submitted `ids`, legacy `id`, or body ids.
+- Operator workflow clarity: enabling monitors has a state-changing method available for safer automation clients, while old GET and DELETE pause paths keep their existing behavior.
+- Context visibility: submitted ids, HTTP method, action route, and downstream monitor-service call remain visible in controller tests without changing payload shape.
+
+## 2026-05-10 Entity Governance State Request-Workspace Boundary
+
+- Hierarchy: governance-state service keeps DTO validation, content shaping, and conversion, while default request workspace lookup for raw reads, upsert-row preparation, and deletes lives in query/write boundaries.
+- Density: do not add governance workspace chips, helper lists, duplicate template rows, resume banners, or explanatory copy while moving scope ownership.
+- Anti-AI-slop: never mix cross-workspace templates, presets, resumes, or activity rows; do not synthesize governance records to make workflows appear complete.
+- Operator workflow clarity: default definition/discovery governance calls must bind current request workspace before storage lookup or mutation, while explicit workspace overloads remain available for compatibility contracts.
+- Context visibility: state scope, kind, key, name, status, content, creator, update time, and workspace scope stay visible through existing governance DTO/service contracts.
+
+## 2026-05-10 Entity Status Refresh Active-Alert Query Boundary
+
+- Hierarchy: mutation-time status refresh keeps bound-monitor collection and runtime-health persistence sequencing, while current request workspace lookup for active alert evidence lives in the alert query boundary.
+- Density: do not add status banners, alert counters, duplicate monitor rows, workspace chips, or new helper text while moving active-alert scope ownership.
+- Anti-AI-slop: never derive entity health from cross-workspace alerts, guessed alert states, synthetic firing rows, or fabricated monitor matches.
+- Operator workflow clarity: default status refresh must fetch active alerts in the current request workspace before runtime health is persisted, while explicit workspace overloads remain available for focused compatibility contracts.
+- Context visibility: entity id, bound monitors, active firing alerts, alert status/severity labels, workspace scope, and persisted runtime health stay visible through existing service contracts.
+
+## 2026-05-10 Entity Alert Request-Workspace Query Boundary
+
+- Hierarchy: alert evidence read models keep status, severity, sort, and page shaping, while current request workspace lookup, raw alert predicates, monitor matching, and workspace filtering live in the alert query boundary.
+- Density: do not add alert-origin columns, workspace chips, helper banners, duplicate alert rows, or new empty-state copy while moving request scope ownership.
+- Anti-AI-slop: never fill scoped alert evidence with cross-workspace rows, guessed severities, synthetic firing alerts, or placeholder acknowledged/resolved rows.
+- Operator workflow clarity: default alert evidence calls must bind the current request workspace before `SingleAlert` lookup, while explicit workspace overloads stay available for status refresh and compatibility paths.
+- Context visibility: monitor names/instances, alert status, severity, update time, content, labels, pagination, and workspace scope remain visible through existing alert DTO/page contracts.
+
+## 2026-05-10 Entity Activity Request-Workspace Query Boundary
+
+- Hierarchy: activity timeline read models keep page-size shaping and DTO mapping, while current request workspace lookup, entity accessibility, raw row lookup, and workspace filtering live in the query boundary.
+- Density: do not add timeline chips, helper rows, scope banners, duplicate activity groups, or new explanatory copy while moving request scope ownership.
+- Anti-AI-slop: never backfill activity rows from cross-workspace entities, guessed workspaces, or synthetic import/update events to make timelines appear complete.
+- Operator workflow clarity: default timeline calls must bind the current request workspace before storage lookup, and explicit overloads keep testable workspace scopes for imports, discovery, and compatibility paths.
+- Context visibility: entity id, activity type, format, status, summary, detail, creator, timestamp, and workspace scope remain visible through existing activity DTO/query contracts.
+
+## 2026-05-10 Entity Activity Record Write Boundary
+
+- Hierarchy: activity lifecycle classification and operator-facing summary/detail wording stay in the activity write model, while raw row persistence, workspace normalization, and stored-entity workspace fallback move behind a dedicated record writer.
+- Density: do not add activity helper rows, workspace chips, extra summaries, or duplicate lifecycle events while moving persistence; activity timelines remain compact persisted evidence.
+- Anti-AI-slop: never fabricate activity workspace ids, guessed stored entities, synthetic success/failure rows, or monitor-bind evidence to make catalog history look complete.
+- Operator workflow clarity: request workspace, entity workspace, and stored fallback are resolved at the final write boundary before the activity row is flushed, so create/import/edit evidence remains scoped and deterministic.
+- Context visibility: entity id, activity type, format, status, summary, detail, workspace scope, and monitor-bind count evidence remain visible through the existing activity contracts.
+
+## 2026-05-10 Old Monitor Batch Body Compatibility Boundary
+
+- Hierarchy: the old batch monitor routes keep action ownership in the monitor controller/service boundary, while entity workflows continue to consume monitor rows through the existing anti-corruption query boundary.
+- Density: do not add response fields, wrapper rows, confirmation copy, helper banners, or duplicate action endpoints while preserving legacy body ids.
+- Anti-AI-slop: batch delete/cancel/enable must act only on submitted ids from query or body; never infer monitor ids from host, entity, alert, or telemetry context.
+- Operator workflow clarity: existing clients that submit JSON id arrays to `/api/monitors`, `/api/monitors/manage`, and `DELETE /api/monitors/manage` keep their side effects, while query `ids` remain the preferred value when both are present.
+- Context visibility: submitted ids, action route, HTTP method, and resulting service call remain visible through controller contracts without changing the response shape.
+
+## 2026-05-10 Entity Identity Query Ownership Boundary
+
+- Hierarchy: entity identity evidence should keep detail/list/candidate consumers focused on row presentation, counts, and scoring order, while raw identity row lookup and match/count DAO access live behind a dedicated query boundary.
+- Density: do not add identity helper rows, workspace chips, duplicate identity groups, or explanatory copy while moving DAO ownership; identity evidence remains compact and operator-scannable.
+- Anti-AI-slop: never invent identity keys, normalized values, counts, or candidate matches to make an entity appear better correlated.
+- Operator workflow clarity: persisted identity rows and counts must be read before detail summaries, list readiness, and monitor-binding candidates are assembled, with no synthetic fallback when storage has no evidence.
+- Context visibility: entity id, identity key/value, normalized value, priority, primary flag, identity counts, and matched identity keys remain visible through existing DTO/service contracts.
+
+## 2026-05-10 Entity Activity Query Ownership Boundary
+
+- Hierarchy: entity activity timelines should keep DTO mapping and limit/page shaping in the read model, while raw definition/lifecycle activity row lookup and workspace/entity eligibility live behind a dedicated query boundary.
+- Density: do not add timeline helper rows, workspace chips, duplicate activity groups, or explanatory copy while moving DAO ownership; activity lists remain compact evidence.
+- Anti-AI-slop: never backfill timelines with cross-workspace rows, guessed import/update events, or synthetic lifecycle activities.
+- Operator workflow clarity: entity access and request workspace filtering must happen before timeline rows and latest definition activities are mapped for detail/list summaries.
+- Context visibility: entity id, activity type, format, status, summary, detail, creator, timestamp, and workspace scope remain visible through existing activity DTO contracts.
+
+## 2026-05-10 Entity Governance State Query Ownership Boundary
+
+- Hierarchy: definition/discovery governance workflows should keep DTO validation and content shaping in the state service, while raw persisted state lookup, upsert, delete, and workspace eligibility live behind a dedicated query boundary.
+- Density: do not add governance helper rows, workspace chips, resume banners, or explanatory copy while moving DAO ownership; templates, activities, resumes, and presets remain compact records.
+- Anti-AI-slop: never fill governance lists with cross-workspace states, guessed templates, synthetic resume tokens, or placeholder discovery activities.
+- Operator workflow clarity: request workspace filtering must happen before governance records are mapped into definition/discovery workflow DTOs, and writes must resolve the persisted workspace consistently.
+- Context visibility: state scope/kind/key/name/status/content, creator, update time, and workspace scope remain visible through existing governance DTO contracts.
+
+## 2026-05-10 Entity Alert Evidence Query Ownership Boundary
+
+- Hierarchy: entity alert evidence should keep status/severity/page shaping in the read model, while raw `SingleAlert` lookup, monitor predicates, and workspace eligibility live behind a dedicated query boundary.
+- Density: do not add alert-origin columns, workspace chips, helper banners, or new empty-state copy while moving DAO ownership; alert rows remain compact evidence.
+- Anti-AI-slop: never fill scoped alert evidence with cross-workspace rows, guessed severities, synthetic firing alerts, or placeholder acknowledged/resolved rows.
+- Operator workflow clarity: monitor identity predicates and request workspace filtering must happen before active-alert lists and paged alert evidence are sorted, counted, and returned.
+- Context visibility: monitor ids/names/instances, alert status, severity, update time, content, labels, page request, and workspace scope remain visible through existing alert DTO/page contracts.
+
+## 2026-05-10 Entity Noise-Control Rule Query Ownership Boundary
+
+- Hierarchy: entity detail noise-control evidence should keep candidate label normalization and compact summary assembly in the read model, while raw silence/inhibit rule lookup and workspace eligibility live behind a dedicated rule query boundary.
+- Density: do not add rule-origin columns, workspace chips, warning rows, or helper prose while moving DAO ownership; active silence and matching inhibit previews stay compact.
+- Anti-AI-slop: never mark possible suppression from cross-workspace rules, guessed labels, disabled rules, or unlabeled non-default rules to make an entity look explained.
+- Operator workflow clarity: request workspace filtering must happen before silence/inhibit rule matching, counts, previews, and possible-suppression flags leave the detail read model.
+- Context visibility: accepted rule id/name/type, matched labels, update time, active-alert presence, candidate entity/monitor labels, and workspace scope remain visible through existing detail DTO contracts.
+
+## 2026-05-10 Entity Relation Target Lookup Boundary
+
+- Hierarchy: relation replacement keeps submitted relation parsing, target-reference resolution, dedupe, and persistence in order while raw entity lookup stays behind the workspace-access boundary.
+- Density: do not add relation helper rows, warning copy, fallback graph edges, or duplicate target labels while moving target lookup.
+- Anti-AI-slop: never resolve relation targets from another workspace, guessed ids, or invented refs to make topology or definition previews appear connected.
+- Operator workflow clarity: direct ids and typed refs must resolve only after workspace access accepts the target row, so import/edit flows preserve real relation evidence.
+- Context visibility: source entity id, target entity id, target ref, relation type/source/status/score, and workspace scope remain visible through existing relation DTO contracts.
+
+## 2026-05-10 Catalog Suggestions Workspace Entity Lookup Boundary
+
+- Hierarchy: catalog suggestions keep workspace-scoped entity acceptance before owner, namespace, system, lifecycle, relation-ref, language, and link-provider aggregation.
+- Density: do not add suggestion helper text, workspace chips, duplicated suggestion buckets, or synthetic fallback values while moving entity lookup.
+- Anti-AI-slop: never use cross-workspace catalog rows or guessed owner/system/reference values to make profile suggestions look richer.
+- Operator workflow clarity: the suggestion read-model should only aggregate from entities accepted by the workspace-access boundary, so import/edit forms keep real reuse hints.
+- Context visibility: accepted entity ids, refs, owners, additional owners, namespaces, environments, systems, lifecycles, tiers, inherit refs, languages, and link providers remain visible through the existing suggestion DTO.
+
+## 2026-05-10 Entity Detail Entity Access Boundary
+
+- Hierarchy: entity detail keeps entity acceptance first, then identity rows, monitor binds, and relations; raw entity lookup belongs to the workspace-access boundary.
+- Density: do not add access banners, workspace chips, empty cards, or explanatory helper text while moving the lookup; missing or inaccessible detail remains a null response.
+- Anti-AI-slop: never borrow entity profile, identities, binds, relations, health, or navigation context from another workspace to make a detail page appear complete.
+- Operator workflow clarity: detail DTO assembly must reject inaccessible entities before child evidence is queried so cross-workspace detail, definition, and evidence flows stay closed.
+- Context visibility: accepted entity profile, identities, monitor binds, relations, route id, and existing cross-signal handoff context remain visible through the unchanged detail DTO shape.
+
+## 2026-05-10 Entity Activity Timeline Workspace Lookup Boundary
+
+- Hierarchy: activity timelines keep access check, persisted activity lookup, workspace filtering, and DTO mapping in that order; entity accessibility lookup belongs to the workspace-access boundary.
+- Density: do not add timeline chips, warning rows, helper copy, or generated explanations while moving the lookup; recent activities stay compact and evidence-first.
+- Anti-AI-slop: never backfill activity rows from cross-workspace entities or invent import/update/failure events when an entity is inaccessible.
+- Operator workflow clarity: entity-scoped timelines must reject inaccessible entities before any activity rows are queried, while catalog-level workspace timelines continue to read only eligible persisted rows.
+- Context visibility: entity id, activity type, format, status, summary, detail, creator, and timestamp remain visible through the existing activity DTO contract.
+
+## 2026-05-10 Identity Candidate Workspace Entity Lookup Boundary
+
+- Hierarchy: monitor-to-entity suggestions keep identity extraction, persisted bind evidence, accessible entity lookup, then candidate scoring output; workspace entity lookup belongs to the workspace-access boundary.
+- Density: do not add workspace chips, empty-state prose, duplicate candidate groups, or new helper text while moving the lookup; suggestions remain scan-first.
+- Anti-AI-slop: never fill filtered candidates with cross-workspace entities, guessed names, fake scores, or invented matched identities.
+- Operator workflow clarity: request workspace scope must be applied before candidate DTO assembly so accepted monitors never suggest inaccessible entities.
+- Context visibility: monitor id, matched identity keys, score, recommendation, entity id/name/type, and already-bound state remain visible through the current response shape.
+
+## 2026-05-10 Identity Resolution Monitor-Bind Ownership Boundary
+
+- Hierarchy: monitor-to-entity suggestions keep identity extraction, candidate scoring, workspace filtering, and already-bound marking in that order; persisted bind lookup belongs to the monitor-bind boundary.
+- Density: do not add already-bound banners, duplicate monitor rows, or extra suggestion rationale while moving the lookup; candidate lists stay compact.
+- Anti-AI-slop: never guess already-bound state, entity names, scores, or matched identities when persisted bind evidence is unavailable.
+- Operator workflow clarity: accepted monitors still surface the same ranked candidates, and already-bound flags must come from real monitor-bind rows before suggestions leave identity resolution.
+- Context visibility: monitor id, matched identity keys, recommendation, score, workspace-filtered entity id/name, and already-bound state remain visible through the existing DTO contract.
+
+## 2026-05-10 Monitor Binding Identity Workspace Lookup Boundary
+
+- Hierarchy: monitor binding suggestions keep monitor acceptance first and identity-ranked entity candidates second; request workspace lookup belongs where identity candidates are filtered.
+- Density: do not add workspace chips, duplicate candidate groups, helper copy, or warning rows while moving the lookup; suggestion lists stay compact.
+- Anti-AI-slop: never broaden suggestions to cross-workspace entities or fabricate scores when identity resolution filters all candidates.
+- Operator workflow clarity: missing monitors still return no suggestions, accepted monitors keep the same identity evidence, and identity resolution applies request workspace scope before candidates leave the boundary.
+- Context visibility: monitor id, app, instance/name labels, matched identity keys, recommendation, score, and already-bound state remain visible through the existing DTO contract.
+
+## 2026-05-10 Entity Evidence Facade Workspace Lookup Boundary
+
+- Hierarchy: entity evidence endpoints keep access check, bound-monitor lookup, and alert/monitor evidence delegation in that order; raw workspace lookup belongs with the alert filtering boundary.
+- Density: do not add workspace labels, helper banners, or explanatory alert rows while moving the lookup; alert and monitor pages stay compact and evidence-led.
+- Anti-AI-slop: never default the evidence facade to a broad workspace or synthesize alert rows when the alert evidence boundary filters everything out.
+- Operator workflow clarity: inaccessible entities still return empty evidence, accepted entities keep the same status/severity/app filters, and alert evidence applies request workspace scope where alert rows are queried.
+- Context visibility: entity id, monitor ids, alert status, severity, pagination, and accepted workspace constraints remain visible through existing service contracts without new UI copy.
+
+## 2026-05-09 Login Session Boundary
+
+- Hierarchy: keep the existing passport login, auth gate, and operator shell hierarchy unchanged; this slice only changes where credentials live after successful login.
+- Density: do not add banners, rails, explanatory cards, or new login controls. The session boundary should be invisible during the normal happy path.
+- Anti-AI-slop: no marketing security copy, no fake "secure" status panel, and no client-visible token preview.
+- Operator workflow clarity: login should still restore the guarded route, bootstrap system config, and redirect exactly as before; logout should clear the BFF session then return to the canonical login route.
+- Context visibility: protected workbench redirects keep the current return target, but access and refresh tokens must move behind same-origin BFF cookies rather than browser localStorage.
+
+## 2026-05-09 Backend Query Workspace Boundary
+
+- Hierarchy: log and trace workbench layouts should keep their existing evidence order; workspace filtering is a data boundary, not a new visual layer.
+- Density: avoid adding explanatory chips or warning cards for normal scoped queries; empty states should remain truthful if a workspace has no matching telemetry.
+- Anti-AI-slop: never pad scoped workspaces with fake zero panels or synthetic rows; filtered-out data must simply stay unavailable.
+- Operator workflow clarity: the active workspace from the authenticated request must constrain query results consistently across list, overview, trend, and detail surfaces.
+- Context visibility: existing route/query context remains visible through current filters and handoffs, while workspace membership is enforced server-side.
+
+## 2026-05-10 Trace Query Workspace Boundary
+
+- Hierarchy: trace center list, overview, detail, and span tree keep their current evidence order; workspace enforcement must not add a new visual tier.
+- Density: do not add workspace chips, warning cards, or explanatory rows for normal scoped trace reads; zero results should remain a truthful absence of matching traces.
+- Anti-AI-slop: never synthesize placeholder spans, fake service paths, or cross-workspace trace summaries to fill scoped views.
+- Operator workflow clarity: the request workspace must constrain trace list, overview, detail, and span endpoints consistently so handoffs never reveal another workspace's trace.
+- Context visibility: existing entity, service, namespace, environment, time, and trace-id filters stay visible while the workspace boundary is enforced by backend resource attributes.
+
+## 2026-05-10 Live Log Stream Workspace Boundary
+
+- Hierarchy: the live log stream keeps the current event feed hierarchy; workspace scoping is a subscriber data boundary, not a new stream mode.
+- Density: do not add workspace badges, warning rows, or explanatory cards for normal scoped streams; filtered events should simply not arrive.
+- Anti-AI-slop: never backfill filtered live streams with synthetic log rows, placeholder errors, or guessed tenant labels.
+- Operator workflow clarity: a subscription created under a request workspace must keep that workspace boundary for the lifetime of the SSE subscriber, even after events are flushed asynchronously.
+- Context visibility: existing severity, content, trace, and span filters stay visible and continue to compose with the server-bound workspace filter.
+
+## 2026-05-10 Entity Catalog Workspace Boundary
+
+- Hierarchy: entity list and catalog entry points keep their current summary, health, evidence, and action order; workspace isolation must not add a new visible tier.
+- Density: do not add workspace chips, scope banners, or empty-state explanations for normal scoped lists; absent entities should remain a plain truthful empty result.
+- Anti-AI-slop: never synthesize entity summaries, health states, owners, or monitor counts to fill a scoped catalog.
+- Operator workflow clarity: the request workspace must constrain entity list reads before summaries, monitor counts, relation counts, alert evidence, and next actions are assembled.
+- Context visibility: existing type, status, owner, source, environment, lifecycle, tier, system, search, and sort filters stay visible while workspace membership is enforced server-side.
+
+## 2026-05-10 Entity Detail Evidence Workspace Boundary
+
+- Hierarchy: entity detail, definition export, bound monitor, and alert evidence entry points should keep their current detail-first structure; a workspace miss behaves like no accessible entity instead of adding a new visible security layer.
+- Density: do not add scoped badges, explanatory banners, or padded empty panels for cross-workspace misses; detail and evidence surfaces should simply return null or empty pages from the server.
+- Anti-AI-slop: never backfill detail health, active alerts, bound monitors, definition YAML, or next actions from another entity/workspace to make a page look complete.
+- Operator workflow clarity: workspace membership must be checked before identities, relations, monitor binds, alerts, definition activities, and cross-signal handoffs are assembled.
+- Context visibility: existing entity id, query filters, status/severity/app filters, and return-to context remain unchanged when the server accepts the workspace.
+
+## 2026-05-10 Monitor Binding Candidate Workspace Boundary
+
+- Hierarchy: monitor-to-entity suggestions stay ranked by identity match strength; workspace enforcement must only remove inaccessible candidates, not add a new grouping tier.
+- Density: do not add workspace labels or warning rows to candidate lists; no accessible match should remain an honest empty result.
+- Anti-AI-slop: never fabricate entity names, match identities, scores, or already-bound states to compensate for filtered cross-workspace candidates.
+- Operator workflow clarity: a bound request workspace must constrain candidate entities before recommendation, score, and already-bound state are presented.
+- Context visibility: existing monitor identity evidence such as `service.name`, `container.name`, `endpoint.url`, and app/instance context stays visible for accepted candidates.
+
+## 2026-05-10 Entity Mutation Workspace Boundary
+
+- Hierarchy: create, edit, definition import/update, and delete flows keep their current entity-first hierarchy; workspace rejection must behave like an inaccessible entity rather than a new UI state.
+- Density: do not add workspace banners, warning panels, or extra confirmation copy for normal scoped writes; accepted writes should keep the existing dense editor flow.
+- Anti-AI-slop: never silently reassign a cross-workspace entity to the caller's workspace, fabricate an updated entity, or delete evidence from another workspace.
+- Operator workflow clarity: request workspace membership must be checked before save/delete side effects, identity replacement, monitor-bind replacement, relation updates, lifecycle activities, and definition activities.
+- Context visibility: existing entity id, definition format, validation feedback, and return navigation remain unchanged for entities inside the accepted workspace.
+
+## 2026-05-10 Entity Delete Write Boundary
+
+- Hierarchy: delete keeps the entity-first mutation flow; identity rows, monitor binds, relations, and catalog row cleanup are backend side effects, not a new operator-visible tier.
+- Density: do not add delete banners, tombstone panels, or explanatory records while moving the cleanup sequence behind a write-model boundary.
+- Anti-AI-slop: never fabricate deleted-state summaries, retained evidence, or cross-workspace cleanup results; inaccessible entities remain a no-op.
+- Operator workflow clarity: workspace acceptance must happen before any identity, monitor-bind, relation, or catalog deletion side effect, and accepted cleanup order must remain deterministic.
+- Context visibility: accepted entity id and workspace context remain the only mutation context; downstream lists/details should reflect real persisted deletion without synthetic placeholders.
+
+## 2026-05-10 Governance State Workspace Boundary
+
+- Hierarchy: definition templates, definition activities, resume drafts, discovery presets, and discovery activities keep their current workflow order; workspace filtering only removes inaccessible shared-state records.
+- Density: do not add workspace chips, warnings, or new panels to normal template, preset, activity, or resume lists; an empty scoped list should remain a truthful empty result.
+- Anti-AI-slop: never synthesize templates, resume drafts, preset defaults, seed bundles, or activity rows to replace filtered records.
+- Operator workflow clarity: request workspace must constrain shared governance state before list, lookup, update, delete, and resume recovery side effects.
+- Context visibility: existing template id, resume token, preset id, activity id, format, source, seed definition, and workspace-path context remain visible for accepted records.
+
+## 2026-05-10 Catalog Suggestion Workspace Boundary
+
+- Hierarchy: catalog autocomplete and definition editor suggestions should keep the current owner, namespace, environment, system, lifecycle, tier, inherited-entity, language, and link-provider order; workspace scoping only changes the source rows.
+- Density: do not add workspace labels, scope banners, or explanatory empty states to suggestion menus; no accessible suggestions should stay a compact empty list.
+- Anti-AI-slop: never fabricate owners, systems, inherited refs, languages, link providers, or entity refs to make a scoped workspace look populated.
+- Operator workflow clarity: request workspace must constrain suggestion source entities before reusable catalog hints are assembled for create, import, edit, and definition workflows.
+- Context visibility: existing suggestion values remain visible for accepted rows, including entity reference shape and namespace context.
+
+## 2026-05-10 Definition Relation Lookup Workspace Boundary
+
+- Hierarchy: definition import/edit relation resolution should keep the existing depends-on/ref flow; workspace scoping only changes which entity row can be resolved as the target.
+- Density: do not add workspace labels, warnings, or explanatory rows to definition relation previews; unresolved scoped references stay compact and truthful.
+- Anti-AI-slop: never resolve a relation to a cross-workspace entity, never rewrite refs to invented targets, and never silently fabricate a related entity.
+- Operator workflow clarity: the request workspace must constrain type/namespace/name and direct-id lookup before relation target ids are attached during parse, import, edit, or bundle flows.
+- Context visibility: accepted relation refs, direct ids, namespace, and relation type/status/score context remain visible exactly as before.
+
+## 2026-05-10 Definition Activity Workspace Boundary
+
+- Hierarchy: catalog-level definition activity timelines should keep the same recent-first order; workspace filtering only changes which persisted rows are eligible.
+- Density: do not add workspace chips, warnings, or explanatory rows to activity timelines; a workspace with no activity stays a compact empty list.
+- Anti-AI-slop: never backfill activity rows from another workspace, and never fabricate import/update/failure rows to make a timeline look active.
+- Operator workflow clarity: request workspace must be written onto new definition activities and used for catalog-level activity reads before the timeline DTO is assembled.
+- Context visibility: accepted activity type, format, status, summary, detail, creator, and timestamp remain visible as before.
+
+## 2026-05-10 Entity Alert Evidence Workspace Boundary
+
+- Hierarchy: entity detail and entity alert evidence keep the current alert-first order; workspace filtering only decides which alert rows are eligible.
+- Density: do not add workspace labels, banners, or explanatory rows to alert tables or detail summaries; no scoped alerts remains a truthful empty state.
+- Anti-AI-slop: never fill an entity with cross-workspace alerts, guessed severity, fake status, or synthetic suppression evidence to make the detail page look active.
+- Operator workflow clarity: request workspace must constrain matched alert rows before active-alert lists, status summaries, response handoffs, and noise-control evidence are assembled.
+- Context visibility: accepted alert status, severity, content, monitor identity, timestamps, and return-to context remain visible exactly as before.
+
+## 2026-05-10 Noise-Control Evidence Workspace Boundary
+
+- Hierarchy: silence and inhibit evidence stays below active-alert context; workspace filtering only decides which suppression rules can be considered.
+- Density: do not add workspace chips, rule-source banners, or explanatory suppression rows to entity detail; filtered rules should simply be absent.
+- Anti-AI-slop: never mark possible suppression from a cross-workspace `matchAll` silence, generic inhibit, or guessed rule match to make an entity look explained.
+- Operator workflow clarity: request workspace must constrain silence and inhibit rules before match-all, label matching, counts, previews, and possible-suppression flags are assembled.
+- Context visibility: accepted rule name, type, matched labels, update time, active-alert context, and existing handoff targets remain visible exactly as before.
+
+## 2026-05-10 Entity Runtime Health Read-Model Boundary
+
+- Hierarchy: entity list/detail health should keep the current status, reason, monitor counts, and active-alert evidence order; this backend split must not introduce a new visual tier.
+- Density: do not add health badges, explanatory cards, or synthetic readiness rows for normal health evaluation; the existing compact status/readiness surfaces should stay unchanged.
+- Anti-AI-slop: never invent healthy/degraded/critical states, fake monitor counts, or placeholder alert counts when no evidence exists; no bound evidence remains `unknown`.
+- Operator workflow clarity: active firing alerts outrank monitor-down evidence, monitor-down evidence outranks healthy up evidence, all-paused monitors stay paused, and empty evidence stays unknown.
+- Context visibility: accepted active-alert, monitor, status reason, evaluated-at, entity id, and existing cross-signal handoff context remain visible exactly as before.
+
+## 2026-05-10 Entity Summary Read-Model Boundary
+
+- Hierarchy: entity list summaries should keep entity identity, counts, ops readiness, next action, evidence time, and definition activity in the existing compact row payload.
+- Density: do not add new summary panels, workspace chips, or readiness prose while moving aggregation out of the large service; rows stay scan-first.
+- Anti-AI-slop: never synthesize identity, monitor, relation, alert, evidence, next-action, or definition-management fields to make an entity look more complete.
+- Operator workflow clarity: summary rows must still derive readiness and first next action from the shared observability gateway after real counts, monitors, alerts, and definition activity are loaded.
+- Context visibility: entity id/name, status reason, monitor count, relation count, active-alert count, last evidence time, next action, and definition activity metadata remain visible exactly as before.
+
+## 2026-05-10 Entity Integration Hint Boundary
+
+- Hierarchy: monitor-to-entity binding suggestions stay as an evidence-backed handoff from monitor context into entity catalog context, not a new operator workflow tier.
+- Density: do not add suggestion banners, generated rationale prose, or placeholder candidate rows while moving monitor lookup behind a service boundary.
+- Anti-AI-slop: never invent binding candidates, identities, scores, already-bound state, or workspace matches when the monitor row or identity evidence is absent.
+- Operator workflow clarity: monitor acceptance and workspace context must be resolved before identity matching leaves the integration-hint boundary.
+- Context visibility: accepted monitor id, monitor labels/annotations, workspace id, matched identity keys, scores, recommendation, and already-bound state remain visible exactly as before.
+
+## 2026-05-10 Entity Activity Monitor-Bind Diff Boundary
+
+- Hierarchy: lifecycle activity remains audit evidence under entity mutation, with monitor-bind diffs feeding activity type selection instead of becoming a separate workflow tier.
+- Density: do not add activity badges, generated explanations, or placeholder discovery rows while moving current-bind lookup behind the activity write model.
+- Anti-AI-slop: never mark discovery governance from guessed monitor binds, stale cross-entity binds, or missing bind evidence just to make an edit look telemetry-driven.
+- Operator workflow clarity: current monitor binds must be loaded inside the activity boundary before comparing next submitted binds, so mutation orchestration stays focused on catalog writes.
+- Context visibility: accepted entity id, source transition, telemetry bind source, monitor ids, workspace-scoped mutation context, and existing activity summaries remain visible exactly as before.
+
+## 2026-05-10 Entity List Query Read-Model Boundary
+
+- Hierarchy: entity catalog list filtering, workspace scoping, runtime health, monitor evidence, alert evidence, and summary row assembly should leave the existing scan-first row hierarchy unchanged.
+- Density: do not add list-level banners, workspace badges, explanation rows, or synthetic "no evidence" panels while moving the query boundary; rows stay compact and filter-driven.
+- Anti-AI-slop: never fabricate entity rows, monitor counts, alert counts, health, next actions, or definition activity to compensate for scoped or filtered results.
+- Operator workflow clarity: type/status/owner/source/environment/lifecycle/tier/system/search/sort filters must apply before summaries leave the read model, with workspace membership enforced before evidence enrichment.
+- Context visibility: accepted entity id, workspace id, filters, sort order, health reason, evidence counts, and definition-activity metadata remain visible through the existing list payload.
+
+## 2026-05-10 Entity Workspace Access Boundary
+
+- Hierarchy: entity list, detail, alert, monitor, definition, mutation, import, and edit entry points keep their current entity-first hierarchy; workspace access only decides eligible rows.
+- Density: do not add workspace badges, warnings, or explanatory access rows to normal successful flows; inaccessible rows should be absent or behave like missing entities.
+- Anti-AI-slop: never borrow cross-workspace entities, identities, monitors, relations, alerts, definitions, or summaries to make an entity page appear complete.
+- Operator workflow clarity: request workspace matching and write-workspace resolution must be applied before read models, mutation side effects, relation resolution, and activity writes.
+- Context visibility: accepted entity id, workspace id, route filters, definition format, return navigation, and existing evidence counts remain visible exactly as before.
+
+## 2026-05-10 Entity Noise-Control Read-Model Boundary
+
+- Hierarchy: entity detail noise-control evidence keeps its current position below active alert and handoff context; extracting the read model must not create a new visual or product tier.
+- Density: do not add rule-origin badges, workspace warnings, or explanatory suppression prose; matched silence/inhibit previews remain compact evidence counts.
+- Anti-AI-slop: never infer suppression from cross-workspace rules, unlabeled non-default rules, or guessed labels; if no real rule matches, the summary must stay empty.
+- Operator workflow clarity: entity, alert, identity, and monitor labels should be normalized into candidate evidence before silence/inhibit rules are matched and workspace-filtered.
+- Context visibility: accepted rule name, type, matched label keys, update time, active-alert presence, and possible-suppression flag remain visible exactly as before.
+
+## 2026-05-10 Entity Detail Observability Assembly Boundary
+
+- Hierarchy: entity detail keeps the existing entity profile, status, evidence, alert, monitor, log, trace, unified evidence, triage, ops, status-page, response-handoff, noise-control, monitor list, active-alert list, query-hint, and activity order.
+- Density: do not add detail banners, evidence explanation rows, synthetic cross-signal cards, or placeholder navigation while moving assembly behind a read model.
+- Anti-AI-slop: never fabricate status, alerts, monitors, log hints, trace hints, evidence rows, recommendations, ops readiness, noise-control rows, or definition activities when the backing evidence is absent.
+- Operator workflow clarity: accepted entity context, workspace-scoped monitors/alerts, runtime health, observability bundle, response handoffs, noise-control evidence, and definition activities must be assembled before the detail payload leaves the read model.
+- Context visibility: entity id, workspace id, identities, relation count, status reason, evidence counts, handoff return context, noise-control evidence, and definition activity metadata remain visible exactly as before.
+
+## 2026-05-10 Entity Response-Handoff Read-Model Boundary
+
+- Hierarchy: entity detail handoffs keep the existing alert, monitor, log, trace, discovery, and editor order; extraction must not introduce new navigation tiers.
+- Density: do not add handoff badges, explanatory link groups, or fake destination rows; handoff payloads stay compact route context.
+- Anti-AI-slop: never invent severity, owner readiness, runbook readiness, relation readiness, telemetry readiness, trace ids, or monitor targets when evidence is missing.
+- Operator workflow clarity: handoff assembly must preserve return-to path, return label, entity ownership/system/environment/source metadata, signal evidence lists, and ops readiness flags before delegating to the shared observability gateway.
+- Context visibility: accepted search tokens, status/severity, monitor filters, trace/log correlation ids, discovery context, editor focus, and return navigation remain visible exactly as before.
+
+## 2026-05-10 Entity Alert Evidence Read-Model Boundary
+
+- Hierarchy: entity alert evidence keeps the existing alert-first order in detail summaries and alert list pages; extraction must not create a new evidence tier.
+- Density: do not add workspace badges, severity explainer rows, or synthetic alert rows while moving the query boundary; scoped empty results stay compact.
+- Anti-AI-slop: never borrow cross-workspace alerts, guess severity from content, or fabricate firing/resolved/acknowledged rows when monitor-linked evidence is missing.
+- Operator workflow clarity: monitor-linked alert matching, workspace label filtering, status normalization, severity filtering, severity-first ordering, and pagination must happen before the DTO/page leaves the read model.
+- Context visibility: accepted alert content, labels, status, severity, monitor identity, update time, and existing return-to/handoff context remain visible exactly as before.
+
+## 2026-05-10 Entity Monitor Evidence Read-Model Boundary
+
+- Hierarchy: entity monitor evidence keeps the existing bound-monitor list order semantics, with down monitors still surfacing before healthy or paused monitors.
+- Density: do not add monitor badges, workspace prose, or explanatory rows while moving list shaping; empty bound-monitor evidence remains a compact empty page.
+- Anti-AI-slop: never fabricate bound monitors, statuses, apps, timestamps, or monitor rows to make an entity appear better instrumented.
+- Operator workflow clarity: status filtering, app filtering, health-priority sorting, recency tie-breaking, id tie-breaking, DTO mapping, and pagination must complete before the page leaves the read model.
+- Context visibility: accepted monitor id, name, app, instance, status, labels, annotations, and update time remain visible exactly as before.
+
+## 2026-05-10 Entity Evidence Facade Boundary
+
+- Hierarchy: entity alert and monitor evidence entry points keep the same entity-first access check, alert-first page, and bound-monitor page order; the facade only owns read orchestration.
+- Density: do not add evidence banners, workspace chips, explanatory rows, or synthetic empty panels while moving access and monitor lookup out of the API facade.
+- Anti-AI-slop: never borrow cross-workspace monitors or alerts, never guess severity/status, and never fabricate instrumentation rows for inaccessible or unbound entities.
+- Operator workflow clarity: workspace acceptance, page normalization, bound monitor lookup, alert filtering, monitor filtering, sorting, and pagination must stay deterministic before evidence leaves the read boundary.
+- Context visibility: accepted entity id, workspace id, status/severity/app filters, monitor identities, alert labels, and existing return-to context remain visible through the current payloads.
+
+## 2026-05-10 Entity Detail DTO Read-Model Boundary
+
+- Hierarchy: entity detail, edit, and definition export keep the existing entity-first payload order of catalog profile, identities, monitor binds, and relations.
+- Density: do not add workspace labels, access banners, or synthetic related-row placeholders while moving DTO assembly behind a read model; inaccessible entities remain null.
+- Anti-AI-slop: never borrow identities, monitor binds, relations, or definition fields from another workspace/entity to make detail or definition pages appear complete.
+- Operator workflow clarity: workspace acceptance must happen before child DAO lookups, then identities, monitor binds, and relations are loaded in their existing stable order.
+- Context visibility: accepted entity id, workspace id, identity priority, monitor bind order, relation endpoints, and definition-export context remain visible exactly as before.
+
+## 2026-05-10 Entity Core Write Boundary
+
+- Hierarchy: entity create, edit, and definition bundle import keep catalog identity, ownership, workspace, definition metadata, monitor binds, relations, and runtime evidence in the existing order.
+- Density: do not add UI copy, workflow steps, synthetic metadata rows, or placeholder hints while moving core field writes behind a service boundary.
+- Anti-AI-slop: never infer owners, systems, tags, workspace, source, status, labels, integrations, extensions, or HertzBeat blocks beyond submitted data and existing defaults.
+- Operator workflow clarity: writes must preserve accepted workspace, explicit source fallback, labels-to-tags fallback only when tags are absent, and the existing relation/monitor-bind sequencing.
+- Context visibility: workspace id, catalog fields, labels/tags, integrations, extensions, and HertzBeat blocks stay persisted exactly as submitted/defaulted for downstream detail, list, and evidence read models.
+
+## 2026-05-10 Entity Catalog Persistence Boundary
+
+- Hierarchy: entity row persistence belongs to the catalog write model before identities, monitor binds, relations, runtime health, and activities are assembled.
+- Density: do not add create/edit/import steps, status banners, generated summaries, or extra rows while moving persistence out of the orchestration service.
+- Anti-AI-slop: never synthesize entity ids, workspace ownership, source, status, tags, or definition fields beyond submitted catalog data and existing defaults.
+- Operator workflow clarity: single create, edit, and bundle import must persist catalog rows in stable order before downstream evidence and activity writers consume entity ids.
+- Context visibility: accepted entity id, workspace id, source fallback, catalog metadata, bundle order, and existing activity/evidence context remain visible exactly as before.
+
+## 2026-05-10 Entity Validation Boundary
+
+- Hierarchy: entity create, edit, definition import, and bundle import keep validation before catalog persistence, identity writes, monitor binds, relations, runtime health, and activities.
+- Density: do not add extra validation screens, explanatory banners, or generated helper copy while moving validation out of the orchestration service.
+- Anti-AI-slop: validation must reject unsupported types, statuses, criticality, blank names, blank identities, and missing monitor ids instead of defaulting them into plausible-looking catalog evidence.
+- Operator workflow clarity: existing error messages and modify-id requirements remain stable so API callers and UI forms can map failures back to the submitted field.
+- Context visibility: submitted type, name, status, criticality, identity key/value, monitor id, and modify entity id stay the only validation evidence.
+
+## 2026-05-10 Entity Runtime Status Refresh Boundary
+
+- Hierarchy: create, edit, and bundle import keep catalog persistence, identity writes, monitor binds, relations, then runtime status refresh before activity evidence.
+- Density: do not add status banners, generated health explanations, empty panels, or workflow copy while moving refresh orchestration out of the large service.
+- Anti-AI-slop: runtime health must be computed only from real bound monitors and active alerts; no fake healthy, zero, or placeholder status evidence.
+- Operator workflow clarity: monitor lookup, active-alert lookup, and runtime persistence must remain in that order so saved entities surface accurate health immediately after mutation.
+- Context visibility: accepted entity id, workspace id, bound monitor ids, active-alert count, status reason, and persisted status stay visible through the existing detail/list read models.
+
+## 2026-05-10 Entity Definition Draft Boundary
+
+- Hierarchy: definition import and edit keep raw document parsing, canonical normalization, workspace-aware DTO mapping, validation, persistence, and activity evidence in the existing order.
+- Density: do not add preview panels, generated summaries, inferred fields, or extra activity rows while moving draft assembly behind a service boundary.
+- Anti-AI-slop: blank documents, multi-document single imports, telemetry identities, monitor binds, relation refs, owners, and HertzBeat extension blocks must reflect submitted definitions only.
+- Operator workflow clarity: single-definition and bundle APIs keep their current success and failure messages so import/edit callers can map errors to the submitted YAML or JSON.
+- Context visibility: request workspace id, target entity id, document order, mapped entity DTOs, relation refs, and definition format remain visible through existing import/export APIs and activity records.
+
+## 2026-05-10 Entity Mutation Workflow Boundary
+
+- Hierarchy: create, edit, definition import, and bundle import keep validation, catalog persistence, identity writes, monitor binds, relations, runtime health refresh, and activity evidence in their existing order.
+- Density: do not add extra mutation stages, generated summaries, preview cards, or duplicate activity rows while moving mutation choreography behind a service boundary.
+- Anti-AI-slop: saved catalog rows, identities, binds, relations, health, and activity evidence must come from submitted DTOs/definitions and existing read models only.
+- Operator workflow clarity: single create/edit/import and multi-entity bundle imports keep the same success ids, failure behavior, and definition activity records so callers can trace exactly what was accepted.
+- Context visibility: request workspace id, accepted entity ids, bundle order, source fallback, relation refs, runtime status, and activity format remain visible through existing APIs and read models.
+
+## 2026-05-10 Entity Definition Document Parser Boundary
+
+- Hierarchy: definition import/edit keeps the existing document-first flow before entity normalization, relation resolution, and activity writes.
+- Density: do not add parser explanation rows, format badges, or fake preview records while extracting the document parser; invalid input remains a direct validation failure.
+- Anti-AI-slop: never invent entity documents from blank content, scalar YAML/JSON, or malformed curl payloads to make an import look usable.
+- Operator workflow clarity: payload extraction, format detection, JSON/YAML loading, multi-document order, and bundle unwrapping must complete before the entity service applies domain defaults.
+- Context visibility: accepted source format, document order, metadata name/namespace, bundle size, and existing definition activity context remain visible exactly as before.
+
+## 2026-05-10 Entity Definition Document Renderer Boundary
+
+- Hierarchy: definition export keeps the existing entity-to-definition-to-document order before the operator sees YAML or JSON output.
+- Density: do not add export banners, format hints, or synthetic preview sections while extracting rendering; the returned document remains the only payload.
+- Anti-AI-slop: never add empty integrations, extensions, hertzbeat blocks, relations, or telemetry placeholders to make an exported entity look richer.
+- Operator workflow clarity: canonical metadata, spec, telemetry, relation, integration, extension, and HertzBeat-specific blocks must render after the entity service builds the real definition model.
+- Context visibility: accepted format, entity kind, metadata, spec fields, telemetry bindings, relation refs, and existing definition export context remain visible exactly as before.
+
+## 2026-05-10 Entity Definition Export Workspace Lookup Boundary
+
+- Hierarchy: definition export keeps entity DTO loading, DTO-to-definition mapping, and document rendering in order while request workspace lookup lives with the detail read-model boundary.
+- Density: do not add export helper copy, preview rows, badges, or wrapper payloads while removing export-level workspace plumbing.
+- Anti-AI-slop: never default workspace scope in the export orchestrator to make inaccessible entity definitions render.
+- Operator workflow clarity: inaccessible entities still return no definition, and accepted entities keep the same canonical YAML/JSON output path.
+- Context visibility: entity id, requested format, stored DTO fields, canonical definition blocks, and request workspace constraints remain visible through existing service contracts.
+
+## 2026-05-10 Entity Monitor Query Anti-Corruption Boundary
+
+- Hierarchy: monitor binding writes and monitor-binding suggestions keep entity workflow ownership, while old monitor DAO lookup lives behind a dedicated query boundary.
+- Density: do not add monitor preview cards, duplicate binding rows, helper copy, or synthetic compatibility metadata while moving the lookup.
+- Anti-AI-slop: never invent missing monitors, never preserve stale binds for nonexistent monitors, and never broaden suggestions beyond accepted monitor records.
+- Operator workflow clarity: missing monitors still produce no suggestions or skipped binds, accepted monitors keep the same order, filters, and identity evidence.
+- Context visibility: monitor ids, monitor app/name/instance labels, bind source/type/status/score, and matched identity keys remain visible through existing DTO and bind contracts.
+
+## 2026-05-10 Entity Definition Export Facade Boundary
+
+- Hierarchy: definition export keeps workspace-scoped entity DTO loading, DTO-to-definition mapping, and document rendering in that order before YAML or JSON reaches the operator.
+- Density: do not add export previews, explanatory wrappers, format badges, or synthetic document sections while moving export orchestration behind a facade.
+- Anti-AI-slop: never render a cross-workspace entity, never invent telemetry, relations, integrations, extensions, or HertzBeat blocks when the stored DTO lacks evidence.
+- Operator workflow clarity: inaccessible entities still return no definition, and accepted entities keep the same canonical document format and fallback behavior.
+- Context visibility: accepted entity id, request workspace id, output format, canonical definition fields, telemetry, relation refs, and extension blocks remain visible through the existing export payload.
+
+## 2026-05-10 Entity Governance Workflow Facade Boundary
+
+- Hierarchy: definition templates, definition activities, resume drafts, discovery presets, and discovery activities keep their current workspace workflow order while request workspace lookup moves behind a facade.
+- Density: do not add governance banners, scope badges, helper prose, or synthetic rows while reducing facade orchestration.
+- Anti-AI-slop: never invent templates, presets, activity trails, resume drafts, seed bundles, or discovery entity refs for an empty workspace.
+- Operator workflow clarity: every read, save, and delete must resolve the same current request workspace before touching shared governance state.
+- Context visibility: accepted template id, activity id, resume token, preset id, seed definition metadata, workspace path, entity refs, creator, and timestamps remain visible through the existing payloads.
+
+## 2026-05-10 Entity Facade Dependency Wiring Boundary
+
+- Hierarchy: the large entity facade remains a thin API entry point that forwards validation, mutation, detail, activity, evidence, governance, catalog, list, and monitor-binding work to dedicated M5 components.
+- Density: do not add fallback service creation, duplicate orchestration, synthetic payload rows, or operator copy while fixing dependency wiring.
+- Anti-AI-slop: never silently skip activity timelines or detail observability assembly because a delegated service was not injected.
+- Operator workflow clarity: entity detail, definition activity, evidence, governance, and catalog entry points must all reach their owning service consistently at runtime.
+- Context visibility: current request workspace id and accepted entity ids stay passed through existing method boundaries instead of being hidden behind fallback objects.
+
+## 2026-05-10 Entity Facade Workspace Lookup Boundary
+
+- Hierarchy: the entity facade remains an API router while workspace lookup lives with detail, definition draft, activity, catalog, list, and integration read-model/workflow owners.
+- Density: do not add workspace badges, helper banners, duplicated context rows, or fallback enrichment while removing facade-level workspace plumbing.
+- Anti-AI-slop: never default or invent workspace scope in the facade to make detail, definition, catalog, list, or binding results appear available.
+- Operator workflow clarity: each delegated service must resolve the current request workspace exactly where it reads or maps operator evidence.
+- Context visibility: accepted entity ids, definition requests, list filters, catalog suggestions, monitor ids, and request workspace constraints remain visible through existing service contracts.
+
+## 2026-05-10 Entity Governance Workflow Workspace Lookup Boundary
+
+- Hierarchy: governance workflow methods remain operator-facing command/read entry points while the state boundary owns request workspace lookup for templates, activities, resumes, presets, and discovery activity rows.
+- Density: do not add scope banners, helper copy, duplicated workflow rows, or synthetic empty-state content while moving workspace lookup down.
+- Anti-AI-slop: never default workspace scope in the workflow facade to make missing templates, presets, resume drafts, or activity trails appear available.
+- Operator workflow clarity: every governance read, save, and delete must resolve workspace scope at the state read/write boundary that touches persisted evidence.
+- Context visibility: template ids, activity ids, resume tokens, preset ids, seed definition metadata, workspace paths, entity refs, creators, timestamps, and request workspace constraints stay visible through existing payloads.
+
+## 2026-05-10 Entity Mutation Workflow Workspace Lookup Boundary
+
+- Hierarchy: mutation workflows keep create, edit, definition import, bundle import, and definition update sequencing while raw workspace lookup lives with draft parsing, relation resolution, status refresh, and activity persistence boundaries.
+- Density: do not add import preview rows, mutation helper copy, generated activity summaries, or duplicate evidence records while moving workspace lookup down.
+- Anti-AI-slop: never default workspace scope in the workflow to make cross-workspace relations, alerts, activities, or definition imports appear valid.
+- Operator workflow clarity: access checks remain explicit in the workflow, but each delegated write/read stage resolves request workspace scope where it touches persisted or derived evidence.
+- Context visibility: accepted entity ids, definition formats, bundle order, relation refs, monitor evidence, lifecycle activity type, and request workspace constraints remain visible through existing API behavior.
+
+## 2026-05-10 Entity Definition Normalization Boundary
+
+- Hierarchy: import/edit normalization keeps raw document parsing separate from canonical entity-definition interpretation before any entity DTO or persistence decision.
+- Density: keep the operator payload as the evidence source; do not add generated enrichment, hidden defaults beyond existing source/type fallbacks, or synthetic catalog details during normalization.
+- Anti-AI-slop: legacy aliases, tags, links, contacts, telemetry, relations, and HertzBeat-specific blocks must normalize only when present or explicitly derivable from the submitted definition.
+- Operator workflow clarity: validation failures and imported fields continue to map back to the submitted YAML/JSON structure, so the editor preview and activity trail explain real input rather than invented status.
+- Context visibility: canonical apiVersion, kind, metadata, spec, telemetry, relation refs, integrations, extensions, and HertzBeat-specific context remain inspectable after normalization.
+
+## 2026-05-10 Entity Definition Mapping Boundary
+
+- Hierarchy: definition import/export keeps document parsing, normalization, entity DTO mapping, and persistence/rendering as separate steps before any operator-facing result.
+- Density: do not add generated entity fields, placeholder identities, fake binds, or synthetic relations while moving DTO mapping.
+- Anti-AI-slop: mapping must preserve only real metadata, catalog links/contacts, telemetry identities, monitor binds, relations, integration/extension blocks, and HertzBeat blocks.
+- Operator workflow clarity: imports, edits, bundle creation, and export previews continue to explain the submitted or stored definition fields without invented enrichment.
+- Context visibility: entity kind/type, metadata, source, owner, namespace, runbook, telemetry, relation refs, and exported definition context remain visible exactly as before.
+
+## 2026-05-10 Entity Definition Source Ownership Boundary
+
+- Hierarchy: definition import/export ownership should stay split across parser, normalization, mapping, renderer, and orchestration rather than drifting back into the large service.
+- Density: do not preserve duplicate helper paths that could add hidden defaults, duplicate rows, or conflicting relation refs during future changes.
+- Anti-AI-slop: source ownership must make it obvious that generated enrichment, placeholder identities, fake binds, and synthetic relation evidence have no fallback path in `ObserveEntityServiceImpl`.
+- Operator workflow clarity: imports, edits, bundle creation, and export previews continue to flow through the dedicated components before persistence or rendering.
+- Context visibility: canonical definition fields, mapped entity context, relation refs, telemetry bindings, and export blocks remain inspectable in their dedicated boundaries.
+
+## 2026-05-10 Entity Deletion Cleanup Ownership Boundary
+
+- Hierarchy: delete access checks stay in the deletion write-model, while identity, monitor-bind, relation, and catalog-row cleanup each remain owned by their domain boundary before the entity disappears from operator views.
+- Density: do not add delete summaries, cleanup banners, duplicate relation rows, or synthetic monitor evidence while moving cleanup orchestration.
+- Anti-AI-slop: never hide dangling monitor binds or relations by inventing fallback cleanup evidence; deletion must remove only persisted dependent rows for the accepted entity id.
+- Operator workflow clarity: inaccessible entities still skip every side effect, and accepted deletes keep the same dependent-row cleanup order before the catalog row is removed.
+- Context visibility: accepted entity id, identity cleanup, monitor-bind cleanup, relation cleanup, and final catalog deletion remain visible through service contracts and tests.
+
+## 2026-05-10 Entity Detail Child Evidence Ownership Boundary
+
+- Hierarchy: entity detail loading keeps workspace acceptance first, then asks monitor-bind and relation boundaries for persisted child evidence instead of reading those stores itself.
+- Density: do not add extra detail sections, duplicate relation rows, monitor previews, or helper copy while moving child evidence ownership.
+- Anti-AI-slop: never synthesize monitor binds or relation context for an accepted entity; the detail DTO must expose only persisted rows from the owning boundaries.
+- Operator workflow clarity: rejected or missing entities still skip every child lookup, while accepted entities keep stable identity, monitor-bind, and relation ordering.
+- Context visibility: accepted entity id, identities, monitor bind ids/status/source, relation refs/types/status, and request workspace checks remain visible through existing detail DTO contracts.
+
+## 2026-05-10 Entity Activity Monitor Evidence Ownership Boundary
+
+- Hierarchy: activity writes keep definition/lifecycle event persistence, while monitor-bind evidence for discovery classification and detail counts stays behind the monitor-bind boundary.
+- Density: do not add extra timeline rows, generated summaries, badges, or duplicated monitor evidence while moving the lookup.
+- Anti-AI-slop: discovery activity must not invent bind counts or telemetry-discovery evidence; it can only reflect persisted monitor bind rows.
+- Operator workflow clarity: source changes, catalog updates, and telemetry discovery decisions keep their existing order and wording, with inaccessible workflow guards handled before activity writes.
+- Context visibility: accepted entity id, activity type, workspace id, source, owner/system/environment, telemetry-discovery bind ids, and monitor-bind count remain visible through activity tests and payloads.
+
+## 2026-05-10 Entity Workspace Query Ownership Boundary
+
+- Hierarchy: request-workspace access decisions stay in `EntityWorkspaceAccessService`, while raw catalog row lookup by id, ids, sorted workspace, and entity reference belongs to a dedicated query boundary.
+- Density: do not add workspace badges, duplicated catalog rows, helper copy, or extra evidence sections while moving lookup ownership.
+- Anti-AI-slop: access checks must not widen scope or synthesize default rows when the persisted catalog has no matching entity.
+- Operator workflow clarity: entity detail, list, activity, relation, identity candidate, and mutation flows keep the same accepted/rejected workspace behavior after raw lookup delegation.
+- Context visibility: request workspace id, entity id, reference type/namespace/name, sorted catalog rows, default-workspace compatibility, and filtered accepted entities remain visible through existing service contracts.
+
+## 2026-05-10 Entity Activity Entity Lookup Ownership Boundary
+
+- Hierarchy: activity writes keep timeline row persistence and activity detail construction, while stored entity workspace fallback lookup belongs to the workspace access boundary.
+- Density: do not add extra timeline rows, generated summaries, workspace helper copy, or duplicated activity evidence while moving the entity lookup.
+- Anti-AI-slop: activity workspace fallback must not invent a scope when the request and stored entity are absent; default workspace remains only the existing last-resort behavior.
+- Operator workflow clarity: definition failures, catalog lifecycle updates, source changes, and telemetry discovery activity continue to show the same summaries and details after the lookup boundary moves.
+- Context visibility: entity id, resolved workspace id, activity type, format, status, summary, detail, and monitor-bind evidence remain visible through the existing activity payload contracts.
+
+## 2026-05-10 Entity Deletion Catalog Row Ownership Boundary
+
+- Hierarchy: deletion orchestration keeps access checks and dependent identity, monitor-bind, and relation cleanup before asking the core write model to remove the accepted catalog row.
+- Density: do not add delete banners, cleanup summaries, duplicate timeline rows, or synthetic cleanup evidence while moving the final delete call.
+- Anti-AI-slop: never invent deletion evidence or hide dangling cleanup by deleting rows outside the accepted entity id.
+- Operator workflow clarity: inaccessible deletes still stop before side effects; accepted deletes keep the same cleanup order and final catalog-row removal.
+- Context visibility: accepted entity id, identity cleanup, monitor-bind cleanup, relation cleanup, and catalog-row delete remain visible through existing service contracts.
+
+## 2026-05-10 Entity Identity Read Evidence Ownership Boundary
+
+- Hierarchy: detail DTO assembly and list summaries keep workspace acceptance and summary composition first, while persisted identity rows and counts come from a dedicated identity read-model boundary.
+- Density: do not add identity badges, duplicate detail sections, extra summary columns, or helper copy while moving identity lookups.
+- Anti-AI-slop: never infer identities from labels, monitors, alerts, or telemetry placeholders; detail identities and identity counts must reflect persisted identity rows only.
+- Operator workflow clarity: rejected or missing entities still skip identity lookups, accepted detail pages keep stable identity ordering, and list summaries keep the same count semantics.
+- Context visibility: accepted entity id, persisted identity key/value/priority rows, identity count, monitor/relation counts, status, and definition activity context remain visible through existing DTO contracts.
+
+## 2026-05-10 Entity Identity Candidate Match Ownership Boundary
+
+- Hierarchy: monitor-binding candidate resolution keeps monitor identity extraction, bind evidence, workspace filtering, and scoring, while persisted identity-match lookup belongs to the identity read-model boundary.
+- Density: do not add candidate badges, extra suggestion rows, helper copy, or duplicate identity evidence while moving the persisted match query.
+- Anti-AI-slop: candidates must not be inferred from monitor labels alone; suggestions require persisted identity rows and accepted workspace entities.
+- Operator workflow clarity: hint calls keep current request workspace behavior, explicit workspace calls keep the same filtering, and already-bound flags continue to reflect persisted monitor binds.
+- Context visibility: monitor id, extracted identity keys/normalized values, persisted identity matches, accepted entity ids, scores, matched identity details, and already-bound state remain visible through existing candidate contracts.
+
+## 2026-05-10 Entity Summary Count Evidence Ownership Boundary
+
+- Hierarchy: entity list summaries keep row assembly and gateway readiness mapping, while monitor-bind and relation counts come from their owning domain boundaries.
+- Density: do not add extra summary badges, list columns, helper copy, or duplicate evidence lookups while moving count ownership.
+- Anti-AI-slop: list summaries must not infer monitor or relation counts from supplied monitor previews, gateway readiness, or placeholder evidence.
+- Operator workflow clarity: identity, monitor, relation, alert, definition activity, and next-action summary values keep the same order and meaning for entity list scanning.
+- Context visibility: accepted entity id, identity count, persisted monitor-bind count, relation count, active alert count, status, ops summary, and latest definition activity remain visible through summary contracts.
+
+## 2026-05-10 Entity Summary Definition Activity Ownership Boundary
+
+- Hierarchy: list summary assembly keeps counts, status, ops readiness, and next action composition, while latest definition activity lookup belongs to the activity read-model boundary.
+- Density: do not add extra list columns, activity badges, timeline previews, or helper copy while moving definition activity lookup.
+- Anti-AI-slop: summaries must not invent import/update evidence from lifecycle rows or missing activity history; only persisted definition import/update rows can mark definition activity context.
+- Operator workflow clarity: entity list scanning keeps the same definition activity status, summary, format, and time semantics after the lookup moves.
+- Context visibility: entity ids, latest definition import/update row, ignored lifecycle activities, summary counts, status, ops readiness, and next action remain visible through the existing list summary contracts.
+
+## 2026-05-10 Entity List Catalog Query Ownership Boundary
+
+- Hierarchy: entity list read-model assembly should keep summary/evidence composition, while catalog page filtering, sorting, workspace predicates, search predicates, and old API type compatibility live in a catalog query boundary.
+- Density: do not add list columns, badges, helper copy, or duplicated fallback rows while moving the query ownership.
+- Anti-AI-slop: list queries must not infer entities outside the persisted catalog page or silently widen workspace scope when filters are absent.
+- Operator workflow clarity: entity list scanning keeps the same pagination, search, type/status/source/owner/environment/lifecycle/tier/system filters, and old api/endpoint compatibility behavior after delegation.
+- Context visibility: request workspace, page request, search text, accepted entity ids, old api endpoint predicates, and persisted catalog rows remain visible through query and list contracts.
+
+## 2026-05-10 Entity Detail Runtime Evidence Ownership Boundary
+
+- Hierarchy: entity detail observability assembly should keep evidence, handoff, noise-control, and activity composition, while bound-monitor lookup, active-alert lookup, and status persistence live behind the runtime status refresh boundary.
+- Density: do not add detail panels, badges, helper rows, loading copy, or duplicated monitor/alert previews while moving runtime evidence ownership.
+- Anti-AI-slop: detail health, active alert count, and monitor context must come from persisted monitor binds plus real alert evidence, never synthetic healthy/zero panels.
+- Operator workflow clarity: accepted entity detail pages keep the same status, alert, monitor, log, trace, response handoff, noise-control, and definition activity semantics after delegation.
+- Context visibility: entity id, request workspace, bound monitors, active alerts, computed status, evidence summary, handoff context, and activity timeline remain visible through existing detail contracts.
+
+## 2026-05-10 Entity List Runtime Evidence Ownership Boundary
+
+- Hierarchy: entity list read-model assembly should keep catalog paging, workspace filtering, latest activity lookup, and summary mapping, while bound-monitor lookup, active-alert lookup, and status persistence live behind the runtime status refresh boundary.
+- Density: do not add list columns, badges, helper rows, duplicated alert previews, or fake status chips while moving runtime evidence ownership.
+- Anti-AI-slop: list health, alert count, and monitor context must come from persisted monitor binds plus real alert evidence, never synthetic healthy/zero rows.
+- Operator workflow clarity: entity list scanning keeps the same status, monitor count, alert count, definition activity, ops summary, next-action, pagination, and total fallback semantics after delegation.
+- Context visibility: request workspace, entity id, bound monitors, active alerts, computed status, latest definition activity, summary evidence, and pagination total remain visible through existing list contracts.
+
+## 2026-05-10 Entity Definition Residual Helper Boundary
+
+- Hierarchy: the large entity service should only orchestrate definition parsing, normalization, mapping, persistence, and rendering; residual conversion helpers belong to the dedicated definition components.
+- Density: removing duplicate helper code must not add copy, wrapper rows, or inferred catalog details to import/export responses.
+- Anti-AI-slop: no hidden helper path may invent owners, contacts, links, telemetry, monitor binds, relations, or HertzBeat extension blocks after the source components have made their decisions.
+- Operator workflow clarity: submitted definitions, edit payloads, bundle imports, and exports continue to preserve their existing success/failure behavior while helper ownership gets simpler.
+- Context visibility: accepted definition fields, entity DTO fields, relation references, and rendered document blocks remain visible through the existing APIs and tests.
+
+## 2026-05-10 Entity Relation Query Ownership Boundary
+
+- Hierarchy: relation replacement and reference resolution stay in the relation domain boundary, while persisted relation evidence and relation counts move behind a dedicated query boundary.
+- Density: do not add topology rows, summary badges, helper copy, duplicate relation previews, or generated relationship hints while moving lookup ownership.
+- Anti-AI-slop: relation detail and count evidence must come from persisted relation rows only; no inferred zero, healthy, dependency, or topology state should appear from missing rows.
+- Operator workflow clarity: entity detail and entity list scanning keep the same relation evidence and count semantics after the storage lookup moves.
+- Context visibility: accepted entity id, incoming/outgoing relation rows, relation counts, normalized target references, and workspace-resolved relation targets remain visible through existing service contracts.
+
+## 2026-05-10 Entity Monitor-Bind Query Ownership Boundary
+
+- Hierarchy: monitor-bind replacement and cleanup stay in the monitor-bind domain boundary, while persisted bind rows, monitor-id reverse lookups, and bind counts move behind a dedicated query boundary.
+- Density: do not add monitor badges, extra summary columns, helper copy, duplicate monitor previews, or synthetic binding hints while moving lookup ownership.
+- Anti-AI-slop: bound monitor evidence and counts must come from persisted bind rows plus real monitor rows only; do not infer binds from names, labels, alerts, or placeholder telemetry.
+- Operator workflow clarity: detail pages, activity evidence, identity candidates, runtime health, and list summaries keep the same bind order and count semantics after delegation.
+- Context visibility: accepted entity id, persisted bind rows, monitor id reverse lookup, bind counts, monitor lookup handoff, and replacement cleanup remain visible through existing contracts.
+
+## 2026-05-10 Entity Runtime Health Persistence Boundary
+
+- Hierarchy: runtime health calculation stays focused on bound monitor and alert evidence, while catalog-row status persistence belongs to a dedicated write-model boundary.
+- Density: do not add status badges, health panels, helper copy, duplicate alert rows, or extra summary fields while moving persistence ownership.
+- Anti-AI-slop: runtime status must persist only the computed value from real bound monitors and active alerts; never invent healthy, degraded, zero, or paused evidence to make a row look complete.
+- Operator workflow clarity: detail pages, list rows, and mutation refresh flows keep the same status, reason, monitor counts, alert counts, and unchanged-status no-op behavior after delegation.
+- Context visibility: accepted entity id, previous status, computed status, bound monitor counts, active alert count, and persisted catalog row remain visible through existing service contracts.
+
+## 2026-05-10 Old Monitor Route Host-Alias Compatibility Boundary
+
+- Hierarchy: the old batch monitor route keeps monitor catalog query ownership, while entity workflows consume monitor rows only through the existing anti-corruption boundary.
+- Density: do not add response fields, wrapper rows, extra filters, helper copy, or duplicate monitor summaries while preserving the legacy query alias.
+- Anti-AI-slop: the `host` alias must map to the real monitor search predicate; do not infer monitor matches from placeholder entity, alert, or telemetry context.
+- Operator workflow clarity: existing clients calling `/api/monitors?host=...` keep the same search behavior as clients using `/api/monitors?search=...`, and explicit `search` remains the preferred value when both are present.
+- Context visibility: monitor ids, app, status, labels, sort/order, pagination, search text, and legacy host alias remain visible through controller and service contracts.
+
+## 2026-05-10 Old Monitor Route Id-Alias Compatibility Boundary
+
+- Hierarchy: the old monitor list route keeps monitor catalog query ownership, while entity workflows continue to read monitor rows only through the anti-corruption query boundary.
+- Density: do not add response fields, wrapper rows, duplicate filters, helper copy, or synthetic monitor summaries while preserving the legacy singular id alias.
+- Anti-AI-slop: the `id` alias must map only to an explicit persisted monitor id filter; never infer monitor identity from entity, alert, topology, or telemetry placeholders.
+- Operator workflow clarity: existing clients calling `/api/monitors?id=...` keep the same filter behavior as clients using `/api/monitors?ids=...`, explicit `ids` remains preferred when both are present, and malformed legacy ids stay ignored rather than breaking old list searches.
+- Context visibility: monitor ids, legacy id alias, app, status, labels, search text, host alias, sort/order, and pagination remain visible through controller and service contracts.
+
+## 2026-05-10 Old Monitor Delete Authorization Config Boundary
+
+- Hierarchy: shipped Sureness templates keep authorization ownership in deployment/runtime config, while monitor controllers and entity read models keep their current route behavior.
+- Density: do not add roles, duplicate monitor route entries, helper copy, or new endpoint patterns while normalizing the old singular monitor DELETE rule.
+- Anti-AI-slop: delete authorization must protect only the explicit `/api/monitor/**` DELETE route with the admin role; do not broaden it to monitor lists, entity routes, or telemetry ingest paths.
+- Operator workflow clarity: private deployments using packaged or compose `sureness.yml` templates keep monitor delete access restricted to admins with a parseable `api===method===roles` rule.
+- Context visibility: route pattern, HTTP method, role list, and shipped config file remain visible through the config contract without changing controller response shapes.
+
+## 2026-05-10 Old Monitor Batch Id-Alias Compatibility Boundary
+
+- Hierarchy: monitor batch mutation routes keep controller-level compatibility, while entity workflows continue to consume monitor rows through the existing anti-corruption query boundary.
+- Density: do not add response fields, wrapper rows, duplicate endpoints, helper copy, or synthetic monitor summaries while accepting the singular id alias.
+- Anti-AI-slop: the `id` alias must map only to explicit persisted monitor ids for delete/manage side effects; never infer batch ids from entity, alert, topology, or telemetry context.
+- Operator workflow clarity: existing clients calling `/api/monitors?id=...`, `DELETE /api/monitors/manage?id=...`, or `GET /api/monitors/manage?id=...` keep their side effects; explicit `ids` remains preferred and body ids stay a legacy fallback.
+- Context visibility: query `ids`, legacy query `id`, body ids, chosen batch-id set, route, method, and service side effect remain visible through controller contracts.
+
+## 2026-05-10 Old Monitor Export Id-Alias Compatibility Boundary
+
+- Hierarchy: monitor export remains a controller-level compatibility affordance for explicit monitor rows, while entity read models keep monitor evidence behind the anti-corruption query boundary.
+- Density: do not add export summary payloads, duplicated export endpoints, or UI hints; only preserve the old singular id request shape for the existing download route.
+- Anti-AI-slop: export ids must come from explicit query `ids` or legacy query `id`; never infer export scope from search text, host filters, entity context, or telemetry evidence.
+- Operator workflow clarity: existing clients calling `/api/monitors/export?id=...&type=JSON` keep the same export side effect as `/api/monitors/export?ids=...&type=JSON`, with `ids` remaining preferred when both are present.
+- Context visibility: query `ids`, legacy query `id`, export type, route, method, and the selected id list remain visible through the controller contract.
+
+## 2026-05-10 Old Monitor Import Multipart Binding Boundary
+
+- Hierarchy: monitor import remains a single controller-level file upload handoff to the existing monitor import service, while entity read models keep monitor evidence behind the anti-corruption query boundary.
+- Density: do not add upload summaries, alternate response payloads, duplicated import endpoints, helper copy, or fake import progress rows while tightening multipart binding.
+- Anti-AI-slop: imported monitor config must come only from the explicit multipart `file` part; never infer import content from params, placeholder entities, telemetry, or generated sample monitors.
+- Operator workflow clarity: existing clients posting `FormData` with `file` to `/api/monitors/import` keep the same success response and real import side effect across compiler/native packaging modes.
+- Context visibility: multipart part name, original filename/content, route, method, response message, and service handoff remain visible through the controller contract.
+
+## 2026-05-10 Entity Detail Facade Handoff Boundary
+
+- Hierarchy: the observe entity facade should delegate entity detail loading plus observability assembly to the detail read-model boundary instead of composing the DTO through a self-call.
+- Density: do not add panels, summaries, badges, helper copy, alternate response wrappers, or duplicate entity detail rows while tightening the handoff.
+- Anti-AI-slop: entity detail evidence must keep coming from persisted entity rows, identities, relations, real bound monitors, active alerts, logs, traces, metrics, and activities; never fabricate empty health or navigation evidence.
+- Operator workflow clarity: entity detail callers keep the same null-on-inaccessible behavior and the same evidence bundle, while the facade stays a thin route-to-service boundary.
+- Context visibility: accepted entity id, request workspace, loaded entity DTO, runtime status evidence, response handoffs, noise-control evidence, and definition activities remain visible through the read-model contracts.
+
+## 2026-05-10 Observability Workspace Gateway Query Boundary
+
+- Hierarchy: observability-facing entity identity, entity row, and bind-count lookups should route through manager entity query boundaries, while raw DAO ownership stays inside the manager domain.
+- Density: do not add observability summary fields, entity cards, helper copy, duplicate monitor evidence, or synthetic workspace rollups while moving the gateway handoff.
+- Anti-AI-slop: observability workspace context must come only from persisted entity identities, entity catalog rows, bind rows, monitors, and collectors; never infer workspace identity from placeholder telemetry or generated labels.
+- Operator workflow clarity: traces, ingest attribution, and workspace summary callers keep the same counts, identity matches, entity lookup, and bind-count semantics after the gateway stops touching entity DAOs directly.
+- Context visibility: identity keys, normalized values, entity ids, entity rows, bind counts, monitor/collector counts, and latest monitor evidence remain visible through explicit query contracts.
+
+## 2026-05-10 Observability Inventory Gateway Query Boundary
+
+- Hierarchy: monitor and collector inventory reads should route through a manager observability inventory query boundary, while raw monitor and collector DAO ownership stays inside that boundary.
+- Density: do not add inventory summary fields, collector cards, monitor badges, helper copy, or synthetic readiness totals while moving the gateway handoff.
+- Anti-AI-slop: monitor counts, collector counts, online collector counts, and latest monitor evidence must come only from persisted monitor and collector rows; never infer inventory health from entity, alert, topology, or telemetry placeholders.
+- Operator workflow clarity: workspace summaries, ingest attribution, trace context, and entity workbench callers keep the same monitor and collector count semantics after the gateway stops touching inventory DAOs directly.
+- Context visibility: monitor count, collector count, collector status byte, latest monitor row, and entity query handoffs remain visible through explicit query contracts.
+
+## 2026-05-10 Old Monitor Singular Delete Query-Id Boundary
+
+- Hierarchy: the singular monitor delete route should preserve old-client query-id compatibility while keeping the delete side effect in the existing monitor service.
+- Density: do not add response wrappers, delete previews, extra route families, helper copy, or synthetic monitor evidence while accepting the legacy query shape.
+- Anti-AI-slop: delete scope must come only from the explicit persisted monitor id path variable or query parameter; never infer a delete target from entity, alert, topology, or telemetry context.
+- Operator workflow clarity: clients using `DELETE /api/monitor/{id}` and legacy clients using `DELETE /api/monitor?id=...` get the same existence check, side effect, and success/not-found message.
+- Context visibility: route, HTTP method, path id, legacy query id, existence lookup, delete side effect, and response message remain visible through controller contracts.
+
+## 2026-05-10 Old Monitor Singular Get Query-Id Boundary
+
+- Hierarchy: the singular monitor read route should preserve old-client query-id compatibility while keeping monitor DTO loading in the existing monitor service.
+- Density: do not add response wrappers, duplicate monitor summaries, helper copy, alternate payloads, or synthetic monitor evidence while accepting the legacy query shape.
+- Anti-AI-slop: read scope must come only from the explicit persisted monitor id path variable or query parameter; never infer monitor identity from entity, alert, topology, or telemetry context.
+- Operator workflow clarity: clients using `GET /api/monitor/{id}` and legacy clients using `GET /api/monitor?id=...` get the same success and not-found response semantics.
+- Context visibility: route, HTTP method, path id, legacy query id, DTO lookup, not-found branch, and response body remain visible through controller contracts.
+
+## 2026-05-10 Old Monitor Pinned Collector Validation Boundary
+
+- Hierarchy: old monitor create/edit validation should keep request normalization and parameter checks in `MonitorServiceImpl`, while persisted collector existence lookup routes through a dedicated query boundary.
+- Density: do not add collector pickers, validation panels, response wrappers, helper copy, duplicate monitor summaries, or synthetic collector evidence while moving the lookup owner.
+- Anti-AI-slop: pinned collector validity must come only from the explicit submitted collector name and persisted collector rows; never infer collector availability from entity, topology, telemetry labels, generated examples, or UI defaults.
+- Operator workflow clarity: legacy create/edit clients keep the same "pinned collector does not exist" rejection and blank collector normalization, but the raw collector DAO read path is now explicit.
+- Context visibility: submitted collector, blank normalization, persisted collector lookup, validation rejection, and later collector-bind persistence remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Page Query Boundary
+
+- Hierarchy: old monitor pagination, filtering, and sort specification should live in a query boundary, while `MonitorServiceImpl` keeps the legacy service API surface.
+- Density: do not add list panels, duplicate counters, alternate route envelopes, helper copy, synthetic monitor rows, or fallback summaries while moving the raw page query.
+- Anti-AI-slop: monitor list rows must come only from persisted monitor rows and explicit request filters; never infer monitor scope from entity context, alert evidence, topology, telemetry labels, or generated examples.
+- Operator workflow clarity: legacy monitor list callers keep the same app, status, search, label, sort, order, and pagination behavior, with repository access isolated for future compatibility work.
+- Context visibility: submitted filters, generated specification, pageable sort, raw DAO page query, and legacy service method remain visible through source and behavior contracts.
+
+## 2026-05-10 Old Monitor Status Write-Model Boundary
+
+- Hierarchy: old monitor status mutation should sit behind a write-model boundary, while `MonitorServiceImpl` keeps route-facing orchestration and compatibility behavior.
+- Density: do not add status panels, duplicate monitor summaries, helper copy, alternate response envelopes, or synthetic status evidence while moving persistence ownership.
+- Anti-AI-slop: monitor status must come only from explicit service inputs and persisted monitor rows; never infer pause, resume, up, or down state from entity context, telemetry placeholders, alerts, topology, or generated examples.
+- Operator workflow clarity: legacy status-update callers keep the same side effect, but raw status persistence is separated from the monitor service orchestration path.
+- Context visibility: monitor id, submitted status, write-model call, raw DAO update, and unchanged legacy service method remain visible through source and behavior contracts.
+
+## 2026-05-10 Old Monitor Copy Query-Id Boundary
+
+- Hierarchy: the singular monitor copy route should preserve old-client query-id compatibility while keeping the copy side effect in the existing monitor service.
+- Density: do not add response wrappers, copy previews, duplicate monitor rows, helper copy, or synthetic monitor evidence while accepting the legacy query shape.
+- Anti-AI-slop: copy scope must come only from the explicit persisted monitor id path variable or query parameter; never infer copy targets from entity, alert, topology, or telemetry context.
+- Operator workflow clarity: clients using `POST /api/monitor/copy/{id}` and legacy clients using `POST /api/monitor/copy?id=...` get the same success and failure response semantics.
+- Context visibility: route, HTTP method, path id, legacy query id, copy side effect, exception branch, and response message remain visible through controller contracts.
+
+## 2026-05-10 Old Monitor Singular Modify Path-Id Boundary
+
+- Hierarchy: the singular monitor modify route should preserve old-client path-id compatibility while keeping validation and mutation side effects in the existing monitor service.
+- Density: do not add response wrappers, duplicate monitor summaries, helper copy, extra editor routes, or synthetic monitor evidence while accepting the legacy path shape.
+- Anti-AI-slop: modify scope must come only from the explicit persisted monitor id path variable or the submitted monitor body; never infer monitor identity from entity, alert, topology, or telemetry context.
+- Operator workflow clarity: clients using `PUT /api/monitor` and legacy clients using `PUT /api/monitor/{id}` get the same validation, mutation side effect, and success response, with the path id binding the submitted monitor row.
+- Context visibility: route, HTTP method, path id, body monitor id, validation call, mutation side effect, and response message remain visible through controller contracts.
+
+## 2026-05-10 Old Monitor Root Authorization Boundary
+
+- Hierarchy: exact root monitor authorization rules should sit beside wildcard monitor rules so old singular and batch controller entrypoints remain protected in every shipped Sureness template.
+- Density: do not add new route families, role variants, response wrappers, helper copy, or duplicate monitor workflows while making the security templates explicit.
+- Anti-AI-slop: route access must be declared only from real controller paths and existing role scopes; never infer authorization from UI navigation, entity context, telemetry labels, or generated examples.
+- Operator workflow clarity: existing clients calling exact root paths such as `GET /api/monitor?id=...`, `PUT /api/monitor`, `DELETE /api/monitor?id=...`, `GET /api/monitors`, and `DELETE /api/monitors` keep the same roles as their wildcard monitor route family.
+- Context visibility: exact path, wildcard path, HTTP method, role list, shipped config file, and malformed-rule rejection stay visible through the config contract.
+
+## 2026-05-10 Entity Governance State Write-Model Boundary
+
+- Hierarchy: governance-state reads should stay in the query boundary, while template, resume, preset, and activity persistence routes through a write-model boundary.
+- Density: do not add governance panels, generated workflow summaries, duplicate state rows, helper copy, or alternate response envelopes while splitting persistence ownership.
+- Anti-AI-slop: governance state must be persisted only from explicit operator inputs and request workspace context; never infer saved templates, resumes, presets, or activities from placeholder telemetry or generated examples.
+- Operator workflow clarity: definition and discovery workspace flows keep the same save/delete behavior, but the code now makes mutation side effects visibly separate from read lookup.
+- Context visibility: state scope, state kind, state key, request workspace, resolved workspace id, saved content, and delete route remain visible through the query/write-model contracts.
+
+## 2026-05-10 Old Monitor Submitted Null Id Boundary
+
+- Hierarchy: old monitor bulk actions should normalize submitted ids inside the shared service-discovery expansion helper before DAO reads, while route/controller compatibility remains unchanged.
+- Density: do not add validation panels, alternate response wrappers, helper copy, duplicate delete summaries, or synthetic child monitor rows while filtering invalid ids.
+- Anti-AI-slop: action scope must come only from explicit non-null submitted monitor ids plus persisted service-discovery child binds; never infer targets from entity, alert, topology, telemetry, or placeholder context.
+- Operator workflow clarity: legacy clients that accidentally include blank/null ids in a bulk request keep the valid requested side effect instead of failing the whole monitor cleanup path.
+- Context visibility: submitted ids, sanitized parent ids, persisted child bind ids, expanded action ids, cleanup side effects, and scheduler events remain visible through the service contract.
+
+## 2026-05-10 Entity Workspace Access Id Normalization Boundary
+
+- Hierarchy: entity workspace access should normalize entity id collections before crossing into the raw workspace query boundary, while read models and facades keep their existing delegation paths.
+- Density: do not add entity panels, summaries, duplicate rows, helper copy, alternate response wrappers, or synthetic workspace totals while tightening id handoff.
+- Anti-AI-slop: accessible entity evidence must come only from explicit non-null entity ids and persisted entity rows; never infer entity scope from generated placeholders, topology guesses, alerts, telemetry, or monitor labels.
+- Operator workflow clarity: entity detail/list/evidence flows that pass sparse or repeated ids keep the valid entity evidence and avoid repository-level null id failures.
+- Context visibility: submitted ids, normalized entity ids, request workspace, persisted entity rows, and workspace filtering remain visible through the access/query contract.
+
+## 2026-05-10 Old Monitor Bulk Id Order Boundary
+
+- Hierarchy: old monitor bulk delete, pause, resume, and export routes should preserve submitted id order as request evidence while keeping side effects inside `MonitorService`.
+- Density: do not add response wrappers, preview rows, helper copy, duplicate monitor summaries, or synthetic child-monitor evidence while making the id container deterministic.
+- Anti-AI-slop: bulk action scope must come only from explicit query/body ids plus persisted service-discovery child binds; never infer order or targets from entity, alert, topology, telemetry, or generated examples.
+- Operator workflow clarity: legacy clients using comma-separated `id`, repeated `ids`, or JSON bodies get the same actions, with deterministic submitted evidence for cleanup, support, and tests.
+- Context visibility: route, HTTP method, submitted id sequence, normalized action set, service handoff, service-discovery expansion, and cleanup side effects remain visible through controller/service contracts.
+
+## 2026-05-10 Entity Runtime Health Catalog Write Boundary
+
+- Hierarchy: runtime health calculation should stay evidence-focused, while catalog-row status persistence routes through the core catalog write model that already owns `ObserveEntityDao` writes.
+- Density: do not add health panels, duplicate status summaries, helper copy, synthetic evidence counters, alternate response envelopes, or generated remediation text while moving persistence ownership.
+- Anti-AI-slop: entity status changes must come only from persisted monitors, active alerts, and the explicit entity row being refreshed; never infer catalog health from placeholder topology, telemetry guesses, or generated defaults.
+- Operator workflow clarity: entity detail, evidence, and list flows keep the same status semantics, but code ownership now makes calculated health separate from raw catalog-row persistence.
+- Context visibility: monitor totals, alert counts, computed status, entity row, catalog status mutation, and DAO save owner remain visible through runtime-health and core-write-model contracts.
+
+## 2026-05-10 Entity Catalog Page Raw Query Boundary
+
+- Hierarchy: entity list filters should build catalog page specifications in `EntityCatalogQueryService`, while raw `ObserveEntityDao.findAll(...)` execution stays in the workspace query boundary.
+- Density: do not add entity list cards, extra summary counters, helper copy, fallback rows, duplicate pagination metadata, or synthetic workspace totals while moving the DAO handoff.
+- Anti-AI-slop: list rows must come only from persisted entity catalog rows plus explicit request filters; never infer entities, owners, status, or workspace scope from placeholder topology, alerts, telemetry, or generated examples.
+- Operator workflow clarity: entity list, detail handoff, and old API compatibility filters keep the same pagination, sort, search, and workspace behavior after the raw query owner moves.
+- Context visibility: submitted filters, generated specification, pageable/sort, request workspace, persisted rows, and workspace filtering remain visible through catalog-query and workspace-query contracts.
+
+## 2026-05-10 Old Monitor Service-Discovery Expansion Query Boundary
+
+- Hierarchy: old monitor delete, pause, and resume should share one service-discovery expansion boundary, while `MonitorServiceImpl` stays focused on action side effects and cleanup sequencing.
+- Density: do not add response wrappers, previews, duplicate monitor summaries, helper copy, alternate route branches, or synthetic child monitor rows while moving the bind lookup.
+- Anti-AI-slop: expanded action scope must come only from explicit non-null submitted parent ids plus persisted `MonitorBind` child ids; never infer targets from entity, alert, topology, telemetry, or generated examples.
+- Operator workflow clarity: legacy clients keep the same parent-plus-child behavior for delete, pause, and resume, but raw service-discovery bind lookup is visible in a dedicated query boundary.
+- Context visibility: submitted ids, sanitized parent ids, persisted child bind rows, expanded action ids, monitor row lookup, cleanup side effects, and scheduler events remain visible through expansion and service contracts.
+
+## 2026-05-10 Old Monitor Service-Discovery Bind Cleanup Boundary
+
+- Hierarchy: old monitor delete should keep monitor-row deletion and scheduler events in `MonitorServiceImpl`, while service-discovery bind cleanup routes through a dedicated write-model boundary.
+- Density: do not add delete previews, alternate route branches, response wrappers, helper copy, duplicate monitor summaries, or synthetic child monitor rows while moving cleanup ownership.
+- Anti-AI-slop: cleanup scope must come only from persisted monitors selected for deletion and their persisted service-discovery bind rows; never infer cleanup targets from entity, alert, topology, telemetry, or generated examples.
+- Operator workflow clarity: legacy delete clients keep the same parent-plus-child cleanup behavior, but parent-bind and child-bind deletion are now explicit write-model calls instead of raw DAO calls inside the monitor service.
+- Context visibility: expanded monitor ids, persisted monitor rows, parent bind cleanup, child bind cleanup, entity bind cleanup, collector bind cleanup, favorites cleanup, scheduler cancellation, and deletion events remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Collector Bind Delete Cleanup Boundary
+
+- Hierarchy: old monitor delete should keep selected monitor rows, job cancellation, and event publishing in `MonitorServiceImpl`, while collector bind cleanup routes through a dedicated write-model boundary.
+- Density: do not add delete previews, collector cards, response wrappers, helper copy, duplicate monitor summaries, or synthetic collector evidence while moving cleanup ownership.
+- Anti-AI-slop: collector bind cleanup must come only from persisted monitors selected for deletion; never infer collector cleanup scope from entity, alert, topology, telemetry labels, or generated examples.
+- Operator workflow clarity: legacy delete clients keep the same collector binding cleanup, but the code now separates collector-bind persistence from the delete orchestration path.
+- Context visibility: expanded action ids, persisted monitor rows, collector bind cleanup, service-discovery cleanup, entity bind cleanup, scheduler cancellation, and deletion events remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Collector Bind Modify Boundary
+
+- Hierarchy: old monitor modify should keep validation, job update, monitor save, and param save in `MonitorServiceImpl`, while collector bind replacement routes through the collector-bind write-model boundary.
+- Density: do not add editor panels, collector cards, response wrappers, helper copy, duplicate monitor summaries, or synthetic collector evidence while moving persistence ownership.
+- Anti-AI-slop: collector assignment must come only from the explicit submitted collector value and persisted monitor id; never infer collector assignment from entity, alert, topology, telemetry labels, or generated examples.
+- Operator workflow clarity: legacy modify clients keep the same delete-then-save collector binding behavior, but collector-bind persistence is now a visible write-model call.
+- Context visibility: submitted monitor id, collector value, updated job id, collector bind replacement, monitor save, param save, and rollback cancellation remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Collector Bind Add Boundary
+
+- Hierarchy: old monitor create should keep id allocation, job creation, detect, monitor save, param save, and dashboard creation in `MonitorServiceImpl`, while collector bind creation routes through the collector-bind write-model boundary.
+- Density: do not add creation previews, collector cards, response wrappers, helper copy, duplicate monitor summaries, or synthetic collector evidence while moving persistence ownership.
+- Anti-AI-slop: collector assignment must come only from the explicit submitted collector value and newly allocated monitor id; never infer collector assignment from entity, alert, topology, telemetry labels, or generated examples.
+- Operator workflow clarity: legacy add clients keep the same optional collector binding behavior, but the code now makes collector-bind persistence a dedicated write-model action.
+- Context visibility: allocated monitor id, submitted collector, created job id, collector bind creation, monitor save, param save, dashboard creation, and rollback cancellation remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Collector Bind Query Boundary
+
+- Hierarchy: old monitor detail, resume, and template-refresh flows should keep DTO assembly and job orchestration in `MonitorServiceImpl`, while collector-bind lookup routes through a dedicated query boundary.
+- Density: do not add detail panels, collector cards, response wrappers, helper copy, duplicate monitor summaries, or synthetic collector evidence while moving read ownership.
+- Anti-AI-slop: collector evidence must come only from persisted collector-bind rows for explicit monitor ids; never infer collector assignment from entity, alert, topology, telemetry labels, or generated examples.
+- Operator workflow clarity: legacy detail and management clients keep the same collector values and pinned collector job behavior, but the raw collector-bind read path is now visible and reusable.
+- Context visibility: monitor id, monitor id set, persisted collector bind rows, detail collector value, resume collector value, template-refresh collector map, and scheduler handoff remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Export-All Catalog Query Boundary
+
+- Hierarchy: old monitor export-all should keep HTTP export orchestration in `MonitorServiceImpl`, while full catalog id selection belongs to the old monitor catalog query boundary.
+- Density: do not add export previews, response wrappers, duplicate monitor summaries, helper copy, synthetic counts, or alternate export screens while moving query ownership.
+- Anti-AI-slop: exported ids must come only from persisted old monitor catalog rows; never infer export scope from entity, alert, topology, telemetry, labels, or generated examples.
+- Operator workflow clarity: legacy export-all clients keep the same type/response behavior, but the full-id read path is now visible as a catalog query contract.
+- Context visibility: export type, persisted monitor ids, catalog query owner, export helper call, and response handoff remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Validation Name Query Boundary
+
+- Hierarchy: old monitor validation should keep request normalization and parameter checks in `MonitorServiceImpl`, while persisted name lookup belongs to the old monitor catalog query boundary.
+- Density: do not add editor panels, inline helper copy, duplicate warnings, alternate response wrappers, or synthetic uniqueness evidence while moving query ownership.
+- Anti-AI-slop: name conflict evidence must come only from explicit submitted monitor names and persisted old monitor catalog rows; never infer conflicts from labels, entities, alerts, topology, telemetry, or generated examples.
+- Operator workflow clarity: legacy create/edit validation keeps the same error messages and modify semantics, but the raw catalog-name lookup is now a visible query contract.
+- Context visibility: submitted name, modify flag, existing monitor row, app-definition name guard, param validation, and collector validation remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Detail Copy Modify Catalog Lookup Boundary
+
+- Hierarchy: old monitor detail, copy, modify, and simple lookup flows should keep DTO assembly, copy construction, scheduler orchestration, and legacy errors in `MonitorServiceImpl`, while persisted monitor-row lookup belongs to the old monitor catalog query boundary.
+- Density: do not add detail panels, copy previews, duplicate monitor summaries, helper copy, alternate response wrappers, or synthetic monitor evidence while moving lookup ownership.
+- Anti-AI-slop: monitor evidence must come only from explicit monitor ids and persisted old monitor catalog rows; never infer source monitors from entities, alerts, topology, telemetry labels, or generated examples.
+- Operator workflow clarity: legacy detail, copy, modify, and get-monitor clients keep the same null/not-found behavior, app-type guard, copy naming, and scheduler side effects, but raw catalog lookup is now a visible query contract.
+- Context visibility: submitted monitor id, persisted source row, not-found branch, detail DTO assembly, copy parameters, app-type compatibility guard, and scheduler handoff remain visible through service contracts.
+
+## 2026-05-10 Old Monitor Alert Bind Delete Cleanup Boundary
+
+- Hierarchy: old monitor delete should keep monitor-row selection, downstream cleanup ordering, scheduler cancellation, and event emission in `MonitorServiceImpl`, while alert-definition bind cleanup belongs to a dedicated write-model boundary.
+- Density: do not add delete previews, alert cards, duplicate monitor summaries, helper copy, alternate response wrappers, or synthetic alert evidence while moving cleanup ownership.
+- Anti-AI-slop: alert-bind cleanup must come only from persisted monitors selected for deletion; never infer alert cleanup scope from entity context, telemetry, topology, labels, or generated examples.
+- Operator workflow clarity: legacy delete clients keep the same monitor/alert cleanup behavior, but raw alert bind persistence is now isolated from old monitor orchestration.
+- Context visibility: expanded monitor ids, persisted monitor rows, alert-bind cleanup, parameter cleanup, entity bind cleanup, collector cleanup, scheduler cancellation, and delete events remain visible through service contracts.
diff --git a/web-next/docs/parity/time-context-visual-brief.md b/web-next/docs/parity/time-context-visual-brief.md
new file mode 100644
index 0000000000..0219ce48b4
--- /dev/null
+++ b/web-next/docs/parity/time-context-visual-brief.md
@@ -0,0 +1,3278 @@
+# Time Context Visual Brief
+
+Local design-gate notes for release-readiness work. Do not commit or upload unless explicitly requested.
+
+## OTLP Ingest RED Time Window Readback Boundary - 2026-05-12
+
+- Hierarchy: keep OTLP ingest RED as a backend evidence summary feeding existing workbench surfaces; this slice only makes durable readback honor explicit start/end query boundaries.
+- Density: do not add time chips, range banners, explanatory helper copy, or synthetic empty-state panels while tightening the query contract.
+- Anti-AI-slop: do not interpret missing rows as healthy zero traffic or fake success; RED counts must come from real Greptime or in-memory audit events inside the requested window.
+- Operator workflow clarity: operators should be able to ask for a closed time window and get the same bounded evidence semantics from durable readback and fallback memory.
+- Context visibility: workspace id, start/end millis, bounded limit, signal/protocol/outcome/status, and latest observed time remain covered by service/controller contracts.
+
+## Entity Definition Document Node Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit, type resolution, metadata, spec profile, telemetry, relations, add-ons, and HertzBeat evidence in one draft-to-catalog flow; this slice only moves raw `spec` and `telemetry` document-node selection behind the document-field boundary.
+- Density: do not add schema previews, telemetry badges, fallback chips, import banners, helper copy, or route chrome while preserving compact submitted definition evidence.
+- Anti-AI-slop: do not infer spec or telemetry maps from entity names, route context, app names, labels, or templates; only explicit submitted document nodes should shape canonical spec and telemetry envelopes.
+- Operator workflow clarity: root `spec` selection and nested `telemetry` selection should stay deterministic so operators can trace imports back to the submitted document.
+- Context visibility: spec-map extraction, telemetry-map extraction, string-key normalization, null-key suppression, and non-object suppression remain covered by service contracts.
+
+## Entity Definition HertzBeat Evidence Envelope Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit, spec profile, telemetry identities, relations, integrations, extensions, and HertzBeat evidence in one draft-to-catalog flow; this slice only moves HertzBeat evidence envelope attachment behind the evidence boundary.
+- Density: do not add saved-query previews, pipeline cards, performance tag chips, code-location panels, helper copy, or route chrome while preserving compact declared evidence.
+- Anti-AI-slop: do not synthesize logs, events, code locations, pipeline fingerprints, performance tags, or release evidence from entity names, routes, labels, or template context; only submitted evidence nodes and explicit legacy fingerprints should attach.
+- Operator workflow clarity: root-vs-spec `hertzbeat` selection and legacy pipeline fallback should stay deterministic so operators can trace imported evidence back to the submitted definition document.
+- Context visibility: HertzBeat root/spec selection, legacy fingerprint fallback, empty-block suppression, and definition attachment remain covered by service contracts.
+
+## Entity Definition Add-on Envelope Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit, spec profile, telemetry identities, relations, integrations, extensions, and HertzBeat evidence in one draft-to-catalog flow; this slice only moves integration/extension envelope attachment behind the add-on boundary.
+- Density: do not add integration previews, extension cards, provider badges, helper copy, or route chrome while preserving compact declared add-on evidence.
+- Anti-AI-slop: do not synthesize integrations, extensions, providers, plugin hints, saved views, or add-on status from entity names, routes, labels, or template context; only submitted object nodes should attach add-ons.
+- Operator workflow clarity: root-vs-spec fallback for `integrations` and `extensions` should stay deterministic so operators can trace imported add-on evidence back to the submitted definition document.
+- Context visibility: integration/extension root/spec selection, non-object suppression, string-key normalization, and definition attachment remain covered by service contracts.
+
+## Entity Definition Relation Envelope Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit, spec profile, telemetry identities, relations, dependency references, integrations, extensions, and HertzBeat evidence in one draft-to-catalog flow; this slice only moves relation envelope assembly behind the relation boundary.
+- Density: do not add topology previews, dependency cards, relation badges, compatibility banners, helper copy, or route chrome while preserving compact declared relation evidence.
+- Anti-AI-slop: do not synthesize relations, dependency references, target entities, scores, statuses, or topology hints from names, routes, labels, or template context; only declared relation/dependency nodes should drive relation evidence.
+- Operator workflow clarity: `relations`, legacy `dependencies`, and `dependsOn` should resolve deterministically so operators can trace topology evidence back to the submitted definition.
+- Context visibility: relation extraction, dependency fallback, dedupe, entity-id reference projection, and spec relation/dependsOn attachment remain covered by service contracts.
+
+## Entity Definition Telemetry Envelope Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit, spec profile, telemetry identities, monitor binds, relations, integrations, extensions, and HertzBeat evidence in the same draft-to-catalog flow; this slice only moves telemetry envelope assembly behind the telemetry boundary.
+- Density: do not add identity panels, monitor-bind previews, evidence badges, route banners, or helper copy while preserving compact declared telemetry evidence.
+- Anti-AI-slop: do not synthesize identities, monitor binds, bind status, scores, or match context from route names, entity names, labels, or template hints; only declared telemetry nodes should attach a telemetry block.
+- Operator workflow clarity: empty telemetry should stay absent so operators can distinguish missing evidence from declared zero-count evidence when reviewing imports.
+- Context visibility: identity extraction, monitor bind extraction, empty-envelope suppression, and spec telemetry attachment remain covered by service contracts.
+
+## Entity Definition Document Field Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit as one draft-to-catalog flow; this slice only moves root/spec field fallback, object-map shaping, and api-version alias resolution behind a small document-field boundary.
+- Density: do not add import previews, schema banners, fallback chips, migration notices, or helper copy while preserving compact definition evidence.
+- Anti-AI-slop: do not infer api versions, spec maps, telemetry blocks, integrations, extensions, or HertzBeat evidence from routes, titles, or template names; only declared document fields should drive canonicalization.
+- Operator workflow clarity: apiVersion/schema-version/schema_version precedence and root-vs-spec fallback should remain deterministic evidence operators can trace back to the submitted definition.
+- Context visibility: object-map key normalization, null-key suppression, first-non-null fallback, text trimming, and default api-version behavior remain covered by service contracts.
+
+## Entity Definition Extension Normalizer Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit, spec, telemetry, relation, integration, extension, and HertzBeat evidence in the same draft-to-catalog flow; this slice only moves object-node add-on parsing behind a narrow boundary.
+- Density: do not add integration cards, extension previews, add-on badges, helper copy, or route banners while preserving compact declared add-on evidence.
+- Anti-AI-slop: do not synthesize integrations, extensions, providers, saved views, or plugin hints from app names, routes, labels, or template context; only declared definition object nodes should be retained.
+- Operator workflow clarity: root/spec fallback for `integrations` and `extensions` should remain deterministic import evidence operators can trace back to the definition document.
+- Context visibility: integrations, extensions, root-vs-spec fallback precedence, non-object suppression, and string-key normalization remain covered by service contracts.
+
+## Entity Definition Spec Normalizer Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit, metadata, telemetry, relation, and HertzBeat evidence blocks in the same draft-to-catalog flow; this slice only moves spec profile field normalization behind a narrow boundary.
+- Density: do not add spec preview panels, component badges, API interface cards, language chips, or helper copy while preserving compact declared spec evidence.
+- Anti-AI-slop: do not synthesize systems, components, owners, languages, API schemas, or lifecycle tiers from route context or template names; only declared definition fields should shape the spec profile.
+- Operator workflow clarity: source, owner, environment, system/component, interface, and language fields should stay deterministic import evidence operators can trace back to the definition document.
+- Context visibility: root/spec fallback aliases, legacy `componentOf` system derivation, API interface schema/file refs, language lists, and manual source defaults remain covered by service contracts.
+
+## Entity Definition Metadata Normalizer Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit as a single draft-to-catalog flow; this slice only moves metadata, owner, contact, link, label, tag, and runbook normalization behind a narrow boundary.
+- Density: do not add owner panels, contact cards, metadata preview rails, compatibility badges, or helper copy while preserving compact definition metadata evidence.
+- Anti-AI-slop: do not synthesize owners, labels, contacts, runbooks, repository context, or team aliases from surrounding route or template hints; only declared definition fields should shape metadata.
+- Operator workflow clarity: labels, tags, contacts, owner refs, runbook links, and display fields should stay deterministic import evidence operators can trace back to the definition document.
+- Context visibility: metadata/spec/root fallbacks, root label/tag legacy fallback, runbook link promotion, contact aliases, and additional owner refs remain covered by service contracts.
+
+## Old Monitor Export Authorization Boundary - 2026-05-10
+
+- Hierarchy: keep the monitor list, export action, export-all action, and existing response download path unchanged; this slice only tightens shipped route authorization templates for export routes.
+- Density: do not add UI warnings, duplicate export controls, or compatibility badges while preserving the existing monitor operations surface.
+- Anti-AI-slop: do not infer export permission from list visibility, selected rows, monitor labels, or broad read access; explicit admin-scoped route rules remain the evidence.
+- Operator workflow clarity: operators with admin rights keep the same export endpoints, while guest/list-only access stays limited to monitor visibility rather than config extraction.
+- Context visibility: `/api/monitors/export`, `/api/monitors/export/all`, root monitor list reads, wildcard monitor reads, and admin-only batch mutations remain visible in the route authorization matrix.
+
+## Entity Detail Observability Workspace Boundary - 2026-05-10
+
+- Hierarchy: keep entity detail status, evidence summary, monitor/log/trace handoffs, noise-control summary, and activity timeline in their existing positions; this slice only moves request-workspace lookup out of the detail assembly orchestrator.
+- Density: do not add workspace chips, banners, extra evidence rails, or explanatory copy while preserving request-scoped status, silence/inhibit, and activity evidence.
+- Anti-AI-slop: do not synthesize entity health, alert suppression, monitor evidence, or activity records from route context; each panel remains backed by its existing query/read-model boundary.
+- Operator workflow clarity: entity detail should remain a single evidence bundle, with workspace scoping resolved where status, noise-control, and activity evidence are actually read.
+- Context visibility: entity id, owner, monitor bindings, active alerts, response handoffs, and request workspace scope stay visible through existing data-backed DTOs and routes.
+
+## Monitor Label Equals Filter Compatibility Boundary - 2026-05-10
+
+- Hierarchy: keep the monitor list filters, label rail handoff, entity-return context, and dense table unchanged; this slice only normalizes backend label query parsing for existing route parameters.
+- Density: do not add visible helper copy, duplicate label chips, or migration banners while supporting both `key:value` and `key=value` label filters.
+- Anti-AI-slop: label filtering must remain explicit and data-backed; do not infer monitor labels from entity names, owners, search text, or surrounding route context.
+- Operator workflow clarity: label links coming from entity, label-management, or legacy monitor surfaces should narrow the same monitor list instead of silently falling back to a broad text match.
+- Context visibility: app/search/status/page/entity/return route state stays unchanged, and the backend continues matching persisted monitor labels as JSON key/value evidence.
+
+## Old Monitor Legacy Comma Id Compatibility Boundary - 2026-05-10
+
+- Hierarchy: keep the monitor list, selected-row batch actions, manage toggles, and export flow unchanged; this slice only preserves legacy singular `id` query lists for old monitor batch callers.
+- Density: do not add compatibility notices, explanatory labels, or alternate controls while accepting comma-separated legacy ids behind the existing API contract.
+- Anti-AI-slop: do not infer monitor selections from names, hosts, filters, or table state; explicit numeric ids remain the only compatibility source.
+- Operator workflow clarity: old clients that submit selected monitors as `id=1,2` should reach the same service path as newer `ids=1,2` callers.
+- Context visibility: monitor ids, search/app filters, manage/delete/export intent, and response handling stay owned by the current controller/service boundary.
+
+## Old Monitor Update Query-Id Compatibility Boundary - 2026-05-10
+
+- Hierarchy: keep the existing monitor edit form, detect action, save action, and monitor list return flow unchanged; this slice only preserves the legacy `PUT /api/monitor?id=...` API entry.
+- Density: do not add compatibility banners, helper text, duplicate edit controls, or route explanation chrome while maintaining the backend alias.
+- Anti-AI-slop: do not infer monitor identity from labels, names, list filters, or route context; the explicit query `id` remains the authoritative legacy update target.
+- Operator workflow clarity: old singular monitor callers using query id should reach the same validation and modify service path as the existing root body update and path-id alias.
+- Context visibility: monitor id, app, instance, params, collector, dashboard payload, and validation result stay owned by the existing monitor DTO/service contract.
+
+## ClientWorkbench Loading Copy Boundary - 2026-05-09
+
+- Hierarchy: keep `ClientWorkbench` as the shared pending/error/data shell for route islands; this slice only aligns its default pending body copy with the global workbench loading message.
+- Density: preserve the single spinner, one title line, and one body line without adding progress bars, skeleton panels, route badges, or explanatory onboarding text.
+- Anti-AI-slop: do not imply backend readiness, ingestion progress, cache freshness, AI preparation, or successful resource discovery while a route island is still loading.
+- Operator workflow clarity: routes with domain-specific loading copy remain authoritative, and routes without custom copy should use the same localized workbench-loading body as global route loading.
+- Context visibility: cache keys, settled TTL forwarding, auth redirect handling, error rendering, and caller-provided loading title/copy remain unchanged.
+
+## Shell Sidebar Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the global sidebar section order, item grouping, active-route semantics, and icon mapping unchanged; this slice only moves the sidebar regression to shared locale helper messages.
+- Density: preserve the compact navigation rail rhythm without adding section descriptions, route badges, workflow hints, or new product copy.
+- Anti-AI-slop: do not infer ingestion readiness, alert state, topology availability, setup progress, or tenant posture from translated navigation labels.
+- Operator workflow clarity: sidebar labels should resolve through the same locale bundle as runtime so operators see stable route names across AppFrame, shell tests, and parity smoke.
+- Context visibility: ingestion, objects, observability, alerting, dashboards, settings, hidden incident/action/status routes, and active alert-setting ancestry remain covered without changing destinations.
+
+## AppFrame Setup Summary Helper Boundary - 2026-05-09
+
+- Hierarchy: keep setup progress as a compact first-screen header utility beside the existing global actions; this slice only moves setup-summary regression copy onto shared helper messages.
+- Density: preserve short progress assertions for the baseline percent and empty live state without adding setup rails, onboarding cards, or extra header prose.
+- Anti-AI-slop: do not invent platform readiness, setup health, AI assistance, live ingestion, or resource availability while replacing local test translators.
+- Operator workflow clarity: route-specific setup baselines should resolve through the same runtime catalog operators see, so progress text cannot drift behind a private `key:percent` test formatter.
+- Context visibility: overview, dashboard, entity creation/import/discovery/definition, log stream, OTLP intake, and dark-ops compatibility baseline routes remain covered by the same AppFrame source contract.
+
+## Entity List Surface Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the object catalog header, metrics strip, search toolbar, dense table, health affordance, and row actions unchanged; only move the regression test copy to the shared helper catalog.
+- Density: preserve the compact full-width catalog rhythm without adding right rails, owner panels, explanatory helper copy, or extra action groups.
+- Anti-AI-slop: do not invent entity health, ownership completeness, alert evidence, monitor coverage, relation readiness, or fake list results while replacing the test translator.
+- Operator workflow clarity: create, discovery, import, refresh, reset, owner, log, and trace handoffs remain the same operator actions and route destinations.
+- Context visibility: search/type/status filters, row health, owner fallback, evidence counts, range copy, and row next-action labels remain visible through locale-owned messages.
+
+## Workspace Navigation Label Helper Boundary - 2026-05-09
+
+- Hierarchy: keep entity, OTLP intake, metrics, monitor, log, and trace workspace tabs in the existing order and route positions; only make the label regression use shared runtime/helper messages.
+- Density: preserve the compact tab-strip rhythm without adding route chips, extra labels, helper prose, or side navigation.
+- Anti-AI-slop: do not infer entity health, signal readiness, monitor status, trace availability, or ingestion state from localized navigation labels.
+- Operator workflow clarity: tabs remain handoffs between existing evidence workbenches and do not introduce new workflow states or fake destinations.
+- Context visibility: entity id/name, service namespace, trace/span filters, log view, metrics query, return path, and monitor context stay unchanged while labels resolve from locale-owned copy.
+
+## Monitor Manage Query Owner Boundary - 2026-05-09
+
+- Hierarchy: keep the monitor center title, summary metrics, filter row, dense monitor list, selected monitor rail, label rail, import/export controls, and entity-return action unchanged; only route query parsing and display-label canonicalization move out of the client island.
+- Density: preserve the compact monitor operations surface without adding route-state chips, entity banners, extra filters, or explanatory helper copy.
+- Anti-AI-slop: do not invent monitor health, scrape freshness, entity linkage, collection status, alert state, or fake fallback results while normalizing URL filters.
+- Operator workflow clarity: entity-origin monitor handoffs still default to down monitors until status is explicit, and unsafe return URLs still resolve to internal entity routes.
+- Context visibility: search/app/labels/status/page filters, entity id/name, return route, cache key, fallback-to-all behavior, and mutation reload state remain visible and unchanged.
+
+## Entity Detail Inherited Context Query Boundary - 2026-05-09
+
+- Hierarchy: keep the entity detail header, compact counts, context panel, related-signal panel, next-action panel, drilldown entries, and destructive-action flow unchanged; only inherited route context parsing moves out of the client island.
+- Density: preserve the full-width detail workbench rhythm without adding context chips, banners, extra filter rows, or route-state explanatory copy.
+- Anti-AI-slop: do not infer health, alert evidence, monitor coverage, topology readiness, trace linkage, or fake status from query parameters.
+- Operator workflow clarity: inherited monitor/time/source context remains an input to the existing detail evidence and handoff rows, supplied by the server wrapper before render.
+- Context visibility: monitor id/name/app/instance, time range, trace/span, source, collector, template, cache key, refresh invalidation, and delete navigation remain visible and unchanged.
+
+## Entity New Telemetry Seed Query Boundary - 2026-05-09
+
+- Hierarchy: keep the entity editor shell, starter draft, catalog suggestions, form sections, preview, and save controls unchanged; only the telemetry seed query handoff moves out of the client island.
+- Density: preserve the existing authoring workflow without adding seed banners, handoff badges, or explanatory route-state panels.
+- Anti-AI-slop: do not invent monitor details, entity identities, telemetry readiness, catalog suggestions, or fake draft content while normalizing the seed query.
+- Operator workflow clarity: telemetry handoff still seeds from `source=telemetry` plus `monitorId`, and incomplete handoffs still fall back to the manual draft.
+- Context visibility: editor mode, draft source, monitor bind evidence, catalog suggestions, cache key, and save/preview flow remain visible and unchanged.
+
+## Entity Catalog Query Owner Boundary - 2026-05-09
+
+- Hierarchy: keep the entity catalog header, compact metrics, search row, dense table, row actions, and evidence handoffs unchanged; only route query ownership moves out of the visible client island.
+- Density: preserve the current filtered-list rhythm without adding route-state badges, extra filter panels, or explanatory helper copy.
+- Anti-AI-slop: do not infer entity health, alert counts, monitor bindings, topology readiness, or fake query results while normalizing URL filters.
+- Operator workflow clarity: search, type, and status filters remain the same operator controls, with initial route state supplied by the server wrapper before the island renders.
+- Context visibility: active query state, cache key, refresh invalidation, row evidence counts, and entity/log/trace next-step links remain visible and unchanged.
+
+## Log Manage Query Placeholder Boundary - 2026-05-09
+
+- Hierarchy: keep the log workbench header, stream/history switch, query row, trend band, dense list, selected evidence, entity context, and cross-signal handoffs unchanged; only the service-query example placeholder moves behind runtime/helper ownership.
+- Density: preserve the compact query-row rhythm without adding helper text, query builders, syntax panels, saved views, or extra filter chrome.
+- Anti-AI-slop: do not invent log availability, parser health, collector readiness, trace coverage, alert evidence, or fake query progress from a placeholder example.
+- Operator workflow clarity: the placeholder remains a small service-attribute query example and does not change search semantics, route state, stream/list modes, or run/reset actions.
+- Context visibility: current query state, time context, cache key, stream/history mode, selected log evidence, and log-to-trace/entity/alert handoffs remain unchanged.
+
+## Log Manage Query Owner Boundary - 2026-05-09
+
+- Hierarchy: keep the log workbench header, live/history switch, query row, trend band, dense list, selected evidence drawer, entity context, and cross-signal handoffs in the existing client island; only raw route parsing moves to the log query owner.
+- Density: preserve the compact workbench rhythm without adding route chips, banners, side rails, query explanations, or synthetic stream status blocks.
+- Anti-AI-slop: do not invent log freshness, parser health, trace coverage, entity readiness, alert causality, or backend availability while normalizing URL state.
+- Operator workflow clarity: initial query, live/history mode, route context, display-label cleanup, run/reset/view/time-control navigation, and evidence handoffs keep the same behavior with typed route state supplied before render.
+- Context visibility: search/content filters, trace/span, severity, entity/service/environment/source, return link, absolute time bounds, cache key, and EventSource routing remain visible to the existing log workbench tests.
+
+## Trace Manage Route Loading Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the trace workbench header, query row, trend band, trace list, detail drawer, entity context, and cross-signal handoffs unchanged; only the route loading copy is asserted through shared runtime/helper ownership.
+- Density: preserve the compact client-workbench loading boundary without adding trace skeleton tables, service health banners, latency summaries, or extra navigation chrome.
+- Anti-AI-slop: do not invent trace availability, span readiness, entity attribution, log linkage, alert evidence, or fake query progress while wiring loading-copy parity.
+- Operator workflow clarity: the route still loads trace overview and list data from the current trace/service/time filters before rendering the existing trace workbench.
+- Context visibility: query state, time context, cache key, detail drawer, attribution diagnostics, and cross-signal return links remain unchanged.
+
+## Public Status Route Loading Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the public status shell, organization brand, component history, incident feed, and locale controls unchanged; only the route loading copy is asserted through shared runtime/helper ownership.
+- Density: preserve the compact client-workbench loading boundary without adding public uptime summaries, incident skeleton feeds, component health rails, or extra navigation chrome.
+- Anti-AI-slop: do not invent component readiness, incident freshness, uptime proof, organization health, or fake refresh progress while wiring loading-copy parity.
+- Operator workflow clarity: the route still loads public org, component, and incident data before the existing public status surface renders for visitors.
+- Context visibility: server wrapper, client island, cache key, incident year query, reload token, and public status shell handoffs remain unchanged.
+
+## Public Status Page Helper Regression Boundary - 2026-05-09
+
+- Hierarchy: keep the public status route regression on the existing status shell, organization brand block, component/incident counts, footer attribution, refresh action, and client-workbench loader; only align test-owned labels with the shared helper catalog.
+- Density: preserve the compact public board and route loading marker without adding summary rails, uptime proof panels, banners, skeleton tables, or extra date captions.
+- Anti-AI-slop: do not invent incidents, component health, uptime, hosted status-page packaging, SaaS positioning, or validation progress while replacing page-local label assumptions.
+- Operator workflow clarity: the route regression should assert helper-owned loading, powered-by, and refresh labels while leaving the controller-backed load path and public status shell props unchanged.
+- Context visibility: organization payload, component/incident counts, public mode, settled cache markers, refresh control, and controller call remain data-backed and unchanged.
+
+## Setting Status Route Loading Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the status settings surface, organization profile, component list, incident list, and public status handoff unchanged; only the route loading copy is asserted through shared runtime/helper ownership.
+- Density: preserve the compact client-workbench loading boundary without adding status summaries, incident health banners, component skeleton tables, or extra navigation chrome.
+- Anti-AI-slop: do not invent public uptime, incident freshness, component readiness, delivery state, or fake validation progress while wiring loading-copy parity.
+- Operator workflow clarity: the route still loads org, component, and incident management data, then lets operators edit status page configuration through the existing surface.
+- Context visibility: server wrapper, client island, cache key, refresh invalidation, incident query state, and public status link remain unchanged.
+
+## Setting Status Page Helper Regression Boundary - 2026-05-09
+
+- Hierarchy: keep the status settings route regression on the existing client-workbench loader and cold status settings surface; only make the mocked provider's locale and loading copy ownership explicit.
+- Density: preserve the compact admin layout marker without adding summary rails, incident skeletons, component health panels, route banners, or nested cards.
+- Anti-AI-slop: do not invent public uptime, incident freshness, publishing state, component readiness, tenant posture, or validation progress while replacing implicit test helper assumptions.
+- Operator workflow clarity: the regression should assert helper-owned status-settings loading copy while the controller-backed org/component/incident load path and public status handoff stay unchanged.
+- Context visibility: server wrapper, client island, status settings surface marker, cache/loading marker, incident query defaults, and public `/status` link remain data-backed and unchanged.
+
+## Setting Server Route Loading Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the settings console title, email/SMS summary rows, configure dialogs, and save feedback unchanged; only the route loading copy is asserted through shared runtime/helper ownership.
+- Density: preserve the compact client-workbench loading boundary without adding message-server health banners, delivery-state panels, provider summaries, or nested cards.
+- Anti-AI-slop: do not invent notification delivery status, provider readiness, queue depth, tenant posture, or fake validation progress while wiring loading-copy parity.
+- Operator workflow clarity: the route still loads `/config/email` and `/config/sms`, then lets operators configure mail and SMS through the existing summary/dialog flow.
+- Context visibility: server wrapper, client island, cache key, email/SMS save invalidation, provider fields, and settings-console context remain unchanged.
+
+## Monitor Manage Route Loading Boundary - 2026-05-09
+
+- Hierarchy: keep monitor management loading feedback in the existing `ClientWorkbench` route island before the dense monitor workbench renders.
+- Density: add only runtime-owned loading-copy parity; do not add monitor health banners, summary rails, skeleton tables, or extra navigation chrome.
+- Anti-AI-slop: do not invent monitor readiness, collection health, entity coverage, alert state, or topology evidence while fixing loading-copy ownership.
+- Operator workflow clarity: the monitor route still loads the filtered monitor list and preserves entity-return, new/edit/detail, import/export, and mutation flows.
+- Context visibility: search filters, entity context, cache key, reload invalidation, selected monitor, and settled TTL remain unchanged.
+
+## Log Manage Route Loading Boundary - 2026-05-09
+
+- Hierarchy: keep log workbench loading feedback in the existing `ClientWorkbench` route island before the live/history log surface renders.
+- Density: add only runtime-owned loading-copy parity; do not add skeleton tables, stream status banners, route chrome, or extra query controls.
+- Anti-AI-slop: do not invent log availability, stream health, collector status, ingestion freshness, trace coverage, or alert evidence while fixing copy ownership.
+- Operator workflow clarity: the log route still loads overview, list, trend, coverage, and query state from the current route context.
+- Context visibility: live/history mode, entity/time/trace context, cache key, settled TTL, and log workbench handoffs remain unchanged.
+
+## Safe Automation Next-Hop Navigation Copy Boundary - 2026-05-09
+
+- Hierarchy: keep overview, entity, and monitor next hops as the existing compact navigation affordances, without adding a secondary nav rail or action routing panel.
+- Density: preserve the current entry/action surface rhythm; only the visible next-hop labels move to locale-owned runtime copy.
+- Anti-AI-slop: do not infer route availability, entity readiness, monitor health, execution readiness, adapter status, or approval progress from localized next-hop labels.
+- Operator workflow clarity: next-hop labels are plain navigation targets and do not introduce a new action workflow state.
+- Context visibility: hrefs, variants, entity/monitor destinations, overview destination, snapshot data, and adapter-pending boundary remain unchanged.
+
+## Safe Automation Surface Chrome Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the action surface kicker, handoff rows, checklist rows, and lane summaries in their existing compact positions, without adding a navigation rail, approval panel, or execution status summary.
+- Density: preserve the catalog/run/approval/checklist/handoff rhythm; only the visible chrome copy moves to locale-owned Chinese/English messages.
+- Anti-AI-slop: do not infer live execution state, adapter readiness, queue freshness, approval routing, action success, or notification delivery from localized surface labels.
+- Operator workflow clarity: surface, handoff, checklist, and lane labels describe the existing roadmap-demo boundary and do not introduce a new workflow state.
+- Context visibility: catalog entries, run rows, approval rows, snapshot markers, handoff destinations, and adapter-pending boundary remain unchanged.
+
+## Safe Automation Snapshot Status Label Boundary - 2026-05-09
+
+- Hierarchy: keep run and approval statuses inside the existing compact snapshot meta lines, without adding live execution badges, approval rails, or incident-workflow panels.
+- Density: preserve the catalog card, run row, approval row, and snapshot marker rhythm so the static action plane stays scannable.
+- Anti-AI-slop: do not infer live execution state, approval routing, adapter readiness, queue freshness, action success, or notification delivery from localized status labels.
+- Operator workflow clarity: status enum values remain data while English and Chinese display labels are locale-owned runtime copy.
+- Context visibility: run id, approval id, started-at time, target, actor, owner, evidence, snapshot marker, and adapter-pending boundary remain unchanged.
+
+## Safe Automation Suggested Action Risk Label Boundary - 2026-05-09
+
+- Hierarchy: keep risk in the existing tiny suggested-action meta line beside the catalog label, without adding a risk legend, approval rail, or execution-status chip.
+- Density: preserve the compact three-card suggestion grid and the meta/title/copy/evidence/action/posture stack.
+- Anti-AI-slop: do not infer execution readiness, approval state, live run history, queue health, automation success, or adapter availability from localized risk labels.
+- Operator workflow clarity: English and Chinese labels are locale-owned runtime copy; risk enum values remain data for governance and tests.
+- Context visibility: catalog label, action id, risk enum, source handoff, evidence URL, confirmation mode, and adapter-pending boundary remain unchanged.
+
+## Safe Automation Suggested Action Target Label Boundary - 2026-05-09
+
+- Hierarchy: keep the suggested action target in the existing title line, without adding an entity-id badge, resolver panel, or catalog drawer.
+- Density: preserve the tiny meta, title, copy, evidence, evidence link, disabled human-confirm button, and posture line in each suggested action card.
+- Anti-AI-slop: do not infer entity display names, service readiness, automation safety, execution availability, approval state, or evidence completeness from an entity id route token.
+- Operator workflow clarity: serviceName and entityName remain the preferred human labels; entityId-only handoffs render localized fallback copy that preserves the raw id for diagnosis.
+- Context visibility: target label, source, signal, trace id, alert group id, evidence URL params, risk, confirmation mode, and adapter-pending boundary remain unchanged beyond the visible fallback label.
+
+## Safe Automation Suggested Action Evidence Value Boundary - 2026-05-09
+
+- Hierarchy: keep source and signal evidence inside the existing compact evidence line in each suggested action card, without adding a source taxonomy panel or signal legend.
+- Density: preserve the tiny meta, title, copy, evidence, evidence link, disabled human-confirm button, and posture line.
+- Anti-AI-slop: do not infer evidence completeness, source trust, signal health, action readiness, approval state, adapter availability, or execution history while replacing visible route slugs.
+- Operator workflow clarity: known source/signal values render operator labels; unknown non-empty values render localized fallback copy that preserves the original route token.
+- Context visibility: source, signal, trace id, alert group id, return URL, evidence URL params, action id, risk, confirmation mode, and adapter-pending boundary remain unchanged.
+
+## Safe Automation Suggested Action Catalog Meta Boundary - 2026-05-09
+
+- Hierarchy: keep suggested action catalog context in the existing tiny meta line above each suggestion title, without adding a catalog drawer or execution panel.
+- Density: preserve the three-card suggestion grid, evidence link, disabled human-confirm button, and posture line.
+- Anti-AI-slop: do not invent action readiness, execution history, approval state, queue health, automation success, or adapter availability while replacing visible catalog slugs.
+- Operator workflow clarity: display the localized catalog name beside the localized risk label; retain catalog ids as data, not as the visible operator label.
+- Context visibility: source handoff, alert evidence URL, confirmation mode, risk value, action id, evidence copy, and adapter-pending boundary remain unchanged.
+
+## Entity Definition Backend Feedback Boundary - 2026-05-09
+
+- Hierarchy: keep unknown backend feedback inside the existing inline entity definition message slot, without adding a banner, retry rail, or diagnostic drawer.
+- Density: preserve the compact editor/action/context panel rhythm in both import and definition modes.
+- Anti-AI-slop: do not infer backend health, validation quality, parser readiness, telemetry freshness, or entity existence from an unknown backend message.
+- Operator workflow clarity: known backend messages keep their existing localized mapping; unknown non-empty messages get localized fallback copy while preserving the original server reason.
+- Context visibility: message tone, load-error placement, entity id, editor content, format, preview/import/save actions, templates, activities, and entry links remain unchanged.
+
+## Entity Definition Format Option Boundary - 2026-05-09
+
+- Hierarchy: keep format option labels inside the existing compact editor toolbar select, without adding helper copy or a separate format panel.
+- Density: preserve the current toolbar rhythm, action row, editor height, template panel, activity panel, and import/definition modes.
+- Anti-AI-slop: do not infer parser support, import readiness, backend availability, telemetry freshness, or template validity from the visible option labels.
+- Operator workflow clarity: format options remain the same YAML/JSON/cURL choices, with their display labels resolved from runtime messages.
+- Context visibility: selected format, editor language, starter draft, placeholders, preview/import/save actions, and surrounding entity definition evidence stay unchanged.
+
+## Entity Import Unknown Format Boundary - 2026-05-09
+
+- Hierarchy: keep definition format labels in the existing import metric and row meta positions, without adding parser warnings or format guidance panels.
+- Density: preserve the current import metric, template row, activity row, preview, and queue rhythm.
+- Anti-AI-slop: do not infer parser support, import success, backend readiness, telemetry freshness, or template trust from an unknown format token.
+- Operator workflow clarity: known YAML/JSON/cURL values keep their compact labels, while unknown non-empty backend tokens render localized fallback copy that preserves the original value.
+- Context visibility: import format, activity status, template source, activity detail, preview validation, and attribution evidence remain owned by the existing import workbench model.
+
+## Entity Import Unknown Kind Source Boundary - 2026-05-09
+
+- Hierarchy: keep entity type and source labels inside the existing import preview row, without adding a source taxonomy panel or import warning rail.
+- Density: preserve the current preview row title, subtitle, type, source, telemetry, validation, and attribution evidence rhythm.
+- Anti-AI-slop: do not infer entity classification, source trust, telemetry readiness, ownership quality, or backend freshness from unknown type/source tokens.
+- Operator workflow clarity: known values keep their current labels, while unknown non-empty backend tokens render localized fallback copy that preserves the original value.
+- Context visibility: entity name, display name, type, source, telemetry binding, gaps, attribution rows, and import validation remain owned by the existing import preview model.
+
+## Entity Import Unknown Activity Boundary - 2026-05-09
+
+- Hierarchy: keep unknown activity status and template source tokens inside the existing import activity/template row meta, without adding warning panels or status explanations.
+- Density: preserve the current import preview, template list, activity list, queue groups, and compact meta rhythm.
+- Anti-AI-slop: do not infer import success, template trust, telemetry freshness, ownership quality, or backend readiness from an unknown token.
+- Operator workflow clarity: known statuses and sources stay localized, unknown non-empty tokens get localized fallback copy that preserves the original backend value.
+- Context visibility: import format, template source, activity status, activity detail, validation labels, and attribution evidence remain owned by the existing entity import workbench.
+
+## Entity Discovery Governance Card Context Boundary - 2026-05-09
+
+- Hierarchy: keep monitor meta, draft subtitle, and candidate context as compact lines inside existing discovery governance cards; do not add a context panel or missing-field explanation.
+- Density: preserve the existing card rhythm, candidate label, completeness, risk, next action, and action row.
+- Anti-AI-slop: do not infer ownership quality, entity readiness, merge correctness, service health, backend freshness, or remediation from missing context fields.
+- Operator workflow clarity: real monitor id/app/instance and owner/system/environment values stay visible, while absent values use the localized empty marker already used by discovery rows.
+- Context visibility: monitor id, app, instance, owner, system, environment, candidate label, state, and handoff links remain unchanged.
+
+## Entity Discovery Service Name Boundary - 2026-05-09
+
+- Hierarchy: keep fallback service names inside the existing discovery table attribution copy and governance card title/candidate label; do not add a naming panel or merge explanation.
+- Density: preserve the compact discovery table, candidate card, draft title, completeness, and action rhythm.
+- Anti-AI-slop: do not infer entity confidence, ownership quality, service health, backend freshness, merge correctness, or remediation from a fallback service name.
+- Operator workflow clarity: existing preset names remain authoritative, while fallback service names localize only the product suffix and keep the raw monitor app/name visible.
+- Context visibility: monitor id, app, instance, candidate action, draft title, attribution state, owner/system/environment, and return links remain unchanged.
+
+## Entity Discovery Unknown Status Boundary - 2026-05-09
+
+- Hierarchy: keep unknown monitor status inside the existing discovery table status chip; do not add an explanation panel or secondary health row.
+- Density: preserve the compact clue, instance, status, owner, system, environment, attribution, and action columns.
+- Anti-AI-slop: do not infer monitor health, ownership quality, entity confidence, backend freshness, or remediation from an unknown status token.
+- Operator workflow clarity: known monitor statuses keep their localized labels, while unknown tokens use a localized fallback that still exposes the original value for diagnosis.
+- Context visibility: monitor id, app, instance, attribution state, candidate action, and governance context remain unchanged.
+
+## Alert Evidence Source Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep unknown alert evidence source copy inside the existing inherited source context row; do not add a source warning panel or routing explanation.
+- Density: preserve the compact time, monitor, source, and trace evidence row stack so alert evidence remains quick to scan.
+- Anti-AI-slop: do not infer source health, topology confidence, collector state, ingestion freshness, or alert causality from an unknown source route token.
+- Operator workflow clarity: known sources keep their localized labels, while unknown source tokens use a localized fallback that still exposes the original token.
+- Context visibility: raw source, time, monitor, trace/span, return context, and evidence handoffs remain owned by the existing alert inherited-context pipeline.
+
+## Alert Entity Context Severity Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep routed alert severity inside the existing compact entity-context summary strip; do not add a severity explanation card or secondary alert health panel.
+- Density: preserve the inline status, severity, and search summary tokens so the alert center header remains quick to scan.
+- Anti-AI-slop: do not invent severity normalization, incident state, health, ownership, or backend freshness for unknown severity route tokens.
+- Operator workflow clarity: known severities keep their localized labels, while unknown routed severities use a localized fallback that still exposes the original token.
+- Context visibility: entity id/name, return link, status, search, and raw route context remain owned by the existing alert entity context pipeline.
+
+## Passport Login Session Copy Locale Boundary - 2026-05-08
+
+- Hierarchy: keep the login session notice as the existing compact post-login context line, not a new security banner or onboarding panel.
+- Density: preserve the current login card rhythm and one-line notice without adding helper rails, token diagrams, or extra form states.
+- Anti-AI-slop: correct locale ownership without claiming HttpOnly/session redesign or completed token-boundary work ahead of Milestone 4.
+- Operator workflow clarity: the notice remains a simple statement that the workspace session resumes and token refresh may be attempted.
+- Context visibility: username/password flow, default-password warning, redirect handling, and session notice remain separate and readable.
+
+## Passport Login Redirect Query Boundary - 2026-05-09
+
+- Hierarchy: keep the Angular-style passport shell, credential card, hidden field labels, remember checkbox, default-password warning, and submit flow unchanged; only the redirect query owner moves from the form to the route/controller.
+- Density: preserve the single compact auth panel without adding redirect badges, route explanation text, guard banners, or helper rows.
+- Anti-AI-slop: do not claim new session security, HttpOnly cookies, MFA, tenant policy, workspace identity, or token-boundary completion while normalizing the post-login redirect.
+- Operator workflow clarity: unsafe or auth-loop redirects still fall back to `/overview`, and valid internal targets are supplied to the form before submit without reading raw URL state in the client card.
+- Context visibility: login alias cleanup, post-login redirect target, token persistence, bootstrap call, password visibility, locale controls, and passport shell markers remain visible to existing auth tests.
+
+## Exception Recovery English Locale Copy Boundary - 2026-05-08
+
+- Hierarchy: keep recovery rail rows as compact handoff options below the existing exception diagnostics, not a new incident or runbook panel.
+- Density: preserve the three-row overview/logs/traces rhythm and button labels without adding helper prose, badges, or new cards.
+- Anti-AI-slop: correct the English locale copy without inventing recovery automation, fake backend status, or SaaS-style troubleshooting language.
+- Operator workflow clarity: each row remains a simple handoff to the existing overview, log, or trace workspace.
+- Context visibility: route targets, row titles, row copy, and action labels stay locale-owned while preserving current exception type context.
+- Helper parity update: keep exception chrome, filters, query controls, table labels, and recovery handoffs on the existing surface while using the shared test translator; do not add retry banners, fake exception health, backend status claims, or new recovery UI.
+
+## Exception Center Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the exception title, facts, filter rail, query bar, table, recovery rail, and route-specific boundary copy in the existing exception center surface; do not add recovery banners or a second shell.
+- Density: preserve the dense filter sidebar, one-line query controls, compact exception table, and three-row recovery rail.
+- Anti-AI-slop: do not invent backend health, exception freshness, RCA, incident state, retry automation, or SaaS-style troubleshooting copy while moving the regression onto shared helper messages.
+- Operator workflow clarity: focused exception center tests should exercise the same shared translator helper used by other surfaces instead of a page-local message map.
+- Context visibility: exception type, normalized copy, filter labels, recovery handoffs, and row detail links remain unchanged.
+
+## Incident Snapshot Checklist Meta Labels - 2026-05-08
+
+- Hierarchy: keep checklist meta as compact status text on the existing incident adapter checklist, not a new progress tracker or readiness panel.
+- Density: preserve the three-row checklist and short meta rhythm beside title/copy rows without adding badges, counters, or helper rails.
+- Anti-AI-slop: translate sample meta labels without implying live adapter readiness, workflow completion, or real drilldown delivery.
+- Operator workflow clarity: meta stays static snapshot display copy under the adapter boundary until incident workflow adapters land.
+- Context visibility: shell ready, adapter next, and drilldown reserved states remain visible while display labels become runtime-localized.
+
+## Incident Snapshot Model Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the incident domain-model regression on the existing title, metrics, cards, timeline, ownership, checklist, and handoff payloads; do not add an incident desk shell or adapter preview.
+- Density: preserve the compact domain-model assertions without snapshot dumps or extra helper-only rows.
+- Anti-AI-slop: do not invent live incident states, escalation policy, notification readiness, RCA, ownership schedules, or adapter health while moving the test onto shared messages.
+- Operator workflow clarity: model tests should use the same shared translator helper as runtime surfaces instead of a private incident-message map.
+- Context visibility: incident titles, owners, blast radius, timeline copy, ownership lanes, checklist meta, and navigation labels remain tied to the same model payload.
+
+## Incident Entry Chrome Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the incident kicker, title, subtitle, overview/entity handoffs, and shell explainer in the existing cold entry header and left panel; do not add incident summaries or a live response desk.
+- Density: preserve the compact header/action row, cold shell panel, checklist rail, and empty adapter state.
+- Anti-AI-slop: do not invent incident counts, paging state, escalation policy, on-call readiness, AI RCA, or adapter availability while cataloging entry chrome copy.
+- Operator workflow clarity: the entry chrome should resolve through shared runtime/helper catalogs so route and view-model regressions do not depend on page-local maps.
+- Context visibility: route wrapper, client island, cold-ops visual baseline, response checklist, empty adapter boundary, and overview/entity exits remain unchanged.
+
+## Incident Snapshot Title Boundary - 2026-05-08
+
+- Hierarchy: keep the incident surface title as the compact domain-model header consumed by the existing surface; do not add a second hero, status banner, or live incident desk.
+- Density: preserve the current facts, tags, incident cards, timeline, ownership lanes, and handoff rows without adding summary rails or decorative response copy.
+- Anti-AI-slop: localize the title without implying real incident adapters, notification routing, escalation policy, or completed closure workflow.
+- Operator workflow clarity: the title remains snapshot display copy under the adapter boundary while real incident response workflows stay in later milestones.
+- Context visibility: title, focus, tags, metrics, incidents, timeline, ownership, checklist, and next hops stay stable while the remaining source-local title becomes runtime-localized.
+
+## Incident Snapshot Ownership Labels - 2026-05-08
+
+- Hierarchy: keep ownership lanes as compact responder rows under the incident snapshot, not as a live on-call schedule, escalation tree, or notification router.
+- Density: preserve owner, queue, copy, and meta rhythm so the lanes remain scannable beside incident cards and timeline rows.
+- Anti-AI-slop: translate sample ownership copy without implying real paging, SLA timers, on-call rotation, or completed notification delivery.
+- Operator workflow clarity: ownership rows stay static adapter-boundary evidence until incident response ownership and escalation workflow lands.
+- Context visibility: checkout, observability, and edge responder context plus active handoff/review meta stay visible while display copy becomes runtime-localized.
+
+## Incident Snapshot Timeline Labels - 2026-05-08
+
+- Hierarchy: keep the response timeline as the compact three-row snapshot under the incident surface model; do not promote it into a live incident chronology or audit log.
+- Density: preserve short title, copy, and meta rhythm so timeline rows stay scannable beside incident cards and ownership lanes.
+- Anti-AI-slop: translate sample timeline text without implying real incident freshness, notification delivery, rollback execution, or completed incident adapters.
+- Operator workflow clarity: timeline rows remain static adapter-boundary context until incident response workflow lands.
+- Context visibility: mitigation, replay, escalation, alert evidence, trace/log handoff, and incident meta labels stay visible while row copy becomes runtime-localized.
+
+## Incident Snapshot Row Labels - 2026-05-08
+
+- Hierarchy: keep incident cards as compact snapshot rows under the incident surface model; do not introduce a live incident table, timeline expansion, or notification workflow.
+- Density: preserve title, service/owner copy, severity/blast-radius eyebrow, and opened-at meta rhythm without adding new status chips.
+- Anti-AI-slop: translate sample incident titles and row copy without implying real incident freshness, alert routing, or notification delivery.
+- Operator workflow clarity: incident rows remain static snapshot context until later workflow adapters land.
+- Context visibility: incident ids, severity, stage, opened-at timestamps, service, owner, and blast-radius evidence stay stable while display labels become runtime-localized.
+
+## Incident Snapshot Tag Labels - 2026-05-08
+
+- Hierarchy: keep incident tags as compact domain context for the shared Signals fact, not as a new incident taxonomy strip or notification workflow panel.
+- Density: preserve the existing Signals count rhythm so the incident entry remains aligned with other cold workbench entries.
+- Anti-AI-slop: translate tag labels without implying live incident ownership, notification routing, or response automation has landed.
+- Operator workflow clarity: tags stay high-level snapshot context; adapter and notification workflow truth remains explicit in later milestones.
+- Context visibility: workspace, focus, entry mode, Signals count, and incident tag data stay stable while tag copy becomes runtime-localized.
+
+## Incident Snapshot Metric Labels - 2026-05-08
+
+- Hierarchy: keep incident snapshot metrics as the compact count row owned by the incident domain model, without adding a new incident dashboard or status summary.
+- Density: preserve the four short metric labels and current count rhythm so they remain scan-friendly beside incident, timeline, and ownership rows.
+- Anti-AI-slop: translate labels without implying live incident adapters, fake incident freshness, or completed notification/ownership workflow.
+- Operator workflow clarity: metric labels remain snapshot display copy under the incident adapter boundary; real closure, routing, and notification workflow still belongs to later milestones.
+- Context visibility: open count, critical count, mitigating count, and ownership queue count stay visible while display labels become runtime-localized.
+
+## Safe Automation Surface Tag Labels - 2026-05-08
+
+- Hierarchy: keep automation tags as domain context for the existing Signals fact, not as a new badge row or action taxonomy panel.
+- Density: preserve the compact Signals count rhythm so the actions entry stays aligned with other workbench facts.
+- Anti-AI-slop: translate tag labels without implying live automation domains, execution readiness, or SaaS-style packaging.
+- Operator workflow clarity: tags remain high-level page context only; adapter boundary and suggested-action caveats continue to carry execution truth.
+- Context visibility: workspace, focus, entry mode, Signals count, and tag data stay stable while tag copy becomes runtime-localized.
+
+## Safe Automation Snapshot Title Boundary - 2026-05-08
+
+- Hierarchy: keep the automation surface title as the compact domain-model header consumed by the existing action control plane; do not add a second hero, approval dashboard, or live execution desk.
+- Density: preserve the current facts, tags, catalog cards, run history, approval rows, checklist, and handoff rows without adding summary rails or decorative automation copy.
+- Anti-AI-slop: localize the title without implying live execution adapters, real approval routing, workflow automation, or self-service actions have landed.
+- Operator workflow clarity: the title remains snapshot display copy under the adapter boundary while real safe automation workflows stay in later milestones.
+- Context visibility: title, focus, tags, metrics, catalog, runs, approvals, adapter-boundary caveat, checklist, and next hops stay stable while the remaining source-local title becomes runtime-localized.
+
+## Safe Automation Checklist Meta Labels - 2026-05-08
+
+- Hierarchy: keep checklist meta as compact status text on the existing action-catalog readiness checklist, not as execution readiness or a new approval tracker.
+- Density: preserve the three-row checklist and short meta rhythm beside title/copy rows without adding badges, counters, or helper rails.
+- Anti-AI-slop: translate sample meta labels without implying live execution adapters, completed workflow automation, or real context continuity.
+- Operator workflow clarity: meta stays static adapter-boundary display copy until automation catalog, run history, and approval adapters land.
+- Context visibility: entry ready, adapters next, and context continuity reserved states remain visible while display labels become runtime-localized.
+
+## Safe Automation Snapshot Catalog Recency Labels - 2026-05-08
+
+- Hierarchy: keep catalog card recency inside the existing risk/last-run/snapshot meta line, not as a new timeline or execution-history panel.
+- Density: preserve the short three-part meta rhythm so catalog cards remain compact beside run and approval snapshots.
+- Anti-AI-slop: translate the sample recency labels without implying live adapter freshness, execution telemetry, SLA age, or real automation state.
+- Operator workflow clarity: recency remains static snapshot display copy under the adapter boundary, separate from real run timestamps and approval state.
+- Context visibility: action ids, risk levels, posture, snapshot marker, and adapter caveat stay stable while visible last-run copy becomes locale-owned.
+
+## Safe Automation Snapshot Approval Labels - 2026-05-08
+
+- Hierarchy: keep approval summary, owner, evidence, status, id, and snapshot marker in the existing compact approval rows, without adding a live-approval rail or execution-adapter claim.
+- Density: preserve one-line approval copy and meta rhythm so pending and approved samples remain scannable beside catalog and run history.
+- Anti-AI-slop: avoid hardcoded English approval examples, fake approval truth, invented escalation state, or copied incident-workflow language while moving only display labels behind runtime messages.
+- Operator workflow clarity: localized approval rows must remain explicitly sample snapshot data under the adapter boundary, not workflow records from a live approval adapter.
+- Context visibility: approval ids, status enums, snapshot state, adapter caveat, and suggested-action handoffs stay stable while visible approval copy becomes locale-owned.
+
+## Safe Automation Adapter Roadmap Labels - 2026-05-08
+
+- Hierarchy: keep the adapter-boundary roadmap chips under the existing execution-boundary copy, not as a new action catalog or workflow section.
+- Density: preserve the current compact chip row and avoid adding helper rails, status blocks, or extra approval controls.
+- Anti-AI-slop: translate the roadmap-domain labels without pretending the automation workflow/app-builder/script adapters are live.
+- Operator workflow clarity: the panel must still read as an explicit adapter boundary with alert-context suggestions as handoff-only.
+- Context visibility: raw adapter ids can remain model/governance data, but visible chips should use runtime labels so operators are not shown implementation slugs.
+
+## Safe Automation Snapshot Metric Labels - 2026-05-08
+
+- Hierarchy: keep the action-domain snapshot metric row as compact summary metadata for the existing automation surface.
+- Density: preserve the four short metric labels without adding new charts, totals, readiness widgets, or execution status claims.
+- Anti-AI-slop: externalize metric labels while keeping catalog/runs/approvals explicitly marked as sample snapshots until adapters land.
+- Operator workflow clarity: metric labels should be translated display copy; counts still come from the static snapshot data and do not imply live execution state.
+- Context visibility: risk, approval, catalog, and recent-run counts stay visible as snapshot context while runtime labels avoid hardcoded English in the model.
+
+## Bulletin Center Tab Navigation Aria Copy Boundary - 2026-05-07
+
+- Hierarchy: keep bulletin tabs directly above the metrics desk inside the existing bulletin center panel.
+- Density: preserve the current compact tab strip and toolbar layout without adding secondary nav, helper text, or summary rails.
+- Anti-AI-slop: do not invent bulletin health, delivery status, or copied status-page navigation while moving only the tab-strip accessibility label behind runtime messages.
+- Operator workflow clarity: tab selection, selected bulletin id, refresh countdown, metrics reload, and delete/edit actions remain unchanged.
+- Context visibility: the tab strip continues to expose the current bulletin list context to assistive navigation without changing visible row or metrics content.
+
+## Bulletin Center Surface Helper Regression Boundary - 2026-05-09
+
+- Hierarchy: keep the bulletin fact strip, toolbar, board tabs, metrics table, manage dialog, and delete flows in their current positions; only align the component regression with shared zh-CN helper copy.
+- Density: preserve the compact list-and-metrics operations layout without adding status summaries, helper rails, nested cards, or extra board navigation.
+- Anti-AI-slop: do not invent bulletin health, delivery readiness, monitor reachability, metrics freshness, or dashboard availability while replacing English default test copy.
+- Operator workflow clarity: refresh, new-board creation, tab navigation, selected-board evidence, and metrics reload should resolve through the same runtime/helper catalog operators see.
+- Context visibility: board names, active selection, metrics table handoff, dialog wiring, batch delete controls, and reload callback behavior remain unchanged.
+
+## Bulletin Center Row Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep bulletin list rows, selected-bulletin summary, metrics table, toolbar actions, and delete modal in the existing bulletin center surface.
+- Density: preserve terse inline fallbacks without adding status cards, missing-field banners, or secondary evidence rails.
+- Anti-AI-slop: do not invent bulletin app names, creators, monitor ownership, delivery health, or status evidence while moving absent facts into the shared fallback.
+- Operator workflow clarity: payload-provided bulletin names, apps, creators, monitor ids, and metric JSON remain authoritative; only blank row/current facts use localized fallback copy.
+- Context visibility: refresh, create/edit/delete actions, selected-bulletin context, metrics table, dialog state, and controller-backed data remain unchanged.
+
+## Alert Notice Receiver Webhook Auth Option Copy Boundary - 2026-05-07
+
+- Hierarchy: keep webhook auth options inside the existing aligned receiver form row for `hookAuthType`.
+- Density: preserve the compact native option list and current cold select wrapper without adding helper rails, descriptions, or a second auth selector.
+- Anti-AI-slop: avoid invented webhook security behavior, fake token validation, or copied notification SaaS copy while moving only option labels behind runtime messages.
+- Operator workflow clarity: None, Basic, and Bearer labels must continue to map to the same submitted values and required-field behavior.
+- Context visibility: translated copy can vary, but `hookAuthType` field visibility, option values, placeholder, draft update path, and receiver channel field rules remain unchanged.
+
+## Shared Signal Route Context Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the compact inherited-context rows for entity, monitor, service, environment, trace, time, and source evidence unchanged; this slice only moves shared row labels, source labels, source meta, live/refresh text, and collector/template prefixes behind runtime messages.
+- Density: preserve the same short row stack and dot-separated meta rhythm without adding context rails, summary cards, or explanatory onboarding.
+- Anti-AI-slop: do not invent source confidence, fake collection health, or cross-signal recommendations while externalizing shared context copy.
+- Operator workflow clarity: machine route tokens, trace/span ids, monitor ids, collector/template evidence, time bounds, refresh, live state, and source handoff values must remain readable in every consuming workbench.
+- Context visibility: `buildSignalEntityContextRows`, source row construction, and signal handoff URLs remain the shared owner for inherited context labels and meta copy across entity, topology, alert, log, trace, and metrics surfaces.
+
+## Shared Signal Route Context Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep inherited-context rows as the same compact evidence stack across entity, topology, alert, log, trace, and metrics surfaces; changing fallback locale order must not add context rails, side panels, or extra summaries.
+- Density: preserve the short label/value/meta row rhythm and dot-separated time/source meta.
+- Anti-AI-slop: do not invent source confidence, collection health, handoff recommendations, topology certainty, or monitor readiness while correcting fallback order.
+- Operator workflow clarity: default context copy resolves from runtime messages with an English fallback when no caller locale is available; route tokens, trace/span ids, monitor ids, collector/template values, time bounds, refresh, live state, and source handoff values stay machine-owned.
+- Context visibility: `buildSignalEntityContextRows`, `buildSourceContextRow`, source meta mapping, and signal handoff URLs remain the shared owner for all inherited context rows.
+
+## Topology Surface Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep topology product identity, source chips, view-mode chips, selected-edge evidence, and inherited fault-context rows in the existing dense graph support model; fallback ordering must not add explanation panels or new topology modes.
+- Density: preserve compact label/copy pairs for sources and views, short evidence rows, and the existing selected-edge evidence payload.
+- Anti-AI-slop: do not invent dependency confidence, blast radius, auto-discovery state, RCA readiness, or fake graph health while correcting fallback order.
+- Operator workflow clarity: default topology model copy should resolve from runtime messages with an English fallback when no caller locale is available; graph ids, entity ids, source kinds, edge ids, route params, and handoff URLs stay machine-owned.
+- Context visibility: fault context, selected-edge evidence, source filtering, view filtering, and signal/entity/alert handoffs remain visible through the existing `buildTopologyServiceMap` data contract.
+
+## Entity Detail View-Model Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep entity facts, overview rows, related signals, health model, next actions, handoff links, inherited context, alerts, relationships, collection source, and attribution rows in the existing detail-page data contract.
+- Density: preserve compact title/copy/meta rows and the current lightweight health model; fallback ordering must not add summary rails, status blocks, or duplicated context.
+- Anti-AI-slop: do not invent health, ownership, monitor readiness, attribution quality, or relationship certainty while correcting fallback order.
+- Operator workflow clarity: default entity-detail view-model copy should resolve from runtime messages with an English fallback when no caller translator is available; ids, statuses, route params, alert labels, identity keys, monitor/template values, and handoff URLs stay machine-owned.
+- Context visibility: entity, service, environment, time, monitor, trace/span, source, collector, template, alert, relationship, and attribution evidence remain visible through the existing view-model builders.
+
+## Entity Detail Surface Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep the cold full-width entity detail surface, compact header, count strip, command row, context center, related signal panels, health model, source/attribution panels, delete modal, and route handoff buttons exactly where they are.
+- Density: preserve compact copy density and existing title/copy/meta rows; fallback ordering must not add a side rail, onboarding prose, summary cards, or extra status decoration.
+- Anti-AI-slop: do not invent entity health, evidence counts, ownership, attribution state, monitor readiness, or alert closure state while correcting fallback order.
+- Operator workflow clarity: default surface chrome should resolve from runtime messages with an English fallback when no locale provider is present; entity ids, hrefs, route context, metrics counts, alert counts, monitor values, and mutation callbacks stay machine-owned.
+- Context visibility: header posture, count strip, inherited time/monitor/source context, action rows, alert/relationship/source evidence, attribution states, and delete confirmation remain visible through the existing `EntityDetailSurface` contract.
+
+## Trace Workbench Route Chrome Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the trace workbench header, query bar, trend band, dense trace table, detail evidence panel, entity context, and cross-signal handoffs in the existing client island; this slice only moves route chrome/status copy behind runtime i18n.
+- Density: preserve the compact cold-matte table-first layout, tight action row, small evidence cards, and narrow time rail without adding summary rails, nested cards, or decorative onboarding.
+- Anti-AI-slop: do not invent root-cause text, AI explanations, demo spans, fake trace health, or copied external APM navigation while externalizing labels.
+- Operator workflow clarity: service/trace/span/status filters, run/reset actions, selected-trace facts, disabled entity/log handoffs, and alert/log/metric/entity/catalog links must remain explicit.
+- Context visibility: trace id, span id, service namespace, entity id, environment, time range, source, attribution diagnostics, and current selection context must remain visible through evidence rows and handoff URLs.
+- Settled-cache boundary update: trace manage route remounts may reuse just-settled overview/list resources briefly while preserving trace/span/service/error filters, waterfall evidence, selected trace facts, entity context, and log/metric/alert handoffs; do not add freshness chrome, fake trace health, fake span coverage, or copied trace-console wording.
+
+## Topology Route Boundary - 2026-05-05
+
+- Hierarchy: keep the entity-centered topology canvas, relationship source strip, selected-edge evidence, alert-impact handoff, and current-entity signal links in the existing topology surface; do not add another visual shell.
+- Density: preserve the compact canvas-plus-evidence-aside posture and small evidence rows, with no new cards, gutters, or decorative status summaries.
+- Anti-AI-slop: copy cleanup only externalizes existing evidence language; do not invent fake blast-radius analysis, root-cause claims, or copied service-map chrome.
+- Operator workflow clarity: selected-edge evidence must still show collected source, endpoint entities, alert impact, and drilldowns to entity/metrics/logs/traces/alerts without hiding the roadmap boundary.
+- Context visibility: entity, service, environment, time, topology view/source, selected edge, trace/span, collector, and template context must continue flowing through topology evidence and handoff links.
+- Copy boundary update: keep topology selected-edge source evidence, row labels, alert-impact state, collector labels, roadmap boundary, and alert-impact helper text behind runtime i18n messages while preserving the existing evidence hierarchy and roadmap caveat.
+- Source/view copy boundary update: keep topology product identity, relationship source labels/copy, view-mode labels/copy, edge labels, and monitor-template node label behind runtime i18n messages while preserving the source-strip hierarchy, canvas density, and no-fake-analysis operator posture.
+- Shared health-affordance copy boundary update: keep the compact node health score, collection-health copy, alert/anomaly meta, and tone behavior unchanged while moving only lightweight health labels behind runtime messages; do not add fake SLO status, readiness scoring, or expanded health panels.
+
+## Entity Detail Route Boundary - 2026-05-04
+
+- Hierarchy: keep the entity profile, evidence summary, related signals, and next actions inside the existing entity detail surface; the server wrapper must not add another visual shell.
+- Density: preserve the current compact full-width workbench density and source-strip rhythm, with no new empty gutters or nested cards.
+- Anti-AI-slop: route migration is infrastructure only; avoid explanatory product prose or decorative status blocks.
+- Operator workflow clarity: refresh, edit, delete confirmation, definition entry, and signal handoff stay in the client island where state and route context are available.
+- Context visibility: inherited time, monitor, entity, service, environment, trace, and topology context must continue flowing into the evidence links.
+- Cache-key boundary update: entity detail loads should resolve the entity id at the server wrapper and use an explicit resource-derived workbench cache key for `/entities/{id}/detail` plus reload invalidation, preserving profile/evidence hierarchy, inherited route context, refresh/delete flows, and signal handoffs without adding fake health or status panels.
+- Settled-cache boundary update: entity detail route remounts can reuse just-settled profile/evidence data briefly, while Refresh continues to bump `reloadNonce` and `router.refresh()`; do not add freshness chrome, fake entity health, fake signal counts, extra navigation rails, or copied service-catalog investigation language.
+- Delete-copy boundary update: keep the destructive confirmation as the existing compact cold modal with title, kicker, body, cancel/confirm actions, and entity id evidence; only move the modal copy behind runtime messages without adding approval steps, fake safety scoring, or extra evidence rails.
+- Attribution-state copy boundary update: keep the compact attribution badges, ready/review/missing color semantics, row density, and source-template binding visibility unchanged while moving only the badge labels behind runtime messages.
+- Header/action copy boundary update: keep the compact entity hero, count strip, refresh/edit/delete command row, and inline operation-error slot unchanged while moving only title fallbacks, dynamic subtitle variants, count labels, and command labels behind runtime messages; do not add fake health summaries, new navigation rails, or decorative investigation prose.
+- Panel-copy boundary update: keep the existing two-column detail grid, overview/context/health/alerts/next/related/relationships/source/drilldown panels, and final disposition note unchanged while moving only panel titles and helper copy behind runtime messages; do not add fake SLO health, root-cause text, extra cards, or copied incident-detail IA.
+- Fact/context-row copy boundary update: keep the compact profile fact strip and inherited context rows unchanged while moving only fact labels and inherited-context label comparisons behind runtime i18n messages; preserve entity/service/environment/time/source visibility without adding fake navigation, fake health, or explanatory onboarding.
+- Overview/status copy boundary update: keep the compact overview row stack and current status semantics unchanged while moving only overview row labels and status fallback labels behind runtime i18n messages; do not add fake health summaries, new state badges, or onboarding copy.
+- Related-summary copy boundary update: keep the compact related-signal row stack and metrics/logs/traces evidence counts unchanged while moving only summary titles, count units, and empty states behind runtime i18n messages; do not add fake signal counts, synthetic health, or extra drilldown panels.
+- Handoff-row copy boundary update: keep the existing signal/alert/monitor/topology/template handoff row order, URLs, and inherited route context unchanged while moving only row titles and destination labels behind runtime i18n messages; do not add fake navigation, new shortcut panels, or copied APM side rails.
+- Health-model copy boundary update: keep the lightweight health score, availability, error-rate, latency, alert, anomaly, and collector evidence rows unchanged while moving only row labels, fallback states, and count templates behind runtime i18n messages; do not introduce SLO authoring, fake health claims, or expanded root-cause panels.
+- Next-action/drilldown copy boundary update: keep the server guidance row order, fallback drilldown links, and next/deep entry panel density unchanged while moving only server-action translations, fallback labels, route titles, and route meta behind runtime i18n messages; do not add fake recommendations, AI triage, or new navigation panels.
+- Current-alert row copy boundary update: keep the active alert row stack, alert id evidence, label summary meta, and empty alert fallback unchanged while moving only alert row titles, empty-state copy, count meta, and pending fallback copy behind runtime i18n messages; do not add fake incident status, AI triage, or extra closure controls.
+- Relationship-row copy boundary update: keep the existing upstream/downstream row stack, relation type evidence, target entity evidence, and empty relationship fallback unchanged while moving only empty-state labels, topology-wait meta, unknown-target fallback, and default relation meta behind runtime i18n messages; do not add fake topology health, impact scoring, or copied service-map analysis.
+- Attribution/source-row copy boundary update: keep the source-template binding panel, attribution row states, monitor/identity evidence counts, source labels, and diagnostic missing-part evidence unchanged while moving only row titles, count templates, fallback copy, and diagnostic meta behind runtime i18n messages; do not add fake ownership scoring, fake resource health, or copied catalog governance panels.
+
+## Entity New Route Boundary - 2026-05-04
+
+- Hierarchy: keep telemetry handoff, source/monitor prefill, catalog suggestions, identity/ownership/signal/relation authoring, preview, and save feedback inside the existing entity editor surface; the server wrapper must not add another shell.
+- Density: preserve the cold editor's compact field groups and definition preview rhythm, with no new gutters, nested cards, or fake status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative workflow cards, or copied competitor IA.
+- Operator workflow clarity: `source` and `monitorId` query-state prefill, catalog suggestions, validation, save, and editor stage state stay in the client island where router and form state are available.
+- Context visibility: telemetry/monitor handoff must continue feeding the new-draft loader so entity identity and monitor bind context are visible before save.
+- Cache-key boundary update: entity new loads should use an explicit workbench cache key derived from the catalog suggestions endpoint and telemetry seed source/monitor context, preserving prefilled identity/monitor-bind visibility, editor validation, and save feedback without adding decorative authoring summaries or fake status panels.
+- Settled-cache boundary update: new-entity route remounts can reuse just-settled catalog suggestions and seed draft context briefly while preserving `source`/`monitorId` prefill, identity/monitor-bind visibility, validation, save flow, and editor stage state; do not add freshness chrome, fake entity readiness, fake catalog coverage, extra navigation rails, or copied service-catalog authoring language.
+- Editor-copy boundary update: keep the compact stage strip, route-tab affordance, submit state, and telemetry/monitor seed visibility unchanged while moving only stage/tab/action copy behind runtime messages; do not add helper prose, fake completion scoring, or extra summary rails.
+- Editor-attribution copy boundary update: keep the compact telemetry attribution check, ready/review/missing state colors, identity/monitor/owner/system evidence rows, and discovery return link unchanged while moving only row titles, count templates, fallback meta, missing copy, and state labels behind runtime messages; do not add fake readiness scoring or extra onboarding panels.
+- Editor-shell copy boundary update: keep the compact header, entity type grid, entry-source pills, preview toggle, definition footer, and cold field rhythm unchanged while moving only title/subtitle, type/source labels, preview/footer copy, and summary badges behind runtime messages; do not add onboarding prose, extra cards, or fake metadata health.
+- Editor-field copy boundary update: keep the active-stage body, basic/ownership/signal/relation field rhythm, contact/link editors, JSON object rows, and definition preview unchanged while moving only field labels, placeholders, add labels, and preview aria copy behind runtime messages; do not add helper prose, new validation flows, or fake evidence fields.
+- Editor-telemetry guide copy boundary update: keep telemetry handoff, monitor/identity evidence chips, attribution-check density, and stage completion posture unchanged while moving only telemetry handoff title/copy/counts, attribution-check title, and stage guide fallback copy behind runtime messages; do not add fake telemetry status, onboarding rails, or extra discovery panels.
+- Editor-placeholder copy boundary update: keep the basic active-stage grid, field order, compact label/input rhythm, and telemetry-seeded draft behavior unchanged while moving only remaining example placeholders behind runtime messages; do not add helper prose, validation hints, fake readiness scoring, or extra onboarding rails.
+
+## Entity Edit Route Boundary - 2026-05-04
+
+- Hierarchy: keep loaded entity detail, catalog suggestions, edit validation, ownership/identity/relation authoring, definition preview, and save feedback inside the existing entity editor surface; the server wrapper must not add another shell.
+- Density: preserve the full editor's compact stage posture and complete-context relation state, with no new gutters, nested cards, or fake status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative edit summaries, or copied competitor IA.
+- Operator workflow clarity: entity-id params, detail fallback, catalog fallback, edit mode, save, and editor stage state stay in the client island where router and form state are available.
+- Context visibility: the requested entity id and loaded entity context must remain visible to the shared editor surface and edit API path.
+- Loading-copy i18n update: keep entity edit loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake edit readiness, catalog coverage placeholders, ownership scorecards, or extra editor navigation chrome.
+- Cache-key boundary update: entity edit loads should resolve entity id in the server wrapper and use an explicit resource-derived workbench cache key for `/entities/{id}` plus catalog suggestions, preserving loaded entity context, fallback behavior, relation state, validation, and save feedback without adding decorative edit summaries or fake status panels.
+- Settled-cache boundary update: entity edit route remounts can reuse just-settled entity detail and catalog suggestion data briefly while preserving entity id, loaded context, fallback draft behavior, edit validation, save feedback, and editor stage state; do not add freshness chrome, fake edit readiness, fake catalog coverage, extra navigation rails, or copied resource-editor language.
+- Editor-copy boundary update: keep the complete-context stage posture, route-tab affordance, submit state, and entity-id visibility unchanged while moving only stage/tab/action copy behind runtime messages; do not add helper prose, fake edit-readiness scoring, or extra summary rails.
+- Editor-attribution copy boundary update: keep the compact telemetry attribution check, ready/review/missing state colors, identity/monitor/owner/system evidence rows, and discovery return link unchanged while moving only row titles, count templates, fallback meta, missing copy, and state labels behind runtime messages; do not add fake readiness scoring or extra ownership panels.
+- Editor-shell copy boundary update: keep the compact header, entity type grid, entry-source pills, preview toggle, definition footer, definition handoff, and cold field rhythm unchanged while moving only title/subtitle, type/source labels, preview/footer copy, and summary badges behind runtime messages; do not add edit-readiness prose, extra cards, or fake metadata health.
+- Editor-field copy boundary update: keep the complete-context active-stage body, basic/ownership/signal/relation field rhythm, contact/link editors, JSON object rows, and definition preview unchanged while moving only field labels, placeholders, add labels, and preview aria copy behind runtime messages; do not add edit-readiness prose, new validation flows, or fake evidence fields.
+- Editor-telemetry guide copy boundary update: keep telemetry handoff, monitor/identity evidence chips, attribution-check density, and complete-context stage posture unchanged while moving only telemetry handoff title/copy/counts, attribution-check title, and stage guide fallback copy behind runtime messages; do not add fake telemetry status, edit-readiness rails, or extra discovery panels.
+- Editor-placeholder copy boundary update: keep the basic active-stage grid, field order, compact label/input rhythm, and complete-context draft behavior unchanged while moving only remaining example placeholders behind runtime messages; do not add helper prose, validation hints, fake readiness scoring, or extra edit-readiness rails.
+
+## Entity Definition Route Boundary - 2026-05-04
+
+- Hierarchy: keep definition loading, code editing, template/activity context, validation feedback, and save/delete actions inside the existing definition workspace surface; the server wrapper must not add another shell.
+- Density: preserve the cold definition editor's compact full-width workbench rhythm, with no extra gutters, nested cards, or fake status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative definition summaries, or copied competitor IA.
+- Operator workflow clarity: entity-id params, workspace data loading, format state, definition update, and recoverable load messages stay in the client island where route and form state are available.
+- Context visibility: the requested entity id, templates, activities, and load diagnostics must remain visible to the definition workspace and update API path.
+- Loading-copy helper update: keep entity definition loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake parser readiness, template-health counters, entity-summary placeholders, or explanatory authoring panels.
+- Cache-key boundary update: entity definition loads should resolve the entity id in the server wrapper and use an explicit resource-derived workbench cache key for the definition, activity, and template endpoints, preserving editor density, recoverable fallback messages, save/delete feedback, and context visibility without adding decorative definition summaries or fake health panels.
+- Settled-cache boundary update: entity definition route remounts can reuse just-settled definition, template, and activity data briefly while preserving entity id, YAML format state, recoverable load messages, editor density, preview/save actions, and update API context; do not add freshness chrome, fake parser health, fake definition readiness, extra navigation rails, or copied template-authoring language.
+- Workspace-shell copy boundary update: preserve the definition loading, code editor, template/activity context, preview/save actions, recoverable feedback, and compact context-panel density while moving only shell/action copy behind runtime messages; do not add decorative summaries, fake parser health, or extra guidance rails.
+
+## Entity Import Route Boundary - 2026-05-04
+
+- Hierarchy: keep bundle import editing, template guidance, activity context, preview/create actions, and validation feedback inside the existing import surface; the server wrapper must not add another shell.
+- Density: preserve the cold import workspace's compact full-width editor and context-panel rhythm, with no new gutters, nested cards, or fake status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative import summaries, or copied competitor IA.
+- Operator workflow clarity: template/activity loading, starter draft state, format switching, parse/create actions, and recoverable helper-endpoint fallbacks stay in the client island where form state is available.
+- Context visibility: templates, recent activities, import diagnostics, and batch results must remain visible to the shared import and definition workspace surfaces.
+- Loading-copy helper update: keep entity import loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake parser health, import-readiness counters, catalog-status placeholders, or explanatory onboarding panels.
+- Cache-key boundary update: entity import loads should use an explicit resource-derived workbench cache key for the shared template and activity endpoints, preserving starter draft state, preview/create actions, helper-endpoint fallback context, and batch result visibility without adding decorative import summaries or fake catalog status.
+- Settled-cache boundary update: import route remounts can reuse just-settled template and activity data briefly while preserving starter draft state, format switching, parse/create actions, helper-endpoint fallback context, and batch result visibility; do not add freshness chrome, fake parser health, fake import readiness, extra navigation rails, or copied catalog-import language.
+- Workspace-shell copy boundary update: preserve the starter draft, format selector, preview/create actions, attribution preview, validation feedback, and minimal import context-panel density while moving only import title/subtitle/helper copy behind runtime messages; do not add decorative import summaries, fake parser health, or competitor-style onboarding rails.
+
+## Entity List Route Boundary - 2026-05-04
+
+- Hierarchy: keep catalog search, entity counts, table rows, creation/import/discovery entries, and per-row evidence actions inside the existing list surface; the server wrapper must not add another visual shell.
+- Density: preserve the compact full-width entity catalog posture, with no new gutters, nested cards, rail panels, or fake status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative summaries, or copied competitor IA.
+- Operator workflow clarity: query-state filters, refresh/reset actions, page loading, and entity navigation stay in the client island where router-derived state is available.
+- Context visibility: entity ids, health affordances, monitor counts, alert counts, relation counts, and list diagnostics must remain visible to the current surface and handoff links.
+- Loading-copy helper update: keep entity list loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake catalog counts, entity-health placeholders, onboarding copy, or extra navigation rails.
+- Cache-key boundary update: entity list loads should use an explicit query-derived workbench cache key that preserves catalog hierarchy, dense row evidence, filter clarity, creation/import/discovery entries, and context visibility without adding UI layers or fake entity status.
+- Health-affordance copy boundary update: keep the compact table health score, collection-health copy, alert/anomaly meta, and tone behavior unchanged while moving only lightweight health labels behind runtime messages; do not add fake SLO status, readiness scoring, or expanded health panels.
+
+## Entity Discovery Route Boundary - 2026-05-04
+
+- Hierarchy: keep telemetry discovery search, candidate attribution, policy strip, source chips, and create/import handoff actions inside the existing discovery surface; the server wrapper must not add another shell.
+- Density: preserve the compact full-width discovery console and dense table posture, with no new gutters, rail panels, nested cards, or fake guidance blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative workflow summaries, or copied competitor IA.
+- Operator workflow clarity: discovery loading, monitor search, selected scope, candidate review, and entity draft handoff stay in the client island where client state is available.
+- Context visibility: presets, recent activities, catalog owners/systems/environments, attribution state, and create-ready diagnostics must remain visible to the existing discovery surface and handoff links.
+- Loading-copy boundary update: definition, import, and discovery loading states keep the same compact entity workspace hierarchy, density, operator flow, and diagnostics while their visible loading copy moves behind runtime i18n keys; do not add loaders, shells, fake entity health, or explanatory product prose.
+- Loading-copy helper update: keep telemetry discovery loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake candidate counts, catalog-health placeholders, service-map hints, or explanatory discovery panels.
+- Cache-key boundary update: entity discovery loads should use an explicit resource-derived workbench cache key for governance preset, governance activity, and catalog suggestion endpoints, preserving monitor search state, candidate attribution, selected scope, and entity draft handoff visibility without adding fake guidance/status panels.
+- Settled-cache boundary update: discovery route remounts can reuse just-settled governance preset, activity, and catalog suggestion data briefly while preserving telemetry search, candidate attribution, selected scope, and entity draft handoff visibility; do not add freshness chrome, fake discovery readiness, fake catalog health, extra navigation rails, or copied service-catalog language.
+- Workspace-shell copy boundary update: preserve the telemetry search, candidate table, policy strip, compact count strip, source chips, create/import/catalog handoffs, and attribution evidence density while moving only discovery shell/table/policy copy behind runtime messages; do not add fake discovery health, onboarding rails, decorative summaries, or competitor-style guidance.
+
+## Monitor Editor Route Boundary - 2026-05-04
+
+- Hierarchy: keep app/template context, monitor identity fields, scrape/schedule/collector settings, label/annotation editing, detect/save actions, and return context inside the existing monitor editor surface; the route wrappers must not add another shell.
+- Density: preserve the compact authoring surface and shared ops-token rhythm, with no new gutters, nested cards, rail panels, or fake status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative setup summaries, or copied competitor IA.
+- Operator workflow clarity: new/edit params, query-state return context, draft loading, scrape draft refresh, validation, create/update, detect, and return navigation stay in the client islands where router and form state are available.
+- Context visibility: app, monitor id, entity handoff, labels, pagination return state, collectors, param definitions, scrape params, and Grafana dashboard context must remain visible to the editor surface and save path.
+- Cache-key boundary update: monitor editor loads should use explicit resource-derived workbench cache keys for collector/app-param resources on new routes and monitor detail/collector resources on edit routes, preserving app/template context, return navigation, validation, detect/save flows, and editor density without adding decorative setup summaries or fake monitor status.
+- Settled-cache boundary update: new-monitor route remounts can reuse just-settled collector and app-param resources briefly while preserving app/template context, entity return handoff, validation, detect/save flows, scrape refresh, and compact editor density; do not add freshness chrome, fake monitor readiness, fake collector coverage, extra navigation rails, or copied setup-wizard language.
+- Settled-cache boundary update: edit-monitor route remounts can reuse just-settled monitor detail and collector resources briefly while preserving monitor identity, app/template context, entity return handoff, validation, detect/update flows, scrape refresh, and compact editor density; do not add freshness chrome, fake monitor readiness, fake collector coverage, extra navigation rails, or copied setup-wizard language.
+- Metadata fallback update: preserve the compact shell facts, payload review rows, detect/save/cancel actions, and app/scrape/name/collector visibility while moving only absent app/name/collector markers into the shared localized fallback; do not add wizard prose, fake readiness, or extra summary rails.
+
+## Monitor Detail Cache-Key Boundary - 2026-05-06
+
+- Hierarchy: keep realtime metrics, history charts, favorites, Grafana fallback, compact console tabs, and signal handoff links inside the existing monitor detail client island; this slice only resolves the route id at the server boundary and keys the first-screen loader.
+- Density: preserve the current dense monitor console, chart/table rhythm, and compact toolbar without adding summary cards, rails, or decorative performance panels.
+- Anti-AI-slop: avoid fake monitor health, fake Grafana readiness, copied resource-detail IA, or product prose while deriving the cache key from real monitor detail, Grafana, and favorite-metric resources.
+- Operator workflow clarity: refresh cadence, tab fallback, history time context, favorite edits, Grafana delete, and signal handoffs stay unchanged while the initial bundle load can be deduped safely.
+- Context visibility: monitor id, detail endpoint, Grafana endpoint, favorite-metric endpoint, time context, entity return, and signal links remain visible to source and route contracts.
+
+## Monitor Manage Route Boundary - 2026-05-04
+
+- Hierarchy: keep search filters, list rows, selected monitor rail, batch controls, import/export, delete confirmation, and entity return context inside the existing monitor manage workbench; the route wrapper must not add another shell.
+- Density: preserve the compact monitor inventory table and right-rail operation density, with no new gutters, nested cards, rail panels, or fake status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative resource summaries, or copied competitor IA.
+- Operator workflow clarity: query-state canonicalization, entity down-first fallback, selection, pagination, import/export, enable/pause/copy/delete, and detail/edit/new handoffs stay in the client island where router, file input, and modal state are available.
+- Context visibility: app, labels, status, page state, selected monitor, entity id/name, safe return URL, and monitor navigation context must remain visible to the workbench and route builders.
+- Cache-key boundary update: list loads should use an explicit query-derived workbench cache key that preserves entity down-first fallback semantics, table density, batch controls, import/export, and return-context visibility without adding UI layers.
+- Copy boundary update: keep the selected-monitor delete confirmation title, body, confirm action, cancel action, and selected-count evidence behind runtime i18n keys while preserving the cold modal density, batch-selection context, and entity/alert cleanup warning; do not add fake impact analysis or copied confirmation prose.
+
+## Setting Status Route Boundary - 2026-05-04
+
+- Hierarchy: keep organization profile, component list, incident list, validation, public status link, and admin controls inside the existing status setting surface; the route wrapper must not add another shell.
+- Density: preserve the compact full-width admin list and tab/table posture, with no new gutters, nested cards, summary rails, or fake uptime/status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative health claims, or copied competitor status-page IA.
+- Operator workflow clarity: organization editing, component create/edit/delete, incident search/pagination/create/edit/delete, refresh tick, and save feedback stay in the client island where local form state is available.
+- Context visibility: organization id/name, component health, incident state, public status URL, validation feedback, and localized save/delete messages must remain visible to the existing surface and controller calls.
+- Cache-key boundary update: status setting loads should use an explicit query-derived workbench cache key that preserves organization/component/incident editing, incident search and pagination context, save/delete refresh invalidation, and public status link visibility without adding summary rails, fake uptime claims, or decorative health panels.
+- Copy boundary update: keep the organization profile form, component and incident tables, public status link, empty table state, and delete confirmation modal intact while moving remaining surface-local copy behind runtime i18n keys; do not add uptime/SLA claims, decorative health summaries, or copied status-page SaaS language.
+
+## Setting Labels Route Boundary - 2026-05-05
+
+- Hierarchy: keep label search, full-width list, copy/edit/delete actions, create/edit dialog, and save/delete feedback inside the existing label management surface; the route wrapper must not add another shell.
+- Density: preserve the compact cold admin-list posture, with no summary rail, nested cards, or decorative label overview blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake label health/status claims, or copied tag-management IA.
+- Operator workflow clarity: search query state, refresh version, draft modal state, add/edit mode, clipboard copy, delete, and save calls stay in the client island where browser and mutation state are available.
+- Context visibility: label name/value/type/description, current search, modal add/edit mode, and localized loading copy must remain visible to the existing surface and controller calls.
+- Cache-key boundary update: label list loads should use an explicit query-derived workbench cache key that preserves search clarity, refresh invalidation, dense admin-list posture, dialog state, and mutation feedback without adding summary rails, fake health, or decorative tag overviews.
+- Copy boundary update: keep the same hierarchy, density, label type chips, table headers, empty state, and create/edit dialog context while moving the remaining surface-local copy behind runtime i18n keys; do not add tag-cloud summaries, fake governance status, or marketplace-style language.
+- Loading-copy helper update: keep label loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add tag-cloud placeholders, fake label health, governance status, or decorative loading panels.
+
+## Setting Plugins Route Boundary - 2026-05-05
+
+- Hierarchy: keep plugin search, full-width plugin table, selection, enable/delete actions, upload/edit dialog, and save feedback inside the existing plugin management surface; the route wrapper must not add another shell.
+- Density: preserve the compact cold admin-list posture, with no summary rail, nested cards, decorative plugin overview, or fake marketplace/status block.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake plugin health claims, fake extension domains, or copied plugin-marketplace IA.
+- Operator workflow clarity: search query state, refresh version, selected ids, upload draft modal state, enable toggle, delete, and save calls stay in the client island where browser and mutation state are available.
+- Context visibility: plugin name, enable status, item type/count, selected ids, current search, draft upload fields, and localized loading copy must remain visible to the existing surface and controller calls.
+- Cache-key boundary update: plugin list loads should use an explicit query-derived workbench cache key that preserves search clarity, refresh invalidation, selected ids, upload/edit dialog state, enable/delete feedback, and dense admin-list context without adding marketplace rails or fake plugin health.
+- Copy boundary update: keep the same hierarchy, density, selected-count affordance, enable-state labels, params column, and empty table state while moving the remaining surface-local copy behind runtime i18n keys; do not add plugin marketplace language, fake health, or extension-domain prose.
+- Settled-cache boundary update: keep plugin search, dense full-width table, selected ids, upload/edit dialog, enable/delete actions, and save/delete feedback inside the existing plugin management surface; short route-remount reuse must not add freshness chrome, marketplace rails, fake extension health, or plugin-domain summary panels. Search still invalidates through the query-derived plugin URL, and Refresh, enable, delete, selected delete, and save continue to bump `reloadVersion`.
+- Loading-copy helper update: keep plugin loading feedback as runtime-owned copy attached to the existing `ClientWorkbench` route island; do not add loading skeletons, marketplace pitch text, fake plugin health, or extension readiness claims.
+
+## Setting Collector Route Boundary - 2026-05-05
+
+- Hierarchy: keep collector search, dense cluster table, refresh/deploy/online/offline/delete controls, row operations, and collector health evidence inside the existing collector management surface; the route wrapper must not add another shell.
+- Density: preserve the compact cold admin-list posture, with no summary rail, nested cards, decorative cluster overview, or fake collector health/status block.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake collector capacity claims, fake topology entries, or copied cluster-console IA.
+- Operator workflow clarity: search query state, refresh query bump, batch controls, row controls, and formatted timestamps stay in the client island where browser and mutation state are available.
+- Context visibility: collector name, ip, mode, version, status, pinned/dispatched monitor counts, current search, and localized loading copy must remain visible to the existing surface and controller calls.
+- Cache-key boundary update: collector list loads should use an explicit query-derived workbench cache key that preserves search clarity, refresh invalidation, dense cluster rows, batch controls, row operations, and collector health evidence without adding summary rails, fake capacity, or topology decoration.
+- Copy boundary update: keep the same hierarchy, density, cluster health strip, row operation buttons, collector status/mode labels, and empty table state while moving remaining surface-local copy behind runtime i18n keys; do not add fake capacity claims, topology shortcuts, or copied cluster-console prose.
+- Loading-copy helper update: keep collector loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add decorative cluster placeholders, fake collector health, capacity claims, or topology entries.
+
+## Setting Define Route Boundary - 2026-05-05
+
+- Hierarchy: keep monitoring-template search, dense definition list, YAML editor, preview panel, datasource status, and create/edit/preview/save/delete actions inside the existing definition surface; the route wrapper must not add another shell.
+- Density: preserve the compact cold admin-list and editor posture, with no summary rail, nested cards, decorative template overview, or fake marketplace/status block.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake template health claims, fake domain entries, or copied template-marketplace IA.
+- Operator workflow clarity: search query state, selected definition, editor draft, dark-mode toggle, preview result, save state, and mutation feedback stay in the client island where browser and form state are available.
+- Context visibility: definition name/type/datasource/expression, datasource status, current YAML label, current search, preview diagnostics, and localized loading copy must remain visible to the existing surface and controller calls.
+- Cache-key boundary update: definition list loads should use an explicit query-derived workbench cache key that preserves template search, selected definition context, YAML editor state, datasource status, preview diagnostics, and save/delete feedback without adding summary rails, marketplace framing, or fake template health.
+- Copy boundary update: keep the dense definition list, YAML editor, dark-mode toggle, preview panel, datasource status, row type/status/meta, and empty/empty-selected states intact while moving remaining surface and view-model copy behind runtime i18n keys; do not add template marketplace prose, fake definition health, or copied SaaS rule-builder language.
+- Loading-copy helper update: keep definition-center loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add marketplace placeholders, fake template health, generated domain entries, or decorative rule-builder loading panels.
+
+## Setting System Config Route Boundary - 2026-05-05
+
+- Hierarchy: keep locale, timezone, theme selectors, validation, save feedback, and theme/locale side effects inside the existing system config form; the route wrapper must not add another settings shell.
+- Density: preserve the compact full-width cold settings form, with no summary rail, nested cards, decorative configuration overview, or fake system-health block.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake runtime status, fake deployment domains, or copied admin-console IA.
+- Operator workflow clarity: draft state, save state, locale setter, theme application, and reload behavior stay in the client island where browser and mutation state are available.
+- Context visibility: current locale, timezone options, theme options, save feedback, and localized field labels must remain visible to the existing settings form and controller calls.
+- Cache-key boundary update: system config loads should use an explicit resource-derived workbench cache key for the system config and timezone endpoints, preserving selector hierarchy, compact form density, save feedback, locale/theme side effects, and context visibility without adding summary rails or fake runtime-health panels.
+- Fact fallback boundary update: keep locale, timezone, theme selectors and save feedback intact while moving only missing fact values into the shared localized fallback; unknown payload values remain visible instead of being hidden or invented.
+- Form label copy update: keep locale, timezone, theme, locale-option, theme-option, and submit copy runtime-owned inside the existing compact selector form; do not add runtime health claims, deployment setup prose, summary rails, or copied admin-console IA.
+
+## Setting Object Store Route Boundary - 2026-05-05
+
+- Hierarchy: keep provider selection, OBS credential fields, validation, save feedback, and storage-controller calls inside the existing object-store settings form; the route wrapper must not add another settings shell.
+- Density: preserve the compact full-width cold settings form, with no summary rail, nested cards, decorative storage overview, or fake capacity/health block.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake bucket status, fake archive metrics, fake cloud domains, or copied cloud-console IA.
+- Operator workflow clarity: draft state, save state, secret fields, validation, and mutation feedback stay in the client island where browser and form state are available.
+- Context visibility: provider, endpoint, bucket, save path, credential placeholders, validation feedback, localized labels, and load diagnostics must remain visible to the existing settings form and controller calls.
+- Cache-key boundary update: object-store loads should use an explicit resource-derived workbench cache key for the `/config/oss` endpoint, preserving provider/credential context, compact form density, mutation feedback, and diagnostics without adding summary rails, fake capacity panels, or invented bucket health.
+- Copy boundary update: keep object-store workspace facts, provider type, bucket, and endpoint evidence behind runtime i18n keys while preserving the compact settings form and validation posture; do not add fake capacity, archive-health, or copied cloud-console prose.
+- Fact fallback boundary update: keep absent provider type, bucket, and endpoint as terse localized empty facts in the existing evidence list; do not infer storage health, object capacity, archive status, or cloud-console setup progress.
+- Provider option copy update: keep the existing DATABASE/FILE/OBS values as backend payloads while provider option labels are runtime-owned localized copy in the compact select; do not add cloud-console setup guidance, fake capacity, or archive-health claims.
+- OBS placeholder copy update: keep access key, secret key, bucket, endpoint, and save path placeholders attached to the existing OBS credential fields and owned by runtime i18n; do not add cloud setup walkthroughs, fake archival readiness, or provider-health claims.
+- Form label copy update: keep the object-store page title, provider label, OBS field labels, and submit action as runtime-owned settings copy while preserving the compact form; do not add capacity summaries, archive policy chrome, or cloud-console onboarding prose.
+- Loading-copy helper update: keep object-store loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add bucket-health placeholders, capacity summaries, archive-policy chrome, or cloud-console loading panels.
+
+## Setting Message Server Route Boundary - 2026-05-05
+
+- Hierarchy: keep email and SMS summary rows, provider-specific dialog fields, validation, save feedback, and message-server controller calls inside the existing message-server settings surface; the route wrapper must not add another settings shell.
+- Density: preserve the compact full-width cold summary list and focused dialogs, with no summary rail, nested cards, decorative notification overview, or fake delivery health block.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake SMTP/SMS status, fake provider domains, fake delivery metrics, or copied notification-console IA.
+- Operator workflow clarity: email draft state, SMS draft/snapshot restore, provider switching, toggles, number-stepper port editing, and mutation feedback stay in the client island where browser and form state are available.
+- Context visibility: mail host/port/user, SMS provider fields, enable states, localized labels, validation feedback, and load diagnostics must remain visible to the existing summary list, dialogs, and controller calls.
+- Cache-key boundary update: message-server loads should use an explicit resource-derived workbench cache key for the email and SMS config endpoints, preserving summary density, dialog editing flow, provider-specific fields, mutation feedback, and diagnostics without adding status panels or invented delivery metrics.
+- Copy boundary update: keep UniSMS auth-mode option labels behind runtime i18n keys while preserving provider-specific dialog density, required-field clarity, SMS snapshot restore, and validation behavior; do not add provider education copy, fake delivery status, or SaaS notification-console prose.
+- Summary fallback boundary update: absent email host/user/port and provider-specific SMS fields stay as terse localized summary facts; do not infer SMTP health, delivery readiness, provider reachability, or notification routing state.
+- Summary action context update: keep the visible configure action compact and unchanged while repeated email/SMS summary buttons receive row-specific accessible names; do not add fake provider readiness, delivery health, or extra notification workflow chrome.
+- Dialog field copy update: keep email/SMS provider labels, credential fields, and provider option names as runtime-owned settings copy inside the existing compact dialogs; do not add onboarding prose, delivery analytics, fake provider health, or copied notification-console IA.
+- Dialog action copy update: keep configure/cancel/save/saving and enable yes/no states as compact shared runtime copy in the existing summary rows and dialog footers; do not add extra confirmation rails, fake delivery impact, or provider-health status.
+
+## Setting Token Route Boundary - 2026-05-05
+
+- Hierarchy: keep token counts, generate action, dense token table, expiry state, token masks, and delete actions inside the existing token management surface; the route wrapper must not add another settings shell.
+- Density: preserve the compact full-width admin table and inline count strip, with no summary rail, nested cards, decorative API-key overview, or fake token health block.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake token scopes, fake usage metrics, fake rotation status, or copied API-key console IA.
+- Operator workflow clarity: token loading, count derivation, expiry checks, generate/delete button affordances, and date formatting stay in the client island where browser and state are available.
+- Context visibility: token name/mask/creator/create-time/expire-time/last-used, empty state, localized labels, and load diagnostics must remain visible to the existing table and controller calls.
+- Loading-copy boundary update: system config, object store, message server, and token loading states keep the same full-width settings hierarchy and compact density while their visible loading copy moves behind runtime i18n keys; do not add loaders, fake health/status summaries, or explanatory admin prose.
+- Cache-key boundary update: token loads should use an explicit resource-derived workbench cache key for the `/account/token` endpoint, preserving token counts, dense table evidence, empty state, generate/delete affordances, expiry context, and diagnostics without adding fake token scopes, usage metrics, or health panels.
+- Copy boundary update: keep token counts, row lifecycle state, creator attribution, expiry options, and empty-state evidence behind runtime i18n keys; do not add fake scope labels, rotation advice, or copied API-key console prose.
+- Row fact fallback boundary update: absent token masks and creators stay as terse localized row facts; do not infer token scope, rotation posture, usage metrics, owner/team, or API-key health.
+- Table-cell fallback update: keep token name, mask, creator, create-time, expire-time, and last-used columns in the dense admin table while moving blank table cells to shared `common.none`; do not add fake token scopes, usage counters, rotation guidance, or SaaS API-key console patterns.
+- Row action context update: keep the visible delete button compact and unchanged while the repeated destructive action receives a token-specific accessible name; do not add fake revocation state, scope chips, rotation guidance, or a delete workflow that is not implemented.
+- Console/table copy update: keep the compact title, count strip, generate action, table headers, empty row, and row delete copy runtime-owned inside the existing dense token management surface; do not add token scope claims, usage analytics, rotation guidance, or copied API-key console IA.
+
+## Bulletin Center Route Boundary - 2026-05-05
+
+- Hierarchy: keep bulletin list, action toolbar, metrics table, refresh behavior, create/edit/delete dialog flows, and controller-backed load path inside the existing bulletin center surface; the route wrapper must not add another shell.
+- Density: preserve the compact public-ops bulletin table posture, with no summary rail, nested cards, decorative announcement overview, or fake delivery/status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake bulletin health, fake usage counters, fake product domains, or copied incident/status-center IA.
+- Operator workflow clarity: refresh tick, reload callback, client workbench load, dialog mutation state, and metrics JSON editing stay in the client island where browser and state are available.
+- Context visibility: bulletin name, app, monitor ids, creator, metric payload, localized loading copy, and controller diagnostics must remain visible to the existing surface and route contract.
+- Loading-copy i18n update: keep bulletin loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake delivery health, announcement readiness placeholders, marketing prose, or extra bulletin-navigation chrome.
+- Cache-key boundary update: bulletin list loads should use an explicit resource-derived workbench cache key for the `/bulletin?pageIndex=0&pageSize=8` list endpoint plus refresh invalidation, preserving compact table evidence, metrics drilldown, mutation dialogs, and reload clarity without adding fake health/status or delivery analytics.
+- Delete-copy boundary update: keep the current-bulletin destructive confirmation as the existing compact cold modal with selected bulletin name evidence and two action buttons; only move the modal title, body, cancel, and confirm copy behind runtime messages without adding extra approval rails or fake delivery-impact scoring.
+
+## Bulletin Page Helper Regression Boundary - 2026-05-09
+
+- Hierarchy: keep the bulletin route regression on the existing client-workbench loader and shared bulletin center surface; only make the mocked provider's locale and loading-copy ownership explicit.
+- Density: preserve the compact bulletin route marker without adding summary rails, announcement skeletons, delivery status panels, route banners, or nested cards.
+- Anti-AI-slop: do not invent bulletin delivery state, publish readiness, monitor count health, usage analytics, or notification-center product prose while replacing implicit helper assumptions.
+- Operator workflow clarity: the regression should assert helper-owned bulletin loading copy while the controller-backed load path, refresh tick, and surface handoff remain unchanged.
+- Context visibility: controller list payload, loading marker, surface marker, refresh invalidation contract, and `Ops board` row evidence stay data-backed and unchanged.
+
+## Public Status Route Boundary - 2026-05-05
+
+- Hierarchy: keep organization brand, component health history, incident feed, mode switch, locale picker, year filter, and powered-by footer inside the existing public status shell; the route wrapper must not add another shell.
+- Density: preserve the compact public status page posture, with no summary rail, nested cards, fake SLA widgets, fake uptime claims, or extra marketing hero.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake health/status, fake incident counts, fake domains, or copied status-page SaaS IA.
+- Operator workflow clarity: incident year state, reload token, incident feed loading/error state, locale switching, and public-controller calls stay in the client island where browser and effects are available.
+- Context visibility: org title/copy/logo/color, component cards, incident cards, current year, selected mode, localized labels, and controller diagnostics must remain visible to the existing public status shell.
+- Cache-key boundary update: the initial public status load should use an explicit resource-derived workbench cache key for `/status/page/public/org`, `/status/page/public/component`, and the current-year public incident URL, preserving the component/incident mode switch, locale controls, year filter, incident reload state, and powered-by context without adding fake SLA or uptime widgets.
+- Incident posture English locale update: keep the incident history posture as a compact public-status locale line without adding status marketing copy, fake reliability claims, public incident counters, or copied status-page SaaS wording.
+
+## Overview Route Boundary - 2026-05-05
+
+- Hierarchy: keep status grid, setup/support panels, summary cards, impacted entities, timeline, checklist/guidance rail, quick entries, and detail/problem-focus dialogs inside the existing overview console; the route wrapper must not add another shell.
+- Density: preserve compact three-signal desk density, with no nested cards, hero/marketing layer, fake zero-health panels, fake readiness claims, or extra gutters.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake incidents, fake setup progress, fake product domains, or copied SaaS dashboard IA.
+- Operator workflow clarity: refresh nonce, problem-focus dialog, selected summary/impacted-entity dialogs, overview load, alert-derived route handoffs, and setup/healthy placeholder decisions stay in the client island.
+- Context visibility: summary apps, alert list, top-alert context, status items, guidance links, quick-entry routes, localized labels, and support/checklist context must remain visible to the existing overview shell and controller/view-model calls.
+- Cache-key boundary update: overview loads should use an explicit refresh-aware workbench cache key for `/summary` and the latest-alert list URL, preserving status grid hierarchy, problem-focus dialogs, guidance rail density, quick entries, and alert-derived handoffs without adding fake readiness or health panels.
+- Dashboard alias boundary update: `/dashboard` remains a compatibility entry into overview and should redirect through the shared compatibility query normalizer, preserving machine context while stripping display-only return labels; do not render a second overview shell or add alias-specific copy, rails, fake dashboard widgets, or copied SaaS dashboard IA.
+
+## OTLP Center Route Boundary - 2026-05-05
+
+- Hierarchy: keep source selection, protocol/setup stepper, live signal ribbons, readiness/self-check rows, collection-loop links, source catalog, filter rail, entity return, and three-signal handoffs inside the existing OTLP center console; the route wrapper must not add another shell.
+- Density: preserve the cold matte catalog density, fixed rail width, compact source cards, and restrained signal counts, with no nested cards, marketing hero, extra gutter, or fake zero/status panels.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, fake source domains, fake readiness claims, copied SaaS catalog IA, or pretending competitor capabilities are implemented features.
+- Operator workflow clarity: source search state, route query context, entity return link, backend-backed readiness/self-check data, and source-card filtering stay in the client island where browser and workbench state are available.
+- Context visibility: incoming entity/return context, signal counts, readiness rows, self-check rows, collection-loop entries, source sections, search tokens, and localized loading copy must remain visible to the existing OTLP center surface and controller/view-model calls.
+
+## Exception Center Copy Boundary - 2026-05-05
+
+- Hierarchy: keep the existing exception filter rail, query bar, dense exception table, recovery links, and status/title facts; this slice only moves visible copy behind runtime i18n keys.
+- Density: preserve the compact error explorer posture, fixed sidebar, tight toolbar, and table-first layout with no extra cards, summary rails, or explanatory gutters.
+- Anti-AI-slop: avoid fake incident evidence, fake exception analytics, copied external error-explorer IA, and new product prose while localizing the existing labels and facts.
+- Operator workflow clarity: type-specific 403/404/500 facts, filters, query controls, table labels, and recovery handoffs stay readable and map directly to the current exception workflow.
+- Context visibility: route type, exception group count, time range, filter groups, error rows, and log/trace/overview recovery context remain visible to the current surface and source contracts.
+
+## Exception Recovery Handoff Boundary - 2026-05-08
+
+- Hierarchy: keep the three recovery links in the existing bottom summary strip, without adding a handoff panel, side rail, or incident-style action list.
+- Density: preserve compact pill links beside the exception group count and subtitle so the table remains the primary operator surface.
+- Anti-AI-slop: avoid component-local href-to-label guesses, fake recovery automation, copied error-explorer navigation semantics, or translated-string branching while moving handoff labels into recovery row data.
+- Operator workflow clarity: overview, log, and trace handoffs must follow the same recovery row payload so the current exception route type, table, filters, and summary stay unchanged.
+- Context visibility: route type, recovery href, recovery label, exception group count, subtitle, and log/trace/overview context stay aligned with the same exception view-model rows.
+
+## Exception Row Detail Href Boundary - 2026-05-08
+
+- Hierarchy: keep exception detail navigation on the existing exception-type table link, without adding a detail action column, drawer, or extra row controls.
+- Density: preserve the dense exception table rhythm so type, message, count, timestamps, and application remain the primary scan path.
+- Anti-AI-slop: avoid component-local detail href construction, fake stacktrace drilldowns, copied error-explorer route semantics, or query-string logic in the surface while moving detail href ownership into exception row data.
+- Operator workflow clarity: row clicks must follow the same exception row payload as type, message, count, timestamps, and application, so filters, type-specific route context, and recovery links remain unchanged.
+- Context visibility: route type, row key, encoded detail href, exception type, error message, and application stay aligned with the same exception view-model row.
+
+## Exception Subtitle Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep the existing bottom summary strip subtitle beside group count and recovery pills; do not add banners, empty panels, or helper copy.
+- Density: preserve the compact table-first exception surface while moving only missing-subtitle fallback ownership into the view model.
+- Anti-AI-slop: avoid surface-local translation-key probing, fake exception analytics, copied error-explorer prose, or fallback copy that implies nonexistent evidence.
+- Operator workflow clarity: route type, copy title, facts, filters, table rows, detail links, and recovery handoffs remain unchanged while subtitle fallback becomes part of the copy payload.
+- Context visibility: type-specific subtitle, default subtitle, group count, row detail hrefs, and recovery context stay aligned through the same exception view-model data.
+
+## Exception Row Meta Copy Boundary - 2026-05-08
+
+- Hierarchy: keep boundary and next-step meta as the existing compact row eyebrow inside the 403/404/500 exception copy stack, without adding a new status rail, incident panel, or diagnostic legend.
+- Density: preserve two rows per exception type so title, copy, facts, table, filters, and recovery pills remain the primary scan path.
+- Anti-AI-slop: avoid hardcoded English meta labels, fake recovery state, copied error-console lifecycle wording, or metadata that implies backend automation evidence.
+- Operator workflow clarity: access, routing, runtime, and operator-action meta labels must stay static row descriptors tied to the same exception type payload.
+- Context visibility: exception type, row title/copy, facts, table rows, detail hrefs, and recovery context remain unchanged while row meta display becomes runtime-localized.
+
+## Incident Surface Copy Boundary - 2026-05-05
+
+- Hierarchy: keep the current cold-matte incident entry, overview/entity actions, shell panel, launch checklist, and adapter empty state; this slice only moves visible copy behind runtime i18n keys.
+- Density: preserve the compact entry posture with the existing two-column shell/rail and full-width empty state, with no new cards, summary rails, fake incident counters, or explanatory gutters.
+- Anti-AI-slop: avoid fake incident feeds, fake health/readiness, copied external incident-desk IA, or new product prose while localizing the current route labels and handoff copy.
+- Operator workflow clarity: overview/entity entry actions, response-timeline checklist, responsibility hints, and future adapter boundary stay recognizable and map directly to the current incident route.
+- Context visibility: incident title/subtitle, shell copy, checklist rows, chips, empty-state copy, and log/trace/overview handoff labels remain visible to the existing cold-ops surface and source contracts.
+
+## Action Surface Copy Boundary - 2026-05-05
+
+- Hierarchy: keep the current cold-matte automation entry, overview/entity actions, shell panel, launch checklist, adapter boundary, suggested-action panel, and empty state; this slice only moves visible copy behind runtime i18n keys and shrinks the route wrapper.
+- Density: preserve the compact two-column entry posture, adapter boundary panel, and suggested action cards with no new summary rails, fake run consoles, extra approval widgets, or explanatory gutters.
+- Anti-AI-slop: avoid fake automation execution, fake success states, copied app-builder/runbook IA, or product prose while localizing the current manual-confirmation labels and roadmap snapshot guardrails.
+- Operator workflow clarity: suggested actions remain evidence-linked, disabled for auto-execution, and explicitly manual-confirmed; overview/entity links and adapter boundary stay readable.
+- Context visibility: incoming alert/entity/trace context, evidence hrefs, action risk, confirmation mode, snapshot labels, checklist rows, and no-auto-execute markers remain visible to the current route and tests.
+- Suggested-action meta boundary update: keep the compact card eyebrow line in the same position while moving translated risk/catalog display meta into suggested-action row data; do not add execution controls, fake status, approval widgets, or copied automation-console phrasing.
+
+## Log Manage Route Chrome Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the cold log workbench header, query bar, trend summary, dense log table, detail panel, selected evidence, entity-context, and handoff actions in the existing client island; this slice only moves route chrome, evidence, and handoff labels behind runtime messages.
+- Density: preserve compact query controls, summary cards, dense table headers, and the right-side detail panel without extra cards, rails, or explanatory gutters.
+- Anti-AI-slop: do not add fake log quality, AI log summaries, new stream surfaces, copied SaaS observability prose, or synthetic evidence.
+- Operator workflow clarity: run/reset, severity filter, trace/span/body inputs, selected log inspection, and trace/metrics/entity/alert handoffs keep their current behavior and machine route context.
+- Context visibility: selected log evidence, entity context rows, trace/span/service/time/source context, and disabled handoff reasons remain visible without inventing data.
+- Settled-cache boundary update: log manage route remounts can reuse just-settled overview, list, trend, and trace-coverage resources briefly while preserving stream/history mode, query filters, selected log evidence, entity context, and trace/metrics/entity/alert handoffs; do not add freshness chrome, fake log readiness, fake stream health, side rails, or copied log-console wording.
+
+## Log Trend Count English Locale Copy - 2026-05-08
+
+- Hierarchy: keep trend row count text inside the existing compact log trend summary, without adding a log-volume legend, quality badge, side rail, or explanatory chart copy.
+- Density: preserve the hour/count/meta rhythm so query controls, summary cards, log table, and selected-log evidence remain one-scan.
+- Anti-AI-slop: avoid English locale catalogs leaking Chinese count text, fake live log volume, copied log-product phrasing, or translated-string branching while changing only the locale-owned count label.
+- Operator workflow clarity: trend rows still reflect the same hourly stats payload; live/history toggles, filters, selected log inspection, and handoff links stay unchanged.
+- Context visibility: hour buckets, count values, trend meta, selected log context, trace/span/entity handoffs, and disabled reasons remain stable while the English count copy becomes locale-owned.
+
+## Log Stream Compatibility Route Boundary - 2026-05-05
+
+- Hierarchy: keep the compatibility URL as a thin entry into the canonical live log workbench; the wrapper must not add another shell, toolbar, fake stream surface, or static legacy panel.
+- Density: preserve the dense EventSource-backed virtualized stream posture from `LogManagePage`, with forced stream view and hidden view toggle.
+- Anti-AI-slop: route migration is infrastructure only; avoid fake connected states, fake zero-log panels, fake filters, copied Angular chrome, or new explanatory product prose.
+- Operator workflow clarity: live stream state, filters, EventSource lifecycle, virtualized rows, query state, and time/context handoffs stay in `log-manage-page.tsx`.
+- Context visibility: compatibility route marker, forced stream mode, hidden view toggle, and canonical log workbench owner must remain visible to route/source contracts.
+- Related trace preview copy update: keep the log-detail-first drilldown, preview drawer hierarchy, compact loading/empty states, and disabled full-trace handoff unchanged while moving the drawer title, badge, labels, loading, empty, error, and full-trace action copy behind runtime i18n keys.
+- Live stream chrome copy update: keep the view switch, live stream header, status chips, pause/clear/reconnect controls, empty stream state, selected-log rail, and log/trace/code/metric handoffs in the existing dense workbench hierarchy while moving visible chrome copy behind runtime i18n keys. Preserve the EventSource-backed stream posture, compact row density, and selected-context visibility without adding fake connected states, fake logs, new explanatory rails, or copied external log [...]
+
+## Explorer Route Boundary - 2026-05-05
+
+- Hierarchy: keep the query bar, quick filters, trend band, result table, and detail panel inside the existing cold explorer surface; the server wrapper must not add another shell or summary layer.
+- Density: preserve the compact cross-signal workbench density, fixed filter rail, dense table rows, and restrained detail panel with no nested cards or extra gutters.
+- Anti-AI-slop: route migration is infrastructure only; avoid fake query execution, fake saved views, fake zero panels, copied external explorer IA, or new explanatory product prose.
+- Operator workflow clarity: query inputs, run affordance, saved-view/alert/dashboard actions, and trace/log/metric/entity handoffs stay in `explorer-page.tsx`.
+- Context visibility: cold workbench route markers, Chinese copy, cross-signal result rows, and handoff URLs must remain visible to route/parity/source contracts.
+- Copy boundary update: keep the same hierarchy, density, handoff clarity, and cold-workbench context while moving visible labels, row copy, filters, and table/detail labels behind runtime i18n keys; do not add new UI, fake query behavior, or generic explanatory copy.
+- Helper parity update: keep the query header, action row, query controls, quick filters, trend band, result table, detail panel, and handoff links unchanged while mirroring runtime-owned `explorer.*` copy into shared test helpers; do not add saved-view state, fake query execution, dashboard status, or adapter readiness.
+
+## Passport Lock Route Boundary - 2026-05-05
+
+- Hierarchy: keep the shared passport shell, brand lockup, unlock panel, password field, validation message, and locale affordance inside the existing auth surface; the server wrapper must not add another shell or security summary.
+- Density: preserve the Angular-parity wide panel, compact password form, and restrained status message with no nested cards, marketing copy, or fake security-status blocks.
+- Anti-AI-slop: route migration is infrastructure only; avoid fake session claims, fake SSO/security controls, copied SaaS auth IA, or new explanatory product prose.
+- Operator workflow clarity: password state, validation, translated copy, and router redirect stay in `passport-lock-page.tsx`.
+- Context visibility: passport shell markers, angular-wide panel marker, lock form marker, shared ops colors, and failure state ownership must remain visible to auth/parity/source contracts.
+
+## Alert Center Route Boundary - 2026-05-04
+
+- Hierarchy: keep the alert list, evidence filters, selected alert details, and acknowledge/recover/silence/inhibit/close actions inside the existing alert center surface; the server wrapper must not add another shell.
+- Density: preserve the compact operations table, filter row, and closure-action density with no extra card layer, fake status block, or empty gutter.
+- Anti-AI-slop: route migration is infrastructure only; avoid new explanatory product copy or copied competitor page hierarchy.
+- Operator workflow clarity: query-state filters, URL cleanup, mutation feedback, and closure actions stay in the client island where router and alert state are available.
+- Context visibility: entity, topology edge, OTLP signal, trace/span, monitor, and time context must continue flowing into alert evidence and handoff links.
+- Loading-copy i18n update: keep alert center loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake alert counts, incident-health placeholders, explanatory response rails, or extra navigation chrome.
+- Alerts alias boundary update: `/alerts` remains a compatibility entry into the canonical alert center and should redirect through the shared alert query-state normalizer, preserving search/status/severity and entity/topology/signal context while stripping display-only labels; do not render a duplicate alert center shell or add alias-specific rails, copy, fake alert status, or copied SaaS alert IA.
+- Alert alias search-param owner update: `/alerts` and `/alert/center` should stay thin route aliases that delegate raw Next search params to `lib/alert-manage/query-state.ts`; the generic compatibility reader belongs inside the alert query owner so both aliases normalize context consistently without route-local plumbing.
+- Inherited-context copy boundary update: keep the selected-alert evidence context rows, source row semantics, time/monitor/trace evidence, and return-link machine context unchanged while moving only inherited context titles/copy/meta/status text behind runtime messages; do not add fake alert health, new evidence rails, AI triage, or copied incident-console prose.
+- Entity-context severity copy boundary update: keep the alert entity-context summary as a compact status/severity/search strip and move only fallback severity labels behind runtime messages; do not add new severity taxonomies, fake prioritization, or copied incident-console naming.
+- Evidence closure panel chrome copy boundary update: keep the existing evidence-closure panel header, evidence/action summary chips, evidence rows, operation rows, inherited context, and closure actions in place while moving only panel chrome and summary labels behind runtime messages; do not add new evidence lanes, fake root-cause claims, AI triage, or copied incident-console phrasing.
+- Row identity fallback boundary update: keep alert list rows, selected-alert details, status/severity labels, fingerprint/creator evidence, and closure actions in the current compact surface while moving only missing identity/fingerprint/creator facts into the shared localized fallback.
+- Empty selection meta fallback update: keep the unselected-detail prompt, alert list, filters, and closure action affordances unchanged while moving only the empty selected-alert meta marker into the shared localized fallback.
+- Alert-center query-owner update: preserve alert filters, grouped alert rows, selected evidence, closure operations, compatibility URL cleanup, and cross-signal/entity/topology/time context inside the existing client island; parse raw route params in `lib/alert-manage/query-state.ts` before render. Do not add alert health panels, incident-state claims, route-state badges, copied incident-console IA, or fake evidence readiness while moving query ownership.
+
+## Alert Setting Route Boundary - 2026-05-04
+
+- Hierarchy: keep threshold rule search, rule table, enable/edit/delete actions, and create/edit dialog state inside the existing alert setting surface; the server wrapper must not add another shell.
+- Density: preserve the compact threshold-rule authoring and dense table posture, with no extra cards, fake status panels, or explanatory gutters.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative health claims, or copied competitor page hierarchy.
+- Operator workflow clarity: query-state source filters, validation, notification/template context, mutation feedback, and delete confirmation stay in the client island where router and rule state are available.
+- Context visibility: incoming entity, monitor, signal, trace/span, service, environment, and time context must continue reaching the rule creation workflow and return links.
+- Loading-copy i18n update: keep alert setting loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake rule counts, datasource-health placeholders, explanatory threshold rails, or extra alert-navigation chrome.
+- Row fallback boundary update: keep rule name, type, expression, template, labels, enable state, updated time, and row actions in the existing dense table while moving only missing expression/template facts into the shared localized fallback.
+- Evidence and label fallback boundary update: preserve the threshold rule search, dense rule table, evidence prefill strip, create/edit/delete actions, and datasource context while moving only absent evidence-label and empty row-label markers into the shared localized fallback.
+- Evidence query-owner boundary update: keep signal/entity/time evidence visible in the existing prefill strip and create dialog, but parse raw route params in a typed alert-setting query owner before the client island renders. Do not add alert-health claims, extra context rails, fake rule recommendations, or copied incident-console IA.
+
+## Alert Notice Route Boundary - 2026-05-04
+
+- Hierarchy: keep receiver, rule, and template tab workspaces plus evidence prefill inside the existing alert notice console; the server wrapper must not add another shell.
+- Density: preserve the compact cold notification table/editing density, with no extra cards, fake status panels, or explanatory gutters.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative health claims, or copied competitor page hierarchy.
+- Operator workflow clarity: receiver/rule/template pagination, search, CRUD dialogs, validation, delete confirmation, and receiver tests stay in the client island where router and notice state are available.
+- Context visibility: incoming entity, signal, trace/span, service, environment, and time context must continue reaching rule prefill, evidence rows, and return links.
+- Cache-key boundary update: receiver/rule list loads should use an explicit query-derived workbench cache key without changing the tab hierarchy, table density, modal authoring flow, loading copy, or signal evidence visibility.
+- Rule receiver-empty copy boundary update: keep the aligned modal form, required receiver row, hidden CSV input, and compact cold multi-select container unchanged while moving only the empty-receiver prompt behind runtime messages; do not add setup guidance, fake receiver health, or extra channel recommendation rails.
+- Loading-copy helper update: keep alert notice loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add receiver-health placeholders, fake delivery readiness, product prose, or extra notification workflow chrome.
+- Evidence query-owner boundary update: preserve the same receiver/rule/template tabs, rule prefill, modal authoring state, evidence rows, and return action, but parse raw signal route params in the alert-notice query owner before the client island renders. Do not add notification-health scores, fake delivery readiness, extra evidence rails, or copied incident-console IA.
+
+## Alert Silence Route Boundary - 2026-05-04
+
+- Hierarchy: keep silence search, selected rule state, create/edit dialog, delete confirmation, and incoming alert evidence context inside the existing silence surface; the server wrapper must not add another shell.
+- Density: preserve the compact cold silence table and authoring density, with no extra cards, fake status panels, or explanatory gutters.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative health claims, or copied competitor page hierarchy.
+- Operator workflow clarity: query-state filters, label options, create/edit/delete flows, validation, and mutation feedback stay in the client island where router and silence state are available.
+- Context visibility: incoming entity, topology, monitor, signal, trace/span, service, environment, and time context must continue reaching silence prefill, return links, and evidence rows.
+- Loading-copy i18n update: keep alert silence loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake mute coverage, incident-health placeholders, explanatory maintenance-window rails, or extra alert-navigation chrome.
+- Day fallback boundary update: keep silence rule name, match strategy, label count, days evidence, selected-rule detail, and row actions in the existing compact silence table while moving only missing day facts into the shared localized fallback.
+- Empty selection meta fallback update: preserve silence search, selected-rule detail prompt, create/edit/delete/batch actions, label option loading, and evidence prefill while moving only the empty selected-rule meta marker into the shared localized fallback.
+- Evidence label fallback update: preserve silence evidence strip hierarchy, return action, row grid density, and signal/time/entity context visibility while moving only the absent prefill-label marker into the shared localized fallback.
+- Evidence query-owner boundary update: preserve the same silence table, editor state, delete confirmation, evidence strip, and return action, but parse raw alert/signal route params in the alert-silence query owner before the client island renders. Do not add mute-health scores, fake matching coverage, extra evidence rails, or copied incident-console IA.
+
+## Alert Silence Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep silence search, selected rule state, create/edit dialog, delete confirmation, batch selection, enable toggle, label options, and incoming alert/topology/signal context inside the existing silence surface; cache reuse must not add freshness chrome, fake mute coverage, or incident-console rails.
+- Density: preserve the compact cold silence table and authoring rhythm so filters, selected ids, checked ids, editor state, validation, and mutation feedback remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent maintenance-window readiness, fake matching coverage, or copy SaaS incident-silencing language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled silence data briefly, while Refresh, save, enable toggle, single delete, and batch delete continue to bump `refreshTick`; search still invalidates through the query-derived silence URL.
+- Context visibility: silence list URL, refresh tick, checked ids, selected rule, draft state, return context, evidence prefill, label options, and localized loading copy remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Alert Inhibit Route Boundary - 2026-05-04
+
+- Hierarchy: keep inhibit search, selected rule state, create/edit dialog, delete confirmation, and incoming alert evidence context inside the existing inhibit surface; the server wrapper must not add another shell.
+- Density: preserve the compact cold inhibit table and authoring density, with no extra cards, fake status panels, or explanatory gutters.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative health claims, or copied competitor page hierarchy.
+- Operator workflow clarity: query-state filters, label options, create/edit/delete flows, validation, mutation feedback, and batch delete stay in the client island where router and inhibit state are available.
+- Context visibility: incoming entity, topology, monitor, signal, trace/span, service, environment, and time context must continue reaching inhibit prefill, return links, and evidence rows.
+- Loading-copy i18n update: keep alert inhibit loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake suppression coverage, matching-readiness placeholders, explanatory incident rails, or extra inhibit-navigation chrome.
+- Equal-label fallback boundary update: keep inhibit rule name, source/target counts, equal-label evidence, timers, selected-rule detail, and row actions in the existing compact inhibit table while moving only missing equal-label facts into the shared localized fallback.
+- Empty selection meta fallback update: preserve inhibit search, selected-rule detail prompt, create/edit/delete/batch actions, label option loading, and evidence prefill while moving only the empty selected-rule meta marker into the shared localized fallback.
+- Evidence label fallback update: preserve the source/target/equal evidence strip, return action, row grid density, and signal/time/entity context visibility while moving only absent source/target/equal prefill-label markers into the shared localized fallback.
+- Evidence query-owner boundary update: preserve the same inhibit table, selected-rule state, batch delete, editor state, source/target/equal evidence strip, and return action, but parse raw alert/signal route params in the alert-inhibit query owner before the client island renders. Do not add suppression-health scores, fake matching readiness, extra evidence rails, or copied incident-console IA.
+
+## Alert Inhibit Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep inhibit search, selected rule state, create/edit dialog, delete confirmation, batch selection, enable toggle, label options, and incoming alert/topology/signal context inside the existing inhibit surface; cache reuse must not add freshness chrome, fake suppression coverage, or incident-console rails.
+- Density: preserve the compact cold inhibit table and authoring rhythm so filters, selected ids, checked ids, editor state, validation, and mutation feedback remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent suppression readiness, fake matching coverage, or copy SaaS incident-suppression language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled inhibit data briefly, while Refresh, save, enable toggle, single delete, and batch delete continue to bump `refreshTick`; search still invalidates through the query-derived inhibit URL.
+- Context visibility: inhibit list URL, refresh tick, checked ids, selected rule, draft state, return context, evidence prefill, label options, and localized loading copy remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Alert Group Route Boundary - 2026-05-04
+
+- Hierarchy: keep group search, selected group state, create/edit dialog, delete confirmation, and incoming alert evidence context inside the existing group surface; the server wrapper must not add another shell.
+- Density: preserve the compact cold grouping table and authoring density, with no extra cards, fake status panels, or explanatory gutters.
+- Anti-AI-slop: route migration is infrastructure only; avoid new product prose, decorative health claims, or copied competitor page hierarchy.
+- Operator workflow clarity: query-state filters, label options, create/edit/delete flows, validation, mutation feedback, and batch delete stay in the client island where router and group state are available.
+- Context visibility: incoming entity, topology, monitor, signal, trace/span, service, environment, and time context must continue reaching group prefill, return links, and evidence rows.
+- Loading-copy i18n update: keep alert group loading feedback runtime-owned on the existing `ClientWorkbench` route island; do not add fake grouping coverage, deduplication-readiness placeholders, explanatory incident rails, or extra group-navigation chrome.
+- Label fallback boundary update: keep group rule name, enable state, grouping labels, timers, selected-rule detail, and row actions in the existing dense grouping table while moving only missing grouping-label facts into the shared localized fallback.
+- Empty selection meta fallback update: preserve group search, selected-rule detail prompt, create/edit/delete/batch actions, label option loading, and evidence prefill while moving only the empty selected-rule meta marker into the shared localized fallback.
+- Evidence label fallback update: preserve the grouping evidence strip, return action, row grid density, and signal/time/entity context visibility while moving only the absent prefill-label marker into the shared localized fallback.
+- Evidence query-owner boundary update: preserve the same group table, selected-rule state, batch delete, editor state, grouping evidence strip, and return action, but parse raw signal route params in the alert-group query owner before the client island renders. Do not add grouping-health scores, fake deduplication readiness, extra evidence rails, or copied incident-console IA.
+
+## Alert Group Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep group search, selected group state, create/edit dialog, delete confirmation, batch selection, enable toggle, label options, and incoming alert/topology/signal context inside the existing group surface; cache reuse must not add freshness chrome, fake grouping coverage, or incident-console rails.
+- Density: preserve the compact cold grouping table and authoring rhythm so filters, selected ids, checked ids, editor state, validation, and mutation feedback remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent grouping readiness, fake deduplication quality, or copy SaaS incident-grouping language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled group data briefly, while Refresh, save, enable toggle, single delete, and batch delete continue to bump `refreshTick`; search still invalidates through the query-derived group URL.
+- Context visibility: group list URL, refresh tick, checked ids, selected group, draft state, evidence prefill, label options, and localized loading copy remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Alert Evidence Closure Copy Boundary - 2026-05-05
+
+- Hierarchy: keep the selected-alert evidence chain in the alert center view-model as five compact rows: entity, metrics, logs, traces, and topology; only move row titles, fallbacks, and helper meta copy behind runtime messages.
+- Density: preserve the current concise operation row list for acknowledge, recover, threshold, notice, grouping, silence, inhibit, automation, and close; do not introduce a new action drawer, summary rail, or fake remediation checklist.
+- Anti-AI-slop: avoid fake alert causality, fake runbook automation, copied incident-desk IA, or generic workflow prose while localizing the existing alert closure evidence copy.
+- Operator workflow clarity: evidence links must still carry entity, service, environment, trace/span, monitor, topology, and time context into the destination workbenches, and automation remains suggestion-only with human confirmation.
+- Context visibility: selected alert group, active count, target entity/service, trace context, and topology return context remain visible through the current rows and href contracts.
+
+## Alert Integration Source Copy Boundary - 2026-05-05
+
+- Hierarchy: keep the existing integration source rail, selected-source document panel, and token-management header action; this slice only moves static chrome/source labels behind runtime messages.
+- Density: preserve the compact cold source-list posture, narrow rail, markdown document surface, and restrained header actions without adding setup cards, readiness summaries, or fake delivery analytics.
+- Anti-AI-slop: avoid new product prose, copied SaaS integration catalog IA, fake provider health, or decorative onboarding claims while localizing the existing labels and fallback copy.
+- Operator workflow clarity: selected integration source, token-management entry, doc fallback behavior, source navigation, and markdown rendering stay unchanged and continue to map to existing alert integration assets.
+- Context visibility: source id, provider name, icon, doc panel, fallback-guide state, and token route remain visible to the existing page contract and operator handoff.
+- Mermaid render-state copy update: keep the embedded diagram host as a restrained markdown artifact inside the document panel, with only pending/error render-state copy moved behind runtime messages. Do not add diagram summaries, fake provider topology, copied setup wizards, or extra health/readiness claims; source id, provider document context, and the original Mermaid SVG host stay visible.
+
+## Alert Integration Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep the same source rail, fact rows, posture rows, markdown document panel, and token-management handoff; changing fallback locale order must not add onboarding cards, readiness panels, or setup wizards.
+- Density: preserve the compact provider list and three-row fact/posture summaries so operators can scan source, document status, and token path without extra chrome.
+- Anti-AI-slop: do not invent delivery health, provider readiness, validation success, copied SaaS integration catalog copy, or fake alert pipeline state while correcting fallback order.
+- Operator workflow clarity: default integration copy resolves from runtime messages with an English fallback when no caller locale is available; selected source ids, document-loaded/fallback state, and token-management route stay data-driven.
+- Context visibility: source id, provider name key, icon path, doc fallback flag, source navigation, and markdown rendering context remain visible to the existing page and view-model contracts.
+
+## Global Loading Copy Boundary - 2026-05-05
+
+- Hierarchy: keep the global route loading shell as one centered status surface with spinner, title, and short copy; this slice only moves the title/copy into runtime messages.
+- Density: preserve the compact cold matte loading panel, status semantics, fixed spinner, and no-fetch behavior without adding progress steps, skeleton dashboards, or fake readiness.
+- Anti-AI-slop: avoid invented loading stages, product claims, animated dashboards, or generic onboarding prose while localizing the existing wait state.
+- Operator workflow clarity: loading remains a plain route transition state that says data is being prepared and does not imply background health checks or successful collection.
+- Context visibility: `role=status`, `aria-busy`, `aria-live`, loading marker, and no external API/http references remain visible to the existing SSR test.
+
+## Global Loading Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep the global route loading shell as the same centered status surface; changing fallback locale order must not add progress steps, skeleton dashboards, health summaries, or a second loading layer.
+- Density: preserve the compact spinner/title/copy rhythm and the existing cold matte panel so route transitions remain quiet and quick to parse.
+- Anti-AI-slop: do not invent loading stages, readiness claims, collection status, product onboarding prose, or animated dashboard decoration while correcting fallback order.
+- Operator workflow clarity: shared loading title/copy resolves from runtime messages with an English fallback when no caller locale is available; the state still only means page data is being prepared.
+- Context visibility: `role=status`, `aria-busy`, `aria-live`, `data-app-route-loading`, spinner marker, and no-fetch/no-http behavior stay unchanged.
+
+## Quiet Route Pending Boundary - 2026-05-14
+
+- Hierarchy: root `app/loading.tsx` should be a quiet route-pending affordance inside the existing shell, not a second workbench panel competing with the actual page.
+- Density: use a single thin pending line and reserved content area; do not show centered cards, large spinners, title/copy blocks, skeleton dashboards, or product explanation while a segment compiles or streams.
+- Anti-AI-slop: do not claim page data is being prepared, backend health is being checked, or collection state is known from a framework route pending fallback.
+- Operator workflow clarity: real page/client workbench loaders can still expose route-owned copy if a resource load is genuinely slow; the framework fallback should stay visually quiet so short dev/prod route waits do not read as product latency.
+- Context visibility: `role=status`, `aria-busy`, route loading marker, and an explicit pending indicator remain available to the app loading contract without external API/http references.
+
+## Client Workbench Deferred Loading Boundary - 2026-05-14
+
+- Hierarchy: keep the shared `ClientWorkbench` loading shell for genuinely slow route islands, but do not replace the page with a centered spinner/title/copy during fast route hops or immediately resolved settled-cache reads.
+- Density: preserve a quiet reserved pending area first, then reveal the existing compact spinner only after the defer window; do not add skeleton dashboards, progress steppers, route-health panels, or explanatory product prose.
+- Anti-AI-slop: do not imply backend health, collection success, freshness, or readiness from the deferred state; short waits should feel like navigation latency, not a product claim.
+- Operator workflow clarity: cached or fast workbench payloads should settle into their real page before users can read "Loading workspace"; real slow loads still expose `role=status`, `aria-busy`, and route-owned loading copy after the delay.
+- Context visibility: cache keys, settled TTL forwarding, error/login redirect behavior, caller-provided loading copy, and the existing global spinner markers remain visible to focused component contracts.
+
+## Shared Confirm Dialog Copy Boundary - 2026-05-05
+
+- Hierarchy: keep `ColdConfirmDialog` as the shared compact confirmation modal with existing title, body copy, warning icon, and footer actions; this slice only moves the default kicker and pending label behind runtime messages.
+- Density: preserve the small modal width, two-button footer, pending disable behavior, and no extra evidence rail or summary layer.
+- Anti-AI-slop: avoid invented confirmation steps, generic safety prose, fake approval metadata, or new alert-specific wording in the shared component.
+- Operator workflow clarity: alert silence/inhibit/group/notice/setting consumers keep their own concrete title/body/actions while the shared modal supplies only neutral operation/pending chrome.
+- Context visibility: `data-cold-confirm-dialog`, footer actions, caller-provided labels, and pending state remain visible to source and component tests.
+
+## Shared Label Record Input Copy Boundary - 2026-05-05
+
+- Hierarchy: keep `LabelRecordInput` as the compact inline key/value label selector with existing equal-width key/value fields, suggestion popovers, hidden input, and row actions; this slice only moves default placeholders and add/remove labels behind runtime messages.
+- Density: preserve the single-row grid, 76px action column, fixed anchored popovers, and no chip-list regression or extra form helper text.
+- Anti-AI-slop: avoid adding explanatory label-management prose, fake label validation summaries, or alert-specific wording to the shared control.
+- Operator workflow clarity: alert silence/inhibit/notice/setting consumers can still pass caller-owned labels while the shared component supplies neutral defaults for generic tag editing.
+- Context visibility: `data-cold-label-selector-*` markers, hidden form value, key/value inputs, add/remove buttons, and suggestion popovers remain visible to source and component tests.
+
+## LabelRecordInput Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep `LabelRecordInput` as the same compact key/value label selector; changing fallback locale order must not add helper text, validation panels, chip metadata, or a second action row.
+- Density: preserve the equal-width key/value grid, 76px add/remove action column, and anchored suggestion popovers so alert and settings forms stay scan-friendly.
+- Anti-AI-slop: do not invent label taxonomy, fake validation summaries, suggestion counts, or copied label-management prose while correcting the fallback catalog order.
+- Operator workflow clarity: shared placeholder/add/remove copy resolves from runtime messages with an English fallback when no caller locale is available; caller-provided labels and label options remain authoritative.
+- Context visibility: hidden input ownership, row update/remove behavior, draft commit behavior, popover positioning, and `data-cold-label-selector-*` markers stay unchanged.
+
+## Shared Tag Input Copy Boundary - 2026-05-05
+
+- Hierarchy: keep `TagInput` as the compact chip-based tag editor with removable chips, inline draft input, hidden form value, and fixed anchored suggestion popover; this slice only moves the add-tag placeholder and remove aria label behind runtime messages.
+- Density: preserve the small chip rhythm, single inline input, popover dimensions, and no select/datalist fallback or helper-copy expansion.
+- Anti-AI-slop: avoid adding tag-management prose, validation summaries, fake suggestion counts, or alert-specific semantics to the shared control.
+- Operator workflow clarity: alert grouping/inhibit consumers keep searchable tag suggestions and caller-provided placeholder copy while the shared component supplies neutral defaults once tags are present.
+- Context visibility: `data-cold-tag-*` markers, hidden value, removable chip buttons, draft input, suggestion popover, and fixed-position owner remain visible to source and component tests.
+
+## Shared Number Stepper Copy Boundary - 2026-05-05
+
+- Hierarchy: keep `NumberStepper` as the compact inline numeric control with one text input and two icon-only step buttons; this slice only moves default decrement/increment screen-reader labels behind runtime messages.
+- Density: preserve the 8px-high control rhythm, narrow action column, border separators, and no helper text, label row, slider, or stepper-card expansion.
+- Anti-AI-slop: avoid adding numeric-range explanations, fake validation summaries, or settings-specific wording to the shared control.
+- Operator workflow clarity: settings/server consumers can still pass caller-owned labels while the shared component supplies neutral accessible defaults for generic numeric editing.
+- Context visibility: `data-cold-number-stepper-*` markers, input value, decrement/increment actions, min/max/step behavior, and screen-reader labels remain visible to source and component tests.
+
+## Alert Notice Template Type Copy Boundary - 2026-05-05
+
+- Hierarchy: keep the alert notice template editor as the existing aligned modal form with name, type selector, preset pill, and code editor; this slice only moves template type and preset labels behind runtime messages.
+- Density: preserve the 132px label column, single full-width select, readonly preset pill, and 220px code editor without adding setup cards, channel descriptions, or helper rails.
+- Anti-AI-slop: avoid invented delivery-channel guidance, fake provider health, copied SaaS catalog copy, or new onboarding prose while localizing the existing template channel labels.
+- Operator workflow clarity: operators still choose the same notice channel values and retain the same custom `typeOptions` escape hatch for controller-provided labels.
+- Context visibility: `data-alert-notice-template-*` markers, option values, preset state, readonly state, and code-editor language switching remain visible to source and component tests.
+
+## Observability Waterfall Copy Boundary - 2026-05-05
+
+- Hierarchy: keep `ObservabilityWaterfall` as the compact trace/event timing visualization with the same overview row, span rows, event badges, and timeline marks; this slice only moves overview/count copy behind runtime or caller-provided labels.
+- Density: preserve the fixed-height lane rhythm, minimap, compact span rows, event markers, and full-width stacked axis without adding a summary rail or explanation block.
+- Anti-AI-slop: avoid invented trace analysis prose, fake root-cause claims, copied trace-product IA, or decorative event storytelling while localizing the existing timeline/count labels.
+- Operator workflow clarity: span duration, event count, timeline axis, and event attribute drilldown remain visible without changing row ordering, selection semantics, or detail behavior.
+- Context visibility: `data-waterfall-*` markers, supplied `spanLabel`/`durationLabel`/`timelineLabel`, overview counts, row event counts, selected event state, and minimap lanes remain visible to component and source contracts.
+
+## Trace Attribution Diagnostics Copy Boundary - 2026-05-06
+
+- Hierarchy: keep attribution diagnostics as a compact evidence block inside the trace workbench detail rail and waterfall drawer; this slice only moves the block title, accessibility label, and present/missing state text behind runtime i18n keys.
+- Density: preserve the two-column diagnostic rows and `hertzbeat.*` badge without adding explanatory rails, new cards, or fake attribution health summaries.
+- Anti-AI-slop: avoid invented root-cause copy, external trace-product language, or simulated attribution success while localizing only the existing diagnostic chrome.
+- Operator workflow clarity: operators must still see which `hertzbeat.entity_id`, collector, template, workspace, and entity-name attributes are present or missing before opening entity/log/metric handoffs.
+- Context visibility: `data-trace-manage-attribution-diagnostics`, row state markers, `hertzbeat.*` labels, and row meta/value evidence remain visible to the source and page tests.
+
+## Trace Waterfall Drawer Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the waterfall drawer as the trace-detail drilldown layer with the same header, stats, waterfall, attribution, event-detail, and handoff order; this slice only moves drawer chrome and status copy behind runtime i18n keys.
+- Density: preserve compact stats and waterfall labels so span count, error count, events, links, selected event, and selected span evidence remain scan-first.
+- Anti-AI-slop: avoid generated root-cause summaries, AI explanations, demo spans, external APM terminology, or new onboarding copy while localizing the drawer.
+- Operator workflow clarity: logs, metrics, and entity handoffs remain explicit, and disabled states stay tied to actual missing trace/entity identifiers.
+- Context visibility: event-vs-span copy, selected event facts, timeline labels, and `hertzbeat.*` attribution diagnostics stay visible inside the drawer.
+
+## OTLP Center Cache-Key Boundary - 2026-05-06
+
+- Hierarchy: keep the OTLP intake catalog, readiness/self-check rows, source search, entity return, and signal handoffs inside the existing client island; this slice only makes the loader cache key explicit.
+- Density: preserve the current cold catalog rail, compact source cards, readiness strips, and signal ribbons without adding summary rails, nested cards, or decorative setup panels.
+- Anti-AI-slop: avoid fake ingestion readiness, copied external catalog IA, or product prose while deriving the cache key from the actual overview/guide/bindings resources.
+- Operator workflow clarity: cached first-screen loads must still reflect the same OTLP overview, setup guide, entity binding, search, and route-context handoff behavior.
+- Context visibility: `/ingestion/otlp/overview`, `/ingestion/otlp/guide`, `/ingestion/otlp/bindings`, entity return parameters, and signal links remain visible to source and page contracts.
+
+## Events Alias Compatibility Boundary - 2026-05-06
+
+- Hierarchy: keep `/events` as a thin compatibility entry into the canonical log workbench, not a second rendered log shell or a new events console.
+- Density: preserve the existing dense log explorer table, query row, trend band, and detail panel after redirect; do not add compatibility banners, summary cards, or explanatory rails.
+- Anti-AI-slop: avoid copied external log-explorer IA, fake event timelines, or marketing prose while moving the alias onto the shared route normalizer.
+- Operator workflow clarity: legacy filters, content search, severity, trace/span, time context, entity context, and return path should all be normalized before landing on `/log/manage?view=list`.
+- Context visibility: the alias must strip display-only labels while preserving machine context so the canonical log workbench still owns evidence handoff, cache keys, and operator navigation.
+- Events alias search-param owner update: `/events` should delegate raw Next search params to `lib/log-manage/query-state.ts`; the generic compatibility reader belongs inside the log query owner so route aliases stay thin and log context normalization remains consistent with the canonical workbench.
+
+## AppFrame Header Cache Boundary - 2026-05-06
+
+- Hierarchy: keep setup progress, notification bell, quick settings, locale menu, and user menu in the current compact header; this slice only extends shared header-resource reuse.
+- Density: preserve the 64px header rhythm and compact dropdowns without adding a summary rail, setup card, or duplicate alert widget.
+- Anti-AI-slop: avoid fake freshness, readiness, or status claims; cache only recently loaded real monitor, collector, entity, definition, governance, and alert resources.
+- Operator workflow clarity: route changes can still recompute route-specific setup baseline locally while reusing the same just-loaded header summary resources.
+- Context visibility: locale remains the cache dimension for translated header copy, while the actual backend resource endpoints stay owned by AppFrame and visible to source contracts.
+
+## Settings Compatibility Redirect Boundary - 2026-05-06
+
+- Hierarchy: keep `/setting` and `/setting/settings` as thin compatibility entries into the existing system-config console, not duplicate settings shells.
+- Density: preserve the current compact settings header, left menu, and config form after redirect without adding compatibility banners, setup cards, or routing explanation text.
+- Anti-AI-slop: avoid fake admin domains, copied SaaS settings IA, or new product prose while moving the redirect target into one shared owner.
+- Operator workflow clarity: both legacy entries should preserve machine query context such as tab, focus, returnTo, and audit mode before landing on `/setting/settings/config`.
+- Context visibility: return labels and timestamp params continue to be normalized by the shared compatibility search-param helper while the settings layout owns the visible navigation.
+
+## Login Compatibility Redirect Boundary - 2026-05-06
+
+- Hierarchy: keep `/login` as a thin compatibility entry into the existing passport login shell, not a second auth screen.
+- Density: preserve the current centered passport surface, locale affordance, login form, and footer after redirect without banners, onboarding panels, or explanatory auth copy.
+- Anti-AI-slop: avoid fake security claims, SaaS sign-in patterns, or new marketing prose while moving the alias target into the passport login controller.
+- Operator workflow clarity: guard source and post-auth redirect context should survive the alias so operators land back on the intended workbench after authentication.
+- Context visibility: return-label cleanup remains in the shared compatibility search-param helper, while the passport controller owns the login entrypoint and post-login redirect behavior.
+
+## Public Status Compatibility Redirect Boundary - 2026-05-06
+
+- Hierarchy: keep `/status/public` as a thin compatibility entry into the existing public status page, not a second status surface.
+- Density: preserve the current public status shell, component health history, incident feed, locale switcher, and footer after redirect without banners or explanation rails.
+- Anti-AI-slop: avoid fake uptime, fake incident health, SaaS-style status branding, or new marketing copy while moving the alias target into the status-center controller.
+- Operator workflow clarity: component, status, year, and return context should survive the alias so shared public status filters and incident history remain the canonical workflow.
+- Context visibility: query normalization stays in the shared compatibility search-param helper, while status-center owns public org/component/incident data and visible page behavior.
+
+## Settings MCP Alias Compatibility Boundary - 2026-05-06
+
+- Hierarchy: keep `/setting/settings/mcp-server` as a thin legacy settings entry into the existing system-config console, not a separate MCP settings screen or navigation branch.
+- Density: preserve the current settings layout, config form, and left-menu rhythm after redirect without compatibility banners, explanatory panels, or duplicate admin controls.
+- Anti-AI-slop: avoid fake MCP readiness, copied integration-center IA, or new settings prose while keeping the alias target owned by the settings layout navigation helper.
+- Operator workflow clarity: tab, focus, return, and audit query context should survive the alias so operators land in the canonical config workflow with their original intent intact.
+- Context visibility: search-param normalization stays in the shared compatibility helper while `lib/setting-settings-layout/navigation.ts` remains the single owner of the config redirect target.
+
+## Monitor Catch-All Compatibility Boundary - 2026-05-06
+
+- Hierarchy: keep unknown `/monitors/*` routes and the monitor not-found boundary as thin returns to the canonical monitor list, not a separate fallback screen.
+- Density: preserve the existing dense monitor workbench, selected-row rail, batch actions, and entity return context after redirect without banners, empty shells, or explanatory detours.
+- Anti-AI-slop: avoid fake monitor status, copied inventory IA, or new product prose while routing malformed monitor paths through the same monitor navigation owner.
+- Operator workflow clarity: legacy query context such as app, entity, labels, time range, return path, and pagination should survive catch-all redirects when the route has search params.
+- Context visibility: the monitor-manage navigation helper remains the single owner for `/monitors` compatibility targets while the list workbench continues to own visible monitor evidence and actions.
+
+## Entity Catch-All Compatibility Boundary - 2026-05-06
+
+- Hierarchy: keep unknown `/entities/*` routes and the entity not-found boundary as thin returns to the canonical entity catalog, not a separate fallback screen.
+- Density: preserve the existing catalog search, status/type filters, entity counts, create/import/discovery entries, and per-row evidence actions after redirect without banners or explanatory shells.
+- Anti-AI-slop: avoid fake entity health, copied resource-catalog IA, or new product prose while routing malformed entity paths through the shared entity query owner.
+- Operator workflow clarity: legacy search, type, status, time, source, and return context should survive catch-all redirects when the route has search params.
+- Context visibility: `lib/entity-manage/query-state.ts` remains the single owner for `/entities` compatibility targets while the entity list workbench continues to own visible catalog evidence and handoffs.
+
+## Root Overview Compatibility Boundary - 2026-05-06
+
+- Hierarchy: keep `/` as a thin product-entry redirect into the existing overview console, not a duplicate landing page, dashboard shell, or marketing hero.
+- Density: preserve the current overview status grid, setup/support panels, summary cards, impacted entities, alert timeline, and quick entries after redirect without compatibility banners or explanatory rails.
+- Anti-AI-slop: avoid fake readiness, fake health, copied SaaS home IA, or product-positioning prose while moving the redirect target into a shared overview navigation owner.
+- Operator workflow clarity: query context such as source, return path, entity, service, environment, and time filters should survive the root entry so operators land in the same overview workflow with their machine context intact.
+- Context visibility: `lib/overview/navigation.ts` remains the single owner for the `/overview` compatibility target while the overview client island continues to own visible evidence, alert-derived handoffs, and cache keys.
+
+## Dashboard Alias Overview Owner Boundary - 2026-05-06
+
+- Hierarchy: keep `/dashboard` as a legacy alias into the same overview console target owned by `lib/overview/navigation.ts`, not a second dashboard IA branch.
+- Density: preserve the current overview console after redirect without adding dashboard-only banners, summary rails, shell wrappers, or route explanation text.
+- Anti-AI-slop: avoid fake dashboard widgets, copied SaaS dashboard naming, or product-positioning copy while removing the duplicate `/overview` literal from the dashboard alias helper.
+- Operator workflow clarity: legacy dashboard query context should pass through the same overview compatibility normalizer as `/`, so return path, entity, service, environment, and time context behave consistently.
+- Context visibility: overview navigation remains the single compatibility target owner while `lib/dashboard/navigation.ts` is only a named alias delegate for old links.
+- Dashboard alias search-param type owner update: the `/dashboard` route should import both the redirect helper and raw Next search-param type from `lib/dashboard/navigation.ts`, keeping route-boundary typing and query ownership behind the dashboard alias delegate instead of reaching back into the generic compatibility helper.
+
+## Alert Topology Context Panel Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the existing compact topology context panel inside alert center, with the same service/entity title, source pills, edge id, time context, and topology return action.
+- Density: preserve the current single-panel treatment and pill row; do not add a topology summary rail, route explanation, or extra impact cards.
+- Anti-AI-slop: avoid fake blast-radius claims, copied topology-console phrasing, or invented dependency evidence while moving only visible panel labels behind runtime messages.
+- Operator workflow clarity: alert-to-topology context must remain obvious through the same `data-alert-topology-context` contract, source kind chip, edge id chip, and sanitized return href.
+- Context visibility: source labels should be translated at render time while machine context such as `viewMode`, `sourceKind`, `edgeId`, environment, and time range remains unchanged.
+
+## Alert Severity Filter Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the existing compact alert-center severity selector inside the single query row; this slice only moves fallback option labels behind runtime messages.
+- Density: preserve the current select size, option order, and filter grouping without adding severity badges, helper text, or another filter panel.
+- Anti-AI-slop: avoid invented severity taxonomy or copied incident-console terms; use the existing HertzBeat warning/critical/emergency/error/info/unknown vocabulary.
+- Operator workflow clarity: the severity filter must keep the same query-state values so alert filtering, entity context summaries, and compatibility URLs remain stable.
+- Context visibility: translated labels can change per locale, but the machine values `critical`, `error`, `warning`, `info`, and `unknown` remain unchanged.
+
+## Entity List Surface Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the cold entity catalog as a dense object-first list with the compact header, command row, metrics strip, query row, table, and row actions in the same order.
+- Density: preserve the current full-width table rhythm and equal-width command buttons without adding summary rails, onboarding copy, nested cards, or fake catalog health panels.
+- Anti-AI-slop: avoid invented entity-readiness claims, copied SaaS resource-catalog language, AI triage wording, or decorative status summaries while moving only visible labels behind runtime messages.
+- Operator workflow clarity: create, discovery, import, owner, log, trace, evidence, status, and row progress labels must stay tied to real entity rows and existing route handoffs.
+- Context visibility: translated copy can vary by locale, but entity hrefs, row status values, monitor/alert/relation counts, updated time, search state, and `data-entity-list-*` contracts remain unchanged.
+
+## Settings Governance Surface Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the settings overview as the existing compact WorkbenchPage with current-route governance rows under the platform governance section.
+- Density: preserve the current RowList treatment without adding future-domain navigation, roadmap cards, health blocks, or explanatory rails.
+- Anti-AI-slop: avoid fake governance domains, copied SaaS admin taxonomy, AI foundation claims, or visible routes that are not implemented.
+- Operator workflow clarity: API access, notifications, templates/plugins, login/lock, config, and MCP alias rows must stay tied to current implemented routes or clearly marked foundation-only routes.
+- Context visibility: translated row titles/copy can change, but route arrays, roadmap-only docs, forbidden future app routes, and current-route-only navigation policy remain unchanged.
+
+## Alert Notice Product Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the notification workbench as the existing receiver/rule/template tabbed console with metrics, lane rows, dense tables, and edit dialogs in the same structure.
+- Density: preserve current compact metric cards, lane row summaries, status copy, row fallbacks, and editor placeholders without adding explanatory rails or new delivery health panels.
+- Anti-AI-slop: avoid fake delivery guarantees, copied incident-routing product language, or invented notification channels while moving visible product copy behind runtime messages.
+- Operator workflow clarity: receiver targets, template previews, rule delivery state, enabled/disabled state, and placeholder examples must remain tied to real form fields and current backend resources.
+- Context visibility: translated copy can vary by locale, but receiver/rule/template IDs, selected rows, channel type keys, route query state, and evidence-return context remain unchanged.
+- Receiver target fallback update: preserve the receiver table's channel type, target cell, timestamps, row actions, selected state, and tab density while moving only missing target facts into the shared localized fallback.
+- Template preview fallback update: preserve plain-text preview normalization, raw-template redaction, preset/custom metadata, and dense template rows while moving only empty preview fallback text into the shared localized fallback.
+- Rule receiver fallback update: preserve the rule table's policy name, receiver column, template column, switches, row actions, and evidence-triggered rule tab while moving only empty receiver lists into the existing localized no-receiver state.
+- Evidence labels fallback update: preserve the signal-route evidence strip, return link, context rows, and prefill attribute visibility while moving only empty generated label text into the shared localized fallback.
+- Type badge fallback update: preserve receiver/template type badge placement, receiver-option labels, template rows, channel names, and dense tab rhythm while moving only absent type facts into the shared localized fallback.
+
+## Alert Silence Authoring Field Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the silence authoring modal as the existing compact field grid with match labels, time bounds, selected alert evidence, and validation feedback in place.
+- Density: preserve the current label-record input and time field rhythm without adding helper rails, preview cards, or extra silence guidance.
+- Anti-AI-slop: avoid fake suppression intelligence, invented silence taxonomy, or copied incident-routing prose while moving only placeholder/start/end labels behind runtime messages.
+- Operator workflow clarity: label key/value placeholders and start/end time labels must stay attached to the actual label matcher and time inputs used by create/edit silence rules.
+- Context visibility: translated copy can vary, but matcher structure, field names, rule payload, incoming alert context, and selected-label evidence remain unchanged.
+
+## Alert Inhibit Authoring Field Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the inhibit authoring form as the existing single-column source/target/equal-label rule builder with shortcut pills, preview labels, and validation feedback in place.
+- Density: preserve the current label-record inputs and compact equal-label tag input without adding helper rails, comparison cards, or extra inhibit guidance.
+- Anti-AI-slop: avoid fake suppression intelligence, invented inhibit policy taxonomy, or copied incident-routing prose while moving only field placeholder examples behind runtime messages.
+- Operator workflow clarity: source label, target label, and equal-label placeholders must remain attached to the actual inputs that shape create/edit inhibit rule payloads.
+- Context visibility: translated copy can vary, but matcher structure, source/target/equal field names, shortcut behavior, incoming alert context, and selected-label evidence remain unchanged.
+
+## Alert Group Authoring Field Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the group authoring form as the existing single-column grouping rule builder with enable state, group-label selector, wait, interval, and repeat cadence controls.
+- Density: preserve the compact tag input and numeric cadence controls without adding helper rails, grouping previews, or extra policy guidance.
+- Anti-AI-slop: avoid fake grouping intelligence, invented notification taxonomy, or copied incident-routing prose while moving only the group-label placeholder example behind runtime messages.
+- Operator workflow clarity: the grouping-label placeholder must remain attached to the actual tag input that shapes create/edit group rule payloads.
+- Context visibility: translated copy can vary, but field names, label-key suggestions, cadence values, route context, and rule edit/create structure remain unchanged.
+
+## Alert Notice Rule Label Field Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the notification rule authoring modal as the existing aligned receiver, template, filter, label matcher, schedule, time, and enable-state field stack.
+- Density: preserve the compact searchable label-record input without adding helper rails, routing previews, or extra notification guidance.
+- Anti-AI-slop: avoid fake routing intelligence, invented receiver taxonomy, or copied incident-routing prose while moving only the label value placeholder behind runtime messages.
+- Operator workflow clarity: the label key example and value placeholder must stay attached to the label matcher that shapes create/edit notice rule payloads.
+- Context visibility: translated copy can vary, but label options, receiver/template filtering, schedule values, evidence-return context, and rule edit/create structure remain unchanged.
+
+## Status Setting Dialog Field Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the status settings console as the existing org form, component table, incident table, and compact create/edit dialogs.
+- Density: preserve the current two-column dialog grids and dense admin table rhythm without helper rails, summary cards, or extra status explanations.
+- Anti-AI-slop: avoid fake uptime claims, invented public-status taxonomy, or copied SaaS status-page prose while moving only placeholder examples behind runtime messages.
+- Operator workflow clarity: method, state, label, and component-id placeholders must remain attached to the actual fields that shape component and incident payloads.
+- Context visibility: translated copy can vary, but numeric state/method conventions, labels, incident component ids, public status href, and save/delete behavior remain unchanged.
+
+## Status Setting Evidence View Model Copy Boundary - 2026-05-07
+
+- Hierarchy: keep status facts, component rows, incident rows, and organization overview rows in the existing status settings console evidence stack.
+- Density: preserve compact label/value facts and row meta strings without adding health summaries, uptime claims, or secondary evidence rails.
+- Anti-AI-slop: avoid fake public-status readiness, invented incident confidence, copied status-page product prose, or synthetic component health while moving only evidence labels/state copy behind runtime messages.
+- Operator workflow clarity: state labels, workspace labels, and feedback labels must remain tied to real component/org/incident payload values and existing row placement.
+- Context visibility: translated copy can vary, but component count, incident count, row keys, timestamps, org links, selected mode, and save/delete control flow remain unchanged.
+
+## Status Setting Component Evidence Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep component rows and selectable component evidence rows in the existing status settings console evidence stack.
+- Density: preserve the compact title, copy, and state/time meta rhythm without adding description warnings, helper rails, or empty-detail badges.
+- Anti-AI-slop: do not invent component descriptions, ownership, uptime, endpoints, or health when description data is absent.
+- Operator workflow clarity: real component descriptions remain authoritative; blank descriptions use the shared runtime empty marker in the existing copy slot.
+- Context visibility: component names, states, timestamps, row keys, incident rows, org overview, and save/delete control flow remain unchanged.
+
+## Status Setting Org Link Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep organization profile and feedback links in the existing org overview row instead of creating a secondary contact panel.
+- Density: preserve the compact title, copy, and state/time meta rhythm without adding helper text, badges, or fake support channels.
+- Anti-AI-slop: do not invent home URLs, feedback addresses, ownership, uptime, public support routes, or synthetic contact guidance when org links are absent.
+- Operator workflow clarity: real home and feedback values remain authoritative; blank or whitespace-only links use the shared runtime empty marker in the existing copy slot.
+- Context visibility: organization name, description, state, timestamps, component rows, incident rows, public status handoff, and save/delete control flow remain unchanged.
+
+## Status Setting Empty Row Meta Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep empty component and incident rows in the existing status settings evidence list rather than adding a separate empty-state panel.
+- Density: preserve the compact title, copy, and meta rhythm without adding badges, helper rails, or fake readiness summaries.
+- Anti-AI-slop: do not invent component health, incident status, timestamps, ownership, uptime, or public-support context when the backend returns no rows.
+- Operator workflow clarity: empty component/incident title and copy remain translated by their existing keys; the meta slot uses the shared runtime empty marker.
+- Context visibility: selected mode, org overview, component/incident tables, public status handoff, and save/delete control flow remain unchanged.
+
+## Status Setting Incident Title Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep incident list rows and selectable incident evidence rows in the existing status settings evidence stack.
+- Density: preserve the compact title, copy, and state/component/time meta rhythm without adding incident cards, confidence badges, or helper rails.
+- Anti-AI-slop: do not invent incident names, ids, ownership, impact, uptime, or remediation context when incident identity data is absent.
+- Operator workflow clarity: real incident names and ids remain authoritative; absent id markers use the shared runtime empty marker inside the existing fallback title.
+- Context visibility: latest incident messages, state labels, component counts, timestamps, row keys, org overview, component rows, and save/delete flow remain unchanged.
+
+## Status Setting Component Description Placeholder Boundary - 2026-05-09
+
+- Hierarchy: keep the component description field in the existing create/edit component dialog as the full-width field under component identity and state controls.
+- Density: preserve the compact two-column dialog rhythm and single full-width description row without adding helper rails, preview cards, or extra status-page guidance.
+- Anti-AI-slop: do not invent component taxonomy, endpoints, uptime, owners, or example domains while moving only the description placeholder behind a dedicated runtime key.
+- Operator workflow clarity: the description placeholder must describe the component description payload, not reuse the component name hint.
+- Context visibility: component name, method, state, labels, save/cancel behavior, validation, table rows, and public status handoff remain unchanged.
+
+## Bulletin Manage Dialog Field Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the bulletin manage dialog as the existing name, monitor type, monitor ids, and metrics JSON authoring stack.
+- Density: preserve the current two-column field rhythm and full-width JSON editor without helper rails, sample cards, or extra bulletin guidance.
+- Anti-AI-slop: avoid fake bulletin analytics, invented monitor selection semantics, or copied status-board prose while moving only the monitor-id placeholder behind runtime messages.
+- Operator workflow clarity: the monitor-id example must remain attached to the field that shapes bulletin monitor bindings.
+- Context visibility: translated copy can vary, but monitor id parsing, app type, metrics JSON editor, save/cancel behavior, and bulletin dialog chrome remain unchanged.
+
+## Entity Editor Relation Placeholder Boundary - 2026-05-06
+
+- Hierarchy: keep the relation stage inside the existing entity editor body, after signals and before the definition preview path.
+- Density: preserve the compact two-column relation fields and inline label editor without helper rails, topology previews, or extra relationship guidance.
+- Anti-AI-slop: avoid fake dependency inference, invented service-map copy, or copied topology product language while moving only placeholder examples behind runtime messages.
+- Operator workflow clarity: component, implementation, and language placeholder examples must stay attached to the fields that shape entity relation arrays.
+- Context visibility: translated copy can vary, but component arrays, implemented-by arrays, language arrays, labels, and save payload behavior remain unchanged.
+
+## Entity Editor Attribution Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep telemetry attribution rows inside the existing entity editor attribution panel; changing fallback locale order must not add ownership scorecards, source rails, topology previews, or save blockers.
+- Density: preserve the compact five-row attribution rhythm for identity, monitor binding, owner, system/environment, and discovery return.
+- Anti-AI-slop: do not invent identity confidence, monitor readiness, owner policy, telemetry health, or copied service-catalog governance prose while correcting fallback order.
+- Operator workflow clarity: default attribution copy resolves from runtime messages with an English fallback when no caller locale is available; row state still derives from real identities, monitor binds, owner/system/environment fields, and discovery href.
+- Context visibility: row keys, state values, meta evidence, discovery return URL, preview payload, and save payload behavior remain unchanged.
+
+## Passport Shell Language Toggle Copy Boundary - 2026-05-06
+
+- Hierarchy: keep the locale toggle in the existing passport shell header, above the same brand lockup and auth panel alignment.
+- Density: preserve the single icon button and compact locale popover without adding helper text, auth guidance, or extra controls.
+- Anti-AI-slop: avoid fake security claims, copied SaaS auth language, or decorative locale explanations while moving only the aria label behind runtime messages.
+- Operator workflow clarity: the language affordance must remain attached to the current globe trigger and locale option list.
+- Context visibility: translated copy can vary, but locale state, locale list, icon trigger markers, shell spacing, and panel layout remain unchanged.
+
+## Alert Integration Mermaid Canvas Aria Boundary - 2026-05-07
+
+- Hierarchy: keep the Mermaid canvas inside the existing alert-integration documentation figure, with pending/error render messages below the same SVG host.
+- Density: preserve the single SVG host and compact render-state text without adding diagram toolbars, legends, or helper rails.
+- Anti-AI-slop: avoid fake integration topology, copied diagram-product language, or invented remediation text while moving only the canvas aria label behind runtime messages.
+- Operator workflow clarity: the accessible canvas label must remain attached to the real Mermaid SVG host rendered from existing integration markdown.
+- Context visibility: translated copy can vary, but source markdown, render status markers, SVG host data marker, strict Mermaid rendering, and fallback behavior remain unchanged.
+
+## Entity Definition Attribution Preview Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the attribution preview inside the current definition/import preview stack, below parsed definition rows and above the existing context rail.
+- Density: preserve the compact attribution heading and row grid without adding validation summaries, relationship cards, or helper rails.
+- Anti-AI-slop: avoid fake entity resolution, invented telemetry confidence copy, or copied catalog-validation language while moving only the attribution preview title behind runtime messages.
+- Operator workflow clarity: the attribution title must stay attached to the actual parsed preview rows and row-level attribution state evidence.
+- Context visibility: translated copy can vary, but preview row filtering, attribution state markers, row titles, and import/save payload behavior remain unchanged.
+
+## Entity Definition Attribution State Badge Copy Boundary - 2026-05-07
+
+- Hierarchy: keep attribution state badges inline with each parsed attribution row inside the existing preview evidence grid.
+- Density: preserve the three compact state labels without adding score legends, validation summaries, or helper copy.
+- Anti-AI-slop: avoid fake entity resolution confidence, invented governance scoring, or copied catalog-readiness language while moving only badge labels behind runtime messages.
+- Operator workflow clarity: ready, missing, and unknown labels must remain tied to the real `attribute.state` values and existing badge tone classes.
+- Context visibility: translated copy can vary, but `data-entity-import-attribution-state`, row evidence, preview filtering, and save/import payload behavior remain unchanged.
+
+## Entity Definition Preview Metric Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the three preview metrics in the existing right context panel above definition queues, templates, and attribution evidence.
+- Density: preserve the current compact inline-count rhythm without adding summary cards, helper rails, or fake readiness narratives.
+- Anti-AI-slop: avoid invented import quality claims, copied catalog scoring language, or fake telemetry coverage while moving only the metric labels behind runtime messages.
+- Operator workflow clarity: ready, attention, and telemetry-gap counts must stay tied to the actual parsed preview rows and gap keys.
+- Context visibility: translated copy can vary, but ready-count, attention-count, telemetry-gap-count, icon choice, preview scoping, and import/save behavior remain unchanged.
+
+## Entity Definition Fact Label Boundary - 2026-05-08
+
+- Hierarchy: keep definition facts as compact route, format, activity, and template metadata owned by the view model; do not reintroduce the fact strip into the current surface or add a new summary layer.
+- Density: preserve the short label/value rhythm so facts remain scannable if the workbench exposes them again.
+- Anti-AI-slop: do not invent parser readiness, template health, import quality, or telemetry coverage while externalizing fixed labels.
+- Operator workflow clarity: the fact values still come from entity id, selected format, activity count, and template count; only fixed label text moves behind runtime keys.
+- Context visibility: definition editing, preview parsing, templates, activities, and save/import behavior remain unchanged.
+
+## Entity Definition Current Preview Title Boundary - 2026-05-07
+
+- Hierarchy: keep the current-definition title directly above the parsed preview queue rows in both definition and import modes.
+- Density: preserve the compact section heading without adding status banners, helper text, or nested summary panels.
+- Anti-AI-slop: avoid fake definition validation claims or invented catalog-health copy while moving only the section title behind runtime messages.
+- Operator workflow clarity: the title must continue to label the real parsed preview rows and attribution preview that operators can scope by queue state.
+- Context visibility: translated copy can vary, but preview-row grouping, row list rendering, attribution preview placement, and save/import behavior remain unchanged.
+
+## Entity Definition Template Panel Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the custom-template panel in the existing right context rail below the parsed-preview metrics and feedback message.
+- Density: preserve the compact title, error helper, and empty row-list rhythm without adding a template marketplace, readiness rail, or nested status card.
+- Anti-AI-slop: avoid fake reusable-template health, invented catalog claims, or copied template-library positioning while moving only the template panel copy behind runtime messages.
+- Operator workflow clarity: the panel title, recoverable-load helper, and empty state must stay attached to the real template rows and definition-error branch.
+- Context visibility: translated copy can vary, but template list length, `buildTemplateRows(templates)`, recoverable error state, and save/import behavior remain unchanged.
+
+## Entity Definition Batch Panel Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the template-and-batch panel directly after the custom-template panel in the right context rail.
+- Density: preserve the compact title/helper/expand affordance without adding a second editor mode, workflow rail, or fake batch preview.
+- Anti-AI-slop: avoid invented automation capabilities, fake template catalog status, or copied bulk-editor positioning while moving only the panel chrome copy behind runtime messages.
+- Operator workflow clarity: the batch panel must remain a lightweight entry for templates, draft source, and optional actions without changing preview/save/import controls.
+- Context visibility: translated copy can vary, but `data-entity-definition-batch-panel`, panel placement, definition-error behavior, and existing editor state remain unchanged.
+
+## Entity Definition Activity Panel Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the recent-activity panel after parsed preview evidence and before entity entry links in the definition context rail.
+- Density: preserve the compact heading and RowList empty state without adding timelines, audit summaries, or fake activity counts.
+- Anti-AI-slop: avoid invented import activity, fake workflow progress, or copied audit-feed language while moving only panel title and empty-state copy behind runtime messages.
+- Operator workflow clarity: real activity rows must still come from `buildActivityRows(activities, t)`, and the empty state should only explain where preview/import activity will appear.
+- Context visibility: translated copy can vary, but definition-error hiding, activity row mapping, row metadata, and save/import behavior remain unchanged.
+- Empty-row meta update: keep template and activity empty states in the same compact RowList slots while moving their metadata marker to shared `common.none`; do not add helper badges, fake activity health, or a second template summary.
+
+## Entity Definition Entry Panel Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the entity entry panel as the final compact navigation block in the definition context rail after activity evidence.
+- Density: preserve the two-link entry rhythm without adding breadcrumbs, route summaries, or explanatory navigation prose.
+- Anti-AI-slop: avoid fake catalog status, copied navigation IA, or invented next-step guidance while moving only the panel title and link labels behind runtime messages.
+- Operator workflow clarity: links must still route to the entity detail when an entity id exists and to the entity list/form fallback otherwise.
+- Context visibility: translated copy can vary, but link hrefs, entry placement, definition-error behavior, and save/import state remain unchanged.
+
+## Entity Definition Feedback Message Copy Boundary - 2026-05-07
+
+- Hierarchy: keep validation, preview, save, import, and recoverable load feedback in the existing compact inline message slot above the definition context rail.
+- Density: preserve one concise feedback line without adding status banners, toast stacks, helper rails, or fake parser-health summaries.
+- Anti-AI-slop: avoid invented import confidence, fake catalog readiness, copied onboarding prose, or AI triage wording while moving only feedback/status copy behind runtime messages.
+- Operator workflow clarity: empty-draft, preview count, blocked import, update, import, backend error mapping, and parse/load failures must remain tied to the existing preview/save/import control flow.
+- Context visibility: translated copy can vary, but `messageTone`, `previewRows`, `definitionErrorState`, entity-id guards, and save/import payload behavior remain unchanged.
+
+## Settings Console Layout Client Boundary - 2026-05-07
+
+- Hierarchy: keep the existing cold settings console header, left settings menu, and content pane unchanged while shrinking only the route layout hydration boundary.
+- Density: preserve the compact 260px navigation rail and single content panel without adding summary rails, nested cards, or extra settings descriptions.
+- Anti-AI-slop: avoid fake system health, fake deployment status, copied admin-console IA, or invented settings domains while moving only the pathname/i18n shell into a client island.
+- Operator workflow clarity: active menu state must still come from the real current pathname and menu labels from runtime messages.
+- Context visibility: translated header copy, active route, menu hrefs, child route content, and `SettingsConsoleShell` markers remain unchanged.
+
+## Bulletin Metrics Table Static Copy Boundary - 2026-05-07
+
+- Hierarchy: keep App and Host as the first two fixed columns in the existing metrics desk table before dynamic metric groups.
+- Density: preserve the compact two-row table header and per-cell badge treatment without adding summaries, helper rails, or fake metric status.
+- Anti-AI-slop: avoid invented uptime/health claims, synthetic metric availability, copied dashboard language, or future-only bulletin evidence while moving only static header and no-data badge copy behind runtime messages.
+- Operator workflow clarity: static labels must remain tied to the same monitor link column, host column, dynamic metric columns, and NO_DATA badge branch.
+- Context visibility: translated copy can vary, but monitor links, host values, metric/field translation lookup, unit badges, and empty/error/loading states remain unchanged.
+
+## Bulletin Metrics Empty Value Boundary - 2026-05-09
+
+- Hierarchy: keep empty metric cells inside the existing metrics desk table cell rather than adding a secondary empty-state row or metric summary.
+- Density: preserve the compact per-cell rhythm and NO_DATA badge treatment without helper rails, warnings, or fake health chips.
+- Anti-AI-slop: do not invent metric readings, uptime, health, or synthetic availability when a monitor row lacks a metric/field value.
+- Operator workflow clarity: absent metric values use the shared runtime empty marker while real NO_DATA fields keep their existing no-data badge branch.
+- Context visibility: monitor links, host values, metric/field translation lookup, unit badges, loading/error/empty table states, and bulletin selection remain unchanged.
+
+## Bulletin Validation And Metrics State Copy Boundary - 2026-05-09
+
+- Hierarchy: keep bulletin form validation in the existing manage dialog and metrics empty/error feedback in the existing metrics panel.
+- Density: preserve the compact bulletin management rhythm without secondary empty-state cards, helper rails, or notification-style health banners.
+- Anti-AI-slop: do not infer metrics freshness, monitor reachability, app health, bulletin publish state, or backend recovery from localized empty/error copy.
+- Operator workflow clarity: copy only distinguishes missing required inputs, empty metrics, and failed metrics loads; create/edit/delete, refresh, and tab selection stay unchanged.
+- Context visibility: selected bulletin, app, monitor ids, metrics JSON, reload action, and current tab remain owned by the existing bulletin center surface.
+
+## Bulletin Search All Label Boundary - 2026-05-09
+
+- Hierarchy: keep the all-bulletins label inside the existing current-query/search summary, not as a new filter chip or helper row.
+- Density: preserve the compact search and selected-board rhythm; only the locale-owned fallback label changes.
+- Anti-AI-slop: do not infer bulletin count, search freshness, monitor health, or metrics availability from an empty query label.
+- Operator workflow clarity: blank search still means all bulletin boards, while non-empty query text remains the raw operator-entered query.
+- Context visibility: search text, selected id, tab state, refresh countdown, and metrics panel ownership remain unchanged.
+
+## Bulletin Monitor Count Label Boundary - 2026-05-09
+
+- Hierarchy: keep monitor counts inside the existing row and selected-summary copy line rather than promoting them to a separate metric card.
+- Density: preserve the compact app-plus-monitor-count rhythm; only the locale-owned noun label changes.
+- Anti-AI-slop: do not imply monitor health, freshness, binding quality, or availability from the count label.
+- Operator workflow clarity: the count remains the raw bound-monitor id length while app identity and empty-value handling stay unchanged.
+- Context visibility: selected id, tabs, fields count, metrics table ownership, refresh cadence, and delete/edit actions remain unchanged.
+
+## Bulletin Row Meta Label Boundary - 2026-05-09
+
+- Hierarchy: keep creator and fields labels in the existing row meta and selected-summary title slots.
+- Density: preserve the compact two-line row and selected-summary rhythm without adding label chips or helper copy.
+- Anti-AI-slop: do not infer ownership, field health, dashboard quality, or metrics readiness from localized meta labels.
+- Operator workflow clarity: creator text remains the backend creator value and fields count remains the selected bulletin field-key count.
+- Context visibility: row title, app, monitor count, selected id, refresh state, metrics table, and edit/delete actions remain unchanged.
+
+## Bulletin Metrics Identity Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep missing monitor and host identity inside the first two fixed metrics table columns, before dynamic metric groups.
+- Density: preserve the compact monitor-link and host-cell rhythm without adding identity warning badges, helper rows, or secondary diagnostics.
+- Anti-AI-slop: do not invent monitor names, host names, service labels, ownership, or availability when bulletin metrics identity data is absent.
+- Operator workflow clarity: real monitor and host values remain authoritative; blank identity values use the shared runtime empty marker in the existing cells.
+- Context visibility: monitor ids, monitor detail links, metric/field columns, unit badges, NO_DATA badges, empty metric cells, loading/error/empty table states, and bulletin selection remain unchanged.
+
+## Bulletin Row Title Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep bulletin identity in the existing row title and selected-summary title slots rather than creating a separate missing-name state.
+- Density: preserve the compact bulletin list and selected-detail rows without adding warning chips, helper copy, or secondary identity rails.
+- Anti-AI-slop: do not invent bulletin names, monitor groups, ownership, health, or dashboard intent when a bulletin name is absent.
+- Operator workflow clarity: real bulletin names remain authoritative; blank row and selected titles use the shared runtime empty marker in the existing title slots.
+- Context visibility: row keys, app facts, monitor counts, creator/update meta, selected fields summary, delete dialog name fallback, metrics table, and refresh behavior remain unchanged.
+
+## Bulletin Current Name Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep the current bulletin name in the existing fact strip and delete-confirmation modal title context.
+- Density: preserve the compact fact strip and cold delete modal without adding missing-name warnings, helper rails, or secondary identity summaries.
+- Anti-AI-slop: do not invent bulletin names, monitor groups, ownership, dashboard intent, or safe-delete claims when the current bulletin name is absent.
+- Operator workflow clarity: real current bulletin names remain authoritative; blank and whitespace-only names use the shared runtime empty marker before deletion or selected-fact display.
+- Context visibility: selected id, row keys, delete action, batch delete state, metrics reload, refresh controls, tabs, and bulletin payload behavior remain unchanged.
+
+## Bulletin Delete Confirmation Copy Boundary - 2026-05-09
+
+- Hierarchy: keep destructive bulletin copy inside the existing cold modal title, body, and primary action slots.
+- Density: preserve the compact confirmation modal without adding secondary warnings, helper panels, or duplicate delete summaries.
+- Anti-AI-slop: do not claim safe deletion, automatic recovery, monitor health, or metrics preservation beyond the existing selected-bulletin context.
+- Operator workflow clarity: delete copy identifies removal from the current workspace and the need to reselect metrics context; cancel/delete actions remain unchanged.
+- Context visibility: current bulletin name fallback, selected id, batch state, refresh behavior, metrics table, and manage dialog ownership remain unchanged.
+
+## Entity Definition Editor Placeholder Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the format-specific placeholder inside the existing cold code editor, below the format selector and helper copy.
+- Density: preserve one concise editor hint per YAML, JSON, and cURL mode without adding parser examples, helper rails, or onboarding prose.
+- Anti-AI-slop: avoid invented schema guarantees, fake automation intelligence, copied import-product language, or future-only editor affordances while moving only placeholder hints behind runtime messages.
+- Operator workflow clarity: placeholder copy must stay tied to the selected import/definition format and cannot change draft starter replacement, preview, save, or import control flow.
+- Context visibility: translated copy can vary, but editor language resolution, placeholder placement, starter draft sync, format selector behavior, and parse payload format remain unchanged.
+
+## Log Stream Detail Attribution Diagnostics Copy Boundary - 2026-05-07
+
+- Hierarchy: keep attribution diagnostics inside the existing log detail drawer after facts and before row evidence.
+- Density: preserve the compact hertzbeat.* diagnostic strip and two state badges without adding summary cards, helper rails, or extra triage copy.
+- Anti-AI-slop: avoid fake entity resolution confidence, invented collection health, copied observability-product prose, or synthetic trace/entity evidence while moving only diagnostic chrome and state labels behind runtime messages.
+- Operator workflow clarity: present and missing labels must stay tied to each diagnostic row's real state and existing tone classes.
+- Context visibility: translated copy can vary, but trace id, selection state, fact rows, diagnostic row values/meta, toolbar badges, actions, row list, and JSON payload rendering remain unchanged.
+
+## Log Related Trace Event Detail Copy Boundary - 2026-05-07
+
+- Hierarchy: keep event facts inside the existing related-trace drawer, with the selected event header above the same RowList evidence stack.
+- Density: preserve the compact waterfall-plus-300px-evidence layout without adding event summaries, helper rails, or trace triage prose.
+- Anti-AI-slop: avoid fake root-cause language, invented span confidence, copied APM product copy, or synthetic trace evidence while moving only drawer/event labels behind runtime messages.
+- Operator workflow clarity: event detail copy must remain tied to real span-event selection and the existing action that clears event selection back to the owning span.
+- Context visibility: translated copy can vary, but selected span key, event key, offset, attributes, stage facts, toolbar badges, waterfall ticks, and row selection callbacks remain unchanged.
+
+## Alert Notice Rule Field Chrome Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the notification rule form as the existing aligned label/control stack, with receiver, template, filter, label, period, weekday, time, and enable rows in the current order.
+- Density: preserve the compact cold multi-select, switch, weekday checkbox, label-record, and shared time-range controls without adding helper rails or secondary summaries.
+- Anti-AI-slop: avoid fake delivery guarantees, invented policy scoring, copied notification-SaaS language, or synthetic receiver/template explanations while moving only field chrome labels and weekday labels behind runtime messages.
+- Operator workflow clarity: labels must remain tied to the same form rows, selected receiver/template filtering, filter-all switch, weekday selection, and time-range payload names.
+- Context visibility: translated copy can vary, but draft state, receiver/template options, label matcher values, hidden inputs, switch markers, and `data-alert-notice-rule-*` markers remain unchanged.
+
+## Entity Discovery Table Row Copy Boundary - 2026-05-07
+
+- Hierarchy: keep discovery table rows inside the existing cold discovery table below the search row and policy strip, with clue, instance, status, owner, system, environment, attribution, and action columns unchanged.
+- Density: preserve compact status and attribution chips without adding row summaries, confidence panels, or secondary action rails.
+- Anti-AI-slop: avoid fake merge confidence, invented ownership scoring, copied service-catalog language, or synthetic entity evidence while moving only row status/activity/attribution/action copy behind runtime messages.
+- Operator workflow clarity: row labels must remain tied to the real preset/search result branch, candidate merge/enrich/create/resolved state, and existing row href action target.
+- Context visibility: translated copy can vary, but monitor ids, preset ids, candidate names, owner/system/environment values, attribution state markers, status tone, and row action hrefs remain unchanged.
+- Empty-governance update: keep missing owner/system/environment/instance cells in the same table columns while moving blank-cell markers to shared `common.none`; matched/create metrics must derive from attribution state, not localized empty-cell text.
+
+## Entity Import Attribution Activity Copy Boundary - 2026-05-07
+
+- Hierarchy: keep attribution preview rows, template rows, and recent activity rows in the existing definition/import workspace context panels without changing their order or surrounding editor controls.
+- Density: preserve compact state badges, activity metadata, and row summaries without adding import scorecards, confidence rails, or onboarding copy.
+- Anti-AI-slop: avoid fake import confidence, invented catalog scoring, copied service-catalog language, or synthetic telemetry evidence while moving only attribution/activity/source labels behind runtime messages.
+- Operator workflow clarity: labels must remain tied to real attribution state, template source, activity status, and activity summary replacements from the import controller payload.
+- Context visibility: translated copy can vary, but entity ids, monitor binds, source metadata, activity detail values, preview/import/save payload paths, and existing row placement remain unchanged.
+- Preview queue copy boundary update: keep import metrics, gap labels, readiness state, telemetry-binding labels, and ready/attention/telemetry queue grouping in the existing import view-model while moving only remaining fallback copy behind runtime messages; do not add import scorecards, fake readiness, copied catalog-import wording, or new summary rails.
+- Empty-value fallback update: keep missing template copy/format/source, activity title/detail/status/format, and preview entity title as compact row metadata inside the existing import workspace while moving empty markers to shared `common.none`; do not add helper legends, fake import health, or new navigation.
+
+## Entity Detail Fallback Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the recoverable fallback detail data inside the existing entity detail workbench, feeding the same header, description, and next-action panels as a real detail response.
+- Density: preserve one temporary description and one definition next action without adding fake health, fake evidence, or extra recovery panels.
+- Anti-AI-slop: avoid synthetic status claims, invented monitor/log/trace evidence, or copied service-catalog recovery prose while moving only fallback display/description/action copy behind runtime messages.
+- Operator workflow clarity: fallback copy must remain tied to recoverable missing-detail responses and the real definition workspace handoff.
+- Context visibility: translated copy can vary, but entity id normalization, fallback owner/system/environment/source fields, zero evidence summaries, and recoverable-error behavior remain unchanged.
+
+## Collector Health Evidence Copy Boundary - 2026-05-07
+
+- Hierarchy: keep collector health evidence as the compact shared strip in collector management and the existing collection-health row inside entity detail.
+- Density: preserve one title, one copy line, one meta badge, and one freshness badge without adding health dashboards, scorecards, or synthetic collector diagnostics.
+- Anti-AI-slop: avoid fake collection readiness, invented task status, copied infra-health prose, or future-only collector claims while moving only shared health evidence copy behind runtime messages.
+- Operator workflow clarity: cluster evidence must remain tied to real collector counts and task counts; monitor fallback evidence must remain tied to bound-monitor health when collector counts are missing.
+- Context visibility: translated copy can vary, but tone calculation, count clamping, last-seen/evidence fallback, and collector/entity surface placement remain unchanged.
+
+## Collector Health Evidence Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep collector health evidence as the same compact title/copy/meta/freshness strip in collector management and entity detail; changing fallback locale order must not add dashboards, scorecards, health cards, or topology panels.
+- Density: preserve the one-line evidence rhythm and badge count density so collector and entity surfaces stay scan-friendly.
+- Anti-AI-slop: do not invent collection readiness, task diagnostics, synthetic monitor health, future collector states, or copied infra-console wording while correcting fallback order.
+- Operator workflow clarity: shared health copy resolves from runtime messages with an English fallback when no caller locale is available; cluster evidence still derives only from collector/task counts and monitor fallback evidence still derives only from bound-monitor health.
+- Context visibility: tone calculation, count clamping, last-seen/evidence fallback, translator injection, and collector/entity surface placement stay unchanged.
+
+## Log Manage Selected Attribution Diagnostics Copy Boundary - 2026-05-07
+
+- Hierarchy: keep selected-log attribution diagnostics inside the existing selected log evidence rail, below facts and before JSON/related evidence controls.
+- Density: preserve the compact hertzbeat.* diagnostic strip and state badge rhythm without adding attribution summary cards, helper rails, or fake entity-resolution confidence.
+- Anti-AI-slop: avoid invented entity resolution, fake collector health, copied observability product prose, or synthetic trace/entity evidence while moving only panel aria/title/state labels behind runtime messages.
+- Operator workflow clarity: present/missing labels must stay tied to each diagnostic row's real state and existing tone classes.
+- Context visibility: translated copy can vary, but row keys, values, meta, selected log facts, actions, trace/entity handoffs, and stream/history selection state remain unchanged.
+
+## Trace Event And Link Evidence Copy Boundary - 2026-05-07
+
+- Hierarchy: keep trace event markers inside the existing waterfall and selected-span event/link rows inside the current drawer evidence stack.
+- Density: preserve compact event dots, selected-span facts, and five-row evidence limits without adding root-cause summaries, confidence rails, or extra trace triage copy.
+- Anti-AI-slop: avoid invented span meaning, fake causality, copied APM prose, or synthetic event attributes while moving only fallback event/link/attribute labels behind runtime messages.
+- Operator workflow clarity: unnamed-event, related-link, empty-attributes, overflow-count, and object-attribute labels must stay tied to the real span event/link payloads.
+- Context visibility: translated copy can vary, but span ids, trace ids, event offsets, attributes, selected event callbacks, waterfall geometry, and handoff links remain unchanged.
+
+## Exception Center Kubernetes Filter Copy Boundary - 2026-05-07
+
+- Hierarchy: keep Kubernetes deployment and pod filters in the existing left filter rail beside service, host, cluster, and namespace filters.
+- Density: preserve compact empty-filter rows without adding Kubernetes summaries, helper copy, or decorative infrastructure panels.
+- Anti-AI-slop: avoid fake K8s discovery status, synthetic cluster evidence, copied APM filter prose, or future-only workload claims while moving only filter labels behind runtime messages.
+- Operator workflow clarity: deployment and pod labels must remain simple filter dimensions tied to the same exception query sidebar and empty-filter affordance.
+- Context visibility: translated copy can vary, but filter order, values, empty-state row behavior, and exception table/query context remain unchanged.
+
+## Trace Attribution Diagnostic Row Copy Boundary - 2026-05-07
+
+- Hierarchy: keep hertzbeat.entity_id, entity_name, workspace_id, collector, and template diagnostics inside the existing trace attribution panel.
+- Density: preserve one compact meta line per diagnostic row without adding confidence scores, RCA summaries, or helper rails.
+- Anti-AI-slop: avoid fake entity resolution, invented collector status, copied APM product language, or synthetic source evidence while moving only row meta copy behind runtime messages.
+- Operator workflow clarity: present and missing meta must stay tied to the same real attribute lookup and the existing present/missing badge state.
+- Context visibility: translated copy can vary, but diagnostic keys, route-context fallback order, values, state tones, and trace/span evidence remain unchanged.
+
+## Entity Discovery Governance Card Copy Boundary - 2026-05-07
+
+- Hierarchy: keep governance cards, scope filters, queue groups, and bulk override chips as the existing discovery view-model outputs without adding a new discovery page surface.
+- Density: preserve compact risk, next-action, candidate, queue, and bulk override labels without adding confidence scorecards, helper rails, or service-catalog prose.
+- Anti-AI-slop: avoid fake merge certainty, invented ownership scoring, copied service-catalog language, or synthetic governance evidence while moving only visible governance card/queue labels behind runtime messages.
+- Operator workflow clarity: labels must remain tied to the real resolved/merge/enrich/create state machine, candidate preset matching, scope counts, and existing action hrefs.
+- Context visibility: translated copy can vary, but monitor ids, candidate names, owner/system/environment context, sample titles, card keys, selected bulk payload, and action kinds remain unchanged.
+
+## Entity Discovery Facts Metrics Catalog Copy Boundary - 2026-05-07
+
+- Hierarchy: keep discovery facts, metrics, and catalog rows as compact view-model evidence that can be reused by the existing discovery workspace without adding panels or navigation.
+- Density: preserve four facts, four metrics, and three catalog rows with the current count/value rhythm and no helper rails, confidence scorecards, or onboarding prose.
+- Anti-AI-slop: avoid fake catalog health, invented governance completion, copied service-catalog language, or synthetic discovery readiness while moving only labels, ready/empty state copy, count meta, and empty copy behind runtime messages.
+- Operator workflow clarity: labels must remain tied to the real preset, activity, owner, system, and environment counts already computed by the view model.
+- Context visibility: translated copy can vary, but row order, count math, preset-coverage tone, workspace value, catalog list values, and catalog empty fallback behavior remain unchanged.
+
+## Monitor Manage Row Scrape Meta Copy Boundary - 2026-05-07
+
+- Hierarchy: keep the monitor scrape metadata inside the existing row evidence extra line beside app/status, not as a new column, filter, or helper panel.
+- Density: preserve the compact `app · collection-mode value` rhythm in the list row and do not add secondary descriptions or monitor-template onboarding copy.
+- Anti-AI-slop: avoid fake collection health, invented scrape explanations, copied infrastructure inventory language, or synthetic monitor evidence while moving only the visible scrape label behind runtime messages.
+- Operator workflow clarity: the label must stay tied to the real `item.scrape` value already returned by the monitor list payload and must not change selection, detail navigation, batch actions, or export/import flows.
+- Context visibility: translated copy can vary, but monitor id, app, instance fallback, status badge, row checkbox, selected-id behavior, and detail href remain unchanged.
+- Identity fallback update: preserve monitor search, dense list rows, selected rail, row status tone, row selection, and detail handoff while moving only absent app/instance/scrape row markers into the shared localized fallback; do not add fake monitor readiness, collector coverage summaries, or extra rails.
+- Empty-label rail update: preserve the selected monitor rail and label evidence slot even when no labels exist, using the shared localized fallback for the empty meta marker without changing selected monitor JSON, controls, or row density.
+- Empty-selected rail update: preserve selected-monitor rail placement, no-selection copy, controls, and route handoffs while moving only the no-selection meta marker into the shared localized fallback; do not add fake monitor status, helper panels, or new navigation.
+- Selected-identity rail update: preserve selected monitor name, status meta, label count, controls, JSON preview, and detail/edit/new handoffs while moving only absent selected app/instance facts into the shared localized fallback; do not add fake inventory health or rail summaries.
+
+## Monitor Manage Transfer Failure Feedback Copy Boundary - 2026-05-07
+
+- Hierarchy: keep export/import transfer errors inside the existing monitor manage controls feedback area, not a new modal, toast rail, or diagnostics panel.
+- Density: preserve one concise failure line with the HTTP status value and no extra recovery prose or backend speculation.
+- Anti-AI-slop: avoid fake import/export completion, invented network diagnosis, copied backup-product language, or synthetic monitor state while moving only HTTP failure copy behind runtime messages.
+- Operator workflow clarity: feedback must remain tied to the same export/import fetch response branch, selected/all export actions, file-import action, and `actionError` rendering path.
+- Context visibility: translated copy can vary, but HTTP status, Authorization/Accept-Language headers, file payload, reload behavior, success messages, fallback filenames, and existing control placement remain unchanged.
+
+## Setting Message Server Apply Feedback Copy Boundary - 2026-05-07
+
+- Hierarchy: keep email/SMS save feedback as the existing compact message line below the settings summary, not a toast stack, modal alert, or new status rail.
+- Density: preserve one success/error line and the current `messageTone` treatment without adding delivery diagnostics, provider health summaries, or troubleshooting prose.
+- Anti-AI-slop: avoid fake notification delivery guarantees, invented provider state, copied SaaS settings language, or synthetic receiver status while adding only local runtime copy for the existing apply feedback keys.
+- Operator workflow clarity: feedback must stay tied to `saveEmail`, `saveSms`, the same `apiMessagePost` controller calls, dialog close behavior, and fallback error handling.
+- Context visibility: translated copy can vary, but email/SMS drafts, SMS snapshot restore, disabled save conditions, summary rows, and settings console placement remain unchanged.
+
+## Entity List Status Tone Boundary - 2026-05-07
+
+- Hierarchy: keep status tone inside the existing dense entity table status badge and fallback progress line, without adding a new health legend, scorecard, or side rail.
+- Density: preserve the compact badge treatment and 40%/20% incomplete-progress fallback rhythm without adding explanation copy.
+- Anti-AI-slop: avoid fake entity health, invented catalog confidence, copied service-catalog language, or translated-string business logic while moving status tone to stable view-model data.
+- Operator workflow clarity: status color and fallback progress must derive from the raw entity status, not localized labels, so search, refresh, links, health affordance, and evidence rows remain unchanged.
+- Context visibility: translated status text can vary by locale, but `statusTone`, row hrefs, monitor/alert/relation evidence, health affordance, and table placement remain stable.
+
+## Entity Detail Row Tone Boundary - 2026-05-07
+
+- Hierarchy: keep tone only on the existing entity detail row meta chips inside overview, health, alert, related, relationship, source, and next-action panels.
+- Density: preserve the compact row stack and existing badge dimensions without adding a health legend, severity sidebar, scorecard, or explanatory copy.
+- Anti-AI-slop: avoid fake health, invented severity, copied APM language, or localized-string status logic while moving tone decisions to stable view-model data.
+- Operator workflow clarity: row tone must derive from raw entity status and evidence counts so inherited context, handoff links, delete/refresh actions, and panel order remain unchanged.
+- Context visibility: translated labels can vary, but row title/copy/meta placement, collector freshness, hrefs, alert evidence, relationship evidence, and context handoffs remain stable.
+
+## Entity Health Affordance Status Boundary - 2026-05-07
+
+- Hierarchy: keep the lightweight health affordance in the existing entity list and topology node health locations without adding a new health panel or severity legend.
+- Density: preserve the compact score, collected-health copy, and alert/anomaly meta line; no additional remediation copy or score explanation.
+- Anti-AI-slop: avoid fake entity health, translated-label business logic, invented SLO semantics, or copied service-catalog scoring while normalizing raw status aliases in the shared helper.
+- Operator workflow clarity: status penalty must use stable raw status values so entity list rows and topology nodes score consistently across locales.
+- Context visibility: translated health copy can vary, but score math, monitor/alert/trace/log inputs, tone thresholds, entity links, and topology placement remain unchanged.
+
+## Entity Health Affordance Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep the lightweight score/copy/meta affordance exactly where entity list rows and topology nodes already render it; changing fallback locale order must not add health panels, legends, scorecards, or remediation rails.
+- Density: preserve one score label, one collected/waiting line, and one alert/anomaly meta line without expanding the row height or topology node chrome.
+- Anti-AI-slop: do not invent health status, SLO semantics, remediation advice, fake monitor data, or copied service-catalog wording while correcting the fallback order.
+- Operator workflow clarity: default health copy resolves from runtime messages with an English fallback when no caller locale is available; score math remains derived only from raw monitor, alert, trace, log, and status inputs.
+- Context visibility: score, scoreText, tone, monitor counts, alert/anomaly counts, status penalties, entity links, and topology placement remain unchanged.
+
+## Collector Status Tone Boundary - 2026-05-07
+
+- Hierarchy: keep collector online/offline status as the existing compact table badge next to row health and last-seen evidence, without adding summary rails or secondary status blocks.
+- Density: preserve the dense cluster table, fixed columns, and icon row actions so collector operations remain one-scan and batch-friendly.
+- Anti-AI-slop: avoid translated-label business logic, fake collector health states, invented cluster semantics, or copied inventory-console status treatments.
+- Operator workflow clarity: badge tone must come from raw collector status/online data so localized online/offline labels can change without changing row color or next action.
+- Context visibility: translated status copy can vary, but row mutation controls, health evidence, freshness, and cluster-level context stay aligned with the same collector payload.
+
+## Public Status Badge Tone Boundary - 2026-05-07
+
+- Hierarchy: keep component and incident state tone on the existing public status badges, without adding a status legend, summary rail, or marketing-style health claim.
+- Density: preserve the compact component rows, incident summary rows, and incident timeline badges so public operators can scan state without extra explanatory copy.
+- Anti-AI-slop: avoid component-local status interpretation, translated-string business logic, fake uptime/incident health, or copied SaaS status-page semantics while moving only tone ownership to the status-center view model.
+- Operator workflow clarity: badge tone must come from normalized component and incident state data so localized labels, public colors, and UI shell rendering remain stable across locales.
+- Context visibility: translated labels and admin-provided colors can vary, but component history, incident card, timeline content, and public status shell placement must stay aligned with the same status-center payload.
+
+## Status Setting Admin Tag Tone Boundary - 2026-05-07
+
+- Hierarchy: keep component and incident state tone inside the existing dense admin tables, without adding summary rails, legends, or public-status preview cards.
+- Density: preserve compact Tag badges, fixed table columns, icon row actions, search, and dialog rhythm so status administration remains one-scan and form-first.
+- Anti-AI-slop: avoid component-local state interpretation, fake service health, invented incident lifecycle claims, or copied status-page admin semantics while moving only tone buckets to the setting-status view model.
+- Operator workflow clarity: admin badge tone must derive from normalized component/incident states so localized labels and table rendering remain stable when state copy changes.
+- Context visibility: labels, timestamps, row actions, component bindings, incident messages, and public-status handoff stay in the same table positions while tone becomes data-layer output.
+
+## Trace Route Status Tone Boundary - 2026-05-07
+
+- Hierarchy: keep trace status tone on the existing dense trace-list badge, without adding a severity legend, trace-health rail, or extra evidence panel.
+- Density: preserve one compact status badge per row and the current table rhythm so operators can scan errors, services, durations, and trace IDs together.
+- Anti-AI-slop: avoid page-local status parsing, fake trace health, invented lifecycle states, copied APM severity semantics, or translated-string status logic while moving tone ownership to trace view-model data.
+- Operator workflow clarity: badge tone must derive from normalized trace status values so filtering, detail-drawer opening, selected-trace context, and cross-signal handoffs remain unchanged.
+- Context visibility: translated headers and status text can vary, but row identity, service/root-span/duration columns, selected detail panel, and trace status color stay aligned with the same trace-list payload.
+
+## Log Severity Tone Boundary - 2026-05-07
+
+- Hierarchy: keep severity tone on the existing live-stream and history-list badges, without adding a severity legend, intake-quality panel, or alert-style status rail.
+- Density: preserve compact row badges in the virtualized stream and dense history table so severity, service, timestamp, body, trace ID, and row actions remain one-scan.
+- Anti-AI-slop: avoid page-local severity parsing, fake log health, invented severity lifecycle states, copied log-explorer semantics, or translated-string status logic while moving tone ownership to shared log display mapping and row model data.
+- Operator workflow clarity: badge tone must derive from normalized log severity text/number so stream selection, detail dialog, trace preview, entity handoff, and history row navigation remain unchanged.
+- Context visibility: translated headers and severity labels can vary, but stream row identity, retention/window markers, history row keys, trace/span handoffs, selected evidence, and badge color stay aligned with the same log payload.
+
+## Overview Impacted Severity Tone Boundary - 2026-05-07
+
+- Hierarchy: keep impacted-entity severity tone on the existing overview affected-items row badge, without adding a legend, health rail, or second status column.
+- Density: preserve the compact two-column row with entity, owner, severity, and status in one scan; no new explanatory copy or extra badges.
+- Anti-AI-slop: avoid component-local severity parsing, fake entity health, invented impact states, copied APM status language, or translated-string business logic while moving tone ownership into overview view-model data.
+- Operator workflow clarity: row color must derive from normalized alert/degradation severity so drawer-first opening, entity links, browse-all action, and healthy filtering remain unchanged.
+- Context visibility: translated severity/status labels can vary, but row identity, owner, last issue, status label, action target, and badge color stay aligned with the same overview payload.
+
+## Overview Problem Focus Badge Tone Boundary - 2026-05-07
+
+- Hierarchy: keep current-issue severity tone on the existing StageSection action badge, without adding a second alert header, severity legend, or status rail.
+- Density: preserve one compact badge beside the current focus title so title, summary, entity, owner, and actions remain one-scan.
+- Anti-AI-slop: avoid page-local severity parsing, fake current-issue health, invented incident lifecycle states, copied APM badge semantics, or translated-string business logic while moving tone ownership into overview view-model data.
+- Operator workflow clarity: badge color must derive from normalized problem-focus severity so the drawer-first Details action, alert handoff, refresh flow, and healthy placeholder branch remain unchanged.
+- Context visibility: translated severity labels can vary, but focus title, entity, owner, summary, StageSection placement, and badge color stay aligned with the same alert payload.
+
+## Overview Detail Dialog Badge Tone Boundary - 2026-05-07
+
+- Hierarchy: keep impacted-entity drawer status tone on the existing detail-dialog badge, without adding a secondary header, legend, or alert rail.
+- Density: preserve one compact badge beside the drawer subtitle and description so owner/status sections stay scannable.
+- Anti-AI-slop: avoid dialog-local severity parsing, fake health, invented impact lifecycle states, copied incident-management badge semantics, or translated-string business logic while passing tone from view-model data.
+- Operator workflow clarity: badge color must follow selected impacted entity `severityTone` so row click, drawer title, last issue, owner/status sections, and close action remain unchanged.
+- Context visibility: translated severity/status labels can vary, but selected entity identity, last issue, owner, status label, dialog placement, and badge color stay aligned with the same overview payload.
+
+## Monitor Manage Status Badge Tone Boundary - 2026-05-07
+
+- Hierarchy: keep monitor status tone on the existing row badge inside the manage list, without adding a new status column, legend, or summary rail.
+- Density: preserve the compact row extra line with status badge, app, scrape value, and checkbox so batch operations remain one-scan.
+- Anti-AI-slop: avoid page-local numeric status parsing, fake monitor health, invented lifecycle states, copied inventory-console semantics, or translated-string business logic while moving badge tone into monitor display mapping.
+- Operator workflow clarity: badge color must derive from the raw monitor status number so selection, detail navigation, export/import, enable/pause/delete actions, and status labels remain unchanged.
+- Context visibility: translated status labels can vary, but monitor id, app, scrape, status badge placement, checkbox selection, and detail href stay aligned with the same monitor row payload.
+
+## Safe Automation Snapshot Catalog Labels - 2026-05-08
+
+- Hierarchy: keep action catalog card name, category, scope, owner, and posture inside the existing compact snapshot cards, without adding a live-execution rail or extra governance copy.
+- Density: preserve the three-card catalog rhythm so operators can scan action intent, target scope, owner, risk, last run, and roadmap-demo state without extra explanatory rows.
+- Anti-AI-slop: avoid hardcoded English demo labels, fake live adapter status, invented execution confidence, or copied automation-product wording while moving only display labels behind runtime messages.
+- Operator workflow clarity: localized catalog labels must still read as sample snapshot data under the adapter boundary, so users do not mistake roadmap examples for executable actions.
+- Context visibility: raw ids, risk values, last-run timestamps, snapshot state, suggested-action handoffs, and adapter-boundary caveats stay stable while visible catalog copy becomes locale-owned.
+
+## Safe Automation Snapshot Run Labels - 2026-05-08
+
+- Hierarchy: keep run history name, target, actor, status, started time, duration, and snapshot marker in the existing compact rows, without adding a live-run rail or execution-adapter claim.
+- Density: preserve one-line run copy and meta rhythm so operators can scan recent action outcomes beside the catalog and approval sections.
+- Anti-AI-slop: avoid hardcoded English run examples, fake execution truth, invented success confidence, or copied automation-console language while moving only display labels behind runtime messages.
+- Operator workflow clarity: localized run rows must remain explicitly sample snapshot data under the adapter boundary, not executable history from a live adapter.
+- Context visibility: run ids, status enums, started timestamps, snapshot state, adapter caveat, and suggested-action handoffs stay stable while visible run copy becomes locale-owned.
+
+## Explorer Signal Tone Boundary - 2026-05-07
+
+- Hierarchy: keep signal color on the existing dense explorer signal badge in the result table, without adding a legend, filter rail copy, or secondary signal column.
+- Density: preserve the compact trace/log/metric table rhythm so service, operation, status, duration, and timestamp remain one-scan.
+- Anti-AI-slop: avoid page-local signal-key color decisions, fake cross-signal health, copied query-console styling semantics, or translated-label logic while moving signal tone ownership into explorer row data.
+- Operator workflow clarity: badge color must follow stable signal row data so query controls, result rows, detail panel, and trace/log/metric/entity handoffs remain unchanged.
+- Context visibility: translated signal labels can vary, but row identity, signal key, operation href, active detail row, and signal badge color stay aligned with the same explorer payload.
+
+## Explorer Checklist Meta Labels - 2026-05-08
+
+- Hierarchy: keep Explorer checklist meta as compact status text inside the existing cold workbench readiness list, not as a query progress tracker or adapter health panel.
+- Density: preserve the three-row checklist and short meta rhythm beside title/copy rows without adding badges, counters, or helper rails.
+- Anti-AI-slop: translate sample meta labels without implying live query adapters, completed cross-signal search, or real signal-link delivery.
+- Operator workflow clarity: meta stays static convergence copy until backend-backed Explorer adapters and query persistence land.
+- Context visibility: route entry ready, adapters next, and signal links reserved states remain visible while display labels become runtime-localized.
+
+## Shell Navigation Runtime Labels - 2026-05-08
+
+- Hierarchy: keep the sidebar IA exactly on the existing section and item rhythm, with labels resolved from route label keys rather than catalog fallback text.
+- Density: preserve compact one-line section headings and nav rows without adding helper copy, badges, or route metadata in the rail.
+- Anti-AI-slop: avoid English fallback labels leaking into localized navigation, stale alias wording, or copied SaaS navigation buckets while keeping route catalog metadata machine-readable.
+- Operator workflow clarity: active route highlighting, icon mapping, and visible href order stay unchanged while display text remains runtime-localized.
+- Context visibility: section keys, item keys, hrefs, and active state remain stable so navigation context stays visible across dashboard, ingestion, observability, alerting, objects, and settings.
+
+## Setting Define Skeleton Seed Copy - 2026-05-08
+
+- Hierarchy: keep the new-definition seed inside the existing YAML editor flow, with no extra onboarding panel, helper rail, or fake template marketplace state.
+- Density: preserve one draft seed name and one summary annotation so the list, editor, preview, and datasource evidence remain compact.
+- Anti-AI-slop: avoid "next migration" wording, generic placeholder copy, or migration-era implementation language leaking into operator-created alert definitions.
+- Operator workflow clarity: the New action still creates an editable PromQL realtime-metric draft with stable period, threshold, labels, and enabled state.
+- Context visibility: datasource, expression, severity label, preview route, save behavior, and selected draft state remain unchanged while visible seed copy becomes runtime-localized.
+
+## Setting Define Skeleton Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the seed-name and summary assertion inside the existing controller regression; do not add a surface-only seed preview or template marketplace panel.
+- Density: preserve the single controller test setup and compact skeleton payload assertions.
+- Anti-AI-slop: do not invent new default alert templates, datasource readiness, preview status, or migration copy while moving the test onto the shared translator helper.
+- Operator workflow clarity: controller tests should consume the same runtime/helper catalog keys as the setting-define page instead of maintaining a local seed-copy map.
+- Context visibility: seed name, PromQL datasource, realtime metric type, expression, warning label, summary annotation, and save payload remain unchanged.
+
+## Setting Define Current Fallback Copy - 2026-05-08
+
+- Hierarchy: keep the current YAML filename chip and delete feedback inside the existing compact definition editor flow, without adding helper copy, a new empty panel, or a template status rail.
+- Density: preserve the single filename label and one mutation message so search, list selection, editor content, preview, and datasource evidence remain one-scan.
+- Anti-AI-slop: avoid hardcoded English fallback text leaking into localized operation, generic "current" copy that hides what object is being acted on, or fake default-definition status.
+- Operator workflow clarity: fallback labels only cover missing datasource/name data and must not change selection, edit, preview, save, or delete feedback behavior.
+- Context visibility: datasource/name still win when available, while the fallback filename and delete target become runtime-localized and stay tied to the same active definition payload.
+
+## Entity List English Identity Label Boundary - 2026-05-08
+
+- Hierarchy: keep entity type, local environment, and unknown status as compact row/table labels inside the existing entity catalog, without adding a legend, rail, or explanatory product copy.
+- Density: preserve one-line row identity and table cells so service, owner, environment, health, counts, and evidence actions remain scannable.
+- Anti-AI-slop: avoid English locale catalogs leaking Chinese fallback labels, fake status semantics, copied external inventory wording, or translated-string business logic.
+- Operator workflow clarity: labels should change by locale only; search, filtering, row navigation, health affordance, and evidence handoffs stay bound to the same entity payload.
+- Context visibility: entity id/name, raw type/status/environment values, status tone, health model, and row href remain stable while visible identity labels become locale-owned.
+- Owner fallback update: keep the row identity rhythm `type · owner · environment` inside the existing table/list row while moving only absent owner markers into the shared localized fallback; do not add ownership helper panels, fake owner state, or new row actions.
+
+## Overview Log Lane English Recommendation Copy - 2026-05-08
+
+- Hierarchy: keep the log lane recommendation stat inside the existing overview investigation-lane row, without adding a new signal priority rail, legend, or helper panel.
+- Density: preserve the compact lane title, eyebrow, copy, stat, and action rhythm so entities, logs, traces, metrics, and dashboard entries remain one-scan.
+- Anti-AI-slop: avoid English locale catalogs leaking Chinese recommendation copy, fake signal priority, copied incident-console language, or translated-string routing logic.
+- Operator workflow clarity: the stat still follows `topAlertHasTraceId`; only the visible English wording changes when logs are secondary to trace-first investigation.
+- Context visibility: alert trace context, log href, trace href, lane order, and action labels remain stable while the log-lane stat becomes locale-owned.
+
+## Entity List English Evidence Summary Copy - 2026-05-08
+
+- Hierarchy: keep page metric cards, row meta, and selected-entity rail labels as compact evidence summaries inside the existing entity catalog, without adding legends, side rails, or explanatory product copy.
+- Density: preserve one-line count and evidence text so entity identity, owner, environment, health, monitor counts, alert counts, relation counts, and handoff actions remain scannable.
+- Anti-AI-slop: avoid English locale catalogs leaking Chinese fallback text, fake health semantics, copied inventory-console wording, or translated-string branching while moving only display text into locale-owned messages.
+- Operator workflow clarity: evidence summary labels must change by locale only; search, filtering, selection, row navigation, and cross-signal handoffs stay bound to the same entity payload.
+- Context visibility: entity id/name, raw counts, last-evidence time, selected rail identity/monitor/relation numbers, and definition activity state remain stable while visible evidence summaries become English-owned.
+- Unselected rail update: preserve entity list filters, dense table, inline row actions, selected rail placement, and cross-signal handoffs while moving only the no-selection title/copy/meta marker into runtime messages; do not add fake entity health, helper panels, or onboarding prose.
+
+## Entity List Abnormal Metric Data Boundary - 2026-05-08
+
+- Hierarchy: keep the abnormal-object metric in the existing compact count strip, without adding a status legend, extra badge rail, or explanatory health panel.
+- Density: preserve the four-metric row and dense entity table rhythm so total, abnormal, alerting, and linked counts remain one-scan above the list.
+- Anti-AI-slop: avoid translated-label business logic, fake health semantics, or locale-specific status string comparisons while moving the abnormal count back to raw entity status data.
+- Operator workflow clarity: healthy rows must remain healthy across English and Chinese labels; search, filtering, table rows, health affordance, and evidence handoffs stay bound to the same entity payload.
+- Context visibility: translated status labels and status badge tone can vary, but the abnormal metric must follow raw entity status so page-level evidence does not drift from row-level context.
+
+## Entity Display Mapping Fallback Copy - 2026-05-08
+
+- Hierarchy: keep type and status labels as compact row/table identity text in the entity catalog, without adding a fallback legend, locale helper, or explanatory product copy.
+- Density: preserve one-line entity identity and status cells so object, owner, environment, health, counts, and next actions remain scannable.
+- Anti-AI-slop: avoid locale-specific fallback literals, translated-string business logic, fake status semantics, or copied inventory-console labels while making missing-key fallbacks neutral and English-owned.
+- Operator workflow clarity: runtime messages still own localized Chinese and English labels; fallback copy only covers missing keys and must not alter filtering, row navigation, status tone, or health affordance behavior.
+- Context visibility: raw type/status values, runtime translation keys, row hrefs, and evidence handoffs stay stable while the emergency fallback layer stops leaking Chinese into English operation.
+- Missing-token update: keep missing type/environment/status as compact identity cells in the existing entity catalog while moving empty values to shared `common.none`; do not add legends, helper rows, fake status, or route changes.
+
+## Alert Evidence Locale Detection Boundary - 2026-05-08
+
+- Hierarchy: keep alert inherited time/source/trace context rows in the existing compact evidence strip, without adding a locale chip, helper row, or route-level settings panel.
+- Density: preserve the same row count and short meta rhythm so alert filters, selected evidence, closure actions, and cross-signal handoffs remain one-scan.
+- Anti-AI-slop: avoid visible translated-label comparisons, copied locale heuristics, fake alert state, or fallback text that changes behavior when runtime copy changes.
+- Operator workflow clarity: date formatting should follow an explicit locale-code message, not the current wording for Refresh or Alert Center, so translators can revise labels without changing alert evidence behavior.
+- Context visibility: time bounds, refresh cadence, live/paused state, timezone, monitor/source/trace rows, and handoff hrefs remain stable while locale detection moves to a non-visible data boundary.
+
+## Shared Date-Time Range Label Boundary - 2026-05-08
+
+- Hierarchy: keep the cold date/time picker as the same inline two-trigger control with a compact portal panel, without adding helper text, locale chips, or a separate action row.
+- Density: preserve the fixed calendar/time-column geometry, short clear/confirm footer, and reserved action-space alignment used by alert authoring forms.
+- Anti-AI-slop: avoid hardcoded Chinese chrome inside the reusable control, generic browser-native pickers, copied date-picker product copy, or hidden locale assumptions in shared UI.
+- Operator workflow clarity: alert notice and silence forms should supply localized empty/hour/minute/month/action labels through runtime i18n while preserving draft edits, clear, confirm, and hidden-input behavior.
+- Context visibility: start/end labels, selected time values, silence/notice rule context, and portal positioning remain stable while shared picker chrome becomes caller-owned locale copy.
+
+## Shared Date-Time Range Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep the cold date/time range control as the same inline two-trigger widget with fixed body portal and clear/confirm footer; fallback ordering must not add helper text, locale chips, or a second action row.
+- Density: preserve the two-value row, arrow separator, compact trigger labels, 392px calendar/time panel, 220px time-only panel, and current reserved action-space grid.
+- Anti-AI-slop: correct the deterministic fallback locale without inventing time intelligence, fake freshness, browser-native replacement controls, copied SaaS date picker language, or hidden locale state.
+- Operator workflow clarity: default control copy resolves from runtime messages with an English fallback when no caller label is provided; alert silence and notice rule forms can still pass caller-owned localized labels.
+- Context visibility: start/end labels, selected values, hidden input names, date/time parsing, portal positioning, month navigation labels, hour/minute columns, clear/confirm actions, and rule-authoring context remain visible through the existing `DateTimeRange` contract.
+
+## Overview Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep the overview first screen on the existing status grid, guidance rail, impacted entities, and quick entries; cache reuse must not add a loading rail, recap card, or summary panel.
+- Density: preserve the compact overview layout and short global loading copy while allowing only a short settled-cache window for route remounts.
+- Anti-AI-slop: do not mask backend errors, fake freshness, invent live readiness data, or copy external incident-console behavior; manual refresh must remain the explicit reload action.
+- Operator workflow clarity: route hops back to overview can reuse just-settled summary/alert data briefly, while refresh still bumps the nonce and reloads `/summary` plus the alert list.
+- Context visibility: top-alert route context, setup guidance, affected entities, and alert-derived signal handoffs stay visible with the same data shape while the first-screen cache boundary becomes explicit.
+
+## Entity List Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep the entity catalog on the existing compact count strip, filter row, dense table, and inline row actions; cache reuse must not add a status rail, loading recap, or fake entity-health panel.
+- Density: preserve the one-screen catalog rhythm so entity identity, owner, environment, status, alert counts, monitor counts, relation counts, and evidence actions remain scannable after route hops.
+- Anti-AI-slop: do not invent freshness labels, cache status badges, fake health, or copied inventory-console affordances while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled list data briefly, but explicit Refresh must bump a separate nonce so operators can force a reload for the current query.
+- Context visibility: search/type/status query state, row handoff hrefs, abnormal-count raw-status logic, and entity evidence summaries stay bound to the same backend list payload while the first-screen cache boundary becomes explicit.
+
+## Alert Center Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep alert filters, grouped alert rows, selected evidence, closure operations, and cross-signal context in the existing alert center surface; cache reuse must not add a freshness badge, status rail, or fake incident panel.
+- Density: preserve the dense alert investigation rhythm so severity, status, entity context, topology/source return context, and action affordances remain one-scan after route remounts.
+- Anti-AI-slop: do not mask backend errors, invent resolved state, fake alert counts, or copy external incident-console language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled alert data briefly, while explicit Refresh and successful closure mutations must bump a cache nonce for the current post-operation query.
+- Context visibility: entity/topology/OTLP return context, signal filters, closure feedback, and URL cleanup stay bound to the same query-controller payload while the first-screen cache boundary becomes explicit.
+
+## Alert Setting Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep threshold-rule search, dense rule table, create/edit authoring dialog, enable toggles, delete confirmation, datasource status, and incoming signal/entity/time evidence inside the existing alert setting surface; cache reuse must not add freshness chrome, fake rule health, or incident-console rails.
+- Density: preserve the compact rule-management rhythm so filters, selected ids, datasource evidence, rule rows, create/edit/delete state, and mutation feedback remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent rule readiness, fake notification coverage, or copy external alert-policy console language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled alert-rule data briefly, while Refresh, create/update, delete/batch delete, and enable toggles continue to bump `refreshKey`; search still invalidates through the query-derived define URL.
+- Context visibility: alert define list URL, refresh key, checked ids, create draft/mode, delete request state, datasource status, route evidence context, and localized loading copy remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Alert Notice Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep receiver/rule/template tabs, dense policy tables, modal authoring, delete confirmation, receiver tests, and incoming signal/entity/time evidence inside the existing notice console; cache reuse must not add freshness chrome, fake delivery health, or notification-center rails.
+- Density: preserve the compact notice-management rhythm so searches, pagination, selected ids, template filtering, switch state, validation feedback, and mutation messages stay scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent receiver readiness, fake template coverage, or copy SaaS notification-console language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled receiver/rule/template data briefly, while Refresh, receiver/rule/template create-update-delete, and rule switches continue to bump `refreshTick`; receiver/rule search still invalidates through query-derived notice URLs.
+- Context visibility: receiver/rule list URLs, refresh tick, selected tab, selected receiver/rule/template ids, modal draft state, evidence prefill, label options, and localized loading copy remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Alert Notice Validation Copy Boundary - 2026-05-09
+
+- Hierarchy: keep receiver/template/rule validation feedback in the existing modal field flow; do not add notification-health banners, validation summary cards, or extra status rails.
+- Density: preserve the compact authoring rhythm for dense alert-notice operations; only locale ownership for required-input/default-row labels changes.
+- Anti-AI-slop: do not infer receiver reachability, template validity, rule delivery health, notification success, or backend readiness from localized validation labels.
+- Operator workflow clarity: validation labels describe missing input only, while save/test/delete flows, selected tab, and mutation feedback remain unchanged.
+- Context visibility: receiver ids, rule ids, template ids, evidence prefill, label options, and modal draft state remain owned by the existing alert notice controller and client island.
+
+## Monitor Manage Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep monitor filters, dense list rows, selected monitor rail, batch controls, import/export, delete confirmation, and entity return context in the existing monitor manage surface; cache reuse must not add freshness chrome, inventory rails, or fake monitor health.
+- Density: preserve the one-screen monitor operations rhythm so status, scrape mode, selection, pagination, import/export controls, and row evidence remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent live state, fake monitor counts, or copy external inventory-console language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled monitor data briefly, while existing reload-key mutations for import, copy, enable, pause, and delete continue to force a fresh load.
+- Context visibility: query state, entity fallback context, selected monitor evidence, batch selection, and mutation feedback remain bound to the same backend list payload while the first-screen cache boundary becomes explicit.
+
+## Bulletin Center Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep bulletin list, action toolbar, metrics table, refresh behavior, and create/edit/delete dialogs in the existing bulletin center surface; cache reuse must not add freshness chrome, delivery analytics, or fake bulletin status.
+- Density: preserve the compact bulletin operations rhythm so list rows, metrics JSON, tab navigation, dialog state, and reload affordance remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent publish state, fake metrics availability, or copy external notification-center language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled bulletin data briefly, while the existing reload tick keeps explicit refresh and surface mutations as fresh-load boundaries.
+- Context visibility: controller-backed list payload, metrics drilldown, selected bulletin state, dialog flows, and reload feedback remain owned by the same surface while the first-screen cache boundary becomes explicit.
+
+## Public Status Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep organization brand, component health history, incident feed, mode switch, locale picker, year filter, and powered-by footer in the existing public status shell; cache reuse must not add freshness chrome, SLA rails, fake uptime, or marketing panels.
+- Density: preserve the compact public status rhythm so component rows, history cells, incident entries, locale/year controls, and mode state remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent reliability claims, fake incident state, or copy hosted status-page/SaaS wording while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled public status data briefly, while incident year/reload token changes continue to force the relevant fresh incident feed load.
+- Context visibility: public org/component payloads, incident year context, locale switching, mode selection, and powered-by context remain bound to the same controller payload while the first-screen cache boundary becomes explicit.
+
+## Status Setting Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep organization profile, component table, incident table, validation, public status link, and admin actions inside the existing status setting surface; cache reuse must not add freshness chrome, summary rails, fake SLA, or decorative status panels.
+- Density: preserve the compact admin editing rhythm so org fields, component rows, incident search/pagination, create/edit dialogs, and save/delete feedback remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent public uptime claims, fake component health, or copy hosted status-page console language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled status-management data briefly, while org/component/incident mutations and search reset/commit continue to bump the refresh tick for a fresh load.
+- Context visibility: org payload, component payload, incident query, public status link, refresh tick, and localized mutation feedback stay bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Label Management Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep label search, dense full-width list, copy/edit/delete actions, create/edit dialog, and save/delete feedback inside the existing label management surface; cache reuse must not add freshness chrome, summary rails, tag clouds, or governance panels.
+- Density: preserve the compact admin-list rhythm so label name, value, type, description, dialog mode, and row actions remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent label health, fake governance status, or copy tag-management marketplace language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled label data briefly, while Refresh, delete, and save continue to bump `reloadVersion`; search still invalidates through the query-derived label URL.
+- Context visibility: current search, label list URL, reload version, modal add/edit state, and localized loading/mutation feedback remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Collector Management Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep collector search, dense cluster table, refresh/deploy/online/offline/delete controls, row operations, health evidence, and timestamp context inside the existing collector management surface; cache reuse must not add freshness chrome, topology rails, fake cluster health, or inventory summary panels.
+- Density: preserve the compact admin-list rhythm so collector name, IP, mode, version, status, pinned/dispatched counts, and row actions remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent collector readiness, fake dispatch capacity, or copy cluster-console/inventory language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled collector data briefly, while Refresh continues to bump `reloadVersion`; search still invalidates through the query-derived collector URL.
+- Context visibility: current search, collector list URL, reload version, health evidence, batch actions, row operations, timestamps, and localized loading copy remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Collector Management Fact Label Boundary - 2026-05-08
+
+- Hierarchy: keep the collector fact strip as compact route/count/health metadata under the existing collector management model; moving the workspace label behind runtime i18n must not add a new summary panel, navigation rail, or fake inventory state.
+- Density: preserve the short fact-label rhythm so workspace, total, current page count, health evidence, pinned, and dispatched counts stay quickly scannable.
+- Anti-AI-slop: do not invent collector readiness, dispatch capacity, topology scope, or SaaS-style fleet wording while externalizing the fixed label.
+- Operator workflow clarity: the workspace fact continues to identify the `setting/collector` surface while localized copy stays owned by the shared runtime catalog.
+- Context visibility: route ownership, cluster health evidence, batch actions, and table rows stay in the same collector workbench context; only the remaining fixed label moves out of source-local copy.
+
+## Collector Management Row Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep collector name, IP, mode, version, status, tasks, health evidence, timestamps, and row actions in the existing dense cluster table.
+- Density: preserve terse inline missing-value fallback text without adding inventory cards, readiness summaries, or topology rails.
+- Anti-AI-slop: do not invent collector IP addresses, versions, readiness, dispatch capacity, or fleet topology while moving absent row facts into the shared fallback.
+- Operator workflow clarity: payload-provided IP and version values remain authoritative; only missing or blank row facts use localized fallback copy.
+- Context visibility: search, refresh, deploy/online/offline/delete controls, health evidence, status tone, mutation affordances, and controller-backed collector data remain unchanged.
+
+## Setting Define Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep monitoring-template search, dense definition list, YAML editor, preview panel, datasource status, draft state, and create/edit/preview/save/delete actions inside the existing definition surface; cache reuse must not add freshness chrome, marketplace rails, fake template health, or rule-builder summary panels.
+- Density: preserve the compact editor rhythm so definition name/type/datasource/expression, current YAML label, datasource status, preview diagnostics, dark mode, and save feedback remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent definition readiness, fake datasource health, or copy SaaS rule-builder/marketplace language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled definition data briefly, while successful saves bump `reloadVersion`; search still invalidates through the query-derived definition URL.
+- Context visibility: current search, definition list URL, reload version, selected definition, editor draft, preview result, save state, and localized loading/mutation feedback remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Setting Config Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep locale, timezone, theme selectors, validation, save feedback, locale/theme side effects, and reload behavior inside the existing system config form; cache reuse must not add freshness chrome, deployment rails, fake system-health blocks, or profile-summary panels.
+- Density: preserve the compact settings-form rhythm so current config, draft values, timezone options, save state, and field labels remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent deployment readiness, fake tenant state, or copy SaaS account-settings language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled config data briefly, while successful saves bump `reloadVersion`; locale/theme side effects and browser reload remain controlled by the existing form.
+- Context visibility: system config URL, timezone URL, reload version, draft locale/timezone/theme, save state, validation, and localized loading/mutation feedback remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Setting Object Store Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep provider selection, OBS credential fields, validation, save feedback, and storage-controller calls inside the existing object-store settings form; cache reuse must not add freshness chrome, summary rails, fake bucket-health blocks, or cloud-console panels.
+- Density: preserve the compact settings-form rhythm so provider, OBS access key, secret key, bucket, endpoint, save path, validation, and save state remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent object-storage capacity, fake archive readiness, or copy cloud-provider console language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled object-store data briefly, while successful saves bump `reloadVersion`; provider switching and secret-field editing remain controlled by the existing form.
+- Context visibility: object-store config URL, reload version, draft provider/OBS fields, save state, validation, and localized loading/mutation feedback remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Setting Message Server Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep email and SMS summary rows, provider-specific dialogs, validation, save feedback, toggles, and message-server controller calls inside the existing settings surface; cache reuse must not add freshness chrome, delivery-health cards, fake provider status, or notification-center rails.
+- Density: preserve the compact summary/dialog rhythm so email host/port/user, SMS provider fields, auth mode, toggles, save state, and mutation feedback remain scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent delivery metrics, fake channel readiness, or copy SaaS notification-center language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled message-server data briefly, while successful email and SMS saves both bump `reloadVersion`; dialog cancel/snapshot behavior remains controlled by the existing form.
+- Context visibility: email config URL, SMS config URL, reload version, email draft, SMS draft/snapshot, open dialog, save state, validation, and localized loading/mutation feedback remain bound to the same controller-backed surface while the first-screen cache boundary becomes explicit.
+
+## Setting Token Workbench Settled Cache Boundary - 2026-05-08
+
+- Hierarchy: keep the token counts strip, dense admin table, masked token values, creator/time columns, and existing generate/delete affordances inside the settings owner; cache reuse must not add freshness chrome, API-key marketing cards, fake scope badges, or token-health panels.
+- Density: preserve the compact admin rhythm so total/active/expired counts, token names, masks, creator, create/expire/last-used times, and row actions stay scannable after route hops.
+- Anti-AI-slop: do not mask backend errors, invent token scopes, fake revocation state, fake last-use freshness, or copy SaaS API-key console language while adding a short settled-cache window.
+- Operator workflow clarity: route remounts can reuse just-settled token list data briefly; because token create/delete mutations are not implemented in this surface yet, this slice must not invent mutation invalidation or pretend those actions are wired.
+- Context visibility: token list URL, localized loading copy, counts, expiry calculation, masked values, and empty state remain bound to the same controller-backed payload while the first-screen cache boundary becomes explicit.
+
+## Settings Governance Review Label Boundary - 2026-05-08
+
+- Hierarchy: keep governance review data as route/current-capability metadata feeding the existing settings surface; moving group names to runtime keys must not add a governance dashboard, menu branch, or future-domain cards.
+- Density: preserve the compact group/route mapping so users, API access, notifications, templates/plugins, and MCP/AI foundations remain scan-friendly without expanding roadmap-only domains into app navigation.
+- Anti-AI-slop: do not invent shipped governance modules, SaaS admin packaging, fake security posture, or future product identity while removing source-local group labels.
+- Operator workflow clarity: review records continue to expose implemented routes and foundation-only entries; label ownership simply follows the same runtime message catalog already used by the governance rows.
+- Context visibility: current route candidates, future roadmap-only docs, navigation policy, and no-fake-future-route boundaries stay unchanged.
+
+## Platform Footer License Copy Boundary - 2026-05-08
+
+- Hierarchy: keep the footer as a quiet shared legal line below the app frame and passport shell; moving the license sentence behind runtime i18n must not add a branded footer band, marketing claim, or extra navigation.
+- Density: preserve the compact two-line footer rhythm so product name, version, copyright, site link, and license remain readable without competing with operator workflows.
+- Anti-AI-slop: do not rewrite Apache/HertzBeat identity, invent commercial packaging, or add SaaS-style trust badges while externalizing the fixed license copy.
+- Operator workflow clarity: app and auth shells pass the same translated license copy into the shared footer, preserving one source of footer layout and legal posture.
+- Context visibility: version, current year, project link, passport shell context, and app-frame context remain unchanged; only the fixed visible license sentence moves to the runtime catalog.
+
+## AppFrame Utility Icon Label Boundary - 2026-05-08
+
+- Hierarchy: keep the top-left menu and GitHub icon buttons as compact utility triggers in the existing header; moving accessible labels behind runtime i18n must not add text buttons, extra nav items, or a second header layer.
+- Density: preserve the icon-only rhythm so menu, GitHub, setup, notification, mute, lock, settings, and user affordances remain a single scannable utility cluster.
+- Anti-AI-slop: do not rename GitHub, add SaaS help text, invent repository status, or change header information architecture while externalizing labels.
+- Operator workflow clarity: aria labels and screen-reader text resolve from the same runtime catalog used by the rest of AppFrame chrome, without changing trigger behavior or links.
+- Context visibility: app navigation, setup progress, GitHub href, menu trigger marker, and route content remain unchanged; only hardcoded accessible copy leaves the component source.
+- Notification-time fallback update: keep alert title, status, and active-time metadata in the same notification rows while moving missing timestamp markers to shared `common.none`; do not add fake alert recency, unread status, or notification-health summaries.
+- Mute-label consistency update: keep mute/unmute as the same compact notification control while sharing one action label between aria text and screen-reader text; do not add visible header text, unread state, or notification-health chrome.
+
+## AppFrame User Menu Label Boundary - 2026-05-08
+
+- Hierarchy: keep the avatar as the same compact header utility trigger; moving its accessible wrapper label behind runtime i18n must not add account rails, profile panels, or extra header text.
+- Density: preserve the icon-only utility cluster rhythm so setup, notification, mute, lock, settings, and user controls remain a single scan path.
+- Anti-AI-slop: do not invent user profile data, tenant badges, SaaS account packaging, security posture, or identity-management claims while localizing the wrapper label.
+- Operator workflow clarity: only the user menu control label is localized; the current username remains data passed into the label and the settings/about/logout menu stays unchanged.
+- Context visibility: user dropdown links, avatar marker, route context, and shared AppFrame chrome stay in the existing positions while the hardcoded accessible copy leaves the component source.
+
+## Overlay Dialog Close Label Boundary - 2026-05-08
+
+- Hierarchy: keep `OverlayDialog` as the shared modal/drawer shell for existing detail, authoring, and confirmation workflows; moving the close label behind messages must not change titles, kickers, footer actions, or placement.
+- Density: preserve the compact square close affordance and current header spacing so dense operator dialogs do not grow extra chrome.
+- Anti-AI-slop: do not add generic modal prose, helper text, SaaS account language, fake workflow hints, or copied design-system branding while externalizing the close aria copy.
+- Operator workflow clarity: close behavior remains owned by each caller's `onClose`; this slice only removes the English source-local accessible label from the shared shell.
+- Context visibility: dialog title-based aria context, side-drawer click-away behavior, centered-dialog behavior, and data markers stay unchanged.
+
+## DataTable Empty State Boundary - 2026-05-08
+
+- Hierarchy: keep `DataTable` as the shared dense evidence table primitive; moving its default empty text behind messages must not change headers, rows, row actions, or table layout.
+- Density: preserve the single-row empty state so empty monitor, detail, and evidence tables stay compact instead of gaining cards, icons, or helper panels.
+- Anti-AI-slop: do not invent fake data, guidance prose, no-data reasons, onboarding copy, or vendor-like empty-state decoration while externalizing the fallback label.
+- Operator workflow clarity: caller-provided `emptyState` remains authoritative for domain-specific table copy; only the shared fallback uses the runtime message catalog.
+- Context visibility: ops token classes, row attributes, row click behavior, column rendering, and table options stay unchanged.
+
+## SearchRow Default Action Label Boundary - 2026-05-08
+
+- Hierarchy: keep `SearchRow` as the compact shared filter/search primitive; default labels must not add helper text, secondary headers, or new chrome around the direct input and detached action buttons.
+- Density: preserve the fixed-width input, inline filter slot, and small icon+text buttons so repeated admin tables and evidence lists stay scan-friendly.
+- Anti-AI-slop: do not invent search guidance, placeholder prose, empty-state explanations, SaaS query language, or fake filter coverage while providing safe default labels.
+- Operator workflow clarity: caller-provided `searchLabel` and `clearLabel` remain authoritative for domain-specific copy; the shared fallback only prevents unlabeled search/clear actions when a caller omits labels.
+- Context visibility: data markers, submit/clear behavior, trailing actions, filter slot order, and controlled input ownership stay unchanged.
+
+## WorkspaceTabStrip Default Navigation Label Boundary - 2026-05-08
+
+- Hierarchy: keep `WorkspaceTabStrip` as a quiet inline tab navigation primitive below existing page headers; externalizing the default aria label must not add visible headings, helper text, or a second navigation band.
+- Density: preserve the compact default/card tab treatments, badge chip rhythm, and flex-wrap behavior so dense workbench headers keep their current scan path.
+- Anti-AI-slop: do not invent workspace names, fake tabs, onboarding prose, or product-positioning copy while moving the shared accessible fallback into runtime messages.
+- Operator workflow clarity: caller-provided `ariaLabel` remains authoritative for domain-specific tabs; the shared default only gives unlabeled workspace tab groups a catalog-backed accessible name.
+- Context visibility: tab labels, href/button behavior, active/disabled state, badge content, data markers, and workbench/shell placement remain unchanged.
+
+## Log Related Trace Dialog Default Chrome Boundary - 2026-05-08
+
+- Hierarchy: keep the right-side trace preview drawer, compact stage meta strip, waterfall/list body, selected evidence facts, and event detail area unchanged while moving only fallback loading/empty/timeline labels into runtime messages.
+- Density: preserve the narrow 300px evidence rail, compact status states, and caller-owned header/action content so log-to-trace drilldown remains a quick inspection surface.
+- Anti-AI-slop: do not invent trace data, fake spans/events, fake full-trace navigation, or vendor-style trace storytelling while externalizing default labels.
+- Operator workflow clarity: page-provided localized labels remain authoritative; shared dialog defaults only cover direct/reused rendering when a caller omits loading, empty, or waterfall labels.
+- Context visibility: selected span/event state, stage facts, badges, meta items, trace waterfall ticks, and event-to-span return action remain visible and data-backed.
+
+## ObservabilityTimeline Empty State Boundary - 2026-05-08
+
+- Hierarchy: keep `ObservabilityTimeline` as the compact chronological evidence primitive; moving the default empty text behind messages must not add headers, helper copy, icons, or a separate empty-state card.
+- Density: preserve the single raised empty row and existing timeline item spacing so activity feeds and operator side panels remain compact.
+- Anti-AI-slop: do not invent events, fake timestamps, onboarding guidance, SaaS activity prose, or decorative timeline states while externalizing the shared fallback label.
+- Operator workflow clarity: caller-provided `emptyText` remains authoritative for domain-specific timelines; the shared fallback only covers direct/reused rendering when a caller omits copy.
+- Context visibility: item title/detail/timestamp rendering, tone dots, connecting rail, ops token classes, and stable list keys remain unchanged.
+
+## OverviewGuidancePanel Default Label Boundary - 2026-05-08
+
+- Hierarchy: keep the overview guidance rail as one compact next-step panel with headline, description, reasons, and follow-up links; moving default section labels behind messages must not add a second guidance header, summary rail, or marketing copy.
+- Density: preserve the compact/default density switches, pill-row reason mode, and current reason/link spacing so the overview rail remains scannable beside status, issue, and setup panels.
+- Anti-AI-slop: do not invent next steps, fake recommendations, onboarding prose, or copied SaaS dashboard packaging while externalizing only the shared fallback labels.
+- Operator workflow clarity: overview page-provided localized labels remain authoritative; shared defaults only cover direct/reused rendering when a caller omits start/reasons/next labels.
+- Context visibility: headline, description, primary/secondary actions, reason values, next-link hrefs, density markers, and data attributes stay unchanged.
+
+## OverviewChecklist Status Label Boundary - 2026-05-08
+
+- Hierarchy: keep the overview checklist as the compact next-step readiness list below the existing guidance/status surfaces; externalizing ready/pending labels must not add badges, icons, helper copy, or a new setup panel.
+- Density: preserve compact and default row spacing, small status text, and two-column layout so the healthy next-steps rail remains scannable.
+- Anti-AI-slop: do not invent readiness scores, fake checks, onboarding prose, or SaaS setup packaging while moving only the shared status words into runtime messages.
+- Operator workflow clarity: checklist item labels remain caller-owned; the shared primitive only owns the fallback ready/pending words derived from the boolean `ready` field.
+- Context visibility: item keys, item labels, readiness booleans, density markers, ops token classes, and row ordering stay unchanged.
+
+## Login Form Credential Chrome Boundary - 2026-05-08
+
+- Hierarchy: keep the Angular-style gray passport card, hidden field labels, credential icon row, remember checkbox, warning notice, and single submit action unchanged while moving small credential chrome labels into runtime messages.
+- Density: preserve the compact four-row form rhythm and eye icon button placement so authentication remains a quick operator entry point.
+- Anti-AI-slop: do not add onboarding prose, fake security posture, SaaS hero language, or new password policy hints while externalizing only show/hide and remember labels.
+- Operator workflow clarity: the password visibility toggle must keep its stateful accessible name, and the remember checkbox must retain the same default checked behavior and cold checkbox owner markers.
+- Context visibility: login heading, username/password placeholders, default-password notice logic, redirect/session bootstrap, panel markers, and passport shell locale controls remain unchanged.
+
+## Login Form Component Locale Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the passport shell hero, gray credential card, hidden labels, field icons, remember checkbox, password visibility control, submit button, notice slot, and footer unchanged; only align the regression with the declared zh-CN provider locale.
+- Density: preserve the compact credential-card rhythm without adding security banners, policy panels, onboarding text, or extra auth status rows.
+- Anti-AI-slop: do not invent MFA, token health, workspace policy, tenant readiness, SaaS hosting, or AI login assistance while replacing English default test copy.
+- Operator workflow clarity: when the mocked provider declares `locale: 'zh-CN'`, the login regression should assert the same zh-CN passport intro, field prompts, remember/password controls, submit action, and footer copy operators see.
+- Context visibility: redirect target ownership, default-password notice logic, session bootstrap, token persistence, password visibility state, panel markers, and locale controls remain unchanged.
+
+## Public Status Display Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep the public status brand block, component history rows, incident cards, footer attribution, and component/incident navigation as-is while moving shared English fallback labels behind runtime messages.
+- Density: preserve the compact public board labels and incident timeline rows; do not add summary rails, badges, helper prose, or extra date captions.
+- Anti-AI-slop: do not invent public incidents, fake uptime, fake component health, commercial status-page packaging, or copied SaaS public-status copy while correcting fallback ownership.
+- Operator workflow clarity: status translators remain authoritative; English constants only cover missing translation keys so component history, incident range, start/update, and footer labels stay deterministic.
+- Context visibility: org state, home/feedback links, year/history/uptime labels, component history blocks, incident ranges, navigation labels, and timestamp formatting remain data-backed and unchanged.
+
+## Public Status Footer Navigation Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep the public status footer attribution, component/incident navigation links, and incident start/update labels in their current public board positions while moving only fallback label ownership into runtime messages.
+- Density: preserve the terse footer/link/date-label copy; do not add extra timestamp captions, breadcrumbs, or status-page marketing language.
+- Anti-AI-slop: do not invent public incidents, uptime claims, external SaaS positioning, or fake navigation while aligning missing-key fallbacks with the runtime catalog.
+- Operator workflow clarity: translated labels still win; English constants only cover missing keys for footer attribution, status/incident navigation, and incident start/update labels.
+- Context visibility: home/feedback links, status view mode, incident range rows, update timestamps, footer brand attribution, and public status route markers remain unchanged.
+
+## Public Status Brand Link Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep the public status brand header, home/feedback affordances, year selector, history labels, uptime rows, update stamp, and 30-day summary in their current board positions while moving fallback ownership into runtime messages.
+- Density: preserve the compact public-board word labels; do not add helper text, breadcrumbs, uptime claims, or extra date captions.
+- Anti-AI-slop: do not invent component health, public incidents, fake uptime, commercial status-page packaging, or copied SaaS public-status language while externalizing only deterministic missing-key fallbacks.
+- Operator workflow clarity: translators remain authoritative; English constants only cover missing keys for brand links, timeline labels, uptime/update labels, and the today/30-day row chrome.
+- Context visibility: organization state, home and feedback URLs, year values, history blocks, uptime percentages, update timestamps, and public status route markers remain data-backed and unchanged.
+
+## Status Center Admin Label Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep the public status page title, settings page title, organization facts, component/incident counters, state field, and current-view fact in their existing admin/public positions while moving fallback ownership into runtime messages.
+- Density: preserve the terse fact labels and metric labels; do not add summary rails, helper text, badges, breadcrumbs, or public-status marketing copy.
+- Anti-AI-slop: do not invent health, incidents, org ownership, uptime, or SaaS status-page packaging while externalizing only deterministic missing-key fallbacks.
+- Operator workflow clarity: setting-specific translation keys remain authoritative, status-center shared keys remain secondary, and English constants only cover missing translations.
+- Context visibility: workspace, organization name/fallback, component count, incident count, org state, current view, and org copy remain derived from real payloads and unchanged.
+
+## Status Center State Label Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep component state badges, incident state rows, organization state summaries, status facts, and public board posture in their current positions while moving only state-word fallback ownership into runtime messages.
+- Density: preserve the compact Normal/Abnormal/Unknown and incident-state wording; do not add legend blocks, helper descriptions, extra badges, or copied status-page posture prose.
+- Anti-AI-slop: do not invent health, uptime, incidents, maintenance states, or fake operational status while externalizing deterministic state fallback labels.
+- Operator workflow clarity: numeric/string normalization and tone mapping remain unchanged; translators remain authoritative and English constants only cover missing translation keys.
+- Context visibility: component state, incident state, organization state, badge tones, state colors, timestamps, and payload-derived rows remain data-backed and unchanged.
+
+## Status Center Component Row Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep component-mode admin rows in the existing status-center list; missing component copy must not add secondary details, banners, or status panels.
+- Density: preserve title, copy, and state/time meta as the compact scan path.
+- Anti-AI-slop: do not invent descriptions, endpoints, uptime, ownership, or health when component rows lack summary data.
+- Operator workflow clarity: real description/endpoint copy remains authoritative; absent copy uses the shared runtime empty marker.
+- Context visibility: active component/incident mode, component state labels, incident rows, org rows, and status page navigation remain unchanged.
+
+## Status Center Incident Title Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep incident fallback titles inside the same incident list/card rows; missing ids must not create helper banners, synthetic incident names, or extra status blocks.
+- Density: preserve the compact incident title, latest-message copy, state/time meta, and public incident card rhythm.
+- Anti-AI-slop: do not invent incident ids, incident names, recovery state, impact, or freshness when payload titles/names/ids are absent.
+- Operator workflow clarity: real incident title/name/id remains authoritative; absent ids use the shared runtime empty marker in the existing fallback title.
+- Context visibility: incident state, timeline contents, component context, mode switch, and timestamp evidence remain unchanged.
+
+## Status Center Empty Row Meta Boundary - 2026-05-09
+
+- Hierarchy: keep empty component/incident rows in the same status-center list position; missing meta must not create empty-state banners, counters, or summary rails.
+- Density: preserve the compact title/copy/meta row shape for empty component and incident modes.
+- Anti-AI-slop: do not invent timestamps, health, incident freshness, uptime, or recovery status when the row is representing filtered/empty data.
+- Operator workflow clarity: empty row titles/copy stay translator-owned and absent meta uses the shared runtime empty marker.
+- Context visibility: active mode, component/incident tabs, public shell, org facts, and real payload rows remain unchanged.
+
+## Observability TabStrip Navigation Label Boundary - 2026-05-09
+
+- Hierarchy: keep shared tab strips as the compact navigation row already used by monitor and workbench panels while moving only the default tablist aria label into runtime messages.
+- Density: preserve tab item labels, selected indicators, card/line variants, and extra action slots; do not add visible helper copy, captions, rails, or nested card chrome.
+- Anti-AI-slop: do not invent workflow states, fake tab destinations, SaaS-style navigation prose, or decorative status labels while externalizing the shared accessibility label.
+- Operator workflow clarity: caller-provided tab labels and optional aria labels remain authoritative; the shared default only covers direct rendering and missing caller labels.
+- Context visibility: selected tab key, panel id prefix, keyboard navigation, tone/variant styling, and extra controls remain unchanged.
+
+## Monitor Manage Status Label Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep monitor status badges and row status labels in the existing dense monitor table while moving only no-translator fallback words into runtime messages.
+- Density: preserve the compact Running/Abnormal/Paused badge rhythm; do not add status legends, helper text, summary rails, or fake health panels.
+- Anti-AI-slop: do not invent monitor health, uptime, collector evidence, SaaS status wording, or new color semantics while correcting fallback ownership.
+- Operator workflow clarity: localized `t(...)` labels still win; fallback labels are only for direct view-model/helper usage without a translator.
+- Context visibility: raw monitor status numbers, badge tone mapping, selection, detail/edit navigation, and row evidence remain unchanged.
+
+## Label Management Metric Label Boundary - 2026-05-09
+
+- Hierarchy: keep label management metrics as compact current-page counters; moving label text behind runtime messages must not add a new summary rail or change the dense label table hierarchy.
+- Density: preserve the three short auto/user/preset counters and their existing tones; do not add helper prose, legends, badges, or label taxonomy explanations.
+- Anti-AI-slop: do not invent label usage, tenant policy, governance status, or copied SaaS label-management copy while externalizing deterministic metric labels.
+- Operator workflow clarity: translated metric labels come from the existing label management translator, while raw label type counts and tone mapping remain data-driven.
+- Context visibility: label type numbers, current page scope, total facts, search state, row actions, and monitor handoff links remain unchanged.
+
+## Plugin Management Metric Label Boundary - 2026-05-09
+
+- Hierarchy: keep plugin management metrics as compact current-page counters; moving labels behind runtime messages must not add a marketplace summary rail or change the dense plugin table hierarchy.
+- Density: preserve the three short enabled/disabled/params counters and their existing tones; do not add helper prose, legends, usage cards, or plugin-category explanations.
+- Anti-AI-slop: do not invent marketplace health, plugin usage, SaaS packaging, tenant policy, or fake execution status while externalizing deterministic metric labels.
+- Operator workflow clarity: translated metric labels come from the plugin management translator, while enabled/disabled counts, parameter counts, and tone mapping stay data-driven.
+- Context visibility: plugin enable state, parameter counts, current page scope, selected ids, upload/edit/delete actions, and table row handoffs remain unchanged.
+
+## Shared Select Empty Trigger Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep `Select` as the shared compact cold dropdown trigger used by dense settings and workbench forms.
+- Density: preserve the single trigger label and chevron; do not add helper text, placeholder badges, empty-menu panels, or extra select chrome.
+- Anti-AI-slop: do not invent a selected option, default domain, form value, or fake configuration state when no option is available.
+- Operator workflow clarity: caller-provided options and selected values remain authoritative; only the no-option trigger fallback uses the shared localized empty value.
+- Context visibility: hidden native select, hidden form input, listbox geometry, keyboard navigation, portal ownership, and selected option markers remain unchanged.
+
+## Setting Define Empty Fact Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep the definition list, YAML editor, preview rows, and datasource status panel in the existing two-column definition workspace.
+- Density: preserve terse inline fallbacks in row copy and preview metadata; do not add badges, helper rows, empty panels, or secondary validation chrome.
+- Anti-AI-slop: do not invent datasource names, expression text, alert types, backend health, or template readiness while localizing missing definition facts.
+- Operator workflow clarity: payload-provided type, datasource, expression, enable state, and selected definition names remain authoritative; only absent facts use the shared localized fallback.
+- Context visibility: search, selected row, editor title, current YAML chip, preview action, dark-mode toggle, datasource JSON, and save/delete actions remain unchanged.
+
+## Setting Define Editor Subtitle Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep editor title/subtitle inside the existing YAML editor header beside the current YAML chip and dark-mode toggle.
+- Density: preserve the single compact subtitle line; do not add helper cards, badges, warnings, or definition-readiness chrome.
+- Anti-AI-slop: do not invent datasource, alert type, expression, health, or template readiness when preview metadata is absent.
+- Operator workflow clarity: preview metadata from the selected definition remains authoritative; only an absent preview meta row uses the shared localized fallback.
+- Context visibility: definition list, selected row, editor title, current YAML chip, preview action, datasource JSON, and save/delete actions remain unchanged.
+
+## Status Setting Table Empty-Cell Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep organization profile, component table, incident table, public status link, and delete confirmations in the existing status settings console.
+- Density: preserve single-cell fallbacks inside dense rows; do not add empty badges, helper rows, summary rails, or public-status health panels.
+- Anti-AI-slop: do not invent component names, labels, incident messages, affected components, uptime, recovery state, or fake status evidence while localizing empty cells.
+- Operator workflow clarity: payload-provided component names, labels, incident messages, and affected component chips remain authoritative; only absent row facts use the shared localized fallback.
+- Context visibility: component/incident tabs, search, row actions, state/method tags, timestamps, create/edit dialogs, and delete modal context remain unchanged.
+
+## Label Management Dialog Preview Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep the label authoring preview as the small side preview inside the existing dialog, not a separate card or wizard step.
+- Density: preserve the single preview pill; do not add helper prose, validation banners, governance hints, or extra preview rows for empty drafts.
+- Anti-AI-slop: do not invent label names, values, descriptions, ownership, or usage evidence while moving the empty preview fallback into runtime messages.
+- Operator workflow clarity: typed name/tag value still controls the preview immediately; only an empty display name uses the shared localized fallback.
+- Context visibility: search, table rows, type badges, description fallback, edit actions, dialog fields, and save/cancel controls remain unchanged.
+
+## Label Management Unknown Type Boundary - 2026-05-09
+
+- Hierarchy: keep unknown label-type evidence in the existing type badge cell beside name, description, edit time, and row actions.
+- Density: preserve one compact badge label for unknown type values; do not add helper panels, warning rows, or governance explanation copy.
+- Anti-AI-slop: do not invent label taxonomy, ownership, policy state, or usage counts while moving only the unknown-type fallback into runtime messages.
+- Operator workflow clarity: known auto/user/preset labels keep their existing localized mappings; only unrecognized numeric types use the catalog-owned fallback.
+- Context visibility: search, display-name links, descriptions, edit-time formatting, copy/edit/delete actions, and dialog preview remain unchanged.
+
+## Label Management Description Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep label name, type, description, edit time, and row actions in the existing dense table without adding a label summary rail or secondary empty panel.
+- Density: preserve the single-cell description fallback; do not add helper paragraphs, status chips, placeholder cards, or extra authoring hints.
+- Anti-AI-slop: do not invent label descriptions, ownership, usage counts, fake governance state, or SaaS taxonomy copy while moving the blank-description fallback into runtime messages.
+- Operator workflow clarity: real label descriptions remain payload-owned; only blank descriptions use the shared localized fallback.
+- Context visibility: label search, display-name links, type badges, copy/edit/delete actions, dialog preview, and table layout remain unchanged.
+
+## Plugin Management Type Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep plugin type evidence in the existing table cell and preserve row identity, status, parameter count, and actions around it.
+- Density: preserve the single compact fallback chip/text when no execution type is present; do not add an empty-state card, helper text, marketplace rail, or secondary status block.
+- Anti-AI-slop: do not invent plugin execution types, plugin health, marketplace availability, or telemetry evidence while moving the no-type fallback into runtime messages.
+- Operator workflow clarity: raw plugin item type labels remain payload-driven and translated through the existing type-label helper; only the empty type-cell fallback becomes catalog-owned.
+- Context visibility: selected ids, enable toggles, edit/delete actions, param counts, upload dialog, and table layout remain unchanged.
+
+## Plugin Management Row Copy Boundary - 2026-05-09
+
+- Hierarchy: keep plugin identity, execution type chips, enabled state, parameter count, and row operations in the existing dense table without adding marketplace or health layers.
+- Density: preserve the compact row/meta copy rhythm; do not add helper paragraphs, summary rails, nested cards, or new plugin-domain prose.
+- Anti-AI-slop: do not invent plugin usage, SaaS marketplace status, fake health, fake telemetry, or future-only plugin types while moving row copy ownership into runtime messages.
+- Operator workflow clarity: row fallback/type/status/parameter text becomes localized, while enable toggles, selected ids, upload/edit/delete actions, and param editing behavior remain data-driven.
+- Context visibility: current page scope, selected rows, table handoff data, raw plugin item types, and existing action labels remain visible and unchanged.
+
+## API Message Error Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep API client errors as shared data-layer failures that downstream loading/error states can display without adding page-local recovery panels.
+- Density: preserve the single-line non-zero response fallback; do not add helper paragraphs, status legends, stack traces, or workflow hints in shared client code.
+- Anti-AI-slop: do not invent backend state, request health, retry advice, fake observability evidence, or SaaS error copy while moving fallback ownership into runtime messages.
+- Operator workflow clarity: backend `msg` values remain authoritative; the runtime fallback only covers non-zero `ApiResponse` payloads without a message.
+- Context visibility: auth refresh, token clearing, request methods, response payload handling, and caller-specific error rendering remain unchanged.
+
+## API Request Status Error Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep failed HTTP responses as shared API-client failures that existing workbench loading/error states can surface without adding route-local recovery UI.
+- Density: preserve the compact status-only fallback; do not add retry guidance, helper paragraphs, stack traces, legends, or new alert chrome in the shared client.
+- Anti-AI-slop: do not invent backend outage state, fake health, remediation advice, SaaS incident language, or observability evidence while moving deterministic fallback copy into runtime messages.
+- Operator workflow clarity: HTTP status remains the only data in this fallback; non-zero `ApiResponse` message handling and backend-provided messages stay on their existing path.
+- Context visibility: path normalization, auth headers, Accept-Language, refresh-on-401, token clearing, cache mode, and caller-owned error rendering remain unchanged.
+
+## Alert Group Mutation Id Error Boundary - 2026-05-09
+
+- Hierarchy: keep invalid alert-group mutation ids as a controller-level validation failure that existing alert action feedback can surface without adding new panels or route-local recovery UI.
+- Density: preserve the terse single-line validation message; do not add helper prose, legends, summaries, or extra close/ack/recover chrome.
+- Anti-AI-slop: do not invent alert status, closure outcome, health, retry advice, fake incident state, or remediation evidence while moving deterministic fallback copy into runtime messages.
+- Operator workflow clarity: valid group-alert ids still build the same acknowledge/recover/reopen/delete endpoints; the fallback only covers empty or invalid id input before mutation.
+- Context visibility: closure action mapping, query refresh behavior, selected alert evidence, entity/topology/time return context, and caller-owned feedback rendering remain unchanged.
+
+## Alert Rule Note Query Copy Boundary - 2026-05-09
+
+- Hierarchy: keep group, inhibit, and silence query notes as the small existing note rows inside their current alert rule workbenches.
+- Density: preserve the two-row group/inhibit notes and the silence count/query note without adding helper rails, query builders, sort panels, or status badges.
+- Anti-AI-slop: do not invent rule freshness, search success, backend health, alert volume, grouping quality, or silence/inhibit effectiveness while localizing the note copy.
+- Operator workflow clarity: note rows still describe the preserved search/sort behavior only; rule table filtering, mutations, and selected-rule context stay unchanged.
+- Context visibility: route wrappers, client islands, evidence context, selected rows, and mutation feedback remain owned by the existing alert rule pages.
+
+## Alert Rule Selected Id Meta Boundary - 2026-05-09
+
+- Hierarchy: keep the selected rule id as the terse meta line on the existing selected group, inhibit, and silence rule rows.
+- Density: preserve the selected-rule row count and layout without adding id badges, rule detail panels, helper labels, or status summaries.
+- Anti-AI-slop: do not invent rule health, validation state, ownership, freshness, grouping quality, silence coverage, or inhibit effectiveness while localizing the id meta.
+- Operator workflow clarity: the id remains only a stable selected-rule identifier; enable state, label counts, timers, and mutation behavior stay unchanged.
+- Context visibility: route wrappers, client islands, evidence context, selected rows, and mutation feedback remain owned by the existing alert rule pages.
+
+## Alert Silence Selected Type Meta Boundary - 2026-05-09
+
+- Hierarchy: keep the silence selected-rule type as the small meta text on the existing strategy row.
+- Density: preserve the selected-rule row count and compact labels without adding type badges, scheduling panels, helper text, or status summaries.
+- Anti-AI-slop: do not invent silence coverage, match quality, rule health, active alert count, schedule validation, or backend freshness while localizing the type meta.
+- Operator workflow clarity: the type meta still reflects only the selected silence rule type; strategy copy, labels, days, and mutation behavior stay unchanged.
+- Context visibility: route wrapper, client island, evidence context, selected rows, and mutation feedback remain owned by the existing alert silence page.
+
+## Alert Silence Match-All Boolean Boundary - 2026-05-09
+
+- Hierarchy: keep the match-all boolean as inline copy in the existing silence table row summary.
+- Density: preserve the single compact row summary without adding boolean badges, explanatory text, schedule panels, or secondary status rows.
+- Anti-AI-slop: do not invent silence coverage, rule correctness, active alert volume, matcher confidence, or backend freshness while localizing the boolean token.
+- Operator workflow clarity: the boolean still reports only the stored `matchAll` value; enable state, label count, selected rows, and mutations stay unchanged.
+- Context visibility: route wrapper, client island, evidence context, row list, and mutation feedback remain owned by the existing alert silence page.
+
+## Alert Group Timer Unit Boundary - 2026-05-09
+
+- Hierarchy: keep group wait, interval, and repeat durations in the existing row meta and selected timer row.
+- Density: preserve the compact timer strings without adding unit badges, duration pickers, helper prose, or secondary timing panels.
+- Anti-AI-slop: do not invent grouping latency, rule health, notification volume, backend freshness, or timer validation while localizing the seconds unit.
+- Operator workflow clarity: values still reflect only stored group timers; enable state, labels, selected id, query notes, and mutations stay unchanged.
+- Context visibility: route wrapper, client island, evidence context, group row list, selected rows, and mutation feedback remain owned by the existing alert group page.
+
+## Alert Center Evidence Id Meta Boundary - 2026-05-09
+
+- Hierarchy: keep monitor id and span id as terse meta tokens inside the existing inherited evidence context rows.
+- Density: preserve the compact context row count without adding id badges, trace panels, monitor detail cards, or helper prose.
+- Anti-AI-slop: do not invent monitor health, trace status, alert causality, backend freshness, or correlation confidence while localizing id meta labels.
+- Operator workflow clarity: ids still reflect only the inherited query context; time, source, monitor, trace, closure, and evidence handoff behavior stay unchanged.
+- Context visibility: alert center route wrapper, client island, evidence context rows, topology/time return context, and closure feedback remain owned by the existing alert center page.
+
+## Public Status Incident Load Failure Boundary - 2026-05-09
+
+- Hierarchy: keep public status incident feed failures as controller-level partial-load state while preserving the existing public brand, component list, incident list, and footer hierarchy.
+- Density: preserve the single incidents error string and empty incident list fallback; do not add retry panels, legends, summary rails, marketing status copy, or route-local recovery UI.
+- Anti-AI-slop: do not invent uptime, incident health, recovery state, fake component status, SaaS outage language, or remediation advice while moving deterministic fallback copy into runtime messages.
+- Operator workflow clarity: thrown backend messages remain authoritative; the runtime fallback only covers missing or blank incident-feed error messages.
+- Context visibility: org fallback handling, component normalization, incident normalization, selected year/range query, and caller-owned public status rendering remain unchanged.
+
+## Public Status Incident Refresh Failure Boundary - 2026-05-09
+
+- Hierarchy: keep year-switch and manual-refresh incident failures in the same public incident slot that already renders initial partial-load errors.
+- Density: preserve the one-line incident error and existing loading/empty states; do not add retry panels, legends, banners, summary rails, or marketing public-status prose.
+- Anti-AI-slop: do not invent component health, uptime, incident recovery progress, SaaS outage language, or remediation advice while sharing the controller-owned fallback.
+- Operator workflow clarity: backend error messages still win; blank refresh errors fall back to the same runtime-message key used by initial incident loading.
+- Context visibility: selected year, reload token, incident query range, current feed, locale-specific labels, and caller-owned public status rendering remain unchanged.
+
+## Public Status Incident Content Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep incident timeline content inside the existing public incident card; missing content state labels should stay in the same compact timeline row.
+- Density: preserve timestamp, state, and message as the only row facts; do not add badges, summaries, retry controls, or incident-health chrome for absent content state.
+- Anti-AI-slop: do not invent incident state, uptime, recovery progress, cause, or remediation text when a timeline content item omits state data.
+- Operator workflow clarity: known content states still use status-center state labels and tones, while absent content state uses the shared runtime empty marker.
+- Context visibility: incident card title, copy, state color, range labels, selected year, component context, and public navigation remain unchanged.
+
+## Public Status Component Uptime Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep component history uptime in the same public component history rows and block tooltips; missing uptime should not create a new health summary.
+- Density: preserve compact component name, description, state, latest time, latest uptime, and history blocks as the only visible facts.
+- Anti-AI-slop: do not invent uptime percentages, health, SLA, availability claims, or remediation copy when history payloads omit uptime data.
+- Operator workflow clarity: real uptime percentages still render as percentages; absent uptime uses the shared runtime empty marker consistently across the latest value and history block title.
+- Context visibility: public brand, component state, history colors, selected year, incident feed, and footer navigation remain unchanged.
+
+## Public Status Component Description Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep component description/endpoint copy inside each existing public component row; missing summary text must not create a secondary detail panel.
+- Density: preserve component title, copy, state, latest time, uptime, and history blocks as the compact scan path for public status readers.
+- Anti-AI-slop: do not invent component descriptions, endpoints, ownership, uptime, SLA, health, or remediation copy when the backend payload omits description and endpoint.
+- Operator workflow clarity: real component descriptions and endpoints remain authoritative; absent copy uses the shared runtime empty marker.
+- Context visibility: public brand, component state, history colors, incident feed, year filter, and footer navigation remain unchanged.
+
+## Exception Center Filter Refresh Boundary - 2026-05-09
+
+- Hierarchy: keep the refresh control as the small icon action in the existing exception filter sidebar header.
+- Density: preserve the compact sidebar title and single icon button; do not add helper text, status chips, retry banners, or a secondary filter toolbar.
+- Anti-AI-slop: do not invent exception freshness, query success, backend health, or remediation state while adding the missing accessible label.
+- Operator workflow clarity: the icon remains a filter-refresh affordance and only its assistive label moves to runtime messages.
+- Context visibility: exception type, query bar, filter groups, table rows, recovery rail, detail hrefs, and existing route wrapper stay unchanged.
+
+## Exception Center Route Type Boundary - 2026-05-09
+
+- Hierarchy: keep unknown exception routes in the existing exception center surface, normalized to the 404 evidence model instead of showing unsupported route types.
+- Density: preserve the single exception explorer surface without adding redirect banners, unknown-route cards, or extra recovery rails.
+- Anti-AI-slop: do not invent exception categories, route health, backend status, or remediation state for unsupported route params.
+- Operator workflow clarity: supported 403/404/500 routes keep their current evidence; unsupported params use the 404 route boundary before rendering.
+- Context visibility: route wrapper, surface markers, query bar, filter groups, table rows, detail hrefs, and recovery handoffs stay unchanged.
+
+## Alert Topology Context View Mode Boundary - 2026-05-09
+
+- Hierarchy: keep topology view mode as the first compact context pill in the existing alert topology context panel.
+- Density: preserve the single-line pill stack without adding mode selectors, route explainers, badges, or side panels.
+- Anti-AI-slop: do not invent topology health, graph confidence, blast radius, relation freshness, or fake dependency evidence while localizing the visible mode label.
+- Operator workflow clarity: machine route context and return links keep the original view-mode slug; only the human-readable pill label moves to runtime topology messages.
+- Context visibility: topology source kind, edge id, environment, time range, selected service/entity, and alert evidence closure remain unchanged.
+
+## Alert Center Selected Trigger Count Boundary - 2026-05-09
+
+- Hierarchy: keep the trigger count inside the existing selected-alert active-window summary row.
+- Density: preserve the compact count plus timestamp text; do not add count badges, helper prose, side panels, or incident workflow state.
+- Anti-AI-slop: do not invent alert volume trends, recurrence quality, incident severity, health, ownership, or backend freshness while localizing the count phrase.
+- Operator workflow clarity: selected alert identity, fingerprint, creator, active window, end time, row order, and closure actions remain unchanged.
+- Context visibility: alert center selected rows, evidence context, mutation feedback, and return-link handoffs remain owned by the existing alert center page.
+
+## Alert Center Refresh Unit Boundary - 2026-05-09
+
+- Hierarchy: keep refresh cadence as inline meta in the existing alert evidence time-context row.
+- Density: preserve the compact inherited time row without adding refresh badges, time pickers, helper prose, or status panels.
+- Anti-AI-slop: do not invent refresh health, polling freshness, backend latency, live state, or time-range validity while localizing the seconds unit.
+- Operator workflow clarity: refresh still reflects only inherited query cadence; time bounds, live/paused state, timezone, source, monitor, trace, closure, and evidence handoff behavior stay unchanged.
+- Context visibility: alert center route wrapper, client island, time context row, topology/time return context, and closure feedback remain owned by the existing alert center page.
+
+## Alert Center Filter And Time Chrome Copy Boundary - 2026-05-09
+
+- Hierarchy: keep search, status filter, group action, and alert time labels inside the existing alert center filter row and grouped alert cards.
+- Density: preserve the compact search/select controls and inline time/trigger summaries; do not add helper banners, extra status rails, or duplicate timestamps.
+- Anti-AI-slop: do not invent alert freshness, incident state, SLA posture, backend health, or polling success while cataloging these labels.
+- Operator workflow clarity: search/filter placeholders, delete action, last/end time labels, and trigger-count tooltip should resolve through runtime/helper catalogs instead of raw `alert.center.*` keys.
+- Context visibility: alert filters, grouped cards, evidence context, topology/time return context, and closure feedback remain unchanged.
+
+## Alert Center Status Chrome Supplemental Boundary - 2026-05-09
+
+- Hierarchy: keep status labels in the existing alert center filter options, entity-context summary, grouped alert cards, and selected evidence rows.
+- Density: preserve the three lifecycle labels only; do not add incident states, status badges, legends, or explanatory status panels.
+- Anti-AI-slop: do not invent incident state, ownership, freshness, automation posture, or remediation confidence while making these labels available at first paint.
+- Operator workflow clarity: firing, acknowledged, and resolved status copy should be available in web-next runtime/helper supplemental catalogs, not only after Angular locale bundle hydration.
+- Context visibility: raw route status tokens, query-state parsing, filter values, grouped-card evidence, and closure feedback stay unchanged.
+
+## Alert Entity Context Status Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep routed alert entity status inside the existing compact entity-context summary strip; do not add a status explanation card or secondary alert health panel.
+- Density: preserve the inline status, severity, and search summary tokens so the alert center header remains quick to scan.
+- Anti-AI-slop: do not invent alert lifecycle state, incident state, health, ownership, or backend freshness for unknown status route tokens.
+- Operator workflow clarity: known alert statuses keep their localized labels, while unknown routed statuses use a localized fallback that still exposes the original token.
+- Context visibility: entity id/name, return link, severity, search, and raw route context remain owned by the existing alert entity context pipeline.
+
+## Alert Topology View Mode Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep view mode as the first compact topology context pill; do not add a route diagnostics panel or topology explanation block.
+- Density: preserve the current one-line pill cluster, edge id, environment, time range, and return action.
+- Anti-AI-slop: do not invent graph health, topology confidence, blast radius, relation freshness, or fake evidence for unknown view-mode slugs.
+- Operator workflow clarity: known view modes keep their existing localized topology labels, while unknown modes use a localized fallback that still exposes the original route token for troubleshooting.
+- Context visibility: data attributes, return links, selected service/entity, source kind, edge id, environment, and time range continue to carry the raw view-mode context.
+
+## Alert Topology Source Kind Fallback Boundary - 2026-05-09
+
+- Hierarchy: keep source kind as the second compact topology context pill beside the view-mode pill; do not add a new alert/topology explanation panel.
+- Density: preserve the existing one-line context row, edge id, environment, and return action without expanding the alert center header.
+- Anti-AI-slop: do not invent relation confidence, graph freshness, topology health, blast radius, or fake dependency evidence for unknown source-kind slugs.
+- Operator workflow clarity: known relation sources keep their localized labels, while unknown slugs use a localized fallback that still exposes the original route token for troubleshooting.
+- Context visibility: data attributes, return links, selected service/entity, edge id, environment, and time range continue to carry the raw source-kind context.
+
+## Overview Investigation Lane Copy Boundary - 2026-05-09
+
+- Hierarchy: keep investigation lane copy inside the existing overview lane cards for entity, logs, traces, and OTLP metrics.
+- Density: preserve the compact lane title, eyebrow, copy, stat, and action rhythm without adding explanatory banners, onboarding panels, or route-choice summaries.
+- Anti-AI-slop: do not invent SaaS-style workspace health, fake ingest readiness, fake trace coverage, fake entity quality, or marketing claims while filling missing runtime keys.
+- Operator workflow clarity: lane copy should explain the next investigation step and keep entity-first triage plus three-signal handoff clear.
+- Context visibility: lane hrefs, top-alert trace decision, entity count, app count, guidance next-link projection, and overview client island ownership remain unchanged.
+
+## Bulletin Action And Empty-State Copy Boundary - 2026-05-09
+
+- Hierarchy: keep bulletin action, editor, empty-selection, and validation copy in the existing toolbar/menu/dialog/view-model slots.
+- Density: preserve the compact action menu, manage dialog, selected-summary rows, and validation message path without adding helper panels or duplicate explanations.
+- Anti-AI-slop: do not invent bulletin health, metric safety, deletion recovery, workspace readiness, or SaaS-style dashboard promises while filling missing runtime keys.
+- Operator workflow clarity: batch delete, new/edit dialog labels, no-selection guidance, and invalid JSON validation should read as concrete workbench actions, not raw translation keys.
+- Context visibility: active selection, metrics table, refresh state, manage dialog ownership, current delete confirmation, and batch checkbox behavior remain unchanged.
+
+## Settings Console Content Label Boundary - 2026-05-09
+
+- Hierarchy: keep the right-side settings content pane as the single destination region for the selected settings route.
+- Density: add only an accessible content-region label; do not add visible headings, cards, breadcrumbs, or extra route chrome.
+- Anti-AI-slop: do not invent system health, policy readiness, token posture, or governance summaries while labeling the existing content pane.
+- Operator workflow clarity: the label should identify the implemented settings content area while each child route keeps owning its form, table, and mutation workflow.
+- Context visibility: active navigation state, compact header, left menu, route content, and row actions remain unchanged while the content label moves into runtime messages.
+
+## Settings Console Navigation Label Boundary - 2026-05-09
+
+- Hierarchy: keep the existing left settings menu as the only settings-console navigation surface.
+- Density: add only an accessible navigation label; do not add visible headings, secondary rails, badges, or menu grouping.
+- Anti-AI-slop: do not invent new settings sections, governance readiness, route health, or SaaS-style admin domains while labeling the existing navigation.
+- Operator workflow clarity: the navigation label should describe the implemented system/message/object-store/token settings menu, with no change to route targets or active-route state.
+- Context visibility: active route, menu labels, compact header, content pane, and row actions remain unchanged while the navigation label moves into runtime messages.
+
+## Settings Console Header Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the compact settings console kicker, title, and supporting copy above the existing left navigation/content grid.
+- Density: preserve the current single compact header without adding a summary rail, landing hero, or secondary admin explainer.
+- Anti-AI-slop: do not invent governance readiness, token scope health, SaaS-style control-plane claims, or extra settings domains while localizing the header chrome.
+- Operator workflow clarity: the header should orient operators to system, message, object-storage, and token settings, while each actual route remains the source of its form workflow.
+- Context visibility: active route highlighting, settings menu labels, row actions, and content pane ownership remain unchanged while header copy moves into runtime messages.
+
+## Settings Console Open Action Boundary - 2026-05-09
+
+- Hierarchy: keep the open action as the small text link inside each settings console row.
+- Density: preserve the existing four-row settings layout and compact row action without adding a toolbar, action column, or secondary navigation block.
+- Anti-AI-slop: do not invent settings readiness, route health, permissions, or workspace policy while localizing the shared open label.
+- Operator workflow clarity: each settings row still opens its implemented route; only the reusable action label moves into runtime messages.
+- Context visibility: active route highlighting, row titles, row copy, route meta, and settings console wrapper ownership remain unchanged.
+
+## Settings Console Open Action Context Boundary - 2026-05-09
+
+- Hierarchy: keep the visible row action as the same small translated open link, with row-specific context only in the accessible name.
+- Density: preserve the compact four-row menu and avoid adding visible helper text, extra route captions, or action buttons.
+- Anti-AI-slop: do not invent settings readiness, permissions, route health, or workflow status while improving the repeated action label.
+- Operator workflow clarity: screen-reader users should hear which concrete settings route the repeated open action targets.
+- Context visibility: row title, route href, visible open label, active-route ownership, and settings shell layout remain unchanged.
+
+## Settings Surface Overview Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the settings overview title and copy in the existing first `SurfaceSection`.
+- Density: preserve the current two-section settings console rhythm without adding a summary rail, dashboard card, or governance explainer.
+- Anti-AI-slop: do not invent settings health, workspace policy, setup progress, tenant posture, or future governance app status while moving section copy into runtime messages.
+- Operator workflow clarity: the settings landing surface should continue to expose only implemented configuration entry points and roadmap-only governance notes.
+- Context visibility: caller-provided page title/subtitle, facts, settings rows, governance rows, and route handoffs remain unchanged.
+
+## Entity New Route Boundary - 2026-05-09
+
+- Hierarchy: keep entity creation loading feedback in the existing `ClientWorkbench` route island before the shared editor surface appears.
+- Density: add only route-owned loading-copy parity; do not add a setup banner, catalog summary rail, skeleton wizard, or extra authoring chrome.
+- Anti-AI-slop: do not invent entity readiness, discovery confidence, owner coverage, health, or telemetry status while fixing loading-copy ownership.
+- Operator workflow clarity: the create route still loads the draft plus catalog suggestions, preserving manual and telemetry handoff paths.
+- Context visibility: source, monitor id, cache key, catalog suggestions, editor mode, and shared entity editor surface behavior remain unchanged.
+
+## Settings Governance Future Meta Boundary - 2026-05-09
+
+- Hierarchy: keep the future-governance marker as the compact meta line on the existing roadmap-boundary row.
+- Density: localize the short meta text only; do not add a warning banner, badge stack, explanatory paragraph, or future-domain navigation.
+- Anti-AI-slop: do not invent readiness, release dates, tenant policy, security posture, or SaaS-style roadmap packaging while fixing the zh-CN label.
+- Operator workflow clarity: the row must still say future domains are planning-only and must not look like implemented app entries.
+- Context visibility: current routes, forbidden future routes, roadmap docs, governance section title/copy, and route handoff behavior remain unchanged.
+
+## Settings Governance Future Domain Boundary - 2026-05-09
+
+- Hierarchy: keep future domains as inline text inside the existing roadmap-boundary row copy.
+- Density: localize only the domain names in zh-CN; do not split them into chips, cards, tabs, or a future-domain navigation list.
+- Anti-AI-slop: do not invent future app pages, readiness state, vendor benchmarks, launch dates, or commercial governance taxonomy while translating planning labels.
+- Operator workflow clarity: translated domain names must still be framed as roadmap-only planning and must not imply implemented settings modules.
+- Context visibility: current route rows, roadmap doc paths, forbidden future routes, section chrome, and governance review contracts remain unchanged.
+
+## Passport Login Loading/Error Copy Boundary - 2026-05-09
+
+- Hierarchy: keep loading and error feedback inside the existing credential card button and `StatusState` slot.
+- Density: preserve the compact passport card, two fields, remember checkbox, notice slot, and single submit action without adding banners, policy panels, or auth health summaries.
+- Anti-AI-slop: do not invent MFA state, account posture, workspace policy, token health, or security guarantees while moving loading and failure copy into runtime messages.
+- Operator workflow clarity: failed authentication should keep the operator on the same login surface with backend error text when present and localized fallback text when not.
+- Context visibility: redirect target ownership, session bootstrap, token persistence, password visibility, remember checkbox, and passport shell layout remain unchanged.
+
+## Log Integration Source Alias Boundary - 2026-05-09
+
+- Hierarchy: keep legacy log integration/source URLs as thin redirects into the existing OTLP intake entry.
+- Density: preserve a redirect-only compatibility route; do not add a source picker, intake summary, legacy panel, or intermediate route shell.
+- Anti-AI-slop: do not invent source-specific setup state, log filters, collector status, validation results, or fake ingestion health from the URL slug.
+- Operator workflow clarity: source aliases carry only shared signal context into `/ingestion/otlp?signal=logs`; discarded log search/content filters stay discarded.
+- Context visibility: time, entity, monitor, return, service, environment, trace/span, source, collector, and template route context ownership remains with `signal-route-context`.
+
+## Log Integration Redirect Shell Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the legacy log integration placeholder as a thin redirect shell with one header, one placeholder panel, and two CTAs; do not add a setup wizard or intake dashboard.
+- Density: preserve the compact compatibility route surface without source cards, status chips, validation rails, or extra explanatory rows.
+- Anti-AI-slop: do not infer ingestion health, collector readiness, source validation, log freshness, or OTLP success from localized redirect copy.
+- Operator workflow clarity: the redirect shell CTAs should resolve through the same zh-CN helper/legacy catalog as production while still pointing operators to unified intake and the logs workbench.
+- Context visibility: auto-navigation to the ingestion route, manage-route fallback, placeholder shell markers, and source alias ownership remain unchanged.
+
+## Settings Governance Roadmap Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the governance section as a compact boundary list under the settings surface; do not add a roadmap panel, badge rail, or future-domain navigation.
+- Density: preserve the existing row list and short section copy while making zh-CN roadmap wording fully localized for quick scanning.
+- Anti-AI-slop: do not invent future app availability, governance readiness, SaaS packaging, or competitor-style product claims while clarifying copy ownership.
+- Operator workflow clarity: current routes remain the only actionable settings entries; future domains stay document-only planning with localized wording.
+- Context visibility: implemented route groups, future-domain labels, roadmap doc links, and the no-`/security` navigation guard remain unchanged.
+
+## Setting Config Route Loading Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the settings console title, locale/timezone/theme form, save feedback, and action row unchanged; only the route loading copy is asserted through shared runtime/helper ownership.
+- Density: preserve the single compact client-workbench loading boundary without adding summary rails, helper banners, deployment health panels, or nested cards.
+- Anti-AI-slop: do not invent system health, configuration drift, tenant posture, rollout status, or fake validation progress while wiring loading-copy parity.
+- Operator workflow clarity: the route still loads `/config/system` and `/config/timezones`, then lets operators edit locale, timezone, and theme through the existing form.
+- Context visibility: server wrapper, client island, cache key, save invalidation, locale/theme side effects, and settings-console context remain unchanged.
+
+## Overview Route Loading Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the overview shell, rail, status grid, issue focus, guidance, and signal handoff surfaces unchanged; only route loading copy ownership moves into shared runtime messages.
+- Density: preserve the compact first-screen loading boundary without adding banners, helper panels, duplicated recap cards, or synthetic setup status.
+- Anti-AI-slop: do not invent overview readiness, fake health, fake signal availability, fake alert status, or SaaS-style onboarding copy while externalizing the loading label.
+- Operator workflow clarity: the overview route still loads summary plus recent alerts through the existing client island and settled cache; the loading text simply names the current console while data resolves.
+- Context visibility: refresh invalidation, cache key, overview route wrapper, client workbench boundary, and cross-signal handoff context remain unchanged.
+
+## Topology Route Context Query Owner Boundary - 2026-05-09
+
+- Hierarchy: keep topology as the existing entity relationship surface; only move route-context parsing out of the visible client island.
+- Density: preserve the header, source strip, controls, canvas, edge evidence panel, and side rail without adding query summaries or debug chips.
+- Anti-AI-slop: do not invent dependency discovery, root-cause analysis, blast radius, edge freshness, or live topology guarantees while changing the URL parsing owner.
+- Operator workflow clarity: incoming entity, service, time, source, edge, view-mode, and signal context still focus the graph and drilldown links, but raw URL normalization belongs to a typed route contract.
+- Context visibility: sourceKind, viewMode, edgeId, entity, service, environment, returnTo, trace, collector, and template params stay available to the topology view model.
+
+## Actions Suggestion Query Owner Boundary - 2026-05-09
+
+- Hierarchy: keep the safe-automation entry surface visually unchanged; only the URL context normalization owner moves out of the client island.
+- Density: preserve the cold entry header, checklist, adapter-boundary panel, and suggested-action cards without adding query debug rows or new context badges.
+- Anti-AI-slop: do not invent live execution state, adapter readiness, automation history, or auto-run capability while moving route params into a typed query contract.
+- Operator workflow clarity: alert/entity/topology handoffs should still produce human-confirmed suggestions with evidence links, but the server route should own raw URL parsing before the client renders.
+- Context visibility: entity, service, time, returnTo, source, signal, alert group, topology edge, collector, and template params stay available as typed suggestion context.
+
+## Root Metadata Product IA Boundary - 2026-05-09
+
+- Hierarchy: keep root metadata as a quiet browser/document identity layer; it should name the release workbench without adding page chrome or changing route ownership.
+- Density: keep the title and description short enough for tabs, bookmarks, and snapshots; do not add marketing claims, SaaS packaging, or migration prose.
+- Anti-AI-slop: remove pilot/Next.js implementation framing from the product surface and do not invent readiness guarantees, health posture, or cloud positioning.
+- Operator workflow clarity: metadata should reinforce HertzBeat as the private-deployable observability workbench operators are actually using across monitors, OTLP signals, alerts, topology, and safe automation.
+- Context visibility: root layout, AppFrame provider ownership, icons, default locale, and route compatibility redirects remain unchanged.
+
+## Monitor Edit Return Context Boundary - 2026-05-09
+
+- Hierarchy: keep the edit-monitor flow in the same editor shell as create; inherited list/entity/time context should be resolved by the route wrapper before the client island edits the monitor payload.
+- Density: preserve the compact edit facts, parameter sections, save/detect/cancel controls, and existing return behavior without adding a second context panel.
+- Anti-AI-slop: do not invent monitor health, save readiness, collector state, entity ownership, or validation progress while moving return-context parsing out of the client island.
+- Operator workflow clarity: editing from a filtered monitor list, entity handoff, or time-scoped investigation must keep cancel/save return navigation consistent through one typed route-state contract.
+- Context visibility: labels, pagination, entity id/name, time window, refresh, timezone, and safe internal return target remain available to the navigation helper without `useSearchParams` in the visible island.
+
+## Monitor New Setup Query Boundary - 2026-05-09
+
+- Hierarchy: keep the new-monitor setup flow in the existing editor shell; query app selection and return context should be resolved before the client island renders the form.
+- Density: preserve the compact editor facts, base fields, parameter sections, and save/detect/cancel actions without adding a handoff banner or duplicate route summary.
+- Anti-AI-slop: do not invent monitor health, collector readiness, parameter validation state, entity ownership, or setup progress while moving URL parsing out of the visible island.
+- Operator workflow clarity: monitor type, entity return, list filters, and preserved time context should arrive as typed route state so create/cancel/save use the same navigation contract.
+- Context visibility: app, labels, pagination, entity id/name, time window, refresh, timezone, and safe internal return target stay visible to navigation helpers instead of being re-read from `useSearchParams`.
+
+## Trace Manage Query Owner Boundary - 2026-05-09
+
+- Hierarchy: keep the trace workbench header, time rail, query row, trend band, dense trace table, selected evidence panel, attribution diagnostics, waterfall drawer, entity context, and cross-signal handoffs exactly where they are; only raw route parsing moves to the trace query owner.
+- Density: do not add URL-state chips, route banners, query explanation rails, copied trace-explorer controls, or fake trace-health panels while passing typed route state into the client island.
+- Anti-AI-slop: do not invent trace availability, service topology, sampling posture, error causality, entity readiness, collector state, backend health, or incident linkage from URL params.
+- Operator workflow clarity: traceId, spanId, service, error-only filters, return links, absolute time cleanup, run/reset/time-control actions, detail drawer selection, and log/metric/entity/alert handoffs must behave the same with server-owned route state.
+- Context visibility: inherited entity, service namespace, environment, source, returnTo, time bounds, refresh/live/tz, and code navigation hints remain visible through existing context rows, diagnostics, and handoff URLs.
+
+## Ops Surface Route Posture Label Boundary - 2026-05-09
+
+- Hierarchy: keep the safe-automation/ops route posture label inside the existing right rail status block, without adding a new readiness panel or explanatory banner.
+- Density: preserve the compact rail card with mode label, entry-shell title, focus copy, and overview handoff; only the label owner changes.
+- Anti-AI-slop: do not infer execution readiness, adapter health, approval state, queue freshness, or automation success from the route-posture label.
+- Operator workflow clarity: the route posture label should describe the current entry-shell mode through ops runtime copy instead of a generic shared `common.mode` fallback.
+- Context visibility: title, subtitle, tags, focus, checklist, status rows, and overview navigation stay visible and unchanged.
+
+## Ops Surface Overview Handoff Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the overview handoff as the single compact button inside the route-posture rail, without adding a second navigation row or explanatory banner.
+- Density: preserve the same right-rail card rhythm; only the handoff copy key moves from an English-only alias to the shared localized navigation key.
+- Anti-AI-slop: do not infer overview readiness, automation execution status, approval posture, or evidence freshness from the navigation label.
+- Operator workflow clarity: the handoff should read as the same overview action used by the rest of the workbench, not a route-local English-only special case.
+- Context visibility: title, subtitle, tags, focus, checklist, route posture mode, and overview href stay unchanged.
+
+## Ops Surface Entity Handoff Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the default header entity handoff beside the overview action, without adding a new entry strip, entity banner, or duplicated navigation row.
+- Density: preserve the existing two-action header fallback; only the entity action copy key changes to the shared localized entity-center label.
+- Anti-AI-slop: do not imply entity readiness, catalog completeness, topology coverage, or monitor binding state from this navigation label.
+- Operator workflow clarity: the entity handoff should match the shared entity-center navigation vocabulary used by actions and shell navigation, not a route-local browse key.
+- Context visibility: overview action, entity href, title, subtitle, facts, route posture rail, and checklist stay unchanged.
+
+## Ops Surface Chrome Copy Catalog Boundary - 2026-05-09
+
+- Hierarchy: keep the operator-surface kicker, route-contract panel, launch checklist, route-posture rail title, and entry-shell title in their current slots.
+- Density: preserve the compact shell and rail text rhythm; only runtime/helper catalog ownership changes.
+- Anti-AI-slop: do not add readiness claims, adapter status, fake route checks, or inferred automation health while cataloging chrome copy.
+- Operator workflow clarity: shared ops-surface chrome should render localized operator vocabulary instead of raw `ops.surface.*` keys when a route uses the default shell.
+- Context visibility: facts, status rows, header actions, route posture mode, overview/entity handoffs, and checklist rows remain unchanged.
+
+## Ops Surface Page Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the ops-surface page regression on the existing shell, rail, status rows, and default overview/entity handoffs; do not add a helper-only route state panel.
+- Density: preserve the compact mocked workbench shell and three focused assertions without adding broad snapshot checks.
+- Anti-AI-slop: do not invent route readiness, adapter health, automation execution, topology coverage, or entity catalog status while moving the test onto shared messages.
+- Operator workflow clarity: the page regression should exercise the same shared translator helper as runtime surfaces instead of carrying a private `ops.surface.*` and `menu.*` map.
+- Context visibility: cold-workbench route tokens, primitive ownership, default handoff labels, and operator-shell source checks remain unchanged.
+
+## AppFrame AI Prompt Chrome Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the compact AI prompt field inside the existing AppFrame header, between setup progress and utility actions.
+- Density: preserve the single-line input plus icon-only submit button; only runtime/helper catalog ownership changes.
+- Anti-AI-slop: do not imply AI RCA availability, automation execution, agent connectivity, or guaranteed remediation from the prompt placeholder.
+- Operator workflow clarity: the placeholder and submit label should resolve through shared runtime catalogs instead of raw `ai.chat.*` keys.
+- Context visibility: setup state, alert notification controls, mute/lock/settings/user actions, and main content shell remain unchanged.
+
+## Monitor Editor Locale Copy Boundary - 2026-05-09
+
+- Hierarchy: keep monitor editor label/annotation controls and validation feedback inside the existing editor form, without adding helper panels, banners, or duplicate validation summaries.
+- Density: preserve the compact key/value editors, base settings grid, detect/save/cancel row, and single-line action feedback; only locale-owned copy changes.
+- Anti-AI-slop: do not invent monitor health, collector readiness, validation progress, save guarantees, entity ownership, or setup wizard state while localizing helper and validation text.
+- Operator workflow clarity: add/remove label and annotation actions plus app/name/interval/cron/required-field validation must read as concrete monitor-authoring controls, not raw English fallback strings in zh-CN.
+- Context visibility: monitor app, scrape mode, schedule type, params, labels, annotations, return context, and save/detect behavior stay unchanged while display copy moves to the runtime/helper catalogs.
+
+## Monitor New Page Locale Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the new-monitor route inside the existing monitor editor surface with the same shell kicker, title, facts, main form, side payload, detect/save/cancel actions, and return context; only align the page regression with a zh-CN helper.
+- Density: preserve the compact editor grid, key/value rows, action row, and client-workbench cache markers without adding setup summaries, health panels, or extra validation rails.
+- Anti-AI-slop: do not invent monitor readiness, collector probes, entity ownership, template recommendations, fake save state, or hosted onboarding copy while replacing English assertions.
+- Operator workflow clarity: the new route should assert localized authoring chrome such as the new-monitor title and save action while the draft loader and handoff app context remain unchanged.
+- Context visibility: app/template context, cache key, settled TTL, return context, param loading path, and monitor-editor client island ownership remain visible and unchanged.
+
+## Monitor Edit Page Locale Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the edit-monitor route inside the existing monitor editor surface with the existing monitor name as the title, unchanged facts, main form, side payload, detect/save/cancel actions, and return context; only align the page regression with a zh-CN helper.
+- Density: preserve the compact editor grid, key/value rows, action row, and client-workbench cache markers without adding setup summaries, migration warnings, health panels, or extra validation rails.
+- Anti-AI-slop: do not invent monitor health, collector readiness, save guarantees, ownership hints, template recommendations, or hosted onboarding copy while replacing English action assertions.
+- Operator workflow clarity: the edit route should assert localized authoring chrome such as the save action while the real monitor name, draft loader, and return-context handoff remain unchanged.
+- Context visibility: monitor id, app/template context, cache key, settled TTL, return context, collector/detail load path, and monitor-editor client island ownership remain visible and unchanged.
+
+## Alert Integration Canonical Source Boundary - 2026-05-09
+
+- Hierarchy: keep the alert integration source page on the existing source rail and markdown document shell, but canonicalize unknown URL slugs before rendering fallback content.
+- Density: preserve the compact source rail, token action, and document panel; do not add unknown-source banners, empty shells, selector summaries, or fake validation cards.
+- Anti-AI-slop: do not invent provider setup state, source health, collector status, validation results, or provider-specific evidence for unknown source params.
+- Operator workflow clarity: known sources render their existing document workflow; unknown source params redirect to the canonical default source route instead of showing default Webhook under a misleading URL.
+- Context visibility: selected source id/name, rail selection, token management handoff, document loading, and fallback guide behavior remain owned by the alert integration source model.
+
+## Passport Lock Fact Copy Boundary - 2026-05-09
+
+- Hierarchy: keep lock-page facts in the small passport lock view-model payload; do not add a visible status banner, account detail panel, or unlock workflow summary.
+- Density: preserve the existing compact unlock card, password field, submit action, and validation error slot.
+- Anti-AI-slop: do not invent session health, account identity, token state, MFA posture, workspace policy, or unlock guarantees while moving fact values into runtime messages.
+- Operator workflow clarity: the unlock form still validates only a non-empty password and returns to overview; only fact copy ownership moves out of source literals.
+- Context visibility: route wrapper, client island, passport shell, unlock marker, validation copy, and shared i18n provider remain unchanged.
+
+## Passport Lock Form Action Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the unlock title, password placeholder, and submit label inside the existing compact lock card; do not add a secondary auth panel, session summary, or security banner.
+- Density: preserve the wide passport panel, centered avatar, single password input, right-aligned action, and inline validation state.
+- Anti-AI-slop: do not invent MFA, account identity, token status, workspace policy, or unlock guarantees while cataloging the form action copy.
+- Operator workflow clarity: lock title/action and placeholder should resolve through shared runtime/helper catalogs before the Angular locale bundle fallback is needed.
+- Context visibility: passport shell, language switch, product intro, route wrapper, client island, and return-to-overview behavior remain unchanged.
+
+## Passport Lock Page Locale Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the same passport shell, wide dark unlock panel, avatar, single password field, submit button, and inline failure slot; only align the page regression with its declared zh-CN provider locale.
+- Density: preserve the compact lock-card rhythm without adding account details, security banners, MFA prompts, or policy copy.
+- Anti-AI-slop: do not invent session risk, token health, workspace policy, tenant state, unlock guarantees, or hosted-service posture while replacing English test assertions.
+- Operator workflow clarity: when the mocked provider declares `locale: 'zh-CN'`, the lock page regression should assert the same zh-CN unlock title/action and placeholder operators see.
+- Context visibility: route wrapper, client island, passport shell markers, background asset, overview redirect, validation path, and language controls remain unchanged.
+
+## Passport Lock Failure Title Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep failed-unlock feedback in the existing inline status state under the password form; do not add a banner, modal, retry wizard, or account policy panel.
+- Density: preserve one password input, one submit action, and one compact failure title/copy block.
+- Anti-AI-slop: do not invent account lockout, MFA, session risk, token posture, workspace policy, or security guarantees while cataloging the failure title.
+- Operator workflow clarity: the failed validation title should resolve through shared runtime/helper catalogs while the validation copy stays owned by the lock view-model.
+- Context visibility: passport shell, unlock form, route wrapper, client island, validation path, and overview redirect remain unchanged.
+
+## Passport Shell About Points Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the six product-truth bullets in the existing passport hero list; do not add a marketing hero, secondary proof panel, or SaaS positioning layer.
+- Density: preserve the single-column desktop bullet list, separators, brand lockup, auth panel, and footer spacing.
+- Anti-AI-slop: do not invent uptime, AI RCA readiness, commercial packaging, hosted-service claims, or copied competitor positioning while cataloging helper copy.
+- Operator workflow clarity: the passport hero should describe HertzBeat's collectors, templates, entities, telemetry, alerts, notification, and private deployment scope through shared message catalogs.
+- Context visibility: language switch, login panel, brand asset, background treatment, and localized footer remain unchanged.
+
+## Passport Shell Product Intro Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the passport description and two-line hero headline in the current brand lockup and desktop intro area; do not add a new marketing hero or proof panel.
+- Density: preserve the compact logo, one-line description, two-line headline, separators, bullet list, auth panel, and footer rhythm.
+- Anti-AI-slop: do not invent hosted SaaS claims, commercial packaging, uptime promises, AI readiness, or copied competitor language while cataloging the helper copy.
+- Operator workflow clarity: the intro should state HertzBeat's open-source private-deployable operations observability scope and its collectors/templates/entities/telemetry/alert closure coverage through shared catalogs.
+- Context visibility: language switch, login panel, product bullets, background treatment, brand asset, and localized footer remain unchanged.
+
+## Passport Shell Component Locale Helper Boundary - 2026-05-09
+
+- Hierarchy: keep `PassportShell` as the shared auth shell with brand lockup, desktop intro, product bullets, language control, panel, and footer; only align the component regression with the declared zh-CN locale.
+- Density: preserve the current compact auth composition, single-column intro list, separators, lowered logo, and footer spacing without adding proof cards, marketing rails, or account-state panels.
+- Anti-AI-slop: do not invent login readiness, tenant posture, SaaS hosting, uptime, AI assistance, or security guarantees while replacing the default English test translator.
+- Operator workflow clarity: when the mocked provider declares `locale: 'zh-CN'`, the shell regression should assert the same zh-CN intro, about points, language switch, and footer copy that production operators see.
+- Context visibility: background asset, locale dropdown wiring, auth panel structure, product intro, about bullets, and Apache license footer remain unchanged.
+
+## Passport Login Credential Action Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the credential tab, missing-field feedback, and submit action inside the existing passport login card; do not add a new security banner, MFA panel, or auth-summary rail.
+- Density: preserve the single credential panel, username/password fields, remember-me row, submit button, and inline validation slots.
+- Anti-AI-slop: do not invent session health, token posture, workspace policy, SaaS security claims, or hosted login flows while cataloging helper copy.
+- Operator workflow clarity: credential tab, submit label, and missing account/password feedback should resolve through shared runtime/helper catalogs so login regressions cannot hide raw keys.
+- Context visibility: redirect target, session notice, password visibility controls, shell intro, language switch, footer, and submit flow remain unchanged.
+
+## Settings Surface Fact Label Boundary - 2026-05-09
+
+- Hierarchy: keep settings facts in the existing `SettingsSurfacePage` header fact row; do not add a governance card, helper banner, or secondary status panel.
+- Density: preserve the compact workspace, mode, and focus facts already passed into the shared workbench page.
+- Anti-AI-slop: do not invent settings health, setup progress, governance status, tenancy, or permission state while localizing the fact labels.
+- Operator workflow clarity: settings entries still receive the caller-provided title and next-step focus; only mode/focus label/value ownership moves into settings runtime keys.
+- Context visibility: settings route wrappers, surface rows, governance rows, and current route handoffs stay unchanged.
+
+## Settings Surface Row Meta Boundary - 2026-05-09
+
+- Hierarchy: keep the two settings overview rows inside the existing flat surface section; the row meta should name the route/API contract role without adding a status badge or extra summary rail.
+- Density: replace the generic `stable` meta with short runtime-owned labels so the row remains scannable in the compact list.
+- Anti-AI-slop: do not invent readiness scores, configuration health, tenant posture, or future settings domains while clarifying the row meta copy.
+- Operator workflow clarity: route navigation and operations configuration remain the two existing rows; only their meta labels become specific to the operator contract they describe.
+- Context visibility: settings surface title/copy, governance rows, route handoffs, and caller-provided next-step focus stay unchanged.
+
+## Ops Surface Fact Copy Boundary - 2026-05-09
+
+- Hierarchy: keep the shared safe-automation/ops entry facts and status rows in the existing compact entry surface.
+- Density: preserve workspace, focus, mode, signal count, and the two status rows without adding setup panels, workflow summaries, or fake run-state badges.
+- Anti-AI-slop: do not invent automation readiness, execution health, approvals, queue depth, or signal health while localizing fact labels and row titles.
+- Operator workflow clarity: caller-provided focus and tag count remain authoritative; only shared ops surface label/title/mode copy moves into runtime messages.
+- Context visibility: route wrapper, client island, safe automation entry payload, and existing row copy remain unchanged.
+
+## Safe Automation Entry Chrome Helper Parity Boundary - 2026-05-09
+
+- Hierarchy: keep the safe automation kicker, title, subtitle, overview/entity handoffs, and shell explainer in the existing cold entry header and left panel; do not add a new automation summary rail.
+- Density: preserve the compact header/action row, cold shell panel, roadmap panel, checklist rail, and empty adapter state.
+- Anti-AI-slop: do not invent executable automation state, approval counts, queue health, AI action readiness, or future adapter availability while cataloging entry chrome copy.
+- Operator workflow clarity: the entry header and first two navigation actions should resolve through shared runtime/helper catalogs rather than page-local test maps.
+- Context visibility: route wrapper, client island, suggestion context, adapter-boundary copy, suggested-action cards, and empty adapter state remain unchanged.
+
+## AppSidebar Component Helper Boundary - 2026-05-09
+
+- Hierarchy: keep `AppSidebar` as the bounded, sectioned navigation component backed by shared sidebar sections; this slice only aligns its component regression with runtime/helper labels.
+- Density: preserve compact section headers, flat rows, icon sizing, and bounded scroll without adding cards, footer panels, operator badges, or extra route descriptions.
+- Anti-AI-slop: do not invent new navigation groups, fake status/public/action routes, onboarding copy, AI routes, or SaaS-style product hierarchy while replacing placeholder labels.
+- Operator workflow clarity: component-level sidebar tests should assert the same zh-CN labels operators see for ingestion, objects, observability, alerting, dashboards, settings, MCP server, and help center.
+- Context visibility: active route state, hidden incident/action/status-public entries, help/MCP links, flat row styling, and scroll ownership remain unchanged.
+
+## AppFrame Utility Chrome Helper Boundary - 2026-05-09
+
+- Hierarchy: keep `AppFrame` utility controls as the existing icon-only header cluster with screen-reader labels and the compact AI prompt.
+- Density: preserve the current trigger count, footer, setup card, and prompt footprint without adding helper text, command palettes, onboarding prose, or secondary nav.
+- Anti-AI-slop: do not imply AI execution readiness, notification health, GitHub sync, account state, or setup completion while moving the regression to shared helper labels.
+- Operator workflow clarity: visible and assistive shell labels should resolve through the shared helper/runtime catalogs so language switching, utility actions, and AI prompt chrome cannot drift into private test strings.
+- Context visibility: setup progress, footer license, help launcher, notification mute state, lock/settings/user controls, and child content rendering remain unchanged.
+
+## Locale Option List Helper Boundary - 2026-05-09
+
+- Hierarchy: keep locale switching as a compact shell utility list; this slice only aligns the regression labels with runtime/helper catalogs.
+- Density: preserve the short option rows with flag abbreviation, localized label, and active marker without adding helper prose, banners, or a settings preview.
+- Anti-AI-slop: do not invent locale availability, translation completeness, browser-language detection, or SaaS-style account preferences while removing the private test map.
+- Operator workflow clarity: the visible language labels should resolve through the same shared helper as production so shell language switching does not drift from settings locale copy.
+- Context visibility: locale code ownership, `onSelect`, active-row rendering, class hooks, and the existing active check marker remain unchanged.
+
+## Safe Automation Page Regression Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the safe automation page regression focused on the same cold entry shell, adapter boundary, checklist rail, and human-confirmed suggestion cards.
+- Density: preserve compact assertions for entry copy, roadmap chips, localized suggestion meta, and evidence links without adding new page scaffolding.
+- Anti-AI-slop: do not invent live automation runs, execution readiness, adapter health, approval counts, or AI action availability while moving the page test to the shared helper.
+- Operator workflow clarity: the page regression should use the same shared translator helper as runtime-facing tests so suggestion and adapter-boundary labels do not drift in a private map.
+- Context visibility: route wrapper, client island, query-state suggestion context, sanitized return URL, and manual-confirmation guardrails stay unchanged.
+
+## Safe Automation Domain Model Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the domain model regression focused on catalog entries, recent runs, approvals, adapter boundary, checklist, and next hops without adding a live execution dashboard.
+- Density: preserve compact domain assertions for labels, owners, posture, run duration, approval evidence, metrics, and roadmap-only adapter labels.
+- Anti-AI-slop: do not invent execution adapter health, queue depth, approval SLA, AI action confidence, live run status, or automation readiness while moving domain copy to the shared helper.
+- Operator workflow clarity: catalog, run, approval, and manual-confirmation copy should resolve through the shared helper so tests mirror runtime text ownership instead of a model-local map.
+- Context visibility: alert/entity suggestion context, evidence URL preservation, adapter-pending boundary, roadmap-demo snapshot state, and no-auto-execute guardrails stay unchanged.
+
+## Safe Automation View Model Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the surface view-model regression on placeholder shell, catalog cards, run rows, approval rows, status tones, handoff rows, and next hops without adding a second automation console.
+- Density: preserve compact row/card assertions for snapshot labels, risk tones, run/approval status tones, handoff copy, and ops fact labels.
+- Anti-AI-slop: do not invent live run progress, execution health, approval SLA, queue depth, AI confidence, or adapter availability while moving view-model copy to the shared helper.
+- Operator workflow clarity: surface chrome, handoffs, status tones, and snapshot markers should resolve through shared helper messages so the page and model regressions cannot drift.
+- Context visibility: alert/entity suggestion context, manual-confirmation posture, evidence URLs, adapter-pending boundary, and roadmap-demo snapshot state remain unchanged.
+
+## Incident Entry Page Regression Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the incident page regression on the existing cold entry header, shell panel, checklist rail, and adapter-waiting empty state.
+- Density: preserve compact assertions for entry copy, chips, checklist rows, and empty adapter copy without adding incident tables, metrics, or a response desk shell.
+- Anti-AI-slop: do not invent live incident counts, escalation state, RCA readiness, ownership schedules, or adapter availability while moving the page test to the shared helper.
+- Operator workflow clarity: the page regression should use the shared translator helper so entry chips, checklist rows, and empty adapter labels do not drift in a private map.
+- Context visibility: route wrapper, client island, overview/entity handoffs, placeholder guardrails, and no-fake-incident assertions remain unchanged.
+
+## Incident View Model Helper Boundary - 2026-05-09
+
+- Hierarchy: keep the incident view-model regression focused on entry shell, incident cards, timeline rows, ownership rows, checklist, handoff rows, and compact ops facts.
+- Density: preserve card/row assertions for severity, stage, blast radius, timeline meta, owner queues, and handoff destinations without adding an incident desk redesign.
+- Anti-AI-slop: do not invent live incident totals, escalation timers, RCA state, on-call schedule health, fake ownership, or adapter readiness while moving view-model copy to the shared helper.
+- Operator workflow clarity: surface chrome, row labels, ownership copy, and handoff labels should resolve through shared helper messages so page, model, and view-model regressions cannot drift.
+- Context visibility: route wrapper, client island, overview/entity actions, log/trace/overview handoffs, and no-fake-incident guardrails stay unchanged.
+
+## ColdConfirm Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep `ColdConfirmDialog` as the shared destructive/confirmation shell; changing fallback locale order must not alter caller-provided titles, body copy, action labels, or footer ownership.
+- Density: preserve the compact warning icon, kicker, body, and two-action footer so alert and setting dialogs remain quick to scan.
+- Anti-AI-slop: do not add risk prose, extra confirmation steps, fake safety scores, SaaS-style destructive-action packaging, or new workflow hints while correcting the fallback catalog order.
+- Operator workflow clarity: shared chrome copy resolves from the runtime catalog with a deterministic English fallback when no caller locale is available; domain-specific labels still come from each caller.
+- Context visibility: pending-state disablement, `onCancel`/`onConfirm`, `OverlayDialog` close behavior, data markers, and destructive confirmation context remain unchanged.
+
+## TagInput Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep `TagInput` as the shared compact chip editor; changing fallback locale order must not add helper text, chip metadata, or a separate suggestion panel.
+- Density: preserve the chip row, tiny remove button, hidden input, and anchored suggestion popover so labels and filters remain quick to scan in dense admin forms.
+- Anti-AI-slop: do not invent tag taxonomy, validation prose, search guidance, fake suggestions, or copied SaaS label-management language while correcting the fallback catalog order.
+- Operator workflow clarity: shared placeholder/remove copy resolves from runtime messages with an English fallback when no caller locale is available; caller placeholders and suggestion lists remain authoritative.
+- Context visibility: hidden field ownership, draft input behavior, suggestion filtering, popover positioning, chip removal, and data markers stay unchanged.
+
+## NumberStepper Default Locale Fallback Boundary - 2026-05-08
+
+- Hierarchy: keep `NumberStepper` as the compact numeric input primitive; changing fallback locale order must not add helper text, validation prose, or a separate stepper toolbar.
+- Density: preserve the single-line input, narrow icon buttons, and screen-reader-only action labels so dense forms keep their current scan rhythm.
+- Anti-AI-slop: do not invent numeric policy copy, fake validation state, SaaS settings language, or decorative status chrome while correcting the fallback catalog order.
+- Operator workflow clarity: shared decrement/increment copy resolves from runtime messages with an English fallback when no caller locale is available; caller-provided labels remain authoritative.
+- Context visibility: min/max/step clamping, disabled behavior, input ownership, action markers, and icon affordances stay unchanged.
+
+## Entity Detail Route Manifest Locale Parity Boundary - 2026-05-08
+
+- Hierarchy: keep the entity detail parity route focused on the cold full-width entity detail surface, compact header, count strip, signal grid, related/next/drilldown panels, footer, help launcher, and action row.
+- Density: preserve the existing selector-heavy smoke path and short text/action snippets without adding extra parity targets, screenshot modes, or synthetic wait states.
+- Anti-AI-slop: update only expected locale snippets for the real default entity detail surface; do not add fake health/status text, fake navigation, or product prose.
+- Operator workflow clarity: route parity must assert the same visible fallback copy operators see after the entity detail surface moved to English default runtime messages.
+- Context visibility: surface markers, header/action/count selectors, footer/help anchors, entity detail copy, and command row labels remain covered by the route manifest.
+
+## Entity Definition Draft Workspace Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit as the same single draft-to-catalog flow; this slice only moves request-workspace lookup to the mapping boundary that owns relation resolution.
+- Density: do not add import banners, workspace chips, explanatory cards, or extra authoring chrome while preserving definition parsing and relation evidence.
+- Anti-AI-slop: do not synthesize relation targets, fake catalog matches, inferred workspace ownership, or import success evidence from route context.
+- Operator workflow clarity: default definition parsing should remain one action, with workspace-scoped relation resolution happening where entity DTO mapping and relation lookup already meet.
+- Context visibility: entity id, relation target ref/id, bundle order, explicit workspace overloads, and request-scoped catalog boundaries stay visible through existing DTOs and tests.
+
+## Entity List Catalog Workspace Boundary - 2026-05-10
+
+- Hierarchy: keep the entity list as the same catalog-summary surface; this slice only moves default request-workspace lookup to the catalog query boundary that owns page predicates.
+- Density: do not add workspace badges, filter chrome, warnings, or empty-state copy while preserving compact list summaries and runtime evidence.
+- Anti-AI-slop: do not infer catalog rows from route context, synthesize hidden entities, fake total counts, or turn workspace scoping into decorative UI copy.
+- Operator workflow clarity: default entity list requests should remain a single list action, with request-scoped filtering happening where catalog page query and persisted rows meet.
+- Context visibility: explicit workspace overloads, page totals, scoped row filtering, latest definition activity, and runtime status evidence stay covered by existing DTO and service contracts.
+
+## Entity Detail Access Workspace Boundary - 2026-05-10
+
+- Hierarchy: keep entity detail as the same single catalog detail surface; this slice only moves default request-workspace lookup to the workspace access boundary that owns entity accessibility.
+- Density: do not add detail banners, workspace chips, filtered-state copy, or extra evidence rows while preserving identity, monitor bind, and relation child evidence.
+- Anti-AI-slop: do not synthesize accessible entities, fake missing-detail fallbacks, inferred workspace ownership, or hidden cross-tenant evidence when default detail access is scoped.
+- Operator workflow clarity: default detail requests should remain one load action, with request-scoped entity acceptance happening before child-evidence assembly.
+- Context visibility: explicit workspace overloads, entity id, accepted entity row, identities, monitor binds, relations, and missing-entity behavior stay covered by service contracts.
+
+## Entity Identity Candidate Workspace Boundary - 2026-05-10
+
+- Hierarchy: keep monitor-to-entity binding candidates as the existing suggestion evidence, not a new catalog or topology surface; this slice only moves default request-workspace lookup to the access boundary.
+- Density: do not add candidate badges, workspace hints, empty-state copy, or extra operator chrome while preserving direct/suggested recommendations and matched identity evidence.
+- Anti-AI-slop: do not synthesize candidate entities, fake identity matches, inferred ownership, or cross-workspace rows when monitor identity hints are scoped.
+- Operator workflow clarity: default binding suggestions should remain one request, with workspace filtering applied before candidate scoring is exposed to the operator.
+- Context visibility: explicit workspace overloads, matched identity keys, already-bound evidence, candidate score, and hidden cross-tenant rows stay covered by service contracts.
+
+## Entity Catalog Suggestions Request Workspace Boundary - 2026-05-10
+
+- Hierarchy: keep catalog suggestions as quiet editor/read-model assistance, not a separate catalog browser; this slice only moves default request-workspace lookup to the access boundary.
+- Density: do not add workspace chips, suggestion explanations, empty-state banners, or additional editor chrome while preserving compact owner, namespace, environment, system, lifecycle, tier, reference, language, and link-provider suggestions.
+- Anti-AI-slop: do not synthesize suggestion values, infer ownership, invent catalog rows, or expose cross-workspace profile hints when default suggestions are scoped.
+- Operator workflow clarity: default suggestions should remain a single lookup, with request-scoped entity filtering applied before profile fields are aggregated.
+- Context visibility: explicit workspace overloads, sort order, suggestion limit, entity refs, inherited refs, and scoped row filtering stay covered by service contracts.
+
+## Entity Relation Replacement Request Workspace Boundary - 2026-05-10
+
+- Hierarchy: keep relation replacement as part of the definition/catalog mutation workflow; this slice only moves default request-workspace lookup behind the access boundary used by target resolution.
+- Density: do not add relation badges, workspace chips, warnings, topology previews, or extra mutation chrome while preserving normalized unique relation rows.
+- Anti-AI-slop: do not synthesize target entities, infer relation edges, invent cross-workspace matches, or turn unresolved refs into fake topology evidence.
+- Operator workflow clarity: default relation replacement should remain one mutation step, with request-scoped direct-id and typed-reference target checks happening before persistence.
+- Context visibility: explicit workspace overloads, source id checks, target refs, dedupe keys, direct-id compatibility, and normalized relation rows stay covered by service contracts.
+
+## Entity Definition Type Resolver Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit as the same draft-to-catalog flow; this slice only moves kind/entity-type/subtype normalization into a narrow resolver boundary.
+- Density: do not add definition UI chrome, import hints, taxonomy badges, or explanatory copy while preserving canonical kind/type output.
+- Anti-AI-slop: do not invent entity types, synthetic subtypes, hidden catalog matches, or SaaS-style taxonomy labels while resolving aliases like `Entity`, `datastore`, and `api`.
+- Operator workflow clarity: definition parsing should still feel like one import action; type resolution becomes a stable domain rule that normalization calls before DTO mapping.
+- Context visibility: raw `kind`, `entityType`, `entity_type`, `type`, Datadog service aliases, explicit subtype keys, and legacy default service fallback stay covered by service contracts.
+
+## Entity Definition Telemetry Normalizer Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit as one draft-to-catalog flow; this slice only moves telemetry identity and monitor-bind extraction into a narrow normalization boundary.
+- Density: do not add import warnings, telemetry panels, match-score badges, or authoring chrome while preserving compact identity and monitor-bind evidence in the definition model.
+- Anti-AI-slop: do not synthesize identities, fake monitor ids, inferred binding scores, or hidden match context while extracting declared telemetry evidence.
+- Operator workflow clarity: telemetry blocks should remain declarative import evidence, with malformed rows quietly ignored instead of becoming fake suggestions or UI state.
+- Context visibility: identity key/value/source/priority/primary, monitor id aliases, bind source/type/status, score, and match context stay covered by service contracts.
+
+## Entity Definition Relation Normalizer Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit as one draft-to-catalog flow; this slice only moves relation and dependency extraction into a narrow normalization boundary.
+- Density: do not add topology previews, dependency badges, relation warnings, or import chrome while preserving compact declared relation evidence in the definition model.
+- Anti-AI-slop: do not synthesize target entities, fake topology edges, hidden relation scores, or inferred dependency refs while extracting declared relation blocks.
+- Operator workflow clarity: relation and dependsOn declarations should remain deterministic import evidence, with malformed rows quietly ignored rather than becoming fake graph state.
+- Context visibility: relation type/source/status, target id/ref, attributes, dependsOn strings, dedupe signatures, and catalog reference fallback stay covered by service contracts.
+
+## Entity Definition HertzBeat Evidence Normalizer Boundary - 2026-05-10
+
+- Hierarchy: keep definition import/edit as one draft-to-catalog flow; this slice only moves HertzBeat-specific evidence block extraction into a narrow normalization boundary.
+- Density: do not add code-location panels, saved-query previews, pipeline chips, performance badges, or import chrome while preserving compact declared evidence in the definition model.
+- Anti-AI-slop: do not synthesize repositories, fake saved queries, inferred performance tags, or invented pipeline fingerprints while extracting HertzBeat evidence blocks.
+- Operator workflow clarity: code locations, events, logs, performance tags, and pipeline fingerprints should stay deterministic import evidence, with malformed rows quietly ignored.
+- Context visibility: repositoryURL aliases, code paths, event/log queries, performance tags, canonical pipelines, and legacy `ci-pipeline-fingerprints` fallback stay covered by service contracts.
+
+## Explorer Row Handoff Href Boundary - 2026-05-07
+
+- Hierarchy: keep the operation link in the existing result-table operation column, without adding a handoff rail, new action column, or extra route chooser.
+- Density: preserve the compact table rhythm so signal, service, operation, status, duration, and timestamp stay readable in one scan.
+- Anti-AI-slop: avoid page-local signal routing, fake navigation, copied query-console route semantics, or translated-label logic while moving operation href ownership into explorer row data.
+- Operator workflow clarity: operation clicks must follow the same row payload as the signal badge, so query controls, result rows, detail panel, and trace/log/metric/entity handoffs remain unchanged.
+- Context visibility: translated signal labels can vary, but row identity, signal key, operation text, operation href, active detail row, and signal badge color stay aligned with the same explorer payload.
diff --git a/web-next/docs/parity/topology-product-ux-baseline.md b/web-next/docs/parity/topology-product-ux-baseline.md
new file mode 100644
index 0000000000..7d57e51f6d
--- /dev/null
+++ b/web-next/docs/parity/topology-product-ux-baseline.md
@@ -0,0 +1,100 @@
+# HertzBeat Topology Product And UX Baseline
+
+Local-only baseline for HertzBeat 2.0 topology work. This file captures the product and interaction direction for M5 so future implementation does not invent an isolated diagram. Keep it as the reference before changing `/topology`, topology APIs, or topology-related UI primitives.
+
+## 2026-05-19 Reference Scan
+
+Primary references checked:
+
+- Datadog Service Map: https://docs.datadoghq.com/tracing/services/services_map/
+- Datadog Service Page dependency map: https://docs.datadoghq.com/tracing/services/service_page/
+- Datadog Device Topology Map: https://docs.datadoghq.com/network_monitoring/devices/topology/
+- Datadog Network Map: https://docs.datadoghq.com/network_monitoring/cloud_network_monitoring/network_map/
+- Grafana Cloud Application Observability service map: https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/map/
+- Grafana Tempo Service Graph: https://grafana.com/docs/grafana/latest/datasources/tempo/service-graph/
+- Grafana Node graph panel: https://grafana.com/docs/grafana/latest/visualizations/panels-visualizations/visualizations/node-graph/
+
+Shared pattern from these products:
+
+- Topology is evidence-first. The graph is built from observed traces, APM/RUM, network discovery, monitor state, or catalog metadata, not from a static product drawing.
+- Nodes and edges are operational objects. A node is a service, resource, device, or entity; an edge is an observed call, physical dependency, VPN tunnel, monitor bind, or explicit relation.
+- A useful service graph carries RED facts directly on the graph: request rate, error rate, and latency or response time.
+- The map is scoped by time, environment, tags/facets, service, team/application, and resource type. Changing scope can legitimately produce a different graph.
+- Hover and click are investigation actions: hover highlights traffic direction/neighbors; click isolates or opens details for upstream/downstream, traces, logs, metrics, incidents, owners, and related pages.
+- Large graphs need grouping, clustering, filtering, search, alternate layouts, and table/detail companions. A canvas alone is not enough.
+
+## HertzBeat Product Rule
+
+HertzBeat topology must answer three operator questions:
+
+1. What is connected to this service/entity/device right now?
+2. Which dependency is unhealthy, slow, noisy, or incident-related in the selected time range?
+3. Where can I drill next without losing the current time, environment, workspace, and evidence context?
+
+Therefore `/topology` should be a compact observability workbench, not a marketing diagram or static architecture map.
+
+## UI Lab Reuse Bottom Line
+
+All topology frontend UI is release-blocking until it is owned by `@hertzbeat/ui` and demonstrated in `/ui-lab`. This is stricter than avoiding copied vendor visuals: the real `/topology` route should compose shared HertzBeat primitives, not invent page-local toolbar, canvas, node, edge, drawer, legend, table, filter, empty, loading, or action styling.
+
+If the real topology page does not visually match the compact dark primitive language shown in `/ui-lab`, treat the page as incomplete even when the API data is real. Add or adjust the shared primitive and its `/ui-lab` demo first, then reuse it in `/topology`.
+
+## Data Construction Contract
+
+- Default source: real topology API data. Never render seeded demo services when the API is pending, empty, or failing.
+- Service-call topology: derive edges from OTLP traces in Greptime using parent-child spans, `service.name`, peer service/database/messaging attributes, status, duration, and entity/workspace attribution.
+- RED aggregation must be pushed toward Greptime queries or Greptime flow tables: `requestCount`, `requestRatePerSecond`, `errorCount`, `errorRate`, `latencyP95Ms`, `latencyAvgMs`, `lastSeen`, and an example trace/span id for drilldown.
+- Scope filters must be first-class in API/query contracts: workspace, time range, environment, service/entity focus, source kind, relation type, depth, pagination/windowing, and hide-internal controls.
+- Entity topology should merge evidence from `EntityRelation`, monitor binds, trace calls, alert impact, incident association, and topology timeline events into one read model. Each edge should carry `sourceKind`, confidence, status, and evidence badges.
+- Nodes should age out or visually mark stale evidence when no selected-window signal exists. Do not present stale topology as current truth without a timestamp.
+- Missing traces, missing root spans, or insufficient discovery data must produce explicit empty/degraded states, not fake continuity.
+
+## Frontend Interaction Contract
+
+- Top toolbar: one compact row with time range, environment/source filters, search, group-by, depth, layout, refresh/live state, and reset/apply actions. It should stay visually aligned with the monitor time toolbar.
+- Canvas default: use a left-to-right layered service layout for call graphs because it makes upstream/downstream flow readable. Use force layout only for dense exploratory maps, and grid/table mode for very large result sets.
+- Node visual language:
+  - Shape/icon shows type: service, database, cache, queue, endpoint, host/device, monitor/entity.
+  - Border or arc shows health/error ratio.
+  - Main stat shows latency or health-critical value; secondary stat shows request rate or evidence count.
+  - Badges show source kinds such as trace, monitor, relation, alert, incident, or stale.
+- Edge visual language:
+  - Direction arrow is always visible.
+  - Thickness maps to request rate or traffic volume.
+  - Color maps to error/health state.
+  - Dashed/low-opacity edges indicate low confidence, stale evidence, or non-live/manual relations.
+  - Hover tooltip shows source, target, relation type, request rate, error rate, p95, last seen, and sample trace.
+- Hover behavior: highlight the hovered node/edge, immediate neighbors, and traffic direction while dimming unrelated graph elements.
+- Click behavior:
+  - Node click opens a right-side detail drawer with identity, RED/USE summary, upstream/downstream lists, alerts/incidents, recent traces/logs, monitors, owners, and timeline.
+  - Edge click opens a relationship drawer with source/target, RED metrics, evidence badges, sample traces, logs, alerts, first/last seen, and actions to open focused traces/logs.
+  - Double-click or explicit action switches the page into focused 1-hop inspect mode; 2-hop expansion is an explicit toggle.
+- Focus mode: show breadcrumb, current focus entity/service, depth, active filters, hidden/clustered count, and a clear exit action.
+- Large graph behavior: support search/facet filtering, group-by team/application/environment/type, collapse clusters with count and worst health, and offer a metric-ranked table companion.
+- Empty/degraded states: name the missing evidence source and selected time scope. Do not suggest the topology is complete when Greptime/trace data is unavailable.
+- All visible copy must use I18N. UI primitives must live in `@hertzbeat/ui`, be demonstrated in `/ui-lab`, and then be reused in `/topology`; page-local topology UI styling is a release-blocking gap.
+
+## HertzBeat-Specific UI Primitive Direction
+
+Topology work should converge on shared primitives rather than page-local canvas chrome:
+
+- `HzTopologyToolbar`: compact time/source/search/group/depth/layout controls.
+- `HzTopologyCanvas`: graph surface with pan, zoom, layout mode, keyboard focus, and empty/degraded states.
+- `HzTopologyNode`: typed node rendering with health arc, primary/secondary stats, badges, and focus state.
+- `HzTopologyEdge`: directional relationship rendering with traffic/error encoding and hover target.
+- `HzTopologyDetailDrawer`: node/edge evidence drawer with stable sections and cross-signal handoffs.
+- `HzTopologyLegend`: compact legend for health, source kind, edge confidence, and stale evidence.
+- `HzTopologyMetricTable`: RED-ranked companion table for dense graphs and keyboard access.
+
+Names above are HertzBeat-owned direction, not copied from Datadog or Grafana. Do not copy vendor code, assets, styles, or component names. The product benchmark is behavior and clarity, not visual cloning.
+
+## Release Gate For M5
+
+M5 real topology is not complete until all of the following are true:
+
+- `/topology` default and focused routes consume real topology APIs only.
+- Trace-derived service-call graph exposes node and edge RED metrics from Greptime-backed data, not fixture-only frontend fields.
+- Entity relations, monitor binds, alert/incident impact, and timeline events appear as typed evidence, not separate page-local guesses.
+- Browser verification can demonstrate default graph, focused 1-hop, focused 2-hop, node drawer, edge drawer, filters, time range, and empty/degraded states.
+- `/ui-lab` demonstrates every topology primitive used by `/topology` with compact dark styling and no copied vendor naming, and the real page visually follows that primitive language.
+- I18N/no-hardcoded-Chinese and no-static-fallback audits pass.
diff --git a/web-next/lib/ecosystem-alignment.test.ts b/web-next/lib/ecosystem-alignment.test.ts
new file mode 100644
index 0000000000..c41e996277
--- /dev/null
+++ b/web-next/lib/ecosystem-alignment.test.ts
@@ -0,0 +1,177 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+  ECOSYSTEM_ALIGNMENT,
+  REQUIRED_ECOSYSTEM_CAPABILITIES,
+  REQUIRED_ECOSYSTEM_SOURCES,
+  buildEcosystemCapabilityMatrix,
+  buildEcosystemMigrationPlan,
+  getEcosystemSourcePlan,
+  validateEcosystemAlignment,
+  validateEcosystemCompletionGate
+} from './ecosystem-alignment';
+import { findForbiddenProductLanguage, isAllowedExternalProductReference } from './hertzbeat-product-language';
+
+describe('HertzBeat ecosystem alignment contract', () => {
+  it('anchors HertzBeat as OTel-first, private-deployable, and entity-centered', () => {
+    expect(ECOSYSTEM_ALIGNMENT.positioning.deploymentModel).toBe('open-source-private-deployable');
+    expect(ECOSYSTEM_ALIGNMENT.positioning.statement).toContain('OTLP 三信号');
+    expect(ECOSYSTEM_ALIGNMENT.positioning.statement).toContain('实体中心');
+    expect(ECOSYSTEM_ALIGNMENT.positioning.statement).toContain('私有化部署');
+    expect(ECOSYSTEM_ALIGNMENT.positioning.coreLoop).toEqual(
+      expect.arrayContaining(['collectors', 'monitor-templates', 'otlp-three-signals', 'entity-catalog', 'alert-closure'])
+    );
+    expect(findForbiddenProductLanguage(ECOSYSTEM_ALIGNMENT.positioning.statement)).toEqual([]);
+  });
+
+  it('uses benchmark products only as migration or compatibility references', () => {
+    expect(REQUIRED_ECOSYSTEM_SOURCES).toEqual(['opentelemetry', 'grafana', 'datadog', 'signoz']);
+
+    for (const sourceKey of REQUIRED_ECOSYSTEM_SOURCES) {
+      const plan = getEcosystemSourcePlan(sourceKey);
+
+      expect(plan).toBeDefined();
+      expect(plan.referenceContext).not.toBe('product-copy');
+      expect(isAllowedExternalProductReference(plan.sourceName, plan.referenceContext)).toBe(true);
+      expect(plan.pageIdentityBorrowed).toBe(false);
+      expect(plan.sourceStyleCopied).toBe(false);
+      expect(plan.benchmarkRole).toMatch(/benchmark|migration|compatibility|standard/i);
+    }
+  });
+
+  it('maps dashboard and alert imports into HertzBeat-native operator targets', () => {
+    for (const sourceKey of ['grafana', 'datadog', 'signoz'] as const) {
+      const plan = getEcosystemSourcePlan(sourceKey);
+      const dashboard = plan.importMappings.find(mapping => mapping.kind === 'dashboard');
+      const alert = plan.importMappings.find(mapping => mapping.kind === 'alert-rule');
+
+      expect(dashboard).toBeDefined();
+      expect(alert).toBeDefined();
+      expect(dashboard?.migrationMode).not.toMatch(/clone|copy-ui|external-style/i);
+      expect(alert?.migrationMode).not.toMatch(/clone|copy-ui|external-style/i);
+      expect(dashboard?.targetSurfaces).toEqual(expect.arrayContaining(['monitor-template', 'entity-workbench']));
+      expect(alert?.targetSurfaces).toEqual(expect.arrayContaining(['alert-center', 'notification-policy']));
+      expect(dashboard?.operatorReviewRequired).toBe(true);
+      expect(alert?.operatorReviewRequired).toBe(true);
+    }
+  });
+
+  it('keeps OpenTelemetry migration native to OTLP without replacing the collector', () => {
+    const plan = getEcosystemSourcePlan('opentelemetry');
+    const otlp = plan.importMappings.find(mapping => mapping.kind === 'otlp-pipeline');
+
+    expect(otlp).toBeDefined();
+    expect(otlp?.migrationMode).toBe('native-otlp');
+    expect(otlp?.targetSurfaces).toEqual(
+      expect.arrayContaining(['otlp-center', 'metrics', 'logs', 'traces', 'entity-catalog'])
+    );
+    expect(plan.nonGoals).toEqual(expect.arrayContaining(['replace-opentelemetry-collector']));
+  });
+
+  it('validates source coverage, import coverage, and non-goals as release blockers', () => {
+    expect(validateEcosystemAlignment(ECOSYSTEM_ALIGNMENT)).toEqual([]);
+    expect(
+      validateEcosystemAlignment({
+        ...ECOSYSTEM_ALIGNMENT,
+        sourcePlans: ECOSYSTEM_ALIGNMENT.sourcePlans.filter(plan => plan.sourceKey !== 'datadog')
+      })
+    ).toEqual(expect.arrayContaining(['missing-source:datadog']));
+    expect(
+      validateEcosystemAlignment({
+        ...ECOSYSTEM_ALIGNMENT,
+        sourcePlans: ECOSYSTEM_ALIGNMENT.sourcePlans.map(plan =>
+          plan.sourceKey === 'grafana' ? { ...plan, nonGoals: [] } : plan
+        )
+      })
+    ).toEqual(expect.arrayContaining(['missing-non-goals:grafana']));
+  });
+
+  it('builds a source-specific migration plan with cutover order and review gates', () => {
+    const datadogPlan = buildEcosystemMigrationPlan('datadog');
+
+    expect(datadogPlan.sourceKey).toBe('datadog');
+    expect(datadogPlan.cutoverSteps.map(step => step.key)).toEqual([
+      'inventory-source-artifacts',
+      'map-identity-and-labels',
+      'translate-dashboards',
+      'translate-alert-rules',
+      'operator-review',
+      'hertzbeat-cutover'
+    ]);
+    expect(datadogPlan.cutoverSteps.find(step => step.key === 'translate-dashboards')?.targetSurfaces).toEqual(
+      expect.arrayContaining(['monitor-template', 'entity-workbench'])
+    );
+    expect(datadogPlan.cutoverSteps.find(step => step.key === 'translate-alert-rules')?.targetSurfaces).toEqual(
+      expect.arrayContaining(['alert-center', 'notification-policy'])
+    );
+    expect(datadogPlan.reviewRequired).toBe(true);
+    expect(datadogPlan.blockedNarratives).toEqual(
+      expect.arrayContaining(['copy-datadog-saas-packaging', 'copy-datadog-dashboard-widgets'])
+    );
+  });
+
+  it('builds an OpenTelemetry migration plan around native OTLP cutover only', () => {
+    const otelPlan = buildEcosystemMigrationPlan('opentelemetry');
+
+    expect(otelPlan.cutoverSteps.map(step => step.key)).toEqual([
+      'inventory-source-artifacts',
+      'map-identity-and-labels',
+      'configure-otlp-ingest',
+      'verify-three-signal-ingest',
+      'hertzbeat-cutover'
+    ]);
+    expect(otelPlan.reviewRequired).toBe(false);
+    expect(otelPlan.cutoverSteps.flatMap(step => step.targetSurfaces)).toEqual(
+      expect.arrayContaining(['otlp-center', 'metrics', 'logs', 'traces', 'entity-catalog'])
+    );
+    expect(otelPlan.blockedNarratives).toEqual(expect.arrayContaining(['replace-opentelemetry-collector']));
+  });
+
+  it('builds a benchmark capability matrix without copied page identity', () => {
+    const matrix = buildEcosystemCapabilityMatrix();
+
+    expect(matrix.map(row => row.capability)).toEqual([
+      'otlp-three-signal-ingest',
+      'resource-to-entity-resolution',
+      'dashboard-import',
+      'alert-rule-import',
+      'topology-and-evidence-handoff',
+      'safe-automation'
+    ]);
+    expect(matrix.find(row => row.capability === 'dashboard-import')?.hertzbeatTarget).toContain('monitor templates');
+    expect(matrix.find(row => row.capability === 'alert-rule-import')?.hertzbeatTarget).toContain('alert closure');
+    expect(matrix.every(row => row.externalReferenceContext !== 'product-copy')).toBe(true);
+    expect(matrix.every(row => row.pageIdentityBorrowed === false)).toBe(true);
+  });
+
+  it('passes a focused M9 completion gate and reports release blockers', () => {
+    expect(REQUIRED_ECOSYSTEM_CAPABILITIES).toEqual([
+      'otlp-three-signal-ingest',
+      'resource-to-entity-resolution',
+      'dashboard-import',
+      'alert-rule-import',
+      'topology-and-evidence-handoff',
+      'safe-automation'
+    ]);
+    expect(validateEcosystemCompletionGate()).toEqual([]);
+    expect(
+      validateEcosystemCompletionGate({
+        ...ECOSYSTEM_ALIGNMENT,
+        positioning: {
+          ...ECOSYSTEM_ALIGNMENT.positioning,
+          statement: 'SigNoZ-style Service Map for all services'
+        }
+      })
+    ).toEqual(expect.arrayContaining(['forbidden-positioning-language']));
+    expect(
+      validateEcosystemCompletionGate({
+        ...ECOSYSTEM_ALIGNMENT,
+        sourcePlans: ECOSYSTEM_ALIGNMENT.sourcePlans.map(plan =>
+          plan.sourceKey === 'datadog'
+            ? { ...plan, importMappings: plan.importMappings.filter(mapping => mapping.kind !== 'alert-rule') }
+            : plan
+        )
+      })
+    ).toEqual(expect.arrayContaining(['alignment:missing-import:datadog:alert-rule']));
+  });
+});
diff --git a/web-next/lib/ecosystem-alignment.ts b/web-next/lib/ecosystem-alignment.ts
new file mode 100644
index 0000000000..078cd3e71d
--- /dev/null
+++ b/web-next/lib/ecosystem-alignment.ts
@@ -0,0 +1,494 @@
+import { findForbiddenProductLanguage, type ExternalProductReferenceContext } from './hertzbeat-product-language';
+
+export type EcosystemSourceKey = 'opentelemetry' | 'grafana' | 'datadog' | 'signoz';
+
+export type EcosystemImportKind = 'otlp-pipeline' | 'dashboard' | 'alert-rule';
+
+export type HertzBeatMigrationSurface =
+  | 'otlp-center'
+  | 'metrics'
+  | 'logs'
+  | 'traces'
+  | 'entity-catalog'
+  | 'entity-workbench'
+  | 'monitor-template'
+  | 'monitor-detail'
+  | 'alert-center'
+  | 'notification-policy';
+
+export type EcosystemMigrationMode =
+  | 'native-otlp'
+  | 'link-and-translate'
+  | 'inventory-and-translate'
+  | 'pipeline-repoint-and-review';
+
+export interface EcosystemImportMapping {
+  kind: EcosystemImportKind;
+  sourceArtifact: string;
+  hertzbeatTarget: string;
+  targetSurfaces: HertzBeatMigrationSurface[];
+  migrationMode: EcosystemMigrationMode;
+  operatorReviewRequired: boolean;
+  notes: string[];
+}
+
+export interface EcosystemSourcePlan {
+  sourceKey: EcosystemSourceKey;
+  sourceName: string;
+  referenceContext: ExternalProductReferenceContext;
+  benchmarkRole: string;
+  migrationPrinciple: string;
+  pageIdentityBorrowed: boolean;
+  sourceStyleCopied: boolean;
+  importMappings: EcosystemImportMapping[];
+  nonGoals: string[];
+}
+
+export interface EcosystemAlignment {
+  positioning: {
+    deploymentModel: 'open-source-private-deployable';
+    statement: string;
+    coreLoop: string[];
+  };
+  sourcePlans: EcosystemSourcePlan[];
+}
+
+export type EcosystemAlignmentIssue =
+  | `missing-source:${EcosystemSourceKey}`
+  | `missing-import:${EcosystemSourceKey}:${'dashboard' | 'alert-rule'}`
+  | `missing-non-goals:${EcosystemSourceKey}`
+  | `borrowed-identity:${EcosystemSourceKey}`;
+
+export type EcosystemCutoverStepKey =
+  | 'inventory-source-artifacts'
+  | 'map-identity-and-labels'
+  | 'configure-otlp-ingest'
+  | 'verify-three-signal-ingest'
+  | 'translate-dashboards'
+  | 'translate-alert-rules'
+  | 'operator-review'
+  | 'hertzbeat-cutover';
+
+export interface EcosystemCutoverStep {
+  key: EcosystemCutoverStepKey;
+  label: string;
+  targetSurfaces: HertzBeatMigrationSurface[];
+  reviewGate: boolean;
+}
+
+export interface EcosystemMigrationPlan {
+  sourceKey: EcosystemSourceKey;
+  sourceName: string;
+  reviewRequired: boolean;
+  cutoverSteps: EcosystemCutoverStep[];
+  blockedNarratives: string[];
+}
+
+export type EcosystemCapability =
+  | 'otlp-three-signal-ingest'
+  | 'resource-to-entity-resolution'
+  | 'dashboard-import'
+  | 'alert-rule-import'
+  | 'topology-and-evidence-handoff'
+  | 'safe-automation';
+
+export interface EcosystemCapabilityRow {
+  capability: EcosystemCapability;
+  benchmarkSources: EcosystemSourceKey[];
+  hertzbeatTarget: string;
+  migrationAction: string;
+  externalReferenceContext: ExternalProductReferenceContext;
+  pageIdentityBorrowed: boolean;
+}
+
+export const REQUIRED_ECOSYSTEM_SOURCES: EcosystemSourceKey[] = ['opentelemetry', 'grafana', 'datadog', 'signoz'];
+export const REQUIRED_ECOSYSTEM_CAPABILITIES: EcosystemCapability[] = [
+  'otlp-three-signal-ingest',
+  'resource-to-entity-resolution',
+  'dashboard-import',
+  'alert-rule-import',
+  'topology-and-evidence-handoff',
+  'safe-automation'
+];
+
+const dashboardTargets: HertzBeatMigrationSurface[] = ['monitor-template', 'entity-workbench', 'monitor-detail'];
+const alertTargets: HertzBeatMigrationSurface[] = ['alert-center', 'notification-policy', 'entity-workbench'];
+
+export const ECOSYSTEM_ALIGNMENT: EcosystemAlignment = {
+  positioning: {
+    deploymentModel: 'open-source-private-deployable',
+    statement:
+      'HertzBeat 是面向私有化部署的开源可观测平台,以采集器、监控模板、OTLP 三信号、实体中心、告警闭环和安全自动化形成企业运维闭环。',
+    coreLoop: ['collectors', 'monitor-templates', 'otlp-three-signals', 'entity-catalog', 'alert-closure', 'safe-automation']
+  },
+  sourcePlans: [
+    {
+      sourceKey: 'opentelemetry',
+      sourceName: 'OpenTelemetry',
+      referenceContext: 'compatibility-note',
+      benchmarkRole: 'OTLP protocol and semantic compatibility standard',
+      migrationPrinciple:
+        'Keep existing SDK or Collector pipelines, repoint OTLP metrics, logs, and traces into HertzBeat, then resolve resources into entities.',
+      pageIdentityBorrowed: false,
+      sourceStyleCopied: false,
+      importMappings: [
+        {
+          kind: 'otlp-pipeline',
+          sourceArtifact: 'OpenTelemetry SDK or Collector OTLP exporters',
+          hertzbeatTarget: 'HertzBeat OTLP ingest endpoints plus entity/resource catalog resolution',
+          targetSurfaces: ['otlp-center', 'metrics', 'logs', 'traces', 'entity-catalog'],
+          migrationMode: 'native-otlp',
+          operatorReviewRequired: false,
+          notes: [
+            'Preserve resource attributes for identity resolution.',
+            'Use HertzBeat ingestion tokens and workspace scope instead of external account identity.'
+          ]
+        }
+      ],
+      nonGoals: ['replace-opentelemetry-collector', 'copy-external-protocol-docs', 'invent-non-otlp-signal-model']
+    },
+    {
+      sourceKey: 'grafana',
+      sourceName: 'Grafana',
+      referenceContext: 'migration-source',
+      benchmarkRole: 'dashboard and alert migration benchmark',
+      migrationPrinciple:
+        'Treat dashboards and alert rules as source artifacts; translate them into HertzBeat monitor templates, entity workbench evidence, and alert closure flows.',
+      pageIdentityBorrowed: false,
+      sourceStyleCopied: false,
+      importMappings: [
+        {
+          kind: 'dashboard',
+          sourceArtifact: 'Dashboard JSON or linked dashboard template',
+          hertzbeatTarget: 'Monitor template, monitor detail, and entity workbench evidence entry points',
+          targetSurfaces: dashboardTargets,
+          migrationMode: 'link-and-translate',
+          operatorReviewRequired: true,
+          notes: [
+            'Keep existing Grafana links when configured on a monitor.',
+            'Translate panels only when their data source and HertzBeat metric template are known.'
+          ]
+        },
+        {
+          kind: 'alert-rule',
+          sourceArtifact: 'Grafana-managed alert rule definitions',
+          hertzbeatTarget: 'HertzBeat alert rules, inhibit/silence policy, and notification routes',
+          targetSurfaces: alertTargets,
+          migrationMode: 'inventory-and-translate',
+          operatorReviewRequired: true,
+          notes: [
+            'Carry threshold intent, labels, and notification ownership forward.',
+            'Require operator review for query expressions and notification semantics.'
+          ]
+        }
+      ],
+      nonGoals: ['copy-grafana-dashboard-layout', 'clone-panel-plugin-dsl', 'make-grafana-the-primary-ia']
+    },
+    {
+      sourceKey: 'datadog',
+      sourceName: 'Datadog',
+      referenceContext: 'migration-source',
+      benchmarkRole: 'SaaS dashboard and monitor migration benchmark',
+      migrationPrinciple:
+        'Use exported dashboard and monitor inventory only as migration input, then rebuild around HertzBeat collectors, templates, entities, alerts, and private deployment.',
+      pageIdentityBorrowed: false,
+      sourceStyleCopied: false,
+      importMappings: [
+        {
+          kind: 'dashboard',
+          sourceArtifact: 'Dashboard JSON or dashboard API inventory',
+          hertzbeatTarget: 'Monitor template coverage, entity workbench drilldowns, and signal-specific views',
+          targetSurfaces: dashboardTargets,
+          migrationMode: 'inventory-and-translate',
+          operatorReviewRequired: true,
+          notes: [
+            'Translate widgets into HertzBeat-native signal views only when metric/log/trace sources are mapped.',
+            'Keep SaaS account, pricing, notebook, and collaboration concepts out of HertzBeat page identity.'
+          ]
+        },
+        {
+          kind: 'alert-rule',
+          sourceArtifact: 'Monitor API inventory',
+          hertzbeatTarget: 'HertzBeat threshold rules, alert grouping, silence/inhibit, and notification closure',
+          targetSurfaces: alertTargets,
+          migrationMode: 'inventory-and-translate',
+          operatorReviewRequired: true,
+          notes: [
+            'Map monitor thresholds and scoped tags to HertzBeat alert labels and resources.',
+            'Require review before enabling actions because notification semantics differ by environment.'
+          ]
+        }
+      ],
+      nonGoals: ['copy-datadog-saas-packaging', 'copy-datadog-dashboard-widgets', 'represent-cost-or-tenant-governance']
+    },
+    {
+      sourceKey: 'signoz',
+      sourceName: 'SigNoZ',
+      referenceContext: 'migration-source',
+      benchmarkRole: 'OpenTelemetry-native migration benchmark',
+      migrationPrinciple:
+        'Repoint OTLP pipelines into HertzBeat and translate dashboards or alerts only as reviewed source artifacts, preserving HertzBeat entity and alert closure workflows.',
+      pageIdentityBorrowed: false,
+      sourceStyleCopied: false,
+      importMappings: [
+        {
+          kind: 'otlp-pipeline',
+          sourceArtifact: 'OTLP metrics, logs, and traces pipeline',
+          hertzbeatTarget: 'HertzBeat OTLP center, three signal workbenches, and entity/resource catalog',
+          targetSurfaces: ['otlp-center', 'metrics', 'logs', 'traces', 'entity-catalog'],
+          migrationMode: 'pipeline-repoint-and-review',
+          operatorReviewRequired: true,
+          notes: [
+            'Preserve service namespace, service name, environment, and deployment attributes.',
+            'Confirm ingestion tokens, quotas, and redaction policy before cutover.'
+          ]
+        },
+        {
+          kind: 'dashboard',
+          sourceArtifact: 'Dashboard or query inventory',
+          hertzbeatTarget: 'Entity workbench, monitor detail, and signal-specific views',
+          targetSurfaces: dashboardTargets,
+          migrationMode: 'inventory-and-translate',
+          operatorReviewRequired: true,
+          notes: ['Translate useful operational views into HertzBeat entity or monitor context instead of copying page hierarchy.']
+        },
+        {
+          kind: 'alert-rule',
+          sourceArtifact: 'Alert rule inventory',
+          hertzbeatTarget: 'HertzBeat alert center, grouping, silence/inhibit, and notification policy',
+          targetSurfaces: alertTargets,
+          migrationMode: 'inventory-and-translate',
+          operatorReviewRequired: true,
+          notes: ['Review labels, routing, and evidence links before enabling alert closure automation.']
+        }
+      ],
+      nonGoals: ['copy-signoz-page-hierarchy', 'copy-signoz-query-workbench', 'describe-hertzbeat-as-apm-only']
+    }
+  ]
+};
+
+export function getEcosystemSourcePlan(sourceKey: EcosystemSourceKey): EcosystemSourcePlan {
+  const plan = ECOSYSTEM_ALIGNMENT.sourcePlans.find(candidate => candidate.sourceKey === sourceKey);
+
+  if (!plan) {
+    throw new Error(`Missing ecosystem source plan: ${sourceKey}`);
+  }
+
+  return plan;
+}
+
+export function validateEcosystemAlignment(alignment: EcosystemAlignment): EcosystemAlignmentIssue[] {
+  const issues: EcosystemAlignmentIssue[] = [];
+
+  for (const sourceKey of REQUIRED_ECOSYSTEM_SOURCES) {
+    const plan = alignment.sourcePlans.find(candidate => candidate.sourceKey === sourceKey);
+
+    if (!plan) {
+      issues.push(`missing-source:${sourceKey}`);
+      continue;
+    }
+
+    if (plan.pageIdentityBorrowed || plan.sourceStyleCopied || plan.referenceContext === 'product-copy') {
+      issues.push(`borrowed-identity:${sourceKey}`);
+    }
+
+    if (sourceKey !== 'opentelemetry') {
+      for (const kind of ['dashboard', 'alert-rule'] as const) {
+        if (!plan.importMappings.some(mapping => mapping.kind === kind)) {
+          issues.push(`missing-import:${sourceKey}:${kind}`);
+        }
+      }
+    }
+
+    if (plan.nonGoals.length === 0) {
+      issues.push(`missing-non-goals:${sourceKey}`);
+    }
+  }
+
+  return issues;
+}
+
+function buildStep(
+  key: EcosystemCutoverStepKey,
+  label: string,
+  targetSurfaces: HertzBeatMigrationSurface[],
+  reviewGate = false
+): EcosystemCutoverStep {
+  return { key, label, targetSurfaces, reviewGate };
+}
+
+function hasMapping(plan: EcosystemSourcePlan, kind: EcosystemImportKind) {
+  return plan.importMappings.some(mapping => mapping.kind === kind);
+}
+
+function surfacesFor(plan: EcosystemSourcePlan, kind: EcosystemImportKind): HertzBeatMigrationSurface[] {
+  return [...new Set(plan.importMappings.filter(mapping => mapping.kind === kind).flatMap(mapping => mapping.targetSurfaces))];
+}
+
+export function buildEcosystemMigrationPlan(
+  sourceKey: EcosystemSourceKey,
+  alignment: EcosystemAlignment = ECOSYSTEM_ALIGNMENT
+): EcosystemMigrationPlan {
+  const plan = alignment.sourcePlans.find(candidate => candidate.sourceKey === sourceKey);
+
+  if (!plan) {
+    throw new Error(`Missing ecosystem source plan: ${sourceKey}`);
+  }
+
+  const cutoverSteps: EcosystemCutoverStep[] = [
+    buildStep('inventory-source-artifacts', 'Inventory migration source artifacts', []),
+    buildStep('map-identity-and-labels', 'Map resource identity, labels, and ownership into HertzBeat entities', [
+      'entity-catalog'
+    ])
+  ];
+
+  if (hasMapping(plan, 'otlp-pipeline')) {
+    cutoverSteps.push(
+      buildStep('configure-otlp-ingest', 'Configure HertzBeat OTLP ingest endpoints and tokens', surfacesFor(plan, 'otlp-pipeline')),
+      buildStep('verify-three-signal-ingest', 'Verify metrics, logs, traces, and entity resolution before cutover', [
+        'metrics',
+        'logs',
+        'traces',
+        'entity-catalog'
+      ])
+    );
+  }
+
+  if (hasMapping(plan, 'dashboard')) {
+    cutoverSteps.push(
+      buildStep('translate-dashboards', 'Translate dashboard intent into HertzBeat monitor and entity views', surfacesFor(plan, 'dashboard'), true)
+    );
+  }
+
+  if (hasMapping(plan, 'alert-rule')) {
+    cutoverSteps.push(
+      buildStep('translate-alert-rules', 'Translate alert intent into HertzBeat alert closure policy', surfacesFor(plan, 'alert-rule'), true)
+    );
+  }
+
+  if (plan.importMappings.some(mapping => mapping.operatorReviewRequired)) {
+    cutoverSteps.push(
+      buildStep('operator-review', 'Review translated thresholds, labels, routing, and evidence links before enabling automation', [
+        'alert-center',
+        'entity-workbench'
+      ], true)
+    );
+  }
+
+  cutoverSteps.push(
+    buildStep('hertzbeat-cutover', 'Cut over to HertzBeat-native collector, template, entity, signal, and alert workflows', [
+      'otlp-center',
+      'entity-workbench',
+      'alert-center'
+    ])
+  );
+
+  return {
+    sourceKey: plan.sourceKey,
+    sourceName: plan.sourceName,
+    reviewRequired: plan.importMappings.some(mapping => mapping.operatorReviewRequired),
+    cutoverSteps,
+    blockedNarratives: [...plan.nonGoals]
+  };
+}
+
+export function buildEcosystemCapabilityMatrix(): EcosystemCapabilityRow[] {
+  return [
+    {
+      capability: 'otlp-three-signal-ingest',
+      benchmarkSources: ['opentelemetry', 'signoz'],
+      hertzbeatTarget: 'native OTLP metrics, logs, and traces ingest through HertzBeat workspace tokens',
+      migrationAction: 'repoint SDK or Collector OTLP exporters, then validate RED ingest evidence',
+      externalReferenceContext: 'compatibility-note',
+      pageIdentityBorrowed: false
+    },
+    {
+      capability: 'resource-to-entity-resolution',
+      benchmarkSources: ['opentelemetry', 'signoz'],
+      hertzbeatTarget: 'entity catalog resolution from service, namespace, environment, and HertzBeat identity attributes',
+      migrationAction: 'map resource attributes into entity identities and review unresolved candidates',
+      externalReferenceContext: 'compatibility-note',
+      pageIdentityBorrowed: false
+    },
+    {
+      capability: 'dashboard-import',
+      benchmarkSources: ['grafana', 'datadog', 'signoz'],
+      hertzbeatTarget: 'reviewed monitor templates, monitor detail context, and entity workbench views',
+      migrationAction: 'inventory dashboards as source artifacts and translate only mapped signal views',
+      externalReferenceContext: 'migration-source',
+      pageIdentityBorrowed: false
+    },
+    {
+      capability: 'alert-rule-import',
+      benchmarkSources: ['grafana', 'datadog', 'signoz'],
+      hertzbeatTarget: 'HertzBeat alert closure through grouping, silence, inhibit, and notification policy',
+      migrationAction: 'translate threshold intent, labels, routing, and evidence links with operator review',
+      externalReferenceContext: 'migration-source',
+      pageIdentityBorrowed: false
+    },
+    {
+      capability: 'topology-and-evidence-handoff',
+      benchmarkSources: ['opentelemetry', 'grafana', 'datadog', 'signoz'],
+      hertzbeatTarget: 'entity-centered topology, alert evidence, and cross-signal drilldowns',
+      migrationAction: 'derive handoffs from HertzBeat entity relations and real telemetry evidence',
+      externalReferenceContext: 'compatibility-note',
+      pageIdentityBorrowed: false
+    },
+    {
+      capability: 'safe-automation',
+      benchmarkSources: ['grafana', 'datadog', 'signoz'],
+      hertzbeatTarget: 'reviewed HertzBeat notification, runbook, and automation actions inside private deployment',
+      migrationAction: 'require operator review before enabling migrated actions or notification semantics',
+      externalReferenceContext: 'migration-source',
+      pageIdentityBorrowed: false
+    }
+  ];
+}
+
+export function validateEcosystemCompletionGate(alignment: EcosystemAlignment = ECOSYSTEM_ALIGNMENT): string[] {
+  const issues = validateEcosystemAlignment(alignment).map(issue => `alignment:${issue}`);
+  const capabilityMatrix = buildEcosystemCapabilityMatrix();
+
+  if (alignment.positioning.deploymentModel !== 'open-source-private-deployable') {
+    issues.push('missing-private-deployable-positioning');
+  }
+
+  if (!alignment.positioning.statement.includes('OTLP 三信号') || !alignment.positioning.statement.includes('实体中心')) {
+    issues.push('missing-otel-entity-positioning');
+  }
+
+  if (findForbiddenProductLanguage(alignment.positioning.statement).length > 0) {
+    issues.push('forbidden-positioning-language');
+  }
+
+  for (const sourceKey of REQUIRED_ECOSYSTEM_SOURCES) {
+    const plan = alignment.sourcePlans.find(candidate => candidate.sourceKey === sourceKey);
+    if (!plan) {
+      continue;
+    }
+
+    const migrationPlan = buildEcosystemMigrationPlan(sourceKey, alignment);
+    if (migrationPlan.cutoverSteps.length < 4) {
+      issues.push(`incomplete-migration-plan:${sourceKey}`);
+    }
+
+    if (migrationPlan.blockedNarratives.length === 0) {
+      issues.push(`missing-blocked-narratives:${sourceKey}`);
+    }
+  }
+
+  for (const capability of REQUIRED_ECOSYSTEM_CAPABILITIES) {
+    const row = capabilityMatrix.find(candidate => candidate.capability === capability);
+    if (!row) {
+      issues.push(`missing-capability:${capability}`);
+      continue;
+    }
+
+    if (row.externalReferenceContext === 'product-copy' || row.pageIdentityBorrowed) {
+      issues.push(`borrowed-capability-identity:${capability}`);
+    }
+
+  }
+
+  return issues;
+}
diff --git a/web-next/lib/hertzbeat-2-gap-audit.test.ts b/web-next/lib/hertzbeat-2-gap-audit.test.ts
new file mode 100644
index 0000000000..24dc9a9c1a
--- /dev/null
+++ b/web-next/lib/hertzbeat-2-gap-audit.test.ts
@@ -0,0 +1,541 @@
+import { describe, expect, it } from 'vitest';
+
+import { buildHertzBeat2GapAudit, validateHertzBeat2ReleaseGate } from './hertzbeat-2-gap-audit';
+
+describe('HertzBeat 2.0 release gap audit', () => {
+  it('does not reopen M10 route gaps after the frontend cutover audit is green', () => {
+    const audit = buildHertzBeat2GapAudit();
+    const routeGaps = audit.gaps.filter(gap => gap.area === 'frontend-parity').map(gap => gap.route);
+
+    expect(routeGaps).toEqual([]);
+  });
+
+  it('tracks the currently known 2.0 functional gaps with decision-ready metadata', () => {
+    const audit = buildHertzBeat2GapAudit();
+    const codes = audit.gaps.map(gap => gap.code);
+
+    expect(codes).toEqual(
+      expect.arrayContaining([
+        'topology-real-data-api',
+        'incident-workbench-domain',
+        'greptime-otlp-query-pushdown',
+        'entity-evidence-read-model',
+        'ui-i18n-reuse-sweep'
+      ])
+    );
+    expect(
+      audit.gaps.every(gap =>
+        gap.owner.length > 0 &&
+        gap.page.length > 0 &&
+        gap.backendApiStatus.length > 0 &&
+        gap.currentState.length > 0 &&
+        gap.requiredClosure.length > 0 &&
+        gap.nextStep.length > 0
+      )
+    ).toBe(true);
+    expect(audit.gaps.find(gap => gap.code === 'incident-workbench-domain')).toMatchObject({
+      backendApiStatus: 'available',
+      releaseBlocking: false,
+      requiredClosure: [
+        'Keep the list/detail/transition Browser proof in the release regression set.',
+        'Fold richer incident CRUD and public-status administration parity into the M10 public-status area.'
+      ],
+      evidence: expect.arrayContaining([
+        '@hertzbeat/ui and /ui-lab demonstrate HzIncidentWorkbench with operator-compact density and cold-matte-hard-edge styling',
+        '/incidents reads GET /status/page/incident?pageIndex=0&pageSize=8 through the local BFF list adapter',
+        '/incidents reads selected incident detail from GET /status/page/incident/{id} when list data exists',
+        '/incidents writes status transitions through PUT /status/page/incident and refreshes list/detail after success',
+        'Browser verification: local empty /incidents renders API-backed empty state with no placeholder copy, total=0, detail not-requested, and transition actions disabled',
+        'Browser verification: seeded local incident transitioned from Identified to Monitoring through the shared state-2 action, detail state refreshed to state=2 with a new timeline entry, and temporary data was deleted afterwards'
+      ]),
+      sourceFiles: expect.arrayContaining([
+        'web-next/packages/hertzbeat-ui/src/index.tsx',
+        'web-next/app/ui-lab/page.tsx',
+        'web-next/lib/incidents-surface/controller.ts',
+        'web-next/app/incidents/incidents-page.tsx'
+      ])
+    });
+    expect(audit.gaps.find(gap => gap.code === 'topology-real-data-api')).toMatchObject({
+      backendApiStatus: 'verified',
+      releaseBlocking: false,
+      requiredClosure: [
+        'Keep live Greptime trace-call RED graph browser evidence in the release regression set.'
+      ],
+      evidence: expect.arrayContaining([
+        'Live Greptime hzb_traces data now browser-verifies /topology service-call RED graph with Checkout API to Payment API, G6 nodeCount=2, edgeCount=1, traceState=ready, and traceRedState=partial',
+        'Default /api/topology graphs are seeded from accessible entity catalog rows instead of static UI data',
+        'Topology sourceKind filters now isolate monitor ownership, trace-call, and relation-backed graph evidence',
+        'API-backed topology graphs expose impact timeline events from entity activities, relations, and monitor binds',
+        'loadTopologyGraph degrades stalled topology API reads with a bounded timeout instead of leaving /topology in api-pending',
+        'loadTopologyGraph aborts stalled topology API fetches after timeout so trace-call degradation does not leave dangling backend work',
+        'loadTopologyGraph deduplicates identical in-flight topology API reads so React dev hydration does not double-query slow Greptime topology scope',
+        'Trace-call topology uses a 60s bounded frontend timeout so slow relation-scoped Greptime graphs can render real API evidence or missing-evidence instead of premature degradation',
+        'Browser verification: /topology renders data-topology-data-source=api with api-empty and without checkout-api/orders-db/redis seed nodes when the API has no default evidence',
+        'Browser verification: /topology focused missing-entity route renders API-owned cmdb empty state with no static seed graph, no old explanatory header, and zero G6 nodes or edges',
+        'Browser verification: /topology service-call trace scope exposes time range, source kind, view mode, relationType=trace-call, trace-call-missing empty state, and zero G6 graph counts when local trace edges are absent',
+        'Focused API topology DOM exposes graph depth, node health, node evidence badges, edge relationship type, and edge evidence badges',
+        'Focused API topology DOM exposes trace-derived service graph request rate, error rate, and p95 latency on nodes, edges, and selected-edge evidence',
+        'Focused API topology DOM exposes trace-derived service graph first-seen and last-seen evidence on selected trace-call edges',
+        'Focused API topology selected edge drawer exposes browser-verifiable first/last seen and sample trace/span evidence window attributes',
+        'Focused API topology route exposes browser-verifiable trace-call edge, RED, evidence-window, and sample counts',
+        'EntityTopologyGraphInfo carries RED metrics on topology nodes and edges from TraceCallTopologyQueryService aggregation',
+        'GreptimeTraceQueryRepository builds service-to-service trace graph RED SQL with parent-child span joins, timestamp and environment predicates, seed service source/target scope, internal service filtering, request/error counts, avg latency, p95 sketch, and sample drilldown columns',
+        'GreptimeTraceQueryRepository selects first_seen and last_seen service graph timestamps and EntityTopologyGraphInfo carries them into topology edge evidence',
+        'TraceCallTopologyQueryService consumes Greptime service graph aggregation rows with seed service names pushed into storage before falling back to bounded raw trace-row topology discovery',
+        'TraceCallTopologyQueryService bounds Greptime service graph aggregation latency and falls back to raw trace-row topology discovery when aggregation fails or times out',
+        '/topology marks trace-call API degradation as Greptime trace graph unavailable instead of generic topology API empty evidence',
+        'Topology edge signal drilldowns prefer the selected trace-call edge sample trace/span context and preserve it in returnTo links',
+        'buildTopologyApiUrl forwards depth, relation type, hide-internal, and pagination scope into the topology API query',
+        'buildTopologyApiUrl defaults service-call and otlp-trace-call reads to relationType=trace-call so trace views do not fetch node-only relation graphs',
+        '/topology exposes Browser-readable topology API request scope metadata including resolved relationType=trace-call for service-call routes',
+        '/topology passes resolved relationType=trace-call into shared loading and empty states for inferred service-call routes',
+        'TopologyController and EntityTopologyQueryService accept backend relation type, hide-internal, and edge pagination scope for API-backed topology graphs',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbenchFrame, HzTopologyWorkbenchHeader, and HzTopologyWorkbenchGrid for the route shell, title/scope/source header, and canvas/companion layout',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbenchFrame boundary variants for route and section topology shells',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbenchSlot for canvas and companion grid ownership',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbench shell header, slot, grid, and boundary ownership',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbenchHeader operational-compact density with assistive copy so topology starts as a tool, not an explanation page',
+        '/topology reuses HzTopologyWorkbenchFrame, HzTopologyWorkbenchHeader, and HzTopologyWorkbenchGrid instead of page-local route shell and grid chrome',
+        '/topology reuses HzTopologyWorkbenchSlot instead of page-local workbench grid child wrappers',
+        '/topology reuses HzTopologyWorkbenchFrame boundary variants instead of page-local route shell borders',
+        '/topology reuses HzTopologyWorkbench shell header, slot, grid, and boundary ownership for the topology workbench',
+        '/topology uses shared HzTopologyWorkbenchHeader operational-compact density and assistive copy instead of page-local explanatory header chrome',
+        '/topology uses a concise operational header with label-only source controls so the graph leads the first viewport instead of explanatory copy',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar graph-first single-row overflow layout so scope controls do not push the G6 graph down',
+        '/topology reuses HzTopologyToolbar graph-first single-row overflow layout for environment, search, depth, layout, group, fit, locate, and reset controls',
+        '/topology reuses the shared HzTopologyFilterStrip source-rail variant so source controls prefer one compact row above the G6 graph',
+        '/topology reuses the shared HzTopologyFocusTrail rail density so focused context stays compact above the G6 graph',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFocusTrail graph-dock density with document-flow, low-interruption, non-occluding focus context',
+        '/topology reuses HzTopologyFocusTrail graph-dock density so focused breadcrumbs and exit affordance stay visible without covering the G6 graph',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFocusTrail mode, depth, entity, and scoped exit href metadata for focused 1-hop graph handoffs',
+        '/topology maps focused 1-hop G6 routes into HzTopologyFocusTrail metadata and exits focus without losing environment/time/source/group scope',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvas for compact topology graph surfaces with layout and interaction markers',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvas interaction state for hover neighbor highlighting, focused depth, and drawer affordance',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvas boundary and interaction-scope variants for route and section graph surfaces',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvasAnnotation for canvas layout and depth status overlays',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvasAnnotation assistive non-occluding mode so canvas status does not cover G6 graph interactions',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas for AntV G6 topology rendering with compact dark RED styling',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas drawer-sync node and edge selection metadata with hover neighbor highlighting',
+        '@hertzbeat/ui widens HzTopologyG6Canvas edge pointer hit targets so real mouse hover and click can open relationship evidence drawers',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas neighbor focus semantics for upstream, downstream, selected, and dimmed graph elements',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas pointer hover state for node and edge neighbor highlighting',
+        '@hertzbeat/ui exposes HzTopologyG6Canvas hover callbacks so /topology can drive live hover evidence tooltips from real G6 pointer state',
+        '@hertzbeat/ui clears HzTopologyG6Canvas live hover state at the canvas boundary so edge evidence does not stick after pointer exit',
+        '@hertzbeat/ui centers HzTopologyG6Canvas after fit and reset controls so the real graph returns to a readable viewport',
+        '@hertzbeat/ui preserves HzTopologyG6Canvas wheel and pan zoom while hover or selection styling redraws graph data without auto-fit',
+        '@hertzbeat/ui preserves HzTopologyG6Canvas zoom and position around same-structure hover and selection redraws',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas live viewport telemetry for Browser zoom and pan verification',
+        '@hertzbeat/ui exposes HzTopologyG6Canvas short viewport zoom and position aliases for Browser interaction regression checks',
+        '@hertzbeat/ui and /ui-lab publish HzTopologyG6Canvas zoom, fit, and reset telemetry after viewport actions settle',
+        '@hertzbeat/ui and /ui-lab expose HzTopologyG6Canvas viewport command bridge for Browser-verifiable zoom and reset actions',
+        '@hertzbeat/ui and /ui-lab route HzTopologyG6Canvas wheel zoom through manual clamped G6 zoomTo control so wheel input cannot over-shrink and drift',
+        '@hertzbeat/ui skips identical HzTopologyG6Canvas render keys and treats blank canvas hover-clear as a no-op without active hover evidence',
+        '@hertzbeat/ui and /ui-lab expose HzTopologyG6Canvas keyboard viewport shortcuts for zoom, fit, and reset when pointer hit testing is blocked',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas non-occluding viewport overview/minimap metadata for scale and zoom orientation',
+        '@hertzbeat/ui keeps HzTopologyG6Canvas live hover metadata out of G6 data redraws so real mouse hover cannot trigger setData/draw viewport resets',
+        '@hertzbeat/ui disables G6 hover-activate state mutation so node border hover cannot move a zoomed viewport',
+        '@hertzbeat/ui makes HzTopologyG6Canvas selected-node viewport focusing explicit-action-only so continuous mouse paths across canvas blank space cannot refocus an operator-zoomed graph',
+        '@hertzbeat/ui guards HzTopologyG6Canvas search-node auto-focus after operator zoom so hover clear cannot refocus and reset an adjusted viewport',
+        '@hertzbeat/ui keeps HzTopologyG6Canvas live hover callbacks out of the G6 mount lifecycle so node border hover cannot remount and refit the graph',
+        '@hertzbeat/ui cancels HzTopologyG6Canvas delayed initial fit when an operator starts wheel zooming or panning',
+        '@hertzbeat/ui disables continuous AntV G6 autoFit after the initial fit pass so wheel zoom is not reset by redraws',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas 50/200/500 node scale fixtures and scale-tier metadata',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas large-graph grouping, filtering, and table-companion strategy metadata',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas render-window budgeting so stress graphs cap primary G6 render data and require table companions',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas render-window priority nodes for search only so ordinary node, edge, and table selection cannot reorder stress graph windows',
+        '@hertzbeat/ui and /ui-lab expose HzTopologyG6Canvas mount-lifecycle metadata so Browser can verify dense graph selection redraws do not remount or refit the canvas',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas search-to-focus controls for graph result highlighting',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas selected-node focus controls for retained drawer context',
+        '/ui-lab demonstrates HzTopologyG6Canvas visible node and edge click selection as in-page state so Browser can verify no URL navigation or remount on synthetic graphs',
+        '/ui-lab exposes a 500-node G6 scale Browser regression anchor for zoom, hover, selection, and table-companion stability checks',
+        '/ui-lab Browser-verifies the 500-node G6 scale anchor keeps URL, render window, zoom, and viewport position stable while table filters and hidden-edge row selection update in-page state',
+        '/ui-lab wires the 500-node render-window metric table rows to actual scale graph edge ids so Browser table selection can inspect partial and hidden graph edges without route changes',
+        '@hertzbeat/ui exposes HzTopologyG6Canvas toolbar action owner, state, target, and behavior metadata for zoom, fit, reset, and focus controls',
+        '@hertzbeat/ui clears HzTopologyG6Canvas live hover state before search or selected-node focus controls recenter the graph',
+        '@hertzbeat/ui and /ui-lab publish HzTopologyG6Canvas explicit focus-action telemetry so Browser can distinguish intentional search or selected-node recentering from hover or selection drift',
+        '@hertzbeat/ui clears HzTopologyG6Canvas live hover state before node or edge drawer selection callbacks run',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas node double-click focus entry for focused 1-hop inspect mode',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas filter-scope metadata for environment, source kind, search, grouping, and visible graph counts',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas filter controls for source, grouping, search, and reset scope actions',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas visual group summary for active group-by scopes',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas non-occluding overlay mode so graph summaries, groups, and filters do not cover the real graph',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas actionable group-summary chips for source-kind scope changes',
+        '/topology reuses HzTopologyG6Canvas as the primary graph renderer instead of hand-rolled SVG/DOM nodes and edges',
+        '/topology wires HzTopologyG6Canvas node and edge clicks into in-page drawer selection state without URL navigation or route remounts',
+        '/topology exposes node-click, edge-click, and table-row-click selection source markers so Browser can distinguish real in-page inspection from route initial state',
+        '/topology clears fallback selected-edge context during node-click inspection so the G6 canvas and drawer do not retain stale edge state after ordinary node selection',
+        '/topology Browser-verifies real G6 edge click opens the relationship drawer in-page without URL navigation, remount, zoom reset, or viewport drift',
+        '/topology Browser-verifies the final M5 route matrix across default, focused, service-call, and empty routes with API-owned graph or empty states and no static fallback',
+        '/topology forwards topology search context into HzTopologyG6Canvas search focus metadata',
+        '/topology reuses HzTopologyG6Canvas selected-node focus action for retained investigation context',
+        '/topology routes HzTopologyG6Canvas double-click focus entry through a soft explicit focus navigation path so ordinary inspection cannot trigger full-page reloads',
+        '/topology keeps the previous API-backed G6 graph mounted during explicit soft focus route transitions until the focused API read resolves',
+        '/topology maps HzTopologyG6Canvas node double-click focus entry to a depth=1 focused graph URL while preserving normal click drawer selection',
+        '/topology routes focus-trail exit through the same soft route path so leaving 1-hop focus does not blank the G6 canvas',
+        '/topology focus exit preserves the selected edgeId so relationship drawer context survives leaving 1-hop mode',
+        '/topology routes toolbar environment, depth, group, and reset scope changes through soft navigation while preserving the previous G6 graph during API reads',
+        '/topology Browser-verifies toolbar depth and group scope changes keep the previous G6 graph mounted during soft route reads while preserving timeRange and sourceKind context',
+        '/topology keeps toolbar layout changes inside the G6 canvas without URL navigation or API graph clearing',
+        '/topology keeps toolbar search changes inside the G6 canvas without URL navigation or API graph clearing',
+        '/topology forwards environment, source, search, and group scope into HzTopologyG6Canvas filter-scope metadata',
+        '/topology reuses HzTopologyG6Canvas filter controls for source, grouping, search, and reset scope actions',
+        '/topology forwards groupBy route scope into HzTopologyG6Canvas grouping metadata and controls',
+        '/topology reuses HzTopologyG6Canvas visual group summary for groupBy route scopes',
+        '/topology uses HzTopologyG6Canvas non-occluding overlay mode so summary, grouping, and filter surfaces stay assistive while zoom controls remain visible',
+        '/topology reuses HzTopologyG6Canvas actionable group-summary chips while preserving groupBy route scope',
+        '/topology preserves groupBy route scope across source and view mode filter strips',
+        '/topology exposes selected node drawer group/source/view context for G6 node click verification',
+        '/topology drives hover tooltip and path summary from live HzTopologyG6Canvas edge hover state before falling back to selected edge evidence',
+        '/topology renders the fixed hover evidence tooltip only for live G6 edge hover so default selected edges do not cover canvas controls',
+        '/topology relies on shared HzTopologyG6Canvas boundary hover clear callbacks before falling back to selected edge evidence',
+        '/topology relies on shared HzTopologyG6Canvas viewport preservation so real graph wheel zoom is not immediately reset by hover or selection updates',
+        '/topology relies on shared HzTopologyG6Canvas zoom-position redraw preservation so mouse hover does not reset an operator-adjusted graph',
+        '/topology relies on shared HzTopologyG6Canvas hover highlight-only viewport behavior so node hover does not recenter an operator-zoomed graph',
+        '/topology relies on shared HzTopologyG6Canvas metadata-only hover updates so real node hover cannot redraw and reset an operator-zoomed graph',
+        '/topology relies on shared HzTopologyG6Canvas explicit selected-node focus actions so hover-clear canvas transitions do not recenter an operator-zoomed graph',
+        '/topology relies on shared HzTopologyG6Canvas search auto-focus guard so entity/search focus does not recenter after an operator zooms then hovers a node',
+        '/topology relies on shared HzTopologyG6Canvas stable hover callback refs so node border hover cannot remount and refit an operator-zoomed graph',
+        '/topology exposes shared HzTopologyG6Canvas live viewport telemetry so Browser checks can read zoom and position directly',
+        '/topology relies on shared HzTopologyG6Canvas post-action viewport telemetry timing for zoom verification',
+        '/topology exposes shared HzTopologyG6Canvas viewport command bridge so Browser can verify zoom without relying on flaky pointer dispatch',
+        '/topology routes wheel zoom through shared HzTopologyG6Canvas manual clamped G6 zoomTo control instead of page-local wheel handlers',
+        '/topology relies on shared HzTopologyG6Canvas identical-render-key redraw skip and blank hover-clear no-op behavior so canvas boundary movement cannot drift a zoomed graph',
+        '/topology exposes the shared HzTopologyG6Canvas render-window no-selection-reorder contract so ordinary node, edge, and table selection cannot reorder dense graph windows',
+        '/topology memoizes the HzTopologyG6Canvas graph input so live hover state does not recreate graph data and redraw the G6 viewport',
+        '/topology reuses shared HzTopologyG6Canvas keyboard viewport shortcuts for zoom, fit, and reset without page-local handlers',
+        '/topology exposes shared HzTopologyG6Canvas non-occluding viewport overview/minimap metadata without covering graph controls',
+        '/topology relies on shared HzTopologyG6Canvas initial-fit cancellation so first wheel zoom is not pulled back by delayed fit',
+        '/topology exposes shared HzTopologyG6Canvas initial-only auto-fit metadata for Browser wheel-zoom verification',
+        '/topology exposes shared HzTopologyG6Canvas mount-lifecycle Browser regression markers so node, edge, and table selection can be verified without URL changes or remounts',
+        '/topology clears live G6 hover evidence before RED metric table row selection navigates to edge detail',
+        '/topology shows shared missing-evidence state when otlp-trace-call has no real trace edges instead of presenting a node-only graph',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyGraphLayer for SVG edge layers inside topology canvases',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvas layout, interaction, boundary, annotation, and graph-layer ownership',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState for API-empty/degraded topology evidence states',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState boundary variants for companion and canvas empty states',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState title, copy, source, time-scope, meta, and boundary ownership',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState scope identity for environment, source kind, relation type, focus entity, depth, result count, and evidence sources',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState degraded topology API state without page-local fallback chrome',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyLoadingState for API-pending topology evidence states',
+        '/topology reuses HzTopologyEmptyState for API-owned empty graph states instead of page-local empty chrome',
+        '/topology reuses HzTopologyEmptyState boundary variants instead of page-local canvas empty-state border chrome',
+        '/topology reuses HzTopologyEmptyState title, copy, source, time-scope, meta, and boundary ownership for API empty states',
+        '/topology passes API empty topology scope identity into HzTopologyEmptyState for empty/degraded browser verification',
+        '/topology renders API unavailable topology as degraded shared HzTopologyEmptyState instead of ordinary empty results',
+        '/topology renders API pending topology as shared HzTopologyLoadingState instead of ordinary empty results',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable as a RED-ranked topology companion table for large graph investigation',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable row-select detail affordance for keyboard/open-edge investigation',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable boundary variants for companion and framed topology surfaces',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable header, row, route, RED cell, badge, and action ownership',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable graph-first density so RED rankings stay low-interruption beside the G6 graph',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable render-window companion metadata so hidden stress-graph nodes stay reachable through RED table investigation',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable row-level render-window visibility for visible, partial, and hidden large-graph relationships',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable render-window row count summaries for visible, partial, hidden, and unknown relationship filters',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable in-page render-window filters for visible, partial, hidden, and unknown relationship slices',
+        '/topology reuses HzTopologyMetricTable beside the graph for I18N-backed RED edge ranking and selected-edge alignment',
+        '/topology reuses HzTopologyMetricTable row action affordance for selected-edge detail alignment',
+        '/topology reuses HzTopologyMetricTable boundary variants instead of page-local metric table border chrome',
+        '/topology reuses HzTopologyMetricTable header, row, route, RED cell, badge, and action ownership for edge ranking',
+        '/topology reuses HzTopologyMetricTable graph-first density for compact companion RED ranking',
+        '/topology wires HzTopologyMetricTable render-window companion metadata from the same G6 graph so hidden nodes keep table investigation context',
+        '/topology marks HzTopologyMetricTable rows with source and target visibility from the current G6 render window instead of losing hidden-edge context',
+        '/topology exposes HzTopologyMetricTable render-window row count summaries for future visible, partial, and hidden relationship filters',
+        '/topology reuses HzTopologyMetricTable in-page render-window filters so ordinary RED table filtering does not navigate or remount the G6 canvas',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar for compact topology filters, search, actions, and scope summary',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar boundary variants for route and section toolbars',
+        '/topology reuses HzTopologyToolbar instead of page-local Input/Select/button toolbar chrome',
+        '/topology reuses HzTopologyToolbar boundary variants instead of page-local toolbar borders',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar state strip for focus, depth, layout, and grouping context',
+        '/topology reuses HzTopologyToolbar state strip for focus/depth/layout/grouping instead of page-local state chips',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar control, summary, and state-item ownership',
+        '/topology reuses HzTopologyToolbar control, summary, and state-item ownership for topology filtering',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar graph-first density with assistive secondary summary/state rows so the G6 canvas leads the first viewport',
+        '/topology reuses HzTopologyToolbar graph-first density so secondary toolbar context does not push the real G6 graph below the fold',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar depth, layout, group-by, and reset controls as shared scope controls',
+        '/topology reuses HzTopologyToolbar depth, layout, group-by, and reset controls for topology scope changes',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail for shared legend, metric table, timeline, and detail drawer layout spacing',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail boundary variants for side and stacked companion surfaces',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail boundary, spacing, and content ownership for companion surfaces',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail graph-first priority so companion evidence stays low-interruption beside the G6 graph',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail contained scroll so long companion evidence stays within the graph-height viewport',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail sticky first-section context so contained companion evidence keeps orientation while scrolling',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail jump-list sticky context so quick jumps own contained companion orientation',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionSection anchor ownership for legend, RED table, timeline, and drawer blocks',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionSection collapsible companion sections for long graph-first right rails',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionJumpList anchor navigation for graph-first right-rail evidence sections',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionJumpList contained-rail scroll handoff so anchor jumps do not move the page',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionJumpList active-section highlighting from contained rail scrolling',
+        '/topology reuses HzTopologyCompanionRail for table/detail companion layout instead of page-local margin wrappers',
+        '/topology reuses HzTopologyCompanionRail boundary variants instead of page-local companion borders',
+        '/topology reuses HzTopologyCompanionRail boundary, spacing, and content ownership for the legend, metrics, timeline, and detail rail',
+        '/topology reuses HzTopologyCompanionRail graph-first priority so the real G6 graph remains the primary first-viewport surface',
+        '/topology reuses HzTopologyCompanionRail contained scroll so the right rail no longer stretches the whole page below the G6 graph',
+        '/topology reuses HzTopologyCompanionRail sticky first-section context so the right rail keeps orientation during internal scroll',
+        '/topology reuses HzTopologyCompanionRail jump-list sticky context with HzTopologyCompanionJumpList as the top orientation control',
+        '/topology reuses HzTopologyCompanionSection anchors for legend, RED table, timeline, and drawer blocks instead of page-local section wrappers',
+        '/topology reuses HzTopologyCompanionSection collapsible controls so long RED/timeline/drawer evidence stays compact',
+        '/topology reuses HzTopologyCompanionJumpList for compact right-rail evidence jumps instead of page-local anchor chips',
+        '/topology reuses HzTopologyCompanionJumpList contained-rail scroll handoff so right-rail jumps keep the G6 canvas fixed',
+        '/topology reuses HzTopologyCompanionJumpList active-section highlighting so the right rail shows the current evidence section',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologySectionLabel for compact topology companion section headings',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologySectionLabel label and text ownership for companion headings',
+        '/topology reuses HzTopologySectionLabel for companion section headings instead of page-local label typography',
+        '/topology reuses HzTopologySectionLabel label and text ownership for companion headings',
+        '/topology reuses HzTopologyCanvas instead of page-local canvas surface chrome',
+        '/topology reuses HzTopologyCanvas interaction state for hover/focus/drawer affordances instead of page-local interaction hints',
+        '/topology reuses HzTopologyCanvas boundary and interaction-scope variants instead of page-local border or hover group classes',
+        '/topology reuses HzTopologyCanvasAnnotation instead of page-local canvas annotation chrome',
+        '/topology renders HzTopologyCanvasAnnotation in assistive non-occluding mode so the G6 graph remains visually and interactively clear',
+        '/topology reuses HzTopologyCanvas layout, interaction, boundary, annotation, and graph-layer ownership for the topology graph surface',
+        '/topology reuses HzTopologyGraphLayer instead of page-local SVG graph layer chrome',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyNode for typed health/focus/evidence topology nodes',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyNode label, health, RED metric, and evidence badge ownership',
+        '/topology reuses HzTopologyNode instead of page-local node tone/focus chrome',
+        '/topology reuses HzTopologyNode label, health, RED metric, and evidence badge ownership for graph nodes',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEdge for directional RED/evidence relationship rendering',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEdge line, arrow, drilldown, hit-target, RED, and badge ownership',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip for edge hover source/target/RED/last-seen/sample-trace evidence',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip placement and sizing for canvas hover evidence',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip live-edge-hover trigger ownership so hover evidence is not a fixed canvas overlay',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip toolbar-safe canvas placement so hover evidence does not cover G6 controls',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip cursor-anchored placement from G6 pointer events so hover evidence stays near the edge instead of fixed over controls',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip canvas-clamped cursor anchors so hover evidence cannot overflow or cover edge toolbar controls',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip header, fact, metric, and badge ownership for investigation hover evidence',
+        '/topology reuses HzTopologyEdge instead of page-local edge color and drilldown-dot chrome',
+        '/topology reuses HzTopologyEdge line, arrow, drilldown, hit-target, RED, and badge ownership for relationships',
+        '/topology reuses HzTopologyHoverTooltip for hover evidence instead of page-local tooltip or title attributes',
+        '/topology reuses HzTopologyHoverTooltip placement and sizing instead of page-local absolute tooltip chrome',
+        '/topology passes HzTopologyHoverTooltip live-edge-hover trigger only from live G6 edge hover state',
+        '/topology positions HzTopologyHoverTooltip below the G6 toolbar controls to keep fit, zoom, and focus actions visible during hover',
+        '/topology anchors live HzTopologyHoverTooltip placement from HzTopologyG6Canvas pointer coordinates instead of fixed page-local hover chrome',
+        '/topology relies on shared HzTopologyHoverTooltip canvas-clamped cursor anchors for live G6 hover evidence',
+        '/topology reuses HzTopologyHoverTooltip header, fact, metric, and badge ownership for edge hover evidence',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyLegend for health, source-kind, confidence, and stale evidence semantics',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyLegend boundary variants for companion and framed topology surfaces',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyLegend header, section, item, swatch, and boundary ownership for topology semantics',
+        '/topology reuses HzTopologyLegend instead of page-local legend/source semantics',
+        '/topology reuses HzTopologyLegend boundary variants instead of page-local legend border chrome',
+        '/topology reuses HzTopologyLegend boundary owner for framed health/source/confidence semantics',
+        '/topology reuses HzTopologyLegend header, section, item, swatch, and boundary ownership for health/source/confidence semantics',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer for node/edge evidence drawers and cross-signal handoffs',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer surface variants for companion and framed topology drawers',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer surface ownership for drawer border and padding chrome',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer header ownership for eyebrow/title/subtitle hierarchy',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer boundary context markers for roadmap/evidence copy',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer boundary ownership for retained context copy',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer boundary copy ownership for retained context text',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer fact group ownership for RED and identity facts',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer fact item ownership for compact RED and identity rows',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer fact label, value, and meta ownership',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer graph-first density so node and edge drawers stay compact beside the G6 graph',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer action group ownership for entity/alert and signal handoffs',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer action link and label ownership for entity/alert handoffs',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer signal action link and label ownership for cross-signal handoffs',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer action copy ownership for retained context helper text',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer signal label ownership for cross-signal handoff headings',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer identity ownership for selected edge and current node drawer context',
+        '/topology reuses HzTopologyDetailDrawer for selected edge evidence instead of page-local evidence panel chrome',
+        '/topology reuses HzTopologyDetailDrawer for current entity evidence instead of page-local current entity panel chrome',
+        '/topology reuses HzTopologyDetailDrawer surface variants instead of page-local drawer border chrome',
+        '/topology reuses HzTopologyDetailDrawer surface ownership for selected-edge and current-entity drawer chrome',
+        '/topology reuses HzTopologyDetailDrawer header ownership for edge and current-entity hierarchy',
+        '/topology reuses HzTopologyDetailDrawer boundary owner for edge and current-entity context copy',
+        '/topology reuses HzTopologyDetailDrawer boundary ownership for edge and current-entity context copy',
+        '/topology reuses HzTopologyDetailDrawer boundary copy ownership for edge and current-entity context text',
+        '/topology reuses HzTopologyDetailDrawer fact group ownership for edge and current-entity RED facts',
+        '/topology reuses HzTopologyDetailDrawer fact item ownership for edge and current-entity RED facts',
+        '/topology reuses HzTopologyDetailDrawer fact label, value, and meta ownership for edge and current-entity facts',
+        '/topology reuses HzTopologyDetailDrawer graph-first density for selected-edge and current-node evidence drawers',
+        '/topology reuses HzTopologyDetailDrawer action group ownership for edge and current-entity handoffs',
+        '/topology reuses HzTopologyDetailDrawer action link and label ownership for edge and current-entity handoffs',
+        '/topology reuses HzTopologyDetailDrawer signal action link and label ownership for edge and current-entity handoffs',
+        '/topology reuses HzTopologyDetailDrawer action copy ownership for selected-edge retained-context helper text',
+        '/topology reuses HzTopologyDetailDrawer signal label ownership for current-entity cross-signal headings',
+        '/topology passes selected edge and current node identity into HzTopologyDetailDrawer for drawer verification',
+        '/topology passes current node upstream and downstream dependency evidence into HzTopologyDetailDrawer for node-click investigation',
+        '/topology annotates current node entity and signal handoffs with selected G6 node context for retained evidence drilldowns',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList for compact fault context and impact timeline evidence',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList boundary variants for toolbar context and companion timeline surfaces',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList boundary ownership for fault context and impact timeline surfaces',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList header and item ownership for compact evidence rows',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList title, copy, count, item label, value, and meta ownership for compact evidence rows',
+        '/topology reuses HzTopologyEvidenceList for incoming fault context and API impact timeline panels',
+        '/topology reuses HzTopologyEvidenceList boundary variants instead of page-local fault context border and padding chrome',
+        '/topology reuses HzTopologyEvidenceList companion timeline boundary for API impact timeline panels',
+        '/topology reuses HzTopologyEvidenceList boundary ownership for incoming fault context and API impact timeline panels',
+        '/topology reuses HzTopologyEvidenceList header and item ownership for incoming fault context and API impact timeline rows',
+        '/topology reuses HzTopologyEvidenceList title, copy, count, item label, value, and meta ownership for incoming fault context and API impact timeline rows',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip for compact source and focus filter controls',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip boundary variants for section and companion filter strips',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip boundary and item ownership for compact filter controls',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip label and copy ownership for source and view controls',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip assistive copy visibility for compact view controls',
+        '/topology reuses HzTopologyFilterStrip for source and view-mode controls instead of page-local filter cards',
+        '/topology reuses HzTopologyFilterStrip boundary variants instead of page-local filter strip borders and padding',
+        '/topology reuses HzTopologyFilterStrip boundary and item ownership for source and view-mode controls',
+        '/topology reuses HzTopologyFilterStrip label and copy ownership for source and view-mode controls',
+        '/topology uses shared HzTopologyFilterStrip assistive copy visibility for view-mode controls so companion options stay label-first',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyActionLink for standalone topology investigation actions',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyActionLink spacing variants for companion and inset topology actions',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyActionLink label, copy, and spacing ownership for topology investigation actions',
+        '/topology reuses HzTopologyActionLink for alert-impact closure instead of page-local action link chrome',
+        '/topology reuses HzTopologyActionLink spacing variants instead of page-local action margins',
+        '/topology reuses HzTopologyActionLink label, copy, and spacing ownership for alert-impact closure',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFocusTrail for focused graph breadcrumbs, active filters, hidden counts, and exit-focus actions',
+        '/topology reuses HzTopologyFocusTrail for focused context instead of toolbar-only local state chips',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyGroupPanel for group-by summaries, collapsed cluster counts, worst health, and large graph actions',
+        '/topology reuses HzTopologyGroupPanel for large graph grouping context instead of page-local group chips',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyPathSummary for selected edge direction, RED facts, evidence badges, and drilldown actions',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyPathSummary interaction identity for selected edge, hovered edge, source, target, relation type, and source kind',
+        '/topology reuses HzTopologyPathSummary for selected path context instead of page-local hover-only edge hints',
+        '/topology passes selected path interaction identity into HzTopologyPathSummary for edge hover, source/target, relation type, and source kind evidence',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyScopeBar for compact time, environment, and refresh scope controls',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyScopeBar boundary variants for header and section scope rows',
+        '@hertzbeat/ui and /ui-lab demonstrate HzTopologyScopeBar item, value, action, and boundary ownership for scope rows',
+        '/topology reuses HzTopologyScopeBar for header scope controls instead of page-local chips and refresh chrome',
+        '/topology reuses HzTopologyScopeBar boundary variants instead of page-local scope borders and padding',
+        '/topology reuses HzTopologyScopeBar item, value, action, and boundary ownership for header scope controls'
+      ]),
+      sourceFiles: expect.arrayContaining([
+        'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/TopologyController.java',
+        'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java',
+        'web-next/packages/hertzbeat-ui/src/index.tsx',
+        'web-next/app/ui-lab/page.tsx',
+        'web-next/lib/topology-surface/controller.ts'
+      ])
+    });
+    expect(audit.gaps.find(gap => gap.code === 'greptime-otlp-query-pushdown')).toMatchObject({
+      backendApiStatus: 'partial',
+      evidence: expect.arrayContaining([
+        'EntityTraceQueryServiceImpl builds entity trace summary/hints from storage-owned recent rows scoped by lookback time, workspace, and canonical entity identity',
+        'GreptimeTraceQueryRepository owns service-to-service trace graph RED SQL aggregation with seed service source/target scope for topology',
+        'GreptimeTraceQueryRepository owns grouped trace-list SQL with total_count, LIMIT, and OFFSET for paged list views',
+        'GreptimeTraceQueryRepository owns trace-id row SQL with time, service, environment, workspace, entity identity, and internal-service filters when route/detail context is present',
+        'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples'
+      ]),
+      sourceFiles: expect.arrayContaining([
+        'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java',
+        'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java'
+      ])
+    });
+
+  });
+
+  it('keeps release closure blocked while tracked 2.0 gaps remain', () => {
+    const audit = buildHertzBeat2GapAudit();
+    const gate = validateHertzBeat2ReleaseGate(audit);
+
+    expect(audit.releaseBlocked).toBe(true);
+    expect(gate.valid).toBe(false);
+    expect(gate.issues).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ code: 'release-blocked-gaps' }),
+        expect.objectContaining({ code: 'greptime-otlp-query-pushdown' })
+      ])
+    );
+    expect(gate.issues).not.toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ code: 'topology-real-data-api' }),
+        expect.objectContaining({ code: 'incident-workbench-domain' })
+      ])
+    );
+  });
+
+  it('records the strict UI reuse source audit for M10 observability workbench surfaces', () => {
+    const audit = buildHertzBeat2GapAudit();
+
+    expect(audit.gaps.find(gap => gap.code === 'ui-i18n-reuse-sweep')).toMatchObject({
+      releaseBlocking: true,
+      evidence: expect.arrayContaining([
+        'frontend-convergence-contract enforces UI Lab backed @hertzbeat/ui ownership for actions, explorer, incidents, and topology workbench surfaces',
+        'frontend-convergence-contract enforces UI Lab backed HzInput, HzSelect, HzButton, and HzControlStack ownership for /log/manage and /trace/manage query controls',
+        'frontend-convergence-contract enforces UI Lab backed HzControlStack ownership for /trace/manage time-control rail',
+        'frontend-convergence-contract enforces UI Lab backed TimeRangeControl narrow-rail alignment ownership for /trace/manage time control',
+        'frontend-convergence-contract enforces UI Lab backed HzControlStack ownership for /log/manage detail action stacks',
+        'frontend-convergence-contract enforces UI Lab backed HzScrollViewport ownership for /log/manage stream viewport',
+        'frontend-convergence-contract enforces UI Lab backed HzLogStreamLiveRow ownership for /log/manage stream rows',
+        'frontend-convergence-contract enforces UI Lab backed HzDetailAside ownership for /log/manage selected-detail aside rail',
+        'frontend-convergence-contract enforces UI Lab backed HzDetailBodyStack ownership for /log/manage selected-detail body spacing',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout and HzActionGroup ownership for /log/manage stream/history view switch layout',
+        'frontend-convergence-contract enforces Link-compatible HzButtonLink ownership for /log/manage and /trace/manage header action rows',
+        'frontend-convergence-contract enforces HzButton ownership for /log/manage stream reconnect, pause, and clear controls',
+        'frontend-convergence-contract enforces HzButton ownership for /log/manage stream/history view toggle controls',
+        'frontend-convergence-contract enforces HzButton ownership for /log/manage and /trace/manage table row detail controls',
+        'frontend-convergence-contract enforces UI Lab backed HzTableRowActionButton ownership for /trace/manage root-span row action density',
+        'frontend-convergence-contract enforces UI Lab backed HzDisabledActionShell ownership for /trace/manage disabled handoff action wrappers',
+        'frontend-convergence-contract enforces UI Lab backed HzDialogEventText ownership for /trace/manage event-detail copy and meta text roles',
+        'frontend-convergence-contract enforces UI Lab backed HzStatCell compact density ownership for /trace/manage drawer stage stats',
+        'frontend-convergence-contract enforces HzButton ownership for /log/manage and /trace/manage event-detail view-span controls',
+        'frontend-convergence-contract enforces UI Lab backed HzActionGroup full-end layout ownership for /trace/manage header and drawer action rows',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage and /trace/manage header and query panel shells',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage and /trace/manage chart panel shells',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage and /trace/manage table panel shells',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface clip ownership for /trace/manage table/detail panel overflow',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface query padding ownership for /trace/manage query panel density',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface header padding ownership for /trace/manage header panel density',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface chart padding ownership for /trace/manage chart panel density',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage and /trace/manage detail panel shells',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage header layout',
+        'frontend-convergence-contract enforces UI Lab backed HzSignalWorkbenchShell ownership for /trace/manage page shell and content width',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchHeaderCopy ownership for /trace/manage header copy typography',
+        'frontend-convergence-contract enforces UI Lab backed HzSearchFieldFrame ownership for /trace/manage query search field',
+        'frontend-convergence-contract enforces UI Lab backed HzSearchFieldIcon ownership for /trace/manage query search field icon',
+        'frontend-convergence-contract enforces UI Lab backed HzInput search-icon inset ownership for /trace/manage query search input',
+        'frontend-convergence-contract enforces UI Lab backed HzQueryTokenField ownership for /trace/manage trace/span token inputs',
+        'frontend-convergence-contract enforces UI Lab backed HzQueryStatusSelect ownership for /trace/manage status select control',
+        'frontend-convergence-contract enforces UI Lab backed HzQueryActionGroup ownership for /trace/manage run/reset query actions',
+        'frontend-convergence-contract enforces UI Lab backed HzButtonIcon ownership for /trace/manage run/reset query action icons',
+        'frontend-convergence-contract enforces UI Lab backed HzButtonIcon ownership for /trace/manage header handoff action icons',
+        'frontend-convergence-contract enforces UI Lab backed HzButtonIcon ownership for /trace/manage detail-footer handoff action icons',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage time-toolbar layout',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage chart summary/trend layout',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage table/detail layout',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage detail body layout',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage detail footer layout',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelHeader ownership for /log/manage and /trace/manage table panel header bars',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelHeader ownership for /log/manage and /trace/manage detail panel header bars',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage stream view switch and stream stage panel shells',
+        'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /log/manage stream-stage split layout',
+        'frontend-convergence-contract enforces UI Lab backed HzPanelHeader ownership for /log/manage stream stage and selected-log section headers',
+        'frontend-convergence-contract enforces UI Lab backed HzStateNotice ownership for /log/manage stream pause and selected-log helper notices',
+        'frontend-convergence-contract blocks page-local actionClass button-style tokens from /log/manage and /trace/manage',
+        'frontend-convergence-contract enforces UI Lab backed HzStatCell ownership for /log/manage and /trace/manage summary stat tiles',
+        'frontend-convergence-contract enforces UI Lab backed HzStatStrip and HzStatCell ownership for log related trace and trace waterfall drawer stage stat strips',
+        'frontend-convergence-contract enforces UI Lab backed HzSignalTrendBars ownership for /log/manage and /trace/manage signal trend bars',
+        'frontend-convergence-contract enforces UI Lab backed HzDetailRows ownership for /log/manage and /trace/manage detail facts, selected evidence, and entity context rows',
+        'frontend-convergence-contract enforces UI Lab backed HzDetailRows ownership for log stream detail, related trace preview, and trace waterfall drawer selected facts',
+        'frontend-convergence-contract enforces UI Lab backed HzAttributeDiagnostics ownership for /log/manage and /trace/manage attribution diagnostic containers',
+        'frontend-convergence-contract enforces UI Lab backed HzAttributeDiagnostics ownership for log stream detail dialog attribution diagnostics',
+        'frontend-convergence-contract enforces UI Lab backed HzEmptyState ownership for /log/manage stream/list and /trace/manage table empty states',
+        'frontend-convergence-contract enforces UI Lab backed HzEmptyState table-panel layout for /trace/manage table empty-state density',
+        'frontend-convergence-contract enforces UI Lab backed HzDataTable ownership for /log/manage and /trace/manage result table chrome',
+        'frontend-convergence-contract enforces UI Lab backed HzDataCellText ownership for /trace/manage table cell typography',
+        'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /log/manage and /trace/manage table status chips',
+        'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /trace/manage table count chip',
+        'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /log/manage stream severity chips',
+      'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /log/manage stream status, count, and live chips',
+      'frontend-convergence-contract enforces UI Lab backed HzChipGroup ownership for log dialog toolbar and trace drawer metadata chip groups',
+      'frontend-convergence-contract enforces UI Lab backed HzDialogMetaItem ownership for trace waterfall drawer metadata item sizing',
+      'frontend-convergence-contract enforces UI Lab backed HzActionGroup ownership for log dialog, trace route header, and trace drawer action groups',
+        'frontend-convergence-contract enforces UI Lab backed HzDialogBodyLayout ownership for log dialog and trace waterfall drawer body layouts',
+        'frontend-convergence-contract enforces UI Lab backed HzInlineContextMark ownership for log related trace subtitle context marks',
+        'frontend-convergence-contract enforces UI Lab backed HzStateNotice ownership for log stream detail warning notices',
+        'frontend-convergence-contract enforces UI Lab backed HzStateNotice and HzDialogEventNotice ownership for log related trace and trace waterfall event-detail header notices',
+        'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /log/manage and /trace/manage attribution diagnostic status chips',
+        'frontend-convergence-contract enforces UI Lab backed HzStateNotice hint ownership for /log/manage and /trace/manage detail handoff hints',
+        'frontend-convergence-contract enforces HzButton and HzButtonLink ownership for /log/manage stream detail view-log, view-trace, and entity actions',
+        'frontend-convergence-contract enforces HzButton and HzButtonLink ownership for /log/manage and /trace/manage history/detail handoff actions'
+      ]),
+      sourceFiles: expect.arrayContaining([
+        'web-next/scripts/frontend-convergence-contract.test.ts',
+        'web-next/packages/hertzbeat-ui/src/index.tsx',
+        'web-next/app/ui-lab/page.tsx',
+        'web-next/app/log/manage/log-manage-page.tsx',
+        'web-next/app/trace/manage/trace-manage-page.tsx',
+        'web-next/components/log-manage/log-stream-detail-dialog.tsx',
+        'web-next/components/log-manage/log-related-trace-dialog.tsx'
+      ]),
+      nextStep: 'Continue the log/trace signal workbench cleanup by auditing interaction parity and any remaining local non-interactive chips through @hertzbeat/ui and /ui-lab before removing the route holds.'
+    });
+  });
+});
diff --git a/web-next/lib/hertzbeat-2-gap-audit.ts b/web-next/lib/hertzbeat-2-gap-audit.ts
new file mode 100644
index 0000000000..ce605d7ade
--- /dev/null
+++ b/web-next/lib/hertzbeat-2-gap-audit.ts
@@ -0,0 +1,708 @@
+import { buildLegacyFrontendParityAudit } from './legacy-frontend-parity';
+
+export type HertzBeat2GapArea =
+  | 'frontend-parity'
+  | 'topology'
+  | 'incidents'
+  | 'greptime-otlp'
+  | 'entity-evidence'
+  | 'ui-i18n';
+
+export type BackendApiStatus = 'available' | 'partial' | 'missing' | 'not-applicable';
+
+export type HertzBeat2Gap = {
+  code: string;
+  area: HertzBeat2GapArea;
+  owner: string;
+  page: string;
+  route?: string;
+  backendApiStatus: BackendApiStatus;
+  releaseBlocking: boolean;
+  currentState: string;
+  requiredClosure: string[];
+  evidence: string[];
+  sourceFiles: string[];
+  nextStep: string;
+};
+
+export type HertzBeat2GapAudit = {
+  milestone: 'M0';
+  releaseBlocked: boolean;
+  gaps: HertzBeat2Gap[];
+  releaseBlockingGapCount: number;
+};
+
+export type HertzBeat2ReleaseGateIssue = {
+  code: string;
+  message: string;
+  affected: string[];
+};
+
+export type HertzBeat2ReleaseGateResult = {
+  valid: boolean;
+  issues: HertzBeat2ReleaseGateIssue[];
+};
+
+const routeGapOwners: Record<string, string> = {
+  '/log/manage': 'M2 Legacy Frontend Parity Gate',
+  '/trace/manage': 'M2 Legacy Frontend Parity Gate',
+  '/monitors/[monitorId]': 'M1 Monitor Time Range and M2 Legacy Frontend Parity Gate',
+  '/passport/login': 'M2 Legacy Frontend Parity Gate',
+  '/incidents': 'M6 Incident Workbench MVP',
+  '/actions': 'M2 Legacy Frontend Parity Gate',
+  '/explorer': 'M2 Legacy Frontend Parity Gate'
+};
+
+function routeGap(route: string, status: 'hold' | 'placeholder'): HertzBeat2Gap {
+  return {
+    code: `frontend-route-${status}:${route}`,
+    area: 'frontend-parity',
+    owner: routeGapOwners[route] ?? 'M2 Legacy Frontend Parity Gate',
+    page: route,
+    route,
+    backendApiStatus: route === '/incidents' ? 'missing' : 'partial',
+    releaseBlocking: true,
+    currentState:
+      status === 'hold'
+        ? 'Route is marked hold and still needs action-level browser or API proof.'
+        : 'Route is marked placeholder and cannot count as feature complete.',
+    requiredClosure: [
+      'Remove the route from hold or placeholder only after product behavior is implemented.',
+      'Attach browser or API evidence for the original Angular workflow where applicable.'
+    ],
+    evidence: [
+      `routeCatalog.cutoverStatus=${status}`,
+      'legacy frontend parity audit marks release closure as blocked'
+    ],
+    sourceFiles: ['web-next/lib/nav.ts', 'web-next/lib/legacy-frontend-parity.ts'],
+    nextStep: 'Close the route-specific parity checklist or keep the route explicitly blocked.'
+  };
+}
+
+const functionalGaps: HertzBeat2Gap[] = [
+  {
+    code: 'topology-real-data-api',
+    area: 'topology',
+    owner: 'M5 Topology Real Data',
+    page: '/topology',
+    route: '/topology',
+    backendApiStatus: 'verified',
+    releaseBlocking: false,
+    currentState: 'A focused entity topology API exists and the frontend consumes relation, monitor-bind, Greptime trace-call, impact timeline, and trace-derived service graph RED evidence. Default API graphs are seeded from the accessible entity catalog instead of static UI data, sourceKind filters isolate backend evidence sources, empty API-backed results stay API-owned, Browser verification confirms the default /topology route no longer shows the old static seed graph, and focused API [...]
+    requiredClosure: [
+      'Keep live Greptime trace-call RED graph browser evidence in the release regression set.'
+    ],
+    evidence: [
+      'Live Greptime hzb_traces data now browser-verifies /topology service-call RED graph with Checkout API to Payment API, G6 nodeCount=2, edgeCount=1, traceState=ready, and traceRedState=partial',
+      'TopologyController exposes /api/topology for focused relation-backed graphs',
+      'EntityTopologyQueryService builds one-hop or two-hop graph DTOs from EntityRelation and EntityMonitorBind evidence',
+      'TraceCallTopologyQueryService derives otlp-trace-call edges from Greptime trace rows',
+      'Trace-call topology forwards URL start/end into Greptime timestamp predicates and skips trace discovery for non-trace source filters',
+      'Default /api/topology graphs are seeded from accessible entity catalog rows instead of static UI data',
+      'Topology sourceKind filters now isolate monitor ownership, trace-call, and relation-backed graph evidence',
+      'API-backed topology graphs expose impact timeline events from entity activities, relations, and monitor binds',
+      'loadTopologyGraph reads /api/topology for the frontend topology surface',
+      'loadTopologyGraph degrades stalled topology API reads with a bounded timeout instead of leaving /topology in api-pending',
+      'loadTopologyGraph aborts stalled topology API fetches after timeout so trace-call degradation does not leave dangling backend work',
+      'loadTopologyGraph deduplicates identical in-flight topology API reads so React dev hydration does not double-query slow Greptime topology scope',
+      'Trace-call topology uses a 60s bounded frontend timeout so slow relation-scoped Greptime graphs can render real API evidence or missing-evidence instead of premature degradation',
+      'buildTopologyApiUrl forwards numeric start/end context into the topology API query',
+      'API-backed empty topology graphs render an API empty state instead of the static fallback seed graph',
+      'Browser verification: /topology renders data-topology-data-source=api with api-empty and without checkout-api/orders-db/redis seed nodes when the API has no default evidence',
+      'Browser verification: /topology focused missing-entity route renders API-owned cmdb empty state with no static seed graph, no old explanatory header, and zero G6 nodes or edges',
+      'Browser verification: /topology service-call trace scope exposes time range, source kind, view mode, relationType=trace-call, trace-call-missing empty state, and zero G6 graph counts when local trace edges are absent',
+      'Focused API topology DOM exposes graph depth, node health, node evidence badges, edge relationship type, and edge evidence badges',
+      'Focused API topology DOM exposes trace-derived service graph request rate, error rate, and p95 latency on nodes, edges, and selected-edge evidence',
+      'Focused API topology DOM exposes trace-derived service graph first-seen and last-seen evidence on selected trace-call edges',
+      'Focused API topology selected edge drawer exposes browser-verifiable first/last seen and sample trace/span evidence window attributes',
+      'Focused API topology route exposes browser-verifiable trace-call edge, RED, evidence-window, and sample counts',
+      'EntityTopologyGraphInfo carries RED metrics on topology nodes and edges from TraceCallTopologyQueryService aggregation',
+      'GreptimeTraceQueryRepository builds service-to-service trace graph RED SQL with parent-child span joins, timestamp and environment predicates, seed service source/target scope, internal service filtering, request/error counts, avg latency, p95 sketch, and sample drilldown columns',
+      'GreptimeTraceQueryRepository selects first_seen and last_seen service graph timestamps and EntityTopologyGraphInfo carries them into topology edge evidence',
+      'TraceCallTopologyQueryService consumes Greptime service graph aggregation rows with seed service names pushed into storage before falling back to bounded raw trace-row topology discovery',
+      'TraceCallTopologyQueryService bounds Greptime service graph aggregation latency and falls back to raw trace-row topology discovery when aggregation fails or times out',
+      '/topology marks trace-call API degradation as Greptime trace graph unavailable instead of generic topology API empty evidence',
+      'Topology edge signal drilldowns prefer the selected trace-call edge sample trace/span context and preserve it in returnTo links',
+      'buildTopologyApiUrl forwards depth, relation type, hide-internal, and pagination scope into the topology API query',
+      'buildTopologyApiUrl defaults service-call and otlp-trace-call reads to relationType=trace-call so trace views do not fetch node-only relation graphs',
+      '/topology exposes Browser-readable topology API request scope metadata including resolved relationType=trace-call for service-call routes',
+      '/topology passes resolved relationType=trace-call into shared loading and empty states for inferred service-call routes',
+      'TopologyController and EntityTopologyQueryService accept backend relation type, hide-internal, and edge pagination scope for API-backed topology graphs',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbenchFrame, HzTopologyWorkbenchHeader, and HzTopologyWorkbenchGrid for the route shell, title/scope/source header, and canvas/companion layout',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbenchFrame boundary variants for route and section topology shells',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbenchSlot for canvas and companion grid ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbench shell header, slot, grid, and boundary ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyWorkbenchHeader operational-compact density with assistive copy so topology starts as a tool, not an explanation page',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvas for compact topology graph surfaces with layout and interaction markers',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvas interaction state for hover neighbor highlighting, focused depth, and drawer affordance',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvas boundary and interaction-scope variants for route and section graph surfaces',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvasAnnotation for canvas layout and depth status overlays',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvasAnnotation assistive non-occluding mode so canvas status does not cover G6 graph interactions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas for AntV G6 topology rendering with compact dark RED styling',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas drawer-sync node and edge selection metadata with hover neighbor highlighting',
+      '@hertzbeat/ui widens HzTopologyG6Canvas edge pointer hit targets so real mouse hover and click can open relationship evidence drawers',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas neighbor focus semantics for upstream, downstream, selected, and dimmed graph elements',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas pointer hover state for node and edge neighbor highlighting',
+      '@hertzbeat/ui exposes HzTopologyG6Canvas hover callbacks so /topology can drive live hover evidence tooltips from real G6 pointer state',
+      '@hertzbeat/ui clears HzTopologyG6Canvas live hover state at the canvas boundary so edge evidence does not stick after pointer exit',
+      '@hertzbeat/ui centers HzTopologyG6Canvas after fit and reset controls so the real graph returns to a readable viewport',
+      '@hertzbeat/ui preserves HzTopologyG6Canvas wheel and pan zoom while hover or selection styling redraws graph data without auto-fit',
+      '@hertzbeat/ui preserves HzTopologyG6Canvas zoom and position around same-structure hover and selection redraws',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas live viewport telemetry for Browser zoom and pan verification',
+      '@hertzbeat/ui exposes HzTopologyG6Canvas short viewport zoom and position aliases for Browser interaction regression checks',
+      '@hertzbeat/ui and /ui-lab publish HzTopologyG6Canvas zoom, fit, and reset telemetry after viewport actions settle',
+      '@hertzbeat/ui and /ui-lab expose HzTopologyG6Canvas viewport command bridge for Browser-verifiable zoom and reset actions',
+      '@hertzbeat/ui and /ui-lab route HzTopologyG6Canvas wheel zoom through manual clamped G6 zoomTo control so wheel input cannot over-shrink and drift',
+      '@hertzbeat/ui skips identical HzTopologyG6Canvas render keys and treats blank canvas hover-clear as a no-op without active hover evidence',
+      '@hertzbeat/ui and /ui-lab expose HzTopologyG6Canvas keyboard viewport shortcuts for zoom, fit, and reset when pointer hit testing is blocked',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas non-occluding viewport overview/minimap metadata for scale and zoom orientation',
+      '@hertzbeat/ui keeps HzTopologyG6Canvas live hover metadata out of G6 data redraws so real mouse hover cannot trigger setData/draw viewport resets',
+      '@hertzbeat/ui disables G6 hover-activate state mutation so node border hover cannot move a zoomed viewport',
+      '@hertzbeat/ui makes HzTopologyG6Canvas selected-node viewport focusing explicit-action-only so continuous mouse paths across canvas blank space cannot refocus an operator-zoomed graph',
+      '@hertzbeat/ui guards HzTopologyG6Canvas search-node auto-focus after operator zoom so hover clear cannot refocus and reset an adjusted viewport',
+      '@hertzbeat/ui keeps HzTopologyG6Canvas live hover callbacks out of the G6 mount lifecycle so node border hover cannot remount and refit the graph',
+      '@hertzbeat/ui cancels HzTopologyG6Canvas delayed initial fit when an operator starts wheel zooming or panning',
+      '@hertzbeat/ui disables continuous AntV G6 autoFit after the initial fit pass so wheel zoom is not reset by redraws',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas 50/200/500 node scale fixtures and scale-tier metadata',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas large-graph grouping, filtering, and table-companion strategy metadata',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas render-window budgeting so stress graphs cap primary G6 render data and require table companions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas render-window priority nodes for search only so ordinary node, edge, and table selection cannot reorder stress graph windows',
+      '@hertzbeat/ui and /ui-lab expose HzTopologyG6Canvas mount-lifecycle metadata so Browser can verify dense graph selection redraws do not remount or refit the canvas',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas search-to-focus controls for graph result highlighting',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas selected-node focus controls for retained drawer context',
+      '/ui-lab demonstrates HzTopologyG6Canvas visible node and edge click selection as in-page state so Browser can verify no URL navigation or remount on synthetic graphs',
+      '/ui-lab exposes a 500-node G6 scale Browser regression anchor for zoom, hover, selection, and table-companion stability checks',
+      '/ui-lab Browser-verifies the 500-node G6 scale anchor keeps URL, render window, zoom, and viewport position stable while table filters and hidden-edge row selection update in-page state',
+      '/ui-lab wires the 500-node render-window metric table rows to actual scale graph edge ids so Browser table selection can inspect partial and hidden graph edges without route changes',
+      '@hertzbeat/ui exposes HzTopologyG6Canvas toolbar action owner, state, target, and behavior metadata for zoom, fit, reset, and focus controls',
+      '@hertzbeat/ui clears HzTopologyG6Canvas live hover state before search or selected-node focus controls recenter the graph',
+      '@hertzbeat/ui and /ui-lab publish HzTopologyG6Canvas explicit focus-action telemetry so Browser can distinguish intentional search or selected-node recentering from hover or selection drift',
+      '@hertzbeat/ui clears HzTopologyG6Canvas live hover state before node or edge drawer selection callbacks run',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas filter-scope metadata for environment, source kind, search, grouping, and visible graph counts',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas filter controls for source, grouping, search, and reset scope actions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas visual group summary for active group-by scopes',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas non-occluding overlay mode so graph summaries, groups, and filters do not cover the real graph',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas actionable group-summary chips for source-kind scope changes',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyG6Canvas node double-click focus entry for focused 1-hop inspect mode',
+      '/topology reuses HzTopologyG6Canvas as the primary graph renderer instead of hand-rolled SVG/DOM nodes and edges',
+      '/topology wires HzTopologyG6Canvas node and edge clicks into in-page drawer selection state without URL navigation or route remounts',
+      '/topology exposes node-click, edge-click, and table-row-click selection source markers so Browser can distinguish real in-page inspection from route initial state',
+      '/topology clears fallback selected-edge context during node-click inspection so the G6 canvas and drawer do not retain stale edge state after ordinary node selection',
+      '/topology Browser-verifies real G6 edge click opens the relationship drawer in-page without URL navigation, remount, zoom reset, or viewport drift',
+      '/topology Browser-verifies the final M5 route matrix across default, focused, service-call, and empty routes with API-owned graph or empty states and no static fallback',
+      '/topology forwards topology search context into HzTopologyG6Canvas search focus metadata',
+      '/topology forwards environment, source, search, and group scope into HzTopologyG6Canvas filter-scope metadata',
+      '/topology reuses HzTopologyG6Canvas filter controls for source, grouping, search, and reset scope actions',
+      '/topology forwards groupBy route scope into HzTopologyG6Canvas grouping metadata and controls',
+      '/topology reuses HzTopologyG6Canvas visual group summary for groupBy route scopes',
+      '/topology uses HzTopologyG6Canvas non-occluding overlay mode so summary, grouping, and filter surfaces stay assistive while zoom controls remain visible',
+      '/topology reuses HzTopologyG6Canvas actionable group-summary chips while preserving groupBy route scope',
+      '/topology preserves groupBy route scope across source and view mode filter strips',
+      '/topology exposes selected node drawer group/source/view context for G6 node click verification',
+      '/topology drives hover tooltip and path summary from live HzTopologyG6Canvas edge hover state before falling back to selected edge evidence',
+      '/topology renders the fixed hover evidence tooltip only for live G6 edge hover so default selected edges do not cover canvas controls',
+      '/topology relies on shared HzTopologyG6Canvas boundary hover clear callbacks before falling back to selected edge evidence',
+      '/topology relies on shared HzTopologyG6Canvas viewport preservation so real graph wheel zoom is not immediately reset by hover or selection updates',
+      '/topology relies on shared HzTopologyG6Canvas zoom-position redraw preservation so mouse hover does not reset an operator-adjusted graph',
+      '/topology relies on shared HzTopologyG6Canvas hover highlight-only viewport behavior so node hover does not recenter an operator-zoomed graph',
+      '/topology relies on shared HzTopologyG6Canvas metadata-only hover updates so real node hover cannot redraw and reset an operator-zoomed graph',
+      '/topology relies on shared HzTopologyG6Canvas explicit selected-node focus actions so hover-clear canvas transitions do not recenter an operator-zoomed graph',
+      '/topology relies on shared HzTopologyG6Canvas search auto-focus guard so entity/search focus does not recenter after an operator zooms then hovers a node',
+      '/topology relies on shared HzTopologyG6Canvas stable hover callback refs so node border hover cannot remount and refit an operator-zoomed graph',
+      '/topology exposes shared HzTopologyG6Canvas live viewport telemetry so Browser checks can read zoom and position directly',
+      '/topology relies on shared HzTopologyG6Canvas post-action viewport telemetry timing for zoom verification',
+      '/topology exposes shared HzTopologyG6Canvas viewport command bridge so Browser can verify zoom without relying on flaky pointer dispatch',
+      '/topology routes wheel zoom through shared HzTopologyG6Canvas manual clamped G6 zoomTo control instead of page-local wheel handlers',
+      '/topology relies on shared HzTopologyG6Canvas identical-render-key redraw skip and blank hover-clear no-op behavior so canvas boundary movement cannot drift a zoomed graph',
+      '/topology exposes the shared HzTopologyG6Canvas render-window no-selection-reorder contract so ordinary node, edge, and table selection cannot reorder dense graph windows',
+      '/topology memoizes the HzTopologyG6Canvas graph input so live hover state does not recreate graph data and redraw the G6 viewport',
+      '/topology reuses shared HzTopologyG6Canvas keyboard viewport shortcuts for zoom, fit, and reset without page-local handlers',
+      '/topology exposes shared HzTopologyG6Canvas non-occluding viewport overview/minimap metadata without covering graph controls',
+      '/topology relies on shared HzTopologyG6Canvas initial-fit cancellation so first wheel zoom is not pulled back by delayed fit',
+      '/topology exposes shared HzTopologyG6Canvas initial-only auto-fit metadata for Browser wheel-zoom verification',
+      '/topology exposes shared HzTopologyG6Canvas mount-lifecycle Browser regression markers so node, edge, and table selection can be verified without URL changes or remounts',
+      '/topology reuses HzTopologyG6Canvas selected-node focus action for retained investigation context',
+      '/topology routes HzTopologyG6Canvas double-click focus entry through a soft explicit focus navigation path so ordinary inspection cannot trigger full-page reloads',
+      '/topology keeps the previous API-backed G6 graph mounted during explicit soft focus route transitions until the focused API read resolves',
+      '/topology maps HzTopologyG6Canvas node double-click focus entry to a depth=1 focused graph URL while preserving normal click drawer selection',
+      '/topology routes focus-trail exit through the same soft route path so leaving 1-hop focus does not blank the G6 canvas',
+      '/topology focus exit preserves the selected edgeId so relationship drawer context survives leaving 1-hop mode',
+      '/topology routes toolbar environment, depth, group, and reset scope changes through soft navigation while preserving the previous G6 graph during API reads',
+      '/topology Browser-verifies toolbar depth and group scope changes keep the previous G6 graph mounted during soft route reads while preserving timeRange and sourceKind context',
+      '/topology keeps toolbar layout changes inside the G6 canvas without URL navigation or API graph clearing',
+      '/topology keeps toolbar search changes inside the G6 canvas without URL navigation or API graph clearing',
+      '/topology clears live G6 hover evidence before RED metric table row selection navigates to edge detail',
+      '/topology shows shared missing-evidence state when otlp-trace-call has no real trace edges instead of presenting a node-only graph',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyGraphLayer for SVG edge layers inside topology canvases',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCanvas layout, interaction, boundary, annotation, and graph-layer ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState for API-empty/degraded topology evidence states',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState boundary variants for companion and canvas empty states',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState title, copy, source, time-scope, meta, and boundary ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState scope identity for environment, source kind, relation type, focus entity, depth, result count, and evidence sources',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEmptyState degraded topology API state without page-local fallback chrome',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyLoadingState for API-pending topology evidence states',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable as a RED-ranked topology companion table for large graph investigation',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable row-select detail affordance for keyboard/open-edge investigation',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable boundary variants for companion and framed topology surfaces',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable header, row, route, RED cell, badge, and action ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable graph-first density so RED rankings stay low-interruption beside the G6 graph',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable render-window companion metadata so hidden stress-graph nodes stay reachable through RED table investigation',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable row-level render-window visibility for visible, partial, and hidden large-graph relationships',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable render-window row count summaries for visible, partial, hidden, and unknown relationship filters',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyMetricTable in-page render-window filters for visible, partial, hidden, and unknown relationship slices',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar for compact topology filters, search, actions, and scope summary',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar boundary variants for route and section toolbars',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar state strip for focus, depth, layout, and grouping context',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar control, summary, and state-item ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar graph-first density with assistive secondary summary/state rows so the G6 canvas leads the first viewport',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail for shared legend, metric table, timeline, and detail drawer layout spacing',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail boundary variants for side and stacked companion surfaces',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail boundary, spacing, and content ownership for companion surfaces',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail graph-first priority so companion evidence stays low-interruption beside the G6 graph',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail contained scroll so long companion evidence stays within the graph-height viewport',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail sticky first-section context so contained companion evidence keeps orientation while scrolling',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionRail jump-list sticky context so quick jumps own contained companion orientation',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionSection anchor ownership for legend, RED table, timeline, and drawer blocks',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionSection collapsible companion sections for long graph-first right rails',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionJumpList anchor navigation for graph-first right-rail evidence sections',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionJumpList contained-rail scroll handoff so anchor jumps do not move the page',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyCompanionJumpList active-section highlighting from contained rail scrolling',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologySectionLabel for compact topology companion section headings',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologySectionLabel label and text ownership for companion headings',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyNode for typed health/focus/evidence topology nodes',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyNode label, health, RED metric, and evidence badge ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEdge for directional RED/evidence relationship rendering',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEdge line, arrow, drilldown, hit-target, RED, and badge ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip for edge hover source/target/RED/last-seen/sample-trace evidence',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip placement and sizing for canvas hover evidence',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip live-edge-hover trigger ownership so hover evidence is not a fixed canvas overlay',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip toolbar-safe canvas placement so hover evidence does not cover G6 controls',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip cursor-anchored placement from G6 pointer events so hover evidence stays near the edge instead of fixed over controls',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip canvas-clamped cursor anchors so hover evidence cannot overflow or cover edge toolbar controls',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyHoverTooltip header, fact, metric, and badge ownership for investigation hover evidence',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyLegend for health, source-kind, confidence, and stale evidence semantics',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyLegend boundary variants for companion and framed topology surfaces',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyLegend header, section, item, swatch, and boundary ownership for topology semantics',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer for node/edge evidence drawers and cross-signal handoffs',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer surface variants for companion and framed topology drawers',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer surface ownership for drawer border and padding chrome',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer header ownership for eyebrow/title/subtitle hierarchy',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer boundary context markers for roadmap/evidence copy',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer boundary ownership for retained context copy',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer boundary copy ownership for retained context text',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer fact group ownership for RED and identity facts',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer fact item ownership for compact RED and identity rows',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer fact label, value, and meta ownership',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer graph-first density so node and edge drawers stay compact beside the G6 graph',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer action group ownership for entity/alert and signal handoffs',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer action link and label ownership for entity/alert handoffs',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer signal action link and label ownership for cross-signal handoffs',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer action copy ownership for retained context helper text',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer signal label ownership for cross-signal handoff headings',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyDetailDrawer identity ownership for selected edge and current node drawer context',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList for compact fault context and impact timeline evidence',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList boundary variants for toolbar context and companion timeline surfaces',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList boundary ownership for fault context and impact timeline surfaces',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList header and item ownership for compact evidence rows',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyEvidenceList title, copy, count, item label, value, and meta ownership for compact evidence rows',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip for compact source and focus filter controls',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip boundary variants for section and companion filter strips',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip boundary and item ownership for compact filter controls',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip label and copy ownership for source and view controls',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFilterStrip assistive copy visibility for compact view controls',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyActionLink for standalone topology investigation actions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyActionLink spacing variants for companion and inset topology actions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyActionLink label, copy, and spacing ownership for topology investigation actions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFocusTrail for focused graph breadcrumbs, active filters, hidden counts, and exit-focus actions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyGroupPanel for group-by summaries, collapsed cluster counts, worst health, and large graph actions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyPathSummary for selected edge direction, RED facts, evidence badges, and drilldown actions',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyPathSummary interaction identity for selected edge, hovered edge, source, target, relation type, and source kind',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyScopeBar for compact time, environment, and refresh scope controls',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyScopeBar boundary variants for header and section scope rows',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyScopeBar item, value, action, and boundary ownership for scope rows',
+      '/topology reuses HzTopologyWorkbenchFrame, HzTopologyWorkbenchHeader, and HzTopologyWorkbenchGrid instead of page-local route shell and grid chrome',
+      '/topology reuses HzTopologyWorkbenchFrame boundary variants instead of page-local route shell borders',
+      '/topology reuses HzTopologyWorkbenchSlot instead of page-local workbench grid child wrappers',
+      '/topology reuses HzTopologyWorkbench shell header, slot, grid, and boundary ownership for the topology workbench',
+      '/topology uses shared HzTopologyWorkbenchHeader operational-compact density and assistive copy instead of page-local explanatory header chrome',
+      '/topology uses a concise operational header with label-only source controls so the graph leads the first viewport instead of explanatory copy',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar graph-first single-row overflow layout so scope controls do not push the G6 graph down',
+      '/topology reuses HzTopologyToolbar graph-first single-row overflow layout for environment, search, depth, layout, group, fit, locate, and reset controls',
+      '/topology reuses the shared HzTopologyFilterStrip source-rail variant so source controls prefer one compact row above the G6 graph',
+      '/topology reuses the shared HzTopologyFocusTrail rail density so focused context stays compact above the G6 graph',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFocusTrail graph-dock density with document-flow, low-interruption, non-occluding focus context',
+      '/topology reuses HzTopologyFocusTrail graph-dock density so focused breadcrumbs and exit affordance stay visible without covering the G6 graph',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyFocusTrail mode, depth, entity, and scoped exit href metadata for focused 1-hop graph handoffs',
+      '/topology maps focused 1-hop G6 routes into HzTopologyFocusTrail metadata and exits focus without losing environment/time/source/group scope',
+      '/topology reuses HzTopologyMetricTable beside the graph for I18N-backed RED edge ranking and selected-edge alignment',
+      '/topology reuses HzTopologyMetricTable row action affordance for selected-edge detail alignment',
+      '/topology reuses HzTopologyMetricTable boundary variants instead of page-local metric table border chrome',
+      '/topology reuses HzTopologyMetricTable header, row, route, RED cell, badge, and action ownership for edge ranking',
+      '/topology reuses HzTopologyMetricTable graph-first density for compact companion RED ranking',
+      '/topology wires HzTopologyMetricTable render-window companion metadata from the same G6 graph so hidden nodes keep table investigation context',
+      '/topology marks HzTopologyMetricTable rows with source and target visibility from the current G6 render window instead of losing hidden-edge context',
+      '/topology exposes HzTopologyMetricTable render-window row count summaries for future visible, partial, and hidden relationship filters',
+      '/topology reuses HzTopologyMetricTable in-page render-window filters so ordinary RED table filtering does not navigate or remount the G6 canvas',
+      '/topology reuses HzTopologyEmptyState for API-owned empty graph states instead of page-local empty chrome',
+      '/topology reuses HzTopologyEmptyState boundary variants instead of page-local canvas empty-state border chrome',
+      '/topology reuses HzTopologyEmptyState title, copy, source, time-scope, meta, and boundary ownership for API empty states',
+      '/topology passes API empty topology scope identity into HzTopologyEmptyState for empty/degraded browser verification',
+      '/topology renders API unavailable topology as degraded shared HzTopologyEmptyState instead of ordinary empty results',
+      '/topology renders API pending topology as shared HzTopologyLoadingState instead of ordinary empty results',
+      '/topology reuses HzTopologyToolbar instead of page-local Input/Select/button toolbar chrome',
+      '/topology reuses HzTopologyToolbar boundary variants instead of page-local toolbar borders',
+      '/topology reuses HzTopologyToolbar state strip for focus/depth/layout/grouping instead of page-local state chips',
+      '/topology reuses HzTopologyToolbar control, summary, and state-item ownership for topology filtering',
+      '/topology reuses HzTopologyToolbar graph-first density so secondary toolbar context does not push the real G6 graph below the fold',
+      '@hertzbeat/ui and /ui-lab demonstrate HzTopologyToolbar depth, layout, group-by, and reset controls as shared scope controls',
+      '/topology reuses HzTopologyToolbar depth, layout, group-by, and reset controls for topology scope changes',
+      '/topology reuses HzTopologyCompanionRail for table/detail companion layout instead of page-local margin wrappers',
+      '/topology reuses HzTopologyCompanionRail boundary variants instead of page-local companion borders',
+      '/topology reuses HzTopologyCompanionRail boundary, spacing, and content ownership for the legend, metrics, timeline, and detail rail',
+      '/topology reuses HzTopologyCompanionRail graph-first priority so the real G6 graph remains the primary first-viewport surface',
+      '/topology reuses HzTopologyCompanionRail contained scroll so the right rail no longer stretches the whole page below the G6 graph',
+      '/topology reuses HzTopologyCompanionRail sticky first-section context so the right rail keeps orientation during internal scroll',
+      '/topology reuses HzTopologyCompanionRail jump-list sticky context with HzTopologyCompanionJumpList as the top orientation control',
+      '/topology reuses HzTopologyCompanionSection anchors for legend, RED table, timeline, and drawer blocks instead of page-local section wrappers',
+      '/topology reuses HzTopologyCompanionSection collapsible controls so long RED/timeline/drawer evidence stays compact',
+      '/topology reuses HzTopologyCompanionJumpList for compact right-rail evidence jumps instead of page-local anchor chips',
+      '/topology reuses HzTopologyCompanionJumpList contained-rail scroll handoff so right-rail jumps keep the G6 canvas fixed',
+      '/topology reuses HzTopologyCompanionJumpList active-section highlighting so the right rail shows the current evidence section',
+      '/topology reuses HzTopologySectionLabel for companion section headings instead of page-local label typography',
+      '/topology reuses HzTopologySectionLabel label and text ownership for companion headings',
+      '/topology reuses HzTopologyCanvas instead of page-local canvas surface chrome',
+      '/topology reuses HzTopologyCanvas interaction state for hover/focus/drawer affordances instead of page-local interaction hints',
+      '/topology reuses HzTopologyCanvas boundary and interaction-scope variants instead of page-local border or hover group classes',
+      '/topology reuses HzTopologyCanvasAnnotation instead of page-local canvas annotation chrome',
+      '/topology renders HzTopologyCanvasAnnotation in assistive non-occluding mode so the G6 graph remains visually and interactively clear',
+      '/topology reuses HzTopologyCanvas layout, interaction, boundary, annotation, and graph-layer ownership for the topology graph surface',
+      '/topology reuses HzTopologyGraphLayer instead of page-local SVG graph layer chrome',
+      '/topology reuses HzTopologyNode instead of page-local node tone/focus chrome',
+      '/topology reuses HzTopologyNode label, health, RED metric, and evidence badge ownership for graph nodes',
+      '/topology reuses HzTopologyEdge instead of page-local edge color and drilldown-dot chrome',
+      '/topology reuses HzTopologyEdge line, arrow, drilldown, hit-target, RED, and badge ownership for relationships',
+      '/topology reuses HzTopologyHoverTooltip for hover evidence instead of page-local tooltip or title attributes',
+      '/topology reuses HzTopologyHoverTooltip placement and sizing instead of page-local absolute tooltip chrome',
+      '/topology passes HzTopologyHoverTooltip live-edge-hover trigger only from live G6 edge hover state',
+      '/topology positions HzTopologyHoverTooltip below the G6 toolbar controls to keep fit, zoom, and focus actions visible during hover',
+      '/topology anchors live HzTopologyHoverTooltip placement from HzTopologyG6Canvas pointer coordinates instead of fixed page-local hover chrome',
+      '/topology relies on shared HzTopologyHoverTooltip canvas-clamped cursor anchors for live G6 hover evidence',
+      '/topology reuses HzTopologyHoverTooltip header, fact, metric, and badge ownership for edge hover evidence',
+      '/topology reuses HzTopologyLegend instead of page-local legend/source semantics',
+      '/topology reuses HzTopologyLegend boundary variants instead of page-local legend border chrome',
+      '/topology reuses HzTopologyLegend boundary owner for framed health/source/confidence semantics',
+      '/topology reuses HzTopologyLegend header, section, item, swatch, and boundary ownership for health/source/confidence semantics',
+      '/topology reuses HzTopologyDetailDrawer for selected edge evidence instead of page-local evidence panel chrome',
+      '/topology reuses HzTopologyDetailDrawer for current entity evidence instead of page-local current entity panel chrome',
+      '/topology reuses HzTopologyDetailDrawer surface variants instead of page-local drawer border chrome',
+      '/topology reuses HzTopologyDetailDrawer surface ownership for selected-edge and current-entity drawer chrome',
+      '/topology reuses HzTopologyDetailDrawer header ownership for edge and current-entity hierarchy',
+      '/topology reuses HzTopologyDetailDrawer boundary owner for edge and current-entity context copy',
+      '/topology reuses HzTopologyDetailDrawer boundary ownership for edge and current-entity context copy',
+      '/topology reuses HzTopologyDetailDrawer boundary copy ownership for edge and current-entity context text',
+      '/topology reuses HzTopologyDetailDrawer fact group ownership for edge and current-entity RED facts',
+      '/topology reuses HzTopologyDetailDrawer fact item ownership for edge and current-entity RED facts',
+      '/topology reuses HzTopologyDetailDrawer fact label, value, and meta ownership for edge and current-entity facts',
+      '/topology reuses HzTopologyDetailDrawer graph-first density for selected-edge and current-node evidence drawers',
+      '/topology reuses HzTopologyDetailDrawer action group ownership for edge and current-entity handoffs',
+      '/topology reuses HzTopologyDetailDrawer action link and label ownership for edge and current-entity handoffs',
+      '/topology reuses HzTopologyDetailDrawer signal action link and label ownership for edge and current-entity handoffs',
+      '/topology reuses HzTopologyDetailDrawer action copy ownership for selected-edge retained-context helper text',
+      '/topology reuses HzTopologyDetailDrawer signal label ownership for current-entity cross-signal headings',
+      '/topology passes selected edge and current node identity into HzTopologyDetailDrawer for drawer verification',
+      '/topology passes current node upstream and downstream dependency evidence into HzTopologyDetailDrawer for node-click investigation',
+      '/topology annotates current node entity and signal handoffs with selected G6 node context for retained evidence drilldowns',
+      '/topology reuses HzTopologyEvidenceList for incoming fault context and API impact timeline panels',
+      '/topology reuses HzTopologyEvidenceList boundary variants instead of page-local fault context border and padding chrome',
+      '/topology reuses HzTopologyEvidenceList companion timeline boundary for API impact timeline panels',
+      '/topology reuses HzTopologyEvidenceList boundary ownership for incoming fault context and API impact timeline panels',
+      '/topology reuses HzTopologyEvidenceList header and item ownership for incoming fault context and API impact timeline rows',
+      '/topology reuses HzTopologyEvidenceList title, copy, count, item label, value, and meta ownership for incoming fault context and API impact timeline rows',
+      '/topology reuses HzTopologyFilterStrip for source and view-mode controls instead of page-local filter cards',
+      '/topology reuses HzTopologyFilterStrip boundary variants instead of page-local filter strip borders and padding',
+      '/topology reuses HzTopologyFilterStrip boundary and item ownership for source and view-mode controls',
+      '/topology reuses HzTopologyFilterStrip label and copy ownership for source and view-mode controls',
+      '/topology uses shared HzTopologyFilterStrip assistive copy visibility for view-mode controls so companion options stay label-first',
+      '/topology reuses HzTopologyActionLink for alert-impact closure instead of page-local action link chrome',
+      '/topology reuses HzTopologyActionLink spacing variants instead of page-local action margins',
+      '/topology reuses HzTopologyActionLink label, copy, and spacing ownership for alert-impact closure',
+      '/topology reuses HzTopologyFocusTrail for focused context instead of toolbar-only local state chips',
+      '/topology reuses HzTopologyGroupPanel for large graph grouping context instead of page-local group chips',
+      '/topology reuses HzTopologyPathSummary for selected path context instead of page-local hover-only edge hints',
+      '/topology passes selected path interaction identity into HzTopologyPathSummary for edge hover, source/target, relation type, and source kind evidence',
+      '/topology reuses HzTopologyScopeBar for header scope controls instead of page-local chips and refresh chrome',
+      '/topology reuses HzTopologyScopeBar boundary variants instead of page-local scope borders and padding',
+      '/topology reuses HzTopologyScopeBar item, value, action, and boundary ownership for header scope controls',
+      'buildTopologyServiceMapFromApiGraph maps monitor-bind node endpoints without dropping non-entity topology edges',
+      'buildTopologyServiceMap remains only as a direct static fixture/view-model fallback, not the default topology route fallback'
+    ],
+    sourceFiles: [
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/TopologyController.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/EntityTopologyQueryService.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/entity/TraceCallTopologyQueryService.java',
+      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/TraceQueryRepository.java',
+      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java',
+      'web-next/packages/hertzbeat-ui/src/index.tsx',
+      'web-next/packages/hertzbeat-ui/src/topology-g6.tsx',
+      'web-next/app/ui-lab/page.tsx',
+      'web-next/lib/topology-surface/controller.ts',
+      'web-next/lib/topology-surface/view-model.ts',
+      'web-next/app/topology/topology-page.tsx'
+    ],
+    nextStep: 'Move any remaining topology regression proof into durable browser fixtures and keep the live Greptime RED graph available for release smoke runs.'
+  },
+  {
+    code: 'incident-workbench-domain',
+    area: 'incidents',
+    owner: 'M6 Incident Workbench MVP',
+    page: '/incidents',
+    route: '/incidents',
+    backendApiStatus: 'available',
+    releaseBlocking: false,
+    currentState:
+      '/incidents now reuses the shared HzIncidentWorkbench from @hertzbeat/ui, has a /ui-lab demo, reads the authenticated status-page incident list and selected detail APIs, and Browser-verifies the shared status transition PUT loop.',
+    requiredClosure: [
+      'Keep the list/detail/transition Browser proof in the release regression set.',
+      'Fold richer incident CRUD and public-status administration parity into the M10 public-status area.'
+    ],
+    evidence: [
+      '@hertzbeat/ui and /ui-lab demonstrate HzIncidentWorkbench with operator-compact density and cold-matte-hard-edge styling',
+      '/incidents reuses HzIncidentWorkbench instead of page-local placeholder chrome',
+      '/incidents reads GET /status/page/incident?pageIndex=0&pageSize=8 through the local BFF list adapter',
+      '/incidents reads selected incident detail from GET /status/page/incident/{id} when list data exists',
+      '/incidents writes status transitions through PUT /status/page/incident and refreshes list/detail after success',
+      'Browser verification: /ui-lab and /incidents expose the same hertzbeat-ui-incident-workbench and hertzbeat-ui-incident-transition-actions ownership markers',
+      'Browser verification: local empty /incidents renders API-backed empty state with no placeholder copy, total=0, detail not-requested, and transition actions disabled',
+      'Browser verification: seeded local incident transitioned from Identified to Monitoring through the shared state-2 action, detail state refreshed to state=2 with a new timeline entry, and temporary data was deleted afterwards'
+    ],
+    sourceFiles: [
+      'web-next/packages/hertzbeat-ui/src/index.tsx',
+      'web-next/app/ui-lab/page.tsx',
+      'web-next/lib/incidents-surface/controller.ts',
+      'web-next/lib/incidents-surface/view-model.ts',
+      'web-next/app/incidents/incidents-page.tsx'
+    ],
+    nextStep: 'Move remaining status-page incident CRUD breadth into the M10 public-status parity checklist while keeping /incidents API smoke evidence fresh.'
+  },
+  {
+    code: 'greptime-otlp-query-pushdown',
+    area: 'greptime-otlp',
+    owner: 'M3 Greptime OTLP Query Productionization',
+    page: '/trace/manage and /log/manage',
+    backendApiStatus: 'partial',
+    releaseBlocking: true,
+    currentState: 'Greptime trace and log reads exist, the topology trace-call path has timestamp, environment, seed service scope, and internal-service SQL predicates plus service graph RED aggregation, and trace list reads now pass time, service namespace, environment, workspace, canonical entity identity scope, grouping, total count, limit, and offset into the Greptime trace repository before Java fallback filtering. Trace-id list/detail reads now have a Greptime trace-row overload th [...]
+    requiredClosure: [
+      'Push time, workspace, entity, service, environment, trace id, pagination, and sorting into Greptime SQL.',
+      'Promote frequently filtered OTLP attributes to queryable columns or tags where benchmarks require it.'
+    ],
+    evidence: [
+      'EntityTraceQueryServiceImpl builds entity trace summary/hints from storage-owned recent rows scoped by lookback time, workspace, and canonical entity identity',
+      'GreptimeTraceQueryRepository owns service, service namespace, environment, workspace, entity identity, internal, and timestamp predicates for recent trace rows',
+      'GreptimeTraceQueryRepository owns service-to-service trace graph RED SQL aggregation with seed service source/target scope for topology',
+      'GreptimeTraceQueryRepository owns grouped trace-list SQL with total_count, LIMIT, and OFFSET for paged list views',
+      'GreptimeTraceQueryRepository owns trace-id row SQL with time, service, environment, workspace, entity identity, and internal-service filters when route/detail context is present',
+      'EntityTraceQueryServiceImpl pushes canonical entity identity into trace detail row reads before Java span aggregation',
+      'EntityTraceQueryServiceImpl consumes storage-owned trace-list pages before falling back to bounded Java samples'
+    ],
+    sourceFiles: [
+      'hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java',
+      'hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/repository/GreptimeTraceQueryRepository.java'
+    ],
+    nextStep: 'Move remaining non-Greptime fallback semantics away from bounded Java samples where feasible, then re-run live trace list/detail smoke after the warehouse compile blocker is cleared.'
+  },
+  {
+    code: 'entity-evidence-read-model',
+    area: 'entity-evidence',
+    owner: 'M4 Entity Evidence Read Model',
+    page: '/entities/[entityId]',
+    route: '/entities/[entityId]',
+    backendApiStatus: 'partial',
+    releaseBlocking: true,
+    currentState: 'Entity relations and detail APIs exist, but topology, incident, and signal workspaces do not share one evidence contract.',
+    requiredClosure: [
+      'Return metrics, logs, traces, alerts, relations, and handoffs through one entity evidence read model.',
+      'Make signal workspaces consume that contract instead of rebuilding route context locally.'
+    ],
+    evidence: [
+      'EntityRelation has DAO and service coverage',
+      'signal workspaces still keep page-specific query and handoff builders'
+    ],
+    sourceFiles: [
+      'hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityRelation.java',
+      'hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/EntityController.java',
+      'web-next/lib/signal-route-context.ts'
+    ],
+    nextStep: 'Define the shared evidence response and migrate one signal handoff at a time.'
+  },
+  {
+    code: 'ui-i18n-reuse-sweep',
+    area: 'ui-i18n',
+    owner: 'M7 UI Reuse and I18N Release Sweep',
+    page: 'monitor, OTLP, entity, log, trace, topology, and incident pages',
+    backendApiStatus: 'not-applicable',
+    releaseBlocking: true,
+    currentState: 'The frontend convergence contract now guards the M10 observability workbench surfaces against drifting off UI Lab backed shared components, and the log/trace signal workbench query controls and trace query/search field/query token/status select/action group/time-control stacks, trace query search field icon sizing, trace query action icon sizing, trace header handoff action icon sizing, trace detail-footer handoff action icon sizing, trace time-range narrow-rail alignm [...]
+    requiredClosure: [
+      'Move user-facing copy through I18N dictionaries.',
+      'Move reusable controls to @hertzbeat/ui with UI Lab coverage before page reuse.'
+    ],
+    evidence: [
+      'frontend-convergence-contract enforces UI Lab backed @hertzbeat/ui ownership for actions, explorer, incidents, and topology workbench surfaces',
+      'frontend-convergence-contract enforces UI Lab backed HzInput, HzSelect, HzButton, and HzControlStack ownership for /log/manage and /trace/manage query controls',
+      'frontend-convergence-contract enforces UI Lab backed HzControlStack ownership for /trace/manage time-control rail',
+      'frontend-convergence-contract enforces UI Lab backed TimeRangeControl narrow-rail alignment ownership for /trace/manage time control',
+      'frontend-convergence-contract enforces UI Lab backed HzControlStack ownership for /log/manage detail action stacks',
+      'frontend-convergence-contract enforces UI Lab backed HzScrollViewport ownership for /log/manage stream viewport',
+      'frontend-convergence-contract enforces UI Lab backed HzLogStreamLiveRow ownership for /log/manage stream rows',
+      'frontend-convergence-contract enforces UI Lab backed HzDetailAside ownership for /log/manage selected-detail aside rail',
+      'frontend-convergence-contract enforces UI Lab backed HzDetailBodyStack ownership for /log/manage selected-detail body spacing',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout and HzActionGroup ownership for /log/manage stream/history view switch layout',
+      'frontend-convergence-contract enforces Link-compatible HzButtonLink ownership for /log/manage and /trace/manage header action rows',
+      'frontend-convergence-contract enforces HzButton ownership for /log/manage stream reconnect, pause, and clear controls',
+      'frontend-convergence-contract enforces HzButton ownership for /log/manage stream/history view toggle controls',
+      'frontend-convergence-contract enforces HzButton ownership for /log/manage and /trace/manage table row detail controls',
+      'frontend-convergence-contract enforces UI Lab backed HzTableRowActionButton ownership for /trace/manage root-span row action density',
+      'frontend-convergence-contract enforces UI Lab backed HzDisabledActionShell ownership for /trace/manage disabled handoff action wrappers',
+      'frontend-convergence-contract enforces UI Lab backed HzDialogEventText ownership for /trace/manage event-detail copy and meta text roles',
+      'frontend-convergence-contract enforces UI Lab backed HzStatCell compact density ownership for /trace/manage drawer stage stats',
+      'frontend-convergence-contract enforces HzButton ownership for /log/manage and /trace/manage event-detail view-span controls',
+      'frontend-convergence-contract enforces UI Lab backed HzActionGroup full-end layout ownership for /trace/manage header and drawer action rows',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage and /trace/manage header and query panel shells',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage and /trace/manage chart panel shells',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage and /trace/manage table panel shells',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface clip ownership for /trace/manage table/detail panel overflow',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface query padding ownership for /trace/manage query panel density',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface header padding ownership for /trace/manage header panel density',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface chart padding ownership for /trace/manage chart panel density',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage and /trace/manage detail panel shells',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage header layout',
+      'frontend-convergence-contract enforces UI Lab backed HzSignalWorkbenchShell ownership for /trace/manage page shell and content width',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchHeaderCopy ownership for /trace/manage header copy typography',
+      'frontend-convergence-contract enforces UI Lab backed HzSearchFieldFrame ownership for /trace/manage query search field',
+      'frontend-convergence-contract enforces UI Lab backed HzSearchFieldIcon ownership for /trace/manage query search field icon',
+      'frontend-convergence-contract enforces UI Lab backed HzInput search-icon inset ownership for /trace/manage query search input',
+      'frontend-convergence-contract enforces UI Lab backed HzQueryTokenField ownership for /trace/manage trace/span token inputs',
+      'frontend-convergence-contract enforces UI Lab backed HzQueryStatusSelect ownership for /trace/manage status select control',
+      'frontend-convergence-contract enforces UI Lab backed HzQueryActionGroup ownership for /trace/manage run/reset query actions',
+      'frontend-convergence-contract enforces UI Lab backed HzButtonIcon ownership for /trace/manage run/reset query action icons',
+      'frontend-convergence-contract enforces UI Lab backed HzButtonIcon ownership for /trace/manage header handoff action icons',
+      'frontend-convergence-contract enforces UI Lab backed HzButtonIcon ownership for /trace/manage detail-footer handoff action icons',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage time-toolbar layout',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage chart summary/trend layout',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage table/detail layout',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage detail body layout',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /trace/manage detail footer layout',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelHeader ownership for /log/manage and /trace/manage table panel header bars',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelHeader ownership for /log/manage and /trace/manage detail panel header bars',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelSurface ownership for /log/manage stream view switch and stream stage panel shells',
+      'frontend-convergence-contract enforces UI Lab backed HzWorkbenchLayout ownership for /log/manage stream-stage split layout',
+      'frontend-convergence-contract enforces UI Lab backed HzPanelHeader ownership for /log/manage stream stage and selected-log section headers',
+      'frontend-convergence-contract enforces UI Lab backed HzStateNotice ownership for /log/manage stream pause and selected-log helper notices',
+      'frontend-convergence-contract blocks page-local actionClass button-style tokens from /log/manage and /trace/manage',
+      'frontend-convergence-contract enforces UI Lab backed HzStatCell ownership for /log/manage and /trace/manage summary stat tiles',
+      'frontend-convergence-contract enforces UI Lab backed HzStatStrip and HzStatCell ownership for log related trace and trace waterfall drawer stage stat strips',
+      'frontend-convergence-contract enforces UI Lab backed HzSignalTrendBars ownership for /log/manage and /trace/manage signal trend bars',
+      'frontend-convergence-contract enforces UI Lab backed HzDetailRows ownership for /log/manage and /trace/manage detail facts, selected evidence, and entity context rows',
+      'frontend-convergence-contract enforces UI Lab backed HzDetailRows ownership for log stream detail, related trace preview, and trace waterfall drawer selected facts',
+      'frontend-convergence-contract enforces UI Lab backed HzAttributeDiagnostics ownership for /log/manage and /trace/manage attribution diagnostic containers',
+      'frontend-convergence-contract enforces UI Lab backed HzAttributeDiagnostics ownership for log stream detail dialog attribution diagnostics',
+      'frontend-convergence-contract enforces UI Lab backed HzEmptyState ownership for /log/manage stream/list and /trace/manage table empty states',
+      'frontend-convergence-contract enforces UI Lab backed HzEmptyState table-panel layout for /trace/manage table empty-state density',
+      'frontend-convergence-contract enforces UI Lab backed HzDataTable ownership for /log/manage and /trace/manage result table chrome',
+      'frontend-convergence-contract enforces UI Lab backed HzDataCellText ownership for /trace/manage table cell typography',
+      'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /log/manage and /trace/manage table status chips',
+      'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /trace/manage table count chip',
+      'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /log/manage stream severity chips',
+      'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /log/manage stream status, count, and live chips',
+      'frontend-convergence-contract enforces UI Lab backed HzChipGroup ownership for log dialog toolbar and trace drawer metadata chip groups',
+      'frontend-convergence-contract enforces UI Lab backed HzDialogMetaItem ownership for trace waterfall drawer metadata item sizing',
+      'frontend-convergence-contract enforces UI Lab backed HzActionGroup ownership for log dialog, trace route header, and trace drawer action groups',
+      'frontend-convergence-contract enforces UI Lab backed HzDialogBodyLayout ownership for log dialog and trace waterfall drawer body layouts',
+      'frontend-convergence-contract enforces UI Lab backed HzInlineContextMark ownership for log related trace subtitle context marks',
+      'frontend-convergence-contract enforces UI Lab backed HzStateNotice ownership for log stream detail warning notices',
+      'frontend-convergence-contract enforces UI Lab backed HzStateNotice and HzDialogEventNotice ownership for log related trace and trace waterfall event-detail header notices',
+      'frontend-convergence-contract enforces UI Lab backed HzStatusBadge ownership for /log/manage and /trace/manage attribution diagnostic status chips',
+      'frontend-convergence-contract enforces UI Lab backed HzStateNotice hint ownership for /log/manage and /trace/manage detail handoff hints',
+      'frontend-convergence-contract enforces HzButton and HzButtonLink ownership for /log/manage stream detail view-log, view-trace, and entity actions',
+      'frontend-convergence-contract enforces HzButton and HzButtonLink ownership for /log/manage and /trace/manage history/detail handoff actions',
+      'OTLP, topology, or incident surfaces still have local view-model copy or local surface structure',
+      'strict UI reuse remains an active monitor and observability migration requirement'
+    ],
+    sourceFiles: [
+      'web-next/scripts/frontend-convergence-contract.test.ts',
+      'web-next/packages/hertzbeat-ui/src/index.tsx',
+      'web-next/app/ui-lab/page.tsx',
+      'web-next/app/log/manage/log-manage-page.tsx',
+      'web-next/app/trace/manage/trace-manage-page.tsx',
+      'web-next/components/log-manage/log-stream-detail-dialog.tsx',
+      'web-next/components/log-manage/log-related-trace-dialog.tsx',
+      'web-next/lib/otlp-center/controller.ts',
+      'web-next/app/topology/topology-page.tsx'
+    ],
+    nextStep: 'Continue the log/trace signal workbench cleanup by auditing interaction parity and any remaining local non-interactive chips through @hertzbeat/ui and /ui-lab before removing the route holds.'
+  }
+];
+
+export function buildHertzBeat2GapAudit(): HertzBeat2GapAudit {
+  const legacyAudit = buildLegacyFrontendParityAudit();
+  const gaps = [
+    ...legacyAudit.routeCoverage.primaryHoldRoutes.map(route => routeGap(route, 'hold')),
+    ...legacyAudit.routeCoverage.primaryPlaceholderRoutes.map(route => routeGap(route, 'placeholder')),
+    ...functionalGaps
+  ];
+
+  return {
+    milestone: 'M0',
+    gaps,
+    releaseBlocked: gaps.some(gap => gap.releaseBlocking),
+    releaseBlockingGapCount: gaps.filter(gap => gap.releaseBlocking).length
+  };
+}
+
+export function validateHertzBeat2ReleaseGate(audit: HertzBeat2GapAudit = buildHertzBeat2GapAudit()): HertzBeat2ReleaseGateResult {
+  const issues: HertzBeat2ReleaseGateIssue[] = [];
+  const releaseBlockingGaps = audit.gaps.filter(gap => gap.releaseBlocking);
+
+  if (releaseBlockingGaps.length > 0) {
+    issues.push({
+      code: 'release-blocked-gaps',
+      message: 'HertzBeat 2.0 release closure is blocked until every tracked release-blocking gap is closed.',
+      affected: releaseBlockingGaps.map(gap => gap.code)
+    });
+  }
+
+  for (const gap of audit.gaps) {
+    const missingMetadata = [
+      gap.owner ? null : 'owner',
+      gap.page ? null : 'page',
+      gap.backendApiStatus ? null : 'backendApiStatus',
+      gap.currentState ? null : 'currentState',
+      gap.requiredClosure.length > 0 ? null : 'requiredClosure',
+      gap.nextStep ? null : 'nextStep'
+    ].filter(Boolean) as string[];
+
+    if (missingMetadata.length > 0) {
+      issues.push({
+        code: 'gap-metadata-incomplete',
+        message: `Gap ${gap.code} is missing release audit metadata.`,
+        affected: missingMetadata
+      });
+    }
+  }
+
+  for (const gap of releaseBlockingGaps) {
+    if (['topology-real-data-api', 'incident-workbench-domain', 'greptime-otlp-query-pushdown'].includes(gap.code)) {
+      issues.push({
+        code: gap.code,
+        message: gap.currentState,
+        affected: gap.requiredClosure
+      });
+    }
+  }
+
+  return {
+    valid: issues.length === 0,
+    issues
+  };
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 23/50: feat(backend): harden OTLP ingestion and token scopes

Posted by zh...@apache.org.
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 f094cf7a2a0d49ac74b631dbaffad048c546f16f
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 09:34:43 2026 +0800

    feat(backend): harden OTLP ingestion and token scopes
    
    Validation:
    
    - ./mvnw -pl hertzbeat-common-core,hertzbeat-common-spring,hertzbeat-warehouse,hertzbeat-observability -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests compile
    
    - ./mvnw -pl hertzbeat-common-core,hertzbeat-common-spring,hertzbeat-warehouse,hertzbeat-observability -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dtest='OtlpWorkspaceDtoMigrationTest,EntitySchemaLegacyColumnCompatibilityTest,GreptimeSqlQueryExecutorTest,GreptimeTraceQueryRepositoryTest,OpenTelemetryConfigTest,OtlpLogProtocolAdapterTest,OtlpGrpcServerConfigTest,OtlpIngestionControllerTest,OtlpLogControllerTest,OtlpLo [...]
    
    - ./mvnw -pl hertzbeat-startup -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -Dtest=ReleaseReadinessRuntimeAssemblyTest test
    
    - ./mvnw -pl hertzbeat-manager -Dcheckstyle.skip=true -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -Dtest='ApiTokenValidationFilterTest,AuthTokenControllerTest,AccountServiceTest,OtlpRouteAuthorizationConfigTest' test
    
    - git diff --cached --check
---
 .../dto/binding/OtlpEntityBindingSummaryDto.java   |   21 +
 .../dto/ingestion/OtlpIngestionRedSummaryDto.java  |   82 +
 .../dto/OtlpWorkspaceDtoMigrationTest.java         |    6 +-
 .../hertzbeat/common/entity/manager/AuthToken.java |   18 +
 .../entity/manager/EntityDefinitionActivity.java   |    6 +
 .../entity/manager/EntityGovernanceState.java      |   13 +-
 .../common/entity/manager/ObserveEntity.java       |   11 +-
 .../gateway/AuthTokenRequestContext.java           |   30 +-
 .../observability/gateway/AuthTokenScopes.java     |   81 +
 .../gateway/ObservabilityAccessTokenGateway.java   |   25 +-
 .../EntitySchemaLegacyColumnCompatibilityTest.java |   46 +
 .../src/test/resources/sureness.yml                |   16 +-
 .../manager/config/ApiTokenValidationFilter.java   |   67 +-
 .../manager/controller/AuthTokenController.java    |   10 +-
 .../apache/hertzbeat/manager/dao/AuthTokenDao.java |   39 +
 .../hertzbeat/manager/service/AccountService.java  |   22 +
 .../manager/service/impl/AccountServiceImpl.java   |  133 +-
 .../config/ApiTokenValidationFilterTest.java       |  115 +-
 .../config/OtlpRouteAuthorizationConfigTest.java   |   80 +
 .../controller/AuthTokenControllerTest.java        |   50 +-
 .../manager/service/AccountServiceTest.java        |  205 +-
 hertzbeat-manager/src/test/resources/sureness.yml  |   21 +-
 .../observability/config/OpenTelemetryConfig.java  |   35 +-
 .../ingestion/adapter/OtlpLogProtocolAdapter.java  |   65 +-
 .../GreptimeOtlpIngestionAuditEventReader.java     |  410 ++
 .../audit/GreptimeOtlpIngestionAuditEventSink.java |  300 ++
 .../OtlpIngestionAuditEvent.java}                  |   33 +-
 .../audit/OtlpIngestionAuditEventReader.java       |   62 +
 .../OtlpIngestionAuditEventSink.java}              |   21 +-
 .../ingestion/audit/OtlpIngestionAuditService.java |  188 +
 .../ingestion/config/OtlpGrpcServerConfig.java     |   85 +-
 .../controller/OtlpIngestionController.java        |   11 +
 .../ingestion/controller/OtlpLogController.java    |  143 +-
 .../controller/OtlpSignalIngestionController.java  |    9 +-
 .../ingestion/enricher/OtlpCorrelationContext.java |    6 +
 .../enricher/OtlpCorrelationEnricher.java          |   70 +-
 .../enricher/OtlpEntityIdentityResolver.java       |  238 +
 .../error/OtlpIngestionBackpressureHeaders.java    |  126 +
 .../error/OtlpIngestionErrorResponseFactory.java   |  260 ++
 .../forwarder/GreptimeApmFlowInitializer.java      |  176 +-
 .../forwarder/GreptimeLogPipelineInitializer.java  |   68 +-
 .../ingestion/forwarder/GreptimeOtlpForwarder.java |  237 +-
 .../forwarder/GreptimeTraceTableInitializer.java   |  153 +
 .../governance/OtlpIngestionGovernanceService.java |  150 +
 .../ingestion/quota/OtlpIngestionQuotaService.java |  271 ++
 .../red/OtlpIngestionRedSummaryService.java        |  478 ++
 .../redaction/OtlpIngestionRedactionService.java   |  160 +
 .../ingestion/retry/OtlpIngestionRetryService.java |  265 ++
 .../OtlpIngestionRequestContextResolver.java       |   53 +
 .../semantic/OtlpResourceSemanticAttributes.java   |   70 +
 .../service/OtlpGrpcIngestionService.java          |    2 +
 .../service/impl/OtlpGrpcIngestionServiceImpl.java | 1232 +++++-
 .../impl/OtlpIngestionWorkspaceServiceImpl.java    |  160 +-
 .../logs/service/impl/LogQueryServiceImpl.java     |  110 +-
 .../logs/service/impl/LogSseServiceImpl.java       |   11 +
 .../logs/sse/LogSseFilterCriteria.java             |   53 +
 .../service/impl/TelemetryIntakeServiceImpl.java   |  102 +-
 .../service/impl/EntityTraceQueryServiceImpl.java  |  322 +-
 .../greptime/flows/hertzbeat_apm_red_1m.sql        |   16 +-
 .../main/resources/greptime/tables/hzb_traces.sql  |   21 +
 .../config/OpenTelemetryConfigTest.java            |   59 +
 .../adapter/OtlpLogProtocolAdapterTest.java        |  147 +
 .../GreptimeOtlpIngestionAuditEventReaderTest.java |  967 ++++
 .../GreptimeOtlpIngestionAuditEventSinkTest.java   |  772 ++++
 .../audit/OtlpIngestionAuditEventReaderTest.java   |  127 +
 .../audit/OtlpIngestionAuditServiceTest.java       |  135 +
 .../OtlpIngestionAuditSourceOwnershipTest.java     |   57 +
 .../ingestion/config/OtlpGrpcServerConfigTest.java |   56 +-
 .../controller/OtlpIngestionControllerTest.java    |   71 +-
 .../OtlpLogControllerSourceOwnershipTest.java      |   42 +
 .../controller/OtlpLogControllerTest.java          |  437 +-
 .../OtlpSignalIngestionControllerTest.java         |   25 +
 .../enricher/OtlpCorrelationEnricherTest.java      |  147 +
 .../enricher/OtlpEntityIdentityResolverTest.java   |  161 +
 .../OtlpIngestionBackpressureHeadersTest.java      |  101 +
 .../OtlpIngestionErrorResponseFactoryTest.java     |  268 ++
 .../OtlpIngestionErrorSourceOwnershipTest.java     |   68 +
 .../forwarder/GreptimeApmFlowInitializerTest.java  |  249 +-
 .../GreptimeLogPipelineInitializerTest.java        |  216 +-
 .../forwarder/GreptimeOtlpForwarderTest.java       |  913 +++-
 .../GreptimeTraceTableInitializerTest.java         |  268 ++
 .../OtlpIngestionGovernanceServiceTest.java        |  101 +
 .../quota/OtlpIngestionQuotaServiceTest.java       |  236 +
 .../OtlpIngestionQuotaSourceOwnershipTest.java     |   70 +
 .../red/OtlpIngestionRedSummaryServiceTest.java    |  846 ++++
 ...OtlpIngestionRedSummarySourceOwnershipTest.java |   56 +
 .../OtlpIngestionRedactionServiceTest.java         |  121 +
 .../retry/OtlpIngestionRetryServiceTest.java       |  416 ++
 .../OtlpIngestionRequestContextResolverTest.java   |   84 +
 ...IngestionRequestContextSourceOwnershipTest.java |   55 +
 .../OtlpResourceSemanticAttributesTest.java        |   39 +
 .../impl/OtlpGrpcIngestionServiceImplTest.java     | 4620 +++++++++++++++++++-
 .../OtlpIngestionWorkspaceServiceImplTest.java     |  149 +-
 .../logs/controller/LogQueryControllerTest.java    |   95 +
 .../logs/controller/LogSseControllerTest.java      |   29 +
 .../impl/TelemetryIntakeServiceImplTest.java       |  115 +
 .../impl/EntityTraceQueryServiceImplTest.java      |  484 +-
 .../h2/V200__create_entity_foundation.sql          |   24 +-
 .../V213__backfill_entity_governance_workspace.sql |   32 +
 .../mysql/V200__create_entity_foundation.sql       |   20 +-
 .../V213__backfill_entity_governance_workspace.sql |   73 +
 .../postgresql/V200__create_entity_foundation.sql  |   24 +-
 .../V213__backfill_entity_governance_workspace.sql |   32 +
 hertzbeat-startup/src/main/resources/sureness.yml  |   21 +-
 .../ReleaseReadinessRuntimeAssemblyTest.java       |  131 +
 .../warehouse/db/GreptimeSqlQueryExecutor.java     |   75 +-
 .../repository/GreptimeTraceQueryRepository.java   |  469 +-
 .../warehouse/repository/TraceQueryRepository.java |  153 +
 .../warehouse/db/GreptimeSqlQueryExecutorTest.java |  152 +
 .../GreptimeTraceQueryRepositoryTest.java          |  230 +-
 pom.xml                                            |   16 +
 .../hertzbeat-mysql-iotdb/conf/sureness.yml        |   20 +-
 .../hertzbeat-mysql-tdengine/conf/sureness.yml     |   20 +-
 .../conf/sureness.yml                              |   20 +-
 .../conf/sureness.yml                              |   20 +-
 .../conf/sureness.yml                              |   20 +-
 script/sureness.yml                                |   23 +-
 117 files changed, 20829 insertions(+), 817 deletions(-)

diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/OtlpEntityBindingSummaryDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/OtlpEntityBindingSummaryDto.java
index b5a0b5797f..d407148c96 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/OtlpEntityBindingSummaryDto.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/binding/OtlpEntityBindingSummaryDto.java
@@ -18,6 +18,7 @@
 package org.apache.hertzbeat.common.observability.dto.binding;
 
 import java.util.List;
+import java.util.Map;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
@@ -38,6 +39,8 @@ public class OtlpEntityBindingSummaryDto {
 
     private List<BoundEntity> recentBoundEntities;
 
+    private List<UnboundEntityCandidate> recentUnboundCandidates;
+
     /**
      * Recent canonical identity sample derived from telemetry intake.
      */
@@ -66,4 +69,22 @@ public class OtlpEntityBindingSummaryDto {
         private String primaryIdentityValue;
         private long monitorBindCount;
     }
+
+    /**
+     * Recent telemetry identity that has not been confirmed as a HertzBeat entity.
+     */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class UnboundEntityCandidate {
+        private String suggestedName;
+        private String suggestedType;
+        private String namespace;
+        private String environment;
+        private String primaryIdentityKey;
+        private String primaryIdentityValue;
+        private List<String> signals;
+        private Map<String, String> canonicalIdentities;
+        private Long latestObservedAt;
+    }
 }
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionRedSummaryDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionRedSummaryDto.java
new file mode 100644
index 0000000000..fbe6c83185
--- /dev/null
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/ingestion/OtlpIngestionRedSummaryDto.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.observability.dto.ingestion;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Recent OTLP ingest request/error/duration summary.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class OtlpIngestionRedSummaryDto {
+
+    private String workspaceId;
+
+    private long requestCount;
+
+    private double requestRatePerMinute;
+
+    private long errorCount;
+
+    private double errorRate;
+
+    private long requestBytes;
+
+    private long signalItems;
+
+    private Long averageSignalItems;
+
+    private Long maxSignalItems;
+
+    private Long averageDurationMillis;
+
+    private Long maxDurationMillis;
+
+    private Long latestObservedAt;
+
+    private List<SignalRedMetric> signals;
+
+    /**
+     * Signal and protocol-level RED summary.
+     */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class SignalRedMetric {
+        private String signal;
+        private String protocol;
+        private long requestCount;
+        private double requestRatePerMinute;
+        private long errorCount;
+        private double errorRate;
+        private long requestBytes;
+        private long signalItems;
+        private Long averageSignalItems;
+        private Long maxSignalItems;
+        private Long averageDurationMillis;
+        private Long maxDurationMillis;
+        private String latestStatusCode;
+        private String latestReason;
+        private Long latestObservedAt;
+    }
+}
diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/OtlpWorkspaceDtoMigrationTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/OtlpWorkspaceDtoMigrationTest.java
index 9208f57b1f..56f77cffd9 100644
--- a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/OtlpWorkspaceDtoMigrationTest.java
+++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/OtlpWorkspaceDtoMigrationTest.java
@@ -70,7 +70,11 @@ class OtlpWorkspaceDtoMigrationTest {
                 List.of("service.name"),
                 List.of("checkout"),
                 List.of(new OtlpEntityBindingSummaryDto.CanonicalIdentitySample("service.name", "checkout", "logs")),
-                List.of(new OtlpEntityBindingSummaryDto.BoundEntity(1L, "service", "checkout", "Checkout", "commerce", "service.name", "checkout", 2L))
+                List.of(new OtlpEntityBindingSummaryDto.BoundEntity(1L, "service", "checkout", "Checkout",
+                        "commerce", "service.name", "checkout", 2L)),
+                List.of(new OtlpEntityBindingSummaryDto.UnboundEntityCandidate(
+                        "billing", "service", "commerce", "prod", "service.name", "billing",
+                        List.of("metrics"), Map.of("service.name", "billing"), 2000L))
         );
         TelemetryIdentitySnapshot identitySnapshot = new TelemetryIdentitySnapshot(
                 "otlp",
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java
index 1845c36e91..0ce7721dad 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java
@@ -29,6 +29,7 @@ import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.springframework.data.annotation.CreatedBy;
 import org.springframework.data.annotation.CreatedDate;
 import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@@ -72,6 +73,16 @@ public class AuthToken {
     @Column(length = 64)
     private String tokenMask;
 
+    @Schema(title = "Token scope", example = "api-admin", accessMode = READ_WRITE)
+    @Column(name = "token_scope", length = 32)
+    @Builder.Default
+    private String tokenScope = AuthTokenScopes.API_ADMIN;
+
+    @Schema(title = "Token workspace boundary", example = "default", accessMode = READ_WRITE)
+    @Column(name = "workspace_id", length = 64)
+    @Builder.Default
+    private String workspaceId = AuthTokenScopes.DEFAULT_WORKSPACE_ID;
+
     @Schema(title = "Token status: 0-active", accessMode = READ_ONLY)
     @Column(nullable = false)
     @Builder.Default
@@ -91,4 +102,11 @@ public class AuthToken {
     @Schema(title = "Token last used time", accessMode = READ_ONLY)
     private LocalDateTime lastUsedTime;
 
+    @Schema(title = "Token revoked time", accessMode = READ_ONLY)
+    private LocalDateTime revokedTime;
+
+    @Schema(title = "Token revoker", accessMode = READ_ONLY)
+    @Column(length = 64)
+    private String revokedBy;
+
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityDefinitionActivity.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityDefinitionActivity.java
index 8d26a2a722..14e8a7d5d1 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityDefinitionActivity.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityDefinitionActivity.java
@@ -64,6 +64,12 @@ public class EntityDefinitionActivity {
     @Column(name = "entity_id", nullable = false)
     private Long entityId;
 
+    @Schema(title = "Workspace ID", example = "default", accessMode = READ_WRITE)
+    @Size(max = 64)
+    @Builder.Default
+    @Column(name = "workspace_id", length = 64)
+    private String workspaceId = "default";
+
     @Schema(title = "Activity type", example = "definition_import", accessMode = READ_WRITE)
     @Size(max = 32)
     @Column(name = "activity_type", length = 32, nullable = false)
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityGovernanceState.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityGovernanceState.java
index bf900fccc9..101199e916 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityGovernanceState.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/EntityGovernanceState.java
@@ -37,6 +37,7 @@ import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.springframework.data.annotation.CreatedBy;
 import org.springframework.data.annotation.CreatedDate;
 import org.springframework.data.annotation.LastModifiedDate;
@@ -49,11 +50,13 @@ import tools.jackson.databind.JsonNode;
 @Entity
 @Table(name = "hzb_entity_governance_state",
         uniqueConstraints = {
-                @UniqueConstraint(name = "uk_hzb_entity_governance_state_scope_kind_key",
-                        columnNames = {"state_scope", "state_kind", "state_key"})
+                @UniqueConstraint(name = "uk_hzb_entity_governance_state_scope_kind_workspace_key",
+                        columnNames = {"state_scope", "state_kind", "workspace_id", "state_key"})
         },
         indexes = {
                 @Index(name = "idx_hzb_entity_governance_state_scope_kind", columnList = "state_scope,state_kind"),
+                @Index(name = "idx_hzb_entity_governance_state_scope_kind_workspace",
+                        columnList = "state_scope,state_kind,workspace_id"),
                 @Index(name = "idx_hzb_entity_governance_state_update", columnList = "gmt_update"),
                 @Index(name = "idx_hzb_entity_governance_state_creator", columnList = "creator")
         })
@@ -80,6 +83,12 @@ public class EntityGovernanceState {
     @Column(name = "state_kind", length = 32, nullable = false)
     private String stateKind;
 
+    @Schema(title = "Workspace ID", example = "default", accessMode = READ_WRITE)
+    @Size(max = 64)
+    @Column(name = "workspace_id", length = 64)
+    @Builder.Default
+    private String workspaceId = AuthTokenScopes.DEFAULT_WORKSPACE_ID;
+
     @Schema(title = "State key", example = "preset-1710000000", accessMode = READ_WRITE)
     @Size(max = 128)
     @Column(name = "state_key", length = 128, nullable = false)
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ObserveEntity.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ObserveEntity.java
index 86405384e7..28619ef7c9 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ObserveEntity.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ObserveEntity.java
@@ -33,9 +33,11 @@ import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
 import lombok.AllArgsConstructor;
+import lombok.Builder.Default;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.common.entity.alerter.JsonMapAttributeConverter;
 import org.springframework.data.annotation.CreatedBy;
 import org.springframework.data.annotation.CreatedDate;
@@ -52,7 +54,8 @@ import tools.jackson.databind.JsonNode;
         @Index(name = "idx_hzb_entity_type", columnList = "entity_type"),
         @Index(name = "idx_hzb_entity_status", columnList = "status"),
         @Index(name = "idx_hzb_entity_name", columnList = "name"),
-        @Index(name = "idx_hzb_entity_owner", columnList = "owner")
+        @Index(name = "idx_hzb_entity_owner", columnList = "owner"),
+        @Index(name = "idx_hzb_entity_workspace", columnList = "workspace_id")
 })
 @Data
 @Builder
@@ -211,6 +214,12 @@ public class ObserveEntity {
     @Column(columnDefinition = "TEXT")
     private List<String> tags;
 
+    @Schema(title = "Entity workspace boundary", example = "default", accessMode = READ_WRITE)
+    @Size(max = 64)
+    @Column(name = "workspace_id", length = 64)
+    @Default
+    private String workspaceId = AuthTokenScopes.DEFAULT_WORKSPACE_ID;
+
     @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
     @CreatedBy
     private String creator;
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/AuthTokenRequestContext.java
similarity index 54%
copy from hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
copy to hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/AuthTokenRequestContext.java
index d4398dd193..032895eadf 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/AuthTokenRequestContext.java
@@ -15,24 +15,34 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.observability.ingestion.enricher;
+package org.apache.hertzbeat.common.observability.gateway;
 
-import java.util.UUID;
 import org.apache.commons.lang3.StringUtils;
 
 /**
- * Request-scoped HertzBeat correlation values to inject into OTLP records.
+ * Per-request authentication boundary for downstream workspace-aware services.
  */
-public record OtlpCorrelationContext(String ingestId, String entityId, String workspaceId) {
+public final class AuthTokenRequestContext {
 
-    public static OtlpCorrelationContext empty() {
-        return new OtlpCorrelationContext(null, null, null);
+    private static final ThreadLocal<String> WORKSPACE_ID = new ThreadLocal<>();
+
+    private AuthTokenRequestContext() {
     }
 
-    OtlpCorrelationContext withIngestId() {
-        if (StringUtils.isNotBlank(ingestId)) {
-            return this;
+    public static void bindWorkspaceId(String workspaceId) {
+        String normalized = StringUtils.trimToNull(workspaceId);
+        if (normalized == null) {
+            clear();
+            return;
         }
-        return new OtlpCorrelationContext(UUID.randomUUID().toString(), entityId, workspaceId);
+        WORKSPACE_ID.set(AuthTokenScopes.normalizeWorkspaceId(normalized));
+    }
+
+    public static String currentWorkspaceId() {
+        return WORKSPACE_ID.get();
+    }
+
+    public static void clear() {
+        WORKSPACE_ID.remove();
     }
 }
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/AuthTokenScopes.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/AuthTokenScopes.java
new file mode 100644
index 0000000000..dc6671135f
--- /dev/null
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/AuthTokenScopes.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.observability.gateway;
+
+import java.util.Locale;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Shared API token scope names and matching rules.
+ */
+public final class AuthTokenScopes {
+
+    public static final String CLAIM_TOKEN_SCOPE = "token_scope";
+    public static final String CLAIM_WORKSPACE_ID = "workspace_id";
+    public static final String WORKSPACE_ID_HEADER = "X-HertzBeat-Workspace-Id";
+    public static final String DEFAULT_WORKSPACE_ID = "default";
+    public static final String UI_SESSION = "ui-session";
+    public static final String API_ADMIN = "api-admin";
+    public static final String READONLY_QUERY = "readonly-query";
+    public static final String OTLP_INGEST = "otlp-ingest";
+
+    private static final Set<String> API_TOKEN_SCOPES = Set.of(API_ADMIN, READONLY_QUERY, OTLP_INGEST);
+    private static final Set<String> REQUIRED_SCOPES = Set.of(API_ADMIN, READONLY_QUERY, OTLP_INGEST, UI_SESSION);
+
+    private AuthTokenScopes() {
+    }
+
+    public static String normalizeApiTokenScope(String tokenScope) {
+        String normalized = normalizeScopeValue(tokenScope, API_ADMIN);
+        if (!API_TOKEN_SCOPES.contains(normalized)) {
+            throw new IllegalArgumentException("Unsupported API token scope: " + tokenScope);
+        }
+        return normalized;
+    }
+
+    public static String normalizeRequiredScope(String requiredScope) {
+        String normalized = normalizeScopeValue(requiredScope, API_ADMIN);
+        if (!REQUIRED_SCOPES.contains(normalized)) {
+            throw new IllegalArgumentException("Unsupported required token scope: " + requiredScope);
+        }
+        return normalized;
+    }
+
+    public static Set<String> allowedTokenScopesFor(String requiredScope) {
+        return switch (normalizeRequiredScope(requiredScope)) {
+            case API_ADMIN -> Set.of(API_ADMIN);
+            case READONLY_QUERY -> Set.of(API_ADMIN, READONLY_QUERY);
+            case OTLP_INGEST -> Set.of(API_ADMIN, OTLP_INGEST);
+            case UI_SESSION -> Set.of(UI_SESSION);
+            default -> Set.of();
+        };
+    }
+
+    public static String normalizeWorkspaceId(String workspaceId) {
+        String normalized = StringUtils.trimToNull(workspaceId);
+        return normalized == null ? DEFAULT_WORKSPACE_ID : normalized;
+    }
+
+    private static String normalizeScopeValue(String scope, String defaultScope) {
+        if (StringUtils.isBlank(scope)) {
+            return defaultScope;
+        }
+        return scope.trim().toLowerCase(Locale.ROOT);
+    }
+}
diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/ObservabilityAccessTokenGateway.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/ObservabilityAccessTokenGateway.java
index 377d4aefd9..a78f3bc408 100644
--- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/ObservabilityAccessTokenGateway.java
+++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/observability/gateway/ObservabilityAccessTokenGateway.java
@@ -32,7 +32,30 @@ public interface ObservabilityAccessTokenGateway {
      * @param tokenValue raw token value
      * @return null when token is valid, otherwise rejection reason
      */
-    String checkTokenStatus(String tokenValue);
+    default String checkTokenStatus(String tokenValue) {
+        return checkTokenStatus(tokenValue, AuthTokenScopes.API_ADMIN);
+    }
+
+    /**
+     * Check the status of a managed token for a required scope.
+     *
+     * @param tokenValue raw token value
+     * @param requiredScope required access boundary
+     * @return null when token is valid for the required scope, otherwise rejection reason
+     */
+    String checkTokenStatus(String tokenValue, String requiredScope);
+
+    /**
+     * Check the status of a managed token for a required scope and workspace boundary.
+     *
+     * @param tokenValue raw token value
+     * @param requiredScope required access boundary
+     * @param workspaceId requested workspace boundary
+     * @return null when token is valid for the required scope/workspace, otherwise rejection reason
+     */
+    default String checkTokenStatus(String tokenValue, String requiredScope, String workspaceId) {
+        return checkTokenStatus(tokenValue, requiredScope);
+    }
 
     /**
      * Check whether the owner behind a managed token is still allowed.
diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/EntitySchemaLegacyColumnCompatibilityTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/EntitySchemaLegacyColumnCompatibilityTest.java
new file mode 100644
index 0000000000..48c473fac2
--- /dev/null
+++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/EntitySchemaLegacyColumnCompatibilityTest.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.manager;
+
+import jakarta.persistence.Column;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Flyway keeps these columns strict for fresh installs; JPA metadata must stay
+ * legacy-upgrade friendly so Hibernate can add columns to existing local H2 tables.
+ */
+class EntitySchemaLegacyColumnCompatibilityTest {
+
+    @Test
+    void additiveBackfillColumnsShouldRemainJpaNullable() throws NoSuchFieldException {
+        assertColumnNullable(AuthToken.class, "tokenScope");
+        assertColumnNullable(AuthToken.class, "workspaceId");
+        assertColumnNullable(ObserveEntity.class, "workspaceId");
+        assertColumnNullable(EntityDefinitionActivity.class, "workspaceId");
+        assertColumnNullable(EntityGovernanceState.class, "workspaceId");
+    }
+
+    private static void assertColumnNullable(Class<?> entityClass, String fieldName) throws NoSuchFieldException {
+        Column column = entityClass.getDeclaredField(fieldName).getAnnotation(Column.class);
+
+        assertTrue(column.nullable(), () -> entityClass.getSimpleName() + "." + fieldName
+                + " must allow legacy Hibernate add-column backfill; enforce NOT NULL in Flyway instead.");
+    }
+}
diff --git a/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml
index 9809632a67..7506824b2d 100644
--- a/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml
@@ -22,13 +22,25 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  - /api/monitor===get===[admin,user,guest]
   - /api/monitor/**===get===[admin,user,guest]
+  - /api/monitor===post===[admin,user]
   - /api/monitor/**===post===[admin,user]
+  - /api/monitor===put===[admin,user]
   - /api/monitor/**===put===[admin,user]
-  - /api/monitor/**===delete==[admin]
+  - /api/monitor===delete===[admin]
+  - /api/monitor/**===delete===[admin]
+  - /api/monitors===get===[admin,user,guest]
+  - /api/monitors/export===get===[admin]
+  - /api/monitors/export/all===get===[admin]
+  - /api/monitors/manage===get===[admin,user,guest]
+  - /api/monitors/manage===post===[admin,user]
+  - /api/monitors/manage===put===[admin,user]
   - /api/monitors/**===get===[admin,user,guest]
   - /api/monitors/**===post===[admin,user]
   - /api/monitors/**===put===[admin,user]
+  - /api/monitors===delete===[admin]
+  - /api/monitors/manage===delete===[admin]
   - /api/monitors/**===delete===[admin]
   - /api/alert/**===get===[admin,user,guest]
   - /api/alert/**===post===[admin,user]
@@ -62,6 +74,8 @@ resourceRole:
   - /api/grafana/**===post===[admin,user]
   - /api/grafana/**===put===[admin,user]
   - /api/grafana/**===delete===[admin]
+  - /api/otlp/**===post===[admin,user]
+  - /api/logs/otlp/**===post===[admin,user]
   - /api/bulletin/**===get===[admin,user,guest]
   - /api/bulletin/**===post===[admin,user]
   - /api/bulletin/**===put===[admin,user]
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
index 4075d2e1d6..ed4001ea37 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
@@ -30,8 +30,11 @@ import java.util.List;
 import java.util.Map;
 
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
 import org.apache.hertzbeat.common.constants.NetworkConstants;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.common.util.JsonUtil;
 import org.apache.hertzbeat.manager.service.AccountService;
 import org.apache.hertzbeat.manager.service.impl.AccountServiceImpl;
@@ -73,7 +76,9 @@ public class ApiTokenValidationFilter implements HandlerInterceptor {
     @Override
     public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler)
         throws IOException {
+        AuthTokenRequestContext.clear();
         SubjectSum subject = SurenessContextHolder.getBindSubject();
+        bindRequestedWorkspace(request, subject);
         if (subject == null || !isManagedToken(subject)) {
             return true;
         }
@@ -83,7 +88,11 @@ public class ApiTokenValidationFilter implements HandlerInterceptor {
             String token = authorization.substring(DispatchConstants.BEARER.length()).trim();
             if (!token.isEmpty()) {
                 try {
-                    String rejectReason = checkManagedToken(subject, token);
+                    String rejectReason = checkManagedToken(
+                            subject,
+                            token,
+                            resolveRequiredScope(request),
+                            AuthTokenRequestContext.currentWorkspaceId());
                     if (rejectReason != null) {
                         return writeError(response, HttpStatus.UNAUTHORIZED, rejectReason);
                     }
@@ -97,6 +106,12 @@ public class ApiTokenValidationFilter implements HandlerInterceptor {
         return true;
     }
 
+    @Override
+    public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
+                                @NonNull Object handler, Exception ex) {
+        AuthTokenRequestContext.clear();
+    }
+
     /**
      * Check if a managed token should be rejected.
      * <p>
@@ -108,8 +123,10 @@ public class ApiTokenValidationFilter implements HandlerInterceptor {
      * If the token is valid, updates the last used time.
      * </p>
      */
-    private String checkManagedToken(SubjectSum subject, String token) {
-        String rejectReason = accountService.checkTokenStatus(token);
+    private String checkManagedToken(SubjectSum subject, String token, String requiredScope, String workspaceId) {
+        String rejectReason = StringUtils.isBlank(workspaceId)
+                ? accountService.checkTokenStatus(token, requiredScope)
+                : accountService.checkTokenStatus(token, requiredScope, workspaceId);
         if (rejectReason != null) {
             return rejectReason;
         }
@@ -156,6 +173,50 @@ public class ApiTokenValidationFilter implements HandlerInterceptor {
         return principal == null ? null : String.valueOf(principal);
     }
 
+    private String resolveRequiredScope(HttpServletRequest request) {
+        String requestUri = request.getRequestURI();
+        if (requestUri != null
+            && (requestUri.startsWith("/api/otlp/") || requestUri.startsWith("/api/logs/otlp"))) {
+            return AuthTokenScopes.OTLP_INGEST;
+        }
+        String method = request.getMethod();
+        if ("GET".equalsIgnoreCase(method) || "HEAD".equalsIgnoreCase(method) || "OPTIONS".equalsIgnoreCase(method)) {
+            return AuthTokenScopes.READONLY_QUERY;
+        }
+        return AuthTokenScopes.API_ADMIN;
+    }
+
+    private void bindRequestedWorkspace(HttpServletRequest request, SubjectSum subject) {
+        String workspaceId = resolveRequestedWorkspaceId(request, subject);
+        if (workspaceId != null) {
+            AuthTokenRequestContext.bindWorkspaceId(workspaceId);
+        }
+    }
+
+    private String resolveRequestedWorkspaceId(HttpServletRequest request, SubjectSum subject) {
+        String workspaceId = StringUtils.trimToNull(request.getHeader(AuthTokenScopes.WORKSPACE_ID_HEADER));
+        if (workspaceId != null) {
+            return workspaceId;
+        }
+        workspaceId = StringUtils.trimToNull(request.getParameter("workspaceId"));
+        if (workspaceId != null) {
+            return workspaceId;
+        }
+        workspaceId = StringUtils.trimToNull(request.getParameter("workspace_id"));
+        if (workspaceId != null) {
+            return workspaceId;
+        }
+        return resolveSubjectWorkspaceId(subject);
+    }
+
+    private String resolveSubjectWorkspaceId(SubjectSum subject) {
+        if (subject == null || subject.getPrincipalMap() == null) {
+            return null;
+        }
+        Object workspaceId = subject.getPrincipalMap().getPrincipal(AuthTokenScopes.CLAIM_WORKSPACE_ID);
+        return workspaceId == null ? null : StringUtils.trimToNull(String.valueOf(workspaceId));
+    }
+
     private boolean writeError(HttpServletResponse response, HttpStatus status, String message) throws IOException {
         response.setStatus(status.value());
         response.setCharacterEncoding("UTF-8");
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java
index 4aa3abb954..5b17e8fae6 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java
@@ -28,8 +28,10 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.hertzbeat.common.entity.dto.Message;
 import org.apache.hertzbeat.common.entity.manager.AuthToken;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.manager.service.AccountService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
@@ -57,7 +59,9 @@ public class AuthTokenController {
     @Operation(summary = "Generate a new API token", description = "Generate a new API token for integrations, optionally with expiration")
     public ResponseEntity<Message<Map<String, String>>> generateToken(
             @RequestParam(value = "name", required = false) @Parameter(description = "Token name/description") String name,
-            @RequestParam(value = "expireSeconds", required = false) @Parameter(description = "Expiration time in seconds, null means never expire") Long expireSeconds) {
+            @RequestParam(value = "expireSeconds", required = false) @Parameter(description = "Expiration time in seconds, null means never expire") Long expireSeconds,
+            @RequestParam(value = "scope", required = false, defaultValue = AuthTokenScopes.API_ADMIN) @Parameter(description = "Token access scope") String scope,
+            @RequestParam(value = "workspaceId", required = false) @Parameter(description = "Token workspace boundary") String workspaceId) {
         SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
         if (subjectSum == null) {
             return ResponseEntity.ok(Message.fail(FAIL_CODE, "No login user"));
@@ -66,7 +70,9 @@ public class AuthTokenController {
             return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission"));
         }
         try {
-            String token = accountService.generateToken(name, expireSeconds);
+            String token = StringUtils.isBlank(workspaceId)
+                    ? accountService.generateToken(name, expireSeconds, scope)
+                    : accountService.generateToken(name, expireSeconds, scope, workspaceId);
             Map<String, String> rep = Collections.singletonMap("token", token);
             return ResponseEntity.ok(Message.success(rep));
         } catch (Exception e) {
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java
index d185cdaecd..49b0f1be50 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java
@@ -26,6 +26,7 @@ import org.springframework.data.repository.query.Param;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -43,11 +44,49 @@ public interface AuthTokenDao extends JpaRepository<AuthToken, Long>, JpaSpecifi
      */
     List<AuthToken> findByStatusAndCreator(Byte status, String creator);
 
+    /**
+     * Find active tokens inside one workspace.
+     */
+    List<AuthToken> findByStatusAndWorkspaceId(Byte status, String workspaceId);
+
+    /**
+     * Find active tokens created by the given user inside one workspace.
+     */
+    List<AuthToken> findByStatusAndCreatorAndWorkspaceId(Byte status, String creator, String workspaceId);
+
     /**
      * Check if an active token exists with the given hash
      */
     boolean existsByTokenHashAndStatus(String tokenHash, Byte status);
 
+    /**
+     * Check if an active token exists with the given hash and one of the allowed scopes
+     */
+    boolean existsByTokenHashAndStatusAndTokenScopeIn(String tokenHash, Byte status, Collection<String> tokenScopes);
+
+    /**
+     * Check if an active token exists with the given hash, scope, and workspace boundary.
+     */
+    boolean existsByTokenHashAndStatusAndTokenScopeInAndWorkspaceId(
+            String tokenHash,
+            Byte status,
+            Collection<String> tokenScopes,
+            String workspaceId);
+
+    /**
+     * Count active tokens created by the given user for one scope.
+     */
+    long countByStatusAndCreatorAndTokenScope(Byte status, String creator, String tokenScope);
+
+    /**
+     * Count active tokens created by the given user for one scope and workspace.
+     */
+    long countByStatusAndCreatorAndTokenScopeAndWorkspaceId(
+            Byte status,
+            String creator,
+            String tokenScope,
+            String workspaceId);
+
     /**
      * Update last used time for a token identified by its hash
      */
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java
index 249c43e213..b2ff12b0b9 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java
@@ -57,6 +57,28 @@ public interface AccountService extends ObservabilityAccessTokenGateway {
      */
     String generateToken(String tokenName, Long expireSeconds) throws AuthenticationException;
 
+    /**
+     * Generate a managed API token with a specific access scope.
+     *
+     * @param tokenName     optional name/description for the token
+     * @param expireSeconds optional expiration time in seconds, null means never expire
+     * @param tokenScope    token access scope
+     * @return token string
+     */
+    String generateToken(String tokenName, Long expireSeconds, String tokenScope) throws AuthenticationException;
+
+    /**
+     * Generate a managed API token with a specific access scope and workspace boundary.
+     *
+     * @param tokenName     optional name/description for the token
+     * @param expireSeconds optional expiration time in seconds, null means never expire
+     * @param tokenScope    token access scope
+     * @param workspaceId   token workspace boundary
+     * @return token string
+     */
+    String generateToken(String tokenName, Long expireSeconds, String tokenScope, String workspaceId)
+            throws AuthenticationException;
+
     /**
      * List all API tokens
      *
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java
index 1cadceb28d..257a8fc42b 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java
@@ -31,6 +31,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hertzbeat.alert.util.CryptoUtils;
 import org.apache.hertzbeat.common.entity.manager.AuthToken;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.common.util.JsonUtil;
 import org.apache.hertzbeat.manager.dao.AuthTokenDao;
 import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
@@ -47,6 +49,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
@@ -72,6 +75,10 @@ public class AccountServiceImpl implements AccountService {
 
     private static final byte TOKEN_STATUS_ACTIVE = 0;
 
+    private static final byte TOKEN_STATUS_REVOKED = 1;
+
+    private static final long MAX_ACTIVE_TOKENS_PER_SCOPE_PER_USER = 20;
+
     /**
      * Custom JWT claim key to mark tokens as managed (persisted in DB for lifecycle management).
      * Only tokens with this claim will be validated against the database.
@@ -169,6 +176,17 @@ public class AccountServiceImpl implements AccountService {
 
     @Override
     public String generateToken(String tokenName, Long expireSeconds) throws AuthenticationException {
+        return generateToken(tokenName, expireSeconds, AuthTokenScopes.API_ADMIN);
+    }
+
+    @Override
+    public String generateToken(String tokenName, Long expireSeconds, String tokenScope) throws AuthenticationException {
+        return generateToken(tokenName, expireSeconds, tokenScope, AuthTokenScopes.DEFAULT_WORKSPACE_ID);
+    }
+
+    @Override
+    public String generateToken(String tokenName, Long expireSeconds, String tokenScope, String workspaceId)
+            throws AuthenticationException {
         SubjectSum subjectSum = requireCurrentSubject();
         String userId = getCurrentUserId(subjectSum);
         SurenessAccount account = accountProvider.loadAccount(userId);
@@ -178,8 +196,15 @@ public class AccountServiceImpl implements AccountService {
         if (account.isDisabledAccount() || account.isExcessiveAttempts()) {
             throw new AuthenticationException("Expired or Illegal Account");
         }
+        String normalizedScope = AuthTokenScopes.normalizeApiTokenScope(tokenScope);
+        String normalizedWorkspaceId = AuthTokenScopes.normalizeWorkspaceId(workspaceId);
+        long activeTokens = authTokenDao.countByStatusAndCreatorAndTokenScopeAndWorkspaceId(
+            TOKEN_STATUS_ACTIVE, userId, normalizedScope, normalizedWorkspaceId);
+        if (activeTokens >= MAX_ACTIVE_TOKENS_PER_SCOPE_PER_USER) {
+            throw new AuthenticationException("Token quota exceeded");
+        }
         List<String> roles = account.getOwnRoles();
-        String token = issueApiToken(userId, roles, expireSeconds);
+        String token = issueApiToken(userId, roles, expireSeconds, normalizedScope, normalizedWorkspaceId);
 
         // Persist token metadata for management
         String tokenHash = CryptoUtils.sha256Hex(token);
@@ -192,6 +217,8 @@ public class AccountServiceImpl implements AccountService {
             .name(tokenName)
             .tokenHash(tokenHash)
             .tokenMask(tokenMask)
+            .tokenScope(normalizedScope)
+            .workspaceId(normalizedWorkspaceId)
             .status(TOKEN_STATUS_ACTIVE)
             .creator(userId)
             .expireTime(expireTime)
@@ -202,7 +229,10 @@ public class AccountServiceImpl implements AccountService {
     }
 
     private String issueAccessToken(String userId, List<String> roles, long expirationSeconds) {
-        return JsonWebTokenUtil.issueJwt(userId, expirationSeconds, roles, new HashMap<>(0));
+        Map<String, Object> customClaimMap = new HashMap<>(2);
+        customClaimMap.put(AuthTokenScopes.CLAIM_TOKEN_SCOPE, AuthTokenScopes.UI_SESSION);
+        customClaimMap.put(AuthTokenScopes.CLAIM_WORKSPACE_ID, AuthTokenScopes.DEFAULT_WORKSPACE_ID);
+        return JsonWebTokenUtil.issueJwt(userId, expirationSeconds, roles, customClaimMap);
     }
 
     @Override
@@ -211,6 +241,17 @@ public class AccountServiceImpl implements AccountService {
         if (subjectSum == null) {
             return List.of();
         }
+        String workspaceId = AuthTokenRequestContext.currentWorkspaceId();
+        if (StringUtils.isNotBlank(workspaceId)) {
+            String normalizedWorkspaceId = AuthTokenScopes.normalizeWorkspaceId(workspaceId);
+            if (subjectSum.hasRole("admin")) {
+                return authTokenDao.findByStatusAndWorkspaceId(TOKEN_STATUS_ACTIVE, normalizedWorkspaceId);
+            }
+            return authTokenDao.findByStatusAndCreatorAndWorkspaceId(
+                    TOKEN_STATUS_ACTIVE,
+                    getCurrentUserId(subjectSum),
+                    normalizedWorkspaceId);
+        }
         if (subjectSum.hasRole("admin")) {
             return authTokenDao.findByStatus(TOKEN_STATUS_ACTIVE);
         }
@@ -228,9 +269,25 @@ public class AccountServiceImpl implements AccountService {
         if (!subjectSum.hasRole("admin") && !StringUtils.equals(userId, token.getCreator())) {
             throw new AuthenticationException("No permission");
         }
-        tokenStatusCache.invalidate(token.getTokenHash());
-        lastUsedWriteTimestamps.remove(token.getTokenHash());
-        authTokenDao.deleteById(id);
+        String workspaceId = AuthTokenRequestContext.currentWorkspaceId();
+        if (StringUtils.isNotBlank(workspaceId)
+                && !StringUtils.equals(
+                        AuthTokenScopes.normalizeWorkspaceId(workspaceId),
+                        AuthTokenScopes.normalizeWorkspaceId(token.getWorkspaceId()))) {
+            throw new AuthenticationException("No workspace permission");
+        }
+        if (!Byte.valueOf(TOKEN_STATUS_ACTIVE).equals(token.getStatus())) {
+            return;
+        }
+        String tokenHash = token.getTokenHash();
+        if (StringUtils.isNotBlank(tokenHash)) {
+            invalidateTokenStatusCache(tokenHash);
+            lastUsedWriteTimestamps.remove(tokenHash);
+        }
+        token.setStatus(TOKEN_STATUS_REVOKED);
+        token.setRevokedBy(userId);
+        token.setRevokedTime(LocalDateTime.now());
+        authTokenDao.save(token);
     }
 
     @Override
@@ -244,6 +301,49 @@ public class AccountServiceImpl implements AccountService {
         return null;
     }
 
+    @Override
+    public String checkTokenStatus(String tokenValue, String requiredScope) {
+        String tokenHash = CryptoUtils.sha256Hex(tokenValue);
+        String normalizedRequiredScope = AuthTokenScopes.normalizeRequiredScope(requiredScope);
+        Set<String> allowedScopes = AuthTokenScopes.allowedTokenScopesFor(normalizedRequiredScope);
+        Boolean active = tokenStatusCache.get(statusCacheKey(tokenHash, normalizedRequiredScope),
+            cacheKey -> authTokenDao.existsByTokenHashAndStatusAndTokenScopeIn(
+                tokenHash, TOKEN_STATUS_ACTIVE, allowedScopes));
+        if (Boolean.TRUE.equals(active)) {
+            return null;
+        }
+        if (authTokenDao.existsByTokenHashAndStatus(tokenHash, TOKEN_STATUS_ACTIVE)) {
+            return "Token scope is not allowed";
+        }
+        return "Token has been revoked";
+    }
+
+    @Override
+    public String checkTokenStatus(String tokenValue, String requiredScope, String workspaceId) {
+        if (StringUtils.isBlank(workspaceId)) {
+            return checkTokenStatus(tokenValue, requiredScope);
+        }
+        String tokenHash = CryptoUtils.sha256Hex(tokenValue);
+        String normalizedRequiredScope = AuthTokenScopes.normalizeRequiredScope(requiredScope);
+        String normalizedWorkspaceId = AuthTokenScopes.normalizeWorkspaceId(workspaceId);
+        Set<String> allowedScopes = AuthTokenScopes.allowedTokenScopesFor(normalizedRequiredScope);
+        Boolean active = tokenStatusCache.get(
+            statusCacheKey(tokenHash, normalizedRequiredScope, normalizedWorkspaceId),
+            cacheKey -> authTokenDao.existsByTokenHashAndStatusAndTokenScopeInAndWorkspaceId(
+                tokenHash, TOKEN_STATUS_ACTIVE, allowedScopes, normalizedWorkspaceId));
+        if (Boolean.TRUE.equals(active)) {
+            return null;
+        }
+        if (authTokenDao.existsByTokenHashAndStatusAndTokenScopeIn(
+            tokenHash, TOKEN_STATUS_ACTIVE, allowedScopes)) {
+            return "Token workspace is not allowed";
+        }
+        if (authTokenDao.existsByTokenHashAndStatus(tokenHash, TOKEN_STATUS_ACTIVE)) {
+            return "Token scope is not allowed";
+        }
+        return "Token has been revoked";
+    }
+
     @Override
     public String checkManagedTokenAccess(String userId, List<String> claimedRoles) {
         if (StringUtils.isBlank(userId)) {
@@ -282,9 +382,15 @@ public class AccountServiceImpl implements AccountService {
      *
      * @param expireSeconds optional expiration in seconds, null means never expire
      */
-    private String issueApiToken(String userId, List<String> roles, Long expireSeconds) {
-        Map<String, Object> customClaimMap = new HashMap<>(2);
+    private String issueApiToken(String userId,
+                                 List<String> roles,
+                                 Long expireSeconds,
+                                 String tokenScope,
+                                 String workspaceId) {
+        Map<String, Object> customClaimMap = new HashMap<>(4);
         customClaimMap.put(CLAIM_MANAGED, true);
+        customClaimMap.put(AuthTokenScopes.CLAIM_TOKEN_SCOPE, tokenScope);
+        customClaimMap.put(AuthTokenScopes.CLAIM_WORKSPACE_ID, workspaceId);
         long effectiveExpire = (expireSeconds != null && expireSeconds > 0) ? expireSeconds : NON_EXPIRING_TOKEN_SECONDS;
         return JsonWebTokenUtil.issueJwt(userId, effectiveExpire, roles, customClaimMap);
     }
@@ -311,4 +417,17 @@ public class AccountServiceImpl implements AccountService {
         Object principal = subjectSum.getPrincipal();
         return principal == null ? null : String.valueOf(principal);
     }
+
+    private void invalidateTokenStatusCache(String tokenHash) {
+        tokenStatusCache.invalidate(tokenHash);
+        tokenStatusCache.asMap().keySet().removeIf(key -> key.startsWith(tokenHash + ":"));
+    }
+
+    private String statusCacheKey(String tokenHash, String requiredScope) {
+        return tokenHash + ":" + requiredScope;
+    }
+
+    private String statusCacheKey(String tokenHash, String requiredScope, String workspaceId) {
+        return tokenHash + ":" + requiredScope + ":" + workspaceId;
+    }
 }
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
index 36414d88f2..c8ccbe9918 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.manager.config;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -33,7 +34,10 @@ import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.List;
 import org.apache.hertzbeat.common.constants.NetworkConstants;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.manager.service.AccountService;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -63,6 +67,14 @@ class ApiTokenValidationFilterTest {
     @BeforeEach
     void setUp() {
         filter = new ApiTokenValidationFilter(accountService);
+        AuthTokenRequestContext.clear();
+        lenient().when(request.getHeader(AuthTokenScopes.WORKSPACE_ID_HEADER)).thenReturn(null);
+        lenient().when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_WORKSPACE_ID)).thenReturn(null);
+    }
+
+    @AfterEach
+    void tearDown() {
+        AuthTokenRequestContext.clear();
     }
 
     @Test
@@ -101,11 +113,30 @@ class ApiTokenValidationFilterTest {
         }
     }
 
+    @Test
+    void testUiSessionWorkspaceClaimBindsRequestContextWithoutManagedValidation() throws Exception {
+        SubjectSum subject = mockSubject();
+        when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_WORKSPACE_ID)).thenReturn("team-a");
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object()));
+            org.junit.jupiter.api.Assertions.assertEquals("team-a", AuthTokenRequestContext.currentWorkspaceId());
+            verify(accountService, never()).checkTokenStatus(any());
+
+            filter.afterCompletion(request, response, new Object(), null);
+            org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentWorkspaceId());
+        }
+    }
+
     @Test
     void testManagedTokenActivePassesThrough() throws Exception {
         String managedToken = "managed-token";
         when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken);
-        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getRequestURI()).thenReturn("/api/monitor");
+        when(accountService.checkTokenStatus(managedToken, AuthTokenScopes.API_ADMIN)).thenReturn(null);
         when(accountService.checkManagedTokenAccess("admin", List.of("admin"))).thenReturn(null);
         doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
         SubjectSum subject = mockManagedSubjectWithClaims();
@@ -114,17 +145,79 @@ class ApiTokenValidationFilterTest {
             mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 
             org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object()));
-            verify(accountService).checkTokenStatus(managedToken);
+            verify(accountService).checkTokenStatus(managedToken, AuthTokenScopes.API_ADMIN);
             verify(accountService).checkManagedTokenAccess("admin", List.of("admin"));
             verify(accountService).touchTokenLastUsedTime(managedToken);
         }
     }
 
+    @Test
+    void testManagedTokenReadRequestRequiresReadonlyScope() throws Exception {
+        String managedToken = "managed-token";
+        when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken);
+        when(request.getMethod()).thenReturn("GET");
+        when(request.getRequestURI()).thenReturn("/api/monitor");
+        when(accountService.checkTokenStatus(managedToken, AuthTokenScopes.READONLY_QUERY)).thenReturn(null);
+        when(accountService.checkManagedTokenAccess("admin", List.of("admin"))).thenReturn(null);
+        doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
+        SubjectSum subject = mockManagedSubjectWithClaims();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object()));
+            verify(accountService).checkTokenStatus(managedToken, AuthTokenScopes.READONLY_QUERY);
+        }
+    }
+
+    @Test
+    void testManagedTokenOtlpRequestRequiresIngestScope() throws Exception {
+        String managedToken = "managed-token";
+        when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken);
+        when(request.getRequestURI()).thenReturn("/api/otlp/v1/metrics");
+        when(accountService.checkTokenStatus(managedToken, AuthTokenScopes.OTLP_INGEST)).thenReturn(null);
+        when(accountService.checkManagedTokenAccess("admin", List.of("admin"))).thenReturn(null);
+        doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
+        SubjectSum subject = mockManagedSubjectWithClaims();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object()));
+            verify(accountService).checkTokenStatus(managedToken, AuthTokenScopes.OTLP_INGEST);
+        }
+    }
+
+    @Test
+    void testManagedTokenWorkspaceHeaderRequiresWorkspaceBoundary() throws Exception {
+        String managedToken = "managed-token";
+        when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken);
+        when(request.getHeader(AuthTokenScopes.WORKSPACE_ID_HEADER)).thenReturn("prod-west");
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getRequestURI()).thenReturn("/api/monitor");
+        when(accountService.checkTokenStatus(managedToken, AuthTokenScopes.API_ADMIN, "prod-west")).thenReturn(null);
+        when(accountService.checkManagedTokenAccess("admin", List.of("admin"))).thenReturn(null);
+        doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
+        SubjectSum subject = mockManagedSubjectWithClaims();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object()));
+            verify(accountService).checkTokenStatus(managedToken, AuthTokenScopes.API_ADMIN, "prod-west");
+            org.junit.jupiter.api.Assertions.assertEquals("prod-west", AuthTokenRequestContext.currentWorkspaceId());
+            filter.afterCompletion(request, response, new Object(), null);
+            org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentWorkspaceId());
+        }
+    }
+
     @Test
     void testManagedTokenRevokedRejected() throws Exception {
         String revokedToken = "revoked-token";
         when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + revokedToken);
-        when(accountService.checkTokenStatus(revokedToken)).thenReturn("Token has been revoked");
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getRequestURI()).thenReturn("/api/monitor");
+        when(accountService.checkTokenStatus(revokedToken, AuthTokenScopes.API_ADMIN)).thenReturn("Token has been revoked");
 
         StringWriter stringWriter = new StringWriter();
         PrintWriter printWriter = new PrintWriter(stringWriter);
@@ -142,7 +235,9 @@ class ApiTokenValidationFilterTest {
     @Test
     void testManagedTokenStatusCheckFailureRejectsRequest() throws Exception {
         when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer managed-token");
-        when(accountService.checkTokenStatus("managed-token")).thenThrow(new RuntimeException("DB down"));
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getRequestURI()).thenReturn("/api/monitor");
+        when(accountService.checkTokenStatus("managed-token", AuthTokenScopes.API_ADMIN)).thenThrow(new RuntimeException("DB down"));
 
         StringWriter stringWriter = new StringWriter();
         PrintWriter printWriter = new PrintWriter(stringWriter);
@@ -162,7 +257,9 @@ class ApiTokenValidationFilterTest {
     void testManagedTokenOutdatedRolesRejected() throws Exception {
         String managedToken = "managed-token";
         when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken);
-        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getRequestURI()).thenReturn("/api/monitor");
+        when(accountService.checkTokenStatus(managedToken, AuthTokenScopes.API_ADMIN)).thenReturn(null);
         when(accountService.checkManagedTokenAccess("admin", List.of("admin")))
                 .thenReturn("Token permissions are outdated");
 
@@ -196,7 +293,9 @@ class ApiTokenValidationFilterTest {
     void testTouchLastUsedTimeFailureDoesNotRejectRequest() throws Exception {
         String managedToken = "managed-token";
         when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken);
-        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getRequestURI()).thenReturn("/api/monitor");
+        when(accountService.checkTokenStatus(managedToken, AuthTokenScopes.API_ADMIN)).thenReturn(null);
         when(accountService.checkManagedTokenAccess("admin", List.of("admin"))).thenReturn(null);
         // touchTokenLastUsedTime throws exception
         org.mockito.Mockito.doThrow(new RuntimeException("DB error"))
@@ -224,7 +323,9 @@ class ApiTokenValidationFilterTest {
     void testManagedTokenAccountValidationFailureRejectsRequest() throws Exception {
         String managedToken = "managed-token";
         when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken);
-        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getRequestURI()).thenReturn("/api/monitor");
+        when(accountService.checkTokenStatus(managedToken, AuthTokenScopes.API_ADMIN)).thenReturn(null);
         when(accountService.checkManagedTokenAccess("admin", List.of("admin")))
                 .thenThrow(new RuntimeException("account store unavailable"));
 
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/OtlpRouteAuthorizationConfigTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/OtlpRouteAuthorizationConfigTest.java
new file mode 100644
index 0000000000..328b2b0d98
--- /dev/null
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/OtlpRouteAuthorizationConfigTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.config;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+/**
+ * Release-readiness contracts for shipped OTLP route authorization templates.
+ */
+class OtlpRouteAuthorizationConfigTest {
+
+    private static final List<String> REQUIRED_OTLP_RESOURCE_RULES = List.of(
+            "  - /api/otlp/**===post===[admin,user]",
+            "  - /api/logs/otlp/**===post===[admin,user]"
+    );
+    private static final List<String> SURENESS_CONFIGS = List.of(
+            "hertzbeat-startup/src/main/resources/sureness.yml",
+            "hertzbeat-manager/src/test/resources/sureness.yml",
+            "hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml",
+            "script/sureness.yml",
+            "script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml",
+            "script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml",
+            "script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml",
+            "script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml",
+            "script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml"
+    );
+
+    @Test
+    void shippedSurenessConfigsRouteOtlpPostThroughManagedTokenBoundary() {
+        List<Executable> checks = new ArrayList<>();
+        for (String config : SURENESS_CONFIGS) {
+            checks.add(() -> assertOtlpRules(config));
+        }
+        assertAll(checks);
+    }
+
+    private static void assertOtlpRules(String config) throws IOException {
+        List<String> lines = Files.readAllLines(repoRoot().resolve(config));
+        for (String rule : REQUIRED_OTLP_RESOURCE_RULES) {
+            assertTrue(lines.contains(rule),
+                    () -> config + " must include OTLP managed-token route rule " + rule.trim());
+        }
+    }
+
+    private static Path repoRoot() {
+        Path current = Paths.get("").toAbsolutePath();
+        while (current != null && !Files.exists(current.resolve("hertzbeat-manager/pom.xml"))) {
+            current = current.getParent();
+        }
+        if (current == null) {
+            throw new IllegalStateException("Cannot locate HertzBeat repository root");
+        }
+        return current;
+    }
+}
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java
index 58910869dc..34e2a3de20 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java
@@ -33,6 +33,7 @@ import java.time.LocalDateTime;
 import java.util.List;
 import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.entity.manager.AuthToken;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.manager.service.AccountService;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -69,7 +70,8 @@ class AuthTokenControllerTest {
 
         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
             mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
-            when(accountService.generateToken(eq("my-token"), eq(3600L))).thenReturn("generated-jwt-token");
+            when(accountService.generateToken(eq("my-token"), eq(3600L), eq(AuthTokenScopes.API_ADMIN)))
+                    .thenReturn("generated-jwt-token");
 
             this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate")
                             .param("name", "my-token")
@@ -86,7 +88,8 @@ class AuthTokenControllerTest {
 
         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
             mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
-            when(accountService.generateToken(any(), any())).thenReturn("generated-jwt-token");
+            when(accountService.generateToken(any(), any(), eq(AuthTokenScopes.API_ADMIN)))
+                    .thenReturn("generated-jwt-token");
 
             this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate"))
                     .andExpect(status().isOk())
@@ -95,6 +98,49 @@ class AuthTokenControllerTest {
         }
     }
 
+    @Test
+    void testGenerateTokenWithScope() throws Exception {
+        SubjectSum subjectSum = mockAdminSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+            when(accountService.generateToken(eq("otlp"), eq(3600L), eq(AuthTokenScopes.OTLP_INGEST)))
+                    .thenReturn("generated-jwt-token");
+
+            this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate")
+                            .param("name", "otlp")
+                            .param("expireSeconds", "3600")
+                            .param("scope", AuthTokenScopes.OTLP_INGEST))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                    .andExpect(jsonPath("$.data.token").value("generated-jwt-token"));
+        }
+    }
+
+    @Test
+    void testGenerateTokenWithWorkspaceBoundary() throws Exception {
+        SubjectSum subjectSum = mockAdminSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+            when(accountService.generateToken(
+                    eq("otlp"),
+                    eq(3600L),
+                    eq(AuthTokenScopes.OTLP_INGEST),
+                    eq("prod-west")))
+                    .thenReturn("generated-jwt-token");
+
+            this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate")
+                            .param("name", "otlp")
+                            .param("expireSeconds", "3600")
+                            .param("scope", AuthTokenScopes.OTLP_INGEST)
+                            .param("workspaceId", "prod-west"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
+                    .andExpect(jsonPath("$.data.token").value("generated-jwt-token"));
+        }
+    }
+
     @Test
     void testGenerateTokenNoLogin() throws Exception {
         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java
index 5f1d95d811..1538472b14 100644
--- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java
@@ -27,6 +27,8 @@ import com.usthe.sureness.util.SurenessContextHolder;
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.MalformedJwtException;
 import org.apache.hertzbeat.common.entity.manager.AuthToken;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.common.util.JsonUtil;
 import org.apache.hertzbeat.manager.dao.AuthTokenDao;
 import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
@@ -44,6 +46,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -53,6 +56,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -116,6 +120,9 @@ class AccountServiceTest {
         assertNotNull(response.get("refreshToken"));
         assertNotNull(response.get("role"));
         assertEquals(JsonUtil.toJson(roles), response.get("role"));
+        Claims accessClaims = JsonWebTokenUtil.parseJwt(response.get("token"));
+        assertEquals(AuthTokenScopes.UI_SESSION, accessClaims.get(AuthTokenScopes.CLAIM_TOKEN_SCOPE, String.class));
+        assertEquals(AuthTokenScopes.DEFAULT_WORKSPACE_ID, accessClaims.get(AuthTokenScopes.CLAIM_WORKSPACE_ID, String.class));
 
     }
 
@@ -160,6 +167,8 @@ class AccountServiceTest {
         Claims accessClaims = JsonWebTokenUtil.parseJwt(response.getToken());
         Claims refreshClaims = JsonWebTokenUtil.parseJwt(response.getRefreshToken());
         assertNull(accessClaims.get("refresh", Boolean.class));
+        assertEquals(AuthTokenScopes.UI_SESSION, accessClaims.get(AuthTokenScopes.CLAIM_TOKEN_SCOPE, String.class));
+        assertEquals(AuthTokenScopes.DEFAULT_WORKSPACE_ID, accessClaims.get(AuthTokenScopes.CLAIM_WORKSPACE_ID, String.class));
         assertEquals(Boolean.TRUE, refreshClaims.get("refresh", Boolean.class));
     }
 
@@ -277,10 +286,81 @@ class AccountServiceTest {
             assertNotNull(saved.getTokenMask());
             assertEquals((byte) 0, saved.getStatus());
             assertEquals(identifier, saved.getCreator());
+            assertEquals(AuthTokenScopes.API_ADMIN, saved.getTokenScope());
             assertNull(saved.getExpireTime());
         }
     }
 
+    @Test
+    void testGenerateTokenPersistsRequestedScope() throws Exception {
+        SurenessAccount account = buildActiveAccount();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            String token = accountService.generateToken("OTLP ingest", null, AuthTokenScopes.OTLP_INGEST);
+
+            Claims claims = JsonWebTokenUtil.parseJwt(token);
+            assertEquals(AuthTokenScopes.OTLP_INGEST, claims.get(AuthTokenScopes.CLAIM_TOKEN_SCOPE, String.class));
+            ArgumentCaptor<AuthToken> captor = ArgumentCaptor.forClass(AuthToken.class);
+            verify(authTokenDao).save(captor.capture());
+            assertEquals(AuthTokenScopes.OTLP_INGEST, captor.getValue().getTokenScope());
+        }
+    }
+
+    @Test
+    void testGenerateTokenPersistsWorkspaceBoundary() throws Exception {
+        SurenessAccount account = buildActiveAccount();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            String token = accountService.generateToken(
+                    "prod ingest",
+                    null,
+                    AuthTokenScopes.OTLP_INGEST,
+                    "prod-west"
+            );
+
+            Claims claims = JsonWebTokenUtil.parseJwt(token);
+            assertEquals("prod-west", claims.get(AuthTokenScopes.CLAIM_WORKSPACE_ID, String.class));
+            ArgumentCaptor<AuthToken> captor = ArgumentCaptor.forClass(AuthToken.class);
+            verify(authTokenDao).save(captor.capture());
+            assertEquals("prod-west", captor.getValue().getWorkspaceId());
+        }
+    }
+
+    @Test
+    void testGenerateTokenRejectsScopeQuotaExceeded() {
+        SurenessAccount account = buildActiveAccount();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.countByStatusAndCreatorAndTokenScopeAndWorkspaceId(
+                eq((byte) 0),
+                eq(identifier),
+                eq(AuthTokenScopes.OTLP_INGEST),
+                eq(AuthTokenScopes.DEFAULT_WORKSPACE_ID)))
+                .thenReturn(20L);
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            AuthenticationException exception = Assertions.assertThrows(
+                    AuthenticationException.class,
+                    () -> accountService.generateToken("extra-otlp", null, AuthTokenScopes.OTLP_INGEST)
+            );
+
+            assertEquals("Token quota exceeded", exception.getMessage());
+            verify(authTokenDao, never()).save(any(AuthToken.class));
+        }
+    }
+
     @Test
     void testGenerateTokenNullName() throws Exception {
         SurenessAccount account = buildActiveAccount();
@@ -375,6 +455,52 @@ class AccountServiceTest {
         }
     }
 
+    @Test
+    void testListTokensForAdminUsesWorkspaceContext() {
+        List<AuthToken> expected = List.of(
+                AuthToken.builder().id(1L).name("Token1").workspaceId("team-a").build()
+        );
+        when(authTokenDao.findByStatusAndWorkspaceId((byte) 0, "team-a")).thenReturn(expected);
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            List<AuthToken> result = accountService.listTokens();
+
+            assertEquals(1, result.size());
+            assertEquals("Token1", result.get(0).getName());
+            verify(authTokenDao).findByStatusAndWorkspaceId((byte) 0, "team-a");
+            verify(authTokenDao, never()).findByStatus((byte) 0);
+        } finally {
+            AuthTokenRequestContext.clear();
+        }
+    }
+
+    @Test
+    void testListTokensForCurrentUserUsesWorkspaceContext() {
+        List<AuthToken> expected = List.of(
+                AuthToken.builder().id(1L).name("Token1").creator("tom").workspaceId("team-a").build()
+        );
+        when(authTokenDao.findByStatusAndCreatorAndWorkspaceId((byte) 0, "tom", "team-a")).thenReturn(expected);
+        SubjectSum subjectSum = mockUserSubject("tom");
+
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            List<AuthToken> result = accountService.listTokens();
+
+            assertEquals(1, result.size());
+            assertEquals("Token1", result.get(0).getName());
+            verify(authTokenDao).findByStatusAndCreatorAndWorkspaceId((byte) 0, "tom", "team-a");
+            verify(authTokenDao, never()).findByStatusAndCreator((byte) 0, "tom");
+        } finally {
+            AuthTokenRequestContext.clear();
+        }
+    }
+
     @Test
     void testDeleteTokenInvalidatesCache() throws Exception {
         AuthToken token = AuthToken.builder().id(1L).tokenHash("hash123").creator(identifier).build();
@@ -387,7 +513,13 @@ class AccountServiceTest {
             accountService.deleteToken(1L);
         }
 
-        verify(authTokenDao).deleteById(1L);
+        ArgumentCaptor<AuthToken> captor = ArgumentCaptor.forClass(AuthToken.class);
+        verify(authTokenDao).save(captor.capture());
+        AuthToken revoked = captor.getValue();
+        assertEquals((byte) 1, revoked.getStatus());
+        assertEquals(identifier, revoked.getRevokedBy());
+        assertNotNull(revoked.getRevokedTime());
+        verify(authTokenDao, never()).deleteById(1L);
     }
 
     @Test
@@ -403,6 +535,29 @@ class AccountServiceTest {
         }
     }
 
+    @Test
+    void testDeleteTokenRejectsWorkspaceMismatch() {
+        AuthToken token = AuthToken.builder()
+                .id(1L)
+                .tokenHash("hash123")
+                .creator(identifier)
+                .workspaceId("team-b")
+                .status((byte) 0)
+                .build();
+        when(authTokenDao.findById(1L)).thenReturn(Optional.of(token));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        AuthTokenRequestContext.bindWorkspaceId("team-a");
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            Assertions.assertThrows(AuthenticationException.class, () -> accountService.deleteToken(1L));
+            verify(authTokenDao, never()).save(any(AuthToken.class));
+        } finally {
+            AuthTokenRequestContext.clear();
+        }
+    }
+
     @Test
     void testCheckTokenStatusActive() throws Exception {
         SurenessAccount account = buildActiveAccount();
@@ -415,14 +570,51 @@ class AccountServiceTest {
             mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
 
             String token = accountService.generateToken("test", null);
-            when(authTokenDao.existsByTokenHashAndStatus(any(String.class), eq((byte) 0))).thenReturn(true);
+            when(authTokenDao.existsByTokenHashAndStatusAndTokenScopeIn(
+                    any(String.class),
+                    eq((byte) 0),
+                    eq(Set.of(AuthTokenScopes.API_ADMIN))))
+                    .thenReturn(true);
 
-            String result = accountService.checkTokenStatus(token);
+            String result = accountService.checkTokenStatus(token, AuthTokenScopes.API_ADMIN);
 
             assertNull(result);
         }
     }
 
+    @Test
+    void testCheckTokenStatusRejectsScopeMismatch() {
+        when(authTokenDao.existsByTokenHashAndStatusAndTokenScopeIn(
+                any(String.class),
+                eq((byte) 0),
+                eq(Set.of(AuthTokenScopes.API_ADMIN, AuthTokenScopes.READONLY_QUERY))))
+                .thenReturn(false);
+        when(authTokenDao.existsByTokenHashAndStatus(any(String.class), eq((byte) 0))).thenReturn(true);
+
+        String result = accountService.checkTokenStatus("readonly-token", AuthTokenScopes.READONLY_QUERY);
+
+        assertEquals("Token scope is not allowed", result);
+    }
+
+    @Test
+    void testCheckTokenStatusRejectsWorkspaceMismatch() {
+        when(authTokenDao.existsByTokenHashAndStatusAndTokenScopeInAndWorkspaceId(
+                any(String.class),
+                eq((byte) 0),
+                eq(Set.of(AuthTokenScopes.API_ADMIN, AuthTokenScopes.OTLP_INGEST)),
+                eq("prod-west")))
+                .thenReturn(false);
+        when(authTokenDao.existsByTokenHashAndStatusAndTokenScopeIn(
+                any(String.class),
+                eq((byte) 0),
+                eq(Set.of(AuthTokenScopes.API_ADMIN, AuthTokenScopes.OTLP_INGEST))))
+                .thenReturn(true);
+
+        String result = accountService.checkTokenStatus("prod-token", AuthTokenScopes.OTLP_INGEST, "prod-west");
+
+        assertEquals("Token workspace is not allowed", result);
+    }
+
     @Test
     void testCheckTokenStatusRevoked() {
         when(authTokenDao.existsByTokenHashAndStatus(any(String.class), eq((byte) 0))).thenReturn(false);
@@ -529,7 +721,12 @@ class AccountServiceTest {
         // After revoke, the next check should hit DB again (cache invalidated for that hash)
         // Note: the tokenHash in DB differs from sha256(tokenValue), so this tests cache invalidation path
         verify(authTokenDao).findById(1L);
-        verify(authTokenDao).deleteById(1L);
+        ArgumentCaptor<AuthToken> captor = ArgumentCaptor.forClass(AuthToken.class);
+        verify(authTokenDao).save(captor.capture());
+        assertEquals((byte) 1, captor.getValue().getStatus());
+        assertEquals(identifier, captor.getValue().getRevokedBy());
+        assertNotNull(captor.getValue().getRevokedTime());
+        verify(authTokenDao, never()).deleteById(1L);
     }
 
     private SurenessAccount buildActiveAccount() {
diff --git a/hertzbeat-manager/src/test/resources/sureness.yml b/hertzbeat-manager/src/test/resources/sureness.yml
index 3368fe7018..6a8672f3c5 100644
--- a/hertzbeat-manager/src/test/resources/sureness.yml
+++ b/hertzbeat-manager/src/test/resources/sureness.yml
@@ -22,13 +22,25 @@
 resourceRole:
   - /api/account/auth/refresh===post===[admin,user,guest]
   - /api/apps/**===get===[admin,user,guest]
+  - /api/monitor===get===[admin,user,guest]
   - /api/monitor/**===get===[admin,user,guest]
+  - /api/monitor===post===[admin,user]
   - /api/monitor/**===post===[admin,user]
+  - /api/monitor===put===[admin,user]
   - /api/monitor/**===put===[admin,user]
-  - /api/monitor/**===delete==[admin]
+  - /api/monitor===delete===[admin]
+  - /api/monitor/**===delete===[admin]
+  - /api/monitors===get===[admin,user,guest]
+  - /api/monitors/export===get===[admin]
+  - /api/monitors/export/all===get===[admin]
+  - /api/monitors/manage===get===[admin,user,guest]
+  - /api/monitors/manage===post===[admin,user]
+  - /api/monitors/manage===put===[admin,user]
   - /api/monitors/**===get===[admin,user,guest]
   - /api/monitors/**===post===[admin,user]
   - /api/monitors/**===put===[admin,user]
+  - /api/monitors===delete===[admin]
+  - /api/monitors/manage===delete===[admin]
   - /api/monitors/**===delete===[admin]
   - /api/alert/**===get===[admin,user,guest]
   - /api/alert/**===post===[admin,user]
@@ -63,10 +75,17 @@ resourceRole:
   - /api/grafana/**===put===[admin,user]
   - /api/grafana/**===delete===[admin]
   - /api/ingestion/otlp/**===get===[admin,user,guest]
+  - /api/otlp/**===post===[admin,user]
+  - /api/logs/otlp/**===post===[admin,user]
   - /api/bulletin/**===get===[admin,user,guest]
   - /api/bulletin/**===post===[admin,user]
   - /api/bulletin/**===put===[admin,user]
   - /api/bulletin/**===delete===[admin]
+  - /api/actions/approval-drafts===get===[admin,user]
+  - /api/actions/approval-drafts===post===[admin,user]
+  - /api/actions/approval-drafts/**===post===[admin,user]
+  - /api/actions/catalog===get===[admin,user]
+  - /api/actions/catalog===post===[admin]
   - /api/sse/**===get===[admin,user]
   - /api/sse/**===post===[admin,user]
   - /api/chat/**===get===[admin,user]
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/OpenTelemetryConfig.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/OpenTelemetryConfig.java
index d6da18e1b4..7d7181e893 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/OpenTelemetryConfig.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/OpenTelemetryConfig.java
@@ -26,6 +26,7 @@ import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
 import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
 import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
 import io.opentelemetry.sdk.resources.Resource;
+import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.HashMap;
@@ -33,6 +34,7 @@ import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.forwarder.GreptimeOtlpForwarder;
 import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -51,14 +53,19 @@ public class OpenTelemetryConfig {
     private static final String DEFAULT_TRACES_TABLE_NAME = "hzb_traces";
     private static final String GREPTIME_DB_NAME_HEADER = "X-Greptime-DB-Name";
     private static final String GREPTIME_LOG_TABLE_NAME_HEADER = "X-Greptime-Log-Table-Name";
+    private static final String GREPTIME_LOG_PIPELINE_NAME_HEADER = "X-Greptime-Log-Pipeline-Name";
     private static final String GREPTIME_TRACE_TABLE_NAME_HEADER = "X-Greptime-Trace-Table-Name";
     private static final String GREPTIME_PIPELINE_NAME_HEADER = "X-Greptime-Pipeline-Name";
+    private static final String GREPTIME_LOGS_PATH = "/v1/otlp/v1/logs";
+    private static final String GREPTIME_TRACES_PATH = "/v1/otlp/v1/traces";
 
     private void addAuthenticationHeaders(Map<String, String> headers, GreptimeProperties greptimeProps) {
-        if (greptimeProps != null && StringUtils.isNotBlank(greptimeProps.username())
-                && StringUtils.isNotBlank(greptimeProps.password())) {
-            String credentials = greptimeProps.username() + ":" + greptimeProps.password();
-            String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
+        String username = greptimeProps == null ? null : StringUtils.trimToNull(greptimeProps.username());
+        String password = greptimeProps == null ? null : StringUtils.trimToNull(greptimeProps.password());
+        if (username != null && password != null) {
+            String credentials = username + ":" + password;
+            String encodedCredentials = Base64.getEncoder()
+                    .encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
             headers.put("Authorization", "Basic " + encodedCredentials);
             log.debug("Added Basic Authentication header for GreptimeDB.");
         } else {
@@ -68,15 +75,16 @@ public class OpenTelemetryConfig {
 
     private Map<String, String> buildGreptimeOtlpLogHeaders(GreptimeProperties greptimeProps) {
         Map<String, String> headers = new HashMap<>();
-        headers.put(GREPTIME_DB_NAME_HEADER, DEFAULT_GREPTIME_DB_NAME);
+        headers.put(GREPTIME_DB_NAME_HEADER, database(greptimeProps));
         headers.put(GREPTIME_LOG_TABLE_NAME_HEADER, WarehouseConstants.LOG_TABLE_NAME);
+        headers.put(GREPTIME_LOG_PIPELINE_NAME_HEADER, GreptimeOtlpForwarder.LOG_PIPELINE_NAME);
         addAuthenticationHeaders(headers, greptimeProps);
         return Collections.unmodifiableMap(headers);
     }
 
     private Map<String, String> buildGreptimeOtlpTraceHeaders(GreptimeProperties greptimeProps) {
         Map<String, String> headers = new HashMap<>();
-        headers.put(GREPTIME_DB_NAME_HEADER, DEFAULT_GREPTIME_DB_NAME);
+        headers.put(GREPTIME_DB_NAME_HEADER, database(greptimeProps));
         headers.put(GREPTIME_TRACE_TABLE_NAME_HEADER, DEFAULT_TRACES_TABLE_NAME);
         headers.put(CONTENT_TYPE, "application/x-protobuf");
         headers.put(GREPTIME_PIPELINE_NAME_HEADER, "greptime_trace_v1");
@@ -84,6 +92,17 @@ public class OpenTelemetryConfig {
         return Collections.unmodifiableMap(headers);
     }
 
+    private String database(GreptimeProperties greptimeProps) {
+        return greptimeProps == null
+                ? DEFAULT_GREPTIME_DB_NAME
+                : StringUtils.defaultIfBlank(StringUtils.trim(greptimeProps.database()), DEFAULT_GREPTIME_DB_NAME);
+    }
+
+    private String greptimeOtlpEndpoint(GreptimeProperties greptimeProps, String path) {
+        String endpoint = greptimeProps == null ? null : greptimeProps.httpEndpoint();
+        return StringUtils.stripEnd(StringUtils.trimToEmpty(endpoint), "/") + path;
+    }
+
     @Bean
     public AutoConfigurationCustomizerProvider defaultOtelCustomizer() {
         log.info("Applying default OpenTelemetry SDK customizations (logs & traces only).");
@@ -113,7 +132,7 @@ public class OpenTelemetryConfig {
                     return newProperties;
                 })
                 .addSpanExporterCustomizer((originalSpanExporter, configProperties) -> {
-                    String traceEndpoint = greptimeProperties.httpEndpoint() + "/v1/otlp/v1/traces";
+                    String traceEndpoint = greptimeOtlpEndpoint(greptimeProperties, GREPTIME_TRACES_PATH);
                     log.info("Configuring OtlpHttpSpanExporter for GreptimeDB. Endpoint: {}", traceEndpoint);
                     OtlpHttpSpanExporterBuilder httpExporterBuilder = OtlpHttpSpanExporter.builder()
                             .setEndpoint(traceEndpoint)
@@ -124,7 +143,7 @@ public class OpenTelemetryConfig {
                 .addLoggerProviderCustomizer((sdkLoggerProviderBuilder, configProperties) -> {
                     log.info("Customizing SdkLoggerProviderBuilder for GreptimeDB logs.");
                     OtlpHttpLogRecordExporter logExporter = OtlpHttpLogRecordExporter.builder()
-                            .setEndpoint(greptimeProperties.httpEndpoint() + "/v1/otlp/v1/logs")
+                            .setEndpoint(greptimeOtlpEndpoint(greptimeProperties, GREPTIME_LOGS_PATH))
                             .setHeaders(() -> buildGreptimeOtlpLogHeaders(greptimeProperties))
                             .setTimeout(10000, TimeUnit.MILLISECONDS)
                             .build();
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/adapter/OtlpLogProtocolAdapter.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/adapter/OtlpLogProtocolAdapter.java
index 0614801bde..2ffa688246 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/adapter/OtlpLogProtocolAdapter.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/adapter/OtlpLogProtocolAdapter.java
@@ -33,7 +33,9 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.common.entity.log.LogEntry;
 import org.apache.hertzbeat.common.entity.observability.TelemetryIntakeSignalEvent;
 import org.apache.hertzbeat.common.queue.CommonDataQueue;
+import org.apache.hertzbeat.observability.ingestion.redaction.OtlpIngestionRedactionService;
 import org.apache.hertzbeat.observability.logs.sse.LogSseManager;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.http.HttpHeaders;
 import org.springframework.stereotype.Service;
@@ -63,19 +65,31 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
     private static final String PROTOCOL_NAME = "otlp";
     private static final String CONTENT_ENCODING = "Content-Encoding";
     private static final String CONTENT_ENCODING_GZIP = "gzip";
+    private static final int OTLP_TRACE_ID_BYTES = 16;
+    private static final int OTLP_SPAN_ID_BYTES = 8;
     private static final Set<String> OTLP_HEX_ID_FIELDS = Set.of("traceId", "spanId");
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
     private final CommonDataQueue commonDataQueue;
     private final LogSseManager logSseManager;
     private final ApplicationEventPublisher applicationEventPublisher;
+    private final OtlpIngestionRedactionService redactionService;
 
+    @Autowired
     public OtlpLogProtocolAdapter(CommonDataQueue commonDataQueue,
                                   LogSseManager logSseManager,
-                                  ApplicationEventPublisher applicationEventPublisher) {
+                                  ApplicationEventPublisher applicationEventPublisher,
+                                  OtlpIngestionRedactionService redactionService) {
         this.commonDataQueue = commonDataQueue;
         this.logSseManager = logSseManager;
         this.applicationEventPublisher = applicationEventPublisher;
+        this.redactionService = redactionService;
+    }
+
+    public OtlpLogProtocolAdapter(CommonDataQueue commonDataQueue,
+                                  LogSseManager logSseManager,
+                                  ApplicationEventPublisher applicationEventPublisher) {
+        this(commonDataQueue, logSseManager, applicationEventPublisher, new OtlpIngestionRedactionService());
     }
 
     @Override
@@ -141,13 +155,29 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
         if (content == null || content.length == 0 || requestHeaders == null) {
             return content;
         }
-        String contentEncoding = requestHeaders.getFirst(CONTENT_ENCODING);
-        if (contentEncoding == null || !CONTENT_ENCODING_GZIP.equalsIgnoreCase(contentEncoding)) {
+        if (!isGzipEncoded(requestHeaders)) {
             return content;
         }
         return decompressGzip(content);
     }
 
+    private boolean isGzipContentEncoding(String contentEncoding) {
+        if (contentEncoding == null || contentEncoding.isBlank()) {
+            return false;
+        }
+        for (String encoding : contentEncoding.split(",")) {
+            if (CONTENT_ENCODING_GZIP.equalsIgnoreCase(encoding.trim())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isGzipEncoded(HttpHeaders requestHeaders) {
+        List<String> contentEncodings = requestHeaders == null ? null : requestHeaders.get(CONTENT_ENCODING);
+        return contentEncodings != null && contentEncodings.stream().anyMatch(this::isGzipContentEncoding);
+    }
+
     private byte[] decompressGzip(byte[] content) {
         try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
              GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
@@ -213,8 +243,8 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
             .body(extractBody(logRecord.getBody()))
             .attributes(extractAttributes(logRecord.getAttributesList()))
             .droppedAttributesCount(logRecord.getDroppedAttributesCount())
-            .traceId(bytesToHex(logRecord.getTraceId().toByteArray()))
-            .spanId(bytesToHex(logRecord.getSpanId().toByteArray()))
+            .traceId(bytesToOtlpIdHex(logRecord.getTraceId().toByteArray(), OTLP_TRACE_ID_BYTES))
+            .spanId(bytesToOtlpIdHex(logRecord.getSpanId().toByteArray(), OTLP_SPAN_ID_BYTES))
             .traceFlags(logRecord.getFlags())
             .resource(resourceAttributes)
             .resourceSchemaUrl(emptyToNull(resourceSchemaUrl))
@@ -302,7 +332,8 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
             Map<String, Object> resultMap = new HashMap<>();
             for (Map.Entry<?, ?> entry : genericMap.entrySet()) {
                 if (entry.getKey() instanceof String) {
-                    resultMap.put((String) entry.getKey(), entry.getValue());
+                    String key = (String) entry.getKey();
+                    resultMap.put(key, redactionService.redactObject(key, entry.getValue()));
                 }
             }
             return resultMap;
@@ -315,7 +346,7 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
      * Extract body content from AnyValue.
      */
     private Object extractBody(AnyValue body) {
-        return extractAnyValue(body);
+        return redactionService.redactObject(null, extractAnyValue(body));
     }
 
     /**
@@ -340,7 +371,9 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
             case KVLIST_VALUE:
                 Map<String, Object> kvMap = new HashMap<>();
                 for (KeyValue kv : anyValue.getKvlistValue().getValuesList()) {
-                    kvMap.put(normalizeKey(kv.getKey()), extractAnyValue(kv.getValue()));
+                    String normalizedKey = normalizeKey(kv.getKey());
+                    kvMap.put(normalizedKey, redactionService.redactObject(normalizedKey,
+                            extractAnyValue(kv.getValue())));
                 }
                 return kvMap;
             case BYTES_VALUE:
@@ -374,6 +407,13 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
     /**
      * Convert byte array to hex string.
      */
+    private String bytesToOtlpIdHex(byte[] bytes, int expectedLength) {
+        if (bytes == null || bytes.length != expectedLength || isAllZero(bytes)) {
+            return null;
+        }
+        return bytesToHex(bytes);
+    }
+
     private String bytesToHex(byte[] bytes) {
         if (bytes == null || bytes.length == 0) {
             return null;
@@ -389,6 +429,15 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
         return hexString.toString();
     }
 
+    private boolean isAllZero(byte[] bytes) {
+        for (byte value : bytes) {
+            if (value != 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private String normalizeOtlpJson(String content) throws InvalidProtocolBufferException {
         try {
             JsonNode root = OBJECT_MAPPER.readTree(content);
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventReader.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventReader.java
new file mode 100644
index 0000000000..43e9e6febd
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventReader.java
@@ -0,0 +1,410 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.audit;
+
+import io.grpc.Status;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.redaction.OtlpIngestionRedactionService;
+import org.apache.hertzbeat.warehouse.db.GreptimeSqlQueryExecutor;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+/**
+ * Reads durable OTLP ingest RED audit events back from Greptime SQL.
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "warehouse.store.greptime", name = "enabled", havingValue = "true")
+public class GreptimeOtlpIngestionAuditEventReader implements OtlpIngestionAuditEventReader {
+
+    private static final int MAX_LIMIT = 256;
+    private static final String STATUS_OK = "OK";
+    private static final Set<String> SUPPORTED_SIGNALS = Set.of("metrics", "logs", "traces");
+    private static final Set<String> SUPPORTED_PROTOCOLS = Set.of("http", "grpc");
+    private static final Set<String> SUPPORTED_OUTCOMES = Set.of("accepted", "rejected", "dropped");
+    private static final Set<String> SUPPORTED_STATUS_CODES = Set.of(
+            "OK",
+            "CANCELLED",
+            "UNKNOWN",
+            "INVALID_ARGUMENT",
+            "DEADLINE_EXCEEDED",
+            "NOT_FOUND",
+            "ALREADY_EXISTS",
+            "PERMISSION_DENIED",
+            "RESOURCE_EXHAUSTED",
+            "FAILED_PRECONDITION",
+            "ABORTED",
+            "OUT_OF_RANGE",
+            "UNIMPLEMENTED",
+            "INTERNAL",
+            "UNAVAILABLE",
+            "DATA_LOSS",
+            "UNAUTHENTICATED"
+    );
+    private static final BigInteger LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
+    private static final BigInteger LONG_MIN_VALUE = BigInteger.valueOf(Long.MIN_VALUE);
+    private static final BigDecimal LONG_MAX_DECIMAL = BigDecimal.valueOf(Long.MAX_VALUE);
+    private static final BigDecimal LONG_MIN_DECIMAL = BigDecimal.valueOf(Long.MIN_VALUE);
+
+    private final ObjectProvider<GreptimeSqlQueryExecutor> greptimeSqlQueryExecutorProvider;
+    private final OtlpIngestionRedactionService redactionService = new OtlpIngestionRedactionService();
+
+    public GreptimeOtlpIngestionAuditEventReader(
+            ObjectProvider<GreptimeSqlQueryExecutor> greptimeSqlQueryExecutorProvider) {
+        this.greptimeSqlQueryExecutorProvider = greptimeSqlQueryExecutorProvider;
+    }
+
+    @Override
+    public List<OtlpIngestionAuditEvent> recentEvents(String workspaceId, int limit) {
+        return recentEvents(workspaceId, limit, null);
+    }
+
+    @Override
+    public List<OtlpIngestionAuditEvent> recentEvents(String workspaceId, int limit, Long startMillis) {
+        return recentEvents(workspaceId, limit, startMillis, null);
+    }
+
+    @Override
+    public List<OtlpIngestionAuditEvent> recentEvents(String workspaceId, int limit, Long startMillis,
+                                                      Long endMillis) {
+        GreptimeSqlQueryExecutor executor = greptimeSqlQueryExecutorOrNull();
+        if (executor == null) {
+            return Collections.emptyList();
+        }
+        try {
+            String safeWorkspaceId = StringUtils.trimToNull(workspaceId);
+            Long safeStartMillis = startMillis == null || startMillis <= 0 ? null : startMillis;
+            Long safeEndMillis = endMillis == null || endMillis <= 0 ? null : endMillis;
+            return executor.execute(recentEventsSql(workspaceId, limit, safeStartMillis, safeEndMillis)).stream()
+                    .filter(Objects::nonNull)
+                    .map(this::toEventOrNull)
+                    .filter(Objects::nonNull)
+                    .filter(this::hasSupportedAuditClassification)
+                    .filter(event -> event.observedAt() > 0)
+                    .filter(event -> safeWorkspaceId == null || safeWorkspaceId.equals(event.workspaceId()))
+                    .filter(event -> safeStartMillis == null || event.observedAt() >= safeStartMillis)
+                    .filter(event -> safeEndMillis == null || event.observedAt() <= safeEndMillis)
+                    .toList();
+        } catch (RuntimeException ex) {
+            log.warn("[otlp-ingest-red] failed to read Greptime audit events: {}", ex.getMessage(), ex);
+            return Collections.emptyList();
+        }
+    }
+
+    private GreptimeSqlQueryExecutor greptimeSqlQueryExecutorOrNull() {
+        try {
+            return greptimeSqlQueryExecutorProvider.getIfAvailable();
+        } catch (RuntimeException ex) {
+            log.warn("[otlp-ingest-red] failed to resolve Greptime audit query executor: {}", ex.getMessage(), ex);
+            return null;
+        }
+    }
+
+    private String recentEventsSql(String workspaceId, int limit, Long startMillis, Long endMillis) {
+        StringBuilder sql = new StringBuilder("SELECT observed_at, workspace_id, entity_id, ingest_id, signal, ")
+                .append("protocol, outcome, status_code, request_bytes, signal_items, duration_millis, reason ")
+                .append("FROM ")
+                .append(GreptimeOtlpIngestionAuditEventSink.TABLE_NAME);
+        List<String> filters = new ArrayList<>();
+        String safeWorkspaceId = StringUtils.trimToNull(workspaceId);
+        if (safeWorkspaceId != null) {
+            filters.add("workspace_id = '" + escapeSql(safeWorkspaceId) + "'");
+        }
+        if (startMillis != null && startMillis > 0) {
+            filters.add("observed_at >= to_timestamp_millis(" + startMillis + ")");
+        }
+        if (endMillis != null && endMillis > 0) {
+            filters.add("observed_at <= to_timestamp_millis(" + endMillis + ")");
+        }
+        if (!filters.isEmpty()) {
+            sql.append(" WHERE ")
+                    .append(String.join(" AND ", filters));
+        }
+        sql.append(" ORDER BY observed_at DESC LIMIT ")
+                .append(boundedLimit(limit));
+        return sql.toString();
+    }
+
+    private OtlpIngestionAuditEvent toEvent(Map<String, Object> row) {
+        return new OtlpIngestionAuditEvent(
+                normalizedDimension(row.get("signal")),
+                normalizedDimension(row.get("protocol")),
+                normalizedDimension(row.get("outcome")),
+                normalizedStatusCode(row.get("status_code")),
+                stringValue(row.get("workspace_id")),
+                stringValue(row.get("entity_id")),
+                stringValue(row.get("ingest_id")),
+                longValue(row.get("request_bytes"), 0L),
+                nullableLongValue(row.get("signal_items")),
+                redactedReason(row.get("reason")),
+                nullableLongValue(row.get("duration_millis")),
+                longValue(row.get("observed_at"), 0L)
+        );
+    }
+
+    private OtlpIngestionAuditEvent toEventOrNull(Map<String, Object> row) {
+        try {
+            return toEvent(row);
+        } catch (RuntimeException ex) {
+            log.warn("[otlp-ingest-red] skipped malformed Greptime audit row: {}", ex.getMessage());
+            return null;
+        }
+    }
+
+    private boolean hasSupportedAuditClassification(OtlpIngestionAuditEvent event) {
+        String signal = event.signal();
+        String protocol = event.protocol();
+        String outcome = event.outcome();
+        String statusCode = StringUtils.trimToNull(event.statusCode());
+        return signal != null && protocol != null
+                && SUPPORTED_SIGNALS.contains(signal)
+                && SUPPORTED_PROTOCOLS.contains(protocol)
+                && statusCode != null
+                && (isSupportedOutcome(outcome) || isNonOkStatus(statusCode));
+    }
+
+    private boolean isSupportedOutcome(String outcome) {
+        return outcome != null && SUPPORTED_OUTCOMES.contains(outcome.toLowerCase(Locale.ROOT));
+    }
+
+    private boolean isNonOkStatus(String statusCode) {
+        return !StringUtils.equalsIgnoreCase(STATUS_OK, statusCode);
+    }
+
+    private int boundedLimit(int limit) {
+        if (limit <= 0) {
+            return MAX_LIMIT;
+        }
+        return Math.min(limit, MAX_LIMIT);
+    }
+
+    private String stringValue(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value);
+        return StringUtils.trimToNull(text);
+    }
+
+    private String normalizedDimension(Object value) {
+        String text = stringValue(value);
+        return text == null ? null : text.toLowerCase(Locale.ROOT);
+    }
+
+    private String normalizedStatusCode(Object value) {
+        String text = stringValue(value);
+        if (text == null) {
+            return null;
+        }
+        String grpcStatusName = numericGrpcStatusName(text);
+        if (grpcStatusName != null) {
+            return grpcStatusName;
+        }
+        if (isNumericStatusCode(text)) {
+            return null;
+        }
+        String normalized = text.replaceAll("[\\s-]+", "_").replaceAll("^_+|_+$", "");
+        normalized = StringUtils.trimToNull(normalized) == null ? null : normalized.toUpperCase(Locale.ROOT);
+        return normalized != null && SUPPORTED_STATUS_CODES.contains(normalized) ? normalized : null;
+    }
+
+    private String numericGrpcStatusName(String text) {
+        String trimmed = StringUtils.trimToEmpty(text);
+        if (trimmed.isEmpty()) {
+            return null;
+        }
+        try {
+            BigDecimal numericStatus = new BigDecimal(trimmed).stripTrailingZeros();
+            if (numericStatus.scale() > 0) {
+                return null;
+            }
+            int codeValue = numericStatus.intValueExact();
+            if (codeValue < Status.Code.OK.value() || codeValue > Status.Code.UNAUTHENTICATED.value()) {
+                return null;
+            }
+            return Status.fromCodeValue(codeValue).getCode().name();
+        } catch (ArithmeticException | NumberFormatException ignored) {
+            return null;
+        }
+    }
+
+    private boolean isNumericStatusCode(String text) {
+        try {
+            new BigDecimal(text);
+            return true;
+        } catch (NumberFormatException ignored) {
+            return false;
+        }
+    }
+
+    private String redactedReason(Object value) {
+        return StringUtils.trimToNull(redactionService.redactText(stringValue(value)));
+    }
+
+    private Long nullableLongValue(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof BigDecimal bigDecimal) {
+            return nonNegativeLong(clampLong(bigDecimal));
+        }
+        if (value instanceof BigInteger bigInteger) {
+            return nonNegativeLong(clampLong(bigInteger));
+        }
+        if (value instanceof Number number) {
+            return nonNegativeLong(number.longValue());
+        }
+        String text = StringUtils.trimToNull(String.valueOf(value));
+        if (text == null) {
+            return null;
+        }
+        return integerTextLongValue(text);
+    }
+
+    private long longValue(Object value, long defaultValue) {
+        if (value == null) {
+            return defaultValue;
+        }
+        if (value instanceof BigDecimal bigDecimal) {
+            return nonNegativeLong(clampLong(bigDecimal));
+        }
+        if (value instanceof BigInteger bigInteger) {
+            return nonNegativeLong(clampLong(bigInteger));
+        }
+        if (value instanceof Number number) {
+            return nonNegativeLong(number.longValue());
+        }
+        if (value instanceof Timestamp timestamp) {
+            return nonNegativeLong(timestamp.toInstant().toEpochMilli());
+        }
+        if (value instanceof Date date) {
+            return nonNegativeLong(date.getTime());
+        }
+        if (value instanceof Instant instant) {
+            return nonNegativeLong(instant.toEpochMilli());
+        }
+        if (value instanceof OffsetDateTime offsetDateTime) {
+            return nonNegativeLong(offsetDateTime.toInstant().toEpochMilli());
+        }
+        if (value instanceof ZonedDateTime zonedDateTime) {
+            return nonNegativeLong(zonedDateTime.toInstant().toEpochMilli());
+        }
+        if (value instanceof LocalDateTime localDateTime) {
+            return nonNegativeLong(localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+        }
+        String text = StringUtils.trimToNull(String.valueOf(value));
+        if (text == null) {
+            return defaultValue;
+        }
+        Long integerValue = integerTextLongValue(text);
+        if (integerValue != null) {
+            return integerValue;
+        }
+        return nonNegativeLong(parseTimestamp(text, defaultValue));
+    }
+
+    private long nonNegativeLong(long value) {
+        return Math.max(value, 0L);
+    }
+
+    private Long integerTextLongValue(String text) {
+        if (!text.matches("[+-]?\\d+")) {
+            return null;
+        }
+        try {
+            return nonNegativeLong(clampLong(new BigInteger(text)));
+        } catch (NumberFormatException ignored) {
+            return null;
+        }
+    }
+
+    private long clampLong(BigInteger value) {
+        if (value.compareTo(LONG_MAX_VALUE) > 0) {
+            return Long.MAX_VALUE;
+        }
+        if (value.compareTo(LONG_MIN_VALUE) < 0) {
+            return Long.MIN_VALUE;
+        }
+        return value.longValue();
+    }
+
+    private long clampLong(BigDecimal value) {
+        if (value.compareTo(LONG_MAX_DECIMAL) > 0) {
+            return Long.MAX_VALUE;
+        }
+        if (value.compareTo(LONG_MIN_DECIMAL) < 0) {
+            return Long.MIN_VALUE;
+        }
+        return value.longValue();
+    }
+
+    private long parseTimestamp(String text, long defaultValue) {
+        try {
+            return Instant.parse(text).toEpochMilli();
+        } catch (DateTimeParseException ignored) {
+            // Try Greptime/DataFusion timestamp text variants.
+        }
+        String normalizedText = normalizedTimestampText(text);
+        try {
+            return OffsetDateTime.parse(normalizedText, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
+                    .toInstant()
+                    .toEpochMilli();
+        } catch (DateTimeParseException ignored) {
+            // Try Greptime/DataFusion's common timestamp text without a zone.
+        }
+        try {
+            return LocalDateTime.parse(normalizedText, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
+                    .atZone(ZoneId.systemDefault())
+                    .toInstant()
+                    .toEpochMilli();
+        } catch (DateTimeParseException ignored) {
+            return defaultValue;
+        }
+    }
+
+    private String normalizedTimestampText(String text) {
+        return text.replaceFirst("\\s+", "T")
+                .replaceFirst("(?i)\\s+UTC$", "Z")
+                .replaceFirst("\\s+(?=Z$|[+-]\\d{2}:?\\d{2}$)", "")
+                .replaceFirst("([+-]\\d{2})(\\d{2})$", "$1:$2");
+    }
+
+    private String escapeSql(String value) {
+        return value == null ? "" : value.replace("'", "''");
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventSink.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventSink.java
new file mode 100644
index 0000000000..b45c9e4b7b
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventSink.java
@@ -0,0 +1,300 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.audit;
+
+import io.grpc.Status;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.redaction.OtlpIngestionRedactionService;
+import org.apache.hertzbeat.observability.ingestion.retry.OtlpIngestionRetryService;
+import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriUtils;
+
+/**
+ * Persists OTLP ingest RED audit events into Greptime SQL for durable release-readiness evidence.
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "warehouse.store.greptime", name = "enabled", havingValue = "true")
+public class GreptimeOtlpIngestionAuditEventSink implements OtlpIngestionAuditEventSink {
+
+    static final String TABLE_NAME = "hertzbeat_otlp_ingest_red";
+
+    private static final String SQL_PATH = "/v1/sql";
+    private static final String DEFAULT_GREPTIME_DB_NAME = "public";
+    private static final Set<String> SUPPORTED_SIGNALS = Set.of("metrics", "logs", "traces");
+    private static final Set<String> SUPPORTED_PROTOCOLS = Set.of("http", "grpc");
+    private static final Set<String> SUPPORTED_OUTCOMES = Set.of("accepted", "rejected", "dropped");
+    private static final Set<String> SUPPORTED_STATUS_CODES = Set.of(
+            "OK",
+            "CANCELLED",
+            "UNKNOWN",
+            "INVALID_ARGUMENT",
+            "DEADLINE_EXCEEDED",
+            "NOT_FOUND",
+            "ALREADY_EXISTS",
+            "PERMISSION_DENIED",
+            "RESOURCE_EXHAUSTED",
+            "FAILED_PRECONDITION",
+            "ABORTED",
+            "OUT_OF_RANGE",
+            "UNIMPLEMENTED",
+            "INTERNAL",
+            "UNAVAILABLE",
+            "DATA_LOSS",
+            "UNAUTHENTICATED"
+    );
+    private static final String CREATE_TABLE_SQL = """
+            CREATE TABLE IF NOT EXISTS hertzbeat_otlp_ingest_red (
+              observed_at TIMESTAMP(3) TIME INDEX,
+              workspace_id STRING NULL,
+              entity_id STRING NULL,
+              ingest_id STRING NULL,
+              signal STRING,
+              protocol STRING,
+              outcome STRING,
+              status_code STRING,
+              request_bytes BIGINT,
+              signal_items BIGINT NULL,
+              duration_millis BIGINT NULL,
+              reason STRING NULL,
+              PRIMARY KEY(workspace_id, signal, protocol, outcome, status_code)
+            )
+            """;
+
+    private final RestTemplate restTemplate;
+    private final ObjectProvider<GreptimeProperties> greptimePropertiesProvider;
+    private final OtlpIngestionRetryService retryService;
+    private final OtlpIngestionRedactionService redactionService;
+    private final AtomicBoolean tableInitialized = new AtomicBoolean(false);
+
+    GreptimeOtlpIngestionAuditEventSink(RestTemplate restTemplate,
+                                        ObjectProvider<GreptimeProperties> greptimePropertiesProvider) {
+        this(restTemplate, greptimePropertiesProvider, new OtlpIngestionRetryService(),
+                new OtlpIngestionRedactionService());
+    }
+
+    public GreptimeOtlpIngestionAuditEventSink(RestTemplate restTemplate,
+                                               ObjectProvider<GreptimeProperties> greptimePropertiesProvider,
+                                               OtlpIngestionRetryService retryService) {
+        this(restTemplate, greptimePropertiesProvider, retryService, new OtlpIngestionRedactionService());
+    }
+
+    @Autowired
+    public GreptimeOtlpIngestionAuditEventSink(RestTemplate restTemplate,
+                                               ObjectProvider<GreptimeProperties> greptimePropertiesProvider,
+                                               OtlpIngestionRetryService retryService,
+                                               OtlpIngestionRedactionService redactionService) {
+        this.restTemplate = restTemplate;
+        this.greptimePropertiesProvider = greptimePropertiesProvider;
+        this.retryService = retryService == null ? new OtlpIngestionRetryService() : retryService;
+        this.redactionService = redactionService == null ? new OtlpIngestionRedactionService() : redactionService;
+    }
+
+    @Override
+    public void record(OtlpIngestionAuditEvent event) {
+        GreptimeProperties greptimeProperties = greptimePropertiesOrNull();
+        if (event == null || greptimeProperties == null || !greptimeProperties.enabled()
+                || StringUtils.isBlank(greptimeProperties.httpEndpoint()) || event.observedAt() <= 0
+                || !hasSupportedAuditClassification(event)) {
+            return;
+        }
+        ensureTable(greptimeProperties);
+        execute(greptimeProperties, insertSql(event));
+    }
+
+    private GreptimeProperties greptimePropertiesOrNull() {
+        try {
+            return greptimePropertiesProvider.getIfAvailable();
+        } catch (RuntimeException ex) {
+            log.warn("[otlp-ingest-red] failed to resolve Greptime properties: {}", ex.getMessage(), ex);
+            return null;
+        }
+    }
+
+    private void ensureTable(GreptimeProperties greptimeProperties) {
+        if (tableInitialized.get()) {
+            return;
+        }
+        synchronized (tableInitialized) {
+            if (tableInitialized.get()) {
+                return;
+            }
+            execute(greptimeProperties, CREATE_TABLE_SQL.strip());
+            tableInitialized.set(true);
+        }
+    }
+
+    private void execute(GreptimeProperties greptimeProperties, String sql) {
+        try {
+            ResponseEntity<String> response = retryService.execute(() -> restTemplate.exchange(
+                            endpoint(greptimeProperties),
+                            HttpMethod.POST,
+                            sqlRequest(greptimeProperties, sql),
+                            String.class
+                    ),
+                    responseEntity -> responseEntity == null
+                            || retryService.isRetryableStatus(responseEntity.getStatusCode()));
+            if (response == null) {
+                throw new IllegalStateException("Greptime SQL returned no response");
+            }
+            HttpStatusCode statusCode = response.getStatusCode();
+            if (!statusCode.is2xxSuccessful()) {
+                throw new IllegalStateException("Greptime SQL returned " + statusCode);
+            }
+        } catch (RestClientException ex) {
+            log.warn("[otlp-ingest-red] failed to persist Greptime audit event: {}", ex.getMessage(), ex);
+            throw ex;
+        }
+    }
+
+    private HttpEntity<String> sqlRequest(GreptimeProperties greptimeProperties, String sql) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        headers.setAccept(List.of(MediaType.APPLICATION_JSON));
+        String username = StringUtils.trimToNull(greptimeProperties.username());
+        String password = StringUtils.trimToNull(greptimeProperties.password());
+        if (username != null && password != null) {
+            headers.setBasicAuth(username, password, StandardCharsets.UTF_8);
+        }
+        return new HttpEntity<>("sql=" + UriUtils.encodeQueryParam(sql, StandardCharsets.UTF_8), headers);
+    }
+
+    private String endpoint(GreptimeProperties greptimeProperties) {
+        String endpoint = StringUtils.stripEnd(StringUtils.trim(greptimeProperties.httpEndpoint()), "/") + SQL_PATH;
+        return endpoint + "?db=" + UriUtils.encodeQueryParam(database(greptimeProperties.database()),
+                StandardCharsets.UTF_8);
+    }
+
+    private String database(String configuredDatabase) {
+        return StringUtils.defaultIfBlank(StringUtils.trim(configuredDatabase), DEFAULT_GREPTIME_DB_NAME);
+    }
+
+    private String insertSql(OtlpIngestionAuditEvent event) {
+        return "INSERT INTO " + TABLE_NAME + " (observed_at, workspace_id, entity_id, ingest_id, signal, protocol, "
+                + "outcome, status_code, request_bytes, signal_items, duration_millis, reason) VALUES ("
+                + "to_timestamp_millis(" + Math.max(event.observedAt(), 0L) + "), "
+                + sqlLiteral(trimmedOptional(event.workspaceId())) + ", "
+                + sqlLiteral(trimmedOptional(event.entityId())) + ", "
+                + sqlLiteral(trimmedOptional(event.ingestId())) + ", "
+                + sqlLiteral(normalizedDimension(event.signal())) + ", "
+                + sqlLiteral(normalizedDimension(event.protocol())) + ", "
+                + sqlLiteral(normalizedDimension(event.outcome())) + ", "
+                + sqlLiteral(normalizedStatusCode(event.statusCode())) + ", "
+                + Math.max(event.requestBytes(), 0L) + ", "
+                + sqlNumber(event.signalItems()) + ", "
+                + sqlNumber(event.durationMillis()) + ", "
+                + sqlLiteral(redactedReason(event.reason())) + ")";
+    }
+
+    private boolean hasSupportedAuditClassification(OtlpIngestionAuditEvent event) {
+        String signal = normalizedDimension(event.signal());
+        String protocol = normalizedDimension(event.protocol());
+        String outcome = normalizedDimension(event.outcome());
+        String statusCode = normalizedStatusCode(event.statusCode());
+        return signal != null && protocol != null && outcome != null
+                && SUPPORTED_SIGNALS.contains(signal)
+                && SUPPORTED_PROTOCOLS.contains(protocol)
+                && SUPPORTED_OUTCOMES.contains(outcome)
+                && statusCode != null;
+    }
+
+    private String normalizedDimension(String value) {
+        String text = StringUtils.trimToNull(value);
+        return text == null ? null : text.toLowerCase(Locale.ROOT);
+    }
+
+    private String normalizedStatusCode(String value) {
+        String text = StringUtils.trimToNull(value);
+        if (text == null) {
+            return null;
+        }
+        String grpcStatusName = numericGrpcStatusName(text);
+        if (grpcStatusName != null) {
+            return grpcStatusName;
+        }
+        if (isNumericStatusCode(text)) {
+            return null;
+        }
+        String normalized = text.replaceAll("[\\s-]+", "_").replaceAll("^_+|_+$", "");
+        normalized = StringUtils.trimToNull(normalized) == null ? null : normalized.toUpperCase(Locale.ROOT);
+        return normalized != null && SUPPORTED_STATUS_CODES.contains(normalized) ? normalized : null;
+    }
+
+    private String numericGrpcStatusName(String text) {
+        try {
+            BigDecimal numericStatus = new BigDecimal(text).stripTrailingZeros();
+            if (numericStatus.scale() > 0) {
+                return null;
+            }
+            int codeValue = numericStatus.intValueExact();
+            if (codeValue < Status.Code.OK.value() || codeValue > Status.Code.UNAUTHENTICATED.value()) {
+                return null;
+            }
+            return Status.fromCodeValue(codeValue).getCode().name();
+        } catch (ArithmeticException | NumberFormatException ignored) {
+            return null;
+        }
+    }
+
+    private boolean isNumericStatusCode(String text) {
+        try {
+            new BigDecimal(text);
+            return true;
+        } catch (NumberFormatException ignored) {
+            return false;
+        }
+    }
+
+    private String trimmedOptional(String value) {
+        return StringUtils.trimToNull(value);
+    }
+
+    private String redactedReason(String value) {
+        return StringUtils.trimToNull(redactionService.redactText(value));
+    }
+
+    private String sqlNumber(Long value) {
+        return value == null ? "NULL" : String.valueOf(Math.max(value, 0L));
+    }
+
+    private String sqlLiteral(String value) {
+        if (value == null) {
+            return "NULL";
+        }
+        return "'" + value.replace("'", "''") + "'";
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEvent.java
similarity index 55%
copy from hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
copy to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEvent.java
index d4398dd193..f5ba3cb078 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEvent.java
@@ -15,24 +15,23 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.observability.ingestion.enricher;
-
-import java.util.UUID;
-import org.apache.commons.lang3.StringUtils;
+package org.apache.hertzbeat.observability.ingestion.audit;
 
 /**
- * Request-scoped HertzBeat correlation values to inject into OTLP records.
+ * Recent OTLP ingest audit/self-observability event.
  */
-public record OtlpCorrelationContext(String ingestId, String entityId, String workspaceId) {
-
-    public static OtlpCorrelationContext empty() {
-        return new OtlpCorrelationContext(null, null, null);
-    }
-
-    OtlpCorrelationContext withIngestId() {
-        if (StringUtils.isNotBlank(ingestId)) {
-            return this;
-        }
-        return new OtlpCorrelationContext(UUID.randomUUID().toString(), entityId, workspaceId);
-    }
+public record OtlpIngestionAuditEvent(
+        String signal,
+        String protocol,
+        String outcome,
+        String statusCode,
+        String workspaceId,
+        String entityId,
+        String ingestId,
+        long requestBytes,
+        Long signalItems,
+        String reason,
+        Long durationMillis,
+        long observedAt
+) {
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEventReader.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEventReader.java
new file mode 100644
index 0000000000..45a70836cb
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEventReader.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.audit;
+
+import java.util.Comparator;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Durable/readback source for OTLP ingest audit events.
+ */
+@FunctionalInterface
+public interface OtlpIngestionAuditEventReader {
+
+    int DEFAULT_MAX_LIMIT = 256;
+
+    List<OtlpIngestionAuditEvent> recentEvents(String workspaceId, int limit);
+
+    default List<OtlpIngestionAuditEvent> recentEvents(String workspaceId, int limit, Long startMillis) {
+        return recentEvents(workspaceId, limit, startMillis, null);
+    }
+
+    default List<OtlpIngestionAuditEvent> recentEvents(String workspaceId, int limit, Long startMillis,
+                                                       Long endMillis) {
+        Long safeStartMillis = startMillis == null || startMillis <= 0 ? null : startMillis;
+        Long safeEndMillis = endMillis == null || endMillis <= 0 ? null : endMillis;
+        String safeWorkspaceId = StringUtils.trimToNull(workspaceId);
+        int safeLimit = boundedLimit(limit);
+        return recentEvents(safeWorkspaceId, safeLimit).stream()
+                .filter(event -> event != null)
+                .filter(event -> event.observedAt() > 0)
+                .filter(event -> safeWorkspaceId == null
+                        || safeWorkspaceId.equals(StringUtils.trimToNull(event.workspaceId())))
+                .filter(event -> safeStartMillis == null || event.observedAt() >= safeStartMillis)
+                .filter(event -> safeEndMillis == null || event.observedAt() <= safeEndMillis)
+                .sorted(Comparator.comparingLong(OtlpIngestionAuditEvent::observedAt).reversed())
+                .limit(safeLimit)
+                .toList();
+    }
+
+    private static int boundedLimit(int limit) {
+        if (limit <= 0) {
+            return DEFAULT_MAX_LIMIT;
+        }
+        return Math.min(limit, DEFAULT_MAX_LIMIT);
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEventSink.java
similarity index 55%
copy from hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
copy to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEventSink.java
index d4398dd193..636e4c81d3 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditEventSink.java
@@ -15,24 +15,13 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.observability.ingestion.enricher;
-
-import java.util.UUID;
-import org.apache.commons.lang3.StringUtils;
+package org.apache.hertzbeat.observability.ingestion.audit;
 
 /**
- * Request-scoped HertzBeat correlation values to inject into OTLP records.
+ * Durable/exportable sink for OTLP ingest audit events.
  */
-public record OtlpCorrelationContext(String ingestId, String entityId, String workspaceId) {
-
-    public static OtlpCorrelationContext empty() {
-        return new OtlpCorrelationContext(null, null, null);
-    }
+@FunctionalInterface
+public interface OtlpIngestionAuditEventSink {
 
-    OtlpCorrelationContext withIngestId() {
-        if (StringUtils.isNotBlank(ingestId)) {
-            return this;
-        }
-        return new OtlpCorrelationContext(UUID.randomUUID().toString(), entityId, workspaceId);
-    }
+    void record(OtlpIngestionAuditEvent event);
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditService.java
new file mode 100644
index 0000000000..95c2f651ca
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/audit/OtlpIngestionAuditService.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.audit;
+
+import io.grpc.Status;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.enricher.OtlpCorrelationContext;
+import org.apache.hertzbeat.observability.ingestion.redaction.OtlpIngestionRedactionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * Shared OTLP ingest audit and self-observability boundary.
+ */
+@Service
+public class OtlpIngestionAuditService {
+
+    private static final int MAX_RECENT_EVENTS = 256;
+    private static final String OUTCOME_ACCEPTED = "accepted";
+    private static final String OUTCOME_REJECTED = "rejected";
+    private static final String OUTCOME_DROPPED = "dropped";
+    private static final String STATUS_OK = "OK";
+    private static final String UNKNOWN = "unknown";
+    private static final Set<String> SUPPORTED_SIGNALS = Set.of("metrics", "logs", "traces");
+    private static final Set<String> SUPPORTED_PROTOCOLS = Set.of("http", "grpc");
+
+    private final ConcurrentLinkedDeque<OtlpIngestionAuditEvent> recentEvents = new ConcurrentLinkedDeque<>();
+    private final OtlpIngestionRedactionService redactionService;
+    private final List<OtlpIngestionAuditEventSink> eventSinks;
+
+    public OtlpIngestionAuditService() {
+        this(new OtlpIngestionRedactionService());
+    }
+
+    @Autowired
+    public OtlpIngestionAuditService(OtlpIngestionRedactionService redactionService,
+                                     List<OtlpIngestionAuditEventSink> eventSinks) {
+        this.redactionService = redactionService == null ? new OtlpIngestionRedactionService() : redactionService;
+        this.eventSinks = eventSinks == null ? List.of() : List.copyOf(eventSinks.stream()
+                .filter(Objects::nonNull)
+                .toList());
+    }
+
+    public OtlpIngestionAuditService(OtlpIngestionRedactionService redactionService) {
+        this(redactionService, List.of());
+    }
+
+    public void recordAccepted(String signal, String protocol, OtlpCorrelationContext context, long requestBytes) {
+        recordAccepted(signal, protocol, context, requestBytes, null, null);
+    }
+
+    public void recordAccepted(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                               Long durationMillis) {
+        recordAccepted(signal, protocol, context, requestBytes, null, durationMillis);
+    }
+
+    public void recordAccepted(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                               Long signalItems, Long durationMillis) {
+        record(signal, protocol, OUTCOME_ACCEPTED, STATUS_OK, context, requestBytes, signalItems, null,
+                durationMillis);
+    }
+
+    public void recordDropped(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                              Long signalItems, String reason, Long durationMillis) {
+        record(signal, protocol, OUTCOME_DROPPED, STATUS_OK, context, requestBytes, signalItems, reason,
+                durationMillis);
+    }
+
+    public void recordRejected(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                               Status.Code statusCode, String reason) {
+        recordRejected(signal, protocol, context, requestBytes, statusCode, reason, null, null);
+    }
+
+    public void recordRejected(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                               Status.Code statusCode, String reason, Long durationMillis) {
+        recordRejected(signal, protocol, context, requestBytes, statusCode, reason, null, durationMillis);
+    }
+
+    public void recordRejected(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                               Status.Code statusCode, String reason, Long signalItems, Long durationMillis) {
+        record(signal, protocol, OUTCOME_REJECTED, statusCode == null ? Status.Code.UNKNOWN.name() : statusCode.name(),
+                context, requestBytes, signalItems, reason, durationMillis);
+    }
+
+    public void recordRejected(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                               Throwable throwable) {
+        recordRejected(signal, protocol, context, requestBytes, throwable, null, null);
+    }
+
+    public void recordRejected(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                               Throwable throwable, Long durationMillis) {
+        recordRejected(signal, protocol, context, requestBytes, throwable, null, durationMillis);
+    }
+
+    public void recordRejected(String signal, String protocol, OtlpCorrelationContext context, long requestBytes,
+                               Throwable throwable, Long signalItems, Long durationMillis) {
+        if (throwable == null) {
+            recordRejected(signal, protocol, context, requestBytes, Status.Code.UNKNOWN, null, signalItems,
+                    durationMillis);
+            return;
+        }
+        Status status = Status.fromThrowable(throwable);
+        recordRejected(signal, protocol, context, requestBytes, status.getCode(),
+                StringUtils.defaultIfBlank(status.getDescription(),
+                        throwable.getMessage()), signalItems, durationMillis);
+    }
+
+    public List<OtlpIngestionAuditEvent> recentEvents() {
+        return Collections.unmodifiableList(recentEvents.stream().toList());
+    }
+
+    private void record(String signal, String protocol, String outcome, String statusCode,
+                        OtlpCorrelationContext context, long requestBytes, Long signalItems, String reason,
+                        Long durationMillis) {
+        String normalizedSignal = normalizeLowerDimension(signal);
+        String normalizedProtocol = normalizeLowerDimension(protocol);
+        if (!hasSupportedSignalProtocol(normalizedSignal, normalizedProtocol)) {
+            return;
+        }
+        OtlpCorrelationContext safeContext = context == null ? OtlpCorrelationContext.empty() : context;
+        OtlpIngestionAuditEvent event = new OtlpIngestionAuditEvent(
+                normalizedSignal,
+                normalizedProtocol,
+                normalizeLowerDimension(outcome),
+                normalizeDimension(statusCode),
+                StringUtils.trimToNull(safeContext.workspaceId()),
+                StringUtils.trimToNull(safeContext.entityId()),
+                StringUtils.trimToNull(safeContext.ingestId()),
+                Math.max(requestBytes, 0L),
+                signalItems == null ? null : Math.max(signalItems, 0L),
+                StringUtils.trimToNull(redactionService.redactText(reason)),
+                durationMillis == null ? null : Math.max(durationMillis, 0L),
+                System.currentTimeMillis()
+        );
+        recentEvents.addFirst(event);
+        trimRecentEvents();
+        publishEvent(event);
+    }
+
+    private void trimRecentEvents() {
+        while (recentEvents.size() > MAX_RECENT_EVENTS) {
+            recentEvents.pollLast();
+        }
+    }
+
+    private void publishEvent(OtlpIngestionAuditEvent event) {
+        for (OtlpIngestionAuditEventSink eventSink : eventSinks) {
+            try {
+                eventSink.record(event);
+            } catch (RuntimeException ignored) {
+                // Ingest audit export must not make the ingest path fail.
+            }
+        }
+    }
+
+    private boolean hasSupportedSignalProtocol(String signal, String protocol) {
+        return SUPPORTED_SIGNALS.contains(signal) && SUPPORTED_PROTOCOLS.contains(protocol);
+    }
+
+    private String normalizeDimension(String value) {
+        return StringUtils.defaultIfBlank(StringUtils.trimToNull(value), UNKNOWN);
+    }
+
+    private String normalizeLowerDimension(String value) {
+        return normalizeDimension(value).toLowerCase(Locale.ROOT);
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/config/OtlpGrpcServerConfig.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/config/OtlpGrpcServerConfig.java
index 0121ce1c32..be81d167da 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/config/OtlpGrpcServerConfig.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/config/OtlpGrpcServerConfig.java
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.observability.ingestion.config;
 
 import com.usthe.sureness.util.JsonWebTokenUtil;
 import io.grpc.Contexts;
+import io.grpc.ForwardingServerCallListener;
 import io.grpc.Metadata;
 import io.grpc.Server;
 import io.grpc.ServerCall;
@@ -35,6 +36,8 @@ import java.util.List;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.common.observability.gateway.ObservabilityAccessTokenGateway;
 import org.apache.hertzbeat.observability.ingestion.grpc.OtlpGrpcLogsService;
 import org.apache.hertzbeat.observability.ingestion.grpc.OtlpGrpcMetricsService;
@@ -103,12 +106,15 @@ public class OtlpGrpcServerConfig {
     static final class BearerTokenInterceptor implements ServerInterceptor {
         private static final Metadata.Key<String> AUTHORIZATION =
                 Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
+        private static final Metadata.Key<String> WORKSPACE_ID =
+                Metadata.Key.of(AuthTokenScopes.WORKSPACE_ID_HEADER.toLowerCase(), Metadata.ASCII_STRING_MARSHALLER);
         private final ObservabilityAccessTokenGateway accessTokenGateway;
 
         @Override
         public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
                                                                      Metadata headers,
                                                                      ServerCallHandler<ReqT, RespT> next) {
+            AuthTokenRequestContext.clear();
             String authorization = headers.get(AUTHORIZATION);
             if (!StringUtils.startsWithIgnoreCase(StringUtils.defaultString(authorization), "Bearer ")) {
                 call.close(Status.UNAUTHENTICATED.withDescription("Missing OTLP access token."), new Metadata());
@@ -116,6 +122,7 @@ public class OtlpGrpcServerConfig {
                 };
             }
             String token = StringUtils.substringAfter(authorization, "Bearer ").trim();
+            String workspaceId = null;
             try {
                 Claims claims = JsonWebTokenUtil.parseJwt(token);
                 Boolean refresh = claims.get("refresh", Boolean.class);
@@ -126,7 +133,10 @@ public class OtlpGrpcServerConfig {
                 if (!Boolean.TRUE.equals(managed)) {
                     throw new IllegalArgumentException("Managed token is required.");
                 }
-                String rejectReason = accessTokenGateway.checkTokenStatus(token);
+                workspaceId = resolveWorkspaceId(headers, claims);
+                String rejectReason = workspaceId == null
+                        ? accessTokenGateway.checkTokenStatus(token, AuthTokenScopes.OTLP_INGEST)
+                        : accessTokenGateway.checkTokenStatus(token, AuthTokenScopes.OTLP_INGEST, workspaceId);
                 if (rejectReason != null) {
                     call.close(Status.UNAUTHENTICATED.withDescription(rejectReason), new Metadata());
                     return new ServerCall.Listener<>() {
@@ -146,11 +156,82 @@ public class OtlpGrpcServerConfig {
                 }
                 accessTokenGateway.touchTokenLastUsedTime(token);
             } catch (Exception ex) {
+                AuthTokenRequestContext.clear();
                 call.close(Status.UNAUTHENTICATED.withDescription("Invalid OTLP access token."), new Metadata());
                 return new ServerCall.Listener<>() {
                 };
             }
-            return Contexts.interceptCall(io.grpc.Context.current(), call, headers, next);
+            return interceptWorkspaceBoundCall(workspaceId, call, headers, next);
+        }
+
+        private String resolveWorkspaceId(Metadata headers, Claims claims) {
+            String workspaceId = StringUtils.trimToNull(headers.get(WORKSPACE_ID));
+            if (workspaceId == null) {
+                workspaceId = StringUtils.trimToNull(claims.get(AuthTokenScopes.CLAIM_WORKSPACE_ID, String.class));
+            }
+            return workspaceId == null ? null : AuthTokenScopes.normalizeWorkspaceId(workspaceId);
+        }
+
+        private <ReqT, RespT> ServerCall.Listener<ReqT> interceptWorkspaceBoundCall(
+                String workspaceId,
+                ServerCall<ReqT, RespT> call,
+                Metadata headers,
+                ServerCallHandler<ReqT, RespT> next) {
+            bindWorkspace(workspaceId);
+            try {
+                ServerCall.Listener<ReqT> listener = Contexts.interceptCall(
+                        io.grpc.Context.current(), call, headers, next);
+                return workspaceBoundListener(listener, workspaceId);
+            } finally {
+                AuthTokenRequestContext.clear();
+            }
+        }
+
+        private <ReqT> ServerCall.Listener<ReqT> workspaceBoundListener(
+                ServerCall.Listener<ReqT> listener, String workspaceId) {
+            return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(listener) {
+                @Override
+                public void onMessage(ReqT message) {
+                    withWorkspace(workspaceId, () -> super.onMessage(message));
+                }
+
+                @Override
+                public void onHalfClose() {
+                    withWorkspace(workspaceId, super::onHalfClose);
+                }
+
+                @Override
+                public void onCancel() {
+                    withWorkspace(workspaceId, super::onCancel);
+                }
+
+                @Override
+                public void onComplete() {
+                    withWorkspace(workspaceId, super::onComplete);
+                }
+
+                @Override
+                public void onReady() {
+                    withWorkspace(workspaceId, super::onReady);
+                }
+            };
+        }
+
+        private void withWorkspace(String workspaceId, Runnable action) {
+            bindWorkspace(workspaceId);
+            try {
+                action.run();
+            } finally {
+                AuthTokenRequestContext.clear();
+            }
+        }
+
+        private void bindWorkspace(String workspaceId) {
+            if (StringUtils.isBlank(workspaceId)) {
+                AuthTokenRequestContext.clear();
+                return;
+            }
+            AuthTokenRequestContext.bindWorkspaceId(workspaceId);
         }
     }
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionController.java
index f8aa57f429..5f12e9c217 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionController.java
@@ -25,7 +25,9 @@ import org.apache.hertzbeat.common.entity.dto.Message;
 import org.apache.hertzbeat.common.observability.dto.binding.OtlpEntityBindingSummaryDto;
 import org.apache.hertzbeat.common.observability.dto.ingestion.OtlpIngestionGuideDto;
 import org.apache.hertzbeat.common.observability.dto.ingestion.OtlpIngestionOverviewDto;
+import org.apache.hertzbeat.common.observability.dto.ingestion.OtlpIngestionRedSummaryDto;
 import org.apache.hertzbeat.common.observability.dto.metrics.OtlpMetricsConsoleDto;
+import org.apache.hertzbeat.observability.ingestion.red.OtlpIngestionRedSummaryService;
 import org.apache.hertzbeat.observability.ingestion.service.OtlpIngestionWorkspaceService;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -43,6 +45,7 @@ import org.springframework.web.bind.annotation.RequestParam;
 public class OtlpIngestionController {
 
     private final OtlpIngestionWorkspaceService otlpIngestionWorkspaceService;
+    private final OtlpIngestionRedSummaryService otlpIngestionRedSummaryService;
 
     @GetMapping("/overview")
     @Operation(summary = "Unified OTLP ingestion overview")
@@ -62,6 +65,14 @@ public class OtlpIngestionController {
         return ResponseEntity.ok(Message.success(otlpIngestionWorkspaceService.getBindingSummary()));
     }
 
+    @GetMapping("/intake/red")
+    @Operation(summary = "Recent OTLP ingest request, error, and duration summary")
+    public ResponseEntity<Message<OtlpIngestionRedSummaryDto>> intakeRedSummary(
+            @RequestParam(value = "start", required = false) Long start,
+            @RequestParam(value = "end", required = false) Long end) {
+        return ResponseEntity.ok(Message.success(otlpIngestionRedSummaryService.getSummary(start, end)));
+    }
+
     @GetMapping("/metrics/console")
     @Operation(summary = "OTLP metrics console query workspace")
     public ResponseEntity<Message<OtlpMetricsConsoleDto>> metricsConsole(
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpLogController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpLogController.java
index d78d3deef8..baeeebe070 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpLogController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpLogController.java
@@ -17,22 +17,11 @@
 
 package org.apache.hertzbeat.observability.ingestion.controller;
 
-import com.fasterxml.jackson.core.io.JsonStringEncoder;
-import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.protobuf.util.JsonFormat;
-import com.google.rpc.Status;
-import io.grpc.StatusRuntimeException;
-import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
-import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.hertzbeat.observability.ingestion.adapter.OtlpLogProtocolAdapter;
-import org.apache.hertzbeat.observability.ingestion.enricher.OtlpCorrelationContext;
-import org.apache.hertzbeat.observability.ingestion.enricher.OtlpCorrelationEnricher;
-import org.apache.hertzbeat.observability.ingestion.forwarder.GreptimeOtlpForwarder;
+import lombok.RequiredArgsConstructor;
+import org.apache.hertzbeat.observability.ingestion.service.OtlpGrpcIngestionService;
 import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -51,60 +40,33 @@ import org.springframework.web.bind.annotation.RestController;
 @Tag(name = "OTLP Log Controller")
 @RestController
 @RequestMapping(path = {"/api/logs/otlp", "/api/otlp"})
-@Slf4j
+@RequiredArgsConstructor
 public class OtlpLogController {
 
     private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf";
-    private static final ExportLogsServiceResponse EMPTY_RESPONSE = ExportLogsServiceResponse.newBuilder().build();
+    private static final String CONTENT_TYPE_PROTOBUF_ALT = "application/protobuf";
 
-    private final OtlpLogProtocolAdapter otlpLogProtocolAdapter;
-    private final OtlpCorrelationEnricher otlpCorrelationEnricher;
-    private final GreptimeOtlpForwarder greptimeOtlpForwarder;
-
-    public OtlpLogController(OtlpLogProtocolAdapter otlpLogProtocolAdapter,
-                             OtlpCorrelationEnricher otlpCorrelationEnricher,
-                             GreptimeOtlpForwarder greptimeOtlpForwarder) {
-        this.otlpLogProtocolAdapter = otlpLogProtocolAdapter;
-        this.otlpCorrelationEnricher = otlpCorrelationEnricher;
-        this.greptimeOtlpForwarder = greptimeOtlpForwarder;
-    }
+    private final OtlpGrpcIngestionService otlpGrpcIngestionService;
 
     /**
      * OTLP/HTTP standard endpoint for logs with JSON-encoded Protobuf payload.
      * Content-Type: application/json
      *
      * Response follows OTLP specification:
-     * - Success: HTTP 200 with ExportLogsServiceResponse (JSON encoded)
-     * - Failure: HTTP 400 with google.rpc.Status (JSON encoded)
+     * - Success: HTTP 200 with ExportLogsServiceResponse (JSON or binary encoded)
+     * - Failure: HTTP 400 with google.rpc.Status (JSON or binary encoded)
      *
      * @param content JSON-encoded ExportLogsServiceRequest
      * @return ExportLogsServiceResponse on success, Status on failure
      */
     @Operation(summary = "Ingest OTLP logs (JSON format)")
-    @PostMapping(value = "/v1/logs", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
-    public ResponseEntity<String> ingestJsonLogs(@RequestBody byte[] content, @RequestHeader HttpHeaders requestHeaders) {
-        log.debug("Receive OTLP JSON logs, content length: {}", content == null ? 0 : content.length);
-        try {
-            byte[] enrichedContent = otlpCorrelationEnricher.enrichLogsHttp(
-                    content, requestHeaders, OtlpCorrelationContext.empty());
-            greptimeOtlpForwarder.forwardLogsProtobuf(enrichedContent);
-            otlpLogProtocolAdapter.publishRealtimeSignals(ExportLogsServiceRequest.parseFrom(enrichedContent));
-            return ResponseEntity.ok(toJsonResponse(EMPTY_RESPONSE));
-        } catch (StatusRuntimeException e) {
-            return ResponseEntity.status(toHttpStatus(e))
-                    .body(toJsonErrorResponse(e.getStatus().getDescription()));
-        } catch (IllegalArgumentException e) {
-            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
-                    .body(toJsonErrorResponse(e.getMessage()));
-        } catch (InvalidProtocolBufferException e) {
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
-                    .body(toJsonErrorResponse(e.getMessage()));
-        } catch (Exception e) {
-            // Server-side errors - unexpected failure
-            log.error("Unexpected error ingesting OTLP JSON logs: {}", e.getMessage(), e);
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
-                    .body(toJsonErrorResponse(e.getMessage()));
-        }
+    @PostMapping(
+            value = "/v1/logs",
+            consumes = MediaType.APPLICATION_JSON_VALUE,
+            produces = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF, CONTENT_TYPE_PROTOBUF_ALT}
+    )
+    public ResponseEntity<byte[]> ingestJsonLogs(@RequestBody byte[] content, @RequestHeader HttpHeaders requestHeaders) {
+        return otlpGrpcIngestionService.ingestLogsHttp(content, requestHeaders);
     }
 
     /**
@@ -119,77 +81,12 @@ public class OtlpLogController {
      * @return ExportLogsServiceResponse on success, Status on failure
      */
     @Operation(summary = "Ingest OTLP logs (binary Protobuf format)")
-    @PostMapping(value = "/v1/logs", consumes = CONTENT_TYPE_PROTOBUF, produces = CONTENT_TYPE_PROTOBUF)
+    @PostMapping(
+            value = "/v1/logs",
+            consumes = {CONTENT_TYPE_PROTOBUF, CONTENT_TYPE_PROTOBUF_ALT},
+            produces = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF, CONTENT_TYPE_PROTOBUF_ALT}
+    )
     public ResponseEntity<byte[]> ingestBinaryLogs(@RequestBody byte[] content, @RequestHeader HttpHeaders requestHeaders) {
-        log.debug("Receive OTLP binary logs, content length: {}", content == null ? 0 : content.length);
-        try {
-            byte[] enrichedContent = otlpCorrelationEnricher.enrichLogsHttp(
-                    content, requestHeaders, OtlpCorrelationContext.empty());
-            greptimeOtlpForwarder.forwardLogsProtobuf(enrichedContent);
-            otlpLogProtocolAdapter.publishRealtimeSignals(ExportLogsServiceRequest.parseFrom(enrichedContent));
-            return ResponseEntity.ok(EMPTY_RESPONSE.toByteArray());
-        } catch (StatusRuntimeException e) {
-            return ResponseEntity.status(toHttpStatus(e))
-                    .body(createBinaryErrorResponse(e.getStatus().getDescription()));
-        } catch (IllegalArgumentException e) {
-            // Client-side validation errors - malformed request
-            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
-                    .body(createBinaryErrorResponse(e.getMessage()));
-        } catch (Exception e) {
-            // Server-side errors - unexpected failure
-            log.error("Unexpected error ingesting OTLP binary logs: {}", e.getMessage(), e);
-            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
-                    .body(createBinaryErrorResponse(e.getMessage()));
-        }
-    }
-
-    private String toJsonResponse(ExportLogsServiceResponse response) {
-        try {
-            return JsonFormat.printer().print(response);
-        } catch (InvalidProtocolBufferException e) {
-            log.error("Failed to convert ExportLogsServiceResponse to JSON: {}", e.getMessage(), e);
-            return "{}";
-        }
-    }
-
-    private String toJsonErrorResponse(String message) {
-        Status status = Status.newBuilder()
-                .setMessage(message != null ? message : "Unknown error")
-                .build();
-        try {
-            return JsonFormat.printer().print(status);
-        } catch (InvalidProtocolBufferException e) {
-            return "{\"message\":\"" + escapeJson(message) + "\"}";
-        }
-    }
-
-    /**
-     * Escapes a string value for safe inclusion in JSON.
-     *
-     * @param message the string to escape
-     * @return the escaped string, or empty string if message is null
-     */
-    private String escapeJson(String message) {
-        if (message == null) {
-            return "";
-        }
-
-        char[] escaped = JsonStringEncoder.getInstance().quoteAsString(message);
-        return new String(escaped);
-    }
-
-    private byte[] createBinaryErrorResponse(String message) {
-        return Status.newBuilder()
-                .setMessage(message != null ? message : "Unknown error")
-                .build()
-                .toByteArray();
-    }
-
-    private HttpStatus toHttpStatus(StatusRuntimeException exception) {
-        return switch (exception.getStatus().getCode()) {
-            case INVALID_ARGUMENT -> HttpStatus.BAD_REQUEST;
-            case UNAVAILABLE -> HttpStatus.SERVICE_UNAVAILABLE;
-            default -> HttpStatus.INTERNAL_SERVER_ERROR;
-        };
+        return otlpGrpcIngestionService.ingestLogsHttp(content, requestHeaders);
     }
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpSignalIngestionController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpSignalIngestionController.java
index a293725c56..83261f8128 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpSignalIngestionController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpSignalIngestionController.java
@@ -40,13 +40,14 @@ import org.springframework.web.bind.annotation.RestController;
 public class OtlpSignalIngestionController {
 
     private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf";
+    private static final String CONTENT_TYPE_PROTOBUF_ALT = "application/protobuf";
     private final OtlpGrpcIngestionService otlpGrpcIngestionService;
 
     @Operation(summary = "Ingest OTLP metrics through unified HertzBeat gateway")
     @PostMapping(
             value = "/v1/metrics",
-            consumes = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF},
-            produces = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF}
+            consumes = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF, CONTENT_TYPE_PROTOBUF_ALT},
+            produces = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF, CONTENT_TYPE_PROTOBUF_ALT}
     )
     public ResponseEntity<byte[]> ingestMetrics(@RequestBody byte[] content, @RequestHeader HttpHeaders requestHeaders) {
         return otlpGrpcIngestionService.ingestMetricsHttp(content, requestHeaders);
@@ -55,8 +56,8 @@ public class OtlpSignalIngestionController {
     @Operation(summary = "Ingest OTLP traces through unified HertzBeat gateway")
     @PostMapping(
             value = "/v1/traces",
-            consumes = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF},
-            produces = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF}
+            consumes = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF, CONTENT_TYPE_PROTOBUF_ALT},
+            produces = {MediaType.APPLICATION_JSON_VALUE, CONTENT_TYPE_PROTOBUF, CONTENT_TYPE_PROTOBUF_ALT}
     )
     public ResponseEntity<byte[]> ingestTraces(@RequestBody byte[] content, @RequestHeader HttpHeaders requestHeaders) {
         return otlpGrpcIngestionService.ingestTracesHttp(content, requestHeaders);
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
index d4398dd193..69bd0c7d92 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationContext.java
@@ -25,6 +25,12 @@ import org.apache.commons.lang3.StringUtils;
  */
 public record OtlpCorrelationContext(String ingestId, String entityId, String workspaceId) {
 
+    public OtlpCorrelationContext {
+        ingestId = StringUtils.trimToNull(ingestId);
+        entityId = StringUtils.trimToNull(entityId);
+        workspaceId = StringUtils.trimToNull(workspaceId);
+    }
+
     public static OtlpCorrelationContext empty() {
         return new OtlpCorrelationContext(null, null, null);
     }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationEnricher.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationEnricher.java
index f08cf60c8a..289241129f 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationEnricher.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpCorrelationEnricher.java
@@ -23,12 +23,14 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.util.JsonFormat;
 import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
 import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
 import io.opentelemetry.proto.common.v1.AnyValue;
 import io.opentelemetry.proto.common.v1.KeyValue;
 import io.opentelemetry.proto.logs.v1.LogRecord;
 import io.opentelemetry.proto.logs.v1.ResourceLogs;
 import io.opentelemetry.proto.logs.v1.ScopeLogs;
+import io.opentelemetry.proto.metrics.v1.ResourceMetrics;
 import io.opentelemetry.proto.resource.v1.Resource;
 import io.opentelemetry.proto.trace.v1.ResourceSpans;
 import java.io.ByteArrayInputStream;
@@ -44,6 +46,7 @@ import java.util.List;
 import java.util.Set;
 import java.util.zip.GZIPInputStream;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.semantic.OtlpResourceSemanticAttributes;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.stereotype.Service;
@@ -57,8 +60,8 @@ public class OtlpCorrelationEnricher {
     public static final String EVENT_ID_ATTRIBUTE = "hertzbeat.event_id";
     public static final String LOG_RECORD_UID_ATTRIBUTE = "log.record.uid";
     public static final String INGEST_ID_ATTRIBUTE = "hertzbeat.ingest_id";
-    public static final String ENTITY_ID_ATTRIBUTE = "hertzbeat.entity_id";
-    public static final String WORKSPACE_ID_ATTRIBUTE = "hertzbeat.workspace_id";
+    public static final String ENTITY_ID_ATTRIBUTE = OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID;
+    public static final String WORKSPACE_ID_ATTRIBUTE = OtlpResourceSemanticAttributes.HERTZBEAT_WORKSPACE_ID;
 
     private static final String CONTENT_ENCODING_GZIP = "gzip";
     private static final Set<String> OTLP_HEX_ID_FIELDS = Set.of("traceId", "spanId", "parentSpanId");
@@ -66,7 +69,7 @@ public class OtlpCorrelationEnricher {
 
     public byte[] enrichLogsHttp(byte[] content, HttpHeaders requestHeaders, OtlpCorrelationContext context) {
         MediaType contentType = requestHeaders == null ? null : requestHeaders.getContentType();
-        byte[] normalizedContent = maybeDecompress(content, requestHeaders);
+        byte[] normalizedContent = maybeDecompress(safeContent(content), requestHeaders, "logs");
         try {
             ExportLogsServiceRequest request;
             if (contentType != null && MediaType.APPLICATION_JSON.includes(contentType)) {
@@ -113,9 +116,26 @@ public class OtlpCorrelationEnricher {
         return requestBuilder.build();
     }
 
+    public ExportMetricsServiceRequest enrichMetrics(ExportMetricsServiceRequest request,
+                                                     OtlpCorrelationContext context) {
+        ExportMetricsServiceRequest source =
+                request == null ? ExportMetricsServiceRequest.getDefaultInstance() : request;
+        OtlpCorrelationContext resolvedContext = context == null ? OtlpCorrelationContext.empty() : context;
+        if (StringUtils.isBlank(resolvedContext.entityId()) && StringUtils.isBlank(resolvedContext.workspaceId())) {
+            return source;
+        }
+        ExportMetricsServiceRequest.Builder requestBuilder = source.toBuilder().clearResourceMetrics();
+        for (ResourceMetrics resourceMetrics : source.getResourceMetricsList()) {
+            ResourceMetrics.Builder resourceBuilder = resourceMetrics.toBuilder();
+            resourceBuilder.setResource(enrichMetricResource(resourceMetrics.getResource(), resolvedContext));
+            requestBuilder.addResourceMetrics(resourceBuilder.build());
+        }
+        return requestBuilder.build();
+    }
+
     public byte[] enrichTracesHttp(byte[] content, HttpHeaders requestHeaders, OtlpCorrelationContext context) {
         MediaType contentType = requestHeaders == null ? null : requestHeaders.getContentType();
-        byte[] normalizedContent = maybeDecompress(content, requestHeaders);
+        byte[] normalizedContent = maybeDecompress(safeContent(content), requestHeaders, "trace");
         try {
             ExportTraceServiceRequest request;
             if (contentType != null && MediaType.APPLICATION_JSON.includes(contentType)) {
@@ -166,10 +186,20 @@ public class OtlpCorrelationEnricher {
                 .build();
     }
 
+    private Resource enrichMetricResource(Resource resource, OtlpCorrelationContext context) {
+        List<KeyValue> attributes = new ArrayList<>(resource.getAttributesList());
+        upsertStringAttributeIfPresent(attributes, ENTITY_ID_ATTRIBUTE, context.entityId());
+        upsertStringAttributeIfPresent(attributes, WORKSPACE_ID_ATTRIBUTE, context.workspaceId());
+        return resource.toBuilder()
+                .clearAttributes()
+                .addAllAttributes(attributes)
+                .build();
+    }
+
     private Resource enrichTraceResource(Resource resource, OtlpCorrelationContext context) {
         List<KeyValue> attributes = new ArrayList<>(resource.getAttributesList());
         addStringAttributeIfMissing(attributes, ENTITY_ID_ATTRIBUTE, context.entityId());
-        addStringAttributeIfMissing(attributes, WORKSPACE_ID_ATTRIBUTE, context.workspaceId());
+        upsertStringAttributeIfPresent(attributes, WORKSPACE_ID_ATTRIBUTE, context.workspaceId());
         return resource.toBuilder()
                 .clearAttributes()
                 .addAllAttributes(attributes)
@@ -287,12 +317,11 @@ public class OtlpCorrelationEnricher {
                 .build();
     }
 
-    private byte[] maybeDecompress(byte[] content, HttpHeaders headers) {
+    private byte[] maybeDecompress(byte[] content, HttpHeaders headers, String signal) {
         if (content == null || content.length == 0 || headers == null) {
             return content;
         }
-        String contentEncoding = headers.getFirst(HttpHeaders.CONTENT_ENCODING);
-        if (!StringUtils.equalsIgnoreCase(contentEncoding, CONTENT_ENCODING_GZIP)) {
+        if (!isGzipEncoded(headers)) {
             return content;
         }
         try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
@@ -301,12 +330,35 @@ public class OtlpCorrelationEnricher {
             gzipInputStream.transferTo(outputStream);
             return outputStream.toByteArray();
         } catch (Exception ex) {
-            throw io.grpc.Status.INVALID_ARGUMENT.withDescription("Malformed gzip-compressed OTLP logs payload.")
+            throw io.grpc.Status.INVALID_ARGUMENT.withDescription("Malformed gzip-compressed OTLP "
+                            + signal + " payload.")
                     .withCause(ex)
                     .asRuntimeException();
         }
     }
 
+    private boolean isGzipContentEncoding(String contentEncoding) {
+        String[] encodings = StringUtils.split(contentEncoding, ',');
+        if (encodings == null || encodings.length == 0) {
+            return false;
+        }
+        for (String encoding : encodings) {
+            if (StringUtils.equalsIgnoreCase(StringUtils.trimToEmpty(encoding), CONTENT_ENCODING_GZIP)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isGzipEncoded(HttpHeaders headers) {
+        List<String> contentEncodings = headers == null ? null : headers.get(HttpHeaders.CONTENT_ENCODING);
+        return contentEncodings != null && contentEncodings.stream().anyMatch(this::isGzipContentEncoding);
+    }
+
+    private byte[] safeContent(byte[] content) {
+        return content == null ? new byte[0] : content;
+    }
+
     private String normalizeOtlpJson(String content) throws InvalidProtocolBufferException {
         try {
             JsonNode root = OBJECT_MAPPER.readTree(content);
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpEntityIdentityResolver.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpEntityIdentityResolver.java
new file mode 100644
index 0000000000..056e3c59f3
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/enricher/OtlpEntityIdentityResolver.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.enricher;
+
+import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
+import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import io.opentelemetry.proto.common.v1.AnyValue;
+import io.opentelemetry.proto.common.v1.KeyValue;
+import io.opentelemetry.proto.logs.v1.ResourceLogs;
+import io.opentelemetry.proto.metrics.v1.ResourceMetrics;
+import io.opentelemetry.proto.resource.v1.Resource;
+import io.opentelemetry.proto.trace.v1.ResourceSpans;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
+import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.common.observability.gateway.ObservabilityWorkspaceQueryGateway;
+import org.apache.hertzbeat.common.observability.model.EntityCanonicalIdentityRegistry;
+import org.apache.hertzbeat.observability.ingestion.semantic.OtlpResourceSemanticAttributes;
+import org.springframework.stereotype.Service;
+
+/**
+ * Resolves OTLP resource identities to existing HertzBeat entities when there is one clear workspace match.
+ */
+@Slf4j
+@Service
+public class OtlpEntityIdentityResolver {
+
+    private final List<ObservabilityWorkspaceQueryGateway> workspaceQueryGateways;
+
+    public OtlpEntityIdentityResolver(List<ObservabilityWorkspaceQueryGateway> workspaceQueryGateways) {
+        this.workspaceQueryGateways = workspaceQueryGateways == null ? List.of() : List.copyOf(workspaceQueryGateways);
+    }
+
+    public ExportMetricsServiceRequest enrichMetrics(ExportMetricsServiceRequest request, String workspaceId) {
+        if (request == null || request.getResourceMetricsCount() == 0) {
+            return request;
+        }
+        ExportMetricsServiceRequest.Builder requestBuilder = request.toBuilder().clearResourceMetrics();
+        for (ResourceMetrics resourceMetrics : request.getResourceMetricsList()) {
+            requestBuilder.addResourceMetrics(resourceMetrics.toBuilder()
+                    .setResource(enrichResource(resourceMetrics.getResource(), workspaceId))
+                    .build());
+        }
+        return requestBuilder.build();
+    }
+
+    public ExportLogsServiceRequest enrichLogs(ExportLogsServiceRequest request, String workspaceId) {
+        if (request == null || request.getResourceLogsCount() == 0) {
+            return request;
+        }
+        ExportLogsServiceRequest.Builder requestBuilder = request.toBuilder().clearResourceLogs();
+        for (ResourceLogs resourceLogs : request.getResourceLogsList()) {
+            requestBuilder.addResourceLogs(resourceLogs.toBuilder()
+                    .setResource(enrichResource(resourceLogs.getResource(), workspaceId))
+                    .build());
+        }
+        return requestBuilder.build();
+    }
+
+    public ExportTraceServiceRequest enrichTraces(ExportTraceServiceRequest request, String workspaceId) {
+        if (request == null || request.getResourceSpansCount() == 0) {
+            return request;
+        }
+        ExportTraceServiceRequest.Builder requestBuilder = request.toBuilder().clearResourceSpans();
+        for (ResourceSpans resourceSpans : request.getResourceSpansList()) {
+            requestBuilder.addResourceSpans(resourceSpans.toBuilder()
+                    .setResource(enrichResource(resourceSpans.getResource(), workspaceId))
+                    .build());
+        }
+        return requestBuilder.build();
+    }
+
+    public Optional<String> resolveEntityId(Map<String, String> resourceAttributes, String workspaceId) {
+        ObservabilityWorkspaceQueryGateway workspaceQueryGateway = workspaceQueryGateway();
+        String safeWorkspaceId = StringUtils.trimToNull(workspaceId);
+        if (workspaceQueryGateway == null || safeWorkspaceId == null || resourceAttributes == null
+                || resourceAttributes.isEmpty()) {
+            return Optional.empty();
+        }
+        Map<String, String> normalizedIdentities = normalizedCanonicalIdentities(resourceAttributes);
+        if (normalizedIdentities.isEmpty()) {
+            return Optional.empty();
+        }
+        try {
+            List<EntityIdentity> matchedIdentities = nullToEmpty(
+                    workspaceQueryGateway.findIdentitiesByKeysAndNormalizedValues(
+                            normalizedIdentities.keySet(), Set.copyOf(normalizedIdentities.values())));
+            Map<Long, Integer> scores = scoreMatchingIdentities(matchedIdentities, normalizedIdentities);
+            if (scores.isEmpty()) {
+                return Optional.empty();
+            }
+            Map<Long, ObserveEntity> entities =
+                    Optional.ofNullable(workspaceQueryGateway.findEntitiesByIds(scores.keySet()))
+                            .orElseGet(Collections::emptyMap);
+            Map<Long, Integer> workspaceScores = scores.entrySet().stream()
+                    .filter(entry -> workspaceMatches(entities.get(entry.getKey()), safeWorkspaceId))
+                    .collect(LinkedHashMap::new, (map, entry) -> map.put(entry.getKey(), entry.getValue()), Map::putAll);
+            if (workspaceScores.isEmpty()) {
+                return Optional.empty();
+            }
+            int topScore = workspaceScores.values().stream().max(Integer::compareTo).orElse(0);
+            List<Long> topEntityIds = workspaceScores.entrySet().stream()
+                    .filter(entry -> entry.getValue() == topScore)
+                    .map(Map.Entry::getKey)
+                    .sorted()
+                    .toList();
+            if (topEntityIds.size() != 1) {
+                return Optional.empty();
+            }
+            return Optional.of(String.valueOf(topEntityIds.getFirst()));
+        } catch (RuntimeException ex) {
+            log.warn("Failed to resolve OTLP resource entity identity for workspace {}: {}",
+                    safeWorkspaceId, ex.toString());
+            return Optional.empty();
+        }
+    }
+
+    private Resource enrichResource(Resource resource, String workspaceId) {
+        Map<String, String> resourceAttributes = stringAttributes(resource.getAttributesList());
+        if (StringUtils.isNotBlank(resourceAttributes.get(OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID))) {
+            return resource;
+        }
+        Optional<String> entityId = resolveEntityId(resourceAttributes, workspaceId);
+        if (entityId.isEmpty()) {
+            return resource;
+        }
+        return resource.toBuilder()
+                .addAttributes(stringAttribute(OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID, entityId.get()))
+                .build();
+    }
+
+    private ObservabilityWorkspaceQueryGateway workspaceQueryGateway() {
+        return workspaceQueryGateways.stream().filter(Objects::nonNull).findFirst().orElse(null);
+    }
+
+    private Map<String, String> normalizedCanonicalIdentities(Map<String, String> resourceAttributes) {
+        Map<String, String> normalized = new LinkedHashMap<>();
+        for (String identityKey : EntityCanonicalIdentityRegistry.CANONICAL_OTEL_RESOURCE_KEYS) {
+            String value = StringUtils.trimToNull(resourceAttributes.get(identityKey));
+            if (value != null) {
+                normalized.put(identityKey, normalizeIdentityValue(value));
+            }
+        }
+        return normalized;
+    }
+
+    private Map<Long, Integer> scoreMatchingIdentities(List<EntityIdentity> matchedIdentities,
+                                                       Map<String, String> normalizedIdentities) {
+        Map<Long, Integer> scores = new LinkedHashMap<>();
+        for (EntityIdentity identity : matchedIdentities) {
+            if (identity == null || identity.getEntityId() == null
+                    || StringUtils.isBlank(identity.getIdentityKey())) {
+                continue;
+            }
+            String expectedValue = normalizedIdentities.get(identity.getIdentityKey());
+            if (expectedValue == null || !expectedValue.equals(normalizedIdentityValue(identity))) {
+                continue;
+            }
+            scores.merge(identity.getEntityId(), identityScore(identity), Integer::sum);
+        }
+        return scores;
+    }
+
+    private int identityScore(EntityIdentity identity) {
+        int score = identity.getPriority() == null
+                ? EntityCanonicalIdentityRegistry.defaultPriority(identity.getIdentityKey())
+                : identity.getPriority();
+        return identity.isPrimaryIdentity() ? score + 10 : score;
+    }
+
+    private String normalizedIdentityValue(EntityIdentity identity) {
+        return StringUtils.defaultIfBlank(identity.getNormalizedValue(),
+                normalizeIdentityValue(identity.getIdentityValue()));
+    }
+
+    private String normalizeIdentityValue(String value) {
+        return value == null ? null : value.trim().toLowerCase(java.util.Locale.ROOT);
+    }
+
+    private boolean workspaceMatches(ObserveEntity entity, String workspaceId) {
+        if (entity == null || StringUtils.isBlank(workspaceId)) {
+            return false;
+        }
+        return AuthTokenScopes.normalizeWorkspaceId(workspaceId)
+                .equals(AuthTokenScopes.normalizeWorkspaceId(entity.getWorkspaceId()));
+    }
+
+    private List<EntityIdentity> nullToEmpty(List<EntityIdentity> identities) {
+        return identities == null ? List.of() : identities;
+    }
+
+    private Map<String, String> stringAttributes(List<KeyValue> attributes) {
+        if (attributes == null || attributes.isEmpty()) {
+            return Map.of();
+        }
+        return attributes.stream()
+                .filter(attribute -> attribute != null && attribute.hasValue()
+                        && attribute.getValue().getValueCase() == AnyValue.ValueCase.STRING_VALUE)
+                .collect(Collectors.toMap(
+                        KeyValue::getKey,
+                        attribute -> attribute.getValue().getStringValue(),
+                        (left, right) -> right,
+                        LinkedHashMap::new));
+    }
+
+    private KeyValue stringAttribute(String key, String value) {
+        return KeyValue.newBuilder()
+                .setKey(key)
+                .setValue(AnyValue.newBuilder().setStringValue(value).build())
+                .build();
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/error/OtlpIngestionBackpressureHeaders.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/error/OtlpIngestionBackpressureHeaders.java
new file mode 100644
index 0000000000..f9f0ea9ba1
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/error/OtlpIngestionBackpressureHeaders.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.error;
+
+import io.grpc.Metadata;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpHeaders;
+
+/**
+ * Carries backend backpressure hints across OTLP HTTP and gRPC boundaries.
+ */
+public final class OtlpIngestionBackpressureHeaders {
+
+    private static final long MAX_RETRY_AFTER_SECONDS = 60L;
+
+    public static final Metadata.Key<String> RETRY_AFTER_TRAILER_KEY =
+            Metadata.Key.of("retry-after", Metadata.ASCII_STRING_MARSHALLER);
+
+    private OtlpIngestionBackpressureHeaders() {
+    }
+
+    public static StatusRuntimeException statusRuntimeException(Status status, String description,
+                                                                 HttpHeaders responseHeaders) {
+        Status describedStatus = status.withDescription(description);
+        Metadata trailers = isBackpressureStatus(status) ? retryAfterTrailers(responseHeaders) : null;
+        return trailers == null ? describedStatus.asRuntimeException() : describedStatus.asRuntimeException(trailers);
+    }
+
+    public static String retryAfter(StatusRuntimeException exception) {
+        if (exception == null || !isBackpressureStatus(exception.getStatus())) {
+            return null;
+        }
+        return retryAfter(exception.getTrailers());
+    }
+
+    private static String retryAfter(Metadata trailers) {
+        return trailers == null ? null : legalRetryAfter(trailers.get(RETRY_AFTER_TRAILER_KEY));
+    }
+
+    private static Metadata retryAfterTrailers(HttpHeaders responseHeaders) {
+        String retryAfter = retryAfter(responseHeaders);
+        if (retryAfter == null) {
+            return null;
+        }
+        Metadata trailers = new Metadata();
+        trailers.put(RETRY_AFTER_TRAILER_KEY, retryAfter);
+        return trailers;
+    }
+
+    private static boolean isBackpressureStatus(Status status) {
+        if (status == null) {
+            return false;
+        }
+        Status.Code code = status.getCode();
+        return Status.Code.RESOURCE_EXHAUSTED == code
+                || Status.Code.UNAVAILABLE == code
+                || Status.Code.DEADLINE_EXCEEDED == code;
+    }
+
+    private static String retryAfter(HttpHeaders responseHeaders) {
+        if (responseHeaders == null) {
+            return null;
+        }
+        List<String> values = responseHeaders.get(HttpHeaders.RETRY_AFTER);
+        if (values == null || values.isEmpty()) {
+            return null;
+        }
+        return values.stream()
+                .map(OtlpIngestionBackpressureHeaders::legalRetryAfter)
+                .filter(value -> value != null)
+                .findFirst()
+                .orElse(null);
+    }
+
+    private static String legalRetryAfter(String value) {
+        String trimmed = StringUtils.trimToNull(value);
+        if (trimmed == null) {
+            return null;
+        }
+        if (trimmed.matches("\\d+")) {
+            return cappedRetryAfterSeconds(trimmed);
+        }
+        try {
+            Instant retryAt = ZonedDateTime.parse(trimmed, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant();
+            long retryAfterSeconds = Duration.between(Instant.now(), retryAt).getSeconds();
+            if (retryAfterSeconds <= 0L) {
+                return "0";
+            }
+            return String.valueOf(Math.min(retryAfterSeconds, MAX_RETRY_AFTER_SECONDS));
+        } catch (DateTimeException ignored) {
+            return null;
+        }
+    }
+
+    private static String cappedRetryAfterSeconds(String value) {
+        try {
+            long seconds = Long.parseLong(value);
+            return String.valueOf(Math.min(seconds, MAX_RETRY_AFTER_SECONDS));
+        } catch (NumberFormatException ignored) {
+            return String.valueOf(MAX_RETRY_AFTER_SECONDS);
+        }
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/error/OtlpIngestionErrorResponseFactory.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/error/OtlpIngestionErrorResponseFactory.java
new file mode 100644
index 0000000000..cde1181785
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/error/OtlpIngestionErrorResponseFactory.java
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.error;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+import io.grpc.StatusRuntimeException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+
+/**
+ * Shared OTLP HTTP error response factory for all ingested signals.
+ */
+@Component
+public class OtlpIngestionErrorResponseFactory {
+
+    private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf";
+    private static final MediaType MEDIA_TYPE_PROTOBUF = MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF);
+    private static final MediaType MEDIA_TYPE_PROTOBUF_ALT = MediaType.parseMediaType("application/protobuf");
+    private static final String DEFAULT_ERROR_MESSAGE = "OTLP signal ingestion failed.";
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    public ResponseEntity<byte[]> httpErrorResponse(MediaType contentType, StatusRuntimeException exception) {
+        return httpErrorResponse(contentType, List.of(), toHttpStatus(exception), statusCode(exception),
+                statusDescription(exception), OtlpIngestionBackpressureHeaders.retryAfter(exception));
+    }
+
+    public ResponseEntity<byte[]> httpErrorResponse(MediaType contentType, List<MediaType> acceptTypes,
+                                                    StatusRuntimeException exception) {
+        return httpErrorResponse(contentType, acceptTypes, toHttpStatus(exception), statusCode(exception),
+                statusDescription(exception), OtlpIngestionBackpressureHeaders.retryAfter(exception));
+    }
+
+    public ResponseEntity<byte[]> httpErrorResponse(MediaType contentType, HttpStatus status, String message) {
+        return httpErrorResponse(contentType, status, grpcCode(status), message);
+    }
+
+    public ResponseEntity<byte[]> httpErrorResponse(MediaType contentType, List<MediaType> acceptTypes,
+                                                    HttpStatus status, String message) {
+        return httpErrorResponse(contentType, acceptTypes, status, grpcCode(status), message);
+    }
+
+    private ResponseEntity<byte[]> httpErrorResponse(MediaType contentType, HttpStatus status, int grpcCode,
+                                                     String message) {
+        return httpErrorResponse(contentType, List.of(), status, grpcCode, message, null);
+    }
+
+    private ResponseEntity<byte[]> httpErrorResponse(MediaType contentType, List<MediaType> acceptTypes,
+                                                     HttpStatus status, int grpcCode, String message) {
+        return httpErrorResponse(contentType, acceptTypes, status, grpcCode, message, null);
+    }
+
+    private ResponseEntity<byte[]> httpErrorResponse(MediaType contentType, List<MediaType> acceptTypes,
+                                                     HttpStatus status, int grpcCode, String message,
+                                                     String retryAfter) {
+        if (expectsJson(contentType, acceptTypes)) {
+            return withRetryAfter(ResponseEntity.status(status)
+                    .contentType(MediaType.APPLICATION_JSON), retryAfter)
+                    .body(jsonErrorBody(grpcCode, message).getBytes(StandardCharsets.UTF_8));
+        }
+        return withRetryAfter(ResponseEntity.status(status)
+                .contentType(MEDIA_TYPE_PROTOBUF), retryAfter)
+                .body(binaryErrorBody(grpcCode, message));
+    }
+
+    private ResponseEntity.BodyBuilder withRetryAfter(ResponseEntity.BodyBuilder builder, String retryAfter) {
+        String headerValue = StringUtils.trimToNull(retryAfter);
+        return headerValue == null ? builder : builder.header(HttpHeaders.RETRY_AFTER, headerValue);
+    }
+
+    private boolean expectsJson(MediaType contentType, List<MediaType> acceptTypes) {
+        double jsonQuality = negotiatedQuality(acceptTypes, true);
+        double protobufQuality = negotiatedQuality(acceptTypes, false);
+        if (jsonQuality >= 0 || protobufQuality >= 0) {
+            double acceptableJsonQuality = Math.max(jsonQuality, 0.0d);
+            double acceptableProtobufQuality = Math.max(protobufQuality, 0.0d);
+            if (Double.compare(acceptableJsonQuality, acceptableProtobufQuality) == 0) {
+                return acceptableJsonQuality > 0 && isExplicitJsonMediaType(contentType);
+            }
+            return acceptableJsonQuality > acceptableProtobufQuality;
+        }
+        return isExplicitJsonMediaType(contentType);
+    }
+
+    private double negotiatedQuality(List<MediaType> acceptTypes, boolean json) {
+        if (acceptTypes == null || acceptTypes.isEmpty()) {
+            return -1.0d;
+        }
+        double explicitQuality = acceptTypes.stream()
+                .filter(json ? this::isExplicitJsonMediaType : this::isExplicitProtobufMediaType)
+                .mapToDouble(MediaType::getQualityValue)
+                .max()
+                .orElse(-1.0d);
+        if (explicitQuality >= 0) {
+            return explicitQuality;
+        }
+        return acceptTypes.stream()
+                .filter(json ? this::isJsonWildcardMediaType : this::isProtobufWildcardMediaType)
+                .mapToDouble(MediaType::getQualityValue)
+                .max()
+                .orElse(-1.0d);
+    }
+
+    private boolean isJsonWildcardMediaType(MediaType mediaType) {
+        return mediaType != null
+                && !isExplicitJsonMediaType(mediaType)
+                && mediaType.includes(MediaType.APPLICATION_JSON);
+    }
+
+    private boolean isProtobufWildcardMediaType(MediaType mediaType) {
+        return mediaType != null
+                && !isExplicitProtobufMediaType(mediaType)
+                && (mediaType.includes(MEDIA_TYPE_PROTOBUF) || mediaType.includes(MEDIA_TYPE_PROTOBUF_ALT));
+    }
+
+    private boolean isExplicitJsonMediaType(MediaType mediaType) {
+        if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) {
+            return false;
+        }
+        return "application".equalsIgnoreCase(mediaType.getType())
+                && ("json".equalsIgnoreCase(mediaType.getSubtype())
+                || mediaType.getSubtype().toLowerCase(Locale.ROOT).endsWith("+json"));
+    }
+
+    private boolean isExplicitProtobufMediaType(MediaType mediaType) {
+        if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) {
+            return false;
+        }
+        return "application".equalsIgnoreCase(mediaType.getType())
+                && ("x-protobuf".equalsIgnoreCase(mediaType.getSubtype())
+                || "protobuf".equalsIgnoreCase(mediaType.getSubtype()));
+    }
+
+    public HttpStatus toHttpStatus(StatusRuntimeException exception) {
+        if (exception == null || exception.getStatus() == null) {
+            return HttpStatus.INTERNAL_SERVER_ERROR;
+        }
+        return switch (exception.getStatus().getCode()) {
+            case INVALID_ARGUMENT -> HttpStatus.BAD_REQUEST;
+            case NOT_FOUND -> HttpStatus.NOT_FOUND;
+            case ALREADY_EXISTS, ABORTED -> HttpStatus.CONFLICT;
+            case FAILED_PRECONDITION, OUT_OF_RANGE -> HttpStatus.BAD_REQUEST;
+            case UNAUTHENTICATED -> HttpStatus.UNAUTHORIZED;
+            case PERMISSION_DENIED -> HttpStatus.FORBIDDEN;
+            case RESOURCE_EXHAUSTED -> HttpStatus.TOO_MANY_REQUESTS;
+            case UNIMPLEMENTED -> HttpStatus.NOT_IMPLEMENTED;
+            case UNAVAILABLE -> HttpStatus.SERVICE_UNAVAILABLE;
+            case DEADLINE_EXCEEDED -> HttpStatus.GATEWAY_TIMEOUT;
+            default -> HttpStatus.INTERNAL_SERVER_ERROR;
+        };
+    }
+
+    public String jsonErrorBody(String message) {
+        return jsonErrorBody(io.grpc.Status.Code.UNKNOWN.value(), message);
+    }
+
+    public String jsonErrorBody(int grpcCode, String message) {
+        String errorMessage = defaultErrorMessage(message);
+        try {
+            return JsonFormat.printer().print(statusPayload(grpcCode, errorMessage));
+        } catch (InvalidProtocolBufferException ex) {
+            try {
+                return OBJECT_MAPPER.writeValueAsString(Map.of("code", grpcCode, "message", errorMessage));
+            } catch (Exception ignored) {
+                return "{\"code\":" + grpcCode + ",\"message\":\"" + errorMessage + "\"}";
+            }
+        }
+    }
+
+    public byte[] binaryErrorBody(String message) {
+        return binaryErrorBody(io.grpc.Status.Code.UNKNOWN.value(), message);
+    }
+
+    public byte[] binaryErrorBody(int grpcCode, String message) {
+        return statusPayload(grpcCode, defaultErrorMessage(message)).toByteArray();
+    }
+
+    public String defaultErrorMessage(String message) {
+        return StringUtils.defaultIfBlank(message, DEFAULT_ERROR_MESSAGE);
+    }
+
+    private String statusDescription(StatusRuntimeException exception) {
+        return exception == null || exception.getStatus() == null
+                ? DEFAULT_ERROR_MESSAGE
+                : exception.getStatus().getDescription();
+    }
+
+    private int statusCode(StatusRuntimeException exception) {
+        if (exception == null || exception.getStatus() == null) {
+            return io.grpc.Status.Code.UNKNOWN.value();
+        }
+        return exception.getStatus().getCode().value();
+    }
+
+    private int grpcCode(HttpStatus status) {
+        if (status == null) {
+            return io.grpc.Status.Code.UNKNOWN.value();
+        }
+        return switch (status) {
+            case BAD_REQUEST -> io.grpc.Status.Code.INVALID_ARGUMENT.value();
+            case NOT_ACCEPTABLE -> io.grpc.Status.Code.INVALID_ARGUMENT.value();
+            case UNSUPPORTED_MEDIA_TYPE -> io.grpc.Status.Code.INVALID_ARGUMENT.value();
+            case UNPROCESSABLE_ENTITY -> io.grpc.Status.Code.INVALID_ARGUMENT.value();
+            case NOT_FOUND -> io.grpc.Status.Code.NOT_FOUND.value();
+            case CONFLICT -> io.grpc.Status.Code.ABORTED.value();
+            case LOCKED -> io.grpc.Status.Code.ABORTED.value();
+            case PRECONDITION_FAILED -> io.grpc.Status.Code.FAILED_PRECONDITION.value();
+            case PRECONDITION_REQUIRED -> io.grpc.Status.Code.FAILED_PRECONDITION.value();
+            case REQUESTED_RANGE_NOT_SATISFIABLE -> io.grpc.Status.Code.OUT_OF_RANGE.value();
+            case UNAUTHORIZED -> io.grpc.Status.Code.UNAUTHENTICATED.value();
+            case FORBIDDEN -> io.grpc.Status.Code.PERMISSION_DENIED.value();
+            case REQUEST_TIMEOUT -> io.grpc.Status.Code.DEADLINE_EXCEEDED.value();
+            case METHOD_NOT_ALLOWED -> io.grpc.Status.Code.UNIMPLEMENTED.value();
+            case PAYLOAD_TOO_LARGE -> io.grpc.Status.Code.RESOURCE_EXHAUSTED.value();
+            case REQUEST_HEADER_FIELDS_TOO_LARGE -> io.grpc.Status.Code.RESOURCE_EXHAUSTED.value();
+            case TOO_MANY_REQUESTS -> io.grpc.Status.Code.RESOURCE_EXHAUSTED.value();
+            case TOO_EARLY -> io.grpc.Status.Code.UNAVAILABLE.value();
+            case BAD_GATEWAY -> io.grpc.Status.Code.UNAVAILABLE.value();
+            case SERVICE_UNAVAILABLE -> io.grpc.Status.Code.UNAVAILABLE.value();
+            case GATEWAY_TIMEOUT -> io.grpc.Status.Code.DEADLINE_EXCEEDED.value();
+            case NOT_IMPLEMENTED -> io.grpc.Status.Code.UNIMPLEMENTED.value();
+            case INSUFFICIENT_STORAGE -> io.grpc.Status.Code.RESOURCE_EXHAUSTED.value();
+            case INTERNAL_SERVER_ERROR -> io.grpc.Status.Code.INTERNAL.value();
+            default -> status.is5xxServerError()
+                    ? io.grpc.Status.Code.INTERNAL.value()
+                    : io.grpc.Status.Code.UNKNOWN.value();
+        };
+    }
+
+    private com.google.rpc.Status statusPayload(int grpcCode, String message) {
+        return com.google.rpc.Status.newBuilder()
+                .setCode(Math.max(0, grpcCode))
+                .setMessage(message)
+                .build();
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeApmFlowInitializer.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeApmFlowInitializer.java
index 0439813de4..933aa49ec9 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeApmFlowInitializer.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeApmFlowInitializer.java
@@ -17,14 +17,21 @@
 
 package org.apache.hertzbeat.observability.ingestion.forwarder;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.retry.OtlpIngestionRetryService;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
 import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.event.ApplicationReadyEvent;
 import org.springframework.context.event.EventListener;
@@ -35,7 +42,7 @@ import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
-import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.RestTemplate;
 import org.springframework.web.util.UriUtils;
 
@@ -50,19 +57,32 @@ public class GreptimeApmFlowInitializer {
     public static final String APM_FLOW_RESOURCE = "greptime/flows/hertzbeat_apm_red_1m.sql";
 
     private static final String SQL_PATH = "/v1/sql";
+    private static final String DEFAULT_GREPTIME_DB_NAME = "public";
+    private static final String TRACE_TABLE = "hzb_traces";
+    private static final String RESOURCE_ATTRIBUTES_COLUMN = "resource_attributes";
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
     private final RestTemplate restTemplate;
     private final ObjectProvider<GreptimeProperties> greptimePropertiesProvider;
+    private final OtlpIngestionRetryService retryService;
 
+    GreptimeApmFlowInitializer(RestTemplate restTemplate,
+                               ObjectProvider<GreptimeProperties> greptimePropertiesProvider) {
+        this(restTemplate, greptimePropertiesProvider, new OtlpIngestionRetryService());
+    }
+
+    @Autowired
     public GreptimeApmFlowInitializer(RestTemplate restTemplate,
-                                      ObjectProvider<GreptimeProperties> greptimePropertiesProvider) {
+                                      ObjectProvider<GreptimeProperties> greptimePropertiesProvider,
+                                      OtlpIngestionRetryService retryService) {
         this.restTemplate = restTemplate;
         this.greptimePropertiesProvider = greptimePropertiesProvider;
+        this.retryService = retryService == null ? new OtlpIngestionRetryService() : retryService;
     }
 
     @EventListener(ApplicationReadyEvent.class)
     public void initialize() {
-        GreptimeProperties greptimeProperties = greptimePropertiesProvider.getIfAvailable();
+        GreptimeProperties greptimeProperties = greptimePropertiesOrNull();
         if (greptimeProperties == null || !greptimeProperties.enabled()
                 || StringUtils.isBlank(greptimeProperties.httpEndpoint())) {
             log.debug("[observability greptime-apm-flow] skip initialization because Greptime is disabled.");
@@ -70,27 +90,64 @@ public class GreptimeApmFlowInitializer {
         }
         try {
             List<String> statements = readFlowStatements();
+            Set<String> traceTableColumns = null;
             for (String statement : statements) {
-                executeStatement(greptimeProperties, statement);
+                try {
+                    if (!executeStatement(greptimeProperties, statement)) {
+                        return;
+                    }
+                } catch (HttpClientErrorException.BadRequest ex) {
+                    if (!isTraceResourceAttributeSchemaFailure(statement, ex)) {
+                        throw ex;
+                    }
+                    if (traceTableColumns == null) {
+                        traceTableColumns = traceTableColumns(greptimeProperties);
+                    }
+                    String adaptedStatement = adaptTraceResourceAttributeExpressions(statement, traceTableColumns);
+                    if (statement.equals(adaptedStatement)) {
+                        throw ex;
+                    }
+                    if (!executeStatement(greptimeProperties, adaptedStatement)) {
+                        return;
+                    }
+                }
             }
             log.info("[observability greptime-apm-flow] initialized Greptime APM RED Flow.");
-        } catch (IOException | RestClientException ex) {
+        } catch (IOException | RuntimeException ex) {
             log.warn("[observability greptime-apm-flow] failed to initialize Greptime APM RED Flow: {}",
                     ex.getMessage(), ex);
         }
     }
 
-    private void executeStatement(GreptimeProperties greptimeProperties, String statement) {
-        ResponseEntity<String> response = restTemplate.exchange(
-                endpoint(greptimeProperties),
-                HttpMethod.POST,
-                sqlRequest(greptimeProperties, statement),
-                String.class
-        );
+    private GreptimeProperties greptimePropertiesOrNull() {
+        try {
+            return greptimePropertiesProvider.getIfAvailable();
+        } catch (RuntimeException ex) {
+            log.warn("[observability greptime-apm-flow] failed to resolve Greptime properties: {}",
+                    ex.getMessage(), ex);
+            return null;
+        }
+    }
+
+    private boolean executeStatement(GreptimeProperties greptimeProperties, String statement) {
+        ResponseEntity<String> response = retryService.execute(() -> restTemplate.exchange(
+                        endpoint(greptimeProperties),
+                        HttpMethod.POST,
+                        sqlRequest(greptimeProperties, statement),
+                        String.class
+                ),
+                responseEntity -> responseEntity == null
+                        || retryService.isRetryableStatus(responseEntity.getStatusCode()));
+        if (response == null) {
+            log.warn("[observability greptime-apm-flow] Greptime SQL statement returned no response.");
+            return true;
+        }
         if (!response.getStatusCode().is2xxSuccessful()) {
             log.warn("[observability greptime-apm-flow] Greptime SQL statement returned status {}.",
                     response.getStatusCode());
+            return false;
         }
+        return true;
     }
 
     private HttpEntity<String> sqlRequest(GreptimeProperties greptimeProperties, String sql) {
@@ -110,18 +167,99 @@ public class GreptimeApmFlowInitializer {
                 .toList();
     }
 
+    private boolean isTraceResourceAttributeSchemaFailure(String statement, HttpClientErrorException ex) {
+        if (!StringUtils.containsIgnoreCase(statement, "CREATE FLOW IF NOT EXISTS hertzbeat_apm_red_1m_flow")) {
+            return false;
+        }
+        String responseBody = ex.getResponseBodyAsString(StandardCharsets.UTF_8);
+        return StringUtils.containsIgnoreCase(ex.getMessage(), "resource_attributes")
+                || StringUtils.containsIgnoreCase(responseBody, "resource_attributes");
+    }
+
+    private Set<String> traceTableColumns(GreptimeProperties greptimeProperties) {
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(
+                    endpoint(greptimeProperties),
+                    HttpMethod.POST,
+                    sqlRequest(greptimeProperties, "DESC " + TRACE_TABLE),
+                    String.class);
+            if (response == null || !response.getStatusCode().is2xxSuccessful()
+                    || StringUtils.isBlank(response.getBody())) {
+                return Collections.singleton(RESOURCE_ATTRIBUTES_COLUMN);
+            }
+            return parseTraceTableColumns(response.getBody());
+        } catch (RuntimeException | IOException ex) {
+            log.debug("[observability greptime-apm-flow] failed to inspect Greptime trace schema: {}",
+                    ex.getMessage(), ex);
+            return Collections.singleton(RESOURCE_ATTRIBUTES_COLUMN);
+        }
+    }
+
+    private Set<String> parseTraceTableColumns(String responseBody) throws IOException {
+        Set<String> columns = new LinkedHashSet<>();
+        JsonNode root = OBJECT_MAPPER.readTree(responseBody);
+        for (JsonNode output : root.path("output")) {
+            JsonNode rows = output.path("records").path("rows");
+            if (!rows.isArray()) {
+                continue;
+            }
+            for (JsonNode row : rows) {
+                if (row.isArray() && !row.isEmpty() && StringUtils.isNotBlank(row.get(0).asText())) {
+                    columns.add(row.get(0).asText());
+                }
+            }
+        }
+        if (columns.isEmpty()) {
+            return Collections.singleton(RESOURCE_ATTRIBUTES_COLUMN);
+        }
+        return Collections.unmodifiableSet(columns);
+    }
+
+    private String adaptTraceResourceAttributeExpressions(String statement, Set<String> traceTableColumns) {
+        String adaptedStatement = statement;
+        for (String key : List.of(
+                "hertzbeat.workspace_id",
+                "hertzbeat.entity_id",
+                "deployment.environment.name",
+                "service.namespace")) {
+            adaptedStatement = adaptedStatement.replace(
+                    "json_get_string(resource_attributes, '$[\"" + key + "\"]')",
+                    resourceAttributeExpression(traceTableColumns, key));
+        }
+        return adaptedStatement;
+    }
+
+    private String resourceAttributeExpression(Set<String> traceTableColumns, String key) {
+        String flattenedColumn = RESOURCE_ATTRIBUTES_COLUMN + "." + key;
+        if (traceTableColumns.contains(flattenedColumn)) {
+            return quoteIdentifier(flattenedColumn);
+        }
+        if (traceTableColumns.contains(RESOURCE_ATTRIBUTES_COLUMN)) {
+            return "json_get_string(resource_attributes, '$[\"" + key.replace("\"", "\\\"") + "\"]')";
+        }
+        return "NULL";
+    }
+
+    private String quoteIdentifier(String column) {
+        return "\"" + column.replace("\"", "\"\"") + "\"";
+    }
+
     private void addAuthenticationHeader(HttpHeaders headers, GreptimeProperties greptimeProperties) {
-        if (StringUtils.isBlank(greptimeProperties.username()) || StringUtils.isBlank(greptimeProperties.password())) {
+        String username = StringUtils.trimToNull(greptimeProperties.username());
+        String password = StringUtils.trimToNull(greptimeProperties.password());
+        if (username == null || password == null) {
             return;
         }
-        headers.setBasicAuth(greptimeProperties.username(), greptimeProperties.password(), StandardCharsets.UTF_8);
+        headers.setBasicAuth(username, password, StandardCharsets.UTF_8);
     }
 
     private String endpoint(GreptimeProperties greptimeProperties) {
-        String endpoint = StringUtils.removeEnd(greptimeProperties.httpEndpoint(), "/") + SQL_PATH;
-        if (StringUtils.isNotBlank(greptimeProperties.database())) {
-            endpoint += "?db=" + UriUtils.encodeQueryParam(greptimeProperties.database(), StandardCharsets.UTF_8);
-        }
-        return endpoint;
+        String endpoint = StringUtils.stripEnd(StringUtils.trim(greptimeProperties.httpEndpoint()), "/") + SQL_PATH;
+        return endpoint + "?db=" + UriUtils.encodeQueryParam(database(greptimeProperties.database()),
+                StandardCharsets.UTF_8);
+    }
+
+    private String database(String configuredDatabase) {
+        return StringUtils.defaultIfBlank(StringUtils.trim(configuredDatabase), DEFAULT_GREPTIME_DB_NAME);
     }
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeLogPipelineInitializer.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeLogPipelineInitializer.java
index b12990b46b..f44340a149 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeLogPipelineInitializer.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeLogPipelineInitializer.java
@@ -23,8 +23,10 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.retry.OtlpIngestionRetryService;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
 import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.event.ApplicationReadyEvent;
 import org.springframework.context.event.EventListener;
@@ -58,16 +60,25 @@ public class GreptimeLogPipelineInitializer {
 
     private final RestTemplate restTemplate;
     private final ObjectProvider<GreptimeProperties> greptimePropertiesProvider;
+    private final OtlpIngestionRetryService retryService;
 
     public GreptimeLogPipelineInitializer(RestTemplate restTemplate,
                                           ObjectProvider<GreptimeProperties> greptimePropertiesProvider) {
+        this(restTemplate, greptimePropertiesProvider, new OtlpIngestionRetryService());
+    }
+
+    @Autowired
+    public GreptimeLogPipelineInitializer(RestTemplate restTemplate,
+                                          ObjectProvider<GreptimeProperties> greptimePropertiesProvider,
+                                          OtlpIngestionRetryService retryService) {
         this.restTemplate = restTemplate;
         this.greptimePropertiesProvider = greptimePropertiesProvider;
+        this.retryService = retryService == null ? new OtlpIngestionRetryService() : retryService;
     }
 
     @EventListener(ApplicationReadyEvent.class)
     public void initialize() {
-        GreptimeProperties greptimeProperties = greptimePropertiesProvider.getIfAvailable();
+        GreptimeProperties greptimeProperties = greptimePropertiesOrNull();
         if (greptimeProperties == null || !greptimeProperties.enabled()
                 || StringUtils.isBlank(greptimeProperties.httpEndpoint())) {
             log.debug("[observability greptime-log] skip pipeline upload because Greptime is disabled.");
@@ -80,12 +91,19 @@ public class GreptimeLogPipelineInitializer {
                         GreptimeOtlpForwarder.LOG_PIPELINE_NAME);
                 return;
             }
-            ResponseEntity<String> response = restTemplate.exchange(
-                    endpoint(greptimeProperties.httpEndpoint()),
-                    HttpMethod.POST,
-                    pipelineUploadRequest(greptimeProperties, pipeline),
-                    String.class
-            );
+            ResponseEntity<String> response = retryService.execute(() -> restTemplate.exchange(
+                            endpoint(greptimeProperties.httpEndpoint()),
+                            HttpMethod.POST,
+                            pipelineUploadRequest(greptimeProperties, pipeline),
+                            String.class
+                    ),
+                    retryableResponse -> retryableResponse == null
+                            || retryService.isRetryableStatus(retryableResponse.getStatusCode()));
+            if (response == null) {
+                log.warn("[observability greptime-log] Greptime returned no response while uploading log pipeline {}.",
+                        GreptimeOtlpForwarder.LOG_PIPELINE_NAME);
+                return;
+            }
             if (response.getStatusCode().is2xxSuccessful()) {
                 log.info("[observability greptime-log] uploaded Greptime log pipeline {}.",
                         GreptimeOtlpForwarder.LOG_PIPELINE_NAME);
@@ -93,20 +111,34 @@ public class GreptimeLogPipelineInitializer {
             }
             log.warn("[observability greptime-log] failed to upload Greptime log pipeline {}, status {}.",
                     GreptimeOtlpForwarder.LOG_PIPELINE_NAME, response.getStatusCode());
-        } catch (IOException | RestClientException ex) {
+        } catch (IOException | RuntimeException ex) {
             log.warn("[observability greptime-log] failed to upload Greptime log pipeline {}: {}",
                     GreptimeOtlpForwarder.LOG_PIPELINE_NAME, ex.getMessage(), ex);
         }
     }
 
+    private GreptimeProperties greptimePropertiesOrNull() {
+        try {
+            return greptimePropertiesProvider.getIfAvailable();
+        } catch (RuntimeException ex) {
+            log.warn("[observability greptime-log] failed to resolve Greptime properties: {}", ex.getMessage(), ex);
+            return null;
+        }
+    }
+
     private boolean latestPipelineMatches(GreptimeProperties greptimeProperties, String bundledPipeline) {
         try {
-            ResponseEntity<String> response = restTemplate.exchange(
-                    endpoint(greptimeProperties.httpEndpoint()),
-                    HttpMethod.GET,
-                    new HttpEntity<Void>(headers(greptimeProperties)),
-                    String.class
-            );
+            ResponseEntity<String> response = retryService.execute(() -> restTemplate.exchange(
+                            endpoint(greptimeProperties.httpEndpoint()),
+                            HttpMethod.GET,
+                            new HttpEntity<Void>(headers(greptimeProperties)),
+                            String.class
+                    ),
+                    retryableResponse -> retryableResponse == null
+                            || retryService.isRetryableStatus(retryableResponse.getStatusCode()));
+            if (response == null) {
+                return false;
+            }
             if (!response.getStatusCode().is2xxSuccessful() || StringUtils.isBlank(response.getBody())) {
                 return false;
             }
@@ -166,13 +198,15 @@ public class GreptimeLogPipelineInitializer {
     }
 
     private void addAuthenticationHeader(HttpHeaders headers, GreptimeProperties greptimeProperties) {
-        if (StringUtils.isBlank(greptimeProperties.username()) || StringUtils.isBlank(greptimeProperties.password())) {
+        String username = StringUtils.trimToNull(greptimeProperties.username());
+        String password = StringUtils.trimToNull(greptimeProperties.password());
+        if (username == null || password == null) {
             return;
         }
-        headers.setBasicAuth(greptimeProperties.username(), greptimeProperties.password(), StandardCharsets.UTF_8);
+        headers.setBasicAuth(username, password, StandardCharsets.UTF_8);
     }
 
     private String endpoint(String baseEndpoint) {
-        return StringUtils.removeEnd(baseEndpoint, "/") + LOG_PIPELINE_PATH;
+        return StringUtils.stripEnd(StringUtils.trim(baseEndpoint), "/") + LOG_PIPELINE_PATH;
     }
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeOtlpForwarder.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeOtlpForwarder.java
index 1cf87ec385..3a555260f4 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeOtlpForwarder.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeOtlpForwarder.java
@@ -22,7 +22,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.util.JsonFormat;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
 import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import io.opentelemetry.proto.common.v1.AnyValue;
 import io.opentelemetry.proto.common.v1.KeyValue;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -34,15 +37,22 @@ import java.util.Set;
 import java.util.zip.GZIPInputStream;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.error.OtlpIngestionBackpressureHeaders;
+import org.apache.hertzbeat.observability.ingestion.redaction.OtlpIngestionRedactionService;
+import org.apache.hertzbeat.observability.ingestion.retry.OtlpIngestionRetryService;
 import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpStatusCodeException;
 import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestTemplate;
 
@@ -69,11 +79,24 @@ public class GreptimeOtlpForwarder {
 
     private final RestTemplate restTemplate;
     private final ObjectProvider<GreptimeProperties> greptimePropertiesProvider;
+    private final OtlpIngestionRedactionService redactionService;
+    private final OtlpIngestionRetryService retryService;
 
+    @Autowired
     public GreptimeOtlpForwarder(RestTemplate restTemplate,
-                                 ObjectProvider<GreptimeProperties> greptimePropertiesProvider) {
+                                 ObjectProvider<GreptimeProperties> greptimePropertiesProvider,
+                                 OtlpIngestionRedactionService redactionService,
+                                 OtlpIngestionRetryService retryService) {
         this.restTemplate = restTemplate;
         this.greptimePropertiesProvider = greptimePropertiesProvider;
+        this.redactionService = redactionService;
+        this.retryService = retryService;
+    }
+
+    public GreptimeOtlpForwarder(RestTemplate restTemplate,
+                                 ObjectProvider<GreptimeProperties> greptimePropertiesProvider) {
+        this(restTemplate, greptimePropertiesProvider, new OtlpIngestionRedactionService(),
+                new OtlpIngestionRetryService());
     }
 
     public ResponseEntity<byte[]> forwardLogsHttp(byte[] content, HttpHeaders requestHeaders) {
@@ -83,7 +106,7 @@ public class GreptimeOtlpForwarder {
     }
 
     public ResponseEntity<byte[]> forwardLogsProtobuf(byte[] protobufContent) {
-        return post(LOGS_PATH, sanitizeLogsForGreptimeNative(protobufContent), logHeaders());
+        return post(LOGS_PATH, sanitizeLogsForGreptimeNative(safeContent(protobufContent)), logHeaders());
     }
 
     public byte[] forwardLogsGrpc(ExportLogsServiceRequest request) {
@@ -96,7 +119,7 @@ public class GreptimeOtlpForwarder {
     }
 
     private ResponseEntity<byte[]> post(String path, byte[] content, HttpHeaders headers) {
-        GreptimeProperties greptimeProperties = greptimePropertiesProvider.getIfAvailable();
+        GreptimeProperties greptimeProperties = greptimePropertiesOrUnavailable();
         if (greptimeProperties == null || !greptimeProperties.enabled()
                 || StringUtils.isBlank(greptimeProperties.httpEndpoint())) {
             throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend is not configured.")
@@ -105,17 +128,24 @@ public class GreptimeOtlpForwarder {
         addGreptimeCommonHeaders(headers, greptimeProperties);
         addAuthenticationHeader(headers, greptimeProperties);
         try {
-            ResponseEntity<byte[]> response = restTemplate.exchange(
+            ResponseEntity<byte[]> response = retryService.execute(() -> restTemplate.exchange(
                     endpoint(greptimeProperties.httpEndpoint(), path),
                     HttpMethod.POST,
                     new HttpEntity<>(content, headers),
                     byte[].class
-            );
-            if (!response.getStatusCode().is2xxSuccessful()) {
-                throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend returned " + response.getStatusCode())
+            ), retryableResponse -> retryableResponse == null
+                    || retryService.isRetryableStatus(retryableResponse.getStatusCode()));
+            if (response == null) {
+                throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend returned no response.")
                         .asRuntimeException();
             }
+            if (!response.getStatusCode().is2xxSuccessful()) {
+                throw backendStatusException(response.getStatusCode(), response.getHeaders());
+            }
             return response;
+        } catch (HttpStatusCodeException ex) {
+            log.error("Failed to forward OTLP payload to Greptime path {}: {}", path, ex.getMessage(), ex);
+            throw backendStatusException(ex.getStatusCode(), ex.getResponseHeaders());
         } catch (RestClientException ex) {
             log.error("Failed to forward OTLP payload to Greptime path {}: {}", path, ex.getMessage(), ex);
             throw io.grpc.Status.UNAVAILABLE
@@ -125,6 +155,17 @@ public class GreptimeOtlpForwarder {
         }
     }
 
+    private GreptimeProperties greptimePropertiesOrUnavailable() {
+        try {
+            return greptimePropertiesProvider.getIfAvailable();
+        } catch (RuntimeException ex) {
+            log.warn("Failed to resolve Greptime OTLP log backend properties: {}", ex.toString());
+            throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend is not configured.")
+                    .withCause(ex)
+                    .asRuntimeException();
+        }
+    }
+
     private HttpHeaders logHeaders() {
         HttpHeaders headers = new HttpHeaders();
         headers.setContentType(MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF));
@@ -135,21 +176,110 @@ public class GreptimeOtlpForwarder {
     }
 
     private void addGreptimeCommonHeaders(HttpHeaders headers, GreptimeProperties greptimeProperties) {
-        headers.set(GREPTIME_DB_NAME_HEADER,
-                StringUtils.defaultIfBlank(greptimeProperties.database(), DEFAULT_GREPTIME_DB_NAME));
+        headers.set(GREPTIME_DB_NAME_HEADER, database(greptimeProperties.database()));
     }
 
     private void addAuthenticationHeader(HttpHeaders headers, GreptimeProperties greptimeProperties) {
-        if (StringUtils.isBlank(greptimeProperties.username()) || StringUtils.isBlank(greptimeProperties.password())) {
+        String username = StringUtils.trimToNull(greptimeProperties.username());
+        String password = StringUtils.trimToNull(greptimeProperties.password());
+        if (username == null || password == null) {
             return;
         }
-        String credentials = greptimeProperties.username() + ":" + greptimeProperties.password();
+        String credentials = username + ":" + password;
         String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
         headers.set(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials);
     }
 
+    private StatusRuntimeException backendStatusException(HttpStatusCode statusCode) {
+        return backendStatusException(statusCode, null);
+    }
+
+    private StatusRuntimeException backendStatusException(HttpStatusCode statusCode, HttpHeaders responseHeaders) {
+        Status status = backendGrpcStatus(statusCode);
+        return OtlpIngestionBackpressureHeaders.statusRuntimeException(
+                status, "OTLP backend returned " + statusCode, responseHeaders);
+    }
+
+    private Status backendGrpcStatus(HttpStatusCode statusCode) {
+        if (statusCode == null) {
+            return Status.UNAVAILABLE;
+        }
+        if (statusCode.value() == HttpStatus.UNAUTHORIZED.value()) {
+            return Status.UNAUTHENTICATED;
+        }
+        if (statusCode.value() == HttpStatus.FORBIDDEN.value()) {
+            return Status.PERMISSION_DENIED;
+        }
+        if (statusCode.value() == HttpStatus.BAD_REQUEST.value()) {
+            return Status.INVALID_ARGUMENT;
+        }
+        if (statusCode.value() == HttpStatus.NOT_ACCEPTABLE.value()) {
+            return Status.INVALID_ARGUMENT;
+        }
+        if (statusCode.value() == HttpStatus.UNSUPPORTED_MEDIA_TYPE.value()) {
+            return Status.INVALID_ARGUMENT;
+        }
+        if (statusCode.value() == HttpStatus.UNPROCESSABLE_ENTITY.value()) {
+            return Status.INVALID_ARGUMENT;
+        }
+        if (statusCode.value() == HttpStatus.NOT_FOUND.value()) {
+            return Status.NOT_FOUND;
+        }
+        if (statusCode.value() == HttpStatus.CONFLICT.value()) {
+            return Status.ABORTED;
+        }
+        if (statusCode.value() == HttpStatus.LOCKED.value()) {
+            return Status.ABORTED;
+        }
+        if (statusCode.value() == HttpStatus.PRECONDITION_FAILED.value()) {
+            return Status.FAILED_PRECONDITION;
+        }
+        if (statusCode.value() == HttpStatus.PRECONDITION_REQUIRED.value()) {
+            return Status.FAILED_PRECONDITION;
+        }
+        if (statusCode.value() == HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value()) {
+            return Status.OUT_OF_RANGE;
+        }
+        if (statusCode.value() == HttpStatus.METHOD_NOT_ALLOWED.value()) {
+            return Status.UNIMPLEMENTED;
+        }
+        if (statusCode.value() == HttpStatus.NOT_IMPLEMENTED.value()) {
+            return Status.UNIMPLEMENTED;
+        }
+        if (statusCode.value() == HttpStatus.PAYLOAD_TOO_LARGE.value()) {
+            return Status.RESOURCE_EXHAUSTED;
+        }
+        if (statusCode.value() == HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE.value()) {
+            return Status.RESOURCE_EXHAUSTED;
+        }
+        if (statusCode.value() == HttpStatus.INSUFFICIENT_STORAGE.value()) {
+            return Status.RESOURCE_EXHAUSTED;
+        }
+        if (statusCode.value() == HttpStatus.TOO_MANY_REQUESTS.value()) {
+            return Status.RESOURCE_EXHAUSTED;
+        }
+        if (statusCode.value() == HttpStatus.TOO_EARLY.value()) {
+            return Status.UNAVAILABLE;
+        }
+        if (statusCode.value() == HttpStatus.REQUEST_TIMEOUT.value()
+                || statusCode.value() == HttpStatus.GATEWAY_TIMEOUT.value()) {
+            return Status.DEADLINE_EXCEEDED;
+        }
+        if (statusCode.value() == HttpStatus.BAD_GATEWAY.value()
+                || statusCode.value() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
+            return Status.UNAVAILABLE;
+        }
+        if (statusCode.is4xxClientError()) {
+            return Status.INTERNAL;
+        }
+        if (statusCode.is5xxServerError()) {
+            return Status.INTERNAL;
+        }
+        return Status.UNAVAILABLE;
+    }
+
     private byte[] normalizeLogsRequestBody(byte[] content, MediaType contentType, HttpHeaders requestHeaders) {
-        byte[] normalizedContent = maybeDecompress(content, requestHeaders);
+        byte[] normalizedContent = maybeDecompress(safeContent(content), requestHeaders);
         if (contentType == null || !MediaType.APPLICATION_JSON.includes(contentType)) {
             return normalizedContent;
         }
@@ -186,6 +316,7 @@ public class GreptimeOtlpForwarder {
                 for (int logIndex = 0; logIndex < scopeLogsBuilder.getLogRecordsCount(); logIndex++) {
                     var logRecordBuilder = scopeLogsBuilder.getLogRecordsBuilder(logIndex);
                     changed |= sanitizeKeyValueAttributes(logRecordBuilder.getAttributesBuilderList());
+                    changed |= redactLogBody(logRecordBuilder.getBodyBuilder());
                 }
             }
         }
@@ -199,16 +330,66 @@ public class GreptimeOtlpForwarder {
                 attributeBuilder.setKey(SAFE_TIMESTAMP_ATTRIBUTE);
                 changed = true;
             }
+            if (redactionService.isSensitiveKey(attributeBuilder.getKey())) {
+                attributeBuilder.setValue(redactedAnyValue());
+                changed = true;
+                continue;
+            }
+            changed |= redactAnyValue(attributeBuilder.getValueBuilder());
         }
         return changed;
     }
 
+    private boolean redactLogBody(AnyValue.Builder bodyBuilder) {
+        if (bodyBuilder == null) {
+            return false;
+        }
+        return redactAnyValue(bodyBuilder);
+    }
+
+    private boolean redactAnyValue(AnyValue.Builder valueBuilder) {
+        if (valueBuilder == null) {
+            return false;
+        }
+        if (valueBuilder.getValueCase() == AnyValue.ValueCase.STRING_VALUE) {
+            String original = valueBuilder.getStringValue();
+            String redacted = redactionService.redactText(original);
+            if (!StringUtils.equals(original, redacted)) {
+                valueBuilder.setStringValue(redacted);
+                return true;
+            }
+        }
+        if (valueBuilder.getValueCase() == AnyValue.ValueCase.KVLIST_VALUE) {
+            boolean changed = false;
+            for (KeyValue.Builder nested : valueBuilder.getKvlistValueBuilder().getValuesBuilderList()) {
+                if (redactionService.isSensitiveKey(nested.getKey())) {
+                    nested.setValue(redactedAnyValue());
+                    changed = true;
+                    continue;
+                }
+                changed |= redactAnyValue(nested.getValueBuilder());
+            }
+            return changed;
+        }
+        if (valueBuilder.getValueCase() == AnyValue.ValueCase.ARRAY_VALUE) {
+            boolean changed = false;
+            for (AnyValue.Builder item : valueBuilder.getArrayValueBuilder().getValuesBuilderList()) {
+                changed |= redactAnyValue(item);
+            }
+            return changed;
+        }
+        return false;
+    }
+
+    private AnyValue redactedAnyValue() {
+        return AnyValue.newBuilder().setStringValue(OtlpIngestionRedactionService.REDACTED).build();
+    }
+
     private byte[] maybeDecompress(byte[] content, HttpHeaders headers) {
         if (content == null || content.length == 0 || headers == null) {
             return content;
         }
-        String contentEncoding = headers.getFirst(HttpHeaders.CONTENT_ENCODING);
-        if (!StringUtils.equalsIgnoreCase(contentEncoding, CONTENT_ENCODING_GZIP)) {
+        if (!isGzipEncoded(headers)) {
             return content;
         }
         try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
@@ -222,6 +403,28 @@ public class GreptimeOtlpForwarder {
         }
     }
 
+    private boolean isGzipContentEncoding(String contentEncoding) {
+        String[] encodings = StringUtils.split(contentEncoding, ',');
+        if (encodings == null || encodings.length == 0) {
+            return false;
+        }
+        for (String encoding : encodings) {
+            if (StringUtils.equalsIgnoreCase(StringUtils.trimToEmpty(encoding), CONTENT_ENCODING_GZIP)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isGzipEncoded(HttpHeaders headers) {
+        List<String> contentEncodings = headers == null ? null : headers.get(HttpHeaders.CONTENT_ENCODING);
+        return contentEncodings != null && contentEncodings.stream().anyMatch(this::isGzipContentEncoding);
+    }
+
+    private byte[] safeContent(byte[] content) {
+        return content == null ? new byte[0] : content;
+    }
+
     private String normalizeOtlpJson(String content) throws InvalidProtocolBufferException {
         try {
             JsonNode root = OBJECT_MAPPER.readTree(content);
@@ -273,6 +476,10 @@ public class GreptimeOtlpForwarder {
     }
 
     private String endpoint(String baseEndpoint, String path) {
-        return StringUtils.removeEnd(baseEndpoint, "/") + path;
+        return StringUtils.stripEnd(StringUtils.trim(baseEndpoint), "/") + path;
+    }
+
+    private String database(String configuredDatabase) {
+        return StringUtils.defaultIfBlank(StringUtils.trim(configuredDatabase), DEFAULT_GREPTIME_DB_NAME);
     }
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeTraceTableInitializer.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeTraceTableInitializer.java
new file mode 100644
index 0000000000..61104c98fc
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/forwarder/GreptimeTraceTableInitializer.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.forwarder;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.retry.OtlpIngestionRetryService;
+import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriUtils;
+
+/**
+ * Creates the optimized Greptime native trace table before APM Flow startup.
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "warehouse.store.greptime", name = "enabled", havingValue = "true")
+public class GreptimeTraceTableInitializer {
+
+    public static final String TRACE_TABLE_RESOURCE = "greptime/tables/hzb_traces.sql";
+
+    private static final String SQL_PATH = "/v1/sql";
+    private static final String DEFAULT_GREPTIME_DB_NAME = "public";
+
+    private final RestTemplate restTemplate;
+    private final ObjectProvider<GreptimeProperties> greptimePropertiesProvider;
+    private final OtlpIngestionRetryService retryService;
+
+    GreptimeTraceTableInitializer(RestTemplate restTemplate,
+                                  ObjectProvider<GreptimeProperties> greptimePropertiesProvider) {
+        this(restTemplate, greptimePropertiesProvider, new OtlpIngestionRetryService());
+    }
+
+    @Autowired
+    public GreptimeTraceTableInitializer(RestTemplate restTemplate,
+                                         ObjectProvider<GreptimeProperties> greptimePropertiesProvider,
+                                         OtlpIngestionRetryService retryService) {
+        this.restTemplate = restTemplate;
+        this.greptimePropertiesProvider = greptimePropertiesProvider;
+        this.retryService = retryService == null ? new OtlpIngestionRetryService() : retryService;
+    }
+
+    @Order(Ordered.HIGHEST_PRECEDENCE)
+    @EventListener(ApplicationReadyEvent.class)
+    public void initialize() {
+        GreptimeProperties greptimeProperties = greptimePropertiesOrNull();
+        if (greptimeProperties == null || !greptimeProperties.enabled()
+                || StringUtils.isBlank(greptimeProperties.httpEndpoint())) {
+            log.debug("[observability greptime-trace-table] skip initialization because Greptime is disabled.");
+            return;
+        }
+        try {
+            executeStatement(greptimeProperties, readTraceTableStatement());
+            log.info("[observability greptime-trace-table] initialized Greptime native trace table.");
+        } catch (IOException | RuntimeException ex) {
+            log.warn("[observability greptime-trace-table] failed to initialize Greptime native trace table: {}",
+                    ex.getMessage(), ex);
+        }
+    }
+
+    private GreptimeProperties greptimePropertiesOrNull() {
+        try {
+            return greptimePropertiesProvider.getIfAvailable();
+        } catch (RuntimeException ex) {
+            log.warn("[observability greptime-trace-table] failed to resolve Greptime properties: {}",
+                    ex.getMessage(), ex);
+            return null;
+        }
+    }
+
+    private void executeStatement(GreptimeProperties greptimeProperties, String statement) {
+        ResponseEntity<String> response = retryService.execute(() -> restTemplate.exchange(
+                        endpoint(greptimeProperties),
+                        HttpMethod.POST,
+                        sqlRequest(greptimeProperties, statement),
+                        String.class
+                ),
+                responseEntity -> responseEntity == null
+                        || retryService.isRetryableStatus(responseEntity.getStatusCode()));
+        if (response == null) {
+            log.warn("[observability greptime-trace-table] Greptime SQL statement returned no response.");
+            return;
+        }
+        if (!response.getStatusCode().is2xxSuccessful()) {
+            log.warn("[observability greptime-trace-table] Greptime SQL statement returned status {}.",
+                    response.getStatusCode());
+        }
+    }
+
+    private HttpEntity<String> sqlRequest(GreptimeProperties greptimeProperties, String sql) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        headers.setAccept(List.of(MediaType.APPLICATION_JSON));
+        addAuthenticationHeader(headers, greptimeProperties);
+        return new HttpEntity<>("sql=" + UriUtils.encodeQueryParam(sql, StandardCharsets.UTF_8), headers);
+    }
+
+    private String readTraceTableStatement() throws IOException {
+        ClassPathResource resource = new ClassPathResource(TRACE_TABLE_RESOURCE);
+        return StringUtils.removeEnd(resource.getContentAsString(StandardCharsets.UTF_8).strip(), ";").strip();
+    }
+
+    private void addAuthenticationHeader(HttpHeaders headers, GreptimeProperties greptimeProperties) {
+        String username = StringUtils.trimToNull(greptimeProperties.username());
+        String password = StringUtils.trimToNull(greptimeProperties.password());
+        if (username == null || password == null) {
+            return;
+        }
+        headers.setBasicAuth(username, password, StandardCharsets.UTF_8);
+    }
+
+    private String endpoint(GreptimeProperties greptimeProperties) {
+        String endpoint = StringUtils.stripEnd(StringUtils.trim(greptimeProperties.httpEndpoint()), "/") + SQL_PATH;
+        return endpoint + "?db=" + UriUtils.encodeQueryParam(database(greptimeProperties.database()),
+                StandardCharsets.UTF_8);
+    }
+
+    private String database(String configuredDatabase) {
+        return StringUtils.defaultIfBlank(StringUtils.trim(configuredDatabase), DEFAULT_GREPTIME_DB_NAME);
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/governance/OtlpIngestionGovernanceService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/governance/OtlpIngestionGovernanceService.java
new file mode 100644
index 0000000000..41cdb396d9
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/governance/OtlpIngestionGovernanceService.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.governance;
+
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
+import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
+import io.opentelemetry.proto.common.v1.AnyValue;
+import io.opentelemetry.proto.common.v1.KeyValue;
+import io.opentelemetry.proto.logs.v1.ResourceLogs;
+import io.opentelemetry.proto.metrics.v1.ResourceMetrics;
+import io.opentelemetry.proto.resource.v1.Resource;
+import io.opentelemetry.proto.trace.v1.ResourceSpans;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.observability.ingestion.semantic.OtlpResourceSemanticAttributes;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * Shared OTLP ingest governance policy boundary.
+ */
+@Service
+public class OtlpIngestionGovernanceService {
+
+    private static final String SERVICE_NAME = OtlpResourceSemanticAttributes.SERVICE_NAME;
+
+    private final Set<String> droppedServiceNames;
+
+    @Autowired
+    public OtlpIngestionGovernanceService(
+            @Value("${hertzbeat.otlp.ingestion.drop-service-names:}") String dropServiceNames) {
+        this.droppedServiceNames = parseValues(dropServiceNames);
+    }
+
+    public Decision evaluateMetrics(String protocol, ExportMetricsServiceRequest request) {
+        if (request == null) {
+            return Decision.allow();
+        }
+        for (ResourceMetrics resourceMetrics : request.getResourceMetricsList()) {
+            Decision decision = evaluateServiceName("metrics", protocol, resourceMetrics.getResource());
+            if (decision.dropped()) {
+                return decision;
+            }
+        }
+        return Decision.allow();
+    }
+
+    public Decision evaluateLogs(String protocol, ExportLogsServiceRequest request) {
+        if (request == null) {
+            return Decision.allow();
+        }
+        for (ResourceLogs resourceLogs : request.getResourceLogsList()) {
+            Decision decision = evaluateServiceName("logs", protocol, resourceLogs.getResource());
+            if (decision.dropped()) {
+                return decision;
+            }
+        }
+        return Decision.allow();
+    }
+
+    public Decision evaluateTraces(String protocol, ExportTraceServiceRequest request) {
+        if (request == null) {
+            return Decision.allow();
+        }
+        for (ResourceSpans resourceSpans : request.getResourceSpansList()) {
+            Decision decision = evaluateServiceName("traces", protocol, resourceSpans.getResource());
+            if (decision.dropped()) {
+                return decision;
+            }
+        }
+        return Decision.allow();
+    }
+
+    private Decision evaluateServiceName(String signal, String protocol, Resource resource) {
+        String serviceName = serviceName(resource);
+        if (!droppedServiceNames.contains(serviceName)) {
+            return Decision.allow();
+        }
+        return Decision.drop("OTLP " + dimension(signal) + " " + dimension(protocol)
+                + " dropped by governance policy: service.name=" + serviceName);
+    }
+
+    private String serviceName(Resource resource) {
+        if (resource == null) {
+            return null;
+        }
+        for (KeyValue attribute : resource.getAttributesList()) {
+            if (SERVICE_NAME.equals(attribute.getKey())) {
+                return stringValue(attribute.getValue());
+            }
+        }
+        return null;
+    }
+
+    private String stringValue(AnyValue value) {
+        if (value == null || !value.hasStringValue()) {
+            return null;
+        }
+        return StringUtils.trimToNull(value.getStringValue());
+    }
+
+    private Set<String> parseValues(String values) {
+        Set<String> parsedValues = new LinkedHashSet<>();
+        if (StringUtils.isBlank(values)) {
+            return parsedValues;
+        }
+        Arrays.stream(values.split("[,;]"))
+                .map(StringUtils::trimToNull)
+                .filter(value -> value != null)
+                .forEach(parsedValues::add);
+        return parsedValues;
+    }
+
+    private String dimension(String value) {
+        return StringUtils.defaultIfBlank(StringUtils.trimToNull(value), "signal");
+    }
+
+    /**
+     * Governance policy decision for a decoded OTLP batch.
+     */
+    public record Decision(boolean dropped, String reason) {
+
+        public static Decision allow() {
+            return new Decision(false, null);
+        }
+
+        public static Decision drop(String reason) {
+            return new Decision(true, reason);
+        }
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/quota/OtlpIngestionQuotaService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/quota/OtlpIngestionQuotaService.java
new file mode 100644
index 0000000000..fa44ace57e
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/quota/OtlpIngestionQuotaService.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.quota;
+
+import io.grpc.Status;
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
+import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
+import io.opentelemetry.proto.logs.v1.ResourceLogs;
+import io.opentelemetry.proto.logs.v1.ScopeLogs;
+import io.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint;
+import io.opentelemetry.proto.metrics.v1.HistogramDataPoint;
+import io.opentelemetry.proto.metrics.v1.Metric;
+import io.opentelemetry.proto.metrics.v1.NumberDataPoint;
+import io.opentelemetry.proto.metrics.v1.ResourceMetrics;
+import io.opentelemetry.proto.metrics.v1.ScopeMetrics;
+import io.opentelemetry.proto.trace.v1.ResourceSpans;
+import io.opentelemetry.proto.trace.v1.ScopeSpans;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryUsage;
+import java.util.List;
+import java.util.function.DoubleSupplier;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * Shared OTLP ingest quota and backpressure guard.
+ */
+@Service
+public class OtlpIngestionQuotaService {
+
+    private final long maxRequestBytes;
+
+    private final long maxSignalItems;
+
+    private final double maxHeapUsageRatio;
+
+    private final DoubleSupplier heapUsageRatioSupplier;
+
+    @Autowired
+    public OtlpIngestionQuotaService(
+            @Value("${hertzbeat.otlp.ingestion.max-request-bytes:10485760}") long maxRequestBytes,
+            @Value("${hertzbeat.otlp.ingestion.max-signal-items:100000}") long maxSignalItems,
+            @Value("${hertzbeat.otlp.ingestion.max-heap-usage-ratio:0.95}") double maxHeapUsageRatio) {
+        this(maxRequestBytes, maxSignalItems, maxHeapUsageRatio,
+                OtlpIngestionQuotaService::currentJvmHeapUsageRatio);
+    }
+
+    public OtlpIngestionQuotaService(long maxRequestBytes) {
+        this(maxRequestBytes, Long.MAX_VALUE);
+    }
+
+    public OtlpIngestionQuotaService(long maxRequestBytes, long maxSignalItems) {
+        this(maxRequestBytes, maxSignalItems, 0D, () -> 0D);
+    }
+
+    public OtlpIngestionQuotaService(long maxRequestBytes, long maxSignalItems, double maxHeapUsageRatio,
+                                     DoubleSupplier heapUsageRatioSupplier) {
+        this.maxRequestBytes = maxRequestBytes;
+        this.maxSignalItems = maxSignalItems;
+        this.maxHeapUsageRatio = maxHeapUsageRatio;
+        this.heapUsageRatioSupplier = heapUsageRatioSupplier;
+    }
+
+    public void checkRequestBytes(String signal, String protocol, long requestBytes) {
+        if (maxRequestBytes > 0 && Math.max(requestBytes, 0L) > maxRequestBytes) {
+            throw Status.RESOURCE_EXHAUSTED
+                    .withDescription("OTLP " + dimension(signal) + " " + dimension(protocol)
+                            + " payload exceeds " + maxRequestBytes + " bytes.")
+                    .asRuntimeException();
+        }
+        checkMemoryPressure(signal, protocol);
+    }
+
+    public void checkMetricItems(String protocol, ExportMetricsServiceRequest request) {
+        checkSignalItems("metrics", protocol, countMetricItems(request));
+    }
+
+    public void checkMetricItems(String protocol, long signalItems) {
+        checkSignalItems("metrics", protocol, signalItems);
+    }
+
+    public void checkTraceItems(String protocol, ExportTraceServiceRequest request) {
+        checkSignalItems("traces", protocol, countTraceItems(request));
+    }
+
+    public void checkTraceItems(String protocol, long signalItems) {
+        checkSignalItems("traces", protocol, signalItems);
+    }
+
+    public void checkLogItems(String protocol, ExportLogsServiceRequest request) {
+        checkSignalItems("logs", protocol, countLogItems(request));
+    }
+
+    public void checkLogItems(String protocol, long signalItems) {
+        checkSignalItems("logs", protocol, signalItems);
+    }
+
+    private void checkSignalItems(String signal, String protocol, long signalItems) {
+        if (maxSignalItems <= 0 || Math.max(signalItems, 0L) <= maxSignalItems) {
+            checkMemoryPressure(signal, protocol);
+            return;
+        }
+        throw Status.RESOURCE_EXHAUSTED
+                .withDescription("OTLP " + dimension(signal) + " " + dimension(protocol)
+                        + " batch exceeds " + maxSignalItems + " signal items.")
+                .asRuntimeException();
+    }
+
+    private void checkMemoryPressure(String signal, String protocol) {
+        if (!isValidHeapPressureLimit()) {
+            return;
+        }
+        double heapUsageRatio = currentHeapUsageRatio();
+        if (heapUsageRatio < maxHeapUsageRatio) {
+            return;
+        }
+        throw Status.RESOURCE_EXHAUSTED
+                .withDescription("OTLP " + dimension(signal) + " " + dimension(protocol)
+                        + " ingestion paused because heap usage is " + percent(heapUsageRatio)
+                        + "% (limit " + percent(maxHeapUsageRatio) + "%).")
+                .asRuntimeException();
+    }
+
+    private boolean isValidHeapPressureLimit() {
+        return !Double.isNaN(maxHeapUsageRatio)
+                && !Double.isInfinite(maxHeapUsageRatio)
+                && maxHeapUsageRatio > 0D
+                && maxHeapUsageRatio <= 1D;
+    }
+
+    public long countMetricItems(ExportMetricsServiceRequest request) {
+        if (request == null) {
+            return 0L;
+        }
+        long count = 0L;
+        for (ResourceMetrics resourceMetrics : request.getResourceMetricsList()) {
+            for (ScopeMetrics scopeMetrics : resourceMetrics.getScopeMetricsList()) {
+                for (Metric metric : scopeMetrics.getMetricsList()) {
+                    count += countMetricDataPoints(metric);
+                }
+            }
+        }
+        return count;
+    }
+
+    private long countMetricDataPoints(Metric metric) {
+        if (metric == null) {
+            return 0L;
+        }
+        if (metric.hasGauge()) {
+            return countNumberDataPoints(metric.getGauge().getDataPointsList());
+        }
+        if (metric.hasSum()) {
+            return countNumberDataPoints(metric.getSum().getDataPointsList());
+        }
+        if (metric.hasHistogram()) {
+            return countHistogramDataPoints(metric.getHistogram().getDataPointsList());
+        }
+        if (metric.hasExponentialHistogram()) {
+            return countExponentialHistogramDataPoints(metric.getExponentialHistogram()
+                    .getDataPointsList());
+        }
+        if (metric.hasSummary()) {
+            return metric.getSummary().getDataPointsCount();
+        }
+        return 1L;
+    }
+
+    private long countNumberDataPoints(List<NumberDataPoint> dataPoints) {
+        long count = 0L;
+        for (NumberDataPoint dataPoint : dataPoints) {
+            count += 1L + dataPoint.getExemplarsCount();
+        }
+        return count;
+    }
+
+    private long countHistogramDataPoints(List<HistogramDataPoint> dataPoints) {
+        long count = 0L;
+        for (HistogramDataPoint dataPoint : dataPoints) {
+            count += 1L + dataPoint.getExemplarsCount();
+        }
+        return count;
+    }
+
+    private long countExponentialHistogramDataPoints(List<ExponentialHistogramDataPoint> dataPoints) {
+        long count = 0L;
+        for (ExponentialHistogramDataPoint dataPoint : dataPoints) {
+            count += 1L + dataPoint.getExemplarsCount();
+        }
+        return count;
+    }
+
+    public long countTraceItems(ExportTraceServiceRequest request) {
+        if (request == null) {
+            return 0L;
+        }
+        long count = 0L;
+        for (ResourceSpans resourceSpans : request.getResourceSpansList()) {
+            for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) {
+                for (var span : scopeSpans.getSpansList()) {
+                    count += 1L + span.getEventsCount() + span.getLinksCount();
+                }
+            }
+        }
+        return count;
+    }
+
+    public long countLogItems(ExportLogsServiceRequest request) {
+        if (request == null) {
+            return 0L;
+        }
+        long count = 0L;
+        for (ResourceLogs resourceLogs : request.getResourceLogsList()) {
+            for (ScopeLogs scopeLogs : resourceLogs.getScopeLogsList()) {
+                count += scopeLogs.getLogRecordsCount();
+            }
+        }
+        return count;
+    }
+
+    private String dimension(String value) {
+        return StringUtils.defaultIfBlank(StringUtils.trimToNull(value), "signal");
+    }
+
+    private double currentHeapUsageRatio() {
+        if (heapUsageRatioSupplier == null) {
+            return 0D;
+        }
+        try {
+            double heapUsageRatio = heapUsageRatioSupplier.getAsDouble();
+            if (Double.isNaN(heapUsageRatio) || Double.isInfinite(heapUsageRatio) || heapUsageRatio < 0D) {
+                return 0D;
+            }
+            return Math.min(heapUsageRatio, 1D);
+        } catch (RuntimeException ex) {
+            return 0D;
+        }
+    }
+
+    private long percent(double ratio) {
+        return Math.round(Math.max(0D, ratio) * 100D);
+    }
+
+    private static double currentJvmHeapUsageRatio() {
+        MemoryUsage heapMemoryUsage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
+        long max = heapMemoryUsage.getMax();
+        long used = heapMemoryUsage.getUsed();
+        if (max <= 0L || used <= 0L) {
+            return 0D;
+        }
+        return (double) used / max;
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/red/OtlpIngestionRedSummaryService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/red/OtlpIngestionRedSummaryService.java
new file mode 100644
index 0000000000..fcd340e6a4
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/red/OtlpIngestionRedSummaryService.java
@@ -0,0 +1,478 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.red;
+
+import io.grpc.Status;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.observability.dto.ingestion.OtlpIngestionRedSummaryDto;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.observability.ingestion.audit.OtlpIngestionAuditEvent;
+import org.apache.hertzbeat.observability.ingestion.audit.OtlpIngestionAuditEventReader;
+import org.apache.hertzbeat.observability.ingestion.audit.OtlpIngestionAuditService;
+import org.apache.hertzbeat.observability.ingestion.redaction.OtlpIngestionRedactionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * Builds recent OTLP ingest RED summaries from real audit events.
+ */
+@Service
+public class OtlpIngestionRedSummaryService {
+
+    private static final String OUTCOME_REJECTED = "rejected";
+    private static final String STATUS_OK = "OK";
+    private static final String UNKNOWN_DIMENSION = "unknown";
+    private static final int MAX_RECENT_EVENTS = 256;
+    private static final long MIN_RATE_WINDOW_MILLIS = 60_000L;
+    private static final List<String> SIGNAL_ORDER = List.of("metrics", "logs", "traces");
+    private static final List<String> PROTOCOL_ORDER = List.of("http", "grpc");
+    private static final Set<String> SUPPORTED_SIGNALS = Set.copyOf(SIGNAL_ORDER);
+    private static final Set<String> SUPPORTED_PROTOCOLS = Set.copyOf(PROTOCOL_ORDER);
+    private static final Set<String> SUPPORTED_OUTCOMES = Set.of("accepted", OUTCOME_REJECTED, "dropped");
+    private static final Set<String> SUPPORTED_STATUS_CODES = Set.of(
+            "OK",
+            "CANCELLED",
+            "UNKNOWN",
+            "INVALID_ARGUMENT",
+            "DEADLINE_EXCEEDED",
+            "NOT_FOUND",
+            "ALREADY_EXISTS",
+            "PERMISSION_DENIED",
+            "RESOURCE_EXHAUSTED",
+            "FAILED_PRECONDITION",
+            "ABORTED",
+            "OUT_OF_RANGE",
+            "UNIMPLEMENTED",
+            "INTERNAL",
+            "UNAVAILABLE",
+            "DATA_LOSS",
+            "UNAUTHENTICATED"
+    );
+
+    private final OtlpIngestionAuditService auditService;
+    private final List<OtlpIngestionAuditEventReader> eventReaders;
+    private final OtlpIngestionRedactionService redactionService;
+
+    public OtlpIngestionRedSummaryService(OtlpIngestionAuditService auditService) {
+        this(auditService, List.of(), new OtlpIngestionRedactionService());
+    }
+
+    public OtlpIngestionRedSummaryService(OtlpIngestionAuditService auditService,
+                                          List<OtlpIngestionAuditEventReader> eventReaders) {
+        this(auditService, eventReaders, new OtlpIngestionRedactionService());
+    }
+
+    @Autowired
+    public OtlpIngestionRedSummaryService(OtlpIngestionAuditService auditService,
+                                          List<OtlpIngestionAuditEventReader> eventReaders,
+                                          OtlpIngestionRedactionService redactionService) {
+        this.auditService = auditService;
+        this.eventReaders = eventReaders == null ? List.of() : List.copyOf(eventReaders.stream()
+                .filter(Objects::nonNull)
+                .toList());
+        this.redactionService = redactionService == null ? new OtlpIngestionRedactionService() : redactionService;
+    }
+
+    public OtlpIngestionRedSummaryDto getSummary() {
+        return getSummary(null);
+    }
+
+    public OtlpIngestionRedSummaryDto getSummary(Long startMillis) {
+        return getSummary(startMillis, null);
+    }
+
+    public OtlpIngestionRedSummaryDto getSummary(Long startMillis, Long endMillis) {
+        String workspaceId = StringUtils.trimToNull(AuthTokenRequestContext.currentWorkspaceId());
+        Long safeStartMillis = normalizeTimeMillis(startMillis);
+        Long safeEndMillis = normalizeTimeMillis(endMillis);
+        List<OtlpIngestionAuditEvent> events = eventsForWorkspace(workspaceId, safeStartMillis, safeEndMillis);
+        List<OtlpIngestionRedSummaryDto.SignalRedMetric> signals =
+                buildSignalSummaries(events, safeStartMillis, safeEndMillis);
+        return new OtlpIngestionRedSummaryDto(
+                workspaceId,
+                countRequests(events),
+                requestRatePerMinute(events, safeStartMillis, safeEndMillis),
+                countErrors(events),
+                errorRate(events),
+                requestBytes(events),
+                signalItems(events),
+                averageSignalItems(events),
+                maxSignalItems(events),
+                averageDurationMillis(events),
+                maxDurationMillis(events),
+                latestObservedAt(events),
+                signals
+        );
+    }
+
+    private List<OtlpIngestionAuditEvent> eventsForWorkspace(String workspaceId, Long startMillis, Long endMillis) {
+        boolean sawDurableRows = false;
+        for (OtlpIngestionAuditEventReader eventReader : eventReaders) {
+            try {
+                List<OtlpIngestionAuditEvent> rawEvents = eventReader
+                        .recentEvents(workspaceId, MAX_RECENT_EVENTS, startMillis, endMillis).stream()
+                        .filter(event -> event != null)
+                        .toList();
+                List<OtlpIngestionAuditEvent> scopedEvents = rawEvents.stream()
+                        .filter(event -> isInRequestedScope(event, workspaceId, startMillis, endMillis))
+                        .sorted(newestObservedAtFirst())
+                        .limit(MAX_RECENT_EVENTS)
+                        .toList();
+                sawDurableRows = sawDurableRows || !scopedEvents.isEmpty();
+                List<OtlpIngestionAuditEvent> events = scopedEvents.stream()
+                        .map(this::normalizeAuditEvent)
+                        .filter(this::hasPositiveObservedAt)
+                        .filter(this::hasSupportedAuditClassification)
+                        .toList();
+                if (!events.isEmpty()) {
+                    return events;
+                }
+            } catch (RuntimeException ignored) {
+                // RED summary should fall back to the in-memory audit window when durable readback is unavailable.
+            }
+        }
+        if (sawDurableRows) {
+            return List.of();
+        }
+        return auditService.recentEvents().stream()
+                .map(this::normalizeAuditEvent)
+                .filter(this::hasPositiveObservedAt)
+                .filter(this::hasSupportedAuditClassification)
+                .filter(event -> isInRequestedScope(event, workspaceId, startMillis, endMillis))
+                .sorted(newestObservedAtFirst())
+                .limit(MAX_RECENT_EVENTS)
+                .toList();
+    }
+
+    private Comparator<OtlpIngestionAuditEvent> newestObservedAtFirst() {
+        return Comparator.comparingLong(OtlpIngestionAuditEvent::observedAt).reversed();
+    }
+
+    private boolean isInRequestedScope(OtlpIngestionAuditEvent event, String workspaceId, Long startMillis,
+                                       Long endMillis) {
+        return (workspaceId == null || workspaceId.equals(StringUtils.trimToNull(event.workspaceId())))
+                && (startMillis == null || event.observedAt() >= startMillis)
+                && (endMillis == null || event.observedAt() <= endMillis);
+    }
+
+    private boolean hasPositiveObservedAt(OtlpIngestionAuditEvent event) {
+        return event.observedAt() > 0;
+    }
+
+    private boolean hasSupportedAuditClassification(OtlpIngestionAuditEvent event) {
+        String signal = normalizedOptionalDimension(event.signal());
+        String protocol = normalizedOptionalDimension(event.protocol());
+        String outcome = normalizedOptionalDimension(event.outcome());
+        String statusCode = normalizedStatusCode(event.statusCode());
+        return signal != null && protocol != null
+                && SUPPORTED_SIGNALS.contains(signal)
+                && SUPPORTED_PROTOCOLS.contains(protocol)
+                && statusCode != null
+                && (isSupportedOutcome(outcome) || isNonOkStatus(statusCode));
+    }
+
+    private boolean isSupportedOutcome(String outcome) {
+        return outcome != null && SUPPORTED_OUTCOMES.contains(outcome);
+    }
+
+    private boolean isNonOkStatus(String statusCode) {
+        return !StringUtils.equalsIgnoreCase(STATUS_OK, statusCode);
+    }
+
+    private Long normalizeTimeMillis(Long timeMillis) {
+        return timeMillis == null || timeMillis <= 0 ? null : timeMillis;
+    }
+
+    private String normalizeDimension(String value) {
+        String dimension = StringUtils.defaultIfBlank(StringUtils.trimToNull(value), UNKNOWN_DIMENSION);
+        return dimension.toLowerCase(Locale.ROOT);
+    }
+
+    private String normalizedOptionalDimension(String value) {
+        String dimension = StringUtils.trimToNull(value);
+        return dimension == null ? null : dimension.toLowerCase(Locale.ROOT);
+    }
+
+    private OtlpIngestionAuditEvent normalizeAuditEvent(OtlpIngestionAuditEvent event) {
+        return new OtlpIngestionAuditEvent(
+                normalizedOptionalDimension(event.signal()),
+                normalizedOptionalDimension(event.protocol()),
+                normalizedOptionalDimension(event.outcome()),
+                normalizedStatusCode(event.statusCode()),
+                StringUtils.trimToNull(event.workspaceId()),
+                StringUtils.trimToNull(event.entityId()),
+                StringUtils.trimToNull(event.ingestId()),
+                event.requestBytes(),
+                event.signalItems(),
+                event.reason(),
+                event.durationMillis(),
+                event.observedAt()
+        );
+    }
+
+    private String normalizedStatusCode(String value) {
+        String text = StringUtils.trimToNull(value);
+        if (text == null) {
+            return null;
+        }
+        String grpcStatusName = numericGrpcStatusName(text);
+        if (grpcStatusName != null) {
+            return grpcStatusName;
+        }
+        if (isNumericStatusCode(text)) {
+            return null;
+        }
+        String normalized = text.replaceAll("[\\s-]+", "_").replaceAll("^_+|_+$", "");
+        normalized = StringUtils.trimToNull(normalized) == null ? null : normalized.toUpperCase(Locale.ROOT);
+        return normalized != null && SUPPORTED_STATUS_CODES.contains(normalized) ? normalized : null;
+    }
+
+    private String numericGrpcStatusName(String text) {
+        String trimmed = StringUtils.trimToEmpty(text);
+        if (trimmed.isEmpty()) {
+            return null;
+        }
+        try {
+            BigDecimal numericStatus = new BigDecimal(trimmed).stripTrailingZeros();
+            if (numericStatus.scale() > 0) {
+                return null;
+            }
+            int codeValue = numericStatus.intValueExact();
+            if (codeValue < Status.Code.OK.value() || codeValue > Status.Code.UNAUTHENTICATED.value()) {
+                return null;
+            }
+            return Status.fromCodeValue(codeValue).getCode().name();
+        } catch (ArithmeticException | NumberFormatException ignored) {
+            return null;
+        }
+    }
+
+    private boolean isNumericStatusCode(String text) {
+        try {
+            new BigDecimal(text);
+            return true;
+        } catch (NumberFormatException ignored) {
+            return false;
+        }
+    }
+
+    private List<OtlpIngestionRedSummaryDto.SignalRedMetric> buildSignalSummaries(
+            List<OtlpIngestionAuditEvent> events, Long startMillis, Long endMillis) {
+        Map<SignalProtocol, List<OtlpIngestionAuditEvent>> groupedEvents = new LinkedHashMap<>();
+        for (OtlpIngestionAuditEvent event : events) {
+            groupedEvents.computeIfAbsent(
+                            new SignalProtocol(normalizeDimension(event.signal()), normalizeDimension(event.protocol())),
+                            ignored -> new ArrayList<>())
+                    .add(event);
+        }
+        return groupedEvents.entrySet().stream()
+                .sorted(Comparator.comparing((Map.Entry<SignalProtocol, List<OtlpIngestionAuditEvent>> entry) ->
+                                signalOrder(entry.getKey().signal()))
+                        .thenComparing(entry -> protocolOrder(entry.getKey().protocol()))
+                        .thenComparing(entry -> entry.getKey().signal())
+                        .thenComparing(entry -> entry.getKey().protocol()))
+                .map(entry -> toSignalSummary(entry.getKey(), entry.getValue(), startMillis, endMillis))
+                .toList();
+    }
+
+    private OtlpIngestionRedSummaryDto.SignalRedMetric toSignalSummary(
+            SignalProtocol signalProtocol, List<OtlpIngestionAuditEvent> events, Long startMillis, Long endMillis) {
+        OtlpIngestionAuditEvent latest = latestEvent(events);
+        return new OtlpIngestionRedSummaryDto.SignalRedMetric(
+                signalProtocol.signal(),
+                signalProtocol.protocol(),
+                countRequests(events),
+                requestRatePerMinute(events, startMillis, endMillis),
+                countErrors(events),
+                errorRate(events),
+                requestBytes(events),
+                signalItems(events),
+                averageSignalItems(events),
+                maxSignalItems(events),
+                averageDurationMillis(events),
+                maxDurationMillis(events),
+                latest == null ? null : latest.statusCode(),
+                latest == null ? null : redactionService.redactText(latest.reason()),
+                latest == null ? null : latest.observedAt()
+        );
+    }
+
+    private long countRequests(List<OtlpIngestionAuditEvent> events) {
+        return events.size();
+    }
+
+    private long countErrors(List<OtlpIngestionAuditEvent> events) {
+        return events.stream().filter(this::isErrorEvent).count();
+    }
+
+    private boolean isErrorEvent(OtlpIngestionAuditEvent event) {
+        String outcome = StringUtils.trimToNull(event.outcome());
+        if (StringUtils.equalsIgnoreCase(OUTCOME_REJECTED, outcome)) {
+            return true;
+        }
+        String statusCode = StringUtils.trimToNull(event.statusCode());
+        return statusCode != null && isNonOkStatus(statusCode);
+    }
+
+    private double requestRatePerMinute(List<OtlpIngestionAuditEvent> events, Long startMillis, Long endMillis) {
+        if (events.isEmpty()) {
+            return 0D;
+        }
+        long windowMillis = rateWindowMillis(events, startMillis, endMillis);
+        return (double) events.size() * MIN_RATE_WINDOW_MILLIS / windowMillis;
+    }
+
+    private long rateWindowMillis(List<OtlpIngestionAuditEvent> events, Long startMillis, Long endMillis) {
+        if (startMillis != null && endMillis != null && endMillis > startMillis) {
+            return endMillis - startMillis;
+        }
+        long firstObservedAt = events.stream()
+                .mapToLong(OtlpIngestionAuditEvent::observedAt)
+                .min()
+                .orElse(0L);
+        long latestObservedAt = events.stream()
+                .mapToLong(OtlpIngestionAuditEvent::observedAt)
+                .max()
+                .orElse(firstObservedAt);
+        return Math.max(MIN_RATE_WINDOW_MILLIS, latestObservedAt - firstObservedAt);
+    }
+
+    private double errorRate(List<OtlpIngestionAuditEvent> events) {
+        if (events.isEmpty()) {
+            return 0D;
+        }
+        return (double) countErrors(events) / events.size();
+    }
+
+    private long requestBytes(List<OtlpIngestionAuditEvent> events) {
+        long total = 0L;
+        for (OtlpIngestionAuditEvent event : events) {
+            total = saturatedAdd(total, nonNegativeCounter(event.requestBytes()));
+        }
+        return total;
+    }
+
+    private long signalItems(List<OtlpIngestionAuditEvent> events) {
+        long total = 0L;
+        for (OtlpIngestionAuditEvent event : events) {
+            Long signalItems = nonNegativeCounter(event.signalItems());
+            if (signalItems != null) {
+                total = saturatedAdd(total, signalItems);
+            }
+        }
+        return total;
+    }
+
+    private Long averageSignalItems(List<OtlpIngestionAuditEvent> events) {
+        List<Long> signalItems = events.stream()
+                .map(OtlpIngestionAuditEvent::signalItems)
+                .map(this::nonNegativeCounter)
+                .filter(value -> value != null)
+                .toList();
+        if (signalItems.isEmpty()) {
+            return null;
+        }
+        return averageCounter(signalItems);
+    }
+
+    private Long maxSignalItems(List<OtlpIngestionAuditEvent> events) {
+        return events.stream()
+                .map(OtlpIngestionAuditEvent::signalItems)
+                .map(this::nonNegativeCounter)
+                .filter(value -> value != null)
+                .max(Long::compareTo)
+                .orElse(null);
+    }
+
+    private Long averageDurationMillis(List<OtlpIngestionAuditEvent> events) {
+        List<Long> durations = events.stream()
+                .map(OtlpIngestionAuditEvent::durationMillis)
+                .map(this::nonNegativeCounter)
+                .filter(duration -> duration != null)
+                .toList();
+        if (durations.isEmpty()) {
+            return null;
+        }
+        return averageCounter(durations);
+    }
+
+    private Long maxDurationMillis(List<OtlpIngestionAuditEvent> events) {
+        return events.stream()
+                .map(OtlpIngestionAuditEvent::durationMillis)
+                .map(this::nonNegativeCounter)
+                .filter(duration -> duration != null)
+                .max(Long::compareTo)
+                .orElse(null);
+    }
+
+    private long nonNegativeCounter(long value) {
+        return Math.max(value, 0L);
+    }
+
+    private Long nonNegativeCounter(Long value) {
+        return value == null ? null : nonNegativeCounter(value.longValue());
+    }
+
+    private long saturatedAdd(long left, long right) {
+        return Long.MAX_VALUE - left < right ? Long.MAX_VALUE : left + right;
+    }
+
+    private long averageCounter(List<Long> values) {
+        double average = values.stream()
+                .mapToDouble(Long::doubleValue)
+                .average()
+                .orElse(0D);
+        if (average >= Long.MAX_VALUE) {
+            return Long.MAX_VALUE;
+        }
+        return Math.round(average);
+    }
+
+    private Long latestObservedAt(List<OtlpIngestionAuditEvent> events) {
+        OtlpIngestionAuditEvent latest = latestEvent(events);
+        return latest == null ? null : latest.observedAt();
+    }
+
+    private OtlpIngestionAuditEvent latestEvent(List<OtlpIngestionAuditEvent> events) {
+        return events.stream()
+                .max(Comparator.comparingLong(OtlpIngestionAuditEvent::observedAt))
+                .orElse(null);
+    }
+
+    private int signalOrder(String signal) {
+        int index = SIGNAL_ORDER.indexOf(signal);
+        return index < 0 ? SIGNAL_ORDER.size() : index;
+    }
+
+    private int protocolOrder(String protocol) {
+        int index = PROTOCOL_ORDER.indexOf(protocol);
+        return index < 0 ? PROTOCOL_ORDER.size() : index;
+    }
+
+    private record SignalProtocol(String signal, String protocol) {
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/redaction/OtlpIngestionRedactionService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/redaction/OtlpIngestionRedactionService.java
new file mode 100644
index 0000000000..24ba4b2147
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/redaction/OtlpIngestionRedactionService.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.redaction;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+/**
+ * Shared sensitive-data redaction boundary for OTLP ingest payloads and self-observability evidence.
+ */
+@Service
+public class OtlpIngestionRedactionService {
+
+    public static final String REDACTED = "[REDACTED]";
+
+    private static final Pattern INLINE_SECRET_PATTERN = Pattern.compile(
+            "(?i)\\b(password|passwd|pwd|token|access[._-]?token|refresh[._-]?token|api[._-]?key|"
+                    + "client[._-]?secret|private[._-]?key|secret|authorization|cookie)\\b([\"']?\\s*[:=]\\s*)"
+                    + "(Bearer\\s+[^\\s,;&]+|Basic\\s+[^\\s,;&]+|\"[^\"]*\"|'[^']*'|[^\\s,;&]+)"
+    );
+    private static final Pattern BEARER_TOKEN_PATTERN = Pattern.compile("(?i)\\bBearer\\s+[^\\s,;&]+");
+    private static final Pattern BASIC_TOKEN_PATTERN = Pattern.compile("(?i)\\bBasic\\s+[^\\s,;&]+");
+
+    public Map<String, Object> redactObjectMap(Map<String, Object> source) {
+        if (source == null || source.isEmpty()) {
+            return Map.of();
+        }
+        Map<String, Object> redacted = new LinkedHashMap<>();
+        source.forEach((key, value) -> {
+            if (StringUtils.isBlank(key)) {
+                return;
+            }
+            redacted.put(key, redactObject(key, value));
+        });
+        return redacted;
+    }
+
+    public Map<String, String> redactStringMap(Map<String, String> source) {
+        if (source == null || source.isEmpty()) {
+            return Map.of();
+        }
+        Map<String, String> redacted = new LinkedHashMap<>();
+        source.forEach((key, value) -> {
+            if (StringUtils.isBlank(key)) {
+                return;
+            }
+            if (isSensitiveKey(key)) {
+                redacted.put(key, REDACTED);
+            } else {
+                redacted.put(key, redactText(value));
+            }
+        });
+        return redacted;
+    }
+
+    public Object redactObject(String key, Object value) {
+        if (isSensitiveKey(key)) {
+            return REDACTED;
+        }
+        if (value instanceof CharSequence text) {
+            return redactText(text.toString());
+        }
+        if (value instanceof Map<?, ?> map) {
+            return redactGenericMap(map);
+        }
+        if (value instanceof Iterable<?> iterable) {
+            List<Object> redacted = new ArrayList<>();
+            iterable.forEach(item -> redacted.add(redactObject(null, item)));
+            return redacted;
+        }
+        return value;
+    }
+
+    public String redactText(String text) {
+        if (StringUtils.isBlank(text)) {
+            return text;
+        }
+        String redacted = redactInlineSecrets(text);
+        redacted = BEARER_TOKEN_PATTERN.matcher(redacted).replaceAll("Bearer " + REDACTED);
+        return BASIC_TOKEN_PATTERN.matcher(redacted).replaceAll("Basic " + REDACTED);
+    }
+
+    public boolean isSensitiveKey(String key) {
+        if (StringUtils.isBlank(key)) {
+            return false;
+        }
+        String normalized = key.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", "_");
+        return normalized.contains("password")
+                || normalized.contains("passwd")
+                || normalized.contains("secret")
+                || normalized.contains("token")
+                || normalized.contains("authorization")
+                || normalized.contains("cookie")
+                || normalized.contains("api_key")
+                || normalized.contains("apikey")
+                || normalized.contains("private_key")
+                || normalized.contains("client_secret")
+                || normalized.contains("access_token")
+                || normalized.contains("refresh_token");
+    }
+
+    private Map<String, Object> redactGenericMap(Map<?, ?> source) {
+        if (source == null || source.isEmpty()) {
+            return Map.of();
+        }
+        Map<String, Object> redacted = new LinkedHashMap<>();
+        source.forEach((key, value) -> {
+            if (key == null || StringUtils.isBlank(String.valueOf(key))) {
+                return;
+            }
+            String normalizedKey = String.valueOf(key);
+            redacted.put(normalizedKey, redactObject(normalizedKey, value));
+        });
+        return redacted;
+    }
+
+    private String redactInlineSecrets(String text) {
+        Matcher matcher = INLINE_SECRET_PATTERN.matcher(text);
+        StringBuffer redacted = new StringBuffer();
+        while (matcher.find()) {
+            String replacement = matcher.group(1) + matcher.group(2) + redactInlineSecretValue(matcher.group(3));
+            matcher.appendReplacement(redacted, Matcher.quoteReplacement(replacement));
+        }
+        matcher.appendTail(redacted);
+        return redacted.toString();
+    }
+
+    private String redactInlineSecretValue(String value) {
+        if (value != null && value.length() >= 2) {
+            char first = value.charAt(0);
+            char last = value.charAt(value.length() - 1);
+            if ((first == '"' || first == '\'') && first == last) {
+                return String.valueOf(first) + REDACTED + first;
+            }
+        }
+        return REDACTED;
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/retry/OtlpIngestionRetryService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/retry/OtlpIngestionRetryService.java
new file mode 100644
index 0000000000..b3a6e9c837
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/retry/OtlpIngestionRetryService.java
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.retry;
+
+import io.grpc.Metadata;
+import io.grpc.Status;
+import io.grpc.StatusException;
+import io.grpc.StatusRuntimeException;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.function.LongConsumer;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import org.apache.hertzbeat.observability.ingestion.error.OtlpIngestionBackpressureHeaders;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.ResourceAccessException;
+
+/**
+ * Shared retry boundary for transient OTLP backend writes.
+ */
+@Service
+public class OtlpIngestionRetryService {
+
+    private static final int DEFAULT_MAX_ATTEMPTS = 2;
+    private static final long DEFAULT_MAX_RETRY_AFTER_MILLIS = 60_000L;
+
+    private final int maxAttempts;
+    private final long backoffMillis;
+    private final long maxRetryAfterMillis;
+    private final LongConsumer backoffSleeper;
+
+    public OtlpIngestionRetryService() {
+        this(DEFAULT_MAX_ATTEMPTS, 0L, OtlpIngestionRetryService::sleep);
+    }
+
+    @Autowired
+    public OtlpIngestionRetryService(
+            @Value("${hertzbeat.otlp.ingestion.retry.max-attempts:2}") int maxAttempts,
+            @Value("${hertzbeat.otlp.ingestion.retry.backoff-millis:0}") long backoffMillis,
+            @Value("${hertzbeat.otlp.ingestion.retry.max-retry-after-millis:60000}") long maxRetryAfterMillis) {
+        this(maxAttempts, backoffMillis, maxRetryAfterMillis, OtlpIngestionRetryService::sleep);
+    }
+
+    public OtlpIngestionRetryService(int maxAttempts) {
+        this(maxAttempts, 0L, OtlpIngestionRetryService::sleep);
+    }
+
+    OtlpIngestionRetryService(int maxAttempts, long backoffMillis, LongConsumer backoffSleeper) {
+        this(maxAttempts, backoffMillis, DEFAULT_MAX_RETRY_AFTER_MILLIS, backoffSleeper);
+    }
+
+    OtlpIngestionRetryService(int maxAttempts, long backoffMillis, long maxRetryAfterMillis,
+                              LongConsumer backoffSleeper) {
+        this.maxAttempts = Math.max(1, maxAttempts);
+        this.backoffMillis = Math.max(0L, backoffMillis);
+        this.maxRetryAfterMillis = Math.max(0L, maxRetryAfterMillis);
+        this.backoffSleeper = backoffSleeper == null ? OtlpIngestionRetryService::sleep : backoffSleeper;
+    }
+
+    public <T> T execute(Supplier<T> operation) {
+        return execute(operation, result -> false);
+    }
+
+    public <T> T execute(Supplier<T> operation, Predicate<T> retryableResult) {
+        RuntimeException lastFailure = null;
+        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
+            try {
+                T result = operation.get();
+                if (attempt < maxAttempts && retryableResult != null && retryableResult.test(result)) {
+                    waitBeforeRetry(result);
+                    continue;
+                }
+                return result;
+            } catch (RuntimeException ex) {
+                lastFailure = ex;
+                if (attempt >= maxAttempts || !isRetryable(ex)) {
+                    throw ex;
+                }
+                waitBeforeRetry(ex);
+            }
+        }
+        throw lastFailure;
+    }
+
+    private boolean isRetryable(RuntimeException exception) {
+        Throwable current = exception;
+        while (current != null) {
+            if (current instanceof ResourceAccessException) {
+                return true;
+            }
+            if (current instanceof HttpStatusCodeException statusException) {
+                return isRetryableStatus(statusException.getStatusCode());
+            }
+            if (current instanceof StatusRuntimeException statusException) {
+                return isRetryableGrpcStatus(statusException.getStatus().getCode());
+            }
+            if (current instanceof StatusException statusException) {
+                return isRetryableGrpcStatus(statusException.getStatus().getCode());
+            }
+            current = current.getCause();
+        }
+        return false;
+    }
+
+    public boolean isRetryableStatus(HttpStatusCode statusCode) {
+        if (statusCode == null) {
+            return false;
+        }
+        return switch (statusCode.value()) {
+            case 408, 425, 429, 502, 503, 504 -> true;
+            default -> false;
+        };
+    }
+
+    private boolean isRetryableGrpcStatus(Status.Code statusCode) {
+        return Status.Code.UNAVAILABLE == statusCode
+                || Status.Code.DEADLINE_EXCEEDED == statusCode
+                || Status.Code.RESOURCE_EXHAUSTED == statusCode;
+    }
+
+    private void waitBeforeRetry(Object retryableResult) {
+        waitBeforeRetry(retryAfterMillis(retryableResult));
+    }
+
+    private void waitBeforeRetry() {
+        waitBeforeRetry(null);
+    }
+
+    private void waitBeforeRetry(Long retryAfterMillis) {
+        long delayMillis = retryAfterMillis == null
+                ? backoffMillis
+                : Math.min(Math.max(0L, retryAfterMillis), maxRetryAfterMillis);
+        if (delayMillis > 0L) {
+            backoffSleeper.accept(delayMillis);
+        }
+    }
+
+    private Long retryAfterMillis(Object retryableResult) {
+        if (retryableResult instanceof ResponseEntity<?> responseEntity) {
+            return retryAfterMillis(responseEntity.getHeaders());
+        }
+        if (retryableResult instanceof Throwable throwable) {
+            return retryAfterMillis(throwable);
+        }
+        return null;
+    }
+
+    private Long retryAfterMillis(Throwable throwable) {
+        Throwable current = throwable;
+        while (current != null) {
+            if (current instanceof HttpStatusCodeException statusException) {
+                return isRetryableStatus(statusException.getStatusCode())
+                        ? retryAfterMillis(statusException.getResponseHeaders())
+                        : null;
+            }
+            if (current instanceof StatusRuntimeException statusException) {
+                return isRetryableGrpcStatus(statusException.getStatus().getCode())
+                        ? retryAfterMillis(statusException.getTrailers())
+                        : null;
+            }
+            if (current instanceof StatusException statusException) {
+                return isRetryableGrpcStatus(statusException.getStatus().getCode())
+                        ? retryAfterMillis(statusException.getTrailers())
+                        : null;
+            }
+            current = current.getCause();
+        }
+        return null;
+    }
+
+    private Long retryAfterMillis(Metadata trailers) {
+        if (trailers == null) {
+            return null;
+        }
+        return retryAfterHeaderMillis(trailers.get(OtlpIngestionBackpressureHeaders.RETRY_AFTER_TRAILER_KEY));
+    }
+
+    private Long retryAfterMillis(HttpHeaders headers) {
+        if (headers == null) {
+            return null;
+        }
+        List<String> values = headers.get(HttpHeaders.RETRY_AFTER);
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            Long delayMillis = retryAfterHeaderMillis(value);
+            if (delayMillis != null) {
+                return delayMillis;
+            }
+        }
+        return null;
+    }
+
+    private Long retryAfterHeaderMillis(String value) {
+        Long delayMillis = retryAfterSecondsMillis(value);
+        if (delayMillis != null) {
+            return delayMillis;
+        }
+        return retryAfterDateMillis(value);
+    }
+
+    private Long retryAfterSecondsMillis(String value) {
+        String trimmed = value == null ? "" : value.trim();
+        if (!trimmed.matches("\\d+")) {
+            return null;
+        }
+        try {
+            long seconds = Long.parseLong(trimmed);
+            if (seconds > Long.MAX_VALUE / 1000L) {
+                return Long.MAX_VALUE;
+            }
+            return seconds * 1000L;
+        } catch (NumberFormatException ignored) {
+            return Long.MAX_VALUE;
+        }
+    }
+
+    private Long retryAfterDateMillis(String value) {
+        String trimmed = value == null ? "" : value.trim();
+        if (trimmed.isEmpty()) {
+            return null;
+        }
+        try {
+            Instant retryAt = ZonedDateTime.parse(trimmed, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant();
+            return Math.max(0L, Duration.between(Instant.now(), retryAt).toMillis());
+        } catch (DateTimeException | ArithmeticException ignored) {
+            return null;
+        }
+    }
+
+    private static void sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("Interrupted while waiting to retry OTLP ingestion.", ex);
+        }
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/security/OtlpIngestionRequestContextResolver.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/security/OtlpIngestionRequestContextResolver.java
new file mode 100644
index 0000000000..7e03fcb293
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/security/OtlpIngestionRequestContextResolver.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.security;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.observability.ingestion.enricher.OtlpCorrelationContext;
+import org.apache.hertzbeat.observability.ingestion.semantic.OtlpResourceSemanticAttributes;
+import org.springframework.stereotype.Component;
+
+/**
+ * Resolves authenticated OTLP ingest request context for signal enrichment and intake records.
+ */
+@Component
+public class OtlpIngestionRequestContextResolver {
+
+    public OtlpCorrelationContext currentCorrelationContext() {
+        String workspaceId = AuthTokenRequestContext.currentWorkspaceId();
+        if (StringUtils.isBlank(workspaceId)) {
+            return OtlpCorrelationContext.empty();
+        }
+        return new OtlpCorrelationContext(null, null, workspaceId);
+    }
+
+    public Map<String, String> withWorkspaceResourceAttributes(Map<String, String> resourceAttributes) {
+        Map<String, String> resolved = new LinkedHashMap<>();
+        if (resourceAttributes != null) {
+            resolved.putAll(resourceAttributes);
+        }
+        String workspaceId = AuthTokenRequestContext.currentWorkspaceId();
+        if (StringUtils.isNotBlank(workspaceId)) {
+            resolved.put(OtlpResourceSemanticAttributes.HERTZBEAT_WORKSPACE_ID, workspaceId);
+        }
+        return resolved;
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/semantic/OtlpResourceSemanticAttributes.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/semantic/OtlpResourceSemanticAttributes.java
new file mode 100644
index 0000000000..a4f9884f7c
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/semantic/OtlpResourceSemanticAttributes.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.semantic;
+
+import java.util.List;
+
+/**
+ * OTLP resource keys HertzBeat treats as stable ingestion and entity-correlation semantics.
+ */
+public final class OtlpResourceSemanticAttributes {
+
+    public static final String SERVICE_NAME = "service.name";
+    public static final String SERVICE_NAMESPACE = "service.namespace";
+    public static final String DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name";
+    public static final String SERVICE_INSTANCE_ID = "service.instance.id";
+    public static final String HOST_NAME = "host.name";
+    public static final String HOST_ID = "host.id";
+    public static final String K8S_CLUSTER_NAME = "k8s.cluster.name";
+    public static final String K8S_NAMESPACE_NAME = "k8s.namespace.name";
+    public static final String K8S_NODE_NAME = "k8s.node.name";
+    public static final String K8S_POD_NAME = "k8s.pod.name";
+    public static final String K8S_CONTAINER_NAME = "k8s.container.name";
+    public static final String HERTZBEAT_ENTITY_ID = "hertzbeat.entity_id";
+    public static final String HERTZBEAT_WORKSPACE_ID = "hertzbeat.workspace_id";
+
+    public static final List<String> PRIMARY_OTEL_IDENTITY_KEYS = List.of(
+            SERVICE_NAME,
+            SERVICE_NAMESPACE,
+            DEPLOYMENT_ENVIRONMENT_NAME
+    );
+
+    public static final List<String> HERTZBEAT_CONTEXT_KEYS = List.of(
+            HERTZBEAT_ENTITY_ID,
+            HERTZBEAT_WORKSPACE_ID
+    );
+
+    public static final List<String> GREPTIME_METRIC_PROMOTED_RESOURCE_KEYS = List.of(
+            SERVICE_NAME,
+            SERVICE_NAMESPACE,
+            DEPLOYMENT_ENVIRONMENT_NAME,
+            SERVICE_INSTANCE_ID,
+            HOST_NAME,
+            HOST_ID,
+            K8S_CLUSTER_NAME,
+            K8S_NAMESPACE_NAME,
+            K8S_NODE_NAME,
+            K8S_POD_NAME,
+            K8S_CONTAINER_NAME,
+            HERTZBEAT_ENTITY_ID,
+            HERTZBEAT_WORKSPACE_ID
+    );
+
+    private OtlpResourceSemanticAttributes() {
+    }
+}
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/OtlpGrpcIngestionService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/OtlpGrpcIngestionService.java
index c67f9e2c68..dce21a6f30 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/OtlpGrpcIngestionService.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/OtlpGrpcIngestionService.java
@@ -34,6 +34,8 @@ public interface OtlpGrpcIngestionService {
 
     ResponseEntity<byte[]> ingestMetricsHttp(byte[] content, HttpHeaders requestHeaders);
 
+    ResponseEntity<byte[]> ingestLogsHttp(byte[] content, HttpHeaders requestHeaders);
+
     ResponseEntity<byte[]> ingestTracesHttp(byte[] content, HttpHeaders requestHeaders);
 
     ExportMetricsServiceResponse ingestMetricsGrpc(ExportMetricsServiceRequest request);
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java
index 00a5c8fcde..ae4b98867a 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java
@@ -23,22 +23,33 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.Message;
 import com.google.protobuf.util.JsonFormat;
-import com.google.rpc.Status;
 import io.grpc.StatusRuntimeException;
 import io.opentelemetry.proto.common.v1.AnyValue;
+import io.opentelemetry.proto.common.v1.ArrayValue;
+import io.opentelemetry.proto.common.v1.InstrumentationScope;
 import io.opentelemetry.proto.common.v1.KeyValue;
+import io.opentelemetry.proto.common.v1.KeyValueList;
 import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
 import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse;
 import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
 import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse;
 import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
 import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse;
+import io.opentelemetry.proto.logs.v1.LogRecord;
+import io.opentelemetry.proto.logs.v1.ResourceLogs;
+import io.opentelemetry.proto.logs.v1.ScopeLogs;
+import io.opentelemetry.proto.metrics.v1.Exemplar;
+import io.opentelemetry.proto.metrics.v1.ExponentialHistogram;
 import io.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint;
+import io.opentelemetry.proto.metrics.v1.Gauge;
+import io.opentelemetry.proto.metrics.v1.Histogram;
 import io.opentelemetry.proto.metrics.v1.HistogramDataPoint;
 import io.opentelemetry.proto.metrics.v1.Metric;
 import io.opentelemetry.proto.metrics.v1.NumberDataPoint;
 import io.opentelemetry.proto.metrics.v1.ResourceMetrics;
 import io.opentelemetry.proto.metrics.v1.ScopeMetrics;
+import io.opentelemetry.proto.metrics.v1.Sum;
+import io.opentelemetry.proto.metrics.v1.Summary;
 import io.opentelemetry.proto.metrics.v1.SummaryDataPoint;
 import io.opentelemetry.proto.trace.v1.ResourceSpans;
 import io.opentelemetry.proto.trace.v1.ScopeSpans;
@@ -54,25 +65,39 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.util.zip.GZIPInputStream;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hertzbeat.common.observability.gateway.ObservabilitySignalIntakeGateway;
 import org.apache.hertzbeat.observability.ingestion.service.OtlpGrpcIngestionService;
 import org.apache.hertzbeat.observability.ingestion.adapter.OtlpLogProtocolAdapter;
+import org.apache.hertzbeat.observability.ingestion.audit.OtlpIngestionAuditService;
 import org.apache.hertzbeat.observability.ingestion.enricher.OtlpCorrelationContext;
 import org.apache.hertzbeat.observability.ingestion.enricher.OtlpCorrelationEnricher;
+import org.apache.hertzbeat.observability.ingestion.enricher.OtlpEntityIdentityResolver;
+import org.apache.hertzbeat.observability.ingestion.error.OtlpIngestionBackpressureHeaders;
+import org.apache.hertzbeat.observability.ingestion.error.OtlpIngestionErrorResponseFactory;
 import org.apache.hertzbeat.observability.ingestion.forwarder.GreptimeOtlpForwarder;
+import org.apache.hertzbeat.observability.ingestion.governance.OtlpIngestionGovernanceService;
+import org.apache.hertzbeat.observability.ingestion.quota.OtlpIngestionQuotaService;
+import org.apache.hertzbeat.observability.ingestion.redaction.OtlpIngestionRedactionService;
+import org.apache.hertzbeat.observability.ingestion.retry.OtlpIngestionRetryService;
+import org.apache.hertzbeat.observability.ingestion.semantic.OtlpResourceSemanticAttributes;
+import org.apache.hertzbeat.observability.ingestion.security.OtlpIngestionRequestContextResolver;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpStatusCodeException;
 import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestTemplate;
 
@@ -84,8 +109,11 @@ import org.springframework.web.client.RestTemplate;
 public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
 
     private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf";
+    private static final MediaType MEDIA_TYPE_PROTOBUF = MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF);
+    private static final MediaType MEDIA_TYPE_PROTOBUF_ALT = MediaType.parseMediaType("application/protobuf");
     private static final String CONTENT_ENCODING = "Content-Encoding";
     private static final String CONTENT_ENCODING_GZIP = "gzip";
+    private static final int GZIP_DECOMPRESSION_BUFFER_BYTES = 8192;
     private static final String GREPTIME_DB_NAME_HEADER = "X-Greptime-DB-Name";
     private static final String GREPTIME_OTLP_METRIC_PROMOTE_ALL_RESOURCE_ATTRS_HEADER =
             "X-Greptime-OTLP-Metric-Promote-All-Resource-Attrs";
@@ -98,20 +126,11 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
     private static final String DEFAULT_GREPTIME_DB_NAME = "public";
     private static final String DEFAULT_TRACES_TABLE_NAME = "hzb_traces";
     private static final String DEFAULT_TRACE_PIPELINE = "greptime_trace_v1";
-    private static final String DEFAULT_METRIC_PROMOTED_RESOURCE_ATTRS = String.join(";",
-            "service.name",
-            "service.namespace",
-            "deployment.environment.name",
-            "service.instance.id",
-            "host.name",
-            "host.id",
-            "k8s.cluster.name",
-            "k8s.namespace.name",
-            "k8s.node.name",
-            "k8s.pod.name",
-            "k8s.container.name"
-    );
+    private static final String DEFAULT_METRIC_PROMOTED_RESOURCE_ATTRS =
+            String.join(";", OtlpResourceSemanticAttributes.GREPTIME_METRIC_PROMOTED_RESOURCE_KEYS);
     private static final Set<String> OTLP_HEX_ID_FIELDS = Set.of("traceId", "spanId", "parentSpanId");
+    private static final int OTLP_TRACE_ID_BYTES = 16;
+    private static final int OTLP_SPAN_ID_BYTES = 8;
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
     private static final String OTLP_METRIC_COMPATIBILITY = "otlp.metric.compatibility";
     private static final String OTLP_METRIC_COMPATIBILITY_REASON = "otlp.metric.compatibility.reason";
@@ -153,74 +172,247 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
     private final OtlpLogProtocolAdapter otlpLogProtocolAdapter;
     private final GreptimeOtlpForwarder greptimeOtlpForwarder;
     private final OtlpCorrelationEnricher otlpCorrelationEnricher;
+    private final OtlpIngestionErrorResponseFactory errorResponseFactory;
+    private final OtlpIngestionRequestContextResolver requestContextResolver;
+    private final OtlpIngestionAuditService auditService;
+    private final OtlpIngestionGovernanceService governanceService;
+    private final OtlpIngestionQuotaService quotaService;
     private final ObservabilitySignalIntakeGateway observabilitySignalIntakeGateway;
+    private final OtlpEntityIdentityResolver otlpEntityIdentityResolver;
+    private final OtlpIngestionRedactionService redactionService = new OtlpIngestionRedactionService();
+    private final OtlpIngestionRetryService retryService;
 
     public OtlpGrpcIngestionServiceImpl(RestTemplate restTemplate,
                                         ObjectProvider<GreptimeProperties> greptimePropertiesProvider,
                                         OtlpLogProtocolAdapter otlpLogProtocolAdapter,
                                         GreptimeOtlpForwarder greptimeOtlpForwarder,
                                         OtlpCorrelationEnricher otlpCorrelationEnricher,
+                                        OtlpIngestionErrorResponseFactory errorResponseFactory,
+                                        OtlpIngestionRequestContextResolver requestContextResolver,
+                                        OtlpIngestionAuditService auditService,
+                                        OtlpIngestionGovernanceService governanceService,
+                                        OtlpIngestionQuotaService quotaService,
                                         @Qualifier("telemetryIntakeServiceImpl")
-                                        ObservabilitySignalIntakeGateway observabilitySignalIntakeGateway) {
+                                        ObservabilitySignalIntakeGateway observabilitySignalIntakeGateway,
+                                        OtlpEntityIdentityResolver otlpEntityIdentityResolver) {
+        this(restTemplate, greptimePropertiesProvider, otlpLogProtocolAdapter, greptimeOtlpForwarder,
+                otlpCorrelationEnricher, errorResponseFactory, requestContextResolver, auditService, governanceService,
+                quotaService, observabilitySignalIntakeGateway, otlpEntityIdentityResolver,
+                new OtlpIngestionRetryService());
+    }
+
+    @Autowired
+    public OtlpGrpcIngestionServiceImpl(RestTemplate restTemplate,
+                                        ObjectProvider<GreptimeProperties> greptimePropertiesProvider,
+                                        OtlpLogProtocolAdapter otlpLogProtocolAdapter,
+                                        GreptimeOtlpForwarder greptimeOtlpForwarder,
+                                        OtlpCorrelationEnricher otlpCorrelationEnricher,
+                                        OtlpIngestionErrorResponseFactory errorResponseFactory,
+                                        OtlpIngestionRequestContextResolver requestContextResolver,
+                                        OtlpIngestionAuditService auditService,
+                                        OtlpIngestionGovernanceService governanceService,
+                                        OtlpIngestionQuotaService quotaService,
+                                        @Qualifier("telemetryIntakeServiceImpl")
+                                        ObservabilitySignalIntakeGateway observabilitySignalIntakeGateway,
+                                        OtlpEntityIdentityResolver otlpEntityIdentityResolver,
+                                        OtlpIngestionRetryService retryService) {
         this.restTemplate = restTemplate;
         this.greptimePropertiesProvider = greptimePropertiesProvider;
         this.otlpLogProtocolAdapter = otlpLogProtocolAdapter;
         this.greptimeOtlpForwarder = greptimeOtlpForwarder;
         this.otlpCorrelationEnricher = otlpCorrelationEnricher;
+        this.errorResponseFactory = errorResponseFactory;
+        this.requestContextResolver = requestContextResolver;
+        this.auditService = auditService;
+        this.governanceService = governanceService;
+        this.quotaService = quotaService;
         this.observabilitySignalIntakeGateway = observabilitySignalIntakeGateway;
+        this.otlpEntityIdentityResolver = otlpEntityIdentityResolver;
+        this.retryService = retryService == null ? new OtlpIngestionRetryService() : retryService;
     }
 
     @Override
     public ResponseEntity<byte[]> ingestMetricsHttp(byte[] content, HttpHeaders requestHeaders) {
-        MediaType contentType = requestHeaders.getContentType();
+        long startedAtNanos = System.nanoTime();
+        HttpHeaders safeRequestHeaders = safeHeaders(requestHeaders);
+        byte[] safeContent = safeContent(content);
+        MediaType contentType = safeRequestHeaders.getContentType();
+        OtlpCorrelationContext correlationContext = requestContextResolver.currentCorrelationContext();
+        long requestBytes = requestBytes(content);
+        Long signalItems = null;
         try {
-            ExportMetricsServiceRequest request = decodeMetricsRequest(content, contentType, requestHeaders);
-            ResponseEntity<byte[]> response = proxySignalHttp(content, requestHeaders, "/v1/otlp/v1/metrics", false);
+            quotaService.checkRequestBytes("metrics", "http", requestBytes);
+            byte[] normalizedContent = normalizeHttpContentForQuota("metrics", "http", safeContent, safeRequestHeaders);
+            ExportMetricsServiceRequest request = normalizeAndEnrichMetricRequest(
+                    decodeMetricsRequest(normalizedContent, contentType), correlationContext);
+            signalItems = quotaService.countMetricItems(request);
+            quotaService.checkMetricItems("http", signalItems);
+            OtlpIngestionGovernanceService.Decision governanceDecision =
+                    governanceService.evaluateMetrics("http", request);
+            if (governanceDecision.dropped()) {
+                auditService.recordDropped("metrics", "http", correlationContext, requestBytes, signalItems,
+                        governanceDecision.reason(), durationMillis(startedAtNanos));
+                return emptySignalHttpSuccess(contentType, safeRequestHeaders.getAccept(), false);
+            }
+            ResponseEntity<byte[]> response = proxySignalHttp(
+                    safeContent, safeRequestHeaders, "/v1/otlp/v1/metrics", false, correlationContext);
             if (response.getStatusCode().is2xxSuccessful()) {
                 recordMetricIntake(request);
+                auditService.recordAccepted("metrics", "http", correlationContext, requestBytes, signalItems,
+                        durationMillis(startedAtNanos));
             }
             return response;
         } catch (StatusRuntimeException ex) {
-            HttpStatus status = switch (ex.getStatus().getCode()) {
-                case INVALID_ARGUMENT -> HttpStatus.BAD_REQUEST;
-                case UNAVAILABLE -> HttpStatus.SERVICE_UNAVAILABLE;
-                default -> HttpStatus.INTERNAL_SERVER_ERROR;
-            };
-            return errorResponse(contentType, status, ex.getStatus().getDescription());
+            auditService.recordRejected("metrics", "http", correlationContext, requestBytes, ex, signalItems,
+                    durationMillis(startedAtNanos));
+            return errorResponseFactory.httpErrorResponse(contentType, safeRequestHeaders.getAccept(), ex);
+        }
+    }
+
+    @Override
+    public ResponseEntity<byte[]> ingestLogsHttp(byte[] content, HttpHeaders requestHeaders) {
+        long startedAtNanos = System.nanoTime();
+        HttpHeaders safeRequestHeaders = safeHeaders(requestHeaders);
+        byte[] safeContent = safeContent(content);
+        MediaType contentType = safeRequestHeaders.getContentType();
+        OtlpCorrelationContext correlationContext = requestContextResolver.currentCorrelationContext();
+        long requestBytes = requestBytes(content);
+        Long signalItems = null;
+        try {
+            quotaService.checkRequestBytes("logs", "http", requestBytes);
+            byte[] normalizedContent = normalizeHttpContentForQuota("logs", "http", safeContent, safeRequestHeaders);
+            HttpHeaders normalizedHeaders = withoutContentEncoding(safeRequestHeaders);
+            byte[] enrichedContent = otlpCorrelationEnricher.enrichLogsHttp(
+                    normalizedContent, normalizedHeaders, correlationContext);
+            ExportLogsServiceRequest enrichedRequest = ExportLogsServiceRequest.parseFrom(enrichedContent);
+            ExportLogsServiceRequest resolvedRequest = otlpEntityIdentityResolver.enrichLogs(
+                    enrichedRequest, correlationContext.workspaceId());
+            ExportLogsServiceRequest redactedRequest = redactLogRequest(resolvedRequest);
+            byte[] redactedContent = redactedRequest.toByteArray();
+            signalItems = quotaService.countLogItems(redactedRequest);
+            quotaService.checkLogItems("http", signalItems);
+            OtlpIngestionGovernanceService.Decision governanceDecision =
+                    governanceService.evaluateLogs("http", redactedRequest);
+            if (governanceDecision.dropped()) {
+                auditService.recordDropped("logs", "http", correlationContext, requestBytes, signalItems,
+                        governanceDecision.reason(), durationMillis(startedAtNanos));
+                return emptyLogsHttpSuccess(contentType, safeRequestHeaders.getAccept());
+            }
+            ResponseEntity<byte[]> forwardResponse = greptimeOtlpForwarder.forwardLogsProtobuf(redactedContent);
+            if (forwardResponse == null) {
+                throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend returned no response.")
+                        .asRuntimeException();
+            }
+            if (!forwardResponse.getStatusCode().is2xxSuccessful()) {
+                throw backendStatusException(forwardResponse.getStatusCode(), forwardResponse.getHeaders());
+            }
+            ResponseEntity<byte[]> successResponse =
+                    logsHttpSuccess(contentType, safeRequestHeaders.getAccept(), forwardResponse.getBody());
+            publishRealtimeSignalsBestEffort(redactedRequest);
+            auditService.recordAccepted("logs", "http", correlationContext, requestBytes, signalItems,
+                    durationMillis(startedAtNanos));
+            return successResponse;
+        } catch (StatusRuntimeException ex) {
+            auditService.recordRejected("logs", "http", correlationContext, requestBytes, ex, signalItems,
+                    durationMillis(startedAtNanos));
+            return errorResponseFactory.httpErrorResponse(contentType, safeRequestHeaders.getAccept(), ex);
+        } catch (IllegalArgumentException ex) {
+            auditService.recordRejected("logs", "http", correlationContext, requestBytes,
+                    io.grpc.Status.Code.INVALID_ARGUMENT, defaultErrorMessage(ex), signalItems,
+                    durationMillis(startedAtNanos));
+            return errorResponseFactory.httpErrorResponse(
+                    contentType, safeRequestHeaders.getAccept(), HttpStatus.BAD_REQUEST, defaultErrorMessage(ex));
+        } catch (InvalidProtocolBufferException ex) {
+            auditService.recordRejected("logs", "http", correlationContext, requestBytes,
+                    io.grpc.Status.Code.INVALID_ARGUMENT, "Malformed OTLP logs payload.", signalItems,
+                    durationMillis(startedAtNanos));
+            return errorResponseFactory.httpErrorResponse(
+                    contentType, safeRequestHeaders.getAccept(), HttpStatus.BAD_REQUEST,
+                    "Malformed OTLP logs payload.");
+        } catch (Exception ex) {
+            log.error("Unexpected error ingesting OTLP HTTP logs: {}", ex.getMessage(), ex);
+            auditService.recordRejected("logs", "http", correlationContext, requestBytes,
+                    io.grpc.Status.Code.INTERNAL, defaultErrorMessage(ex), signalItems,
+                    durationMillis(startedAtNanos));
+            return errorResponseFactory.httpErrorResponse(
+                    contentType, safeRequestHeaders.getAccept(), HttpStatus.INTERNAL_SERVER_ERROR,
+                    defaultErrorMessage(ex));
         }
     }
 
     @Override
     public ResponseEntity<byte[]> ingestTracesHttp(byte[] content, HttpHeaders requestHeaders) {
-        MediaType contentType = requestHeaders.getContentType();
-        OtlpCorrelationContext correlationContext = OtlpCorrelationContext.empty();
+        long startedAtNanos = System.nanoTime();
+        HttpHeaders safeRequestHeaders = safeHeaders(requestHeaders);
+        byte[] safeContent = safeContent(content);
+        MediaType contentType = safeRequestHeaders.getContentType();
+        OtlpCorrelationContext correlationContext = requestContextResolver.currentCorrelationContext();
+        long requestBytes = requestBytes(content);
+        Long signalItems = null;
         try {
+            quotaService.checkRequestBytes("traces", "http", requestBytes);
+            byte[] normalizedContent = normalizeHttpContentForQuota("traces", "http", safeContent, safeRequestHeaders);
             ExportTraceServiceRequest request = normalizeAndEnrichTraceRequest(
-                    decodeTraceRequest(content, contentType, requestHeaders), correlationContext);
+                    decodeTraceRequest(normalizedContent, contentType), correlationContext);
+            signalItems = quotaService.countTraceItems(request);
+            quotaService.checkTraceItems("http", signalItems);
+            OtlpIngestionGovernanceService.Decision governanceDecision =
+                    governanceService.evaluateTraces("http", request);
+            if (governanceDecision.dropped()) {
+                auditService.recordDropped("traces", "http", correlationContext, requestBytes, signalItems,
+                        governanceDecision.reason(), durationMillis(startedAtNanos));
+                return emptySignalHttpSuccess(contentType, safeRequestHeaders.getAccept(), true);
+            }
             ResponseEntity<byte[]> response = proxySignalHttp(
-                    content, requestHeaders, "/v1/otlp/v1/traces", true, correlationContext);
+                    safeContent, safeRequestHeaders, "/v1/otlp/v1/traces", true, correlationContext);
             if (response.getStatusCode().is2xxSuccessful()) {
                 recordTraceIntake(request);
+                auditService.recordAccepted("traces", "http", correlationContext, requestBytes, signalItems,
+                        durationMillis(startedAtNanos));
             }
             return response;
         } catch (StatusRuntimeException ex) {
-            HttpStatus status = switch (ex.getStatus().getCode()) {
-                case INVALID_ARGUMENT -> HttpStatus.BAD_REQUEST;
-                case UNAVAILABLE -> HttpStatus.SERVICE_UNAVAILABLE;
-                default -> HttpStatus.INTERNAL_SERVER_ERROR;
-            };
-            return errorResponse(contentType, status, ex.getStatus().getDescription());
+            auditService.recordRejected("traces", "http", correlationContext, requestBytes, ex, signalItems,
+                    durationMillis(startedAtNanos));
+            return errorResponseFactory.httpErrorResponse(contentType, safeRequestHeaders.getAccept(), ex);
         }
     }
 
     @Override
     public ExportMetricsServiceResponse ingestMetricsGrpc(ExportMetricsServiceRequest request) {
-        byte[] response = proxySignalBinary(request.toByteArray(), "/v1/otlp/v1/metrics", false);
-        recordMetricIntake(request);
+        long startedAtNanos = System.nanoTime();
+        OtlpCorrelationContext correlationContext = requestContextResolver.currentCorrelationContext();
+        long requestBytes = requestBytes(request);
+        Long signalItems = null;
         try {
-            return response.length == 0 ? ExportMetricsServiceResponse.getDefaultInstance()
+            quotaService.checkRequestBytes("metrics", "grpc", requestBytes);
+            ExportMetricsServiceRequest redactedRequest = normalizeAndEnrichMetricRequest(request, correlationContext);
+            signalItems = quotaService.countMetricItems(redactedRequest);
+            quotaService.checkMetricItems("grpc", signalItems);
+            OtlpIngestionGovernanceService.Decision governanceDecision =
+                    governanceService.evaluateMetrics("grpc", redactedRequest);
+            if (governanceDecision.dropped()) {
+                auditService.recordDropped("metrics", "grpc", correlationContext, requestBytes, signalItems,
+                        governanceDecision.reason(), durationMillis(startedAtNanos));
+                return ExportMetricsServiceResponse.getDefaultInstance();
+            }
+            byte[] response = proxySignalBinary(redactedRequest.toByteArray(), "/v1/otlp/v1/metrics", false,
+                    correlationContext);
+            ExportMetricsServiceResponse parsedResponse = response.length == 0
+                    ? ExportMetricsServiceResponse.getDefaultInstance()
                     : ExportMetricsServiceResponse.parseFrom(response);
+            recordMetricIntake(redactedRequest);
+            auditService.recordAccepted("metrics", "grpc", correlationContext, requestBytes, signalItems,
+                    durationMillis(startedAtNanos));
+            return parsedResponse;
+        } catch (StatusRuntimeException ex) {
+            auditService.recordRejected("metrics", "grpc", correlationContext, requestBytes, ex, signalItems,
+                    durationMillis(startedAtNanos));
+            throw ex;
         } catch (InvalidProtocolBufferException e) {
+            auditService.recordRejected("metrics", "grpc", correlationContext, requestBytes,
+                    io.grpc.Status.Code.INTERNAL, "OTLP metrics response is malformed.", signalItems,
+                    durationMillis(startedAtNanos));
             throw io.grpc.Status.INTERNAL.withDescription("OTLP metrics response is malformed.").withCause(e)
                     .asRuntimeException();
         }
@@ -228,22 +420,58 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
 
     @Override
     public ExportLogsServiceResponse ingestLogsGrpc(ExportLogsServiceRequest request) {
+        long startedAtNanos = System.nanoTime();
+        OtlpCorrelationContext correlationContext = requestContextResolver.currentCorrelationContext();
+        long requestBytes = requestBytes(request);
+        Long signalItems = null;
         try {
+            quotaService.checkRequestBytes("logs", "grpc", requestBytes);
             ExportLogsServiceRequest enrichedRequest = otlpCorrelationEnricher.enrichLogs(
-                    request, OtlpCorrelationContext.empty());
-            byte[] response = greptimeOtlpForwarder.forwardLogsGrpc(enrichedRequest);
-            otlpLogProtocolAdapter.publishRealtimeSignals(enrichedRequest);
-            return response.length == 0 ? ExportLogsServiceResponse.getDefaultInstance()
+                    request, correlationContext);
+            ExportLogsServiceRequest resolvedRequest = otlpEntityIdentityResolver.enrichLogs(
+                    enrichedRequest, correlationContext.workspaceId());
+            ExportLogsServiceRequest redactedRequest = redactLogRequest(resolvedRequest);
+            signalItems = quotaService.countLogItems(redactedRequest);
+            quotaService.checkLogItems("grpc", signalItems);
+            OtlpIngestionGovernanceService.Decision governanceDecision =
+                    governanceService.evaluateLogs("grpc", redactedRequest);
+            if (governanceDecision.dropped()) {
+                auditService.recordDropped("logs", "grpc", correlationContext, requestBytes, signalItems,
+                        governanceDecision.reason(), durationMillis(startedAtNanos));
+                return ExportLogsServiceResponse.getDefaultInstance();
+            }
+            byte[] response = greptimeOtlpForwarder.forwardLogsGrpc(redactedRequest);
+            if (response == null) {
+                throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend returned no response.")
+                        .asRuntimeException();
+            }
+            ExportLogsServiceResponse parsedResponse = response.length == 0
+                    ? ExportLogsServiceResponse.getDefaultInstance()
                     : ExportLogsServiceResponse.parseFrom(response);
+            publishRealtimeSignalsBestEffort(redactedRequest);
+            auditService.recordAccepted("logs", "grpc", correlationContext, requestBytes, signalItems,
+                    durationMillis(startedAtNanos));
+            return parsedResponse;
         } catch (StatusRuntimeException ex) {
+            auditService.recordRejected("logs", "grpc", correlationContext, requestBytes, ex, signalItems,
+                    durationMillis(startedAtNanos));
             throw ex;
         } catch (InvalidProtocolBufferException ex) {
+            auditService.recordRejected("logs", "grpc", correlationContext, requestBytes,
+                    io.grpc.Status.Code.INTERNAL, "OTLP logs response is malformed.", signalItems,
+                    durationMillis(startedAtNanos));
             throw io.grpc.Status.INTERNAL.withDescription("OTLP logs response is malformed.").withCause(ex)
                     .asRuntimeException();
         } catch (IllegalArgumentException ex) {
+            auditService.recordRejected("logs", "grpc", correlationContext, requestBytes,
+                    io.grpc.Status.Code.INVALID_ARGUMENT, defaultErrorMessage(ex), signalItems,
+                    durationMillis(startedAtNanos));
             throw io.grpc.Status.INVALID_ARGUMENT.withDescription(defaultErrorMessage(ex)).withCause(ex)
                     .asRuntimeException();
         } catch (Exception ex) {
+            auditService.recordRejected("logs", "grpc", correlationContext, requestBytes,
+                    io.grpc.Status.Code.INTERNAL, defaultErrorMessage(ex), signalItems,
+                    durationMillis(startedAtNanos));
             throw io.grpc.Status.INTERNAL.withDescription(defaultErrorMessage(ex)).withCause(ex)
                     .asRuntimeException();
         }
@@ -251,14 +479,39 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
 
     @Override
     public ExportTraceServiceResponse ingestTracesGrpc(ExportTraceServiceRequest request) {
-        ExportTraceServiceRequest enrichedRequest = normalizeAndEnrichTraceRequest(
-                request, OtlpCorrelationContext.empty());
-        byte[] response = proxySignalBinary(enrichedRequest.toByteArray(), "/v1/otlp/v1/traces", true);
-        recordTraceIntake(enrichedRequest);
+        long startedAtNanos = System.nanoTime();
+        OtlpCorrelationContext correlationContext = requestContextResolver.currentCorrelationContext();
+        long requestBytes = requestBytes(request);
+        Long signalItems = null;
         try {
-            return response.length == 0 ? ExportTraceServiceResponse.getDefaultInstance()
+            quotaService.checkRequestBytes("traces", "grpc", requestBytes);
+            ExportTraceServiceRequest enrichedRequest = normalizeAndEnrichTraceRequest(request, correlationContext);
+            signalItems = quotaService.countTraceItems(enrichedRequest);
+            quotaService.checkTraceItems("grpc", signalItems);
+            OtlpIngestionGovernanceService.Decision governanceDecision =
+                    governanceService.evaluateTraces("grpc", enrichedRequest);
+            if (governanceDecision.dropped()) {
+                auditService.recordDropped("traces", "grpc", correlationContext, requestBytes, signalItems,
+                        governanceDecision.reason(), durationMillis(startedAtNanos));
+                return ExportTraceServiceResponse.getDefaultInstance();
+            }
+            byte[] response = proxySignalBinary(
+                    enrichedRequest.toByteArray(), "/v1/otlp/v1/traces", true, correlationContext);
+            ExportTraceServiceResponse parsedResponse = response.length == 0
+                    ? ExportTraceServiceResponse.getDefaultInstance()
                     : ExportTraceServiceResponse.parseFrom(response);
+            recordTraceIntake(enrichedRequest);
+            auditService.recordAccepted("traces", "grpc", correlationContext, requestBytes, signalItems,
+                    durationMillis(startedAtNanos));
+            return parsedResponse;
+        } catch (StatusRuntimeException ex) {
+            auditService.recordRejected("traces", "grpc", correlationContext, requestBytes, ex, signalItems,
+                    durationMillis(startedAtNanos));
+            throw ex;
         } catch (InvalidProtocolBufferException e) {
+            auditService.recordRejected("traces", "grpc", correlationContext, requestBytes,
+                    io.grpc.Status.Code.INTERNAL, "OTLP trace response is malformed.", signalItems,
+                    durationMillis(startedAtNanos));
             throw io.grpc.Status.INTERNAL.withDescription("OTLP trace response is malformed.").withCause(e)
                     .asRuntimeException();
         }
@@ -269,53 +522,155 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
         return List.of("metrics", "logs", "traces");
     }
 
-    private ExportMetricsServiceRequest decodeMetricsRequest(byte[] content, MediaType contentType, HttpHeaders headers) {
+    private ResponseEntity<byte[]> emptyLogsHttpSuccess(MediaType requestContentType, List<MediaType> acceptTypes) {
+        return logsHttpSuccess(requestContentType, acceptTypes,
+                ExportLogsServiceResponse.getDefaultInstance().toByteArray());
+    }
+
+    private ResponseEntity<byte[]> logsHttpSuccess(MediaType requestContentType, List<MediaType> acceptTypes,
+                                                   byte[] upstreamBody) {
+        MediaType responseContentType = resolveResponseContentType(requestContentType, acceptTypes);
+        byte[] protobufBody = upstreamBody == null || upstreamBody.length == 0
+                ? ExportLogsServiceResponse.getDefaultInstance().toByteArray()
+                : upstreamBody;
+        ExportLogsServiceResponse parsedResponse = parseLogsResponseBody(protobufBody);
+        if (MediaType.APPLICATION_JSON.includes(responseContentType)) {
+            return ResponseEntity.ok()
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .body(normalizeLogsResponseBodyForClient(parsedResponse));
+        }
+        return ResponseEntity.ok()
+                .contentType(MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF))
+                .body(protobufBody);
+    }
+
+    private ExportLogsServiceResponse parseLogsResponseBody(byte[] upstreamBody) {
+        byte[] safeBody = upstreamBody == null ? new byte[0] : upstreamBody;
+        try {
+            return safeBody.length == 0
+                    ? ExportLogsServiceResponse.getDefaultInstance()
+                    : ExportLogsServiceResponse.parseFrom(safeBody);
+        } catch (InvalidProtocolBufferException ex) {
+            throw io.grpc.Status.INTERNAL.withDescription("OTLP logs response is malformed.").withCause(ex)
+                    .asRuntimeException();
+        }
+    }
+
+    private byte[] normalizeLogsResponseBodyForClient(ExportLogsServiceResponse response) {
+        try {
+            return JsonFormat.printer().print(response).getBytes(StandardCharsets.UTF_8);
+        } catch (InvalidProtocolBufferException ex) {
+            throw io.grpc.Status.INTERNAL.withDescription("OTLP logs response is malformed.").withCause(ex)
+                    .asRuntimeException();
+        }
+    }
+
+    private ExportMetricsServiceRequest decodeMetricsRequest(byte[] content, MediaType contentType) {
         try {
-            byte[] normalizedContent = maybeDecompress(content, headers);
             if (contentType != null && MediaType.APPLICATION_JSON.includes(contentType)) {
                 ExportMetricsServiceRequest.Builder builder = ExportMetricsServiceRequest.newBuilder();
                 JsonFormat.parser().ignoringUnknownFields()
-                        .merge(normalizeOtlpJson(new String(normalizedContent, StandardCharsets.UTF_8)), builder);
+                        .merge(normalizeOtlpJson(new String(content, StandardCharsets.UTF_8)), builder);
                 return builder.build();
             }
-            return ExportMetricsServiceRequest.parseFrom(normalizedContent);
+            return ExportMetricsServiceRequest.parseFrom(content);
         } catch (InvalidProtocolBufferException ex) {
             throw io.grpc.Status.INVALID_ARGUMENT.withDescription("Malformed OTLP metrics payload.")
                     .withCause(ex).asRuntimeException();
         }
     }
 
-    private ExportTraceServiceRequest decodeTraceRequest(byte[] content, MediaType contentType, HttpHeaders headers) {
+    private ExportTraceServiceRequest decodeTraceRequest(byte[] content, MediaType contentType) {
         try {
-            byte[] normalizedContent = maybeDecompress(content, headers);
             if (contentType != null && MediaType.APPLICATION_JSON.includes(contentType)) {
                 ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder();
                 JsonFormat.parser().ignoringUnknownFields()
-                        .merge(normalizeOtlpJson(new String(normalizedContent, StandardCharsets.UTF_8)), builder);
+                        .merge(normalizeOtlpJson(new String(content, StandardCharsets.UTF_8)), builder);
                 return builder.build();
             }
-            return ExportTraceServiceRequest.parseFrom(normalizedContent);
+            return ExportTraceServiceRequest.parseFrom(content);
         } catch (InvalidProtocolBufferException ex) {
             throw io.grpc.Status.INVALID_ARGUMENT.withDescription("Malformed OTLP trace payload.")
                     .withCause(ex).asRuntimeException();
         }
     }
 
+    private byte[] normalizeHttpContentForQuota(String signal, String protocol, byte[] content, HttpHeaders headers) {
+        if (!isGzipEncoded(headers)) {
+            return content;
+        }
+        return decompressGzip(content, signal, protocol);
+    }
+
+    private HttpHeaders safeHeaders(HttpHeaders headers) {
+        return headers == null ? new HttpHeaders() : headers;
+    }
+
+    private byte[] safeContent(byte[] content) {
+        return content == null ? new byte[0] : content;
+    }
+
+    private HttpHeaders withoutContentEncoding(HttpHeaders headers) {
+        HttpHeaders normalizedHeaders = new HttpHeaders();
+        if (headers != null) {
+            normalizedHeaders.putAll(headers);
+            normalizedHeaders.remove(CONTENT_ENCODING);
+            normalizedHeaders.remove(HttpHeaders.CONTENT_ENCODING);
+        }
+        return normalizedHeaders;
+    }
+
+    private boolean isGzipEncoded(HttpHeaders headers) {
+        List<String> contentEncodings = headers == null ? null : headers.get(CONTENT_ENCODING);
+        return contentEncodings != null && contentEncodings.stream().anyMatch(this::isGzipContentEncoding);
+    }
+
     private byte[] maybeDecompress(byte[] content, HttpHeaders headers) {
         if (content == null || content.length == 0 || headers == null) {
             return content;
         }
-        String contentEncoding = headers.getFirst(CONTENT_ENCODING);
-        if (!StringUtils.equalsIgnoreCase(contentEncoding, CONTENT_ENCODING_GZIP)) {
+        if (!isGzipEncoded(headers)) {
+            return content;
+        }
+        return decompressGzip(content, null, null);
+    }
+
+    private boolean isGzipContentEncoding(String contentEncoding) {
+        String[] encodings = StringUtils.split(contentEncoding, ',');
+        if (encodings == null || encodings.length == 0) {
+            return false;
+        }
+        for (String encoding : encodings) {
+            if (StringUtils.equalsIgnoreCase(StringUtils.trimToEmpty(encoding), CONTENT_ENCODING_GZIP)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private byte[] decompressGzip(byte[] content, String signal, String protocol) {
+        if (content == null || content.length == 0) {
             return content;
         }
         try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
              GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
              ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
-            gzipInputStream.transferTo(outputStream);
+            byte[] buffer = new byte[GZIP_DECOMPRESSION_BUFFER_BYTES];
+            int read;
+            while ((read = gzipInputStream.read(buffer)) != -1) {
+                if (signal != null && protocol != null) {
+                    quotaService.checkRequestBytes(signal, protocol, (long) outputStream.size() + read);
+                }
+                outputStream.write(buffer, 0, read);
+            }
             return outputStream.toByteArray();
+        } catch (StatusRuntimeException ex) {
+            throw ex;
         } catch (Exception ex) {
-            throw io.grpc.Status.INVALID_ARGUMENT.withDescription("Malformed gzip-compressed OTLP payload.")
+            String description = StringUtils.isNotBlank(signal) && StringUtils.isNotBlank(protocol)
+                    ? "Malformed gzip-compressed OTLP " + signal + " " + protocol + " payload."
+                    : "Malformed gzip-compressed OTLP payload.";
+            throw io.grpc.Status.INVALID_ARGUMENT.withDescription(description)
                     .withCause(ex).asRuntimeException();
         }
     }
@@ -325,20 +680,26 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
             return;
         }
         for (ResourceMetrics resourceMetrics : request.getResourceMetricsList()) {
-            Map<String, String> resourceAttributes = toStringMap(resourceMetrics.getResource().getAttributesList());
+            Map<String, String> resourceAttributes = requestContextResolver.withWorkspaceResourceAttributes(
+                    toStringMap(resourceMetrics.getResource().getAttributesList()));
             for (ScopeMetrics scopeMetrics : resourceMetrics.getScopeMetricsList()) {
                 for (Metric metric : scopeMetrics.getMetricsList()) {
                     List<MetricObservation> observations = extractMetricObservations(metric);
                     for (MetricObservation observation : observations) {
-                        observabilitySignalIntakeGateway.recordOtlpMetricIntake(
-                                resourceAttributes,
-                                observation.observedAt(),
-                                StringUtils.trimToNull(metric.getName()),
-                                observation.metricType(),
-                                StringUtils.trimToNull(metric.getUnit()),
-                                observation.value(),
-                                observation.attributes()
-                        );
+                        try {
+                            observabilitySignalIntakeGateway.recordOtlpMetricIntake(
+                                    resourceAttributes,
+                                    observation.observedAt(),
+                                    StringUtils.trimToNull(metric.getName()),
+                                    observation.metricType(),
+                                    StringUtils.trimToNull(metric.getUnit()),
+                                    observation.value(),
+                                    observation.attributes()
+                            );
+                        } catch (RuntimeException ex) {
+                            log.warn("Failed to record OTLP metric intake read model for metric {}: {}",
+                                    StringUtils.trimToNull(metric.getName()), ex.toString());
+                        }
                     }
                 }
             }
@@ -350,59 +711,114 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
             return;
         }
         for (ResourceSpans resourceSpans : request.getResourceSpansList()) {
-            Map<String, String> resourceAttributes = toStringMap(resourceSpans.getResource().getAttributesList());
+            Map<String, String> resourceAttributes = requestContextResolver.withWorkspaceResourceAttributes(
+                    toStringMap(resourceSpans.getResource().getAttributesList()));
             for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) {
                 for (Span span : scopeSpans.getSpansList()) {
-                    observabilitySignalIntakeGateway.recordOtlpTraceIntake(
-                            resourceAttributes,
-                            nanosToMillis(span.getStartTimeUnixNano()),
-                            HexFormat.of().formatHex(span.getTraceId().toByteArray()),
-                            HexFormat.of().formatHex(span.getSpanId().toByteArray()),
-                            StringUtils.trimToNull(span.getName()),
-                            span.getStatus().getCode().name().toLowerCase(Locale.ROOT),
-                            toStringMap(span.getAttributesList())
-                    );
+                    if (!hasValidTraceIdentifiers(span)) {
+                        continue;
+                    }
+                    String traceId = HexFormat.of().formatHex(span.getTraceId().toByteArray());
+                    String spanId = HexFormat.of().formatHex(span.getSpanId().toByteArray());
+                    try {
+                        observabilitySignalIntakeGateway.recordOtlpTraceIntake(
+                                resourceAttributes,
+                                nanosToMillis(span.getStartTimeUnixNano()),
+                                traceId,
+                                spanId,
+                                StringUtils.trimToNull(span.getName()),
+                                span.getStatus().getCode().name().toLowerCase(Locale.ROOT),
+                                toStringMap(span.getAttributesList())
+                        );
+                    } catch (RuntimeException ex) {
+                        log.warn("Failed to record OTLP trace intake read model for trace {} span {}: {}",
+                                traceId, spanId, ex.toString());
+                    }
                 }
             }
         }
     }
 
+    private boolean hasValidTraceIdentifiers(Span span) {
+        return span != null
+                && span.getTraceId().size() == OTLP_TRACE_ID_BYTES
+                && span.getSpanId().size() == OTLP_SPAN_ID_BYTES
+                && !isAllZero(span.getTraceId().toByteArray())
+                && !isAllZero(span.getSpanId().toByteArray());
+    }
+
+    private boolean isAllZero(byte[] value) {
+        if (value == null || value.length == 0) {
+            return true;
+        }
+        for (byte item : value) {
+            if (item != 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void publishRealtimeSignalsBestEffort(ExportLogsServiceRequest request) {
+        try {
+            otlpLogProtocolAdapter.publishRealtimeSignals(request);
+        } catch (RuntimeException ex) {
+            log.warn("Failed to publish OTLP log realtime signals: {}", ex.toString());
+        }
+    }
+
     private ResponseEntity<byte[]> proxySignalHttp(byte[] content, HttpHeaders requestHeaders, String path,
                                                    boolean traceSignal) {
-        return proxySignalHttp(content, requestHeaders, path, traceSignal, OtlpCorrelationContext.empty());
+        return proxySignalHttp(
+                content, requestHeaders, path, traceSignal, requestContextResolver.currentCorrelationContext());
     }
 
     private ResponseEntity<byte[]> proxySignalHttp(byte[] content, HttpHeaders requestHeaders, String path,
                                                    boolean traceSignal, OtlpCorrelationContext correlationContext) {
         MediaType contentType = requestHeaders.getContentType();
-        try {
-            byte[] responseBody = proxySignalInternal(content, path, traceSignal, contentType,
-                    requestHeaders.getAccept(), requestHeaders, correlationContext);
-            return ResponseEntity.ok()
-                    .contentType(resolveResponseContentType(contentType, requestHeaders.getAccept()))
-                    .body(responseBody);
-        } catch (StatusRuntimeException ex) {
-            HttpStatus status = switch (ex.getStatus().getCode()) {
-                case INVALID_ARGUMENT -> HttpStatus.BAD_REQUEST;
-                case UNAVAILABLE -> HttpStatus.SERVICE_UNAVAILABLE;
-                default -> HttpStatus.INTERNAL_SERVER_ERROR;
-            };
-            return errorResponse(contentType, status, ex.getStatus().getDescription());
-        }
+        byte[] responseBody = proxySignalInternal(content, path, traceSignal, contentType,
+                requestHeaders.getAccept(), requestHeaders, correlationContext);
+        return ResponseEntity.ok()
+                .contentType(resolveResponseContentType(contentType, requestHeaders.getAccept()))
+                .body(responseBody);
+    }
+
+    private ResponseEntity<byte[]> emptySignalHttpSuccess(MediaType requestContentType, List<MediaType> acceptTypes,
+                                                         boolean traceSignal) {
+        MediaType responseContentType = resolveResponseContentType(requestContentType, acceptTypes);
+        byte[] responseBody = emptySignalResponseBody(responseContentType, traceSignal);
+        return ResponseEntity.ok()
+                .contentType(responseContentType)
+                .body(responseBody);
+    }
+
+    private byte[] emptySignalResponseBody(MediaType responseContentType, boolean traceSignal) {
+        byte[] protobufBody = traceSignal
+                ? ExportTraceServiceResponse.getDefaultInstance().toByteArray()
+                : ExportMetricsServiceResponse.getDefaultInstance().toByteArray();
+        if (responseContentType != null && MediaType.APPLICATION_JSON.includes(responseContentType)) {
+            return normalizeResponseBodyForClient(protobufBody, traceSignal);
+        }
+        return protobufBody;
     }
 
     private byte[] proxySignalBinary(byte[] content, String path, boolean traceSignal) {
+        return proxySignalBinary(content, path, traceSignal, requestContextResolver.currentCorrelationContext());
+    }
+
+    private byte[] proxySignalBinary(byte[] content, String path, boolean traceSignal,
+                                     OtlpCorrelationContext correlationContext) {
         return proxySignalInternal(content, path, traceSignal,
                 MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF),
                 List.of(MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF)),
                 null,
-                OtlpCorrelationContext.empty());
+                correlationContext);
     }
 
     private byte[] proxySignalInternal(byte[] content, String path, boolean traceSignal,
                                        MediaType contentType, List<MediaType> acceptTypes,
                                        HttpHeaders requestHeaders, OtlpCorrelationContext correlationContext) {
-        GreptimeProperties greptimeProperties = greptimePropertiesProvider.getIfAvailable();
+        GreptimeProperties greptimeProperties = greptimePropertiesOrUnavailable();
         if (greptimeProperties == null || !greptimeProperties.enabled()
                 || !StringUtils.isNotBlank(greptimeProperties.httpEndpoint())) {
             throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend is not configured.")
@@ -413,16 +829,16 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
             byte[] upstreamContent = normalizeRequestBodyForUpstream(
                     content, contentType, traceSignal, requestHeaders, correlationContext);
             HttpHeaders upstreamHeaders = new HttpHeaders();
-            if (contentType != null) {
-                upstreamHeaders.setContentType(resolveUpstreamContentType(contentType));
-            }
+            upstreamHeaders.setContentType(resolveUpstreamContentType(contentType));
             if (clientExpectsJson) {
                 upstreamHeaders.setAccept(List.of(MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF)));
-            } else if (acceptTypes != null && !acceptTypes.isEmpty()) {
-                upstreamHeaders.setAccept(acceptTypes);
+            } else {
+                List<MediaType> upstreamAcceptTypes = resolveUpstreamAcceptTypes(acceptTypes);
+                if (!upstreamAcceptTypes.isEmpty()) {
+                    upstreamHeaders.setAccept(upstreamAcceptTypes);
+                }
             }
-            upstreamHeaders.set(GREPTIME_DB_NAME_HEADER,
-                    StringUtils.defaultIfBlank(greptimeProperties.database(), DEFAULT_GREPTIME_DB_NAME));
+            upstreamHeaders.set(GREPTIME_DB_NAME_HEADER, database(greptimeProperties.database()));
             if (traceSignal) {
                 upstreamHeaders.set(GREPTIME_TRACE_TABLE_NAME_HEADER, DEFAULT_TRACES_TABLE_NAME);
                 upstreamHeaders.set(GREPTIME_PIPELINE_NAME_HEADER, DEFAULT_TRACE_PIPELINE);
@@ -437,21 +853,29 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
             }
             addAuthenticationHeader(upstreamHeaders, greptimeProperties);
 
-            ResponseEntity<byte[]> response = restTemplate.exchange(
-                    greptimeProperties.httpEndpoint() + path,
+            ResponseEntity<byte[]> response = retryService.execute(() -> restTemplate.exchange(
+                    endpoint(greptimeProperties.httpEndpoint(), path),
                     HttpMethod.POST,
                     new HttpEntity<>(upstreamContent, upstreamHeaders),
                     byte[].class
-            );
-            if (!response.getStatusCode().is2xxSuccessful()) {
-                throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend returned " + response.getStatusCode())
+            ), retryableResponse -> retryableResponse == null
+                    || retryService.isRetryableStatus(retryableResponse.getStatusCode()));
+            if (response == null) {
+                throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend returned no response.")
                         .asRuntimeException();
             }
+            if (!response.getStatusCode().is2xxSuccessful()) {
+                throw backendStatusException(response.getStatusCode(), response.getHeaders());
+            }
             byte[] responseBody = response.getBody() == null ? new byte[0] : response.getBody();
+            validateSignalResponseBody(responseBody, traceSignal);
             if (!clientExpectsJson) {
                 return responseBody;
             }
             return normalizeResponseBodyForClient(responseBody, traceSignal);
+        } catch (HttpStatusCodeException ex) {
+            log.error("Failed to proxy OTLP signal {}: {}", traceSignal ? "traces" : "metrics", ex.getMessage(), ex);
+            throw backendStatusException(ex.getStatusCode(), ex.getResponseHeaders());
         } catch (RestClientException ex) {
             log.error("Failed to proxy OTLP signal {}: {}", traceSignal ? "traces" : "metrics", ex.getMessage(), ex);
             throw io.grpc.Status.UNAVAILABLE.withDescription(defaultErrorMessage(ex)).withCause(ex)
@@ -459,19 +883,32 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
         }
     }
 
+    private GreptimeProperties greptimePropertiesOrUnavailable() {
+        try {
+            return greptimePropertiesProvider.getIfAvailable();
+        } catch (RuntimeException ex) {
+            log.warn("Failed to resolve Greptime OTLP backend properties: {}", ex.toString());
+            throw io.grpc.Status.UNAVAILABLE.withDescription("OTLP backend is not configured.")
+                    .withCause(ex)
+                    .asRuntimeException();
+        }
+    }
+
     private byte[] normalizeRequestBodyForUpstream(byte[] content, MediaType contentType, boolean traceSignal,
                                                    HttpHeaders requestHeaders,
                                                    OtlpCorrelationContext correlationContext) {
         byte[] normalizedContent = maybeDecompress(content, requestHeaders);
         if (contentType == null || !MediaType.APPLICATION_JSON.includes(contentType)) {
-            if (!traceSignal) {
-                return normalizedContent;
-            }
             try {
+                if (!traceSignal) {
+                    return normalizeAndEnrichMetricRequest(
+                            ExportMetricsServiceRequest.parseFrom(normalizedContent), correlationContext).toByteArray();
+                }
                 return normalizeAndEnrichTraceRequest(
                         ExportTraceServiceRequest.parseFrom(normalizedContent), correlationContext).toByteArray();
             } catch (InvalidProtocolBufferException ex) {
-                throw io.grpc.Status.INVALID_ARGUMENT.withDescription("Malformed OTLP trace payload.")
+                throw io.grpc.Status.INVALID_ARGUMENT.withDescription(
+                                traceSignal ? "Malformed OTLP trace payload." : "Malformed OTLP metrics payload.")
                         .withCause(ex).asRuntimeException();
             }
         }
@@ -485,100 +922,589 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService {
             ExportMetricsServiceRequest.Builder builder = ExportMetricsServiceRequest.newBuilder();
             JsonFormat.parser().ignoringUnknownFields()
                     .merge(normalizeOtlpJson(new String(normalizedContent, StandardCharsets.UTF_8)), builder);
-            return builder.build().toByteArray();
+            return normalizeAndEnrichMetricRequest(builder.build(), correlationContext).toByteArray();
         } catch (InvalidProtocolBufferException ex) {
             throw io.grpc.Status.INVALID_ARGUMENT.withDescription("Malformed OTLP JSON request.")
                     .withCause(ex).asRuntimeException();
         }
     }
 
+    private ExportMetricsServiceRequest normalizeAndEnrichMetricRequest(ExportMetricsServiceRequest request,
+                                                                        OtlpCorrelationContext correlationContext) {
+        OtlpCorrelationContext safeContext = correlationContext == null
+                ? OtlpCorrelationContext.empty()
+                : correlationContext;
+        ExportMetricsServiceRequest resolved =
+                otlpEntityIdentityResolver.enrichMetrics(request, safeContext.workspaceId());
+        return redactMetricRequest(otlpCorrelationEnricher.enrichMetrics(resolved, safeContext));
+    }
+
     private ExportTraceServiceRequest normalizeAndEnrichTraceRequest(ExportTraceServiceRequest request,
                                                                      OtlpCorrelationContext correlationContext) {
+        OtlpCorrelationContext safeContext = correlationContext == null
+                ? OtlpCorrelationContext.empty()
+                : correlationContext;
         ExportTraceServiceRequest normalized = normalizeTraceRequest(request);
-        return otlpCorrelationEnricher.enrichTraces(normalized, correlationContext);
+        ExportTraceServiceRequest resolved =
+                otlpEntityIdentityResolver.enrichTraces(normalized, safeContext.workspaceId());
+        return redactTraceRequest(otlpCorrelationEnricher.enrichTraces(resolved, safeContext));
+    }
+
+    private ExportMetricsServiceRequest redactMetricRequest(ExportMetricsServiceRequest request) {
+        if (request == null || request.getResourceMetricsCount() == 0) {
+            return request;
+        }
+        ExportMetricsServiceRequest.Builder requestBuilder = request.toBuilder().clearResourceMetrics();
+        for (ResourceMetrics resourceMetrics : request.getResourceMetricsList()) {
+            ResourceMetrics.Builder resourceBuilder = resourceMetrics.toBuilder().clearScopeMetrics()
+                    .setResource(resourceMetrics.getResource().toBuilder().clearAttributes()
+                            .addAllAttributes(redactAttributes(resourceMetrics.getResource().getAttributesList()))
+                            .build());
+            for (ScopeMetrics scopeMetrics : resourceMetrics.getScopeMetricsList()) {
+                ScopeMetrics.Builder scopeBuilder = scopeMetrics.toBuilder().clearMetrics()
+                        .setScope(redactInstrumentationScope(scopeMetrics.getScope()));
+                for (Metric metric : scopeMetrics.getMetricsList()) {
+                    scopeBuilder.addMetrics(redactMetric(metric));
+                }
+                resourceBuilder.addScopeMetrics(scopeBuilder.build());
+            }
+            requestBuilder.addResourceMetrics(resourceBuilder.build());
+        }
+        return requestBuilder.build();
+    }
+
+    private Metric redactMetric(Metric metric) {
+        if (metric == null) {
+            return null;
+        }
+        Metric.Builder metricBuilder = metric.toBuilder();
+        if (metric.hasGauge()) {
+            Gauge.Builder builder = metric.getGauge().toBuilder().clearDataPoints();
+            for (NumberDataPoint dataPoint : metric.getGauge().getDataPointsList()) {
+                builder.addDataPoints(redactNumberDataPoint(dataPoint));
+            }
+            metricBuilder.setGauge(builder.build());
+        }
+        if (metric.hasSum()) {
+            Sum.Builder builder = metric.getSum().toBuilder().clearDataPoints();
+            for (NumberDataPoint dataPoint : metric.getSum().getDataPointsList()) {
+                builder.addDataPoints(redactNumberDataPoint(dataPoint));
+            }
+            metricBuilder.setSum(builder.build());
+        }
+        if (metric.hasHistogram()) {
+            Histogram.Builder builder = metric.getHistogram().toBuilder().clearDataPoints();
+            for (HistogramDataPoint dataPoint : metric.getHistogram().getDataPointsList()) {
+                builder.addDataPoints(redactHistogramDataPoint(dataPoint));
+            }
+            metricBuilder.setHistogram(builder.build());
+        }
+        if (metric.hasExponentialHistogram()) {
+            ExponentialHistogram.Builder builder = metric.getExponentialHistogram().toBuilder().clearDataPoints();
+            for (ExponentialHistogramDataPoint dataPoint : metric.getExponentialHistogram().getDataPointsList()) {
+                builder.addDataPoints(redactExponentialHistogramDataPoint(dataPoint));
+            }
+            metricBuilder.setExponentialHistogram(builder.build());
+        }
+        if (metric.hasSummary()) {
+            Summary.Builder builder = metric.getSummary().toBuilder().clearDataPoints();
+            for (SummaryDataPoint dataPoint : metric.getSummary().getDataPointsList()) {
+                builder.addDataPoints(redactSummaryDataPoint(dataPoint));
+            }
+            metricBuilder.setSummary(builder.build());
+        }
+        return metricBuilder.build();
+    }
+
+    private NumberDataPoint redactNumberDataPoint(NumberDataPoint dataPoint) {
+        NumberDataPoint.Builder builder = dataPoint.toBuilder()
+                .clearAttributes()
+                .clearExemplars()
+                .addAllAttributes(redactAttributes(dataPoint.getAttributesList()));
+        for (Exemplar exemplar : dataPoint.getExemplarsList()) {
+            builder.addExemplars(redactExemplar(exemplar));
+        }
+        return builder.build();
+    }
+
+    private HistogramDataPoint redactHistogramDataPoint(HistogramDataPoint dataPoint) {
+        HistogramDataPoint.Builder builder = dataPoint.toBuilder()
+                .clearAttributes()
+                .clearExemplars()
+                .addAllAttributes(redactAttributes(dataPoint.getAttributesList()));
+        for (Exemplar exemplar : dataPoint.getExemplarsList()) {
+            builder.addExemplars(redactExemplar(exemplar));
+        }
+        return builder.build();
+    }
+
+    private ExponentialHistogramDataPoint redactExponentialHistogramDataPoint(
+            ExponentialHistogramDataPoint dataPoint) {
+        ExponentialHistogramDataPoint.Builder builder = dataPoint.toBuilder()
+                .clearAttributes()
+                .clearExemplars()
+                .addAllAttributes(redactAttributes(dataPoint.getAttributesList()));
+        for (Exemplar exemplar : dataPoint.getExemplarsList()) {
+            builder.addExemplars(redactExemplar(exemplar));
+        }
+        return builder.build();
+    }
+
+    private SummaryDataPoint redactSummaryDataPoint(SummaryDataPoint dataPoint) {
+        return dataPoint.toBuilder().clearAttributes()
+                .addAllAttributes(redactAttributes(dataPoint.getAttributesList()))
+                .build();
+    }
+
+    private Exemplar redactExemplar(Exemplar exemplar) {
+        if (exemplar == null) {
+            return null;
+        }
+        return exemplar.toBuilder()
+                .clearFilteredAttributes()
+                .addAllFilteredAttributes(redactAttributes(exemplar.getFilteredAttributesList()))
+                .build();
+    }
+
+    private ExportLogsServiceRequest redactLogRequest(ExportLogsServiceRequest request) {
+        if (request == null || request.getResourceLogsCount() == 0) {
+            return request;
+        }
+        ExportLogsServiceRequest.Builder requestBuilder = request.toBuilder().clearResourceLogs();
+        for (ResourceLogs resourceLogs : request.getResourceLogsList()) {
+            ResourceLogs.Builder resourceBuilder = resourceLogs.toBuilder().clearScopeLogs()
+                    .setResource(resourceLogs.getResource().toBuilder().clearAttributes()
+                            .addAllAttributes(redactAttributes(resourceLogs.getResource().getAttributesList()))
+                            .build());
+            for (ScopeLogs scopeLogs : resourceLogs.getScopeLogsList()) {
+                ScopeLogs.Builder scopeBuilder = scopeLogs.toBuilder().clearLogRecords()
+                        .setScope(redactInstrumentationScope(scopeLogs.getScope()));
+                for (LogRecord logRecord : scopeLogs.getLogRecordsList()) {
+                    scopeBuilder.addLogRecords(redactLogRecord(logRecord));
+                }
+                resourceBuilder.addScopeLogs(scopeBuilder.build());
+            }
+            requestBuilder.addResourceLogs(resourceBuilder.build());
+        }
+        return requestBuilder.build();
+    }
+
+    private LogRecord redactLogRecord(LogRecord logRecord) {
+        if (logRecord == null) {
+            return null;
+        }
+        return logRecord.toBuilder()
+                .setBody(redactAnyValue(logRecord.getBody()))
+                .clearAttributes()
+                .addAllAttributes(redactAttributes(logRecord.getAttributesList()))
+                .build();
+    }
+
+    private ExportTraceServiceRequest redactTraceRequest(ExportTraceServiceRequest request) {
+        if (request == null || request.getResourceSpansCount() == 0) {
+            return request;
+        }
+        ExportTraceServiceRequest.Builder requestBuilder = request.toBuilder().clearResourceSpans();
+        for (ResourceSpans resourceSpans : request.getResourceSpansList()) {
+            ResourceSpans.Builder resourceBuilder = resourceSpans.toBuilder().clearScopeSpans()
+                    .setResource(resourceSpans.getResource().toBuilder().clearAttributes()
+                            .addAllAttributes(redactAttributes(resourceSpans.getResource().getAttributesList()))
+                            .build());
+            for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) {
+                ScopeSpans.Builder scopeBuilder = scopeSpans.toBuilder().clearSpans()
+                        .setScope(redactInstrumentationScope(scopeSpans.getScope()));
+                for (Span span : scopeSpans.getSpansList()) {
+                    scopeBuilder.addSpans(redactSpan(span));
+                }
+                resourceBuilder.addScopeSpans(scopeBuilder.build());
+            }
+            requestBuilder.addResourceSpans(resourceBuilder.build());
+        }
+        return requestBuilder.build();
+    }
+
+    private Span redactSpan(Span span) {
+        if (span == null) {
+            return null;
+        }
+        Span.Builder spanBuilder = span.toBuilder()
+                .setName(redactionService.redactText(span.getName()))
+                .clearAttributes()
+                .clearEvents()
+                .clearLinks();
+        spanBuilder.addAllAttributes(redactAttributes(span.getAttributesList()));
+        if (span.hasStatus()) {
+            spanBuilder.setStatus(redactSpanStatus(span.getStatus()));
+        }
+        for (Span.Event event : span.getEventsList()) {
+            spanBuilder.addEvents(redactSpanEvent(event));
+        }
+        for (Span.Link link : span.getLinksList()) {
+            spanBuilder.addLinks(redactSpanLink(link));
+        }
+        return spanBuilder.build();
+    }
+
+    private io.opentelemetry.proto.trace.v1.Status redactSpanStatus(
+            io.opentelemetry.proto.trace.v1.Status status) {
+        if (status == null) {
+            return null;
+        }
+        return status.toBuilder()
+                .setMessage(redactionService.redactText(status.getMessage()))
+                .build();
+    }
+
+    private Span.Event redactSpanEvent(Span.Event event) {
+        if (event == null) {
+            return null;
+        }
+        return event.toBuilder()
+                .setName(redactionService.redactText(event.getName()))
+                .clearAttributes()
+                .addAllAttributes(redactAttributes(event.getAttributesList()))
+                .build();
+    }
+
+    private Span.Link redactSpanLink(Span.Link link) {
+        if (link == null) {
+            return null;
+        }
+        return link.toBuilder()
+                .clearAttributes()
+                .addAllAttributes(redactAttributes(link.getAttributesList()))
+                .build();
+    }
+
+    private InstrumentationScope redactInstrumentationScope(InstrumentationScope scope) {
+        if (scope == null || scope.getAttributesCount() == 0) {
+            return scope;
+        }
+        return scope.toBuilder().clearAttributes()
+                .addAllAttributes(redactAttributes(scope.getAttributesList()))
+                .build();
+    }
+
+    private List<KeyValue> redactAttributes(List<KeyValue> attributes) {
+        if (attributes == null || attributes.isEmpty()) {
+            return List.of();
+        }
+        List<KeyValue> redacted = new ArrayList<>(attributes.size());
+        for (KeyValue attribute : attributes) {
+            redacted.add(redactAttribute(attribute));
+        }
+        return redacted;
+    }
+
+    private KeyValue redactAttribute(KeyValue attribute) {
+        if (attribute == null || !StringUtils.isNotBlank(attribute.getKey())) {
+            return attribute;
+        }
+        if (redactionService.isSensitiveKey(attribute.getKey())) {
+            return attribute.toBuilder().setValue(redactedAnyValue()).build();
+        }
+        if (!attribute.hasValue()) {
+            return attribute;
+        }
+        return attribute.toBuilder().setValue(redactAnyValue(attribute.getValue())).build();
+    }
+
+    private AnyValue redactAnyValue(AnyValue value) {
+        if (value == null) {
+            return null;
+        }
+        return switch (value.getValueCase()) {
+            case STRING_VALUE -> value.toBuilder()
+                    .setStringValue(redactionService.redactText(value.getStringValue()))
+                    .build();
+            case ARRAY_VALUE -> value.toBuilder()
+                    .setArrayValue(redactArrayValue(value.getArrayValue()))
+                    .build();
+            case KVLIST_VALUE -> value.toBuilder()
+                    .setKvlistValue(redactKeyValueList(value.getKvlistValue()))
+                    .build();
+            default -> value;
+        };
+    }
+
+    private ArrayValue redactArrayValue(ArrayValue value) {
+        ArrayValue.Builder builder = value.toBuilder().clearValues();
+        for (AnyValue item : value.getValuesList()) {
+            builder.addValues(redactAnyValue(item));
+        }
+        return builder.build();
+    }
+
+    private KeyValueList redactKeyValueList(KeyValueList value) {
+        KeyValueList.Builder builder = value.toBuilder().clearValues();
+        for (KeyValue item : value.getValuesList()) {
+            builder.addValues(redactAttribute(item));
+        }
+        return builder.build();
+    }
+
+    private AnyValue redactedAnyValue() {
+        return AnyValue.newBuilder().setStringValue(OtlpIngestionRedactionService.REDACTED).build();
     }
 
     private MediaType resolveUpstreamContentType(MediaType contentType) {
-        if (MediaType.APPLICATION_JSON.includes(contentType)) {
-            return MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF);
+        if (contentType == null) {
+            return MEDIA_TYPE_PROTOBUF;
+        }
+        if (MediaType.APPLICATION_JSON.includes(contentType) || isExplicitProtobufMediaType(contentType)) {
+            return MEDIA_TYPE_PROTOBUF;
         }
         return contentType;
     }
 
-    private boolean acceptsJson(MediaType requestContentType, List<MediaType> acceptTypes) {
-        if (acceptTypes != null && acceptTypes.stream().anyMatch(mediaType -> mediaType.includes(MediaType.APPLICATION_JSON))) {
-            return true;
+    private List<MediaType> resolveUpstreamAcceptTypes(List<MediaType> acceptTypes) {
+        if (acceptTypes == null || acceptTypes.isEmpty()) {
+            return List.of();
         }
-        return requestContentType != null && MediaType.APPLICATION_JSON.includes(requestContentType);
+        return acceptTypes.stream()
+                .map(this::resolveUpstreamAcceptType)
+                .toList();
+    }
+
+    private MediaType resolveUpstreamAcceptType(MediaType acceptType) {
+        if (isExplicitProtobufMediaType(acceptType)) {
+            return new MediaType(MEDIA_TYPE_PROTOBUF.getType(), MEDIA_TYPE_PROTOBUF.getSubtype(),
+                    acceptType.getParameters());
+        }
+        return acceptType;
+    }
+
+    private boolean acceptsJson(MediaType requestContentType, List<MediaType> acceptTypes) {
+        return prefersJsonResponse(requestContentType, acceptTypes);
     }
 
     private byte[] normalizeResponseBodyForClient(byte[] upstreamBody, boolean traceSignal) {
         try {
-            Message response;
+            return JsonFormat.printer().print(parseSignalResponse(upstreamBody, traceSignal))
+                    .getBytes(StandardCharsets.UTF_8);
+        } catch (InvalidProtocolBufferException ex) {
+            throw io.grpc.Status.INTERNAL.withDescription(signalResponseMalformedReason(traceSignal))
+                    .withCause(ex).asRuntimeException();
+        }
+    }
+
+    private void validateSignalResponseBody(byte[] upstreamBody, boolean traceSignal) {
+        parseSignalResponse(upstreamBody, traceSignal);
+    }
+
+    private Message parseSignalResponse(byte[] upstreamBody, boolean traceSignal) {
+        byte[] safeBody = upstreamBody == null ? new byte[0] : upstreamBody;
+        try {
             if (traceSignal) {
-                response = upstreamBody.length == 0
+                return safeBody.length == 0
                         ? ExportTraceServiceResponse.getDefaultInstance()
-                        : ExportTraceServiceResponse.parseFrom(upstreamBody);
-            } else {
-                response = upstreamBody.length == 0
-                        ? ExportMetricsServiceResponse.getDefaultInstance()
-                        : ExportMetricsServiceResponse.parseFrom(upstreamBody);
+                        : ExportTraceServiceResponse.parseFrom(safeBody);
             }
-            return JsonFormat.printer().print(response).getBytes(StandardCharsets.UTF_8);
+            return safeBody.length == 0
+                    ? ExportMetricsServiceResponse.getDefaultInstance()
+                    : ExportMetricsServiceResponse.parseFrom(safeBody);
         } catch (InvalidProtocolBufferException ex) {
-            throw io.grpc.Status.INTERNAL.withDescription("OTLP backend response is malformed.")
+            throw io.grpc.Status.INTERNAL.withDescription(signalResponseMalformedReason(traceSignal))
                     .withCause(ex).asRuntimeException();
         }
     }
 
+    private String signalResponseMalformedReason(boolean traceSignal) {
+        return traceSignal ? "OTLP trace response is malformed." : "OTLP metrics response is malformed.";
+    }
+
     private void addAuthenticationHeader(HttpHeaders headers, GreptimeProperties greptimeProperties) {
-        if (StringUtils.isBlank(greptimeProperties.username()) || StringUtils.isBlank(greptimeProperties.password())) {
+        String username = StringUtils.trimToNull(greptimeProperties.username());
+        String password = StringUtils.trimToNull(greptimeProperties.password());
+        if (username == null || password == null) {
             return;
         }
-        String credentials = greptimeProperties.username() + ":" + greptimeProperties.password();
+        String credentials = username + ":" + password;
         String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
         headers.set(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials);
     }
 
-    private MediaType resolveResponseContentType(MediaType requestContentType, List<MediaType> acceptTypes) {
-        if (acceptTypes != null && acceptTypes.stream().anyMatch(mediaType -> mediaType.includes(MediaType.APPLICATION_JSON))) {
-            return MediaType.APPLICATION_JSON;
+    private String endpoint(String baseEndpoint, String path) {
+        return StringUtils.stripEnd(StringUtils.trim(baseEndpoint), "/") + path;
+    }
+
+    private StatusRuntimeException backendStatusException(HttpStatusCode statusCode) {
+        return backendStatusException(statusCode, null);
+    }
+
+    private StatusRuntimeException backendStatusException(HttpStatusCode statusCode, HttpHeaders responseHeaders) {
+        io.grpc.Status status = backendGrpcStatus(statusCode);
+        return OtlpIngestionBackpressureHeaders.statusRuntimeException(
+                status, "OTLP backend returned " + statusCode, responseHeaders);
+    }
+
+    private io.grpc.Status backendGrpcStatus(HttpStatusCode statusCode) {
+        if (statusCode == null) {
+            return io.grpc.Status.UNAVAILABLE;
+        }
+        if (statusCode.value() == HttpStatus.UNAUTHORIZED.value()) {
+            return io.grpc.Status.UNAUTHENTICATED;
+        }
+        if (statusCode.value() == HttpStatus.FORBIDDEN.value()) {
+            return io.grpc.Status.PERMISSION_DENIED;
+        }
+        if (statusCode.value() == HttpStatus.BAD_REQUEST.value()) {
+            return io.grpc.Status.INVALID_ARGUMENT;
+        }
+        if (statusCode.value() == HttpStatus.NOT_ACCEPTABLE.value()) {
+            return io.grpc.Status.INVALID_ARGUMENT;
+        }
+        if (statusCode.value() == HttpStatus.UNSUPPORTED_MEDIA_TYPE.value()) {
+            return io.grpc.Status.INVALID_ARGUMENT;
+        }
+        if (statusCode.value() == HttpStatus.UNPROCESSABLE_ENTITY.value()) {
+            return io.grpc.Status.INVALID_ARGUMENT;
         }
-        if (requestContentType != null && MediaType.APPLICATION_JSON.includes(requestContentType)) {
-            return MediaType.APPLICATION_JSON;
+        if (statusCode.value() == HttpStatus.NOT_FOUND.value()) {
+            return io.grpc.Status.NOT_FOUND;
         }
-        return MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF);
+        if (statusCode.value() == HttpStatus.CONFLICT.value()) {
+            return io.grpc.Status.ABORTED;
+        }
+        if (statusCode.value() == HttpStatus.LOCKED.value()) {
+            return io.grpc.Status.ABORTED;
+        }
+        if (statusCode.value() == HttpStatus.PRECONDITION_FAILED.value()) {
+            return io.grpc.Status.FAILED_PRECONDITION;
+        }
+        if (statusCode.value() == HttpStatus.PRECONDITION_REQUIRED.value()) {
+            return io.grpc.Status.FAILED_PRECONDITION;
+        }
+        if (statusCode.value() == HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value()) {
+            return io.grpc.Status.OUT_OF_RANGE;
+        }
+        if (statusCode.value() == HttpStatus.METHOD_NOT_ALLOWED.value()) {
+            return io.grpc.Status.UNIMPLEMENTED;
+        }
+        if (statusCode.value() == HttpStatus.NOT_IMPLEMENTED.value()) {
+            return io.grpc.Status.UNIMPLEMENTED;
+        }
+        if (statusCode.value() == HttpStatus.PAYLOAD_TOO_LARGE.value()) {
+            return io.grpc.Status.RESOURCE_EXHAUSTED;
+        }
+        if (statusCode.value() == HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE.value()) {
+            return io.grpc.Status.RESOURCE_EXHAUSTED;
+        }
+        if (statusCode.value() == HttpStatus.INSUFFICIENT_STORAGE.value()) {
+            return io.grpc.Status.RESOURCE_EXHAUSTED;
+        }
+        if (statusCode.value() == HttpStatus.TOO_MANY_REQUESTS.value()) {
+            return io.grpc.Status.RESOURCE_EXHAUSTED;
+        }
+        if (statusCode.value() == HttpStatus.TOO_EARLY.value()) {
+            return io.grpc.Status.UNAVAILABLE;
+        }
+        if (statusCode.value() == HttpStatus.REQUEST_TIMEOUT.value()
+                || statusCode.value() == HttpStatus.GATEWAY_TIMEOUT.value()) {
+            return io.grpc.Status.DEADLINE_EXCEEDED;
+        }
+        if (statusCode.value() == HttpStatus.BAD_GATEWAY.value()
+                || statusCode.value() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
+            return io.grpc.Status.UNAVAILABLE;
+        }
+        if (statusCode.is4xxClientError()) {
+            return io.grpc.Status.INTERNAL;
+        }
+        if (statusCode.is5xxServerError()) {
+            return io.grpc.Status.INTERNAL;
+        }
+        return io.grpc.Status.UNAVAILABLE;
     }
 
-    private ResponseEntity<byte[]> errorResponse(MediaType contentType, HttpStatus status, String message) {
-        String errorMessage = StringUtils.defaultIfBlank(message, "OTLP signal ingestion failed.");
-        if (contentType != null && MediaType.APPLICATION_JSON.includes(contentType)) {
-            return ResponseEntity.status(status)
-                    .contentType(MediaType.APPLICATION_JSON)
-                    .body(toJsonErrorResponse(errorMessage));
+    private String database(String configuredDatabase) {
+        return StringUtils.defaultIfBlank(StringUtils.trim(configuredDatabase), DEFAULT_GREPTIME_DB_NAME);
+    }
+
+    private MediaType resolveResponseContentType(MediaType requestContentType, List<MediaType> acceptTypes) {
+        return prefersJsonResponse(requestContentType, acceptTypes)
+                ? MediaType.APPLICATION_JSON
+                : MEDIA_TYPE_PROTOBUF;
+    }
+
+    private boolean prefersJsonResponse(MediaType requestContentType, List<MediaType> acceptTypes) {
+        double jsonQuality = negotiatedQuality(acceptTypes, true);
+        double protobufQuality = negotiatedQuality(acceptTypes, false);
+        if (jsonQuality >= 0 || protobufQuality >= 0) {
+            double acceptableJsonQuality = Math.max(jsonQuality, 0.0d);
+            double acceptableProtobufQuality = Math.max(protobufQuality, 0.0d);
+            if (Double.compare(acceptableJsonQuality, acceptableProtobufQuality) == 0) {
+                return acceptableJsonQuality > 0 && isExplicitJsonMediaType(requestContentType);
+            }
+            return acceptableJsonQuality > acceptableProtobufQuality;
         }
-        return ResponseEntity.status(status)
-                .contentType(MediaType.parseMediaType(CONTENT_TYPE_PROTOBUF))
-                .body(toBinaryErrorResponse(errorMessage));
+        return isExplicitJsonMediaType(requestContentType);
     }
 
-    private byte[] toBinaryErrorResponse(String message) {
-        return Status.newBuilder().setMessage(message).build().toByteArray();
+    private double negotiatedQuality(List<MediaType> acceptTypes, boolean json) {
+        if (acceptTypes == null || acceptTypes.isEmpty()) {
+            return -1.0d;
+        }
+        double explicitQuality = acceptTypes.stream()
+                .filter(json ? this::isExplicitJsonMediaType : this::isExplicitProtobufMediaType)
+                .mapToDouble(MediaType::getQualityValue)
+                .max()
+                .orElse(-1.0d);
+        if (explicitQuality >= 0) {
+            return explicitQuality;
+        }
+        return acceptTypes.stream()
+                .filter(json ? this::isJsonWildcardMediaType : this::isProtobufWildcardMediaType)
+                .mapToDouble(MediaType::getQualityValue)
+                .max()
+                .orElse(-1.0d);
     }
 
-    private byte[] toJsonErrorResponse(String message) {
-        try {
-            return JsonFormat.printer().print(Status.newBuilder().setMessage(message).build())
-                    .getBytes(StandardCharsets.UTF_8);
-        } catch (InvalidProtocolBufferException e) {
-            return ("{\"message\":\"" + message + "\"}").getBytes(StandardCharsets.UTF_8);
+    private boolean isJsonWildcardMediaType(MediaType mediaType) {
+        return mediaType != null
+                && !isExplicitJsonMediaType(mediaType)
+                && mediaType.includes(MediaType.APPLICATION_JSON);
+    }
+
+    private boolean isProtobufWildcardMediaType(MediaType mediaType) {
+        return mediaType != null
+                && !isExplicitProtobufMediaType(mediaType)
+                && (mediaType.includes(MEDIA_TYPE_PROTOBUF) || mediaType.includes(MEDIA_TYPE_PROTOBUF_ALT));
+    }
+
+    private boolean isExplicitJsonMediaType(MediaType mediaType) {
+        if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) {
+            return false;
         }
+        return "application".equalsIgnoreCase(mediaType.getType())
+                && ("json".equalsIgnoreCase(mediaType.getSubtype())
+                || mediaType.getSubtype().toLowerCase(Locale.ROOT).endsWith("+json"));
+    }
+
+    private boolean isExplicitProtobufMediaType(MediaType mediaType) {
+        if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) {
+            return false;
+        }
+        return "application".equalsIgnoreCase(mediaType.getType())
+                && ("x-protobuf".equalsIgnoreCase(mediaType.getSubtype())
+                || "protobuf".equalsIgnoreCase(mediaType.getSubtype()));
     }
 
     private String defaultErrorMessage(Exception ex) {
         return StringUtils.defaultIfBlank(ex.getMessage(), "OTLP signal ingestion failed.");
     }
 
+    private long requestBytes(byte[] content) {
+        return content == null ? 0L : content.length;
+    }
+
+    private long requestBytes(Message request) {
+        return request == null ? 0L : request.getSerializedSize();
+    }
+
+    private long durationMillis(long startedAtNanos) {
+        return Math.max(0L, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos));
+    }
+
     private String normalizeOtlpJson(String content) throws InvalidProtocolBufferException {
         try {
             JsonNode root = OBJECT_MAPPER.readTree(content);
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
index d7ccdc34d4..7efc672823 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
@@ -88,6 +88,7 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
     private static final String DEFAULT_METRICS_AGGREGATION = "sum";
     private static final String METRICS_CONSOLE_REF_ID = "otlp-metrics-console";
     private static final int DEFAULT_RECENT_SERVICE_LIMIT = 6;
+    private static final int DEFAULT_RECENT_UNBOUND_CANDIDATE_LIMIT = 6;
     private static final int DEFAULT_RECENT_METRIC_NAME_LIMIT = 64;
     private static final int DEFAULT_METRICS_QUERY_CANDIDATE_LIMIT = 64;
     private static final Set<String> WORKSPACE_INFRA_SERVICE_NAMES = Set.of(
@@ -585,6 +586,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
         List<String> recentServices = collectRecentServices(identitySnapshots, DEFAULT_RECENT_SERVICE_LIMIT);
 
         Map<Long, List<EntityIdentity>> entityIdentityMap = collectRecentBoundEntityIdentities(identitySnapshots);
+        Set<String> boundIdentityMatches = boundIdentityMatchKeys(entityIdentityMap);
+        List<OtlpEntityBindingSummaryDto.UnboundEntityCandidate> unboundCandidates =
+                buildUnboundEntityCandidates(identitySnapshots, boundIdentityMatches);
 
         Map<Long, ObserveEntity> entityMap = workspaceQueryGateway.findEntitiesByIds(entityIdentityMap.keySet());
         List<OtlpEntityBindingSummaryDto.BoundEntity> boundEntities = new ArrayList<>();
@@ -618,7 +622,8 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
                 EntityCanonicalIdentityRegistry.CANONICAL_OTEL_RESOURCE_KEYS,
                 recentServices,
                 samples,
-                boundEntities
+                boundEntities,
+                unboundCandidates
         );
     }
 
@@ -889,6 +894,95 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
         return StringUtils.hasText(value) ? value.trim().toLowerCase(Locale.ROOT) : null;
     }
 
+    private Set<String> boundIdentityMatchKeys(Map<Long, List<EntityIdentity>> entityIdentityMap) {
+        if (CollectionUtils.isEmpty(entityIdentityMap)) {
+            return Collections.emptySet();
+        }
+        Set<String> matches = new LinkedHashSet<>();
+        for (List<EntityIdentity> identities : entityIdentityMap.values()) {
+            if (CollectionUtils.isEmpty(identities)) {
+                continue;
+            }
+            for (EntityIdentity identity : identities) {
+                if (identity == null || !StringUtils.hasText(identity.getIdentityKey())) {
+                    continue;
+                }
+                String normalizedValue = normalizeIdentityValue(
+                        defaultText(identity.getNormalizedValue(), identity.getIdentityValue())
+                );
+                if (!StringUtils.hasText(normalizedValue)) {
+                    continue;
+                }
+                matches.add(identity.getIdentityKey() + "\u0000" + normalizedValue);
+            }
+        }
+        return matches;
+    }
+
+    private List<OtlpEntityBindingSummaryDto.UnboundEntityCandidate> buildUnboundEntityCandidates(
+            List<TelemetryIdentitySnapshot> snapshots, Set<String> boundIdentityMatches) {
+        if (CollectionUtils.isEmpty(snapshots)) {
+            return Collections.emptyList();
+        }
+        Map<String, CandidateAccumulator> candidates = new LinkedHashMap<>();
+        for (TelemetryIdentitySnapshot snapshot : snapshots) {
+            if (snapshot == null || CollectionUtils.isEmpty(snapshot.getCanonicalIdentities())) {
+                continue;
+            }
+            if (matchesKnownEntityIdentity(snapshot.getCanonicalIdentities(), boundIdentityMatches)) {
+                continue;
+            }
+            String serviceName = trimToNull(firstText(
+                    snapshot.getServiceName(),
+                    snapshot.getCanonicalIdentities().get("service.name")
+            ));
+            if (!StringUtils.hasText(serviceName) || isWorkspaceNoiseService(serviceName)) {
+                continue;
+            }
+            String namespace = trimToNull(firstText(
+                    snapshot.getServiceNamespace(),
+                    snapshot.getCanonicalIdentities().get("service.namespace")
+            ));
+            String environment = trimToNull(firstText(
+                    snapshot.getEnvironmentName(),
+                    snapshot.getCanonicalIdentities().get("deployment.environment.name")
+            ));
+            String candidateKey = normalizeIdentityValue(serviceName) + "|"
+                    + defaultText(normalizeIdentityValue(namespace), "") + "|"
+                    + defaultText(normalizeIdentityValue(environment), "");
+            CandidateAccumulator accumulator = candidates.computeIfAbsent(candidateKey,
+                    ignored -> new CandidateAccumulator(serviceName, namespace, environment,
+                            snapshot.getCanonicalIdentities()));
+            accumulator.add(snapshot.getSignal(), snapshot.getObservedAt(), snapshot.getCanonicalIdentities());
+        }
+        return candidates.values().stream()
+                .sorted(Comparator.comparing(
+                        CandidateAccumulator::latestObservedAt,
+                        Comparator.nullsLast(Comparator.reverseOrder())
+                ))
+                .limit(DEFAULT_RECENT_UNBOUND_CANDIDATE_LIMIT)
+                .map(CandidateAccumulator::toDto)
+                .toList();
+    }
+
+    private boolean matchesKnownEntityIdentity(Map<String, String> canonicalIdentities, Set<String> boundIdentityMatches) {
+        if (CollectionUtils.isEmpty(canonicalIdentities) || CollectionUtils.isEmpty(boundIdentityMatches)) {
+            return false;
+        }
+        Set<String> identityKeys = canonicalIdentityKeySet();
+        for (Map.Entry<String, String> entry : canonicalIdentities.entrySet()) {
+            if (!identityKeys.contains(entry.getKey())) {
+                continue;
+            }
+            String normalizedValue = normalizeIdentityValue(entry.getValue());
+            if (StringUtils.hasText(normalizedValue)
+                    && boundIdentityMatches.contains(entry.getKey() + "\u0000" + normalizedValue)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private String truncateLogBody(Object body) {
         if (body == null) {
             return null;
@@ -1030,12 +1124,15 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
         }
         String trimmed = host.trim();
         if (trimmed.startsWith("[") && trimmed.contains("]")) {
-            return trimmed;
+            return trimmed.substring(0, trimmed.indexOf(']') + 1);
         }
         int colonIndex = trimmed.lastIndexOf(':');
         if (colonIndex > 0 && trimmed.indexOf(':') == colonIndex) {
             return trimmed.substring(0, colonIndex);
         }
+        if (colonIndex > 0 && trimmed.indexOf(':') != colonIndex) {
+            return "[" + trimmed + "]";
+        }
         return trimmed;
     }
 
@@ -1623,6 +1720,65 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace
         return StringUtils.hasText(value) ? value.trim() : null;
     }
 
+    private final class CandidateAccumulator {
+        private final String suggestedName;
+        private final String namespace;
+        private final String environment;
+        private final LinkedHashSet<String> signals = new LinkedHashSet<>();
+        private final LinkedHashMap<String, String> canonicalIdentities = new LinkedHashMap<>();
+        private Long latestObservedAt;
+
+        private CandidateAccumulator(String suggestedName, String namespace, String environment,
+                                     Map<String, String> canonicalIdentities) {
+            this.suggestedName = suggestedName;
+            this.namespace = namespace;
+            this.environment = environment;
+            mergeCanonicalIdentities(canonicalIdentities);
+        }
+
+        private void add(String signal, Long observedAt, Map<String, String> canonicalIdentities) {
+            String normalizedSignal = trimToNull(signal);
+            if (StringUtils.hasText(normalizedSignal)) {
+                signals.add(normalizedSignal);
+            }
+            if (observedAt != null && (latestObservedAt == null || observedAt > latestObservedAt)) {
+                latestObservedAt = observedAt;
+            }
+            mergeCanonicalIdentities(canonicalIdentities);
+        }
+
+        private Long latestObservedAt() {
+            return latestObservedAt;
+        }
+
+        private OtlpEntityBindingSummaryDto.UnboundEntityCandidate toDto() {
+            String primaryIdentityValue = firstText(canonicalIdentities.get("service.name"), suggestedName);
+            return new OtlpEntityBindingSummaryDto.UnboundEntityCandidate(
+                    suggestedName,
+                    "service",
+                    namespace,
+                    environment,
+                    "service.name",
+                    primaryIdentityValue,
+                    List.copyOf(signals),
+                    new LinkedHashMap<>(canonicalIdentities),
+                    latestObservedAt
+            );
+        }
+
+        private void mergeCanonicalIdentities(Map<String, String> identities) {
+            if (CollectionUtils.isEmpty(identities)) {
+                return;
+            }
+            for (String key : EntityCanonicalIdentityRegistry.CANONICAL_OTEL_RESOURCE_KEYS) {
+                String value = trimToNull(identities.get(key));
+                if (StringUtils.hasText(value)) {
+                    canonicalIdentities.putIfAbsent(key, value);
+                }
+            }
+        }
+    }
+
     private record MetricsQueryExecution(String datasource,
                                          DatasourceQueryData results,
                                          OtlpMetricsConsoleDto.Stats stats,
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
index 6cf24800fe..2f8daf53ff 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
@@ -31,6 +31,9 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.common.entity.log.LogEntry;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
+import org.apache.hertzbeat.observability.ingestion.enricher.OtlpCorrelationEnricher;
 import org.apache.hertzbeat.observability.logs.service.LogQueryService;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -66,6 +69,11 @@ public class LogQueryServiceImpl implements LogQueryService {
             "flagd",
             "flagd-ui"
     );
+    private static final Set<String> WORKSPACE_RESOURCE_KEYS = Set.of(
+            OtlpCorrelationEnricher.WORKSPACE_ID_ATTRIBUTE,
+            AuthTokenScopes.CLAIM_WORKSPACE_ID,
+            "workspace.id"
+    );
 
     private final List<HistoryDataReader> historyDataReaders;
 
@@ -184,6 +192,9 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readSeverityBuckets(Long start, Long end, String traceId, String spanId,
                                                   Integer severityNumber, String severityText, String search,
                                                   boolean hideInternal, boolean hideNoise) {
+        if (hasWorkspaceContext()) {
+            return null;
+        }
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
                 Map<String, Long> aggregate = historyDataReader.countLogsBySeverityBuckets(
@@ -202,6 +213,9 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readTraceCoverage(Long start, Long end, String traceId, String spanId,
                                                 Integer severityNumber, String severityText, String search,
                                                 boolean hideInternal, boolean hideNoise) {
+        if (hasWorkspaceContext()) {
+            return null;
+        }
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
                 Map<String, Long> aggregate = historyDataReader.countLogTraceCoverage(
@@ -220,6 +234,9 @@ public class LogQueryServiceImpl implements LogQueryService {
     private Map<String, Long> readHourlyStats(Long start, Long end, String traceId, String spanId,
                                               Integer severityNumber, String severityText, String search,
                                               boolean hideInternal, boolean hideNoise) {
+        if (hasWorkspaceContext()) {
+            return null;
+        }
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
                 Map<String, Long> aggregate = historyDataReader.countLogsByHour(
@@ -250,7 +267,7 @@ public class LogQueryServiceImpl implements LogQueryService {
                             start, end, traceId, spanId, severityNumber, severityText, search);
                 }
                 if (logs != null && !logs.isEmpty()) {
-                    return filterWorkspaceLogs(logs, hideInternal, hideNoise);
+                    return filterQueryLogs(logs, hideInternal, hideNoise);
                 }
             } catch (UnsupportedOperationException ex) {
                 // Try the next reader. Not every history store supports log queries.
@@ -266,6 +283,11 @@ public class LogQueryServiceImpl implements LogQueryService {
         Sort sort = Sort.by(Sort.Direction.DESC, "timeUnixNano");
         PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sort);
 
+        if (hasWorkspaceContext()) {
+            return getWorkspacePagedLogs(start, end, traceId, spanId, severityNumber, severityText, search,
+                    pageRequest, offset, pageSize, hideInternal, hideNoise);
+        }
+
         for (HistoryDataReader historyDataReader : historyDataReaders) {
             try {
                 if (hideInternal || hideNoise) {
@@ -280,7 +302,7 @@ public class LogQueryServiceImpl implements LogQueryService {
                     List<LogEntry> pagedLogs = historyDataReader.queryLogsByMultipleConditionsWithPagination(
                             start, end, traceId, spanId, severityNumber, severityText, search, offset, pageSize,
                             hiddenServiceNames, requireServiceName);
-                    List<LogEntry> filteredLogs = filterWorkspaceLogs(pagedLogs, hideInternal, hideNoise);
+                    List<LogEntry> filteredLogs = filterQueryLogs(pagedLogs, hideInternal, hideNoise);
                     long safeTotal = filteredLogs.size() < (pagedLogs == null ? 0 : pagedLogs.size())
                             ? offset + filteredLogs.size()
                             : totalElements;
@@ -293,7 +315,41 @@ public class LogQueryServiceImpl implements LogQueryService {
                 }
                 List<LogEntry> pagedLogs = historyDataReader.queryLogsByMultipleConditionsWithPagination(
                         start, end, traceId, spanId, severityNumber, severityText, search, offset, pageSize);
-                return new PageImpl<>(pagedLogs == null ? Collections.emptyList() : pagedLogs, pageRequest, totalElements);
+                List<LogEntry> filteredLogs = filterQueryLogs(pagedLogs, hideInternal, hideNoise);
+                long safeTotal = filteredLogs.size() < (pagedLogs == null ? 0 : pagedLogs.size())
+                        ? offset + filteredLogs.size()
+                        : totalElements;
+                return new PageImpl<>(filteredLogs, pageRequest, safeTotal);
+            } catch (UnsupportedOperationException ex) {
+                // Try the next reader. Not every history store supports log queries.
+            }
+        }
+        return new PageImpl<>(Collections.emptyList(), pageRequest, 0);
+    }
+
+    private Page<LogEntry> getWorkspacePagedLogs(Long start, Long end, String traceId, String spanId,
+                                                 Integer severityNumber, String severityText, String search,
+                                                 PageRequest pageRequest, int offset, int pageSize,
+                                                 boolean hideInternal, boolean hideNoise) {
+        for (HistoryDataReader historyDataReader : historyDataReaders) {
+            try {
+                List<LogEntry> logs;
+                if (hideInternal || hideNoise) {
+                    logs = historyDataReader.queryLogsByMultipleConditions(
+                            start, end, traceId, spanId, severityNumber, severityText, search,
+                            hiddenServiceNames(hideInternal, hideNoise), shouldRequireServiceName(hideInternal, hideNoise));
+                } else {
+                    logs = historyDataReader.queryLogsByMultipleConditions(
+                            start, end, traceId, spanId, severityNumber, severityText, search);
+                }
+                if (logs == null || logs.isEmpty()) {
+                    continue;
+                }
+                List<LogEntry> filteredLogs = filterQueryLogs(logs, hideInternal, hideNoise);
+                int fromIndex = Math.min(offset, filteredLogs.size());
+                int toIndex = Math.min(fromIndex + pageSize, filteredLogs.size());
+                return new PageImpl<>(List.copyOf(filteredLogs.subList(fromIndex, toIndex)),
+                        pageRequest, filteredLogs.size());
             } catch (UnsupportedOperationException ex) {
                 // Try the next reader. Not every history store supports log queries.
             }
@@ -319,12 +375,20 @@ public class LogQueryServiceImpl implements LogQueryService {
         return hideInternal || hideNoise;
     }
 
-    private List<LogEntry> filterWorkspaceLogs(List<LogEntry> logs, boolean hideInternal, boolean hideNoise) {
-        if ((!hideInternal && !hideNoise) || logs == null || logs.isEmpty()) {
+    private List<LogEntry> filterQueryLogs(List<LogEntry> logs, boolean hideInternal, boolean hideNoise) {
+        if (logs == null || logs.isEmpty()) {
             return logs == null ? Collections.emptyList() : logs;
         }
+        String workspaceId = AuthTokenRequestContext.currentWorkspaceId();
+        if (!StringUtils.hasText(workspaceId) && !hideInternal && !hideNoise) {
+            return logs;
+        }
+        String normalizedWorkspaceId = StringUtils.hasText(workspaceId)
+                ? AuthTokenScopes.normalizeWorkspaceId(workspaceId)
+                : null;
         return logs.stream()
                 .filter(log -> !shouldHideWorkspaceLog(log, hideInternal, hideNoise))
+                .filter(log -> matchesWorkspace(log, normalizedWorkspaceId))
                 .toList();
     }
 
@@ -339,6 +403,34 @@ public class LogQueryServiceImpl implements LogQueryService {
         return hideNoise && DEMO_INFRA_SERVICE_NAMES.contains(serviceName);
     }
 
+    private boolean hasWorkspaceContext() {
+        return StringUtils.hasText(AuthTokenRequestContext.currentWorkspaceId());
+    }
+
+    private boolean matchesWorkspace(LogEntry logEntry, String workspaceId) {
+        if (!StringUtils.hasText(workspaceId)) {
+            return true;
+        }
+        String logWorkspaceId = resolveWorkspaceId(logEntry);
+        if (!StringUtils.hasText(logWorkspaceId)) {
+            return AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(workspaceId);
+        }
+        return workspaceId.equals(AuthTokenScopes.normalizeWorkspaceId(logWorkspaceId));
+    }
+
+    private String resolveWorkspaceId(LogEntry logEntry) {
+        if (logEntry == null || logEntry.getResource() == null || logEntry.getResource().isEmpty()) {
+            return null;
+        }
+        for (String key : WORKSPACE_RESOURCE_KEYS) {
+            String value = normalizeRawValue(logEntry.getResource().get(key));
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
     private String resolveServiceName(LogEntry logEntry) {
         if (logEntry == null || logEntry.getResource() == null || logEntry.getResource().isEmpty()) {
             return null;
@@ -350,6 +442,14 @@ public class LogQueryServiceImpl implements LogQueryService {
         return serviceName;
     }
 
+    private String normalizeRawValue(Object rawValue) {
+        if (rawValue == null) {
+            return null;
+        }
+        String normalized = String.valueOf(rawValue).trim();
+        return normalized.isEmpty() ? null : normalized;
+    }
+
     private String normalizeResourceValue(Object rawValue) {
         if (rawValue == null) {
             return null;
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogSseServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogSseServiceImpl.java
index 4d4f96aee7..f233e2fa16 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogSseServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogSseServiceImpl.java
@@ -17,11 +17,14 @@
 
 package org.apache.hertzbeat.observability.logs.service.impl;
 
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
 import org.apache.hertzbeat.observability.logs.service.LogSseService;
 import org.apache.hertzbeat.observability.logs.sse.LogSseFilterCriteria;
 import org.apache.hertzbeat.observability.logs.sse.LogSseManager;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 /**
@@ -39,6 +42,14 @@ public class LogSseServiceImpl implements LogSseService {
     @Override
     public SseEmitter subscribe(LogSseFilterCriteria filterCriteria) {
         Long clientId = SnowFlakeIdGenerator.generateId();
+        bindRequestWorkspace(filterCriteria);
         return emitterManager.createEmitter(clientId, filterCriteria);
     }
+
+    private void bindRequestWorkspace(LogSseFilterCriteria filterCriteria) {
+        String workspaceId = AuthTokenRequestContext.currentWorkspaceId();
+        if (filterCriteria != null && StringUtils.hasText(workspaceId)) {
+            filterCriteria.setWorkspaceId(AuthTokenScopes.normalizeWorkspaceId(workspaceId));
+        }
+    }
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/sse/LogSseFilterCriteria.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/sse/LogSseFilterCriteria.java
index 99d4bc5e38..d9052dfe69 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/sse/LogSseFilterCriteria.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/sse/LogSseFilterCriteria.java
@@ -19,13 +19,17 @@
 
 package org.apache.hertzbeat.observability.logs.sse;
 
+import java.util.Map;
+import java.util.Set;
 import lombok.Data;
 import lombok.AllArgsConstructor;
 import lombok.NoArgsConstructor;
 import org.apache.hertzbeat.common.entity.log.LogEntry;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.springframework.util.StringUtils;
 import io.swagger.v3.oas.annotations.media.Schema;
 import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
+import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
 
 /**
  * Log filtering criteria for SSE (Server-Sent Events) log streaming
@@ -35,6 +39,13 @@ import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
 @NoArgsConstructor
 @Schema(description = "Log filtering criteria for SSE (Server-Sent Events) log streaming")
 public class LogSseFilterCriteria {
+
+    private static final Set<String> WORKSPACE_RESOURCE_KEYS = Set.of(
+            "hertzbeat.workspace_id",
+            AuthTokenScopes.CLAIM_WORKSPACE_ID,
+            "workspace.id"
+    );
+
     /**
      * Numerical value of the severity.
      * Smaller numerical values correspond to less severe events (such as debug events),
@@ -71,6 +82,20 @@ public class LogSseFilterCriteria {
     @Schema(description = "A unique identifier for a span.", example = "1234567890", accessMode = READ_WRITE)
     private String spanId;
 
+    /**
+     * Workspace boundary captured from the authenticated request.
+     */
+    @Schema(description = "Server-bound workspace boundary.", accessMode = READ_ONLY)
+    private String workspaceId;
+
+    public LogSseFilterCriteria(Integer severityNumber, String severityText, String logContent, String traceId,
+                                String spanId) {
+        this.severityNumber = severityNumber;
+        this.severityText = severityText;
+        this.logContent = logContent;
+        this.traceId = traceId;
+        this.spanId = spanId;
+    }
 
     /**
      * Core filtering logic to determine if a log entry matches the criteria
@@ -79,6 +104,9 @@ public class LogSseFilterCriteria {
      */
     public boolean matches(LogEntry log) {
         if (log == null) return false;
+        if (!matchesWorkspace(log)) {
+            return false;
+        }
         // Check severity text match
         if (StringUtils.hasText(severityText) && !severityText.equalsIgnoreCase(log.getSeverityText())) {
             return false;
@@ -113,4 +141,29 @@ public class LogSseFilterCriteria {
         }
         return true;
     }
+
+    private boolean matchesWorkspace(LogEntry log) {
+        if (!StringUtils.hasText(workspaceId)) {
+            return true;
+        }
+        String normalizedWorkspaceId = AuthTokenScopes.normalizeWorkspaceId(workspaceId);
+        String logWorkspaceId = resolveWorkspaceId(log.getResource());
+        if (!StringUtils.hasText(logWorkspaceId)) {
+            return AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(normalizedWorkspaceId);
+        }
+        return normalizedWorkspaceId.equals(AuthTokenScopes.normalizeWorkspaceId(logWorkspaceId));
+    }
+
+    private String resolveWorkspaceId(Map<String, Object> resource) {
+        if (resource == null || resource.isEmpty()) {
+            return null;
+        }
+        for (String key : WORKSPACE_RESOURCE_KEYS) {
+            Object value = resource.get(key);
+            if (value != null && StringUtils.hasText(String.valueOf(value))) {
+                return String.valueOf(value);
+            }
+        }
+        return null;
+    }
 }
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java
index 586294fedd..379a75fba6 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java
@@ -56,6 +56,7 @@ import org.apache.hertzbeat.common.observability.dto.trace.TraceListItemDto;
 import org.apache.hertzbeat.common.observability.model.CodeNavigationHint;
 import org.apache.hertzbeat.common.observability.model.EntityCanonicalIdentityRegistry;
 import org.apache.hertzbeat.common.observability.model.ObservedEntityContext;
+import org.apache.hertzbeat.observability.ingestion.semantic.OtlpResourceSemanticAttributes;
 import org.apache.hertzbeat.warehouse.repository.LogQueryRepository;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.event.EventListener;
@@ -93,6 +94,8 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
     private static final String OTLP_METRIC_SUMMARY_QUANTILES = "otlp.metric.summary.quantiles";
     private static final String OTLP_METRIC_HISTOGRAM_BUCKET_COUNTS = "otlp.metric.histogram.bucket_counts";
     private static final String OTLP_METRIC_HISTOGRAM_EXPLICIT_BOUNDS = "otlp.metric.histogram.explicit_bounds";
+    private static final String HERTZBEAT_ENTITY_ID = OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID;
+    private static final String HERTZBEAT_WORKSPACE_ID = OtlpResourceSemanticAttributes.HERTZBEAT_WORKSPACE_ID;
     private static final Set<String> WORKSPACE_INFRA_SERVICE_NAMES = Set.of(
             "otelcol-contrib",
             "otel-collector",
@@ -604,22 +607,17 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
     @Override
     public List<LogEvidence> buildLogEvidence(ObservedEntityContext entityContext, EntityLogSummaryInfo logSummary,
                                               List<EntityLogQueryHint> logQueryHints) {
-        if (entityContext == null || CollectionUtils.isEmpty(entityContext.getIdentities())) {
+        long entityId = entityId(entityContext);
+        if (entityContext == null || entityId == 0L) {
             return Collections.emptyList();
         }
-        long entityId = entityContext == null || entityContext.getEntity() == null || entityContext.getEntity().getId() == null
-                ? 0L
-                : entityContext.getEntity().getId();
         Set<String> entityIdentityKeys = entityIdentityKeySet(entityContext);
         Map<String, String> entityIdentityValues = entityIdentityValueMap(entityContext);
-        if (entityIdentityKeys.isEmpty()) {
-            return Collections.emptyList();
-        }
         EntityLogQueryHint preferredHint = CollectionUtils.isEmpty(logQueryHints) ? null : logQueryHints.getFirst();
         TelemetryBindingResult bindingResult = buildBindingResult(entityContext, SOURCE_OTLP, SIGNAL_LOGS);
         List<LogEvidence> results = new ArrayList<>();
         for (RecentLogSignal signal : recentLogSignals) {
-            if (!matchesEntityIdentities(signal.canonicalIdentities(), entityIdentityKeys, entityIdentityValues)) {
+            if (!matchesEntitySignal(signal.canonicalIdentities(), entityIdentityKeys, entityIdentityValues, entityId)) {
                 continue;
             }
             TelemetryIdentitySnapshot identitySnapshot = new TelemetryIdentitySnapshot(
@@ -727,7 +725,7 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
         List<LogEvidence> results = new ArrayList<>();
         for (LogEntry log : storedLogs) {
             Map<String, String> canonicalIdentities = extractCanonicalStringMap(log == null ? null : log.getResource());
-            if (!matchesEntityIdentities(canonicalIdentities, entityIdentityKeys, entityIdentityValues)) {
+            if (!matchesEntitySignal(canonicalIdentities, entityIdentityKeys, entityIdentityValues, entityId)) {
                 continue;
             }
             Map<String, String> resource = extractStringMap(log == null ? null : log.getResource());
@@ -773,20 +771,18 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
 
     @Override
     public EntityTraceSummaryDto buildTraceSummary(ObservedEntityContext entityContext) {
-        if (entityContext == null || CollectionUtils.isEmpty(entityContext.getIdentities())) {
+        long entityId = entityId(entityContext);
+        if (entityContext == null || entityId == 0L) {
             return new EntityTraceSummaryDto(0, 0, null, false, null);
         }
         Set<String> entityIdentityKeys = entityIdentityKeySet(entityContext);
         Map<String, String> entityIdentityValues = entityIdentityValueMap(entityContext);
-        if (entityIdentityKeys.isEmpty()) {
-            return new EntityTraceSummaryDto(0, 0, null, false, null);
-        }
         int recentTraceCount = 0;
         int recentErrorTraceCount = 0;
         Long latestObservedAt = null;
         String latestTraceId = null;
         for (RecentTraceSignal signal : recentTraceSignals) {
-            if (!matchesEntityIdentities(signal.canonicalIdentities(), entityIdentityKeys, entityIdentityValues)) {
+            if (!matchesEntitySignal(signal.canonicalIdentities(), entityIdentityKeys, entityIdentityValues, entityId)) {
                 continue;
             }
             recentTraceCount++;
@@ -811,17 +807,15 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
 
     @Override
     public List<EntityTraceQueryHintDto> buildTraceQueryHints(ObservedEntityContext entityContext) {
-        if (entityContext == null || CollectionUtils.isEmpty(entityContext.getIdentities())) {
+        long entityId = entityId(entityContext);
+        if (entityContext == null || entityId == 0L) {
             return Collections.emptyList();
         }
         Set<String> entityIdentityKeys = entityIdentityKeySet(entityContext);
         Map<String, String> entityIdentityValues = entityIdentityValueMap(entityContext);
-        if (entityIdentityKeys.isEmpty()) {
-            return Collections.emptyList();
-        }
         RecentTraceSignal preferredSignal = null;
         for (RecentTraceSignal signal : recentTraceSignals) {
-            if (!matchesEntityIdentities(signal.canonicalIdentities(), entityIdentityKeys, entityIdentityValues)) {
+            if (!matchesEntitySignal(signal.canonicalIdentities(), entityIdentityKeys, entityIdentityValues, entityId)) {
                 continue;
             }
             if (preferredSignal == null
@@ -936,7 +930,8 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
                 logsActive,
                 tracesActive,
                 metricEvidence == null ? 0 : metricEvidence.size(),
-                logSummary == null ? 0 : logSummary.getHintCount(),
+                Math.max(logSummary == null ? 0 : logSummary.getHintCount(),
+                        logEvidence == null ? 0 : logEvidence.size()),
                 traceSummary == null ? 0 : traceSummary.getRecentTraceCount(),
                 latestObservedAt,
                 activeSignals
@@ -1091,6 +1086,16 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
                         .ifPresent(value -> canonicalIdentities.put(key, value));
             }
         }
+        long entityId = entityId(entityContext);
+        if (entityId > 0) {
+            canonicalIdentities.putIfAbsent(HERTZBEAT_ENTITY_ID, String.valueOf(entityId));
+        }
+        String workspaceId = entityContext == null || entityContext.getEntity() == null
+                ? null
+                : trimToNull(entityContext.getEntity().getWorkspaceId());
+        if (workspaceId != null) {
+            canonicalIdentities.putIfAbsent(HERTZBEAT_WORKSPACE_ID, workspaceId);
+        }
         return new TelemetryIdentitySnapshot(
                 source,
                 signal,
@@ -1141,7 +1146,7 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
         }
         Map<String, String> canonical = new LinkedHashMap<>();
         for (String key : EntityCanonicalIdentityRegistry.CANONICAL_OTEL_RESOURCE_KEYS) {
-            Object value = values.get(key);
+            Object value = canonicalResourceValue(values, key);
             if (value == null) {
                 continue;
             }
@@ -1150,21 +1155,43 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
                 canonical.put(key, normalized);
             }
         }
+        putCanonicalResourceValue(canonical, values, HERTZBEAT_ENTITY_ID);
+        putCanonicalResourceValue(canonical, values, HERTZBEAT_WORKSPACE_ID);
         return canonical;
     }
 
+    private void putCanonicalResourceValue(Map<String, String> canonical, Map<?, ?> values, String key) {
+        Object value = canonicalResourceValue(values, key);
+        if (value == null) {
+            return;
+        }
+        String normalized = trimToNull(String.valueOf(value));
+        if (normalized != null) {
+            canonical.put(key, normalized);
+        }
+    }
+
+    private Object canonicalResourceValue(Map<?, ?> values, String key) {
+        Object value = values.get(key);
+        if (value != null) {
+            return value;
+        }
+        return values.get(normalizeOtelResourceStorageKey(key));
+    }
+
+    private String normalizeOtelResourceStorageKey(String key) {
+        return key == null ? null : key.replace(".", "_");
+    }
+
     private List<MetricEvidence> buildOtlpMetricEvidence(ObservedEntityContext entityContext, long entityId) {
-        if (entityContext == null || CollectionUtils.isEmpty(entityContext.getIdentities()) || recentMetricSignals.isEmpty()) {
+        if (entityContext == null || entityId == 0L || recentMetricSignals.isEmpty()) {
             return Collections.emptyList();
         }
         List<MetricEvidence> result = new ArrayList<>();
         Set<String> entityIdentityKeys = entityIdentityKeySet(entityContext);
         Map<String, String> entityIdentityValues = entityIdentityValueMap(entityContext);
-        if (entityIdentityKeys.isEmpty()) {
-            return Collections.emptyList();
-        }
         for (RecentMetricSignal signal : recentMetricSignals) {
-            if (!matchesEntityIdentities(signal.canonicalIdentities(), entityIdentityKeys, entityIdentityValues)) {
+            if (!matchesEntitySignal(signal.canonicalIdentities(), entityIdentityKeys, entityIdentityValues, entityId)) {
                 continue;
             }
             TelemetryIdentitySnapshot snapshot = new TelemetryIdentitySnapshot(
@@ -1265,6 +1292,23 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
         return String.join(";", segments);
     }
 
+    private boolean matchesEntitySignal(Map<String, String> canonicalIdentities,
+                                        Set<String> entityIdentityKeys,
+                                        Map<String, String> entityIdentityValues,
+                                        long entityId) {
+        if (matchesExplicitEntityId(canonicalIdentities, entityId)) {
+            return true;
+        }
+        return matchesEntityIdentities(canonicalIdentities, entityIdentityKeys, entityIdentityValues);
+    }
+
+    private boolean matchesExplicitEntityId(Map<String, String> canonicalIdentities, long entityId) {
+        if (entityId <= 0 || CollectionUtils.isEmpty(canonicalIdentities)) {
+            return false;
+        }
+        return Objects.equals(String.valueOf(entityId), trimToNull(canonicalIdentities.get(HERTZBEAT_ENTITY_ID)));
+    }
+
     private boolean matchesEntityIdentities(Map<String, String> canonicalIdentities,
                                             Set<String> entityIdentityKeys,
                                             Map<String, String> entityIdentityValues) {
@@ -1299,6 +1343,12 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway {
         return matched;
     }
 
+    private long entityId(ObservedEntityContext entityContext) {
+        return entityContext == null || entityContext.getEntity() == null || entityContext.getEntity().getId() == null
+                ? 0L
+                : entityContext.getEntity().getId();
+    }
+
     private Set<String> entityIdentityKeySet(ObservedEntityContext entityContext) {
         if (entityContext == null || CollectionUtils.isEmpty(entityContext.getIdentities())) {
             return Collections.emptySet();
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
index ad3b20d56b..8b23e9b6af 100644
--- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
@@ -17,11 +17,15 @@
 
 package org.apache.hertzbeat.observability.traces.service.impl;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.sql.Timestamp;
 import java.time.Duration;
 import java.time.Instant;
 import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
 import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -37,6 +41,8 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
 import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
+import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 import org.apache.hertzbeat.common.observability.gateway.ObservabilityWorkspaceQueryGateway;
 import org.apache.hertzbeat.common.observability.model.EntityCanonicalIdentityRegistry;
 import org.apache.hertzbeat.common.observability.model.ObservedEntityContext;
@@ -48,6 +54,7 @@ import org.apache.hertzbeat.common.observability.dto.trace.TraceOverviewDto;
 import org.apache.hertzbeat.common.observability.dto.trace.TraceSpanEventDto;
 import org.apache.hertzbeat.common.observability.dto.trace.TraceSpanLinkDto;
 import org.apache.hertzbeat.common.observability.dto.trace.TraceSpanNodeDto;
+import org.apache.hertzbeat.observability.ingestion.enricher.OtlpCorrelationEnricher;
 import org.apache.hertzbeat.observability.traces.service.EntityTraceQueryService;
 import org.apache.hertzbeat.warehouse.repository.TraceQueryRepository;
 import org.springframework.data.domain.Page;
@@ -73,6 +80,15 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
     private static final int TRACE_DETAIL_LIMIT = 5000;
     private static final long DEFAULT_LOOKBACK_MILLIS = Duration.ofHours(24).toMillis();
     private static final long ACTIVE_TRACE_WINDOW_MILLIS = Duration.ofMinutes(15).toMillis();
+    private static final BigInteger LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
+    private static final BigInteger LONG_MIN_VALUE = BigInteger.valueOf(Long.MIN_VALUE);
+    private static final BigDecimal LONG_MAX_DECIMAL = BigDecimal.valueOf(Long.MAX_VALUE);
+    private static final BigDecimal LONG_MIN_DECIMAL = BigDecimal.valueOf(Long.MIN_VALUE);
+    private static final Set<String> WORKSPACE_RESOURCE_KEYS = Set.of(
+            OtlpCorrelationEnricher.WORKSPACE_ID_ATTRIBUTE,
+            AuthTokenScopes.CLAIM_WORKSPACE_ID,
+            "workspace.id"
+    );
 
     private final TraceQueryRepository traceQueryRepository;
     private final ObservabilityWorkspaceQueryGateway workspaceQueryGateway;
@@ -84,7 +100,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
             return new EntityTraceSummaryDto(0, 0, null, false, null);
         }
         long now = System.currentTimeMillis();
-        List<TraceAggregate> traces = aggregateTraceRows(queryRecentRows())
+        List<TraceAggregate> traces = aggregateTraceRows(queryRecentRows(identityValues, now))
                 .stream()
                 .filter(trace -> matchesEntity(trace, identityValues))
                 .filter(trace -> trace.getStartTime() == null || trace.getStartTime() >= now - DEFAULT_LOOKBACK_MILLIS)
@@ -151,12 +167,37 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
                                                  String serviceName, String serviceNamespace, String environment,
                                                  int pageIndex, int pageSize, Boolean hideInternal) {
         Map<String, Set<String>> identityValues = entityId == null ? Collections.emptyMap() : canonicalIdentityValues(loadEntityContext(entityId));
-        List<TraceAggregate> filtered = aggregateTraceRows(queryRowsForList(traceId, serviceName, hideInternal)).stream()
+        PageRequest pageRequest = PageRequest.of(Math.max(pageIndex, 0), Math.max(pageSize, 1));
+        if (!StringUtils.hasText(traceId) && traceQueryRepository.supportsTraceListRows()) {
+            List<Map<String, Object>> rows = traceQueryRepository.queryTraceListRows(
+                    start,
+                    end,
+                    errorOnly,
+                    serviceName,
+                    serviceNamespace,
+                    environment,
+                    AuthTokenRequestContext.currentWorkspaceId(),
+                    identityValues,
+                    hideInternal,
+                    Math.toIntExact(pageRequest.getOffset()),
+                    pageRequest.getPageSize()
+            );
+            List<TraceListItemDto> items = rows.stream()
+                    .map(this::toTraceListItem)
+                    .toList();
+            long total = rows.stream()
+                    .map(row -> readLongValue(row, "total_count", "totalCount"))
+                    .filter(Objects::nonNull)
+                    .findFirst()
+                    .orElse((long) pageRequest.getOffset() + items.size());
+            return new PageImpl<>(items, pageRequest, total);
+        }
+        List<TraceAggregate> filtered = aggregateTraceRows(queryRowsForList(traceId, start, end, serviceName,
+                serviceNamespace, environment, identityValues, hideInternal)).stream()
                 .filter(trace -> matchesTraceFilters(trace, identityValues, start, end, traceId, errorOnly,
                         serviceName, serviceNamespace, environment, hideInternal))
                 .sorted(Comparator.comparing(TraceAggregate::getStartTime, Comparator.nullsLast(Comparator.reverseOrder())))
                 .toList();
-        PageRequest pageRequest = PageRequest.of(Math.max(pageIndex, 0), Math.max(pageSize, 1));
         int safeStart = Math.min((int) pageRequest.getOffset(), filtered.size());
         int safeEnd = Math.min(safeStart + pageRequest.getPageSize(), filtered.size());
         List<TraceListItemDto> items = filtered.subList(safeStart, safeEnd).stream()
@@ -171,7 +212,8 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
             return null;
         }
         Map<String, Set<String>> identityValues = entityId == null ? Collections.emptyMap() : canonicalIdentityValues(loadEntityContext(entityId));
-        TraceAggregate aggregate = aggregateTraceRows(queryTraceRows(traceId)).stream()
+        TraceAggregate aggregate = aggregateTraceRows(queryTraceRows(traceId, null, null, null, null, null,
+                identityValues, false)).stream()
                 .filter(trace -> identityValues.isEmpty() || matchesEntity(trace, identityValues))
                 .findFirst()
                 .orElse(null);
@@ -214,15 +256,95 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         return traceQueryRepository.queryRecentTraceRows(TRACE_LIST_SAMPLE_LIMIT);
     }
 
-    private List<Map<String, Object>> queryRowsForList(String traceId, String serviceName, Boolean hideInternal) {
+    private List<Map<String, Object>> queryRecentRows(Map<String, Set<String>> identityValues, long now) {
+        if (CollectionUtils.isEmpty(identityValues)) {
+            return queryRecentRows();
+        }
+        return traceQueryRepository.queryRecentTraceRows(
+                TRACE_LIST_SAMPLE_LIMIT,
+                Math.max(0L, now - DEFAULT_LOOKBACK_MILLIS),
+                now,
+                preferredIdentityValue(identityValues, "service.name"),
+                preferredIdentityValue(identityValues, "service.namespace"),
+                preferredIdentityValue(identityValues, "deployment.environment.name"),
+                AuthTokenRequestContext.currentWorkspaceId(),
+                identityValues,
+                false
+        );
+    }
+
+    private List<Map<String, Object>> queryRowsForList(String traceId,
+                                                       Long start,
+                                                       Long end,
+                                                       String serviceName,
+                                                       String serviceNamespace,
+                                                       String environment,
+                                                       Map<String, Set<String>> identityValues,
+                                                       Boolean hideInternal) {
         if (StringUtils.hasText(traceId)) {
-            return queryTraceRows(traceId);
+            return queryTraceRows(traceId, start, end, serviceName, serviceNamespace, environment,
+                    identityValues, hideInternal);
         }
-        return traceQueryRepository.queryRecentTraceRows(TRACE_LIST_SAMPLE_LIMIT, serviceName, hideInternal);
+        return traceQueryRepository.queryRecentTraceRows(
+                TRACE_LIST_SAMPLE_LIMIT,
+                start,
+                end,
+                serviceName,
+                serviceNamespace,
+                environment,
+                AuthTokenRequestContext.currentWorkspaceId(),
+                identityValues,
+                hideInternal
+        );
     }
 
     private List<Map<String, Object>> queryTraceRows(String traceId) {
-        return traceQueryRepository.queryTraceRows(traceId, TRACE_DETAIL_LIMIT);
+        return queryTraceRows(traceId, null, null, null, null, null, Collections.emptyMap(), false);
+    }
+
+    private List<Map<String, Object>> queryTraceRows(String traceId,
+                                                     Long start,
+                                                     Long end,
+                                                     String serviceName,
+                                                     String serviceNamespace,
+                                                     String environment,
+                                                     Map<String, Set<String>> identityValues,
+                                                     Boolean hideInternal) {
+        String workspaceId = AuthTokenRequestContext.currentWorkspaceId();
+        if (!hasTraceRowPushdownFilters(start, end, serviceName, serviceNamespace, environment,
+                workspaceId, identityValues, hideInternal)) {
+            return traceQueryRepository.queryTraceRows(traceId, TRACE_DETAIL_LIMIT);
+        }
+        return traceQueryRepository.queryTraceRows(
+                traceId,
+                TRACE_DETAIL_LIMIT,
+                start,
+                end,
+                serviceName,
+                serviceNamespace,
+                environment,
+                workspaceId,
+                identityValues,
+                hideInternal
+        );
+    }
+
+    private boolean hasTraceRowPushdownFilters(Long start,
+                                               Long end,
+                                               String serviceName,
+                                               String serviceNamespace,
+                                               String environment,
+                                               String workspaceId,
+                                               Map<String, Set<String>> identityValues,
+                                               Boolean hideInternal) {
+        return start != null
+                || end != null
+                || StringUtils.hasText(serviceName)
+                || StringUtils.hasText(serviceNamespace)
+                || StringUtils.hasText(environment)
+                || StringUtils.hasText(workspaceId)
+                || !CollectionUtils.isEmpty(identityValues)
+                || Boolean.TRUE.equals(hideInternal);
     }
 
     private List<TraceAggregate> aggregateTraceRows(List<Map<String, Object>> rows) {
@@ -235,12 +357,41 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
             if (!StringUtils.hasText(span.getTraceId())) {
                 continue;
             }
+            if (!matchesRequestWorkspace(span)) {
+                continue;
+            }
             TraceAggregate aggregate = traceMap.computeIfAbsent(span.getTraceId(), TraceAggregate::new);
             aggregate.accept(span);
         }
         return traceMap.values().stream().map(TraceAggregate::normalize).toList();
     }
 
+    private boolean matchesRequestWorkspace(TraceSpanNodeDto span) {
+        String workspaceId = AuthTokenRequestContext.currentWorkspaceId();
+        if (!StringUtils.hasText(workspaceId)) {
+            return true;
+        }
+        String spanWorkspaceId = resolveWorkspaceId(span);
+        String normalizedWorkspaceId = AuthTokenScopes.normalizeWorkspaceId(workspaceId);
+        if (!StringUtils.hasText(spanWorkspaceId)) {
+            return AuthTokenScopes.DEFAULT_WORKSPACE_ID.equals(normalizedWorkspaceId);
+        }
+        return normalizedWorkspaceId.equals(AuthTokenScopes.normalizeWorkspaceId(spanWorkspaceId));
+    }
+
+    private String resolveWorkspaceId(TraceSpanNodeDto span) {
+        if (span == null || CollectionUtils.isEmpty(span.getResourceAttributes())) {
+            return null;
+        }
+        for (String key : WORKSPACE_RESOURCE_KEYS) {
+            String value = trimText(span.getResourceAttributes().get(key));
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
     private boolean matchesTraceFilters(TraceAggregate trace, Map<String, Set<String>> identityValues, Long start, Long end,
                                         String traceId, Boolean errorOnly, String serviceName, String serviceNamespace,
                                         String environment, Boolean hideInternal) {
@@ -358,6 +509,14 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         }
     }
 
+    private String preferredIdentityValue(Map<String, Set<String>> identityValues, String key) {
+        Set<String> values = identityValues.get(key);
+        if (CollectionUtils.isEmpty(values)) {
+            return null;
+        }
+        return values.stream().filter(StringUtils::hasText).findFirst().orElse(null);
+    }
+
     private TraceListItemDto toTraceListItem(TraceAggregate aggregate) {
         return new TraceListItemDto(
                 aggregate.getTraceId(),
@@ -373,6 +532,27 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         );
     }
 
+    private TraceListItemDto toTraceListItem(Map<String, Object> row) {
+        Map<String, String> resourceAttributes = parseAttributes(row.get("resource_attributes"), "resource_attributes.", row);
+        String serviceName = defaultText(readText(row, "service_name"), resourceAttributes.get("service.name"));
+        String serviceNamespace = defaultText(readText(row, "service_namespace"),
+                resourceAttributes.get("service.namespace"));
+        String status = normalizeStatus(defaultText(readText(row, "span_status_code"), readText(row, "status")));
+        Integer errorSpanCount = readNonNegativeIntValue(row, "error_span_count", "errorSpanCount");
+        return new TraceListItemDto(
+                readText(row, "trace_id"),
+                defaultText(readText(row, "root_span_id"), readText(row, "span_id")),
+                serviceName,
+                serviceNamespace,
+                defaultText(readText(row, "root_span_name"), defaultText(readText(row, "span_name"), readText(row, "name"))),
+                readNonNegativeLong(row, "duration_nano"),
+                status,
+                readTimestamp(row, "timestamp"),
+                errorSpanCount == null ? ("error".equals(status) ? 1 : 0) : errorSpanCount,
+                resourceAttributes
+        );
+    }
+
     private TraceDetailDto toTraceDetail(TraceAggregate aggregate) {
         return new TraceDetailDto(
                 aggregate.getTraceId(),
@@ -405,7 +585,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         span.setTraceState(readText(row, "trace_state"));
         span.setScopeName(readText(row, "scope_name"));
         span.setScopeVersion(readText(row, "scope_version"));
-        span.setDurationNanos(readLong(row, "duration_nano"));
+        span.setDurationNanos(readNonNegativeLong(row, "duration_nano"));
         span.setStartTime(readTimestamp(row, "timestamp"));
         span.setHighlighted(isErrorStatus(status));
         span.setResourceAttributes(resourceAttributes);
@@ -430,7 +610,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
                     readLongValue(item, "time_unix_nano", "timeUnixNano"),
                     defaultText(readTextValue(item, "name"), readTextValue(item, "event_name")),
                     readObjectMap(item, "attributes"),
-                    readIntValue(item, "dropped_attributes_count", "droppedAttributesCount")
+                    readNonNegativeIntValue(item, "dropped_attributes_count", "droppedAttributesCount")
             ));
         }
         return events;
@@ -451,7 +631,7 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
                     defaultText(readTextValue(item, "span_id"), readTextValue(item, "spanId")),
                     defaultText(readTextValue(item, "trace_state"), readTextValue(item, "traceState")),
                     readObjectMap(item, "attributes"),
-                    readIntValue(item, "dropped_attributes_count", "droppedAttributesCount")
+                    readNonNegativeIntValue(item, "dropped_attributes_count", "droppedAttributesCount")
             ));
         }
         return links;
@@ -507,18 +687,9 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
             if (!StringUtils.hasText(key) || !row.containsKey(key)) {
                 continue;
             }
-            Object value = row.get(key);
-            if (value instanceof Number number) {
-                return number.longValue();
-            }
-            String text = trimText(Objects.toString(value, null));
-            if (!StringUtils.hasText(text)) {
-                continue;
-            }
-            try {
-                return Long.parseLong(text);
-            } catch (NumberFormatException ignored) {
-                // Try next key.
+            Long value = coerceLong(row.get(key));
+            if (value != null) {
+                return value;
             }
         }
         return null;
@@ -526,7 +697,21 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
 
     private Integer readIntValue(Map<String, Object> row, String... keys) {
         Long value = readLongValue(row, keys);
-        return value == null ? null : value.intValue();
+        if (value == null) {
+            return null;
+        }
+        if (value > Integer.MAX_VALUE) {
+            return Integer.MAX_VALUE;
+        }
+        if (value < Integer.MIN_VALUE) {
+            return Integer.MIN_VALUE;
+        }
+        return value.intValue();
+    }
+
+    private Integer readNonNegativeIntValue(Map<String, Object> row, String... keys) {
+        Integer value = readIntValue(row, keys);
+        return value == null ? null : Math.max(0, value);
     }
 
     private Map<String, String> parseAttributes(Object rawValue, String prefix, Map<String, Object> row) {
@@ -610,7 +795,24 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
     }
 
     private Long readLong(Map<String, Object> row, String key) {
-        Object value = row.get(key);
+        return coerceLong(row.get(key));
+    }
+
+    private Long readNonNegativeLong(Map<String, Object> row, String key) {
+        Long value = readLong(row, key);
+        return value == null ? null : Math.max(0L, value);
+    }
+
+    private Long coerceLong(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof BigInteger bigInteger) {
+            return clampLong(bigInteger);
+        }
+        if (value instanceof BigDecimal bigDecimal) {
+            return clampLong(bigDecimal);
+        }
         if (value instanceof Number number) {
             return number.longValue();
         }
@@ -621,8 +823,32 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         try {
             return Long.parseLong(text);
         } catch (NumberFormatException ex) {
-            return null;
+            try {
+                return clampLong(new BigInteger(text));
+            } catch (NumberFormatException ignored) {
+                return null;
+            }
+        }
+    }
+
+    private Long clampLong(BigInteger value) {
+        if (value.compareTo(LONG_MAX_VALUE) > 0) {
+            return Long.MAX_VALUE;
+        }
+        if (value.compareTo(LONG_MIN_VALUE) < 0) {
+            return Long.MIN_VALUE;
+        }
+        return value.longValue();
+    }
+
+    private Long clampLong(BigDecimal value) {
+        if (value.compareTo(LONG_MAX_DECIMAL) > 0) {
+            return Long.MAX_VALUE;
+        }
+        if (value.compareTo(LONG_MIN_DECIMAL) < 0) {
+            return Long.MIN_VALUE;
         }
+        return value.longValue();
     }
 
     private Long readTimestamp(Map<String, Object> row, String key) {
@@ -630,35 +856,61 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
         if (value instanceof Timestamp timestamp) {
             return timestamp.toInstant().toEpochMilli();
         }
+        if (value instanceof java.util.Date date) {
+            return date.getTime();
+        }
         if (value instanceof LocalDateTime dateTime) {
             return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
         }
         if (value instanceof Instant instant) {
             return instant.toEpochMilli();
         }
+        if (value instanceof ZonedDateTime dateTime) {
+            return dateTime.toInstant().toEpochMilli();
+        }
         if (value instanceof Number number) {
-            long numeric = number.longValue();
-            if (numeric > 9_999_999_999L) {
-                return numeric / 1_000_000L;
-            }
-            return numeric;
+            return normalizeEpochMillis(number.longValue());
         }
         String text = trimText(Objects.toString(value, null));
         if (!StringUtils.hasText(text)) {
             return null;
         }
+        if (text.matches("-?\\d+")) {
+            Long numeric = coerceLong(text);
+            return numeric == null ? null : normalizeEpochMillis(numeric);
+        }
         try {
             return Instant.parse(text).toEpochMilli();
+        } catch (Exception ignored) {
+            // Try offset and local date time fallbacks.
+        }
+        String normalizedText = text.replace(' ', 'T');
+        try {
+            return OffsetDateTime.parse(normalizedText).toInstant().toEpochMilli();
         } catch (Exception ignored) {
             // Try local date time fallback.
         }
         try {
-            return LocalDateTime.parse(text.replace(' ', 'T')).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+            return LocalDateTime.parse(normalizedText).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
         } catch (Exception ignored) {
             return null;
         }
     }
 
+    private long normalizeEpochMillis(long numeric) {
+        long magnitude = numeric == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(numeric);
+        if (magnitude < 100_000_000_000L) {
+            return numeric * 1_000L;
+        }
+        if (magnitude < 100_000_000_000_000L) {
+            return numeric;
+        }
+        if (magnitude < 100_000_000_000_000_000L) {
+            return numeric / 1_000L;
+        }
+        return numeric / 1_000_000L;
+    }
+
     private String normalizeStatus(String rawStatus) {
         String normalized = trimText(rawStatus);
         if (normalized == null) {
@@ -811,8 +1063,10 @@ public class EntityTraceQueryServiceImpl implements EntityTraceQueryService {
             for (TraceSpanNodeDto root : roots) {
                 appendNode(root, children, ordered);
             }
-            if (ordered.isEmpty()) {
-                ordered.addAll(this.spans);
+            for (TraceSpanNodeDto span : this.spans) {
+                if (!ordered.contains(span)) {
+                    ordered.add(span);
+                }
             }
             return ordered;
         }
diff --git a/hertzbeat-observability/src/main/resources/greptime/flows/hertzbeat_apm_red_1m.sql b/hertzbeat-observability/src/main/resources/greptime/flows/hertzbeat_apm_red_1m.sql
index f4ec606247..25499c4bf6 100644
--- a/hertzbeat-observability/src/main/resources/greptime/flows/hertzbeat_apm_red_1m.sql
+++ b/hertzbeat-observability/src/main/resources/greptime/flows/hertzbeat_apm_red_1m.sql
@@ -20,16 +20,20 @@ SINK TO hertzbeat_apm_red_1m
 EXPIRE AFTER '6 hours'::INTERVAL
 AS SELECT
   date_bin('1 minute'::INTERVAL, "timestamp") AS time_window,
-  service_name,
-  span_name AS operation,
-  span_kind,
+  COALESCE(NULLIF(service_name, ''), 'unknown_service') AS service_name,
+  COALESCE(NULLIF(span_name, ''), 'unknown_operation') AS operation,
+  CASE
+    WHEN span_kind IN ('SPAN_KIND_SERVER', 'SERVER') THEN 'SERVER'
+    WHEN span_kind IN ('SPAN_KIND_CONSUMER', 'CONSUMER') THEN 'CONSUMER'
+    ELSE 'UNKNOWN'
+  END AS span_kind,
   json_get_string(resource_attributes, '$["hertzbeat.workspace_id"]') AS workspace_id,
   json_get_string(resource_attributes, '$["hertzbeat.entity_id"]') AS entity_id,
-  json_get_string(resource_attributes, '$["deployment.environment"]') AS deployment_environment,
+  json_get_string(resource_attributes, '$["deployment.environment.name"]') AS deployment_environment,
   json_get_string(resource_attributes, '$["service.namespace"]') AS service_namespace,
   COUNT(*) AS calls_total,
-  SUM(CASE WHEN span_status_code = 'STATUS_CODE_ERROR' THEN 1 ELSE 0 END) AS error_total,
-  SUM(duration_nano) AS duration_sum_nano,
+  SUM(CASE WHEN span_status_code IN ('STATUS_CODE_ERROR', 'ERROR') THEN 1 ELSE 0 END) AS error_total,
+  COALESCE(SUM(duration_nano), 0) AS duration_sum_nano,
   COUNT(duration_nano) AS duration_count,
   uddsketch_state(128, 0.01, duration_nano) AS duration_sketch
 FROM hzb_traces
diff --git a/hertzbeat-observability/src/main/resources/greptime/tables/hzb_traces.sql b/hertzbeat-observability/src/main/resources/greptime/tables/hzb_traces.sql
new file mode 100644
index 0000000000..3aa7f0ee36
--- /dev/null
+++ b/hertzbeat-observability/src/main/resources/greptime/tables/hzb_traces.sql
@@ -0,0 +1,21 @@
+CREATE TABLE IF NOT EXISTS hzb_traces (
+  "timestamp" TIMESTAMP(9) TIME INDEX,
+  "timestamp_end" TIMESTAMP(9) NULL,
+  "duration_nano" BIGINT UNSIGNED NULL,
+  "trace_id" STRING NULL SKIPPING INDEX WITH(granularity = '10240', type = 'BLOOM'),
+  "span_id" STRING NULL,
+  "parent_span_id" STRING NULL,
+  "span_kind" STRING NULL,
+  "span_name" STRING NULL,
+  "span_status_code" STRING NULL,
+  "span_status_message" STRING NULL,
+  "trace_state" STRING NULL,
+  "scope_name" STRING NULL,
+  "scope_version" STRING NULL,
+  "service_name" STRING NULL,
+  "resource_attributes" JSON NULL,
+  "span_attributes" JSON NULL,
+  "span_events" JSON NULL,
+  "span_links" JSON NULL,
+  PRIMARY KEY("service_name")
+) WITH (append_mode = true, table_data_model = 'greptime_trace_v1');
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/config/OpenTelemetryConfigTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/config/OpenTelemetryConfigTest.java
index f4c5602c2d..89a3575f16 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/config/OpenTelemetryConfigTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/config/OpenTelemetryConfigTest.java
@@ -20,7 +20,10 @@ package org.apache.hertzbeat.observability.config;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 import java.util.Map;
+import org.apache.hertzbeat.observability.ingestion.forwarder.GreptimeOtlpForwarder;
 import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
 import org.junit.jupiter.api.Test;
@@ -44,5 +47,61 @@ class OpenTelemetryConfigTest {
         );
 
         assertEquals(WarehouseConstants.LOG_TABLE_NAME, headers.get("X-Greptime-Log-Table-Name"));
+        assertEquals(GreptimeOtlpForwarder.LOG_PIPELINE_NAME, headers.get("X-Greptime-Log-Pipeline-Name"));
+    }
+
+    @Test
+    void greptimeSdkLogAndTraceExportersUseConfiguredDatabaseHeader() throws Exception {
+        GreptimeProperties properties = new GreptimeProperties(true, "127.0.0.1:4001",
+                "http://127.0.0.1:4000", " observability ", "greptime", "greptime", "1d");
+
+        Map<String, String> logHeaders = greptimeHeaders("buildGreptimeOtlpLogHeaders", properties);
+        Map<String, String> traceHeaders = greptimeHeaders("buildGreptimeOtlpTraceHeaders", properties);
+
+        assertEquals("observability", logHeaders.get("X-Greptime-DB-Name"));
+        assertEquals("observability", traceHeaders.get("X-Greptime-DB-Name"));
+        assertEquals("hzb_traces", traceHeaders.get("X-Greptime-Trace-Table-Name"));
+        assertEquals("greptime_trace_v1", traceHeaders.get("X-Greptime-Pipeline-Name"));
+    }
+
+    @Test
+    void greptimeSdkLogAndTraceExportersNormalizeConfiguredEndpoint() throws Exception {
+        GreptimeProperties properties = new GreptimeProperties(true, "127.0.0.1:4001",
+                "  http://greptime:4000///  ", "public", "greptime", "greptime", "1d");
+
+        assertEquals("http://greptime:4000/v1/otlp/v1/logs",
+                greptimeEndpoint(properties, "/v1/otlp/v1/logs"));
+        assertEquals("http://greptime:4000/v1/otlp/v1/traces",
+                greptimeEndpoint(properties, "/v1/otlp/v1/traces"));
+    }
+
+    @Test
+    void greptimeSdkLogAndTraceExportersTrimBasicAuthCredentials() throws Exception {
+        GreptimeProperties properties = new GreptimeProperties(true, "127.0.0.1:4001",
+                "http://127.0.0.1:4000", "public", " greptime ", " secret ", "1d");
+        String expectedAuthorization = "Basic "
+                + Base64.getEncoder().encodeToString("greptime:secret".getBytes(StandardCharsets.UTF_8));
+
+        assertEquals(expectedAuthorization,
+                greptimeHeaders("buildGreptimeOtlpLogHeaders", properties).get("Authorization"));
+        assertEquals(expectedAuthorization,
+                greptimeHeaders("buildGreptimeOtlpTraceHeaders", properties).get("Authorization"));
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, String> greptimeHeaders(String methodName, GreptimeProperties properties) throws Exception {
+        Method method = OpenTelemetryConfig.class.getDeclaredMethod(methodName, GreptimeProperties.class);
+        method.setAccessible(true);
+        return (Map<String, String>) method.invoke(new OpenTelemetryConfig(), properties);
+    }
+
+    private String greptimeEndpoint(GreptimeProperties properties, String path) throws Exception {
+        Method method = OpenTelemetryConfig.class.getDeclaredMethod(
+                "greptimeOtlpEndpoint",
+                GreptimeProperties.class,
+                String.class
+        );
+        method.setAccessible(true);
+        return (String) method.invoke(new OpenTelemetryConfig(), properties, path);
     }
 }
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/adapter/OtlpLogProtocolAdapterTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/adapter/OtlpLogProtocolAdapterTest.java
index ea5fd6eaef..5fcbf88afa 100644
--- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/adapter/OtlpLogProtocolAdapterTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/adapter/OtlpLogProtocolAdapterTest.java
@@ -28,6 +28,7 @@ import io.opentelemetry.proto.logs.v1.ScopeLogs;
 import io.opentelemetry.proto.resource.v1.Resource;
 import io.opentelemetry.proto.common.v1.InstrumentationScope;
 import org.apache.hertzbeat.common.entity.log.LogEntry;
+import org.apache.hertzbeat.common.entity.observability.TelemetryIntakeSignalEvent;
 import org.apache.hertzbeat.common.queue.CommonDataQueue;
 import org.apache.hertzbeat.observability.logs.sse.LogSseManager;
 import org.junit.jupiter.api.BeforeEach;
@@ -45,7 +46,9 @@ import java.util.Map;
 import java.util.zip.GZIPOutputStream;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
@@ -146,6 +149,58 @@ class OtlpLogProtocolAdapterTest {
         assertEquals("binary log message", capturedList.get(0).getBody());
     }
 
+    @Test
+    void testIngestBinaryWithGzipContentEncodingOptionalWhitespace() throws Exception {
+        byte[] binaryPayload = gzip(createValidOtlpLogBinaryPayload());
+        HttpHeaders headers = new HttpHeaders();
+        headers.add(HttpHeaders.CONTENT_ENCODING, " gzip ");
+
+        adapter.ingestBinary(binaryPayload, headers);
+
+        ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
+        verify(commonDataQueue, never()).sendLogEntryToStorageBatch(anyList());
+        verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(listCaptor.capture());
+        List<LogEntry> capturedList = listCaptor.getValue();
+        assertNotNull(capturedList);
+        assertEquals(1, capturedList.size());
+        assertEquals("binary log message", capturedList.get(0).getBody());
+    }
+
+    @Test
+    void testIngestBinaryWithGzipContentEncodingBlankFirstValue() throws Exception {
+        byte[] binaryPayload = gzip(createValidOtlpLogBinaryPayload());
+        HttpHeaders headers = new HttpHeaders();
+        headers.add(HttpHeaders.CONTENT_ENCODING, " ");
+        headers.add(HttpHeaders.CONTENT_ENCODING, "gzip");
+
+        adapter.ingestBinary(binaryPayload, headers);
+
+        ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
+        verify(commonDataQueue, never()).sendLogEntryToStorageBatch(anyList());
+        verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(listCaptor.capture());
+        List<LogEntry> capturedList = listCaptor.getValue();
+        assertNotNull(capturedList);
+        assertEquals(1, capturedList.size());
+        assertEquals("binary log message", capturedList.get(0).getBody());
+    }
+
+    @Test
+    void testIngestBinaryWithGzipContentEncodingCommaSeparatedBlankValue() throws Exception {
+        byte[] binaryPayload = gzip(createValidOtlpLogBinaryPayload());
+        HttpHeaders headers = new HttpHeaders();
+        headers.add(HttpHeaders.CONTENT_ENCODING, " , gzip ");
+
+        adapter.ingestBinary(binaryPayload, headers);
+
+        ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
+        verify(commonDataQueue, never()).sendLogEntryToStorageBatch(anyList());
+        verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(listCaptor.capture());
+        List<LogEntry> capturedList = listCaptor.getValue();
+        assertNotNull(capturedList);
+        assertEquals(1, capturedList.size());
+        assertEquals("binary log message", capturedList.get(0).getBody());
+    }
+
     @Test
     void testIngestWithValidOtlpLogData() throws Exception {
         String otlpPayload = createValidOtlpLogPayload();
@@ -238,6 +293,98 @@ class OtlpLogProtocolAdapterTest {
         assertEquals(1, capturedEntry.getTraceFlags());
     }
 
+    @Test
+    void allZeroTraceAndSpanIdsAreNotPublishedAsLocalCorrelationEvidence() {
+        ExportLogsServiceRequest request = ExportLogsServiceRequest.newBuilder()
+            .addResourceLogs(ResourceLogs.newBuilder()
+                .setResource(Resource.newBuilder()
+                    .addAttributes(KeyValue.newBuilder()
+                        .setKey("service.name")
+                        .setValue(AnyValue.newBuilder().setStringValue("checkout").build())
+                        .build())
+                    .build())
+                .addScopeLogs(ScopeLogs.newBuilder()
+                    .setScope(InstrumentationScope.newBuilder().build())
+                    .addLogRecords(LogRecord.newBuilder()
+                        .setTimeUnixNano(System.currentTimeMillis() * 1_000_000)
+                        .setBody(AnyValue.newBuilder().setStringValue("zero ids should stay unlinked").build())
+                        .setTraceId(ByteString.fromHex("00000000000000000000000000000000"))
+                        .setSpanId(ByteString.fromHex("0000000000000000"))
+                        .build())
+                    .build())
+                .build())
+            .build();
+
+        adapter.ingestBinary(request.toByteArray());
+
+        ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
+        verify(commonDataQueue, never()).sendLogEntryToStorageBatch(anyList());
+        verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(listCaptor.capture());
+        verify(logSseManager, times(1)).broadcast(any(LogEntry.class));
+
+        LogEntry capturedEntry = listCaptor.getValue().get(0);
+        assertNull(capturedEntry.getTraceId());
+        assertNull(capturedEntry.getSpanId());
+
+        ArgumentCaptor<TelemetryIntakeSignalEvent> eventCaptor =
+                ArgumentCaptor.forClass(TelemetryIntakeSignalEvent.class);
+        verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture());
+        assertNull(eventCaptor.getValue().getTraceId());
+        assertNull(eventCaptor.getValue().getSpanId());
+    }
+
+    @Test
+    void redactsSensitiveLogBodyAndAttributesBeforeLocalPublication() {
+        ExportLogsServiceRequest request = ExportLogsServiceRequest.newBuilder()
+            .addResourceLogs(ResourceLogs.newBuilder()
+                .setResource(Resource.newBuilder()
+                    .addAttributes(KeyValue.newBuilder()
+                        .setKey("service.name")
+                        .setValue(AnyValue.newBuilder().setStringValue("checkout").build())
+                        .build())
+                    .addAttributes(KeyValue.newBuilder()
+                        .setKey("cloud.auth.token")
+                        .setValue(AnyValue.newBuilder().setStringValue("resource-token").build())
+                        .build())
+                    .build())
+                .addScopeLogs(ScopeLogs.newBuilder()
+                    .setScope(InstrumentationScope.newBuilder().build())
+                    .addLogRecords(LogRecord.newBuilder()
+                        .setTimeUnixNano(System.currentTimeMillis() * 1_000_000)
+                        .setBody(AnyValue.newBuilder()
+                            .setStringValue("failed login password=hunter2 token=abc123")
+                            .build())
+                        .addAttributes(KeyValue.newBuilder()
+                            .setKey("http.request.header.authorization")
+                            .setValue(AnyValue.newBuilder().setStringValue("Bearer live-token").build())
+                            .build())
+                        .addAttributes(KeyValue.newBuilder()
+                            .setKey("user.id")
+                            .setValue(AnyValue.newBuilder().setStringValue("alice").build())
+                            .build())
+                        .build())
+                    .build())
+                .build())
+            .build();
+
+        adapter.ingestBinary(request.toByteArray());
+
+        ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
+        verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(listCaptor.capture());
+        LogEntry capturedEntry = listCaptor.getValue().get(0);
+        assertEquals("checkout", capturedEntry.getResource().get("service_name"));
+        assertEquals("[REDACTED]", capturedEntry.getResource().get("cloud_auth_token"));
+        assertEquals("failed login password=[REDACTED] token=[REDACTED]", capturedEntry.getBody());
+        assertEquals("[REDACTED]", capturedEntry.getAttributes().get("http_request_header_authorization"));
+        assertEquals("alice", capturedEntry.getAttributes().get("user_id"));
+
+        String rendered = capturedEntry.toString();
+        assertFalse(rendered.contains("resource-token"));
+        assertFalse(rendered.contains("hunter2"));
+        assertFalse(rendered.contains("abc123"));
+        assertFalse(rendered.contains("live-token"));
+    }
+
     @Test
     void testIngestWithInvalidJsonContent() {
         String invalidJson = "{ invalid json content }";
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventReaderTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventReaderTest.java
new file mode 100644
index 0000000000..2713aab2a0
--- /dev/null
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/audit/GreptimeOtlpIngestionAuditEventReaderTest.java
@@ -0,0 +1,967 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.observability.ingestion.audit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.warehouse.db.GreptimeSqlQueryExecutor;
... 15915 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 11/50: feat(web-next): restore collector plugin label parity

Posted by zh...@apache.org.
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 cae4d7576509ad76380796d6553f7915344aaac4
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 01:02:59 2026 +0800

    feat(web-next): restore collector plugin label parity
---
 web-next/app/setting/collector/page.test.tsx       |  433 ++++++-
 web-next/app/setting/collector/page.tsx            |   44 +-
 .../setting/collector/setting-collector-page.tsx   |  340 ++++++
 web-next/app/setting/define/page.test.tsx          |  910 +++++++++++++-
 web-next/app/setting/define/page.tsx               |  153 +--
 .../app/setting/define/setting-define-page.tsx     |  257 ++++
 web-next/app/setting/labels/page.test.tsx          |  602 +++++++++-
 web-next/app/setting/labels/page.tsx               |   71 +-
 .../app/setting/labels/setting-labels-page.tsx     |  215 ++++
 web-next/app/setting/plugins/page.test.tsx         |  404 ++++++-
 web-next/app/setting/plugins/page.tsx              |   70 +-
 .../app/setting/plugins/setting-plugins-page.tsx   |  325 +++++
 .../pages/collector-manage-surface.test.tsx        |  830 ++++++++++++-
 .../components/pages/collector-manage-surface.tsx  |  645 +++++++++-
 .../components/pages/label-manage-surface.test.tsx |  887 +++++++++++++-
 web-next/components/pages/label-manage-surface.tsx |  493 ++++++--
 .../pages/plugin-manage-surface.test.tsx           | 1037 +++++++++++++++-
 .../components/pages/plugin-manage-surface.tsx     | 1253 +++++++++++++++++---
 .../pages/setting-define-surface.test.tsx          | 1151 ++++++++++++++----
 .../components/pages/setting-define-surface.tsx    |  751 ++++++++----
 web-next/lib/collector-health-evidence.test.ts     |   40 +-
 web-next/lib/collector-health-evidence.ts          |   33 +-
 web-next/lib/collector-manage/controller.test.ts   |   53 +-
 web-next/lib/collector-manage/controller.ts        |   81 ++
 web-next/lib/collector-manage/query-state.test.ts  |   12 +-
 web-next/lib/collector-manage/query-state.ts       |    6 +-
 web-next/lib/collector-manage/view-model.test.ts   |   29 +-
 web-next/lib/collector-manage/view-model.ts        |   32 +-
 web-next/lib/label-manage/controller.test.ts       |   87 +-
 web-next/lib/label-manage/controller.ts            |   26 +-
 web-next/lib/label-manage/query-state.test.ts      |    4 +-
 web-next/lib/label-manage/query-state.ts           |    6 +-
 web-next/lib/label-manage/view-model.test.ts       |   16 +-
 web-next/lib/label-manage/view-model.ts            |   25 +-
 web-next/lib/plugin-manage/controller.test.ts      |  128 +-
 web-next/lib/plugin-manage/controller.ts           |  140 ++-
 web-next/lib/plugin-manage/query-state.test.ts     |    6 +-
 web-next/lib/plugin-manage/query-state.ts          |    6 +-
 web-next/lib/plugin-manage/view-model.test.ts      |   33 +-
 web-next/lib/plugin-manage/view-model.ts           |   24 +-
 web-next/lib/setting-define/controller.test.ts     |  138 ++-
 web-next/lib/setting-define/controller.ts          |  156 ++-
 web-next/lib/setting-define/query-state.test.ts    |   25 +-
 web-next/lib/setting-define/query-state.ts         |   32 +-
 web-next/lib/setting-define/view-model.test.ts     |  186 ++-
 web-next/lib/setting-define/view-model.ts          |  126 +-
 46 files changed, 10801 insertions(+), 1520 deletions(-)

diff --git a/web-next/app/setting/collector/page.test.tsx b/web-next/app/setting/collector/page.test.tsx
index d34c05efa6..9f2406a33a 100644
--- a/web-next/app/setting/collector/page.test.tsx
+++ b/web-next/app/setting/collector/page.test.tsx
@@ -1,12 +1,21 @@
+// @vitest-environment jsdom
+
 import React from 'react';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 (globalThis as { React?: typeof React }).React = React;
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
+  lastSurfaceProps: null as null | Record<string, any>,
+  renderErrorMessage: null as string | null,
   renderData: {
     list: {
       content: [
@@ -31,23 +40,20 @@ const mockState = vi.hoisted(() => ({
 }));
 
 const apiMessageGet = vi.hoisted(() => vi.fn());
+const apiMessageDelete = vi.hoisted(() => vi.fn());
+const apiMessagePost = vi.hoisted(() => vi.fn());
+const apiMessagePut = vi.hoisted(() => vi.fn());
 const loadCollectorData = vi.hoisted(() => vi.fn());
+const buildCollectorDeployCommands = vi.hoisted(() => vi.fn());
+const deleteCollectors = vi.hoisted(() => vi.fn());
+const generateCollectorIdentity = vi.hoisted(() => vi.fn());
+const goOnlineCollectors = vi.hoisted(() => vi.fn());
+const goOfflineCollectors = vi.hoisted(() => vi.fn());
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'menu.advanced.collector': '采集集群',
-        'collector.deploy': '部署采集器',
-        'collector.online': '上线采集器',
-        'collector.offline': '下线采集器',
-        'collector.delete': '删除采集器',
-        'collector.name': '采集器名称',
-        'collector.status': '运行状态',
-        'common.refresh': '刷新',
-        'common.search': '搜索'
-      }
+      locale: 'zh-CN'
     })
   })
 }));
@@ -55,37 +61,144 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    renderError
   }: {
     children: (data: unknown) => React.ReactNode;
     load: () => Promise<unknown>;
+    renderError?: (message: string, retry: () => void) => React.ReactNode;
   }) => {
     mockState.lastLoad = load;
+    if (mockState.renderErrorMessage && renderError) {
+      return <div data-client-workbench="true">{renderError(mockState.renderErrorMessage, vi.fn())}</div>;
+    }
     return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
   }
 }));
 
 vi.mock('@/components/pages/collector-manage-surface', () => ({
-  CollectorManageSurface: ({ search, data }: any) => (
-    <div data-collector-manage-surface="otlp-cold-collector-console" data-collector-manage-style-baseline="hertzbeat-cold-matte">
-      <section data-collector-admin-layout="full-width-admin-list">
-        <span>刷新</span>
-        <span>部署采集器</span>
-        <span>上线采集器</span>
-        <span>下线采集器</span>
-        <span>删除采集器</span>
-        <span>搜索</span>
-        <span>采集集群</span>
-        <span>{search}</span>
-        <span>{data.list.content[0].collector.name}</span>
-        <span>{data.list.content[0].collector.ip}</span>
-      </section>
-    </div>
-  )
+  CollectorManageSurface: (props: any) => {
+    mockState.lastSurfaceProps = props;
+    const {
+      search,
+      data,
+      selectedCollectors,
+      actionMessage,
+      actionError,
+      actionMeta,
+      actionTone,
+      actionFeedbackContract,
+      loadError,
+      deleteTarget,
+      isDeletePending,
+      isLoadPending,
+      deployOpen,
+      deployName,
+      deployIdentity,
+      deployError,
+      deployCopyMessage,
+      isDeployPending,
+      deployNameValidationVisible,
+      onDeploy,
+      onDeployNameChange,
+      onDeployGenerate,
+      onDeployClose
+    } = props;
+    return (
+      <div
+        data-collector-manage-surface="otlp-cold-collector-console"
+        data-collector-manage-style-baseline="hertzbeat-cold-matte"
+        data-collector-delete-warning-contract="angular-no-select-warning"
+        data-collector-delete-confirm-contract="angular-modal-confirm"
+        data-collector-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"
+        data-collector-delete-feedback-contract="angular-delete-notify"
+        data-collector-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+        data-collector-delete-api-contract="angular-repeated-collectors-query"
+        data-collector-delete-page-clamp-contract="angular-update-page-index"
+        data-collector-delete-page-clamp-owner="route-state-contract"
+        data-collector-search-submit-contract="angular-enter-and-clear"
+        data-collector-search-clear-contract="angular-cleared-load"
+        data-collector-search-clear-owner="shared-search-row"
+        data-collector-operate-warning-contract="angular-no-select-online-offline"
+        data-collector-operate-confirm-contract="angular-modal-confirm"
+        data-collector-operate-confirm-contract-owner="hertzbeat-ui-confirm-dialog"
+        data-collector-operate-feedback-contract="angular-operate-notify"
+        data-collector-operate-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+        data-collector-operate-api-contract="angular-put-repeated-collectors-query"
+        data-collector-mutation-loading-contract="angular-nz-table-loading"
+        data-collector-load-failure-contract="angular-console-only-shell"
+        data-collector-load-failure-owner="collector-route-controller"
+        data-collector-load-failure={loadError ? 'angular-console-only-shell' : 'none'}
+        data-collector-table-loading-contract="angular-load-collectors-table"
+        data-collector-table-loading-owner="angular-nz-table-loading"
+        data-collector-table-loading-scope="load-search-refresh-pagination-mutation"
+        data-collector-table-loading-source={isLoadPending || isDeletePending ? 'route-load-or-mutation' : 'idle'}
+        data-collector-deploy-modal-contract="angular-generate-identity-modal"
+        data-collector-deploy-api-contract="angular-post-generate-identity"
+        data-collector-deploy-failure-contract="angular-apply-fail-notification"
+        data-collector-deploy-validation-contract="angular-submit-marks-required"
+        data-collector-deploy-validation-raw-contract="angular-required-before-trim"
+        data-collector-deploy-validation-raw-owner="collector-route-controller"
+        data-collector-deploy-loading-contract="angular-nz-ok-loading"
+        data-collector-deploy-mask-contract="angular-mask-closable-false"
+        data-collector-deploy-width-contract="angular-width-45-percent"
+        data-collector-deploy-field-layout-contract="angular-label-7-control-12"
+        data-collector-deploy-field-layout-owner="route-form-field-grid"
+        data-collector-deploy-command-contract="angular-docker-package-shells"
+        data-collector-deploy-command-owner="hertzbeat-ui-code-editor"
+        data-collector-deploy-package-link-contract="angular-github-releases-link"
+        data-collector-deploy-copy-contract="angular-copy-identity"
+        data-collector-deploy-copy-feedback-contract="angular-copy-success-duration-800"
+        data-collector-deploy-close-reset-contract="angular-close-clears-name"
+        data-collector-deploy-close-reset-owner="collector-route-controller"
+        data-collector-deploy-close-pending-result-contract="angular-close-ignores-late-generate"
+        data-collector-deploy-close-pending-result-owner="collector-route-controller"
+        data-collector-pagination-contract="angular-search-pagination"
+        data-collector-pagination-owner="hertzbeat-ui-pagination-bar"
+      >
+        <section data-collector-admin-layout="full-width-admin-list">
+          <span>刷新</span>
+          <span>部署采集器</span>
+          <span>上线采集器</span>
+          <span>下线采集器</span>
+          <span>删除采集器</span>
+          <span>搜索</span>
+          <span>采集集群</span>
+          <span>{search}</span>
+          <span>{selectedCollectors?.length ?? 0}</span>
+          <span>{actionMessage ?? 'no-action-message'}</span>
+          <span>{actionError ?? 'no-action-error'}</span>
+          <span>{actionMeta ?? 'no-action-meta'}</span>
+          <span>{actionTone ?? 'no-action-tone'}</span>
+          <span>{actionFeedbackContract ?? 'no-action-feedback-contract'}</span>
+          <span>{deleteTarget ? deleteTarget.label : 'no-delete-target'}</span>
+          <span>{isDeletePending ? 'delete-pending' : 'delete-idle'}</span>
+          <span>{isLoadPending ? 'route-load-pending' : 'route-load-idle'}</span>
+          <span data-test-deploy-open={deployOpen ? 'true' : 'false'}>{deployOpen ? 'deploy-open' : 'deploy-closed'}</span>
+          <span data-test-deploy-name={deployName ?? ''}>{deployName ?? ''}</span>
+          <span data-test-deploy-identity={deployIdentity ?? ''}>{deployIdentity ?? ''}</span>
+          <span data-test-deploy-error={deployError ?? ''}>{deployError ?? ''}</span>
+          <span data-test-deploy-copy={deployCopyMessage ?? ''}>{deployCopyMessage ?? ''}</span>
+          <span data-test-deploy-pending={isDeployPending ? 'true' : 'false'}>{isDeployPending ? 'deploy-pending' : 'deploy-idle'}</span>
+          <span>{deployNameValidationVisible ? 'deploy-name-dirty-invalid' : 'deploy-name-pristine'}</span>
+          <span>{data.list.content[0]?.collector.name ?? 'no-collector-name'}</span>
+          <span>{data.list.content[0]?.collector.ip ?? 'no-collector-ip'}</span>
+          <span>collector-pagination</span>
+          <button data-test-deploy-open-button="true" onClick={onDeploy}>deploy</button>
+          <button data-test-deploy-name-button="true" onClick={() => onDeployNameChange?.(' edge-b ')}>name</button>
+          <button data-test-deploy-generate-button="true" onClick={onDeployGenerate}>generate</button>
+          <button data-test-deploy-close-button="true" onClick={onDeployClose}>close</button>
+        </section>
+      </div>
+    );
+  }
 }));
 
 vi.mock('@/lib/api-client', () => ({
-  apiMessageGet
+  apiMessageGet,
+  apiMessageDelete,
+  apiMessagePost,
+  apiMessagePut
 }));
 
 vi.mock('@/lib/format', () => ({
@@ -93,17 +206,45 @@ vi.mock('@/lib/format', () => ({
 }));
 
 vi.mock('@/lib/collector-manage/controller', () => ({
-  loadCollectorData
+  loadCollectorData,
+  buildCollectorDeployCommands,
+  deleteCollectors,
+  generateCollectorIdentity,
+  goOnlineCollectors,
+  goOfflineCollectors
 }));
 
 describe('setting collector page', () => {
+  let interactionContainer: HTMLDivElement | null = null;
+  let interactionRoot: Root | null = null;
+
   beforeEach(() => {
     mockState.lastLoad = null;
+    mockState.lastSurfaceProps = null;
+    mockState.renderErrorMessage = null;
     apiMessageGet.mockReset();
+    apiMessageDelete.mockReset();
+    apiMessagePost.mockReset();
+    apiMessagePut.mockReset();
     loadCollectorData.mockReset().mockImplementation(async (apiGet, query) => {
-      await apiGet(`/collector?pageIndex=0&pageSize=8${query.search ? `&name=${query.search}` : ''}`);
+      await apiGet(`/collector?pageIndex=${query.pageIndex}&pageSize=${query.pageSize}${query.search ? `&name=${query.search}` : ''}`);
       return mockState.renderData;
     });
+    buildCollectorDeployCommands.mockReset().mockReturnValue({ dockerShell: 'docker shell', packageShell: 'package shell' });
+    deleteCollectors.mockReset().mockResolvedValue(undefined);
+    generateCollectorIdentity.mockReset().mockResolvedValue({ identity: 'identity-123', host: '10.0.0.10' });
+    goOnlineCollectors.mockReset().mockResolvedValue(undefined);
+    goOfflineCollectors.mockReset().mockResolvedValue(undefined);
+  });
+
+  afterEach(async () => {
+    await act(async () => {
+      interactionRoot?.unmount();
+      await Promise.resolve();
+    });
+    interactionRoot = null;
+    interactionContainer?.remove();
+    interactionContainer = null;
   });
 
   it('renders the collector toolbar and table shell inside the route contract', async () => {
@@ -113,6 +254,53 @@ describe('setting collector page', () => {
     expect(html).toContain('data-client-workbench="true"');
     expect(html).toContain('data-collector-manage-surface="otlp-cold-collector-console"');
     expect(html).toContain('data-collector-manage-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-collector-delete-warning-contract="angular-no-select-warning"');
+    expect(html).toContain('data-collector-delete-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-collector-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-collector-delete-feedback-contract="angular-delete-notify"');
+    expect(html).toContain('data-collector-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-delete-api-contract="angular-repeated-collectors-query"');
+    expect(html).toContain('data-collector-delete-page-clamp-contract="angular-update-page-index"');
+    expect(html).toContain('data-collector-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-collector-search-submit-contract="angular-enter-and-clear"');
+    expect(html).toContain('data-collector-search-clear-contract="angular-cleared-load"');
+    expect(html).toContain('data-collector-search-clear-owner="shared-search-row"');
+    expect(html).toContain('data-collector-operate-warning-contract="angular-no-select-online-offline"');
+    expect(html).toContain('data-collector-operate-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-collector-operate-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-collector-operate-feedback-contract="angular-operate-notify"');
+    expect(html).toContain('data-collector-operate-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-operate-api-contract="angular-put-repeated-collectors-query"');
+    expect(html).toContain('data-collector-mutation-loading-contract="angular-nz-table-loading"');
+    expect(html).toContain('data-collector-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-collector-load-failure-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-load-failure="none"');
+    expect(html).toContain('data-collector-table-loading-contract="angular-load-collectors-table"');
+    expect(html).toContain('data-collector-table-loading-owner="angular-nz-table-loading"');
+    expect(html).toContain('data-collector-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-collector-table-loading-source="idle"');
+    expect(html).toContain('data-collector-deploy-modal-contract="angular-generate-identity-modal"');
+    expect(html).toContain('data-collector-deploy-api-contract="angular-post-generate-identity"');
+    expect(html).toContain('data-collector-deploy-failure-contract="angular-apply-fail-notification"');
+    expect(html).toContain('data-collector-deploy-validation-contract="angular-submit-marks-required"');
+    expect(html).toContain('data-collector-deploy-validation-raw-contract="angular-required-before-trim"');
+    expect(html).toContain('data-collector-deploy-validation-raw-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-deploy-loading-contract="angular-nz-ok-loading"');
+    expect(html).toContain('data-collector-deploy-mask-contract="angular-mask-closable-false"');
+    expect(html).toContain('data-collector-deploy-width-contract="angular-width-45-percent"');
+    expect(html).toContain('data-collector-deploy-field-layout-contract="angular-label-7-control-12"');
+    expect(html).toContain('data-collector-deploy-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-collector-deploy-command-contract="angular-docker-package-shells"');
+    expect(html).toContain('data-collector-deploy-command-owner="hertzbeat-ui-code-editor"');
+    expect(html).toContain('data-collector-deploy-package-link-contract="angular-github-releases-link"');
+    expect(html).toContain('data-collector-deploy-copy-contract="angular-copy-identity"');
+    expect(html).toContain('data-collector-deploy-copy-feedback-contract="angular-copy-success-duration-800"');
+    expect(html).toContain('data-collector-deploy-close-reset-contract="angular-close-clears-name"');
+    expect(html).toContain('data-collector-deploy-close-reset-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-deploy-close-pending-result-contract="angular-close-ignores-late-generate"');
+    expect(html).toContain('data-collector-deploy-close-pending-result-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-pagination-contract="angular-search-pagination"');
+    expect(html).toContain('data-collector-pagination-owner="hertzbeat-ui-pagination-bar"');
     expect(html).toContain('data-collector-admin-layout="full-width-admin-list"');
     expect(html).not.toContain('data-collector-summary-rail=');
     expect(html).not.toContain('当前采集集群');
@@ -124,11 +312,190 @@ describe('setting collector page', () => {
     expect(html).toContain('删除采集器');
     expect(html).toContain('搜索');
     expect(html).toContain('采集集群');
+    expect(html).toContain('no-action-message');
+    expect(html).toContain('no-action-error');
+    expect(html).toContain('no-action-meta');
+    expect(html).toContain('success');
+    expect(html).toContain('no-action-feedback-contract');
+    expect(html).toContain('no-delete-target');
+    expect(html).toContain('delete-idle');
+    expect(html).toContain('route-load-idle');
+    expect(html).toContain('deploy-name-pristine');
     expect(html).toContain('edge-a');
     expect(html).toContain('10.0.0.1');
+    expect(html).toContain('collector-pagination');
 
     await mockState.lastLoad?.();
 
     expect(apiMessageGet).toHaveBeenCalledWith('/collector?pageIndex=0&pageSize=8');
   });
+
+  it('keeps the collector shell mounted on list load failure like Angular console-only handling', async () => {
+    mockState.renderErrorMessage = 'backend refused collector load';
+    const { default: SettingCollectorPage } = await import('./page');
+    const html = renderToStaticMarkup(<SettingCollectorPage />);
+
+    expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-collector-manage-surface="otlp-cold-collector-console"');
+    expect(html).toContain('data-collector-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-collector-load-failure-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-collector-admin-layout="full-width-admin-list"');
+    expect(html).toContain('collector-pagination');
+    expect(html).not.toContain('backend refused collector load');
+  });
+
+  it('keeps collector management remounts on a short settled cache window while reloads invalidate it', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/collector/setting-collector-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_COLLECTOR_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['setting-collector', collectorListUrl, reloadVersion].join(':')");
+    expect(source).toContain('[collectorListUrl, reloadVersion]');
+    expect(source).toContain("const [query, setQuery] = useState<CollectorQueryState>({ pageIndex: 0, pageSize: 8, search: '' })");
+    expect(source).toContain('const [selectedCollectors, setSelectedCollectors] = useState<string[]>([])');
+    expect(source).toContain('const [actionMessage, setActionMessage] = useState<string | null>(null)');
+    expect(source).toContain('const [actionError, setActionError] = useState<string | null>(null)');
+    expect(source).toContain('const [actionMeta, setActionMeta] = useState<string | null>(null)');
+    expect(source).toContain("const [actionTone, setActionTone] = useState<'success' | 'warning' | 'critical'>('success')");
+    expect(source).toContain('const [actionFeedbackContract, setActionFeedbackContract] = useState<string | null>(null)');
+    expect(source).toContain('const [deleteTarget, setDeleteTarget] = useState<CollectorDeleteTarget | null>(null)');
+    expect(source).toContain('const [isDeletePending, setIsDeletePending] = useState(false)');
+    expect(source).toContain('const [isLoadPending, setIsLoadPending] = useState(false)');
+    expect(source).toContain('buildFallbackCollectorData');
+    expect(source).toContain('renderError={(message, retry) => renderCollectorSurface(buildFallbackCollectorData(), message, retry)}');
+    expect(source).toContain('loadError={loadError}');
+    expect(source).toContain('const [deployOpen, setDeployOpen] = useState(false)');
+    expect(source).toContain("const [deployHost, setDeployHost] = useState('127.0.0.1')");
+    expect(source).toContain('const [deployDockerShell, setDeployDockerShell] = useState');
+    expect(source).toContain('const [deployPackageShell, setDeployPackageShell] = useState');
+    expect(source).toContain('const [deployNameValidationVisible, setDeployNameValidationVisible] = useState(false)');
+    expect(source).toContain("setActionError(t('common.notify.no-select-delete'))");
+    expect(source).toContain("setActionError(t(action === 'online' ? 'collector.notify.no-select-online' : 'collector.notify.no-select-offline'))");
+    expect(source).toContain("setActionMessage(t(deleteTarget.action === 'online' || deleteTarget.action === 'offline' ? 'common.notify.operate-success' : 'common.notify.delete-success'))");
+    expect(source).toContain("setActionError(t(isOperateAction ? 'common.notify.operate-fail' : 'common.notify.delete-fail'))");
+    expect(source).toContain('setActionMeta(error instanceof Error ? error.message : null)');
+    expect(source).toContain('const remainingTotal = Math.max(0, data.list.totalElements - targetCollectors.length)');
+    expect(source).toContain('const lastPageIndex = Math.max(0, Math.ceil(remainingTotal / current.pageSize) - 1)');
+    expect(source).toContain('current.pageIndex > lastPageIndex ? { ...current, pageIndex: lastPageIndex } : current');
+    expect(source).toContain('setSelectedCollectors([]);');
+    expect(source).not.toContain('setSelectedCollectors(collectors => collectors.filter');
+    expect(source).toContain('deleteCollectors(apiMessageDelete, targetCollectors)');
+    expect(source).toContain('goOnlineCollectors(apiMessagePut, targetCollectors)');
+    expect(source).toContain('goOfflineCollectors(apiMessagePut, targetCollectors)');
+    expect(source).toContain('generateCollectorIdentity(apiMessagePost, collector)');
+    expect(source).toContain('buildCollectorDeployCommands(t, identity, managerHost)');
+    expect(source).toContain('COLLECTOR_DEPLOY_COPY_SUCCESS_DURATION_MS = 800');
+    expect(source).toContain('const deployCopyClearTimerRef = useRef');
+    expect(source).toContain('const deployGenerationRequestRef = useRef(0);');
+    expect(source).toContain('const clearDeployCopyTimer = useCallback');
+    expect(source).toContain('const closeDeployDialog = useCallback');
+    expect(source).toContain('deployGenerationRequestRef.current += 1;');
+    expect(source).toContain("setDeployOpen(false);");
+    expect(source).toContain("setDeployName('');");
+    expect(source).toContain('window.clearTimeout(deployCopyClearTimerRef.current)');
+    expect(source).toContain('deployCopyClearTimerRef.current = window.setTimeout');
+    expect(source).toContain('COLLECTOR_DEPLOY_COPY_SUCCESS_DURATION_MS');
+    expect(source).toContain('setIsDeployPending(true);');
+    expect(source).toContain('setIsDeployPending(false);');
+    expect(source).toContain("setDeployError(error instanceof Error ? error.message : t('common.notify.apply-fail'))");
+    expect(source).toContain('setIsLoadPending(true);');
+    expect(source).toContain('setIsLoadPending(false);');
+    expect(source).toContain('return await loadCollectorData(apiMessageGet, query);');
+    expect(source).toContain("setDeployError(error instanceof Error ? error.message : t('common.notify.apply-fail'))");
+    expect(source).toContain("setDeployCopyMessage(t('common.notify.copy-success'))");
+    expect(source).toContain('onRefresh={() => {');
+    expect(source).toContain('setReloadVersion(version => version + 1);');
+    expect(source).toContain('selectedCollectors={selectedCollectors}');
+    expect(source).toContain('actionMeta={actionMeta}');
+    expect(source).toContain('actionFeedbackContract={actionFeedbackContract}');
+    expect(source).toContain('deployOpen={deployOpen}');
+    expect(source).toContain('deployIdentity={deployIdentity}');
+    expect(source).toContain('deployDockerShell={deployDockerShell}');
+    expect(source).toContain('deployPackageShell={deployPackageShell}');
+    expect(source).toContain('deployNameValidationVisible={deployNameValidationVisible}');
+    expect(source).toContain('onSelectedCollectorsChange={setSelectedCollectors}');
+    expect(source).toContain('onPageIndexChange={pageIndex => {');
+    expect(source).toContain('setQuery(current => ({ ...current, pageIndex }))');
+    expect(source).toContain('onPageSizeChange={pageSize => {');
+    expect(source).toContain('setQuery(current => ({ ...current, pageIndex: 0, pageSize }))');
+    expect(source).toContain('setQuery(current => ({ ...current, pageIndex: 0, search }))');
+    expect(source).toContain('onSearchClear={() => {');
+    expect(source).toContain("setSearch('');");
+    expect(source).toContain("setQuery(current => ({ ...current, pageIndex: 0, search: '' }))");
+    expect(source).toContain('onDeployGenerate={() => {');
+    expect(source).toContain('if (isDeployPending) return;');
+    expect(source).toContain('const collector = deployName.trim();');
+    expect(source).toContain('if (deployName.length === 0) {');
+    expect(source).toContain('const deployGenerationRequest = deployGenerationRequestRef.current + 1;');
+    expect(source).toContain('deployGenerationRequestRef.current = deployGenerationRequest;');
+    expect(source).toContain('if (deployGenerationRequestRef.current !== deployGenerationRequest) return;');
+    expect(source).toContain('setDeployNameValidationVisible(true);');
+    expect(source).toContain('setDeployNameValidationVisible(false);');
+    expect(source).toContain('onDeployCopy={value => {');
+    expect(source).toContain("onGoOnline={() => openBatchOperateTarget('online')}");
+    expect(source).toContain("onGoOffline={() => openBatchOperateTarget('offline')}");
+    expect(source).toContain('deleteTarget={deleteTarget}');
+    expect(source).toContain('isDeletePending={isDeletePending}');
+    expect(source).toContain('isLoadPending={isLoadPending}');
+    expect(source).toContain('cacheKey={collectorManageCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_COLLECTOR_SETTLED_CACHE_TTL_MS}');
+  });
+
+  it('ignores late collector deploy identity results after the dialog is closed', async () => {
+    let resolveGenerate: ((value: { identity: string; host: string }) => void) | undefined;
+    generateCollectorIdentity.mockImplementationOnce(() => new Promise(resolve => {
+      resolveGenerate = resolve as (value: { identity: string; host: string }) => void;
+    }));
+    const { default: SettingCollectorPage } = await import('./page');
+    interactionContainer = document.createElement('div');
+    document.body.appendChild(interactionContainer);
+    interactionRoot = createRoot(interactionContainer);
+
+    await act(async () => {
+      interactionRoot?.render(<SettingCollectorPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDeploy();
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDeployNameChange(' edge-b ');
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDeployGenerate();
+      await Promise.resolve();
+    });
+
+    expect(generateCollectorIdentity).toHaveBeenCalledWith(apiMessagePost, 'edge-b');
+    expect(mockState.lastSurfaceProps?.isDeployPending).toBe(true);
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDeployClose();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.deployOpen).toBe(false);
+    expect(mockState.lastSurfaceProps?.deployName).toBe('');
+    expect(mockState.lastSurfaceProps?.deployIdentity).toBe('');
+    expect(mockState.lastSurfaceProps?.isDeployPending).toBe(false);
+
+    await act(async () => {
+      resolveGenerate?.({ identity: 'late-identity', host: '10.0.0.20' });
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.deployOpen).toBe(false);
+    expect(mockState.lastSurfaceProps?.deployName).toBe('');
+    expect(mockState.lastSurfaceProps?.deployIdentity).toBe('');
+    expect(mockState.lastSurfaceProps?.deployDockerShell).toBe('');
+    expect(mockState.lastSurfaceProps?.deployPackageShell).toBe('');
+    expect(mockState.lastSurfaceProps?.isDeployPending).toBe(false);
+    expect(buildCollectorDeployCommands).not.toHaveBeenCalled();
+  });
 });
diff --git a/web-next/app/setting/collector/page.tsx b/web-next/app/setting/collector/page.tsx
index f2f93c3abe..fa779d1efd 100644
--- a/web-next/app/setting/collector/page.tsx
+++ b/web-next/app/setting/collector/page.tsx
@@ -1,43 +1,7 @@
-'use client';
+import React from 'react';
 
-import { useCallback, useState } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { CollectorManageSurface } from '@/components/pages/collector-manage-surface';
-import { apiMessageGet } from '@/lib/api-client';
-import { formatTime } from '@/lib/format';
-import { loadCollectorData } from '@/lib/collector-manage/controller';
-import type { CollectorQueryState } from '@/lib/collector-manage/query-state';
+import SettingCollectorPage from './setting-collector-page';
 
-export default function SettingCollectorPage() {
-  const { t } = useI18n();
-  const [search, setSearch] = useState('');
-  const [query, setQuery] = useState<CollectorQueryState>({ search: '' });
-
-  const load = useCallback(async () => {
-    return loadCollectorData(apiMessageGet, query);
-  }, [query]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('menu.advanced.collector')}>
-      {data => (
-        <CollectorManageSurface
-          t={t}
-          data={data}
-          search={search}
-          formatTime={formatTime}
-          onSearchChange={setSearch}
-          onSearch={() => setQuery({ search })}
-          onRefresh={() => setQuery(current => ({ ...current }))}
-          onDeploy={() => {}}
-          onGoOnline={() => {}}
-          onGoOffline={() => {}}
-          onDelete={() => {}}
-          onRowGoOnline={() => {}}
-          onRowGoOffline={() => {}}
-          onRowDelete={() => {}}
-        />
-      )}
-    </ClientWorkbench>
-  );
+export default function SettingCollectorRoutePage() {
+  return <SettingCollectorPage />;
 }
diff --git a/web-next/app/setting/collector/setting-collector-page.tsx b/web-next/app/setting/collector/setting-collector-page.tsx
new file mode 100644
index 0000000000..14dd340616
--- /dev/null
+++ b/web-next/app/setting/collector/setting-collector-page.tsx
@@ -0,0 +1,340 @@
+'use client';
+
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { CollectorManageSurface, type CollectorDeleteTarget } from '@/components/pages/collector-manage-surface';
+import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
+import { formatTime } from '@/lib/format';
+import {
+  buildCollectorDeployCommands,
+  deleteCollectors,
+  generateCollectorIdentity,
+  goOfflineCollectors,
+  goOnlineCollectors,
+  loadCollectorData
+} from '@/lib/collector-manage/controller';
+import { buildCollectorUrl, type CollectorQueryState } from '@/lib/collector-manage/query-state';
+
+const SETTING_COLLECTOR_SETTLED_CACHE_TTL_MS = 10_000;
+const COLLECTOR_DEPLOY_COPY_SUCCESS_DURATION_MS = 800;
+type CollectorManagePageData = Awaited<ReturnType<typeof loadCollectorData>>;
+
+export default function SettingCollectorPage() {
+  const { t } = useI18n();
+  const [search, setSearch] = useState('');
+  const [query, setQuery] = useState<CollectorQueryState>({ pageIndex: 0, pageSize: 8, search: '' });
+  const [reloadVersion, setReloadVersion] = useState(0);
+  const [selectedCollectors, setSelectedCollectors] = useState<string[]>([]);
+  const [actionMessage, setActionMessage] = useState<string | null>(null);
+  const [actionError, setActionError] = useState<string | null>(null);
+  const [actionMeta, setActionMeta] = useState<string | null>(null);
+  const [actionTone, setActionTone] = useState<'success' | 'warning' | 'critical'>('success');
+  const [actionFeedbackContract, setActionFeedbackContract] = useState<string | null>(null);
+  const [deleteTarget, setDeleteTarget] = useState<CollectorDeleteTarget | null>(null);
+  const [isDeletePending, setIsDeletePending] = useState(false);
+  const [isLoadPending, setIsLoadPending] = useState(false);
+  const [deployOpen, setDeployOpen] = useState(false);
+  const [deployName, setDeployName] = useState('');
+  const [deployIdentity, setDeployIdentity] = useState('');
+  const [deployHost, setDeployHost] = useState('127.0.0.1');
+  const [deployDockerShell, setDeployDockerShell] = useState('');
+  const [deployPackageShell, setDeployPackageShell] = useState('');
+  const [deployError, setDeployError] = useState<string | null>(null);
+  const [deployCopyMessage, setDeployCopyMessage] = useState<string | null>(null);
+  const [isDeployPending, setIsDeployPending] = useState(false);
+  const [deployNameValidationVisible, setDeployNameValidationVisible] = useState(false);
+  const deployCopyClearTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
+  const deployGenerationRequestRef = useRef(0);
+  const collectorListUrl = useMemo(() => buildCollectorUrl(query), [query]);
+  const collectorManageCacheKey = useMemo(
+    () => ['setting-collector', collectorListUrl, reloadVersion].join(':'),
+    [collectorListUrl, reloadVersion]
+  );
+
+  const load = useCallback(async () => {
+    setIsLoadPending(true);
+    try {
+      return await loadCollectorData(apiMessageGet, query);
+    } finally {
+      setIsLoadPending(false);
+    }
+  }, [query]);
+
+  const clearActionFeedback = useCallback(() => {
+    setActionMessage(null);
+    setActionError(null);
+    setActionMeta(null);
+    setActionTone('success');
+    setActionFeedbackContract(null);
+  }, []);
+
+  const buildFallbackCollectorData = useCallback((): CollectorManagePageData => ({
+    list: {
+      content: [],
+      totalElements: 0,
+      pageIndex: query.pageIndex ?? 0,
+      pageSize: query.pageSize ?? 8
+    }
+  }), [query.pageIndex, query.pageSize]);
+
+  const clearDeployCopyTimer = useCallback(() => {
+    if (!deployCopyClearTimerRef.current) return;
+    window.clearTimeout(deployCopyClearTimerRef.current);
+    deployCopyClearTimerRef.current = null;
+  }, []);
+
+  useEffect(() => clearDeployCopyTimer, [clearDeployCopyTimer]);
+
+  const openBatchOperateTarget = useCallback((action: 'online' | 'offline') => {
+    clearActionFeedback();
+    if (selectedCollectors.length === 0) {
+      setActionTone('warning');
+      setActionFeedbackContract(action === 'online' ? 'angular-no-select-online' : 'angular-no-select-offline');
+      setActionError(t(action === 'online' ? 'collector.notify.no-select-online' : 'collector.notify.no-select-offline'));
+      return;
+    }
+    setDeleteTarget({
+      action,
+      collectors: selectedCollectors,
+      label: t('setting.collector.selected-count', { count: selectedCollectors.length }),
+      mode: 'batch'
+    });
+  }, [clearActionFeedback, selectedCollectors, t]);
+
+  const closeDeployDialog = useCallback(() => {
+    clearDeployCopyTimer();
+    deployGenerationRequestRef.current += 1;
+    setDeployOpen(false);
+    setDeployName('');
+    setDeployIdentity('');
+    setDeployHost('127.0.0.1');
+    setDeployDockerShell('');
+    setDeployPackageShell('');
+    setDeployError(null);
+    setDeployCopyMessage(null);
+    setIsDeployPending(false);
+    setDeployNameValidationVisible(false);
+  }, [clearDeployCopyTimer]);
+
+  function renderCollectorSurface(data: CollectorManagePageData, loadError: string | null = null, retryLoad?: () => void) {
+    return (
+      <CollectorManageSurface
+          t={t}
+          data={data}
+          search={search}
+          selectedCollectors={selectedCollectors}
+          actionMessage={actionMessage}
+          actionError={actionError}
+          actionMeta={actionMeta}
+          actionTone={actionTone}
+          actionFeedbackContract={actionFeedbackContract}
+          loadError={loadError}
+          deleteTarget={deleteTarget}
+          isDeletePending={isDeletePending}
+          isLoadPending={isLoadPending}
+          deployOpen={deployOpen}
+          deployName={deployName}
+          deployIdentity={deployIdentity}
+          deployHost={deployHost}
+          deployDockerShell={deployDockerShell}
+          deployPackageShell={deployPackageShell}
+          deployError={deployError}
+          deployCopyMessage={deployCopyMessage}
+          isDeployPending={isDeployPending}
+          deployNameValidationVisible={deployNameValidationVisible}
+          formatTime={formatTime}
+          onSearchChange={setSearch}
+          onSearch={() => {
+            clearActionFeedback();
+            setSelectedCollectors([]);
+            setQuery(current => ({ ...current, pageIndex: 0, search }));
+          }}
+          onSearchClear={() => {
+            clearActionFeedback();
+            setSelectedCollectors([]);
+            setSearch('');
+            setQuery(current => ({ ...current, pageIndex: 0, search: '' }));
+          }}
+          onRefresh={() => {
+            clearActionFeedback();
+            setSelectedCollectors([]);
+            if (retryLoad) {
+              retryLoad();
+              return;
+            }
+            setReloadVersion(version => version + 1);
+          }}
+          onDeploy={() => {
+            clearDeployCopyTimer();
+            clearActionFeedback();
+            setDeployOpen(true);
+            setDeployIdentity('');
+            setDeployError(null);
+            setDeployCopyMessage(null);
+            setDeployNameValidationVisible(false);
+          }}
+          onGoOnline={() => openBatchOperateTarget('online')}
+          onGoOffline={() => openBatchOperateTarget('offline')}
+          onDelete={() => {
+            clearActionFeedback();
+            if (selectedCollectors.length === 0) {
+              setActionTone('warning');
+              setActionFeedbackContract('angular-no-select-warning');
+              setActionError(t('common.notify.no-select-delete'));
+              return;
+            }
+            setDeleteTarget({
+              action: 'delete',
+              collectors: selectedCollectors,
+              label: t('setting.collector.selected-count', { count: selectedCollectors.length }),
+              mode: 'batch'
+            });
+          }}
+          onSelectedCollectorsChange={setSelectedCollectors}
+          onPageIndexChange={pageIndex => {
+            clearActionFeedback();
+            setSelectedCollectors([]);
+            setQuery(current => ({ ...current, pageIndex }));
+          }}
+          onPageSizeChange={pageSize => {
+            clearActionFeedback();
+            setSelectedCollectors([]);
+            setQuery(current => ({ ...current, pageIndex: 0, pageSize }));
+          }}
+          onDeleteCancel={() => {
+            if (!isDeletePending) setDeleteTarget(null);
+          }}
+          onDeleteConfirm={() => {
+            if (!deleteTarget) return;
+            clearActionFeedback();
+            setIsDeletePending(true);
+            const targetCollectors = deleteTarget.collectors;
+            const mutation =
+              deleteTarget.action === 'online'
+                ? goOnlineCollectors(apiMessagePut, targetCollectors)
+                : deleteTarget.action === 'offline'
+                  ? goOfflineCollectors(apiMessagePut, targetCollectors)
+                  : deleteCollectors(apiMessageDelete, targetCollectors);
+            void mutation
+              .then(() => {
+                setActionTone('success');
+                setActionFeedbackContract(deleteTarget.action === 'online' || deleteTarget.action === 'offline' ? 'angular-operate-notify' : 'angular-delete-notify');
+                setActionMessage(t(deleteTarget.action === 'online' || deleteTarget.action === 'offline' ? 'common.notify.operate-success' : 'common.notify.delete-success'));
+                setSelectedCollectors([]);
+                if (deleteTarget.action !== 'online' && deleteTarget.action !== 'offline') {
+                  setQuery(current => {
+                    const remainingTotal = Math.max(0, data.list.totalElements - targetCollectors.length);
+                    const lastPageIndex = Math.max(0, Math.ceil(remainingTotal / current.pageSize) - 1);
+                    return current.pageIndex > lastPageIndex ? { ...current, pageIndex: lastPageIndex } : current;
+                  });
+                }
+                setDeleteTarget(null);
+                setReloadVersion(version => version + 1);
+              })
+              .catch(error => {
+                const isOperateAction = deleteTarget.action === 'online' || deleteTarget.action === 'offline';
+                setActionTone('critical');
+                setActionFeedbackContract(isOperateAction ? 'angular-operate-fail' : 'angular-delete-fail');
+                setActionError(t(isOperateAction ? 'common.notify.operate-fail' : 'common.notify.delete-fail'));
+                setActionMeta(error instanceof Error ? error.message : null);
+              })
+              .finally(() => {
+                setIsDeletePending(false);
+              });
+          }}
+          onDeployNameChange={value => {
+            clearDeployCopyTimer();
+            setDeployName(value);
+            setDeployError(null);
+            setDeployCopyMessage(null);
+            setDeployNameValidationVisible(false);
+          }}
+          onDeployGenerate={() => {
+            const collector = deployName.trim();
+            if (isDeployPending) return;
+            if (deployName.length === 0) {
+              setDeployNameValidationVisible(true);
+              return;
+            }
+            setDeployError(null);
+            clearDeployCopyTimer();
+            setDeployCopyMessage(null);
+            setDeployNameValidationVisible(false);
+            setIsDeployPending(true);
+            const deployGenerationRequest = deployGenerationRequestRef.current + 1;
+            deployGenerationRequestRef.current = deployGenerationRequest;
+            void generateCollectorIdentity(apiMessagePost, collector)
+              .then(result => {
+                if (deployGenerationRequestRef.current !== deployGenerationRequest) return;
+                const identity = result.identity ?? '';
+                const managerHost = result.host ?? '127.0.0.1';
+                const commands = buildCollectorDeployCommands(t, identity, managerHost);
+                setDeployName(collector);
+                setDeployIdentity(identity);
+                setDeployHost(managerHost);
+                setDeployDockerShell(commands.dockerShell);
+                setDeployPackageShell(commands.packageShell);
+              })
+              .catch(error => {
+                if (deployGenerationRequestRef.current !== deployGenerationRequest) return;
+                setDeployError(error instanceof Error ? error.message : t('common.notify.apply-fail'));
+              })
+              .finally(() => {
+                if (deployGenerationRequestRef.current !== deployGenerationRequest) return;
+                setIsDeployPending(false);
+              });
+          }}
+          onDeployClose={closeDeployDialog}
+          onDeployCopy={value => {
+            clearDeployCopyTimer();
+            setDeployCopyMessage(t('common.notify.copy-success'));
+            deployCopyClearTimerRef.current = window.setTimeout(() => {
+              setDeployCopyMessage(null);
+              deployCopyClearTimerRef.current = null;
+            }, COLLECTOR_DEPLOY_COPY_SUCCESS_DURATION_MS);
+            void navigator.clipboard?.writeText(value).catch(() => undefined);
+          }}
+          onRowGoOnline={collector => {
+            clearActionFeedback();
+            setDeleteTarget({
+              action: 'online',
+              collectors: [collector],
+              label: collector,
+              mode: 'single'
+            });
+          }}
+          onRowGoOffline={collector => {
+            clearActionFeedback();
+            setDeleteTarget({
+              action: 'offline',
+              collectors: [collector],
+              label: collector,
+              mode: 'single'
+            });
+          }}
+          onRowDelete={collector => {
+            clearActionFeedback();
+            setDeleteTarget({
+              action: 'delete',
+              collectors: [collector],
+              label: collector,
+              mode: 'single'
+            });
+          }}
+        />
+    );
+  }
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.collector.loading')}
+      cacheKey={collectorManageCacheKey}
+      cacheSettledTtlMs={SETTING_COLLECTOR_SETTLED_CACHE_TTL_MS}
+      renderError={(message, retry) => renderCollectorSurface(buildFallbackCollectorData(), message, retry)}
+    >
+      {data => renderCollectorSurface(data)}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/setting/define/page.test.tsx b/web-next/app/setting/define/page.test.tsx
index 5d74653518..357cf19468 100644
--- a/web-next/app/setting/define/page.test.tsx
+++ b/web-next/app/setting/define/page.test.tsx
@@ -1,52 +1,64 @@
+// @vitest-environment jsdom
+
 import React from 'react';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { act } from 'react';
+import { createRoot } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 (globalThis as { React?: typeof React }).React = React;
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
+  lastSurfaceProps: null as null | Record<string, any>,
+  renderErrorMessage: null as string | null,
   renderData: {
-    list: {
-      content: [
-        {
-          id: 1,
-          name: 'cpu-alert',
-          type: 'realtime_metric',
-          datasource: 'http',
-          expr: 'up == 0',
-          enable: true,
-          period: 60
-        }
-      ],
-      totalElements: 1,
-      pageIndex: 0,
-      pageSize: 8
-    },
-    datasourceStatus: {
-      code: 0,
-      data: {
-        http: 'ready'
+    menuGroups: [
+      {
+        key: 'database',
+        label: 'DATABASE',
+        items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
       }
-    }
+    ],
+    appLabels: { mysql: 'MySQL' },
+    selectedApp: 'mysql',
+    yaml: 'app: mysql\ncategory: database',
+    originalYaml: 'app: mysql\ncategory: database'
+  },
+  defaultRenderData: {
+    menuGroups: [
+      {
+        key: 'database',
+        label: 'DATABASE',
+        items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+      }
+    ],
+    appLabels: { mysql: 'MySQL' },
+    selectedApp: 'mysql',
+    yaml: 'app: mysql\ncategory: database',
+    originalYaml: 'app: mysql\ncategory: database'
   }
 }));
 
-const apiGet = vi.hoisted(() => vi.fn());
 const apiMessageGet = vi.hoisted(() => vi.fn());
 const apiMessagePost = vi.hoisted(() => vi.fn());
 const apiMessagePut = vi.hoisted(() => vi.fn());
+const apiMessageDelete = vi.hoisted(() => vi.fn());
 const loadDefineCenterData = vi.hoisted(() => vi.fn());
+const saveTemplateDefine = vi.hoisted(() => vi.fn());
+const deleteTemplateDefine = vi.hoisted(() => vi.fn());
+const updateTemplateVisibility = vi.hoisted(() => vi.fn());
+const reloadTemplateDefinitionStartupContext = vi.hoisted(() => vi.fn());
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
+    locale: 'zh-CN',
     t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'setting.define.loading': '加载定义中心',
-        'menu.advanced.define': '定义'
-      }
+      locale: 'zh-CN'
     })
   })
 }));
@@ -54,80 +66,848 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    renderError
   }: {
     children: (data: unknown) => React.ReactNode;
     load: () => Promise<unknown>;
+    renderError?: (message: string, retry: () => void) => React.ReactNode;
   }) => {
     mockState.lastLoad = load;
+    if (mockState.renderErrorMessage && renderError) {
+      return <div data-client-workbench="true">{renderError(mockState.renderErrorMessage, vi.fn())}</div>;
+    }
     return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
   }
 }));
 
 vi.mock('@/components/pages/setting-define-surface', () => ({
-  SettingDefineSurface: ({ selectedDefine, yamlLabel, darkMode, isEditing }: any) => (
-    <div data-setting-define-surface="otlp-cold-define-console">
-      <span>定义</span>
-      <span>{selectedDefine?.name}</span>
-      <span>{yamlLabel}</span>
-      <span>{String(darkMode)}</span>
-      <span>{String(isEditing)}</span>
-    </div>
-  )
+  SettingDefineSurface: (props: any) => {
+    mockState.lastSurfaceProps = props;
+    const {
+      selectedApp,
+      yamlLabel,
+      editorValue,
+      originalYaml,
+      darkMode,
+      isEditing,
+      menuLoading,
+      editorLoading,
+      loadError,
+      message,
+      messageMeta,
+      messageContract,
+      onNew,
+      onSave,
+      onDelete,
+      onToggleTemplateVisibility
+    } = props;
+    return (
+      <div
+        data-setting-define-surface="otlp-cold-define-console"
+        data-setting-define-menu-filtered-contract="angular-hide-prometheus-system"
+        data-setting-define-menu-filtered-owner="setting-define-controller"
+        data-setting-define-menu-loading-contract="angular-monitor-select-list-loading"
+        data-setting-define-menu-loading-owner="hertzbeat-ui-template-picker"
+        data-setting-define-menu-loading={menuLoading ? 'true' : 'false'}
+        data-setting-define-load-failure-contract="angular-console-only-shell"
+        data-setting-define-load-failure-owner="setting-define-controller"
+        data-setting-define-load-failure={loadError ? 'angular-console-only-shell' : 'none'}
+        data-setting-define-theme-switch-contract="angular-nz-switch-code-editor-theme"
+        data-setting-define-theme-switch-owner="hertzbeat-ui-switch"
+        data-setting-define-theme-switch-state={darkMode ? 'vs-dark' : 'vs'}
+        data-setting-define-save-reload-contract="angular-load-app-define-content-after-save"
+        data-setting-define-save-reload-owner="setting-define-controller"
+        data-setting-define-save-reload-scope={selectedApp ? 'existing-template' : 'new-template-draft'}
+        data-setting-define-startup-reload-contract="angular-startup-load-after-success"
+        data-setting-define-startup-reload-owner="startup-service"
+        data-setting-define-startup-reload-scope="save-delete-visibility-success"
+        data-setting-define-startup-reload-failure-contract="angular-fire-and-forget"
+        data-setting-define-startup-reload-failure-owner="startup-service"
+        data-setting-define-editor-option-contract="angular-yaml-vs-folding-automatic-layout"
+        data-setting-define-editor-option-owner="cold-code-editor"
+        data-setting-define-editor-loading-contract="angular-nz-code-editor-loading"
+        data-setting-define-editor-loading-owner="cold-code-editor"
+        data-setting-define-editor-loading={editorLoading ? 'true' : 'false'}
+        data-setting-define-editor-loading-save-contract="angular-save-hidden-while-editor-loading"
+        data-setting-define-new-draft-contract="angular-locale-comment-five-newlines"
+        data-setting-define-new-draft-owner="setting-define-controller"
+        data-setting-define-new-draft-state={selectedApp ? 'existing-template' : 'new-template'}
+        data-setting-define-new-action-contract="angular-current-app-reset-url-retained"
+        data-setting-define-new-action-owner="setting-define-controller"
+        data-setting-define-new-action-state={selectedApp ? 'available' : 'hidden'}
+        data-setting-define-menu-select-query-contract="angular-replace-with-app-only"
+        data-setting-define-menu-select-query-owner="setting-define-page-router"
+      >
+        <span>定义</span>
+        <span>{selectedApp}</span>
+        <span>{yamlLabel}</span>
+        <span>{editorValue}</span>
+        <span data-setting-define-original-yaml="true">{originalYaml}</span>
+        <span>{String(darkMode)}</span>
+        <span>{String(isEditing)}</span>
+        <span data-setting-define-load-error={loadError ? 'present' : 'none'} />
+        <span data-setting-define-test-save-pending="true">{String(props.savePending)}</span>
+        <span
+          data-setting-define-test-message={message || ''}
+          data-setting-define-test-message-meta={messageMeta || ''}
+          data-setting-define-test-message-contract={messageContract || ''}
+        />
+        <button data-setting-define-test-new="true" onClick={onNew}>new</button>
+        <button data-setting-define-test-save="true" onClick={onSave}>save</button>
+        <button data-setting-define-test-delete="true" onClick={onDelete}>delete</button>
+        <button data-setting-define-test-visibility="true" onClick={() => onToggleTemplateVisibility('mysql', true)}>visibility</button>
+      </div>
+    );
+  }
 }));
 
 vi.mock('@/lib/api-client', () => ({
-  apiGet,
   apiMessageGet,
   apiMessagePost,
-  apiMessagePut
-}));
-
-vi.mock('@/lib/format', () => ({
-  formatTime: () => '2026-04-10 18:00:00'
+  apiMessagePut,
+  apiMessageDelete
 }));
 
 vi.mock('@/lib/setting-define/controller', () => ({
   loadDefineCenterData,
-  buildPreviewUrl: () => '/alert/define/preview/http?type=realtime_metric&expr=up+%3D%3D+0',
-  buildSkeletonDefine: () => ({
-    id: 0,
-    name: 'next-migrated-define',
-    datasource: 'promql',
-    expr: 'up == 0'
-  }),
-  createSkeletonDefine: vi.fn(),
-  saveDefine: vi.fn()
+  buildNewTemplateYaml: () => 'app: custom',
+  buildNewTemplateDraft: () => ({ yaml: 'app: custom\n\n\n\n\n', originalYaml: 'app: custom' }),
+  saveTemplateDefine,
+  deleteTemplateDefine,
+  updateTemplateVisibility,
+  reloadTemplateDefinitionStartupContext
 }));
 
 describe('setting define page', () => {
   beforeEach(() => {
+    window.history.pushState({}, '', '/setting/define');
+    window.localStorage.clear();
+    document.body.removeAttribute('data-theme');
+    document.documentElement.removeAttribute('data-theme');
     mockState.lastLoad = null;
-    apiGet.mockReset();
+    mockState.lastSurfaceProps = null;
+    mockState.renderErrorMessage = null;
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database',
+      originalYaml: 'app: mysql\ncategory: database'
+    };
     apiMessageGet.mockReset();
     apiMessagePost.mockReset();
     apiMessagePut.mockReset();
-    loadDefineCenterData.mockReset().mockImplementation(async (apiGetFn, search) => {
-      await apiGetFn(`/alert/defines?pageIndex=0&pageSize=8&sort=id&order=desc${search ? `&search=${search}` : ''}`);
-      await apiGetFn('/alert/define/datasource/status');
-      return mockState.renderData;
-    });
+    apiMessageDelete.mockReset();
+    loadDefineCenterData.mockReset().mockResolvedValue(mockState.renderData);
+    saveTemplateDefine.mockReset().mockResolvedValue(undefined);
+    deleteTemplateDefine.mockReset().mockResolvedValue(undefined);
+    updateTemplateVisibility.mockReset().mockResolvedValue(undefined);
+    reloadTemplateDefinitionStartupContext.mockReset().mockResolvedValue(undefined);
   });
 
-  it('renders the shared define surface and keeps the route focused on load composition', async () => {
+  it('renders the monitor-template YML surface and loads through old app define APIs', async () => {
     const { default: SettingDefinePage } = await import('./page');
     const html = renderToStaticMarkup(<SettingDefinePage />);
 
     expect(html).toContain('data-client-workbench="true"');
     expect(html).toContain('data-setting-define-surface="otlp-cold-define-console"');
-    expect(html).toContain('定义');
-    expect(html).toContain('cpu-alert');
-    expect(html).toContain('http.yml');
-    expect(html).toContain('false');
+    expect(html).toContain('data-setting-define-menu-filtered-contract="angular-hide-prometheus-system"');
+    expect(html).toContain('data-setting-define-menu-filtered-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-menu-loading-contract="angular-monitor-select-list-loading"');
+    expect(html).toContain('data-setting-define-menu-loading-owner="hertzbeat-ui-template-picker"');
+    expect(html).toContain('data-setting-define-menu-loading="false"');
+    expect(html).toContain('data-setting-define-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-setting-define-load-failure-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-load-failure="none"');
+    expect(html).toContain('data-setting-define-theme-switch-contract="angular-nz-switch-code-editor-theme"');
+    expect(html).toContain('data-setting-define-theme-switch-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-setting-define-theme-switch-state="vs-dark"');
+    expect(html).toContain('data-setting-define-save-reload-contract="angular-load-app-define-content-after-save"');
+    expect(html).toContain('data-setting-define-save-reload-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-save-reload-scope="new-template-draft"');
+    expect(html).toContain('data-setting-define-startup-reload-contract="angular-startup-load-after-success"');
+    expect(html).toContain('data-setting-define-startup-reload-owner="startup-service"');
+    expect(html).toContain('data-setting-define-startup-reload-scope="save-delete-visibility-success"');
+    expect(html).toContain('data-setting-define-startup-reload-failure-contract="angular-fire-and-forget"');
+    expect(html).toContain('data-setting-define-startup-reload-failure-owner="startup-service"');
+    expect(html).toContain('data-setting-define-editor-option-contract="angular-yaml-vs-folding-automatic-layout"');
+    expect(html).toContain('data-setting-define-editor-option-owner="cold-code-editor"');
+    expect(html).toContain('data-setting-define-editor-loading-contract="angular-nz-code-editor-loading"');
+    expect(html).toContain('data-setting-define-editor-loading-owner="cold-code-editor"');
+    expect(html).toContain('data-setting-define-editor-loading="false"');
+    expect(html).toContain('data-setting-define-editor-loading-save-contract="angular-save-hidden-while-editor-loading"');
+    expect(html).toContain('data-setting-define-new-draft-contract="angular-locale-comment-five-newlines"');
+    expect(html).toContain('data-setting-define-new-draft-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-new-draft-state="new-template"');
+    expect(html).toContain('data-setting-define-new-action-contract="angular-current-app-reset-url-retained"');
+    expect(html).toContain('data-setting-define-new-action-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-new-action-state="hidden"');
+    expect(html).toContain('data-setting-define-menu-select-query-contract="angular-replace-with-app-only"');
+    expect(html).toContain('data-setting-define-menu-select-query-owner="setting-define-page-router"');
+    expect(html).toContain('mysql');
+    expect(html).toContain('app-mysql.yml');
+    expect(html).toContain('app: mysql');
+    expect(html).toContain('data-setting-define-original-yaml="true"');
+    expect(html).toContain('true');
+
+    await mockState.lastLoad?.();
+
+    expect(loadDefineCenterData).toHaveBeenCalledWith(apiMessageGet, null, 'zh-CN');
+  });
+
+  it('keeps the Angular setting define shell visible when menu or YML loading fails', async () => {
+    mockState.renderErrorMessage = 'backend refused define load';
+    const { default: SettingDefinePage } = await import('./page');
+    const html = renderToStaticMarkup(<SettingDefinePage />);
+
+    expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-setting-define-surface="otlp-cold-define-console"');
+    expect(html).toContain('data-setting-define-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-setting-define-load-failure-owner="setting-define-controller"');
+    expect(html).toContain('data-setting-define-load-failure="angular-console-only-shell"');
+    expect(html).toContain('app-custom.yml');
+    expect(html).toContain('app: custom');
+    expect(html).not.toContain('backend refused define load');
+    expect(html).not.toContain('common.load-failed');
+    expect(html).not.toContain('data-observability-status');
+    expect(mockState.lastSurfaceProps?.data.menuGroups).toEqual([]);
+    expect(mockState.lastSurfaceProps?.loadError).toBe('backend refused define load');
+  });
+
+  it('initializes the YML editor dark toggle from the Angular theme service contract', async () => {
+    const { default: SettingDefinePage } = await import('./page');
+
+    window.localStorage.setItem('theme', 'light-ops');
+    renderToStaticMarkup(<SettingDefinePage />);
+    expect(mockState.lastSurfaceProps?.darkMode).toBe(false);
+
+    document.body.setAttribute('data-theme', 'dark-ops');
+    renderToStaticMarkup(<SettingDefinePage />);
+    expect(mockState.lastSurfaceProps?.darkMode).toBe(true);
+  });
+
+  it('keeps setting define remounts keyed by selected app and reload version', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/define/setting-define-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_DEFINE_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('loadDefineCenterData(apiMessageGet, selectedApp ?? null, locale)');
+    expect(source).toContain('buildNewTemplateDraft');
+    expect(source).toContain('buildFallbackDefineData');
+    expect(source).toContain('renderError={(message) => renderDefineSurface(buildFallbackDefineData(), message)}');
+    expect(source).toContain('loadError={loadError}');
+    expect(source).toContain('menuLoading={false}');
+    expect(source).toContain('editorLoading={false}');
+    expect(source).toContain('reloadTemplateDefinitionStartupContext');
+    expect(source).toContain("if (typeof window === 'undefined') return undefined;");
+    expect(source).toContain('useState<string | null | undefined>(() => readInitialSelectedApp())');
+    expect(source).toContain('writeSelectedAppToUrl(app)');
+    expect(source).toContain("const nextSearch = app ? `?app=${encodeURIComponent(app)}` : '';");
+    expect(source).toContain("window.history.pushState({}, '', `${url.pathname}${nextSearch}`);");
+    expect(source).not.toContain("url.searchParams.set('app', app)");
+    expect(source).toContain('setEditorValue(null)');
+    expect(source).toContain('setOriginalYaml(null)');
+    expect(source).toContain('resolveDefineWorkbenchTheme');
+    expect(source).toContain("window.localStorage.getItem('theme')");
+    expect(source).toContain("document.body?.getAttribute('data-theme')");
+    expect(source).toContain("document.documentElement?.getAttribute('data-theme')");
+    expect(source).toContain("const [darkMode, setDarkMode] = useState(() => readInitialDefineDarkMode());");
+    expect(source).toContain("['setting-define-yml', selectedApp || 'new', reloadVersion].join(':')");
+    expect(source).toContain('[selectedApp, reloadVersion]');
+    expect(source).toContain('[selectedApp, locale]');
+    expect(source).toContain('cacheKey={settingDefineCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_DEFINE_SETTLED_CACHE_TTL_MS}');
+    expect(source).toContain('const activeApp = selectedApp === undefined ? data.selectedApp : selectedApp;');
+    expect(source).not.toContain('const activeApp = selectedApp ?? data.selectedApp;');
+    expect(source).toContain('originalYaml={resolvedOriginalYaml}');
+    expect(source).not.toContain('const [darkMode, setDarkMode] = useState(false);');
+    expect(source).toContain('const [savePending, setSavePending] = useState(false);');
+    expect(source).toContain('const [messageMeta, setMessageMeta] = useState<string | null>(null);');
+    expect(source).toContain('const [messageContract, setMessageContract] = useState<string | null>(null);');
+    expect(source).toContain('function readMutationFailureDetail(error: unknown, fallback = \'\')');
+    expect(source).toContain('savePending={savePending}');
+    expect(source).toContain('messageMeta={messageMeta}');
+    expect(source).toContain('messageContract={messageContract}');
+    expect(source).toContain('setEditorValue(draft.yaml)');
+    expect(source).toContain('setOriginalYaml(draft.originalYaml)');
+    expect(source).not.toContain('writeSelectedAppToUrl(null);');
+    expect(source).toContain('currentYaml === currentOriginalYaml');
+    expect(source).toContain("currentYaml === ''");
+    expect(source).toContain("t('define.save-apply.no-code')");
+    expect(source).toContain("await saveTemplateDefine(apiMessagePost, apiMessagePut, currentYaml, !currentApp);");
+    expect(source).toContain("setMessage(t('common.notify.apply-success'));");
+    expect(source).toContain("setMessage(t('common.notify.apply-fail'));");
+    expect(source).toContain("setMessageMeta(readMutationFailureDetail(error, t('setting.define.save.failed')));");
+    expect(source).toContain("setMessageContract('angular-apply-fail-notification');");
+    expect(source).toContain("setMessage(t('common.notify.delete-fail'));");
+    expect(source).toContain('setMessageMeta(readMutationFailureDetail(error));');
+    expect(source).toContain("setMessageContract('angular-delete-fail-notification');");
+    expect(source).not.toContain('buildMutationFailureMessage(');
+    expect(source).toContain('setEditorValue(null);');
+    expect(source).toContain('setOriginalYaml(null);');
+    expect(source).toContain('refreshStartupContextAfterMutation();');
+    expect(source).toContain('void reloadTemplateDefinitionStartupContext(apiMessageGet, locale).catch(() => undefined);');
+    expect(source).not.toContain("setMessage(t('setting.define.save.success'))");
+  });
+
+  it('re-reads existing app YML from workbench data after a successful save like Angular loadAppDefineContent', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql');
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database\n# backend canonical',
+      originalYaml: 'app: mysql\ncategory: database\n# backend canonical'
+    };
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.editorValue).toBe('app: mysql\ncategory: database\n# backend canonical');
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onEditorValueChange('app: mysql\ncategory: database\n# local edit before save');
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.editorValue).toBe('app: mysql\ncategory: database\n# local edit before save');
+    expect(mockState.lastSurfaceProps?.isEditing).toBe(true);
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSave();
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(saveTemplateDefine).toHaveBeenCalledWith(
+      apiMessagePost,
+      apiMessagePut,
+      'app: mysql\ncategory: database\n# local edit before save',
+      false
+    );
+    expect(mockState.lastSurfaceProps?.editorValue).toBe('app: mysql\ncategory: database\n# backend canonical');
+    expect(mockState.lastSurfaceProps?.originalYaml).toBe('app: mysql\ncategory: database\n# backend canonical');
+    expect(mockState.lastSurfaceProps?.isEditing).toBe(false);
+    expect(reloadTemplateDefinitionStartupContext).toHaveBeenCalledWith(apiMessageGet, 'zh-CN');
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  });
+
+  it('refreshes startup config context after successful YML mutations', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql');
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database\n# changed',
+      originalYaml: 'app: mysql\ncategory: database'
+    };
+    const { default: SettingDefinePage } = await import('./page');
+    const html = renderToStaticMarkup(<SettingDefinePage />);
+    expect(html).toContain('data-setting-define-test-save="true"');
+
+    mockState.lastSurfaceProps?.onSave();
+    await Promise.resolve();
+    await Promise.resolve();
+
+    expect(saveTemplateDefine).toHaveBeenCalledWith(
+      apiMessagePost,
+      apiMessagePut,
+      'app: mysql\ncategory: database\n# changed',
+      false
+    );
+    expect(reloadTemplateDefinitionStartupContext).toHaveBeenCalledWith(apiMessageGet, 'zh-CN');
+
+    mockState.lastSurfaceProps?.onDelete();
+    await Promise.resolve();
+    await Promise.resolve();
+
+    expect(deleteTemplateDefine).toHaveBeenCalledWith(apiMessageDelete, 'mysql');
+    expect(reloadTemplateDefinitionStartupContext).toHaveBeenCalledTimes(2);
+
+    mockState.lastSurfaceProps?.onToggleTemplateVisibility('mysql', true);
+    await Promise.resolve();
+    await Promise.resolve();
+
+    expect(updateTemplateVisibility).toHaveBeenCalledWith(apiMessagePut, 'mysql', true);
+    expect(reloadTemplateDefinitionStartupContext).toHaveBeenCalledTimes(3);
+  });
+
+  it('keeps mutation success when the Angular startup refresh side effect fails', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql');
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database\n# changed',
+      originalYaml: 'app: mysql\ncategory: database'
+    };
+    reloadTemplateDefinitionStartupContext.mockRejectedValueOnce(new Error('startup refresh down'));
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSave();
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(saveTemplateDefine).toHaveBeenCalledWith(
+      apiMessagePost,
+      apiMessagePut,
+      'app: mysql\ncategory: database\n# changed',
+      false
+    );
+    expect(reloadTemplateDefinitionStartupContext).toHaveBeenCalledWith(apiMessageGet, 'zh-CN');
+    expect(mockState.lastSurfaceProps?.message).toContain('应用成功');
+    expect(mockState.lastSurfaceProps?.message).not.toContain('startup refresh down');
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  });
+
+  it('keeps the Angular URL app parameter when starting a new draft or after delete success', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql&returnTo=%2Fsettings');
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database',
+      originalYaml: 'app: mysql\ncategory: database'
+    };
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    expect(window.location.search).toContain('app=mysql');
+    expect(window.location.search).toContain('returnTo=%2Fsettings');
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onNew();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.selectedApp).toBeNull();
+    expect(mockState.lastSurfaceProps?.editorValue).toBe('app: custom\n\n\n\n\n');
+    expect(window.location.search).toContain('app=mysql');
+    expect(window.location.search).toContain('returnTo=%2Fsettings');
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+
+    window.history.pushState({}, '', '/setting/define?app=mysql&returnTo=%2Fsettings');
+    const deleteContainer = document.createElement('div');
+    document.body.appendChild(deleteContainer);
+    const deleteRoot = createRoot(deleteContainer);
+
+    await act(async () => {
+      deleteRoot.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDelete();
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(deleteTemplateDefine).toHaveBeenCalledWith(apiMessageDelete, 'mysql');
+    expect(mockState.lastSurfaceProps?.selectedApp).toBeNull();
+    expect(mockState.lastSurfaceProps?.editorValue).toBe('app: custom\n\n\n\n\n');
+    expect(mockState.lastSurfaceProps?.originalYaml).toBe('app: custom');
+    expect(mockState.lastSurfaceProps?.isEditing).toBe(false);
+    expect(window.location.search).toContain('app=mysql');
+    expect(window.location.search).toContain('returnTo=%2Fsettings');
+
+    await act(async () => {
+      deleteRoot.unmount();
+      await Promise.resolve();
+    });
+    deleteContainer.remove();
+  });
+
+  it('navigates selected menu templates through the Angular app query contract', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql&returnTo=%2Fsettings');
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.selectedApp).toBe('mysql');
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSelectApp('linux');
+      await Promise.resolve();
+    });
+
+    expect(window.location.search).toContain('app=linux');
+    expect(window.location.search).toBe('?app=linux');
+    expect(window.location.search).not.toContain('returnTo=%2Fsettings');
+    expect(mockState.lastSurfaceProps?.selectedApp).toBe('linux');
+    expect(mockState.lastSurfaceProps?.isEditing).toBe(false);
 
     await mockState.lastLoad?.();
+    expect(loadDefineCenterData).toHaveBeenLastCalledWith(apiMessageGet, 'linux', 'zh-CN');
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  });
+
+  it('keeps the surface save action busy while save/apply or visibility updates are pending', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql');
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database\n# changed',
+      originalYaml: 'app: mysql\ncategory: database'
+    };
+    let resolveSave: (() => void) | undefined;
+    saveTemplateDefine.mockImplementationOnce(() => new Promise<void>(resolve => {
+      resolveSave = resolve;
+    }));
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.savePending).toBe(false);
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSave();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.savePending).toBe(true);
+
+    await act(async () => {
+      resolveSave?.();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.savePending).toBe(false);
+
+    let resolveVisibility: (() => void) | undefined;
+    updateTemplateVisibility.mockImplementationOnce(() => new Promise<void>(resolve => {
+      resolveVisibility = resolve;
+    }));
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onToggleTemplateVisibility('mysql', true);
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.savePending).toBe(true);
+
+    await act(async () => {
+      resolveVisibility?.();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.savePending).toBe(false);
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  });
+
+  it('preserves the old Angular edit flag when saving a direct new-template YML draft', async () => {
+    const draftYaml = 'app: custom\ncategory: custom\n# still drafting';
+    const draftOriginalYaml = '# 请在此通过编写YML内容来定义新的监控类型';
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: null,
+      yaml: draftYaml,
+      originalYaml: draftOriginalYaml
+    };
+    window.history.pushState({}, '', '/setting/define');
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.selectedApp).toBeNull();
+    expect(mockState.lastSurfaceProps?.isEditing).toBe(false);
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSave();
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(saveTemplateDefine).toHaveBeenCalledWith(apiMessagePost, apiMessagePut, draftYaml, true);
+    expect(mockState.lastSurfaceProps?.selectedApp).toBeNull();
+    expect(mockState.lastSurfaceProps?.editorValue).toBe(draftYaml);
+    expect(mockState.lastSurfaceProps?.originalYaml).toBe(draftOriginalYaml);
+    expect(mockState.lastSurfaceProps?.isEditing).toBe(false);
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  });
+
+  it('keeps old Angular apply-fail title with backend detail when YML save fails', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql');
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database\n# invalid',
+      originalYaml: 'app: mysql\ncategory: database'
+    };
+    saveTemplateDefine.mockRejectedValueOnce(new Error('schema invalid'));
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSave();
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(saveTemplateDefine).toHaveBeenCalledWith(
+      apiMessagePost,
+      apiMessagePut,
+      'app: mysql\ncategory: database\n# invalid',
+      false
+    );
+    expect(mockState.lastSurfaceProps?.message).toContain('应用失败');
+    expect(mockState.lastSurfaceProps?.message).not.toContain('schema invalid');
+    expect(mockState.lastSurfaceProps?.messageMeta).toBe('schema invalid');
+    expect(mockState.lastSurfaceProps?.messageContract).toBe('angular-apply-fail-notification');
+    expect(reloadTemplateDefinitionStartupContext).not.toHaveBeenCalled();
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  });
+
+  it('keeps old Angular delete-fail title with backend detail when YML delete fails', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql');
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database',
+      originalYaml: 'app: mysql\ncategory: database'
+    };
+    deleteTemplateDefine.mockRejectedValueOnce(new Error('template is still in use'));
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDelete();
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(deleteTemplateDefine).toHaveBeenCalledWith(apiMessageDelete, 'mysql');
+    expect(mockState.lastSurfaceProps?.message).toContain('删除失败');
+    expect(mockState.lastSurfaceProps?.message).not.toContain('template is still in use');
+    expect(mockState.lastSurfaceProps?.messageMeta).toBe('template is still in use');
+    expect(mockState.lastSurfaceProps?.messageContract).toBe('angular-delete-fail-notification');
+    expect(mockState.lastSurfaceProps?.selectedApp).toBe('mysql');
+    expect(mockState.lastSurfaceProps?.editorValue).toBe('app: mysql\ncategory: database');
+    expect(mockState.lastSurfaceProps?.originalYaml).toBe('app: mysql\ncategory: database');
+    expect(reloadTemplateDefinitionStartupContext).not.toHaveBeenCalled();
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  });
+
+  it('keeps old Angular apply-fail title with backend detail when template visibility update fails', async () => {
+    window.history.pushState({}, '', '/setting/define?app=mysql');
+    mockState.renderData = {
+      menuGroups: [
+        {
+          key: 'database',
+          label: 'DATABASE',
+          items: [{ category: 'database', value: 'mysql', label: 'MySQL', hide: false }]
+        }
+      ],
+      appLabels: { mysql: 'MySQL' },
+      selectedApp: 'mysql',
+      yaml: 'app: mysql\ncategory: database',
+      originalYaml: 'app: mysql\ncategory: database'
+    };
+    updateTemplateVisibility.mockRejectedValueOnce(new Error('visibility locked by policy'));
+    const { default: SettingDefinePage } = await import('./page');
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingDefinePage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onToggleTemplateVisibility('mysql', true);
+      await Promise.resolve();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(updateTemplateVisibility).toHaveBeenCalledWith(apiMessagePut, 'mysql', true);
+    expect(mockState.lastSurfaceProps?.message).toContain('应用失败');
+    expect(mockState.lastSurfaceProps?.message).not.toContain('visibility locked by policy');
+    expect(mockState.lastSurfaceProps?.messageMeta).toBe('visibility locked by policy');
+    expect(mockState.lastSurfaceProps?.messageContract).toBe('angular-apply-fail-notification');
+    expect(mockState.lastSurfaceProps?.selectedApp).toBe('mysql');
+    expect(mockState.lastSurfaceProps?.editorValue).toBe('app: mysql\ncategory: database');
+    expect(reloadTemplateDefinitionStartupContext).not.toHaveBeenCalled();
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  });
+
+  it('does not depend on alert definition preview or datasource APIs', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/define/setting-define-page.tsx'), 'utf8');
 
-    expect(apiMessageGet).toHaveBeenNthCalledWith(1, '/alert/defines?pageIndex=0&pageSize=8&sort=id&order=desc');
-    expect(apiMessageGet).toHaveBeenNthCalledWith(2, '/alert/define/datasource/status');
+    expect(source).toContain('saveTemplateDefine');
+    expect(source).toContain('deleteTemplateDefine');
+    expect(source).toContain('updateTemplateVisibility');
+    expect(source).toContain('refreshStartupContextAfterMutation');
+    expect(source).toContain('.catch(() => undefined)');
+    expect(source).toContain('apiMessageDelete');
+    expect(source).not.toContain('/alert/define');
+    expect(source).not.toContain('buildPreviewUrl');
+    expect(source).not.toContain('buildSkeletonDefine');
   });
 });
diff --git a/web-next/app/setting/define/page.tsx b/web-next/app/setting/define/page.tsx
index eb4f0480b5..4a44cd227b 100644
--- a/web-next/app/setting/define/page.tsx
+++ b/web-next/app/setting/define/page.tsx
@@ -1,152 +1,7 @@
-'use client';
+import React from 'react';
 
-import { useCallback, useState } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { SettingDefineSurface } from '@/components/pages/setting-define-surface';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { apiGet, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
-import { formatTime } from '@/lib/format';
-import { buildPreviewUrl, buildSkeletonDefine, loadDefineCenterData, saveDefine } from '@/lib/setting-define/controller';
-import type { AlertDefine } from '@/lib/types';
+import SettingDefinePage from './setting-define-page';
 
-function buildDefineEditorValue(define: AlertDefine | null) {
-  const draft = define || buildSkeletonDefine();
-  return [
-    `name: ${draft.name || ''}`,
-    `type: ${draft.type || ''}`,
-    `datasource: ${draft.datasource || ''}`,
-    `enable: ${draft.enable === false ? 'false' : 'true'}`,
-    `period: ${draft.period || 0}`,
-    `times: ${draft.times || 0}`,
-    'expr: |',
-    `  ${(draft.expr || '').replace(/\n/g, '\n  ')}`
-  ].join('\n');
-}
-
-function buildCurrentYamlLabel(define: AlertDefine | null) {
-  if (!define) return null;
-  const stem = define.datasource || define.name || 'current';
-  return `${stem}.yml`;
-}
-
-function cloneDefine(define: AlertDefine) {
-  return {
-    ...define,
-    labels: define.labels ? { ...define.labels } : define.labels,
-    annotations: define.annotations ? { ...define.annotations } : define.annotations
-  };
-}
-
-export default function SettingDefinePage() {
-  const { t } = useI18n();
-  const [search, setSearch] = useState('');
-  const [query, setQuery] = useState('');
-  const [selected, setSelected] = useState<AlertDefine | null>(null);
-  const [preview, setPreview] = useState<unknown>(null);
-  const [message, setMessage] = useState<string | null>(null);
-  const [saving, setSaving] = useState(false);
-  const [editorValue, setEditorValue] = useState('');
-  const [darkMode, setDarkMode] = useState(false);
-  const [isEditing, setIsEditing] = useState(false);
-
-  const load = useCallback(async () => {
-    return loadDefineCenterData(apiMessageGet, query);
-  }, [query]);
-
-  async function previewDefine(current: AlertDefine | null) {
-    if (!current?.datasource || !current?.expr) return;
-    setMessage(null);
-    try {
-      const result = await apiGet<{ code: number; data?: unknown; msg?: string }>(buildPreviewUrl(current));
-      setPreview(result.data);
-      setMessage(result.code === 0 ? t('setting.define.preview.success') : result.msg || t('setting.define.preview.failed'));
-    } catch (error) {
-      setPreview(null);
-      setMessage(error instanceof Error ? error.message : t('setting.define.preview.failed'));
-    }
-  }
-
-  async function saveSelected(current: AlertDefine | null) {
-    if (!current) return;
-    setSaving(true);
-    setMessage(null);
-    try {
-      if (current.id && current.id > 0) {
-        await saveDefine(apiMessagePut, current);
-      } else {
-        await apiMessagePost('/alert/define', current);
-      }
-      setMessage(t('setting.define.save.success'));
-      setIsEditing(false);
-    } catch (error) {
-      setMessage(error instanceof Error ? error.message : t('setting.define.save.failed'));
-    } finally {
-      setSaving(false);
-    }
-  }
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('setting.define.loading')}>
-      {data => {
-        const activeDefine = selected ?? data.list.content[0] ?? null;
-        const resolvedEditorValue = editorValue || buildDefineEditorValue(activeDefine);
-
-        return (
-          <SettingDefineSurface
-            t={t}
-            data={data}
-            search={search}
-            selectedDefine={activeDefine}
-            editorValue={resolvedEditorValue}
-            yamlLabel={buildCurrentYamlLabel(activeDefine)}
-            darkMode={darkMode}
-            isEditing={isEditing}
-            formatTime={formatTime}
-            message={message}
-            preview={preview}
-            onSearchChange={setSearch}
-            onSearch={() => setQuery(search)}
-            onSelectDefine={define => {
-              setSelected(cloneDefine(define));
-              setEditorValue(buildDefineEditorValue(define));
-              setMessage(null);
-              setPreview(null);
-              setIsEditing(false);
-            }}
-            onNew={() => {
-              const skeleton = buildSkeletonDefine();
-              setSelected(skeleton);
-              setEditorValue(buildDefineEditorValue(skeleton));
-              setMessage(null);
-              setPreview(null);
-              setIsEditing(true);
-            }}
-            onEdit={() => {
-              if (activeDefine) {
-                setSelected(cloneDefine(activeDefine));
-                setEditorValue(resolvedEditorValue);
-                setIsEditing(true);
-              }
-            }}
-            onCancel={() => {
-              setEditorValue(buildDefineEditorValue(activeDefine));
-              setMessage(null);
-              setPreview(null);
-              setIsEditing(false);
-            }}
-            onSave={() => void saveSelected(activeDefine)}
-            onDelete={() => setMessage(t('define.delete', { app: activeDefine?.name || 'current' }))}
-            onToggleDarkMode={setDarkMode}
-            onPreview={() => void previewDefine(activeDefine)}
-            onEditorValueChange={value => {
-              setEditorValue(value);
-              if (!isEditing) {
-                setIsEditing(true);
-              }
-            }}
-          />
-        );
-      }}
-    </ClientWorkbench>
-  );
+export default function SettingDefineRoutePage() {
+  return <SettingDefinePage />;
 }
diff --git a/web-next/app/setting/define/setting-define-page.tsx b/web-next/app/setting/define/setting-define-page.tsx
new file mode 100644
index 0000000000..daa9b32f8f
--- /dev/null
+++ b/web-next/app/setting/define/setting-define-page.tsx
@@ -0,0 +1,257 @@
+'use client';
+
+import { useCallback, useMemo, useState } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { SettingDefineSurface } from '@/components/pages/setting-define-surface';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
+import {
+  buildNewTemplateDraft,
+  deleteTemplateDefine,
+  loadDefineCenterData,
+  reloadTemplateDefinitionStartupContext,
+  saveTemplateDefine,
+  updateTemplateVisibility,
+  type SettingDefinePageData
+} from '@/lib/setting-define/controller';
+import { readSettingDefineRouteStateFromSearch } from '@/lib/setting-define/query-state';
+
+const SETTING_DEFINE_SETTLED_CACHE_TTL_MS = 10_000;
+
+function readInitialSelectedApp() {
+  if (typeof window === 'undefined') return undefined;
+  return readSettingDefineRouteStateFromSearch(window.location.search).app;
+}
+
+function writeSelectedAppToUrl(app: string | null) {
+  if (typeof window === 'undefined') return;
+  const url = new URL(window.location.href);
+  const nextSearch = app ? `?app=${encodeURIComponent(app)}` : '';
+  window.history.pushState({}, '', `${url.pathname}${nextSearch}`);
+}
+
+function buildYamlLabel(app: string | null, yaml: string) {
+  if (app) return `app-${app}.yml`;
+  const match = yaml.match(/^\s*app:\s*([^\s#]+)/m);
+  return `app-${match?.[1]?.trim() || 'custom'}.yml`;
+}
+
+function resolveDefineWorkbenchTheme(theme: string | null | undefined): 'dark-ops' | 'light-ops' {
+  return theme === 'light-ops' || theme === 'default' || theme === 'compact' ? 'light-ops' : 'dark-ops';
+}
+
+function readMutationFailureDetail(error: unknown, fallback = '') {
+  if (error instanceof Error) return error.message;
+  return fallback;
+}
+
+function readStoredTheme() {
+  if (typeof window === 'undefined') return null;
+  try {
+    return window.localStorage.getItem('theme');
+  } catch {
+    return null;
+  }
+}
+
+function readInitialDefineDarkMode() {
+  if (typeof document === 'undefined') return false;
+  const domTheme = document.body?.getAttribute('data-theme') || document.documentElement?.getAttribute('data-theme');
+  const theme = resolveDefineWorkbenchTheme(domTheme || readStoredTheme() || 'dark-ops');
+  return theme === 'dark-ops';
+}
+
+export default function SettingDefinePage() {
+  const { locale, t } = useI18n();
+  const [search, setSearch] = useState('');
+  const [selectedApp, setSelectedApp] = useState<string | null | undefined>(() => readInitialSelectedApp());
+  const [message, setMessage] = useState<string | null>(null);
+  const [messageMeta, setMessageMeta] = useState<string | null>(null);
+  const [messageContract, setMessageContract] = useState<string | null>(null);
+  const [editorValue, setEditorValue] = useState<string | null>(null);
+  const [originalYaml, setOriginalYaml] = useState<string | null>(null);
+  const [darkMode, setDarkMode] = useState(() => readInitialDefineDarkMode());
+  const [isEditing, setIsEditing] = useState(false);
+  const [savePending, setSavePending] = useState(false);
+  const [reloadVersion, setReloadVersion] = useState(0);
+  const settingDefineCacheKey = useMemo(
+    () => ['setting-define-yml', selectedApp || 'new', reloadVersion].join(':'),
+    [selectedApp, reloadVersion]
+  );
+
+  const load = useCallback(async () => {
+    return loadDefineCenterData(apiMessageGet, selectedApp ?? null, locale);
+  }, [selectedApp, locale]);
+
+  const refreshStartupContextAfterMutation = useCallback(() => {
+    void reloadTemplateDefinitionStartupContext(apiMessageGet, locale).catch(() => undefined);
+  }, [locale]);
+
+  const clearMutationMessage = useCallback(() => {
+    setMessage(null);
+    setMessageMeta(null);
+    setMessageContract(null);
+  }, []);
+
+  const buildFallbackDefineData = useCallback((): SettingDefinePageData => {
+    const draft = buildNewTemplateDraft(locale);
+    return {
+      menuGroups: [],
+      appLabels: {},
+      selectedApp: selectedApp ?? null,
+      yaml: draft.yaml,
+      originalYaml: draft.originalYaml
+    };
+  }, [locale, selectedApp]);
+
+  function selectApp(app: string) {
+    setSelectedApp(app);
+    writeSelectedAppToUrl(app);
+    setEditorValue(null);
+    setOriginalYaml(null);
+    clearMutationMessage();
+    setIsEditing(false);
+  }
+
+  function startNewTemplate() {
+    const draft = buildNewTemplateDraft(locale);
+    setSelectedApp(null);
+    setEditorValue(draft.yaml);
+    setOriginalYaml(draft.originalYaml);
+    clearMutationMessage();
+    setIsEditing(true);
+  }
+
+  async function saveCurrent(currentApp: string | null, currentYaml: string, currentOriginalYaml: string) {
+    if (currentYaml === currentOriginalYaml) return;
+    if (currentYaml === '') {
+      setMessage(t('define.save-apply.no-code'));
+      setMessageMeta(null);
+      setMessageContract(null);
+      return;
+    }
+    clearMutationMessage();
+    setSavePending(true);
+    try {
+      await saveTemplateDefine(apiMessagePost, apiMessagePut, currentYaml, !currentApp);
+      setMessage(t('common.notify.apply-success'));
+      setMessageMeta(null);
+      setMessageContract(null);
+      if (currentApp) {
+        setEditorValue(null);
+        setOriginalYaml(null);
+        setIsEditing(false);
+      } else {
+        setOriginalYaml(currentOriginalYaml);
+      }
+      setReloadVersion(version => version + 1);
+      refreshStartupContextAfterMutation();
+    } catch (error) {
+      setMessage(t('common.notify.apply-fail'));
+      setMessageMeta(readMutationFailureDetail(error, t('setting.define.save.failed')));
+      setMessageContract('angular-apply-fail-notification');
+    } finally {
+      setSavePending(false);
+    }
+  }
+
+  async function deleteCurrent(currentApp: string | null) {
+    if (!currentApp) return;
+    clearMutationMessage();
+    try {
+      await deleteTemplateDefine(apiMessageDelete, currentApp);
+      const draft = buildNewTemplateDraft(locale);
+      setSelectedApp(null);
+      setEditorValue(draft.yaml);
+      setOriginalYaml(draft.originalYaml);
+      setMessage(t('common.notify.delete-success'));
+      setMessageMeta(null);
+      setMessageContract(null);
+      setReloadVersion(version => version + 1);
+      refreshStartupContextAfterMutation();
+    } catch (error) {
+      setMessage(t('common.notify.delete-fail'));
+      setMessageMeta(readMutationFailureDetail(error));
+      setMessageContract('angular-delete-fail-notification');
+    }
+  }
+
+  async function toggleTemplateVisibility(app: string, hide: boolean) {
+    clearMutationMessage();
+    setSavePending(true);
+    try {
+      await updateTemplateVisibility(apiMessagePut, app, hide);
+      setMessage(t('common.notify.apply-success'));
+      setMessageMeta(null);
+      setMessageContract(null);
+      setReloadVersion(version => version + 1);
+      refreshStartupContextAfterMutation();
+    } catch (error) {
+      setMessage(t('common.notify.apply-fail'));
+      setMessageMeta(readMutationFailureDetail(error));
+      setMessageContract('angular-apply-fail-notification');
+    } finally {
+      setSavePending(false);
+    }
+  }
+
+  function renderDefineSurface(data: SettingDefinePageData, loadError: string | null = null) {
+    const activeApp = selectedApp === undefined ? data.selectedApp : selectedApp;
+    const resolvedEditorValue = editorValue ?? data.yaml;
+    const resolvedOriginalYaml = originalYaml ?? data.originalYaml;
+
+    return (
+      <SettingDefineSurface
+        t={t}
+        data={data}
+        search={search}
+        selectedApp={activeApp}
+        editorValue={resolvedEditorValue}
+        originalYaml={resolvedOriginalYaml}
+        yamlLabel={buildYamlLabel(activeApp, resolvedEditorValue)}
+        darkMode={darkMode}
+        isEditing={isEditing}
+        menuLoading={false}
+        editorLoading={false}
+        savePending={savePending}
+        message={message}
+        messageMeta={messageMeta}
+        messageContract={messageContract}
+        loadError={loadError}
+        onSearchChange={setSearch}
+        onSearch={() => undefined}
+        onSelectApp={selectApp}
+        onNew={startNewTemplate}
+        onEdit={() => setIsEditing(true)}
+        onCancel={() => {
+          setEditorValue(resolvedOriginalYaml);
+          clearMutationMessage();
+          setIsEditing(false);
+        }}
+        onSave={() => void saveCurrent(activeApp, resolvedEditorValue, resolvedOriginalYaml)}
+        onDelete={() => void deleteCurrent(activeApp)}
+        onToggleTemplateVisibility={(app, hide) => void toggleTemplateVisibility(app, hide)}
+        onToggleDarkMode={setDarkMode}
+        onEditorValueChange={value => {
+          setEditorValue(value);
+          clearMutationMessage();
+          if (!isEditing) {
+            setIsEditing(true);
+          }
+        }}
+      />
+    );
+  }
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.define.loading')}
+      cacheKey={settingDefineCacheKey}
+      cacheSettledTtlMs={SETTING_DEFINE_SETTLED_CACHE_TTL_MS}
+      renderError={(message) => renderDefineSurface(buildFallbackDefineData(), message)}
+    >
+      {data => renderDefineSurface(data)}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/setting/labels/page.test.tsx b/web-next/app/setting/labels/page.test.tsx
index 22e16e93e5..e90db4cb8e 100644
--- a/web-next/app/setting/labels/page.test.tsx
+++ b/web-next/app/setting/labels/page.test.tsx
@@ -1,14 +1,22 @@
+// @vitest-environment jsdom
+
 import React from 'react';
 import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
+import { act } from 'react';
+import { createRoot } from 'react-dom/client';
 import { renderToStaticMarkup } from 'react-dom/server';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
 import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 (globalThis as { React?: typeof React }).React = React;
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
+  lastRetry: null as null | (() => void),
+  renderErrorMessage: null as string | null,
+  lastSurfaceProps: null as null | Record<string, any>,
   renderData: {
     list: {
       content: [
@@ -32,14 +40,14 @@ const apiMessagePost = vi.hoisted(() => vi.fn());
 const apiMessagePut = vi.hoisted(() => vi.fn());
 const apiMessageDelete = vi.hoisted(() => vi.fn());
 const loadLabelData = vi.hoisted(() => vi.fn());
+const saveLabel = vi.hoisted(() => vi.fn());
+const deleteLabel = vi.hoisted(() => vi.fn());
+let SettingLabelsPage: React.ComponentType;
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
     t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'setting.labels.loading': '正在加载标签'
-      }
+      locale: 'zh-CN'
     })
   })
 }));
@@ -47,30 +55,136 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    renderError
   }: {
     children: (data: unknown) => React.ReactNode;
     load: () => Promise<unknown>;
+    renderError?: (message: string, retry: () => void) => React.ReactNode;
   }) => {
     mockState.lastLoad = load;
+    if (mockState.renderErrorMessage && renderError) {
+      const retry = vi.fn();
+      mockState.lastRetry = retry;
+      return <div data-client-workbench="true">{renderError(mockState.renderErrorMessage, retry)}</div>;
+    }
     return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
   }
 }));
 
 vi.mock('@/components/pages/label-manage-surface', () => ({
-  LabelManageSurface: ({ search, draftLabel, isManageModalAdd }: any) => (
+  LabelManageSurface: (props: any) => {
+    mockState.lastSurfaceProps = props;
+    const {
+      data,
+      search,
+      draftLabel,
+      isManageModalAdd,
+      isNameValidationVisible,
+      deleteTarget,
+      isLoadPending,
+      isDeletePending,
+      isSavePending,
+      actionMessage,
+      actionError,
+      actionMeta,
+      loadError,
+      onSearchClear,
+      onEdit,
+      onDraftChange,
+      onCloseDialog
+    } = props;
+    return (
     <div
       data-label-manage-surface="otlp-cold-label-console"
       data-label-manage-style-baseline="hertzbeat-cold-matte"
+      data-label-action-feedback={actionError ? 'angular-action-error' : actionMessage ? 'angular-action-success' : 'none'}
+      data-label-save-feedback={actionError ? 'angular-save-error' : actionMessage ? 'angular-save-success' : 'none'}
+      data-label-action-meta={actionMeta ?? 'no-action-meta'}
+      data-label-save-feedback-contract="angular-new-edit-notify"
+      data-label-save-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+      data-label-save-failure-contract="angular-new-edit-fail-notification"
+      data-label-save-failure-owner="hertzbeat-ui-inline-feedback"
+      data-label-load-failure-contract="angular-console-only-shell"
+      data-label-load-failure-owner="label-route-controller"
+      data-label-load-failure={loadError ? 'angular-console-only-shell' : 'none'}
+      data-label-copy-feedback-contract="angular-copy-notify"
+      data-label-copy-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+      data-label-delete-confirm-contract="angular-modal-confirm"
+      data-label-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"
+      data-label-delete-confirm-state-contract="angular-modal-confirm-state"
+      data-label-delete-confirm-state-contract-owner="hertzbeat-ui-confirm-dialog"
+      data-label-delete-confirm-state={deleteTarget ? 'open' : 'closed'}
+      data-label-delete-confirm-close-contract="angular-close-before-delete-result"
+      data-label-delete-confirm-close-owner="route-state-contract"
+      data-label-delete-feedback-contract="angular-delete-notify"
+      data-label-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+      data-label-delete-failure-contract="angular-delete-fail-notification"
+      data-label-delete-failure-owner="hertzbeat-ui-inline-feedback"
+      data-label-delete-query-contract="angular-repeated-ids-query"
+      data-label-delete-query-owner="route-mutation-contract"
+      data-label-card-loading-contract="angular-table-loading"
+      data-label-card-loading-owner="label-route-controller"
+      data-label-card-loading={isLoadPending || isDeletePending ? 'true' : 'false'}
+      data-label-save-loading-contract="angular-nz-ok-loading"
+      data-label-save-loading-contract-owner="angular-nz-ok-loading"
+      data-label-save-trim-contract="angular-trim-before-save"
+      data-label-save-trim-owner="label-manage-controller"
+      data-label-optional-save-payload-contract="angular-preserve-undefined-optional-fields"
+      data-label-optional-save-payload-owner="label-manage-controller"
+      data-label-type-save-payload-contract="angular-preserve-implicit-type"
+      data-label-type-save-payload-owner="label-manage-controller"
+      data-label-description-display-contract="angular-truthy-description-raw"
+      data-label-description-display-owner="label-manage-surface"
+      data-label-dialog-mask-closable-contract="angular-mask-closable-false"
+      data-label-dialog-mask-closable-owner="angular-nz-modal"
+      data-label-dialog-width-contract="angular-width-30-percent"
+      data-label-dialog-width-owner="angular-nz-modal"
+      data-label-dialog-field-layout-contract="angular-label-7-control-12"
+      data-label-dialog-field-layout-owner="route-form-field-grid"
+      data-label-dialog-preview-visibility-contract="angular-name-defined-preview"
+      data-label-dialog-preview-visibility-owner="route-form-state"
+      data-label-dialog-preview-frame-contract="angular-inline-form-item"
+      data-label-dialog-preview-frame-owner="route-form-field-grid"
+      data-label-edit-reference-contract="angular-edit-direct-reference"
+      data-label-edit-reference-owner="route-form-state"
+      data-label-save-ok-loading={isSavePending ? 'true' : 'false'}
+      data-label-save-ok-loading-owner="angular-nz-ok-loading"
+      data-label-name-validation-contract="angular-required-before-submit"
+      data-label-name-validation-trigger="angular-ok-marks-dirty"
+      data-label-name-validation-trigger-owner="route-form-contract"
+      data-label-name-validation-raw-contract="angular-required-before-trim"
+      data-label-name-validation-raw-owner="route-form-contract"
+      data-label-name-validation-visible={isNameValidationVisible ? 'true' : 'false'}
+      data-label-query-contract="angular-load-all-labels"
+      data-label-query-owner="label-query-state"
+      data-label-query-param-order-contract="angular-page-index-size-type-search"
+      data-label-query-param-order-owner="label-query-state"
+      data-label-search-submit="angular-enter-and-clear"
+      data-label-search-submit-owner="cold-search-row"
+      data-label-search-clear-wired={typeof onSearchClear === 'function' ? 'true' : 'false'}
+      data-label-card-grid-contract="angular-card-grid"
+      data-label-card-grid-owner="hertzbeat-ui-label-tag"
+      data-label-monitor-handoff-contract="angular-routerlink-monitors-labels"
+      data-label-monitor-handoff-owner="next-monitor-query-link"
     >
       <section data-label-admin-layout="full-width-admin-list">
         <span>标签管理</span>
         <span>{search}</span>
+        <span data-label-test-first-name="true">{data.list.content[0]?.name || 'no-row'}</span>
         <span>{draftLabel ? draftLabel.name : 'no-draft'}</span>
         <span>{String(isManageModalAdd)}</span>
+        <span>{deleteTarget ? deleteTarget.name : 'no-delete-target'}</span>
+        <span>{actionMessage || actionError || 'no-feedback'}</span>
+        <span>{actionMeta || 'no-meta'}</span>
+        <span>{loadError || 'no-load-error'}</span>
+        <button data-label-test-edit="true" onClick={() => onEdit(data.list.content[0])}>edit</button>
+        <button data-label-test-change-name="true" onClick={() => onDraftChange({ name: 'platform' })}>change name</button>
+        <button data-label-test-close="true" onClick={onCloseDialog}>close</button>
       </section>
     </div>
-  )
+    );
+  }
 }));
 
 vi.mock('@/lib/api-client', () => ({
@@ -86,27 +200,52 @@ vi.mock('@/lib/format', () => ({
 
 vi.mock('@/lib/label-manage/controller', () => ({
   loadLabelData,
-  createEmptyLabelDraft: () => ({ id: 0, name: '', tagValue: '', description: '', type: 1 }),
+  createEmptyLabelDraft: () => ({ id: 0, name: undefined, tagValue: undefined, description: undefined, type: undefined }),
   cloneLabelDraft: (label: any) => ({ ...label }),
-  saveLabel: vi.fn(),
-  deleteLabel: vi.fn()
+  saveLabel,
+  deleteLabel
 }));
 
 describe('setting labels page', () => {
+  beforeAll(async () => {
+    const pageModule = await import('./page');
+    SettingLabelsPage = pageModule.default;
+  }, 30000);
+
   beforeEach(() => {
     mockState.lastLoad = null;
+    mockState.lastRetry = null;
+    mockState.renderErrorMessage = null;
+    mockState.lastSurfaceProps = null;
+    mockState.renderData = {
+      list: {
+        content: [
+          {
+            id: 1,
+            name: 'team',
+            tagValue: 'ops',
+            description: 'ops team',
+            type: 1
+          }
+        ],
+        totalElements: 1,
+        pageIndex: 0,
+        pageSize: 8
+      }
+    };
     apiMessageGet.mockReset();
     apiMessagePost.mockReset();
     apiMessagePut.mockReset();
     apiMessageDelete.mockReset();
+    saveLabel.mockReset().mockResolvedValue(undefined);
+    deleteLabel.mockReset().mockResolvedValue(undefined);
     loadLabelData.mockReset().mockImplementation(async (apiGetFn, query) => {
-      await apiGetFn(`/label?pageIndex=0&pageSize=8${query.search ? `&search=${query.search}` : ''}`);
+      await apiGetFn(`/label?pageIndex=0&pageSize=9999${query.search ? `&search=${query.search}` : ''}`);
       return mockState.renderData;
     });
   });
 
   it('renders the shared label surface and keeps the route focused on load composition', async () => {
-    const { default: SettingLabelsPage } = await import('./page');
     const html = renderToStaticMarkup(<SettingLabelsPage />);
 
     expect(html).toContain('data-client-workbench="true"');
@@ -119,16 +258,156 @@ describe('setting labels page', () => {
     expect(html).toContain('标签管理');
     expect(html).toContain('no-draft');
     expect(html).toContain('false');
+    expect(html).toContain('data-label-save-feedback="none"');
+    expect(html).toContain('data-label-save-feedback-contract="angular-new-edit-notify"');
+    expect(html).toContain('data-label-save-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-save-failure-contract="angular-new-edit-fail-notification"');
+    expect(html).toContain('data-label-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-label-load-failure-owner="label-route-controller"');
+    expect(html).toContain('data-label-load-failure="none"');
+    expect(html).toContain('data-label-copy-feedback-contract="angular-copy-notify"');
+    expect(html).toContain('data-label-copy-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-delete-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-label-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-label-delete-confirm-state-contract="angular-modal-confirm-state"');
+    expect(html).toContain('data-label-delete-confirm-state-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-label-delete-confirm-state="closed"');
+    expect(html).toContain('data-label-delete-confirm-close-contract="angular-close-before-delete-result"');
+    expect(html).toContain('data-label-delete-confirm-close-owner="route-state-contract"');
+    expect(html).toContain('data-label-delete-feedback-contract="angular-delete-notify"');
+    expect(html).toContain('data-label-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-delete-failure-contract="angular-delete-fail-notification"');
+    expect(html).toContain('data-label-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-delete-query-contract="angular-repeated-ids-query"');
+    expect(html).toContain('data-label-delete-query-owner="route-mutation-contract"');
+    expect(html).toContain('data-label-card-loading-contract="angular-table-loading"');
+    expect(html).toContain('data-label-card-loading-owner="label-route-controller"');
+    expect(html).toContain('data-label-card-loading="false"');
+    expect(html).toContain('data-label-dialog-field-layout-contract="angular-label-7-control-12"');
+    expect(html).toContain('data-label-dialog-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-label-dialog-preview-visibility-contract="angular-name-defined-preview"');
+    expect(html).toContain('data-label-dialog-preview-visibility-owner="route-form-state"');
+    expect(html).toContain('data-label-dialog-preview-frame-contract="angular-inline-form-item"');
+    expect(html).toContain('data-label-dialog-preview-frame-owner="route-form-field-grid"');
+    expect(html).toContain('data-label-edit-reference-contract="angular-edit-direct-reference"');
+    expect(html).toContain('data-label-edit-reference-owner="route-form-state"');
+    expect(html).toContain('data-label-save-loading-contract="angular-nz-ok-loading"');
+    expect(html).toContain('data-label-save-loading-contract-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-label-save-trim-contract="angular-trim-before-save"');
+    expect(html).toContain('data-label-save-trim-owner="label-manage-controller"');
+    expect(html).toContain('data-label-optional-save-payload-contract="angular-preserve-undefined-optional-fields"');
+    expect(html).toContain('data-label-optional-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-label-type-save-payload-contract="angular-preserve-implicit-type"');
+    expect(html).toContain('data-label-type-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-label-dialog-mask-closable-contract="angular-mask-closable-false"');
+    expect(html).toContain('data-label-dialog-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-label-dialog-width-contract="angular-width-30-percent"');
+    expect(html).toContain('data-label-dialog-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-label-save-ok-loading="false"');
+    expect(html).toContain('data-label-save-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-label-name-validation-contract="angular-required-before-submit"');
+    expect(html).toContain('data-label-name-validation-trigger="angular-ok-marks-dirty"');
+    expect(html).toContain('data-label-name-validation-trigger-owner="route-form-contract"');
+    expect(html).toContain('data-label-name-validation-raw-contract="angular-required-before-trim"');
+    expect(html).toContain('data-label-name-validation-raw-owner="route-form-contract"');
+    expect(html).toContain('data-label-name-validation-visible="false"');
+    expect(html).toContain('data-label-query-contract="angular-load-all-labels"');
+    expect(html).toContain('data-label-query-owner="label-query-state"');
+    expect(html).toContain('data-label-query-param-order-contract="angular-page-index-size-type-search"');
+    expect(html).toContain('data-label-query-param-order-owner="label-query-state"');
+    expect(html).toContain('data-label-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-label-search-submit-owner="cold-search-row"');
+    expect(html).toContain('data-label-search-clear-wired="true"');
+    expect(html).toContain('data-label-card-grid-contract="angular-card-grid"');
+    expect(html).toContain('data-label-card-grid-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-label-monitor-handoff-contract="angular-routerlink-monitors-labels"');
+    expect(html).toContain('data-label-monitor-handoff-owner="next-monitor-query-link"');
+    expect(html).toContain('no-delete-target');
+    expect(html).toContain('no-feedback');
+    expect(html).toContain('no-meta');
+    expect(html).toContain('no-load-error');
 
     await mockState.lastLoad?.();
 
-    expect(apiMessageGet).toHaveBeenCalledWith('/label?pageIndex=0&pageSize=8');
+    expect(apiMessageGet).toHaveBeenCalledWith('/label?pageIndex=0&pageSize=9999');
+  }, 30000);
+
+  it('keeps the Angular label shell visible when list loading fails', () => {
+    mockState.renderErrorMessage = 'backend refused label load';
+
+    const html = renderToStaticMarkup(<SettingLabelsPage />);
+
+    expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-label-manage-surface="otlp-cold-label-console"');
+    expect(html).toContain('data-label-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-label-load-failure-owner="label-route-controller"');
+    expect(html).toContain('data-label-load-failure="angular-console-only-shell"');
+    expect(html).toContain('backend refused label load');
+    expect(html).toContain('data-label-card-loading="false"');
+    expect(html).toContain('data-label-save-feedback="none"');
+    expect(html).toContain('data-label-test-first-name="true">no-row');
+    expect(html).not.toContain('data-observability-status');
+    expect(html).not.toContain('common.load-failed');
+    expect(mockState.lastRetry).toBeTypeOf('function');
   });
 
+  it('keeps the Angular edit direct-reference behavior when canceling the label dialog', async () => {
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingLabelsPage />);
+      await Promise.resolve();
+    });
+
+    expect(container.querySelector('[data-label-test-first-name="true"]')?.textContent).toBe('team');
+    expect(mockState.lastSurfaceProps?.draftLabel).toBeNull();
+
+    await act(async () => {
+      (container.querySelector('[data-label-test-edit="true"]') as HTMLButtonElement).click();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.draftLabel).toBe(mockState.renderData.list.content[0]);
+    expect(mockState.lastSurfaceProps?.draftLabel?.name).toBe('team');
+
+    await act(async () => {
+      (container.querySelector('[data-label-test-change-name="true"]') as HTMLButtonElement).click();
+      await Promise.resolve();
+    });
+
+    expect(mockState.renderData.list.content[0].name).toBe('platform');
+    expect(container.querySelector('[data-label-test-first-name="true"]')?.textContent).toBe('platform');
+    expect(mockState.lastSurfaceProps?.draftLabel?.name).toBe('platform');
+
+    await act(async () => {
+      (container.querySelector('[data-label-test-close="true"]') as HTMLButtonElement).click();
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.draftLabel).toBeNull();
+    expect(mockState.renderData.list.content[0].name).toBe('platform');
+    expect(container.querySelector('[data-label-test-first-name="true"]')?.textContent).toBe('platform');
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  }, 30000);
+
   it('keeps the route thin and leaves cold label visuals in the shared surface', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/setting/labels/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/labels/setting-labels-page.tsx'), 'utf8');
 
     expect(source).toContain('LabelManageSurface');
+    expect(source).toContain('EMPTY_LABEL_DATA');
+    expect(source).toContain('renderError={(message, retry) => renderSurface(EMPTY_LABEL_DATA, message, retry)}');
+    expect(source).toContain('buildLabelUrl(query)');
+    expect(source).toContain('setDraftLabel(label)');
+    expect(source).toContain('Object.assign(sourceLabel, patch)');
+    expect(source).not.toContain('cloneLabelDraft');
     expect(source).not.toContain('WorkbenchPage');
     expect(source).not.toContain('SurfaceSection');
     expect(source).not.toContain('ToolbarRow');
@@ -137,4 +416,297 @@ describe('setting labels page', () => {
     expect(source).not.toContain('data-label-toolbar');
     expect(source).not.toContain('otlp-cold-label-console');
   });
+
+  it('keeps label management remounts on a short settled cache window while reloads invalidate it', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/labels/setting-labels-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_LABELS_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain('const [isLoadPending, setIsLoadPending] = useState(false)');
+    expect(source).toContain('setIsLoadPending(true);');
+    expect(source).toContain('return await loadLabelData(apiMessageGet, query);');
+    expect(source).toContain('setIsLoadPending(false);');
+    expect(source).toContain('isLoadPending={isLoadPending}');
+    expect(source).toContain("['setting-labels', labelListUrl, reloadVersion].join(':')");
+    expect(source).toContain('[labelListUrl, reloadVersion]');
+    expect(source).toContain('onRefresh={() => {');
+    expect(source).toContain('if (retry) {');
+    expect(source).toContain('retry();');
+    expect(source).toContain('setReloadVersion(version => version + 1)');
+    expect(source).toContain('cacheKey={labelManageCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_LABELS_SETTLED_CACHE_TTL_MS}');
+  });
+
+  it('resets the search draft and query when the Angular clear event fires', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/labels/setting-labels-page.tsx'), 'utf8');
+
+    expect(source).toContain("onSearch={() => setQuery({ search, type: '' })}");
+    expect(source).toContain('onSearchClear={() => {');
+    expect(source).toContain("setSearch('');");
+    expect(source).toContain("setQuery({ search: '', type: '' });");
+  });
+
+  it('routes label save feedback through Angular create/edit notification keys', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/labels/setting-labels-page.tsx'), 'utf8');
+
+    expect(source).toContain('const [actionMessage, setActionMessage] = useState<string | null>(null)');
+    expect(source).toContain('const [actionError, setActionError] = useState<string | null>(null)');
+    expect(source).toContain('const [actionMeta, setActionMeta] = useState<string | null>(null)');
+    expect(source).toContain('const [isNameValidationVisible, setIsNameValidationVisible] = useState(false)');
+    expect(source).toContain('actionMessage={actionMessage}');
+    expect(source).toContain('actionError={actionError}');
+    expect(source).toContain('actionMeta={actionMeta}');
+    expect(source).toContain('isNameValidationVisible={isNameValidationVisible}');
+    expect(source).toContain('if (draftLabel.name == undefined || draftLabel.name.length === 0) {');
+    expect(source).toContain('setIsNameValidationVisible(true);');
+    expect(source).toContain('return;');
+    expect(source).toContain('setIsNameValidationVisible(false);');
+    expect(source).toContain("setActionMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'))");
+    expect(source).toContain("setActionError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'))");
+    expect(source).toContain('setActionMeta(error instanceof Error ? error.message : null)');
+    expect(source).not.toContain("setActionMessage(t('common.save-success'))");
+    expect(source).not.toContain("setActionError(t('common.save-failed'))");
+  });
+
+  it('lets a whitespace-only label name follow Angular required-before-trim save behavior', async () => {
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+
+    await act(async () => {
+      root.render(<SettingLabelsPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onNew();
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDraftChange({ name: '   ', tagValue: 'ops' });
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSaveDialog();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(saveLabel).toHaveBeenCalledWith(
+      apiMessagePost,
+      apiMessagePut,
+      expect.objectContaining({ name: '   ', tagValue: 'ops' }),
+      true
+    );
+    expect(mockState.lastSurfaceProps?.isNameValidationVisible).toBe(false);
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  }, 30000);
+
+  it('keeps Angular label create failure title with backend detail', async () => {
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+    saveLabel.mockRejectedValue(new Error('backend refused label create'));
+
+    await act(async () => {
+      root.render(<SettingLabelsPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onNew();
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDraftChange({ name: 'team', tagValue: 'ops' });
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSaveDialog();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(saveLabel).toHaveBeenCalledWith(
+      apiMessagePost,
+      apiMessagePut,
+      expect.objectContaining({ name: 'team', tagValue: 'ops' }),
+      true
+    );
+    expect(mockState.lastSurfaceProps?.actionError).toBe('新增失败!');
+    expect(mockState.lastSurfaceProps?.actionMeta).toBe('backend refused label create');
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  }, 30000);
+
+  it('keeps Angular label edit failure title with backend detail', async () => {
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+    saveLabel.mockRejectedValue(new Error('backend refused label edit'));
+
+    await act(async () => {
+      root.render(<SettingLabelsPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onEdit(mockState.renderData.list.content[0]);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onSaveDialog();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(saveLabel).toHaveBeenCalledWith(
+      apiMessagePost,
+      apiMessagePut,
+      mockState.renderData.list.content[0],
+      false
+    );
+    expect(mockState.lastSurfaceProps?.actionError).toBe('修改失败!');
+    expect(mockState.lastSurfaceProps?.actionMeta).toBe('backend refused label edit');
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  }, 30000);
+
+  it('routes label copy feedback through Angular clipboard notification keys', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/labels/setting-labels-page.tsx'), 'utf8');
+
+    expect(source).toContain("import { buildLabelDisplayName } from '@/lib/label-manage/view-model'");
+    expect(source).toContain('const text = buildLabelDisplayName(label)');
+    expect(source).toContain("setActionError(t('common.notify.copy-fail'))");
+    expect(source).toContain("then(() => setActionMessage(t('common.notify.copy-success')))");
+    expect(source).toContain("catch(() => setActionError(t('common.notify.copy-fail')))");
+    expect(source).not.toContain('navigator.clipboard.writeText(text);');
+  });
+
+  it('closes the Angular label delete confirmation before the delete result settles', async () => {
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+    deleteLabel.mockReturnValue(new Promise(() => undefined));
+
+    await act(async () => {
+      root.render(<SettingLabelsPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDeleteRequest(mockState.renderData.list.content[0]);
+      await Promise.resolve();
+    });
+
+    expect(mockState.lastSurfaceProps?.deleteTarget?.name).toBe('team');
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDeleteConfirm();
+      await Promise.resolve();
+    });
+
+    expect(deleteLabel).toHaveBeenCalledWith(apiMessageDelete, 1);
+    expect(mockState.lastSurfaceProps?.deleteTarget).toBeNull();
+    expect(mockState.lastSurfaceProps?.isDeletePending).toBe(true);
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  }, 30000);
+
+  it('keeps Angular label delete failure title with backend detail', async () => {
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+    const root = createRoot(container);
+    deleteLabel.mockRejectedValue(new Error('backend refused label delete'));
+
+    await act(async () => {
+      root.render(<SettingLabelsPage />);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDeleteRequest(mockState.renderData.list.content[0]);
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      mockState.lastSurfaceProps?.onDeleteConfirm();
+      await Promise.resolve();
+      await Promise.resolve();
+    });
+
+    expect(deleteLabel).toHaveBeenCalledWith(apiMessageDelete, 1);
+    expect(mockState.lastSurfaceProps?.deleteTarget).toBeNull();
+    expect(mockState.lastSurfaceProps?.actionError).toBe('删除失败!');
+    expect(mockState.lastSurfaceProps?.actionMeta).toBe('backend refused label delete');
+
+    await act(async () => {
+      root.unmount();
+      await Promise.resolve();
+    });
+    container.remove();
+  }, 30000);
+
+  it('routes label delete through Angular confirmation and notification keys', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/labels/setting-labels-page.tsx'), 'utf8');
+
+    expect(source).toContain('const [deleteTarget, setDeleteTarget] = useState<Label | null>(null)');
+    expect(source).toContain('const [isDeletePending, setIsDeletePending] = useState(false)');
+    expect(source).toContain('const [isSavePending, setIsSavePending] = useState(false)');
+    expect(source).toContain('deleteTarget={deleteTarget}');
+    expect(source).toContain('isDeletePending={isDeletePending}');
+    expect(source).toContain('isSavePending={isSavePending}');
+    expect(source).toContain('onDeleteRequest={label =>');
+    expect(source).toContain('setDeleteTarget(label)');
+    expect(source).toContain('onDeleteConfirm={() =>');
+    expect(source).toContain('const target = deleteTarget;');
+    expect(source).toContain('setDeleteTarget(null);');
+    expect(source).toContain('void deleteLabel(apiMessageDelete, target.id)');
+    expect(source).toContain("setActionMessage(t('common.notify.delete-success'))");
+    expect(source).toContain("setActionError(t('common.notify.delete-fail'))");
+    expect(source).toContain('setActionMeta(error instanceof Error ? error.message : null)');
+    expect(source).not.toContain('onDelete={label =>');
+  });
+
+  it('routes label save pending state through Angular nzOkLoading semantics', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/labels/setting-labels-page.tsx'), 'utf8');
+
+    expect(source).toContain('const [isSavePending, setIsSavePending] = useState(false)');
+    expect(source).toContain('isSavePending={isSavePending}');
+    expect(source).toContain('setIsSavePending(true);');
+    expect(source).toContain('setIsSavePending(false);');
+    expect(source).toContain('.finally(() => {');
+    expect(source).not.toContain('isManageModalOkLoading');
+  });
+
+  it('keeps Angular trim-before-save normalization in the label controller', () => {
+    const source = readFileSync(resolve(process.cwd(), 'lib/label-manage/controller.ts'), 'utf8');
+
+    expect(source).toContain('function normalizeLabelDraft(draft: Label): Label');
+    expect(source).toContain("name: (draft.name ?? '').trim()");
+    expect(source).toContain('tagValue: draft.tagValue == undefined ? undefined : draft.tagValue.trim()');
+    expect(source).toContain('description: draft.description == undefined ? undefined : draft.description.trim()');
+    expect(source).toContain('const payload = normalizeLabelDraft(draft)');
+  });
 });
diff --git a/web-next/app/setting/labels/page.tsx b/web-next/app/setting/labels/page.tsx
index 5a041ce0a7..63839a61ce 100644
--- a/web-next/app/setting/labels/page.tsx
+++ b/web-next/app/setting/labels/page.tsx
@@ -1,70 +1,7 @@
-'use client';
+import React from 'react';
 
-import { useCallback, useState } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { LabelManageSurface } from '@/components/pages/label-manage-surface';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
-import { formatTime } from '@/lib/format';
-import { cloneLabelDraft, createEmptyLabelDraft, deleteLabel, loadLabelData, saveLabel } from '@/lib/label-manage/controller';
-import type { LabelQueryState } from '@/lib/label-manage/query-state';
-import type { Label } from '@/lib/types';
+import SettingLabelsPage from './setting-labels-page';
 
-export default function SettingLabelsPage() {
-  const { t } = useI18n();
-  const [search, setSearch] = useState('');
-  const [query, setQuery] = useState<LabelQueryState>({ search: '', type: '' });
-  const [draftLabel, setDraftLabel] = useState<Label | null>(null);
-  const [isManageModalAdd, setIsManageModalAdd] = useState(false);
-  const [reloadVersion, setReloadVersion] = useState(0);
-
-  const load = useCallback(async () => {
-    return loadLabelData(apiMessageGet, query);
-  }, [query, reloadVersion]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('setting.labels.loading')}>
-      {data => (
-        <LabelManageSurface
-          t={t}
-          data={data}
-          search={search}
-          draftLabel={draftLabel}
-          isManageModalAdd={isManageModalAdd}
-          formatTime={formatTime}
-          onSearchChange={setSearch}
-          onSearch={() => setQuery({ search, type: '' })}
-          onRefresh={() => setReloadVersion(version => version + 1)}
-          onNew={() => {
-            setDraftLabel(createEmptyLabelDraft());
-            setIsManageModalAdd(true);
-          }}
-          onCopy={label => {
-            const text = `${label.name}${label.tagValue ? `:${label.tagValue}` : ''}`;
-            if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
-              void navigator.clipboard.writeText(text);
-            }
-          }}
-          onEdit={label => {
-            setDraftLabel(cloneLabelDraft(label));
-            setIsManageModalAdd(false);
-          }}
-          onDelete={label => {
-            void deleteLabel(apiMessageDelete, label.id).then(() => {
-              setReloadVersion(version => version + 1);
-            });
-          }}
-          onDraftChange={patch => setDraftLabel(previous => (previous ? { ...previous, ...patch } : previous))}
-          onCloseDialog={() => setDraftLabel(null)}
-          onSaveDialog={() => {
-            if (!draftLabel) return;
-            void saveLabel(apiMessagePost, apiMessagePut, draftLabel, isManageModalAdd).then(() => {
-              setDraftLabel(null);
-              setReloadVersion(version => version + 1);
-            });
-          }}
-        />
-      )}
-    </ClientWorkbench>
-  );
+export default function SettingLabelsRoutePage() {
+  return <SettingLabelsPage />;
 }
diff --git a/web-next/app/setting/labels/setting-labels-page.tsx b/web-next/app/setting/labels/setting-labels-page.tsx
new file mode 100644
index 0000000000..912928a90e
--- /dev/null
+++ b/web-next/app/setting/labels/setting-labels-page.tsx
@@ -0,0 +1,215 @@
+'use client';
+
+import { useCallback, useMemo, useState } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { LabelManageSurface } from '@/components/pages/label-manage-surface';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
+import { formatTime } from '@/lib/format';
+import { createEmptyLabelDraft, deleteLabel, loadLabelData, saveLabel } from '@/lib/label-manage/controller';
+import { buildLabelUrl, type LabelQueryState } from '@/lib/label-manage/query-state';
+import { buildLabelDisplayName } from '@/lib/label-manage/view-model';
+import type { Label } from '@/lib/types';
+
+const SETTING_LABELS_SETTLED_CACHE_TTL_MS = 10_000;
+const EMPTY_LABEL_DATA = {
+  list: {
+    content: [],
+    totalElements: 0,
+    pageIndex: 0,
+    pageSize: 9999
+  }
+};
+
+export default function SettingLabelsPage() {
+  const { t } = useI18n();
+  const [search, setSearch] = useState('');
+  const [query, setQuery] = useState<LabelQueryState>({ search: '', type: '' });
+  const [draftLabel, setDraftLabel] = useState<Label | null>(null);
+  const [isManageModalAdd, setIsManageModalAdd] = useState(false);
+  const [deleteTarget, setDeleteTarget] = useState<Label | null>(null);
+  const [isLoadPending, setIsLoadPending] = useState(false);
+  const [isDeletePending, setIsDeletePending] = useState(false);
+  const [isSavePending, setIsSavePending] = useState(false);
+  const [isNameValidationVisible, setIsNameValidationVisible] = useState(false);
+  const [actionMessage, setActionMessage] = useState<string | null>(null);
+  const [actionError, setActionError] = useState<string | null>(null);
+  const [actionMeta, setActionMeta] = useState<string | null>(null);
+  const [reloadVersion, setReloadVersion] = useState(0);
+  const labelListUrl = useMemo(() => buildLabelUrl(query), [query]);
+  const labelManageCacheKey = useMemo(
+    () => ['setting-labels', labelListUrl, reloadVersion].join(':'),
+    [labelListUrl, reloadVersion]
+  );
+
+  const load = useCallback(async () => {
+    setIsLoadPending(true);
+    try {
+      return await loadLabelData(apiMessageGet, query);
+    } finally {
+      setIsLoadPending(false);
+    }
+  }, [query]);
+
+  const renderSurface = useCallback(
+    (data: Awaited<ReturnType<typeof loadLabelData>>, loadError: string | null = null, retry?: () => void) => (
+      <LabelManageSurface
+        t={t}
+        data={data}
+        search={search}
+        draftLabel={draftLabel}
+        isManageModalAdd={isManageModalAdd}
+        isNameValidationVisible={isNameValidationVisible}
+        deleteTarget={deleteTarget}
+        isLoadPending={isLoadPending}
+        isDeletePending={isDeletePending}
+        isSavePending={isSavePending}
+        actionMessage={actionMessage}
+        actionError={actionError}
+        actionMeta={actionMeta}
+        loadError={loadError}
+        formatTime={formatTime}
+        onSearchChange={setSearch}
+        onSearch={() => setQuery({ search, type: '' })}
+        onSearchClear={() => {
+          setSearch('');
+          setQuery({ search: '', type: '' });
+        }}
+        onRefresh={() => {
+          if (retry) {
+            retry();
+            return;
+          }
+          setReloadVersion(version => version + 1);
+        }}
+        onNew={() => {
+          setActionMessage(null);
+          setActionError(null);
+          setActionMeta(null);
+          setIsNameValidationVisible(false);
+          setDeleteTarget(null);
+          setDraftLabel(createEmptyLabelDraft());
+          setIsManageModalAdd(true);
+        }}
+        onCopy={label => {
+          const text = buildLabelDisplayName(label);
+          setActionMessage(null);
+          setActionError(null);
+          setActionMeta(null);
+          if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
+            setActionError(t('common.notify.copy-fail'));
+            return;
+          }
+          void navigator.clipboard
+            .writeText(text)
+            .then(() => setActionMessage(t('common.notify.copy-success')))
+            .catch(() => setActionError(t('common.notify.copy-fail')));
+        }}
+        onEdit={label => {
+          setActionMessage(null);
+          setActionError(null);
+          setActionMeta(null);
+          setIsNameValidationVisible(false);
+          setDeleteTarget(null);
+          setDraftLabel(label);
+          setIsManageModalAdd(false);
+        }}
+        onDeleteRequest={label => {
+          setActionMessage(null);
+          setActionError(null);
+          setActionMeta(null);
+          setDeleteTarget(label);
+        }}
+        onDeleteCancel={() => {
+          if (!isDeletePending) setDeleteTarget(null);
+        }}
+        onDeleteConfirm={() => {
+          if (!deleteTarget) return;
+          const target = deleteTarget;
+          setActionMessage(null);
+          setActionError(null);
+          setActionMeta(null);
+          setDeleteTarget(null);
+          setIsDeletePending(true);
+          void deleteLabel(apiMessageDelete, target.id)
+            .then(() => {
+              setActionMessage(t('common.notify.delete-success'));
+              setReloadVersion(version => version + 1);
+            })
+            .catch(error => {
+              setActionError(t('common.notify.delete-fail'));
+              setActionMeta(error instanceof Error ? error.message : null);
+            })
+            .finally(() => {
+              setIsDeletePending(false);
+            });
+        }}
+        onDraftChange={patch => setDraftLabel(previous => {
+          if (!previous) return previous;
+          if (!isManageModalAdd && previous.id) {
+            const sourceLabel = data.list.content.find(label => label.id === previous.id);
+            if (sourceLabel) Object.assign(sourceLabel, patch);
+          }
+          return { ...previous, ...patch };
+        })}
+        onCloseDialog={() => {
+          setIsNameValidationVisible(false);
+          setDraftLabel(null);
+        }}
+        onSaveDialog={() => {
+          if (!draftLabel) return;
+          if (draftLabel.name == undefined || draftLabel.name.length === 0) {
+            setIsNameValidationVisible(true);
+            return;
+          }
+          const isEdit = !isManageModalAdd && Boolean(draftLabel.id);
+          setActionMessage(null);
+          setActionError(null);
+          setActionMeta(null);
+          setIsNameValidationVisible(false);
+          setIsSavePending(true);
+          void saveLabel(apiMessagePost, apiMessagePut, draftLabel, isManageModalAdd)
+            .then(() => {
+              setIsNameValidationVisible(false);
+              setDraftLabel(null);
+              setActionMessage(t(isEdit ? 'common.notify.edit-success' : 'common.notify.new-success'));
+              setReloadVersion(version => version + 1);
+            })
+            .catch(error => {
+              setActionError(t(isEdit ? 'common.notify.edit-fail' : 'common.notify.new-fail'));
+              setActionMeta(error instanceof Error ? error.message : null);
+            })
+            .finally(() => {
+              setIsSavePending(false);
+            });
+        }}
+      />
+    ),
+    [
+      actionError,
+      actionMessage,
+      actionMeta,
+      deleteTarget,
+      draftLabel,
+      isDeletePending,
+      isLoadPending,
+      isManageModalAdd,
+      isNameValidationVisible,
+      isSavePending,
+      search,
+      t
+    ]
+  );
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.labels.loading')}
+      cacheKey={labelManageCacheKey}
+      cacheSettledTtlMs={SETTING_LABELS_SETTLED_CACHE_TTL_MS}
+      renderError={(message, retry) => renderSurface(EMPTY_LABEL_DATA, message, retry)}
+    >
+      {data => renderSurface(data)}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/setting/plugins/page.test.tsx b/web-next/app/setting/plugins/page.test.tsx
index 0b0efc5612..50cedeec34 100644
--- a/web-next/app/setting/plugins/page.test.tsx
+++ b/web-next/app/setting/plugins/page.test.tsx
@@ -9,6 +9,7 @@ import { createTranslatorMock } from '../../../test/i18n-test-helper';
 
 const mockState = vi.hoisted(() => ({
   lastLoad: null as null | (() => Promise<unknown>),
+  renderErrorMessage: null as string | null,
   renderData: {
     list: {
       content: [
@@ -35,11 +36,9 @@ const loadPluginData = vi.hoisted(() => vi.fn());
 
 vi.mock('@/components/providers/i18n-provider', () => ({
   useI18n: () => ({
+    locale: 'zh-CN',
     t: createTranslatorMock({
-      locale: 'zh-CN',
-      overrides: {
-        'setting.plugins.loading': '加载插件'
-      }
+      locale: 'zh-CN'
     })
   })
 }));
@@ -47,28 +46,161 @@ vi.mock('@/components/providers/i18n-provider', () => ({
 vi.mock('@/components/workbench/client-workbench', () => ({
   ClientWorkbench: ({
     children,
-    load
+    load,
+    renderError
   }: {
     children: (data: unknown) => React.ReactNode;
     load: () => Promise<unknown>;
+    renderError?: (message: string, retry: () => void) => React.ReactNode;
   }) => {
     mockState.lastLoad = load;
+    if (mockState.renderErrorMessage && renderError) {
+      return <div data-client-workbench="true">{renderError(mockState.renderErrorMessage, vi.fn())}</div>;
+    }
     return <div data-client-workbench="true">{children(mockState.renderData)}</div>;
   }
 }));
 
 vi.mock('@/components/pages/plugin-manage-surface', () => ({
-  PluginManageSurface: ({ search, draftPlugin }: any) => (
+  PluginManageSurface: ({ data, search, draftPlugin, isUploadDialogOpen, paramDraft, actionMessage, actionError, actionMeta, actionTone, actionKind, loadError, uploadValidation, isUploadPending, isParamPending, isLoadPending, isTogglePending, optimisticEnableStatus, deleteTarget }: any) => (
     <div
       data-plugin-manage-surface="otlp-cold-plugin-console"
       data-plugin-manage-style-baseline="hertzbeat-cold-matte"
+      data-plugin-enable-feedback-contract="angular-edit-notify"
+      data-plugin-enable-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+      data-plugin-enable-failure-contract="angular-edit-fail-notification"
+      data-plugin-enable-failure-owner="hertzbeat-ui-inline-feedback"
+      data-plugin-enable-optimistic-contract="angular-toggle-mutates-row-before-put"
+      data-plugin-enable-optimistic-owner="route-state-contract"
+      data-plugin-table-loading-contract="angular-load-plugins-table"
+      data-plugin-table-loading-scope="load-search-refresh-pagination-mutation"
+      data-plugin-load-failure-contract="angular-console-only-shell"
+      data-plugin-load-failure-owner="plugin-route-controller"
+      data-plugin-load-failure={loadError ? 'angular-console-only-shell' : 'none'}
+      data-plugin-toggle-loading-contract="angular-nz-table-loading"
+      data-plugin-toggle-loading-contract-owner="angular-nz-table-loading"
+      data-plugin-selection-reset-contract="angular-load-clears-selection"
+      data-plugin-selection-reset-owner="route-state-contract"
+      data-plugin-query-param-order-contract="angular-page-index-size-search"
+      data-plugin-query-param-order-owner="plugin-query-state"
+      data-plugin-search-submit-contract="angular-enter-and-clear"
+      data-plugin-search-clear-contract="angular-cleared-load"
+      data-plugin-search-clear-owner="shared-search-row"
+      data-plugin-delete-warning-contract="angular-no-select-warning"
+      data-plugin-delete-confirm-contract="angular-modal-confirm"
+      data-plugin-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"
+      data-plugin-delete-feedback-contract="angular-delete-notify"
+      data-plugin-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+      data-plugin-delete-failure-contract="angular-delete-fail-notification"
+      data-plugin-delete-failure-owner="hertzbeat-ui-inline-feedback"
+      data-plugin-delete-query-contract="angular-repeated-ids-query"
+      data-plugin-delete-query-owner="route-mutation-contract"
+      data-plugin-delete-page-clamp-contract="angular-update-page-index"
+      data-plugin-table-columns-contract="angular-five-column-edit-actions"
+      data-plugin-table-columns-owner="angular-nz-table"
+      data-plugin-table-stable-height-contract="viewport-fill-on-empty-or-short"
+      data-plugin-table-stable-height-owner="route-layout-contract"
+      data-plugin-toolbar-delete-menu-contract="angular-toolbar-ellipsis-delete"
+      data-plugin-toolbar-delete-menu-owner="hertzbeat-ui-table-row-action-button"
+      data-plugin-row-delete-menu-contract="angular-ellipsis-dropdown-delete"
+      data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"
+      data-plugin-upload-validation-contract="angular-required-before-submit"
+      data-plugin-upload-invalid-submit-contract="angular-mark-dirty-keep-open"
+      data-plugin-upload-invalid-submit-owner="route-validation-contract"
+      data-plugin-upload-loading-contract="angular-nz-ok-loading"
+      data-plugin-upload-payload-contract="angular-form-data"
+      data-plugin-upload-name-payload-contract="angular-raw-name-no-trim"
+      data-plugin-upload-name-payload-owner="plugin-manage-controller"
+      data-plugin-upload-save-lifecycle-contract="angular-close-success-keep-open-fail"
+      data-plugin-upload-save-lifecycle-owner="route-state-contract"
+      data-plugin-upload-success-reset-contract="angular-reset-form-after-success"
+      data-plugin-upload-success-reset-owner="route-state-contract"
+      data-plugin-upload-cancel-contract="angular-cancel-preserves-form"
+      data-plugin-upload-cancel-contract-owner="route-state-contract"
+      data-plugin-upload-cancel-pending-contract="angular-cancel-allowed-during-ok-loading"
+      data-plugin-upload-cancel-pending-owner="angular-nz-modal"
+      data-plugin-upload-mask-closable-contract="angular-mask-closable-false"
+      data-plugin-upload-width-contract="angular-width-30-percent"
+      data-plugin-upload-modal-owner="angular-nz-modal"
+      data-plugin-upload-field-layout-contract="angular-label-8-control-14"
+      data-plugin-upload-field-layout-owner="route-form-field-grid"
+      data-plugin-upload-status-control-contract="angular-nz-switch"
+      data-plugin-upload-status-control-owner="hertzbeat-ui-switch"
+      data-plugin-upload-file-list-contract="angular-before-upload-single-replace"
+      data-plugin-upload-file-list-owner="hertzbeat-ui-file-input"
+      data-plugin-upload-file-remove-contract="angular-nz-remove-clears-jar"
+      data-plugin-upload-file-remove-owner="hertzbeat-ui-file-input"
+      data-plugin-upload-feedback-contract="angular-new-notify"
+      data-plugin-upload-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+      data-plugin-upload-failure-contract="angular-new-fail-notification"
+      data-plugin-upload-failure-owner="hertzbeat-ui-inline-feedback"
+      data-plugin-param-edit-contract="angular-params-define-modal"
+      data-plugin-param-action-visibility-contract="angular-paramcount-zero-only"
+      data-plugin-param-action-visibility-owner="plugin-view-model"
+      data-plugin-param-mask-closable-contract="angular-mask-closable-false"
+      data-plugin-param-modal-owner="angular-nz-modal"
+      data-plugin-param-save-contract="angular-params-post"
+      data-plugin-param-payload-contract="angular-object-values-payload"
+      data-plugin-param-save-lifecycle-contract="angular-close-success-keep-open-fail"
+      data-plugin-param-save-lifecycle-owner="route-state-contract"
+      data-plugin-param-save-loading-contract="angular-no-ok-loading"
+      data-plugin-param-save-loading-owner="angular-modal-ok-contract"
+      data-plugin-param-feedback-contract="angular-edit-notify"
+      data-plugin-param-failure-contract="angular-edit-fail-notification"
+      data-plugin-param-failure-owner="hertzbeat-ui-inline-feedback"
+      data-plugin-param-empty-contract="angular-empty-params-modal"
+      data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"
+      data-plugin-param-pending-editable-contract="angular-controls-remain-enabled"
+      data-plugin-param-pending-editable-owner="angular-param-modal"
+      data-plugin-param-width-contract="angular-default-modal-width"
+      data-plugin-param-width-owner="angular-nz-modal"
+      data-plugin-param-radio-contract="angular-nz-radio-group"
+      data-plugin-param-radio-owner="hertzbeat-ui-radio-button-group"
+      data-plugin-param-textarea-contract="angular-textarea-rows-8"
+      data-plugin-param-textarea-owner="hertzbeat-ui-textarea"
+      data-plugin-param-advanced-fields-contract="angular-configurable-and-multi-func"
+      data-plugin-param-advanced-fields-owner="hertzbeat-ui-advanced-param-controls"
+      data-plugin-param-configurable-field-contract="angular-app-configurable-field"
+      data-plugin-param-configurable-field-owner="hertzbeat-ui-configurable-field"
+      data-plugin-param-key-value-contract="angular-app-configurable-field"
+      data-plugin-param-key-value-owner="hertzbeat-ui-key-value-editor"
+      data-plugin-param-labels-contract="angular-app-configurable-field"
+      data-plugin-param-labels-owner="hertzbeat-ui-key-value-editor"
+      data-plugin-param-label-selector-contract="angular-app-label-selector"
+      data-plugin-param-label-selector-owner="cold-label-selector"
+      data-plugin-param-metrics-field-contract="angular-app-configurable-field"
+      data-plugin-param-metrics-field-owner="hertzbeat-ui-configurable-field-editor"
+      data-plugin-param-array-contract="angular-app-multi-func-input"
+      data-plugin-param-array-owner="hertzbeat-ui-input"
+      data-plugin-param-field-layout-contract="angular-label-7-control-8"
+      data-plugin-param-field-layout-owner="hertzbeat-ui-monitor-editor-field-grid"
+      data-plugin-param-form-owner="hertzbeat-ui-monitor-editor-field-grid"
+      data-plugin-pagination="cold-dense-pagination"
+      data-plugin-pagination-owner="hertzbeat-ui-pagination-bar"
+      data-plugin-pagination-contract="angular-search-pagination"
     >
       <section data-plugin-admin-layout="full-width-admin-list">
         <span>插件表格</span>
       </section>
+      <span data-plugin-test-row-count="true">{data.list.content.length}</span>
       <span>插件管理</span>
       <span>{search}</span>
       <span>{draftPlugin ? draftPlugin.name : 'no-draft'}</span>
+      <span>{isUploadDialogOpen ? 'upload-dialog-open' : 'upload-dialog-closed'}</span>
+      <span>{paramDraft ? paramDraft.plugin.name : 'no-param-draft'}</span>
+      <span>{actionMessage ?? 'no-action-message'}</span>
+      <span>{actionError ?? 'no-action-error'}</span>
+      <span>{actionMeta ?? 'no-action-meta'}</span>
+      <span>{actionTone ?? 'no-action-tone'}</span>
+      <span>{actionKind ?? 'no-action-kind'}</span>
+      <span data-plugin-test-load-error={loadError ? 'present' : 'none'} />
+      <span>{uploadValidation?.name ? 'name-invalid' : 'name-valid'}</span>
+      <span>{isUploadPending ? 'upload-pending' : 'upload-idle'}</span>
+      <span>{isParamPending ? 'param-pending' : 'param-idle'}</span>
+      <span>{isLoadPending ? 'load-pending' : 'load-idle'}</span>
+      <span>{isTogglePending ? 'toggle-pending' : 'toggle-idle'}</span>
+      <span>{Object.keys(optimisticEnableStatus ?? {}).length === 0 ? 'no-optimistic-enable-status' : 'optimistic-enable-status'}</span>
+      <span>{deleteTarget ? deleteTarget.label : 'no-delete-target'}</span>
     </div>
   )
 }));
@@ -82,15 +214,30 @@ vi.mock('@/lib/api-client', () => ({
 
 vi.mock('@/lib/plugin-manage/controller', () => ({
   loadPluginData,
-  createEmptyPluginDraft: () => ({ name: '', jarFileName: '', enableStatus: true }),
+  createEmptyPluginDraft: () => ({ name: '', jarFileName: '', jarFile: null, enableStatus: true }),
   savePlugin: vi.fn(),
+  loadPluginParamDraft: vi.fn(),
+  updatePluginParamDraft: vi.fn((draft, field, value) => ({
+    ...draft,
+    params: {
+      ...draft.params,
+      [field]: {
+        ...draft.params[field],
+        paramValue: value
+      }
+    }
+  })),
+  savePluginParams: vi.fn(),
   togglePluginStatus: vi.fn(),
-  deletePlugin: vi.fn()
+  validatePluginUploadDraft: vi.fn(() => ({ name: true, jarFile: true })),
+  deletePlugins: vi.fn(),
+  clampPluginPageIndexAfterDelete: vi.fn(() => 0)
 }));
 
 describe('setting plugins page', () => {
   beforeEach(() => {
     mockState.lastLoad = null;
+    mockState.renderErrorMessage = null;
     apiMessageGet.mockReset();
     apiMessagePost.mockReset();
     apiMessagePut.mockReset();
@@ -108,25 +255,264 @@ describe('setting plugins page', () => {
     expect(html).toContain('data-client-workbench="true"');
     expect(html).toContain('data-plugin-manage-surface="otlp-cold-plugin-console"');
     expect(html).toContain('data-plugin-manage-style-baseline="hertzbeat-cold-matte"');
+    expect(html).toContain('data-plugin-enable-feedback-contract="angular-edit-notify"');
+    expect(html).toContain('data-plugin-enable-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-enable-failure-contract="angular-edit-fail-notification"');
+    expect(html).toContain('data-plugin-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-enable-optimistic-contract="angular-toggle-mutates-row-before-put"');
+    expect(html).toContain('data-plugin-enable-optimistic-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-table-loading-contract="angular-load-plugins-table"');
+    expect(html).toContain('data-plugin-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-plugin-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-plugin-load-failure-owner="plugin-route-controller"');
+    expect(html).toContain('data-plugin-load-failure="none"');
+    expect(html).toContain('data-plugin-toggle-loading-contract="angular-nz-table-loading"');
+    expect(html).toContain('data-plugin-toggle-loading-contract-owner="angular-nz-table-loading"');
+    expect(html).toContain('data-plugin-selection-reset-contract="angular-load-clears-selection"');
+    expect(html).toContain('data-plugin-selection-reset-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-search-submit-contract="angular-enter-and-clear"');
+    expect(html).toContain('data-plugin-search-clear-contract="angular-cleared-load"');
+    expect(html).toContain('data-plugin-search-clear-owner="shared-search-row"');
+    expect(html).toContain('data-plugin-query-param-order-contract="angular-page-index-size-search"');
+    expect(html).toContain('data-plugin-query-param-order-owner="plugin-query-state"');
+    expect(html).toContain('data-plugin-delete-warning-contract="angular-no-select-warning"');
+    expect(html).toContain('data-plugin-delete-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-plugin-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-plugin-delete-feedback-contract="angular-delete-notify"');
+    expect(html).toContain('data-plugin-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-delete-failure-contract="angular-delete-fail-notification"');
+    expect(html).toContain('data-plugin-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-delete-query-contract="angular-repeated-ids-query"');
+    expect(html).toContain('data-plugin-delete-query-owner="route-mutation-contract"');
+    expect(html).toContain('data-plugin-delete-page-clamp-contract="angular-update-page-index"');
+    expect(html).toContain('data-plugin-table-columns-contract="angular-five-column-edit-actions"');
+    expect(html).toContain('data-plugin-table-columns-owner="angular-nz-table"');
+    expect(html).toContain('data-plugin-table-stable-height-contract="viewport-fill-on-empty-or-short"');
+    expect(html).toContain('data-plugin-table-stable-height-owner="route-layout-contract"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-contract="angular-toolbar-ellipsis-delete"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-upload-validation-contract="angular-required-before-submit"');
+    expect(html).toContain('data-plugin-upload-invalid-submit-contract="angular-mark-dirty-keep-open"');
+    expect(html).toContain('data-plugin-upload-invalid-submit-owner="route-validation-contract"');
+    expect(html).toContain('data-plugin-upload-loading-contract="angular-nz-ok-loading"');
+    expect(html).toContain('data-plugin-upload-payload-contract="angular-form-data"');
+    expect(html).toContain('data-plugin-upload-name-payload-contract="angular-raw-name-no-trim"');
+    expect(html).toContain('data-plugin-upload-name-payload-owner="plugin-manage-controller"');
+    expect(html).toContain('data-plugin-upload-save-lifecycle-contract="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-upload-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-upload-success-reset-contract="angular-reset-form-after-success"');
+    expect(html).toContain('data-plugin-upload-success-reset-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-upload-cancel-contract="angular-cancel-preserves-form"');
+    expect(html).toContain('data-plugin-upload-cancel-contract-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-upload-cancel-pending-contract="angular-cancel-allowed-during-ok-loading"');
+    expect(html).toContain('data-plugin-upload-cancel-pending-owner="angular-nz-modal"');
+    expect(html).toContain('data-plugin-upload-mask-closable-contract="angular-mask-closable-false"');
+    expect(html).toContain('data-plugin-upload-width-contract="angular-width-30-percent"');
+    expect(html).toContain('data-plugin-upload-modal-owner="angular-nz-modal"');
+    expect(html).toContain('data-plugin-upload-field-layout-contract="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-plugin-upload-status-control-contract="angular-nz-switch"');
+    expect(html).toContain('data-plugin-upload-status-control-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-plugin-upload-file-list-contract="angular-before-upload-single-replace"');
+    expect(html).toContain('data-plugin-upload-file-list-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-plugin-upload-file-remove-contract="angular-nz-remove-clears-jar"');
+    expect(html).toContain('data-plugin-upload-file-remove-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-plugin-upload-feedback-contract="angular-new-notify"');
+    expect(html).toContain('data-plugin-upload-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-upload-failure-contract="angular-new-fail-notification"');
+    expect(html).toContain('data-plugin-upload-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-param-edit-contract="angular-params-define-modal"');
+    expect(html).toContain('data-plugin-param-action-visibility-contract="angular-paramcount-zero-only"');
+    expect(html).toContain('data-plugin-param-action-visibility-owner="plugin-view-model"');
+    expect(html).toContain('data-plugin-param-mask-closable-contract="angular-mask-closable-false"');
+    expect(html).toContain('data-plugin-param-modal-owner="angular-nz-modal"');
+    expect(html).toContain('data-plugin-param-save-contract="angular-params-post"');
+    expect(html).toContain('data-plugin-param-payload-contract="angular-object-values-payload"');
+    expect(html).toContain('data-plugin-param-save-lifecycle-contract="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-param-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-param-save-loading-contract="angular-no-ok-loading"');
+    expect(html).toContain('data-plugin-param-save-loading-owner="angular-modal-ok-contract"');
+    expect(html).toContain('data-plugin-param-feedback-contract="angular-edit-notify"');
+    expect(html).toContain('data-plugin-param-failure-contract="angular-edit-fail-notification"');
+    expect(html).toContain('data-plugin-param-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-param-empty-contract="angular-empty-params-modal"');
+    expect(html).toContain('data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-plugin-param-pending-editable-contract="angular-controls-remain-enabled"');
+    expect(html).toContain('data-plugin-param-pending-editable-owner="angular-param-modal"');
+    expect(html).toContain('data-plugin-param-width-contract="angular-default-modal-width"');
+    expect(html).toContain('data-plugin-param-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-plugin-param-radio-contract="angular-nz-radio-group"');
+    expect(html).toContain('data-plugin-param-radio-owner="hertzbeat-ui-radio-button-group"');
+    expect(html).toContain('data-plugin-param-textarea-contract="angular-textarea-rows-8"');
+    expect(html).toContain('data-plugin-param-textarea-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-plugin-param-advanced-fields-contract="angular-configurable-and-multi-func"');
+    expect(html).toContain('data-plugin-param-advanced-fields-owner="hertzbeat-ui-advanced-param-controls"');
+    expect(html).toContain('data-plugin-param-configurable-field-contract="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-configurable-field-owner="hertzbeat-ui-configurable-field"');
+    expect(html).toContain('data-plugin-param-key-value-contract="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-key-value-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-plugin-param-labels-contract="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-labels-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-plugin-param-label-selector-contract="angular-app-label-selector"');
+    expect(html).toContain('data-plugin-param-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-plugin-param-metrics-field-contract="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-metrics-field-owner="hertzbeat-ui-configurable-field-editor"');
+    expect(html).toContain('data-plugin-param-array-contract="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-array-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-param-field-layout-contract="angular-label-7-control-8"');
+    expect(html).toContain('data-plugin-param-field-layout-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-plugin-param-form-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-plugin-pagination="cold-dense-pagination"');
+    expect(html).toContain('data-plugin-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-plugin-pagination-contract="angular-search-pagination"');
     expect(html).toContain('data-plugin-admin-layout="full-width-admin-list"');
     expect(html).not.toContain('data-plugin-summary-rail=');
     expect(html).not.toContain('插件摘要');
     expect(html).not.toContain('当前插件');
     expect(html).toContain('插件管理');
     expect(html).toContain('no-draft');
+    expect(html).toContain('upload-dialog-closed');
+    expect(html).toContain('no-param-draft');
+    expect(html).toContain('no-action-message');
+    expect(html).toContain('no-action-error');
+    expect(html).toContain('no-action-meta');
+    expect(html).toContain('success');
+    expect(html).toContain('enable');
+    expect(html).toContain('name-valid');
+    expect(html).toContain('upload-idle');
+    expect(html).toContain('param-idle');
+    expect(html).toContain('load-idle');
+    expect(html).toContain('toggle-idle');
+    expect(html).toContain('no-optimistic-enable-status');
+    expect(html).toContain('no-delete-target');
 
     await mockState.lastLoad?.();
 
     expect(apiMessageGet).toHaveBeenCalledWith('/plugin?pageIndex=0&pageSize=8');
   });
 
+  it('keeps the Angular plugin table shell visible when plugin loading fails', async () => {
+    mockState.renderErrorMessage = 'backend refused plugin load';
+    const { default: SettingPluginsPage } = await import('./page');
+    const html = renderToStaticMarkup(<SettingPluginsPage />);
+
+    expect(html).toContain('data-client-workbench="true"');
+    expect(html).toContain('data-plugin-manage-surface="otlp-cold-plugin-console"');
+    expect(html).toContain('data-plugin-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-plugin-load-failure-owner="plugin-route-controller"');
+    expect(html).toContain('data-plugin-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-plugin-test-row-count="true">0');
+    expect(html).toContain('插件表格');
+    expect(html).not.toContain('backend refused plugin load');
+    expect(html).not.toContain('common.load-failed');
+    expect(html).not.toContain('data-observability-status');
+  });
+
   it('keeps route composition out of the old observability/workbench visual owner', () => {
-    const source = readFileSync(resolve(process.cwd(), 'app/setting/plugins/page.tsx'), 'utf8');
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/plugins/setting-plugins-page.tsx'), 'utf8');
 
     expect(source).toContain('PluginManageSurface');
+    expect(source).toContain('const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false)');
+    expect(source).toContain('const [actionMessage, setActionMessage] = useState<string | null>(null)');
+    expect(source).toContain('const [actionError, setActionError] = useState<string | null>(null)');
+    expect(source).toContain("const [actionTone, setActionTone] = useState<'success' | 'warning' | 'critical'>('success')");
+    expect(source).toContain("const [paramDraft, setParamDraft] = useState<PluginParamDraft | null>(null)");
+    expect(source).toContain("const [actionKind, setActionKind] = useState<'enable' | 'delete' | 'upload' | 'params'>('enable')");
+    expect(source).toContain('const [optimisticEnableStatus, setOptimisticEnableStatus] = useState<Record<number, boolean>>({})');
+    expect(source).toContain('const [uploadValidation, setUploadValidation] = useState<{ name?: boolean; jarFile?: boolean }>({})');
+    expect(source).toContain('const [isUploadPending, setIsUploadPending] = useState(false)');
+    expect(source).toContain('const [isParamPending, setIsParamPending] = useState(false)');
+    expect(source).toContain('const [deleteTarget, setDeleteTarget] = useState<PluginDeleteTarget | null>(null)');
+    expect(source).toContain('const [isLoadPending, setIsLoadPending] = useState(false)');
+    expect(source).toContain('buildFallbackPluginData');
+    expect(source).toContain('renderError={(message, retry) => renderPluginSurface(buildFallbackPluginData(), message, retry)}');
+    expect(source).toContain('loadError={loadError}');
+    expect(source).toContain('const [isDeletePending, setIsDeletePending] = useState(false)');
+    expect(source).toContain('const [isTogglePending, setIsTogglePending] = useState(false)');
+    expect(source).toContain("setActionMessage(t('common.notify.edit-success'))");
+    expect(source).toContain("setActionError(t('common.notify.edit-fail'))");
+    expect(source).toContain('setActionMeta(error instanceof Error ? error.message : null)');
+    expect(source).toContain("setActionError(t('common.notify.no-select-delete'))");
+    expect(source).toContain("setActionMessage(t('common.notify.delete-success'))");
+    expect(source).toContain("setActionError(t('common.notify.delete-fail'))");
+    expect(source).toContain('validatePluginUploadDraft(draftPlugin)');
+    expect(source).toContain('if (!validation.name || !validation.jarFile) {');
+    expect(source).toContain('setUploadValidation({');
+    expect(source).toContain('isUploadDialogOpen={isUploadDialogOpen}');
+    expect(source).toContain('setIsUploadDialogOpen(true)');
+    expect(source).toContain('setIsUploadDialogOpen(false)');
+    expect(source).toContain('onCloseDialog={() => {');
+    expect(source).toContain('setIsUploadDialogOpen(false);');
+    expect(source).not.toContain('if (!isUploadPending) {');
+    expect(source).toContain('if (!draftPlugin) {');
+    expect(source).toContain("setActionKind('upload')");
+    expect(source).toContain("setActionMessage(t('common.notify.new-success'))");
+    expect(source).toContain("setActionError(t('common.notify.new-fail'))");
+    expect(source).toContain('setUploadValidation({});');
+    expect(source).toContain('setDraftPlugin(null);');
+    expect(source).toContain('void deletePlugins(apiMessageDelete, targetIds)');
+    expect(source).toContain('void savePlugin(apiMessagePost, draftPlugin)');
+    expect(source).toContain('void loadPluginParamDraft(apiMessageGet, plugin, locale)');
+    expect(source).toContain("setActionKind('params')");
+    expect(source).toContain('updatePluginParamDraft(previous, field, value)');
+    expect(source).toContain('void savePluginParams(apiMessagePost, paramDraft.params)');
+    expect(source).toContain("setActionMessage(t('common.notify.edit-success'))");
+    expect(source).toContain('setParamDraft(null);');
+    expect(source).toContain("setActionError(t('common.notify.edit-fail'))");
+    expect(source).toContain('onCloseParamDialog={() => {');
+    expect(source).toContain('setParamDraft(null);');
+    expect(source).not.toContain('if (!isParamPending) setParamDraft(null);');
+    expect(source).toContain('isParamPending={isParamPending}');
+    expect(source).toContain('clampPluginPageIndexAfterDelete({');
+    expect(source).toContain('onPageIndexChange={pageIndex => {');
+    expect(source).toContain('onPageSizeChange={pageSize => {');
+    expect(source.match(/setSelectedIds\(\[\]\)/g)?.length).toBeGreaterThanOrEqual(6);
+    expect(source).toContain('onSearch={() => {');
+    expect(source).toContain('onSearchClear={() => {');
+    expect(source).toContain("setQuery({ search: '', pageIndex: 0, pageSize: query.pageSize });");
+    expect(source).toContain('onRefresh={() => {');
+    expect(source).toContain('setQuery(current => ({ ...current, pageIndex }))');
+    expect(source).toContain('setQuery(current => ({ ...current, pageIndex: 0, pageSize }))');
+    expect(source).toContain('actionMessage={actionMessage}');
+    expect(source).toContain('actionError={actionError}');
+    expect(source).toContain('actionMeta={actionMeta}');
+    expect(source).toContain('actionTone={actionTone}');
+    expect(source).toContain('actionKind={actionKind}');
+    expect(source).toContain('uploadValidation={uploadValidation}');
+    expect(source).toContain('isUploadPending={isUploadPending}');
+    expect(source).toContain('paramDraft={paramDraft}');
+    expect(source).toContain('isParamPending={isParamPending}');
+    expect(source).toContain('deleteTarget={deleteTarget}');
+    expect(source).toContain('isLoadPending={isLoadPending}');
+    expect(source).toContain('isDeletePending={isDeletePending}');
+    expect(source).toContain('isTogglePending={isTogglePending}');
+    expect(source).toContain('optimisticEnableStatus={optimisticEnableStatus}');
+    expect(source).toContain('setIsLoadPending(true)');
+    expect(source).toContain('setOptimisticEnableStatus({});');
+    expect(source).toContain('setIsLoadPending(false)');
+    expect(source).toContain('setIsTogglePending(true)');
+    expect(source).toContain('setOptimisticEnableStatus(current => ({');
+    expect(source).toContain('[plugin.id]: !Boolean(plugin.enableStatus)');
+    expect(source).toContain('setIsTogglePending(false)');
     expect(source).not.toContain('WorkbenchPage');
     expect(source).not.toContain('SurfaceSection');
     expect(source).not.toContain('StatusState');
     expect(source).not.toContain('buildPluginFacts');
   });
+
+  it('keeps plugin management remounts on a short settled cache window while reloads invalidate it', () => {
+    const source = readFileSync(resolve(process.cwd(), 'app/setting/plugins/setting-plugins-page.tsx'), 'utf8');
+
+    expect(source).toContain('SETTING_PLUGINS_SETTLED_CACHE_TTL_MS = 10_000');
+    expect(source).toContain("['setting-plugins', pluginListUrl, reloadVersion].join(':')");
+    expect(source).toContain('[pluginListUrl, reloadVersion]');
+    expect(source).toContain('onRefresh={() => {');
+    expect(source).toContain('setActionMessage(null);');
+    expect(source).toContain('setActionError(null);');
+    expect(source).toContain('setReloadVersion(version => version + 1)');
+    expect(source).toContain('cacheKey={pluginManageCacheKey}');
+    expect(source).toContain('cacheSettledTtlMs={SETTING_PLUGINS_SETTLED_CACHE_TTL_MS}');
+  });
 });
diff --git a/web-next/app/setting/plugins/page.tsx b/web-next/app/setting/plugins/page.tsx
index 4ea0b59a07..4cce6d5647 100644
--- a/web-next/app/setting/plugins/page.tsx
+++ b/web-next/app/setting/plugins/page.tsx
@@ -1,69 +1,7 @@
-'use client';
+import React from 'react';
 
-import { useCallback, useState } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { PluginManageSurface } from '@/components/pages/plugin-manage-surface';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
-import { createEmptyPluginDraft, deletePlugin, loadPluginData, savePlugin, togglePluginStatus, type PluginUploadDraft } from '@/lib/plugin-manage/controller';
-import type { PluginQueryState } from '@/lib/plugin-manage/query-state';
-import type { Plugin } from '@/lib/types';
+import SettingPluginsPage from './setting-plugins-page';
 
-export default function SettingPluginsPage() {
-  const { t } = useI18n();
-  const [search, setSearch] = useState('');
-  const [query, setQuery] = useState<PluginQueryState>({ search: '' });
-  const [reloadVersion, setReloadVersion] = useState(0);
-  const [selectedIds, setSelectedIds] = useState<number[]>([]);
-  const [draftPlugin, setDraftPlugin] = useState<PluginUploadDraft | null>(null);
-
-  const load = useCallback(async () => {
-    return loadPluginData(apiMessageGet, query);
-  }, [query, reloadVersion]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('setting.plugins.loading')}>
-      {data => (
-        <PluginManageSurface
-          t={t}
-          data={data}
-          search={search}
-          selectedIds={selectedIds}
-          draftPlugin={draftPlugin}
-          onSearchChange={setSearch}
-          onSearch={() => setQuery({ search })}
-          onRefresh={() => setReloadVersion(version => version + 1)}
-          onNew={() => setDraftPlugin(createEmptyPluginDraft())}
-          onDeleteSelected={() => {
-            selectedIds.forEach(id => {
-              void deletePlugin(apiMessageDelete, id);
-            });
-            setSelectedIds([]);
-            setReloadVersion(version => version + 1);
-          }}
-          onSelectedIdsChange={setSelectedIds}
-          onEditParams={(_plugin: Plugin) => {}}
-          onToggleEnabled={plugin => {
-            void togglePluginStatus(apiMessagePut, plugin).then(() => {
-              setReloadVersion(version => version + 1);
-            });
-          }}
-          onDeleteOne={plugin => {
-            void deletePlugin(apiMessageDelete, plugin.id).then(() => {
-              setReloadVersion(version => version + 1);
-            });
-          }}
-          onDraftChange={patch => setDraftPlugin(previous => (previous ? { ...previous, ...patch } : previous))}
-          onCloseDialog={() => setDraftPlugin(null)}
-          onSaveDialog={() => {
-            if (!draftPlugin) return;
-            void savePlugin(apiMessagePost, draftPlugin).then(() => {
-              setDraftPlugin(null);
-              setReloadVersion(version => version + 1);
-            });
-          }}
-        />
-      )}
-    </ClientWorkbench>
-  );
+export default function SettingPluginsRoutePage() {
+  return <SettingPluginsPage />;
 }
diff --git a/web-next/app/setting/plugins/setting-plugins-page.tsx b/web-next/app/setting/plugins/setting-plugins-page.tsx
new file mode 100644
index 0000000000..eb16afa8b2
--- /dev/null
+++ b/web-next/app/setting/plugins/setting-plugins-page.tsx
@@ -0,0 +1,325 @@
+'use client';
+
+import { useCallback, useMemo, useState } from 'react';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { PluginManageSurface, type PluginDeleteTarget } from '@/components/pages/plugin-manage-surface';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { apiMessageDelete, apiMessageGet, apiMessagePost, apiMessagePut } from '@/lib/api-client';
+import { clampPluginPageIndexAfterDelete, createEmptyPluginDraft, deletePlugins, loadPluginData, loadPluginParamDraft, savePlugin, savePluginParams, togglePluginStatus, updatePluginParamDraft, validatePluginUploadDraft, type PluginManagePageData, type PluginParamDraft, type PluginUploadDraft } from '@/lib/plugin-manage/controller';
+import { buildPluginUrl, type PluginQueryState } from '@/lib/plugin-manage/query-state';
+import type { Plugin } from '@/lib/types';
+
+const SETTING_PLUGINS_SETTLED_CACHE_TTL_MS = 10_000;
+
+export default function SettingPluginsPage() {
+  const { t, locale } = useI18n();
+  const [search, setSearch] = useState('');
+  const [query, setQuery] = useState<PluginQueryState>({ search: '' });
+  const [reloadVersion, setReloadVersion] = useState(0);
+  const [selectedIds, setSelectedIds] = useState<number[]>([]);
+  const [draftPlugin, setDraftPlugin] = useState<PluginUploadDraft | null>(null);
+  const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
+  const [paramDraft, setParamDraft] = useState<PluginParamDraft | null>(null);
+  const [actionMessage, setActionMessage] = useState<string | null>(null);
+  const [actionError, setActionError] = useState<string | null>(null);
+  const [actionMeta, setActionMeta] = useState<string | null>(null);
+  const [actionTone, setActionTone] = useState<'success' | 'warning' | 'critical'>('success');
+  const [actionKind, setActionKind] = useState<'enable' | 'delete' | 'upload' | 'params'>('enable');
+  const [uploadValidation, setUploadValidation] = useState<{ name?: boolean; jarFile?: boolean }>({});
+  const [isUploadPending, setIsUploadPending] = useState(false);
+  const [isParamPending, setIsParamPending] = useState(false);
+  const [deleteTarget, setDeleteTarget] = useState<PluginDeleteTarget | null>(null);
+  const [isLoadPending, setIsLoadPending] = useState(false);
+  const [isDeletePending, setIsDeletePending] = useState(false);
+  const [isTogglePending, setIsTogglePending] = useState(false);
+  const [optimisticEnableStatus, setOptimisticEnableStatus] = useState<Record<number, boolean>>({});
+  const pluginListUrl = useMemo(() => buildPluginUrl(query), [query]);
+  const pluginManageCacheKey = useMemo(
+    () => ['setting-plugins', pluginListUrl, reloadVersion].join(':'),
+    [pluginListUrl, reloadVersion]
+  );
+
+  const load = useCallback(async () => {
+    setIsLoadPending(true);
+    try {
+      return await loadPluginData(apiMessageGet, query);
+    } finally {
+      setOptimisticEnableStatus({});
+      setIsLoadPending(false);
+    }
+  }, [query]);
+
+  const clearActionFeedback = useCallback(() => {
+    setActionMessage(null);
+    setActionError(null);
+    setActionMeta(null);
+    setActionTone('success');
+  }, []);
+
+  const buildFallbackPluginData = useCallback((): PluginManagePageData => ({
+    list: {
+      content: [],
+      totalElements: 0,
+      pageIndex: query.pageIndex ?? 0,
+      pageSize: query.pageSize ?? 8
+    }
+  }), [query.pageIndex, query.pageSize]);
+
+  function renderPluginSurface(data: PluginManagePageData, loadError: string | null = null, retryLoad?: () => void) {
+    return (
+      <PluginManageSurface
+        t={t}
+        data={data}
+        search={search}
+        selectedIds={selectedIds}
+        draftPlugin={draftPlugin}
+        isUploadDialogOpen={isUploadDialogOpen}
+        paramDraft={paramDraft}
+        actionMessage={actionMessage}
+        actionError={actionError}
+        actionMeta={actionMeta}
+        actionTone={actionTone}
+        actionKind={actionKind}
+        loadError={loadError}
+        uploadValidation={uploadValidation}
+        isUploadPending={isUploadPending}
+        isParamPending={isParamPending}
+        deleteTarget={deleteTarget}
+        isLoadPending={isLoadPending}
+        isDeletePending={isDeletePending}
+        isTogglePending={isTogglePending}
+        optimisticEnableStatus={optimisticEnableStatus}
+        onSearchChange={setSearch}
+        onSearch={() => {
+          clearActionFeedback();
+          setSelectedIds([]);
+          setQuery({ search, pageIndex: 0, pageSize: query.pageSize });
+        }}
+        onSearchClear={() => {
+          clearActionFeedback();
+          setSelectedIds([]);
+          setSearch('');
+          setQuery({ search: '', pageIndex: 0, pageSize: query.pageSize });
+        }}
+        onRefresh={() => {
+          clearActionFeedback();
+          setSelectedIds([]);
+          if (retryLoad) {
+            retryLoad();
+            return;
+          }
+          setReloadVersion(version => version + 1);
+        }}
+        onNew={() => {
+          clearActionFeedback();
+          setDeleteTarget(null);
+          setParamDraft(null);
+          if (!draftPlugin) {
+            setUploadValidation({});
+            setDraftPlugin(createEmptyPluginDraft());
+          }
+          setIsUploadDialogOpen(true);
+        }}
+        onDeleteSelected={() => {
+          clearActionFeedback();
+          if (selectedIds.length === 0) {
+            setActionKind('delete');
+            setActionTone('warning');
+            setActionError(t('common.notify.no-select-delete'));
+            return;
+          }
+          setActionKind('delete');
+          setDeleteTarget({
+            ids: selectedIds,
+            label: t('setting.plugins.selected-count', { count: selectedIds.length }),
+            mode: 'batch'
+          });
+        }}
+        onDeleteCancel={() => {
+          if (!isDeletePending) setDeleteTarget(null);
+        }}
+        onDeleteConfirm={() => {
+          if (!deleteTarget) return;
+          clearActionFeedback();
+          setActionKind('delete');
+          setIsDeletePending(true);
+          const targetIds = deleteTarget.ids;
+          void deletePlugins(apiMessageDelete, targetIds)
+            .then(() => {
+              const pageSize = data.list.pageSize ?? query.pageSize ?? 8;
+              const currentPageIndex = data.list.pageIndex ?? query.pageIndex ?? 0;
+              const nextPageIndex = clampPluginPageIndexAfterDelete({
+                pageIndex: currentPageIndex + 1,
+                pageSize,
+                totalElements: data.list.totalElements ?? 0,
+                deleteCount: targetIds.length
+              });
+              setActionTone('success');
+              setActionMessage(t('common.notify.delete-success'));
+              setSelectedIds([]);
+              setDeleteTarget(null);
+              setQuery(previous => ({
+                ...previous,
+                pageIndex: nextPageIndex,
+                pageSize
+              }));
+              setReloadVersion(version => version + 1);
+            })
+            .catch(error => {
+              setActionTone('critical');
+              setActionError(t('common.notify.delete-fail'));
+              setActionMeta(error instanceof Error ? error.message : null);
+            })
+            .finally(() => {
+              setIsDeletePending(false);
+            });
+        }}
+        onSelectedIdsChange={setSelectedIds}
+        onPageIndexChange={pageIndex => {
+          clearActionFeedback();
+          setSelectedIds([]);
+          setQuery(current => ({ ...current, pageIndex }));
+        }}
+        onPageSizeChange={pageSize => {
+          clearActionFeedback();
+          setSelectedIds([]);
+          setQuery(current => ({ ...current, pageIndex: 0, pageSize }));
+        }}
+        onEditParams={(plugin: Plugin) => {
+          clearActionFeedback();
+          setDraftPlugin(null);
+          setDeleteTarget(null);
+          setActionKind('params');
+          setIsParamPending(true);
+          void loadPluginParamDraft(apiMessageGet, plugin, locale)
+            .then(draft => {
+              setParamDraft(draft);
+            })
+            .catch(error => {
+              setActionTone('critical');
+              setActionError(t('common.notify.edit-fail'));
+              setActionMeta(error instanceof Error ? error.message : null);
+            })
+            .finally(() => {
+              setIsParamPending(false);
+            });
+        }}
+        onParamChange={(field, value) => {
+          setParamDraft(previous => (previous ? updatePluginParamDraft(previous, field, value) : previous));
+        }}
+        onCloseParamDialog={() => {
+          setParamDraft(null);
+        }}
+        onSaveParamDialog={() => {
+          if (!paramDraft) return;
+          clearActionFeedback();
+          setActionKind('params');
+          setIsParamPending(true);
+          void savePluginParams(apiMessagePost, paramDraft.params)
+            .then(() => {
+              setActionTone('success');
+              setActionMessage(t('common.notify.edit-success'));
+              setParamDraft(null);
+            })
+            .catch(error => {
+              setActionTone('critical');
+              setActionError(t('common.notify.edit-fail'));
+              setActionMeta(error instanceof Error ? error.message : null);
+            })
+            .finally(() => {
+              setIsParamPending(false);
+            });
+        }}
+        onToggleEnabled={plugin => {
+          clearActionFeedback();
+          setActionKind('enable');
+          setOptimisticEnableStatus(current => ({
+            ...current,
+            [plugin.id]: !Boolean(plugin.enableStatus)
+          }));
+          setIsTogglePending(true);
+          void togglePluginStatus(apiMessagePut, plugin)
+            .then(() => {
+              setActionTone('success');
+              setActionMessage(t('common.notify.edit-success'));
+              setSelectedIds([]);
+              setReloadVersion(version => version + 1);
+            })
+            .catch(error => {
+              setActionTone('critical');
+              setActionError(t('common.notify.edit-fail'));
+              setActionMeta(error instanceof Error ? error.message : null);
+            })
+            .finally(() => {
+              setIsTogglePending(false);
+            });
+        }}
+        onDeleteOne={plugin => {
+          clearActionFeedback();
+          setActionKind('delete');
+          setDeleteTarget({
+            ids: [plugin.id],
+            label: plugin.name,
+            mode: 'single'
+          });
+        }}
+        onDraftChange={patch => {
+          setUploadValidation(previous => ({
+            ...previous,
+            ...(Object.prototype.hasOwnProperty.call(patch, 'name') ? { name: false } : {}),
+            ...(Object.prototype.hasOwnProperty.call(patch, 'jarFile') ? { jarFile: false } : {})
+          }));
+          setDraftPlugin(previous => (previous ? { ...previous, ...patch } : previous));
+        }}
+        onCloseDialog={() => {
+          setIsUploadDialogOpen(false);
+        }}
+        onSaveDialog={() => {
+          if (!draftPlugin) return;
+          clearActionFeedback();
+          setActionKind('upload');
+          const validation = validatePluginUploadDraft(draftPlugin);
+          if (!validation.name || !validation.jarFile) {
+            setUploadValidation({
+              name: !validation.name,
+              jarFile: !validation.jarFile
+            });
+            return;
+          }
+          setUploadValidation({});
+          setIsUploadPending(true);
+          void savePlugin(apiMessagePost, draftPlugin)
+            .then(() => {
+              setActionTone('success');
+              setActionMessage(t('common.notify.new-success'));
+              setUploadValidation({});
+              setIsUploadDialogOpen(false);
+              setDraftPlugin(null);
+              setSelectedIds([]);
+              setReloadVersion(version => version + 1);
+            })
+            .catch(error => {
+              setActionTone('critical');
+              setActionError(t('common.notify.new-fail'));
+              setActionMeta(error instanceof Error ? error.message : null);
+            })
+            .finally(() => {
+              setIsUploadPending(false);
+            });
+        }}
+      />
+    );
+  }
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('setting.plugins.loading')}
+      cacheKey={pluginManageCacheKey}
+      cacheSettledTtlMs={SETTING_PLUGINS_SETTLED_CACHE_TTL_MS}
+      renderError={(message, retry) => renderPluginSurface(buildFallbackPluginData(), message, retry)}
+    >
+      {data => renderPluginSurface(data)}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/components/pages/collector-manage-surface.test.tsx b/web-next/components/pages/collector-manage-surface.test.tsx
index fe189bfbb2..1fea926bc8 100644
--- a/web-next/components/pages/collector-manage-surface.test.tsx
+++ b/web-next/components/pages/collector-manage-surface.test.tsx
@@ -4,10 +4,10 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
+import { CollectorManageSurface } from './collector-manage-surface';
 
 describe('collector manage surface', () => {
-  it('renders the cold-matte collector toolbar and dense table shell', async () => {
-    const { CollectorManageSurface } = await import('./collector-manage-surface');
+  it('renders the cold-matte collector toolbar and dense table shell', () => {
     const t = createTranslatorMock({
       locale: 'zh-CN',
       overrides: {
@@ -68,8 +68,8 @@ describe('collector manage surface', () => {
                 dispatchMonitorNum: 3
               }
             ],
-            totalElements: 2,
-            pageIndex: 0,
+            totalElements: 18,
+            pageIndex: 1,
             pageSize: 8
           }
         }}
@@ -77,6 +77,7 @@ describe('collector manage surface', () => {
         formatTime={() => '2026-04-10 18:00:00'}
         onSearchChange={() => {}}
         onSearch={() => {}}
+        onSearchClear={() => {}}
         onRefresh={() => {}}
         onDeploy={() => {}}
         onGoOnline={() => {}}
@@ -96,17 +97,99 @@ describe('collector manage surface', () => {
     expect(html).toContain('data-collector-health-tone="warning"');
     expect(html).toContain('data-collector-health-freshness="last-seen"');
     expect(html).toContain('data-collector-admin-layout="full-width-admin-list"');
+    expect(html).toContain('data-collector-delete-warning-contract="angular-no-select-warning"');
+    expect(html).toContain('data-collector-delete-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-collector-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-collector-delete-feedback-contract="angular-delete-notify"');
+    expect(html).toContain('data-collector-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-delete-api-contract="angular-repeated-collectors-query"');
+    expect(html).toContain('data-collector-delete-page-clamp-contract="angular-update-page-index"');
+    expect(html).toContain('data-collector-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-collector-operate-warning-contract="angular-no-select-online-offline"');
+    expect(html).toContain('data-collector-operate-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-collector-operate-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-collector-operate-feedback-contract="angular-operate-notify"');
+    expect(html).toContain('data-collector-operate-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-operate-api-contract="angular-put-repeated-collectors-query"');
+    expect(html).toContain('data-collector-mutation-loading-contract="angular-nz-table-loading"');
+    expect(html).toContain('data-collector-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-collector-load-failure-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-load-failure="none"');
+    expect(html).toContain('data-collector-deploy-modal-contract="angular-generate-identity-modal"');
+    expect(html).toContain('data-collector-deploy-api-contract="angular-post-generate-identity"');
+    expect(html).toContain('data-collector-deploy-failure-contract="angular-apply-fail-notification"');
+    expect(html).toContain('data-collector-deploy-loading-contract="angular-nz-ok-loading"');
+    expect(html).toContain('data-collector-deploy-validation-raw-contract="angular-required-before-trim"');
+    expect(html).toContain('data-collector-deploy-validation-raw-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-deploy-mask-contract="angular-mask-closable-false"');
+    expect(html).toContain('data-collector-deploy-width-contract="angular-width-45-percent"');
+    expect(html).toContain('data-collector-deploy-field-layout-contract="angular-label-7-control-12"');
+    expect(html).toContain('data-collector-deploy-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-collector-deploy-command-contract="angular-docker-package-shells"');
+    expect(html).toContain('data-collector-deploy-command-owner="hertzbeat-ui-code-editor"');
+    expect(html).toContain('data-collector-deploy-package-link-contract="angular-github-releases-link"');
+    expect(html).toContain('data-collector-deploy-copy-contract="angular-copy-identity"');
+    expect(html).toContain('data-collector-deploy-copy-feedback-contract="angular-copy-success-duration-800"');
+    expect(html).toContain('data-collector-deploy-result-close-contract="angular-danger-close-after-identity"');
+    expect(html).toContain('data-collector-deploy-result-close-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-collector-deploy-close-reset-contract="angular-close-clears-name"');
+    expect(html).toContain('data-collector-deploy-close-reset-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-deploy-close-pending-result-contract="angular-close-ignores-late-generate"');
+    expect(html).toContain('data-collector-deploy-close-pending-result-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-pagination="cold-dense-pagination"');
+    expect(html).toContain('data-collector-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-collector-pagination-contract="angular-search-pagination"');
+    expect(html).toContain('data-hz-ui="pagination-bar"');
+    expect(html).toContain('data-hz-pagination-page-size="select-menu"');
+    expect(html).toContain('data-hz-pagination-page-jump="number-input"');
+    expect(html).toContain('data-collector-pagination-page-size-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-collector-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-collector-deploy-open="angular-deploy-modal"');
+    expect(html).toContain('data-collector-online-selected="angular-batch-online-entry"');
+    expect(html).toContain('data-collector-offline-selected="angular-batch-offline-entry"');
+    expect(html).toContain('data-collector-delete-selected="angular-batch-delete-entry"');
     expect(html).toContain('data-collector-toolbar="cold-table-toolbar"');
     expect(html).toContain('data-collector-search-owner="shared-search-row"');
+    expect(html).toContain('data-collector-search-submit-contract="angular-enter-and-clear"');
+    expect(html).toContain('data-collector-search-clear-contract="angular-cleared-load"');
+    expect(html).toContain('data-collector-search-clear-owner="shared-search-row"');
     expect(html).toContain('data-cold-search-row-owner="cold-search-row"');
     expect(html).toContain('data-cold-search-input="fixed-width-direct"');
     expect(html).toContain('data-cold-search-control="direct-input"');
     expect(html).toContain('data-cold-search-chrome="no-extra-input-shell"');
     expect(html).toContain('data-cold-search-action="submit"');
+    expect(html).toContain('data-cold-search-action="clear"');
     expect(html).toContain('data-collector-table-shell="cold-dense-table"');
+    expect(html).toContain('data-collector-table-loading="false"');
+    expect(html).toContain('data-collector-table-loading-contract="angular-load-collectors-table"');
+    expect(html).toContain('data-collector-table-loading-owner="angular-nz-table-loading"');
+    expect(html).toContain('data-collector-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-collector-table-loading-source="idle"');
+    expect(html).toContain('aria-busy="false"');
     expect(html).toContain('data-collector-manage-table="cold-collector-table"');
+    expect(html).toContain('data-collector-select-all="cold-checkbox"');
+    expect(html).toContain('data-collector-row-select="edge-a"');
+    expect(html).toContain('data-collector-row-select="edge-b"');
     expect(html).toContain('data-collector-row-health="collector-status"');
     expect(html).toContain('data-collector-row-actions="cold-icon-actions"');
+    expect(html).toContain('data-collector-row-delete-menu="edge-a"');
+    expect(html).toContain('data-collector-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-collector-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-collector-row-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-collector-row-delete-menu-open="false"');
+    expect(html).toContain('aria-expanded="false"');
+    expect(html).toContain('data-collector-row-delete-menu-trigger="edge-a"');
+    expect(html).toContain('data-collector-row-delete-menu-panel="edge-a"');
+    expect(html).toContain('data-collector-row-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(html).toContain('data-collector-row-delete-menu-clearance-panel="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-collector-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-collector-row-delete-menu-panel-open="false"');
+    expect(html).toContain('data-collector-delete-one="edge-a"');
+    expect(html).toContain('data-collector-delete-one-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-collector-offline-one="edge-a"');
+    expect(html).toContain('data-collector-online-one="edge-b"');
+    expect(html).toContain('data-collector-status-tone="success"');
+    expect(html).toContain('data-collector-status-tone="danger"');
     expect(html).not.toContain('data-cold-search-input-shell');
     expect(html).not.toContain('data-collector-summary-rail=');
     expect(html).toContain('刷新');
@@ -117,6 +200,9 @@ describe('collector manage surface', () => {
     expect(html).toContain('采集器 1 / 2 在线');
     expect(html).toContain('任务 11 · 离线 1');
     expect(html).toContain('最近上报 2026-04-10 18:00:00');
+    expect(html).toContain('第 2 / 3 页 · 9-10 / 18');
+    expect(html).toContain('每页条数');
+    expect(html).toContain('页码');
     expect(html).toContain('搜索');
     expect(html).toContain('采集集群');
     expect(html).toContain('采集器名称');
@@ -146,10 +232,148 @@ describe('collector manage surface', () => {
     expect(source).toContain('data-collector-manage-surface="otlp-cold-collector-console"');
     expect(source).toContain('data-collector-manage-style-baseline={coldCollectorVisual.canvasName}');
     expect(source).toContain('data-collector-admin-layout="full-width-admin-list"');
+    expect(source).toContain('data-collector-delete-warning-contract="angular-no-select-warning"');
+    expect(source).toContain('data-collector-delete-confirm-contract="angular-modal-confirm"');
+    expect(source).toContain('data-collector-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-collector-delete-feedback-contract="angular-delete-notify"');
+    expect(source).toContain('data-collector-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-collector-delete-api-contract="angular-repeated-collectors-query"');
+    expect(source).toContain('data-collector-delete-page-clamp-contract="angular-update-page-index"');
+    expect(source).toContain('data-collector-delete-page-clamp-owner="route-state-contract"');
+    expect(source).toContain('data-collector-selection-reset-contract="angular-load-clears-selection"');
+    expect(source).toContain('data-collector-selection-reset-owner="route-state-contract"');
+    expect(source).toContain('data-collector-search-submit-contract="angular-enter-and-clear"');
+    expect(source).toContain('data-collector-search-clear-contract="angular-cleared-load"');
+    expect(source).toContain('data-collector-search-clear-owner="shared-search-row"');
+    expect(source).toContain('data-collector-operate-warning-contract="angular-no-select-online-offline"');
+    expect(source).toContain('data-collector-operate-confirm-contract="angular-modal-confirm"');
+    expect(source).toContain('data-collector-operate-feedback-contract="angular-operate-notify"');
+    expect(source).toContain('data-collector-operate-api-contract="angular-put-repeated-collectors-query"');
+    expect(source).toContain('data-collector-mutation-loading-contract="angular-nz-table-loading"');
+    expect(source).toContain('data-collector-load-failure-contract="angular-console-only-shell"');
+    expect(source).toContain('data-collector-load-failure-owner="collector-route-controller"');
+    expect(source).toContain("data-collector-load-failure={loadError ? 'angular-console-only-shell' : 'none'}");
+    expect(source).toContain('meta={actionMeta ?? undefined}');
+    expect(source).toContain('data-collector-delete-feedback-title');
+    expect(source).toContain('data-collector-delete-feedback-detail');
+    expect(source).toContain('data-collector-operate-feedback-title');
+    expect(source).toContain('data-collector-operate-feedback-detail');
+    expect(source).toContain('const mutationPending = Boolean(isDeletePending)');
+    expect(source).toContain('const tableLoading = Boolean(isLoadPending || mutationPending)');
+    expect(source).toContain('data-collector-deploy-modal-contract="angular-generate-identity-modal"');
+    expect(source).toContain('data-collector-deploy-api-contract="angular-post-generate-identity"');
+    expect(source).toContain('data-collector-deploy-failure-contract="angular-apply-fail-notification"');
+    expect(source).toContain('data-collector-deploy-validation-contract="angular-submit-marks-required"');
+    expect(source).toContain('data-collector-deploy-validation-raw-contract="angular-required-before-trim"');
+    expect(source).toContain('data-collector-deploy-validation-raw-owner="collector-route-controller"');
+    expect(source).toContain('data-collector-deploy-command-contract="angular-docker-package-shells"');
+    expect(source).toContain('data-collector-deploy-command-owner="hertzbeat-ui-code-editor"');
+    expect(source).toContain('data-collector-deploy-package-link-contract="angular-github-releases-link"');
+    expect(source).toContain('data-collector-deploy-package-link="angular-github-releases-link"');
+    expect(source).toContain('href="https://github.com/apache/hertzbeat/releases"');
+    expect(source).toContain('target="_blank"');
+    expect(source).toContain("title={t('common.notify.apply-fail')}");
+    expect(source).toContain('meta={deployError}');
+    expect(source).toContain('data-collector-deploy-feedback="angular-apply-fail-notification"');
+    expect(source).toContain('data-collector-deploy-feedback-title="common.notify.apply-fail"');
+    expect(source).toContain('data-collector-deploy-feedback-detail="backend-message"');
+    expect(source).toContain('data-collector-deploy-copy-contract="angular-copy-identity"');
+    expect(source).toContain('data-collector-deploy-copy-feedback-contract="angular-copy-success-duration-800"');
+    expect(source).toContain('data-collector-deploy-copy-feedback-duration-ms="800"');
+    expect(source).toContain('data-collector-deploy-dialog="angular-generate-identity-modal"');
+    expect(source).toContain('data-collector-deploy-mask-contract="angular-mask-closable-false"');
+    expect(source).toContain('data-collector-deploy-mask-closable="angular-mask-closable-false"');
+    expect(source).toContain('data-collector-deploy-mask-closable-owner="angular-nz-modal"');
+    expect(source).toContain('data-collector-deploy-width-contract="angular-width-45-percent"');
+    expect(source).toContain('data-collector-deploy-width="angular-width-45-percent"');
+    expect(source).toContain('data-collector-deploy-width-owner="angular-nz-modal"');
+    expect(source).toContain('md:w-[45vw] md:max-w-[45vw]');
+    expect(source).toContain('data-collector-deploy-field-layout-contract="angular-label-7-control-12"');
+    expect(source).toContain('data-collector-deploy-field-layout-owner="route-form-field-grid"');
+    expect(source).toContain('data-collector-deploy-field="name"');
+    expect(source).toContain('data-collector-deploy-field-layout="angular-label-7-control-12"');
+    expect(source).toContain('data-collector-deploy-label-span="7"');
+    expect(source).toContain('data-collector-deploy-control-span="12"');
+    expect(source).toContain('md:grid-cols-[7fr_12fr]');
+    expect(source).toContain('data-collector-deploy-name-input="angular-required-name"');
+    expect(source).toContain('data-collector-deploy-name-validation-raw="angular-required-before-trim"');
+    expect(source).toContain('data-collector-deploy-name-validation-raw-owner="collector-route-controller"');
+    expect(source).toContain("data-collector-deploy-name-lock={resolvedDeployIdentity ? 'angular-disable-after-identity' : 'editable-before-identity'}");
+    expect(source).toContain('data-collector-deploy-name-lock-owner="angular-ngmodel-input"');
+    expect(source).toContain("data-collector-deploy-name-disabled={resolvedDeployIdentity ? 'true' : 'false'}");
+    expect(source).toContain('data-collector-deploy-name-validation-state');
+    expect(source).toContain('data-collector-deploy-name-validation-contract="angular-submit-marks-required"');
+    expect(source).toContain('data-collector-deploy-loading-contract="angular-nz-ok-loading"');
+    expect(source).toContain('data-collector-deploy-generate="angular-generate-identity"');
+    expect(source).toContain('data-collector-deploy-generate-validation="angular-click-before-required"');
+    expect(source).toContain("data-collector-deploy-ok-loading={isDeployPending ? 'true' : 'false'}");
+    expect(source).toContain('data-collector-deploy-ok-loading-owner="angular-nz-ok-loading"');
+    expect(source).toContain('disabled={Boolean(isDeployPending || resolvedDeployIdentity)}');
+    expect(source).toContain('data-collector-deploy-result-close-contract="angular-danger-close-after-identity"');
+    expect(source).toContain('data-collector-deploy-result-close-owner="hertzbeat-ui-button"');
+    expect(source).toContain('data-collector-deploy-close-reset-contract="angular-close-clears-name"');
+    expect(source).toContain('data-collector-deploy-close-reset="angular-close-clears-name"');
+    expect(source).toContain('data-collector-deploy-close-reset-owner="collector-route-controller"');
+    expect(source).toContain('data-collector-deploy-result-close-reset="angular-close-clears-name"');
+    expect(source).toContain('data-collector-deploy-result-close-reset-owner="collector-route-controller"');
+    expect(source).toContain('data-collector-deploy-docker-shell="angular-docker-shell"');
+    expect(source).toContain('data-collector-deploy-package-shell="angular-package-shell"');
+    expect(source).toContain('data-collector-online-selected="angular-batch-online-entry"');
+    expect(source).toContain('data-collector-offline-selected="angular-batch-offline-entry"');
+    expect(source).toContain('data-collector-delete-selected="angular-batch-delete-entry"');
+    expect(source).toContain('HzConfirmDialog');
+    expect(source).toContain('HzInlineFeedback');
+    expect(source).toContain('HzPaginationBar');
+    expect(source).toContain('HzTableRowActionButton');
+    expect(source).toContain('pageSizeOptions = [8, 15, 25]');
+    expect(source).toContain('data-collector-pagination="cold-dense-pagination"');
+    expect(source).toContain('data-collector-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(source).toContain('data-collector-pagination-contract="angular-search-pagination"');
+    expect(source).toContain('data-collector-pagination-page-size-owner');
+    expect(source).toContain('data-collector-pagination-page-jump-owner');
+    expect(source).toContain("t('collector.pagination.summary'");
+    expect(source).toContain("t('collector.pagination.page-size')");
+    expect(source).toContain("t('collector.pagination.page')");
+    expect(source).toContain('data-collector-select-all="cold-checkbox"');
+    expect(source).toContain('data-collector-select-all-contract="angular-header-includes-disabled-default"');
+    expect(source).toContain('data-collector-selectable-names={collectorNames.join');
+    expect(source).toContain('data-collector-immutable-names={immutableCollectorNames.join');
+    expect(source).toContain('event.target.checked ? collectorNames : []');
+    expect(source).toContain('data-collector-row-select={row.name}');
+    expect(source).toContain("row.canMutate ? 'angular-selectable' : 'angular-disabled-visual-only'");
+    expect(source).toContain('checked={row.canMutate && resolvedSelectedCollectors.includes(row.name)}');
+    expect(source).toContain('data-collector-online-one={row.name}');
+    expect(source).toContain('data-collector-offline-one={row.name}');
+    expect(source).toContain('const [openRowActionMenuId, setOpenRowActionMenuId] = React.useState<string | null>(null)');
+    expect(source).toContain('const isRowActionMenuOpen = openRowActionMenuId === rowActionMenuId');
+    expect(source).toContain('data-collector-row-delete-menu={rowActionMenuId}');
+    expect(source).toContain('data-collector-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(source).toContain('data-collector-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(source).toContain('data-collector-row-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(source).toContain("data-collector-row-delete-menu-open={isRowActionMenuOpen ? 'true' : 'false'}");
+    expect(source).toContain('aria-expanded={isRowActionMenuOpen}');
+    expect(source).toContain('data-collector-row-delete-menu-trigger={rowActionMenuId}');
+    expect(source).toContain('data-collector-row-delete-menu-panel={rowActionMenuId}');
+    expect(source).toContain('data-collector-row-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(source).toContain('data-collector-row-delete-menu-clearance-panel="floating-overlay-no-panel-crop"');
+    expect(source).toContain('data-collector-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain("data-collector-row-delete-menu-panel-open={isRowActionMenuOpen ? 'true' : 'false'}");
+    expect(source).not.toContain('<details');
+    expect(source).not.toContain('</details>');
+    expect(source).toContain('collectorActionMenuRootClassName');
+    expect(source).toContain('collectorActionMenuPanelBaseClassName');
+    expect(source).toContain('data-collector-delete-one={row.name}');
+    expect(source).toContain('data-collector-delete-one-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('data-collector-delete-confirm="angular-modal-confirm"');
     expect(source).toContain('data-collector-health-evidence="cluster-status"');
     expect(source).toContain('data-collector-health-freshness="last-seen"');
     expect(source).toContain('data-collector-row-health="collector-status"');
     expect(source).toContain('data-collector-table-shell="cold-dense-table"');
+    expect(source).toContain("data-collector-table-loading={tableLoading ? 'true' : 'false'}");
+    expect(source).toContain('data-collector-table-loading-contract="angular-load-collectors-table"');
+    expect(source).toContain('data-collector-table-loading-owner="angular-nz-table-loading"');
+    expect(source).toContain('data-collector-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(source).toContain("data-collector-table-loading-source={tableLoading ? 'route-load-or-mutation' : 'idle'}");
     expect(source).toContain('data-collector-manage-table="cold-collector-table"');
     expect(source).not.toContain('data-collector-summary-rail');
     expect(source).not.toContain('coldCollectorVisual.search.row');
@@ -180,4 +404,602 @@ describe('collector manage surface', () => {
     expect(source).toContain('text-[#a9b0bb]');
     expect(source).toContain('text-[#858d9a]');
   });
+
+  it('keeps the table busy while route reloads run like Angular loadCollectorsTable', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        isLoadPending
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-collector-table-loading="true"');
+    expect(html).toContain('data-collector-table-loading-contract="angular-load-collectors-table"');
+    expect(html).toContain('data-collector-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-collector-table-loading-source="route-load-or-mutation"');
+    expect(html).toContain('aria-busy="true"');
+  });
+
+  it('keeps the collector shell mounted after list load failure without rendering backend text', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        loadError="backend refused collector load"
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-collector-manage-surface="otlp-cold-collector-console"');
+    expect(html).toContain('data-collector-admin-layout="full-width-admin-list"');
+    expect(html).toContain('data-collector-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-collector-load-failure-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-collector-table-shell="cold-dense-table"');
+    expect(html).not.toContain('backend refused collector load');
+  });
+
+  it('renders Angular collector delete warning, confirmation, and feedback contracts', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [
+              {
+                collector: {
+                  name: 'edge-a',
+                  ip: '10.0.0.1',
+                  status: 0,
+                  mode: 'public',
+                  version: '1.0.0',
+                  gmtUpdate: '2026-04-10T10:00:00Z'
+                },
+                pinMonitorNum: 2,
+                dispatchMonitorNum: 5
+              }
+            ],
+            totalElements: 1,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedCollectors={['edge-a']}
+        actionError="请先选择要删除的监控"
+        actionTone="warning"
+        deleteTarget={{
+          collectors: ['edge-a'],
+          label: 'edge-a',
+          mode: 'single'
+        }}
+        isDeletePending
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onSelectedCollectorsChange={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-collector-delete-warning-contract="angular-no-select-warning"');
+    expect(html).toContain('data-collector-delete-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-collector-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-collector-delete-feedback-contract="angular-delete-notify"');
+    expect(html).toContain('data-collector-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-delete-api-contract="angular-repeated-collectors-query"');
+    expect(html).toContain('data-collector-delete-page-clamp-contract="angular-update-page-index"');
+    expect(html).toContain('data-collector-delete-page-clamp-owner="route-state-contract"');
+    expect(html).toContain('data-collector-delete-selected="angular-batch-delete-entry"');
+    expect(html).toContain('data-collector-delete-feedback="angular-no-select-warning"');
+    expect(html).toContain('data-collector-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-collector-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-collector-delete-confirm-submit="angular-modal-confirm"');
+    expect(html).toContain('data-collector-table-loading="true"');
+    expect(html).toContain('aria-busy="true"');
+    expect(html).toContain('data-collector-delete-confirm-target="edge-a"');
+    expect(html).toContain('data-collector-row-select="edge-a"');
+    expect(html).toContain('data-collector-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-collector-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-collector-row-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-collector-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-collector-row-delete-menu-panel-open="false"');
+    expect(html).toContain('data-collector-delete-one="edge-a"');
+    expect(html).toContain('请先选择要删除的监控');
+    expect(html).toContain('请确认是否删除!');
+  });
+
+  it('renders Angular collector delete and operate failure titles with backend detail metadata', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const deleteFailureHtml = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        actionError="删除失败!"
+        actionMeta="backend delete refused"
+        actionTone="critical"
+        actionFeedbackContract="angular-delete-fail"
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(deleteFailureHtml).toContain('data-collector-delete-feedback="angular-delete-fail"');
+    expect(deleteFailureHtml).toContain('data-collector-delete-feedback-title="common.notify.delete-fail"');
+    expect(deleteFailureHtml).toContain('data-collector-delete-feedback-detail="backend-message"');
+    expect(deleteFailureHtml).toContain('删除失败!');
+    expect(deleteFailureHtml).toContain('backend delete refused');
+
+    const operateFailureHtml = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        actionError="操作失败!"
+        actionMeta="backend operate refused"
+        actionTone="critical"
+        actionFeedbackContract="angular-operate-fail"
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(operateFailureHtml).toContain('data-collector-operate-feedback="angular-operate-fail"');
+    expect(operateFailureHtml).toContain('data-collector-operate-feedback-title="common.notify.operate-fail"');
+    expect(operateFailureHtml).toContain('data-collector-operate-feedback-detail="backend-message"');
+    expect(operateFailureHtml).toContain('操作失败!');
+    expect(operateFailureHtml).toContain('backend operate refused');
+  });
+
+  it('renders Angular collector online/offline warning, confirmation, and feedback contracts', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [
+              {
+                collector: {
+                  name: 'edge-a',
+                  ip: '10.0.0.1',
+                  status: 0,
+                  mode: 'public',
+                  version: '1.0.0',
+                  gmtUpdate: '2026-04-10T10:00:00Z'
+                },
+                pinMonitorNum: 2,
+                dispatchMonitorNum: 5
+              }
+            ],
+            totalElements: 1,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedCollectors={['edge-a']}
+        actionError="未选中任何待下线项!"
+        actionTone="warning"
+        actionFeedbackContract="angular-no-select-offline"
+        deleteTarget={{
+          action: 'offline',
+          collectors: ['edge-a'],
+          label: 'edge-a',
+          mode: 'single'
+        }}
+        isDeletePending
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onSelectedCollectorsChange={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-collector-operate-warning-contract="angular-no-select-online-offline"');
+    expect(html).toContain('data-collector-operate-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-collector-operate-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-collector-operate-feedback-contract="angular-operate-notify"');
+    expect(html).toContain('data-collector-operate-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-operate-api-contract="angular-put-repeated-collectors-query"');
+    expect(html).toContain('data-collector-action-feedback-contract="angular-no-select-offline"');
+    expect(html).toContain('data-collector-operate-feedback="angular-no-select-offline"');
+    expect(html).toContain('data-collector-operate-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-operate-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-collector-operate-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-collector-operate-confirm-submit="angular-modal-confirm"');
+    expect(html).toContain('data-collector-table-loading="true"');
+    expect(html).toContain('aria-busy="true"');
+    expect(html).toContain('data-collector-delete-confirm-target="edge-a"');
+    expect(html).toContain('data-collector-offline-one="edge-a"');
+    expect(html).toContain('未选中任何待下线项!');
+    expect(html).toContain('请确认是否下线此采集器!');
+  });
+
+  it('keeps the Angular header select-all batch payload while the default row stays visually disabled', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [
+              {
+                collector: {
+                  name: 'main-default-collector',
+                  ip: '127.0.0.1',
+                  status: 0,
+                  mode: 'public',
+                  version: '2.0.0',
+                  gmtUpdate: '2026-04-10T10:00:00Z'
+                },
+                pinMonitorNum: 5,
+                dispatchMonitorNum: 8
+              },
+              {
+                collector: {
+                  name: 'edge-a',
+                  ip: '10.0.0.1',
+                  status: 1,
+                  mode: 'private',
+                  version: '1.0.0',
+                  gmtUpdate: '2026-04-09T10:00:00Z'
+                },
+                pinMonitorNum: 2,
+                dispatchMonitorNum: 5
+              }
+            ],
+            totalElements: 2,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedCollectors={['edge-a']}
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onSelectedCollectorsChange={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-collector-select-all-contract="angular-header-includes-disabled-default"');
+    expect(html).toContain('data-collector-selectable-names="main-default-collector,edge-a"');
+    expect(html).toContain('data-collector-immutable-names="main-default-collector"');
+    expect(html).toContain('data-collector-row-select="main-default-collector"');
+    expect(html).toContain('data-collector-row-select-contract="angular-disabled-visual-only"');
+    expect(html).toContain('data-collector-row-select="edge-a"');
+    expect(html).toContain('data-collector-row-select-contract="angular-selectable"');
+  });
+
+  it('renders Angular collector deploy identity modal and code editor contracts', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        deployOpen
+        deployName="edge-a"
+        deployIdentity="identity-123"
+        deployHost="10.0.0.10"
+        deployDockerShell="$ docker run -d -e IDENTITY=identity-123"
+        deployPackageShell="identity: identity-123\nmanager-host: 10.0.0.10"
+        deployCopyMessage="复制成功!"
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onDeployNameChange={() => {}}
+        onDeployGenerate={() => {}}
+        onDeployClose={() => {}}
+        onDeployCopy={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-collector-deploy-dialog="angular-generate-identity-modal"');
+    expect(html).toContain('data-collector-deploy-mask-closable="angular-mask-closable-false"');
+    expect(html).toContain('data-collector-deploy-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-collector-deploy-width="angular-width-45-percent"');
+    expect(html).toContain('data-collector-deploy-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-collector-deploy-field="name"');
+    expect(html).toContain('data-collector-deploy-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-collector-deploy-label-span="7"');
+    expect(html).toContain('data-collector-deploy-control-span="12"');
+    expect(html).toContain('data-collector-deploy-name-input="angular-required-name"');
+    expect(html).toContain('data-collector-deploy-name-lock="angular-disable-after-identity"');
+    expect(html).toContain('data-collector-deploy-name-lock-owner="angular-ngmodel-input"');
+    expect(html).toContain('data-collector-deploy-name-disabled="true"');
+    expect(html).toContain('disabled=""');
+    expect(html).toContain('data-collector-deploy-result="angular-identity-shells"');
+    expect(html).toContain('data-collector-deploy-copy-identity="angular-copy-identity"');
+    expect(html).toContain('data-collector-deploy-copy-feedback="angular-copy-success"');
+    expect(html).toContain('data-collector-deploy-result-close-contract="angular-danger-close-after-identity"');
+    expect(html).toContain('data-collector-deploy-copy-feedback-duration-ms="800"');
+    expect(html).toContain('data-collector-deploy-result-close-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-collector-deploy-close-reset="angular-close-clears-name"');
+    expect(html).toContain('data-collector-deploy-close-reset-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-deploy-close-pending-result="angular-close-ignores-late-generate"');
+    expect(html).toContain('data-collector-deploy-close-pending-result-owner="collector-route-controller"');
+    expect(html).toContain('data-collector-deploy-result-close-reset="angular-close-clears-name"');
+    expect(html).toContain('data-collector-deploy-result-close-reset-owner="collector-route-controller"');
+    expect(html).toContain('data-hz-button-tier="solid-danger"');
+    expect(html).toContain('data-collector-deploy-docker-shell="angular-docker-shell"');
+    expect(html).toContain('data-collector-deploy-package-shell="angular-package-shell"');
+    expect(html).toContain('data-collector-deploy-code-owner="hertzbeat-ui-code-editor"');
+    expect(html).toContain('data-collector-deploy-package-link="angular-github-releases-link"');
+    expect(html).toContain('data-collector-deploy-package-link-owner="angular-nz-modal"');
+    expect(html).toContain('href="https://github.com/apache/hertzbeat/releases"');
+    expect(html).toContain('target="_blank"');
+    expect(html).toContain('identity-123');
+    expect(html).toContain('10.0.0.10');
+    expect(html).toContain('通过 Docker 方式部署');
+    expect(html).toContain('通过安装包部署');
+  });
+
+  it('keeps collector deploy required-name validation submit-triggered like Angular', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const pristineHtml = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        deployOpen
+        deployName=""
+        deployNameValidationVisible={false}
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onDeployNameChange={() => {}}
+        onDeployGenerate={() => {}}
+        onDeployClose={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+    const dirtyHtml = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        deployOpen
+        deployName=""
+        deployNameValidationVisible
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onDeployNameChange={() => {}}
+        onDeployGenerate={() => {}}
+        onDeployClose={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(pristineHtml).toContain('data-collector-deploy-validation-contract="angular-submit-marks-required"');
+    expect(pristineHtml).toContain('data-collector-deploy-validation-raw-contract="angular-required-before-trim"');
+    expect(pristineHtml).toContain('data-collector-deploy-name-validation-state="pristine"');
+    expect(pristineHtml).toContain('data-collector-deploy-name-validation-raw="angular-required-before-trim"');
+    expect(pristineHtml).toContain('data-collector-deploy-name-lock="editable-before-identity"');
+    expect(pristineHtml).toContain('data-collector-deploy-name-disabled="false"');
+    expect(pristineHtml).toContain('data-collector-deploy-generate-validation="angular-click-before-required"');
+    expect(pristineHtml).not.toContain('data-collector-deploy-name-validation="validation.required"');
+    expect(dirtyHtml).toContain('data-collector-deploy-name-validation-state="dirty-invalid"');
+    expect(dirtyHtml).toContain('data-collector-deploy-name-validation="validation.required"');
+    expect(dirtyHtml).toContain('data-collector-deploy-name-validation-contract="angular-submit-marks-required"');
+    expect(dirtyHtml).toContain('请填充必填项!');
+  });
+
+  it('keeps collector deploy OK loading visible while identity generation is pending', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        deployOpen
+        deployName="edge-a"
+        isDeployPending
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onDeployNameChange={() => {}}
+        onDeployGenerate={() => {}}
+        onDeployClose={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-collector-deploy-loading-contract="angular-nz-ok-loading"');
+    expect(html).toContain('data-collector-deploy-ok-loading="true"');
+    expect(html).toContain('data-collector-deploy-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('aria-busy="true"');
+    expect(html).toContain('disabled=""');
+    expect(html).toContain('加载中');
+  });
+
+  it('keeps collector deploy generate failures as Angular apply-fail notifications', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <CollectorManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        deployOpen
+        deployName="edge-a"
+        deployError="collector already exists"
+        formatTime={() => '2026-04-10 18:00:00'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onDeploy={() => {}}
+        onGoOnline={() => {}}
+        onGoOffline={() => {}}
+        onDelete={() => {}}
+        onDeployNameChange={() => {}}
+        onDeployGenerate={() => {}}
+        onDeployClose={() => {}}
+        onRowGoOnline={() => {}}
+        onRowGoOffline={() => {}}
+        onRowDelete={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-collector-deploy-failure-contract="angular-apply-fail-notification"');
+    expect(html).toContain('data-collector-deploy-feedback="angular-apply-fail-notification"');
+    expect(html).toContain('data-collector-deploy-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-collector-deploy-feedback-title="common.notify.apply-fail"');
+    expect(html).toContain('data-collector-deploy-feedback-detail="backend-message"');
+    expect(html).toContain('collector already exists');
+  });
 });
diff --git a/web-next/components/pages/collector-manage-surface.tsx b/web-next/components/pages/collector-manage-surface.tsx
index 950313a96f..2aedcc3523 100644
--- a/web-next/components/pages/collector-manage-surface.tsx
+++ b/web-next/components/pages/collector-manage-surface.tsx
@@ -1,27 +1,68 @@
 'use client';
 
 import React from 'react';
-import { Cloud, Inbox, Power, PowerOff, RefreshCw, Trash2, UploadCloud } from 'lucide-react';
+import { Cloud, Copy, Inbox, MoreHorizontal, Power, PowerOff, RefreshCw, Trash2, UploadCloud, X } from 'lucide-react';
+import { HzButton, HzCodeEditor, HzConfirmDialog, HzInlineFeedback, HzPaginationBar, HzTableRowActionButton } from '@hertzbeat/ui';
 import { Button } from '../ui/button';
+import { Checkbox } from '../ui/checkbox';
 import { SearchRow } from '../ui/search-row';
 import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
 import { buildCollectorClusterHealthEvidence, buildCollectorTableRows } from '../../lib/collector-manage/view-model';
 import type { CollectorSummary, PageResult } from '../../lib/types';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
+type CollectorActionTone = 'success' | 'warning' | 'critical';
+type CollectorMutationAction = 'delete' | 'online' | 'offline';
+export type CollectorDeleteTarget = {
+  collectors: string[];
+  label: string;
+  mode: 'single' | 'batch';
+  action?: CollectorMutationAction;
+};
 
 type CollectorManageSurfaceProps = {
   t: Translator;
   data: { list: PageResult<CollectorSummary> };
   search: string;
+  selectedCollectors?: string[];
+  actionMessage?: string | null;
+  actionError?: string | null;
+  actionMeta?: string | null;
+  actionTone?: CollectorActionTone;
+  actionFeedbackContract?: string | null;
+  loadError?: string | null;
+  deleteTarget?: CollectorDeleteTarget | null;
+  isDeletePending?: boolean;
+  isLoadPending?: boolean;
+  deployOpen?: boolean;
+  deployName?: string;
+  deployIdentity?: string;
+  deployHost?: string;
+  deployDockerShell?: string;
+  deployPackageShell?: string;
+  deployError?: string | null;
+  deployCopyMessage?: string | null;
+  isDeployPending?: boolean;
+  deployNameValidationVisible?: boolean;
   formatTime: (value?: number | string | null) => string;
   onSearchChange: (value: string) => void;
   onSearch: () => void;
+  onSearchClear: () => void;
   onRefresh: () => void;
   onDeploy: () => void;
   onGoOnline: () => void;
   onGoOffline: () => void;
   onDelete: () => void;
+  onSelectedCollectorsChange?: (collectors: string[]) => void;
+  pageSizeOptions?: number[];
+  onPageIndexChange?: (pageIndex: number) => void;
+  onPageSizeChange?: (pageSize: number) => void;
+  onDeleteCancel?: () => void;
+  onDeleteConfirm?: () => void;
+  onDeployNameChange?: (value: string) => void;
+  onDeployGenerate?: () => void;
+  onDeployClose?: () => void;
+  onDeployCopy?: (value: string) => void;
   onRowGoOnline: (collector: string) => void;
   onRowGoOffline: (collector: string) => void;
   onRowDelete: (collector: string) => void;
@@ -38,14 +79,29 @@ const coldPrimaryButtonClassName =
 const coldIconButtonClassName =
   'h-8 w-8 min-w-0 rounded-[3px] border-[#2b3039] bg-[#101217] text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white disabled:opacity-40';
 
-function statusClassName(statusLabel: string) {
-  if (statusLabel.includes('在线') || statusLabel.toLowerCase().includes('online')) {
+const collectorActionMenuRootClassName = 'relative z-40 inline-block overflow-visible';
+const collectorActionMenuPanelBaseClassName =
+  'absolute top-9 z-[100] min-w-[132px] rounded-[3px] border border-[#2b3039] bg-[#101217] p-1 shadow-[0_18px_42px_rgba(0,0,0,0.42)]';
+
+function statusClassName(statusTone: 'success' | 'danger') {
+  if (statusTone === 'success') {
     return 'border-[#166534]/45 bg-[#0f2f23] text-[#86efac]';
   }
   return 'border-[#7f1d1d]/55 bg-[#2a1214] text-[#fca5a5]';
 }
 
-function EmptyTableRow({ colSpan }: { colSpan: number }) {
+function resolveCollectorTargetAction(target?: CollectorDeleteTarget | null): CollectorMutationAction {
+  return target?.action ?? 'delete';
+}
+
+function resolveCollectorConfirmTitle(target: CollectorDeleteTarget | null | undefined, t: Translator) {
+  const action = resolveCollectorTargetAction(target);
+  if (action === 'online') return t(target?.mode === 'batch' ? 'collector.confirm.online-batch' : 'collector.confirm.online');
+  if (action === 'offline') return t(target?.mode === 'batch' ? 'collector.confirm.offline-batch' : 'collector.confirm.offline');
+  return t(target?.mode === 'batch' ? 'common.confirm.delete-batch' : 'common.confirm.delete');
+}
+
+function EmptyTableRow({ colSpan, t }: { colSpan: number; t: Translator }) {
   return (
     <tr data-collector-empty-state="cold-table-empty" className="bg-[#0b0c0e]">
       <td colSpan={colSpan} className="h-[240px] px-3 py-10 text-center text-[#858d9a]">
@@ -53,7 +109,7 @@ function EmptyTableRow({ colSpan }: { colSpan: number }) {
           <span className="inline-flex h-10 w-10 items-center justify-center rounded-[4px] border border-[#303743] bg-[#101217] text-[#cbd5e1]">
             <Inbox className="h-5 w-5" aria-hidden="true" />
           </span>
-          <div className="text-[13px] font-semibold text-[#dbe4f0]">暂无数据</div>
+          <div className="text-[13px] font-semibold text-[#dbe4f0]">{t('common.no-data')}</div>
         </div>
       </td>
     </tr>
@@ -64,30 +120,99 @@ export function CollectorManageSurface({
   t,
   data,
   search,
+  selectedCollectors,
+  actionMessage,
+  actionError,
+  actionMeta,
+  actionTone,
+  actionFeedbackContract,
+  loadError,
+  deleteTarget,
+  isDeletePending,
+  isLoadPending,
+  deployOpen,
+  deployName,
+  deployIdentity,
+  deployHost,
+  deployDockerShell,
+  deployPackageShell,
+  deployError,
+  deployCopyMessage,
+  isDeployPending,
+  deployNameValidationVisible,
   formatTime,
   onSearchChange,
   onSearch,
+  onSearchClear,
   onRefresh,
   onDeploy,
   onGoOnline,
   onGoOffline,
   onDelete,
+  onSelectedCollectorsChange,
+  pageSizeOptions = [8, 15, 25],
+  onPageIndexChange,
+  onPageSizeChange,
+  onDeleteCancel,
+  onDeleteConfirm,
+  onDeployNameChange,
+  onDeployGenerate,
+  onDeployClose,
+  onDeployCopy,
   onRowGoOnline,
   onRowGoOffline,
   onRowDelete
 }: CollectorManageSurfaceProps) {
   const collectors = data.list.content ?? [];
   const rows = buildCollectorTableRows(collectors, t, formatTime);
-  const clusterHealth = buildCollectorClusterHealthEvidence(collectors, formatTime);
+  const clusterHealth = buildCollectorClusterHealthEvidence(collectors, formatTime, t);
+  const collectorNames = rows.map(row => row.name);
+  const immutableCollectorNames = rows.filter(row => !row.canMutate).map(row => row.name);
+  const resolvedSelectedCollectors = selectedCollectors ?? [];
+  const currentPageIndex = Math.max(0, data.list.pageIndex ?? 0);
+  const currentPageSize = Math.max(1, data.list.pageSize ?? pageSizeOptions[0] ?? 8);
+  const totalElements = data.list.totalElements || 0;
+  const totalPages = Math.max(1, Math.ceil(totalElements / currentPageSize));
+  const currentPage = Math.min(currentPageIndex + 1, totalPages);
+  const pageStart = totalElements === 0 || rows.length === 0 ? 0 : currentPageIndex * currentPageSize + 1;
+  const pageEnd = totalElements === 0 ? 0 : Math.min(totalElements, currentPageIndex * currentPageSize + rows.length);
+  const allSelected = collectorNames.length > 0 && collectorNames.every(name => resolvedSelectedCollectors.includes(name));
+  const actionFeedbackTitle = actionError ?? actionMessage ?? '';
+  const resolvedActionTone = actionTone ?? (actionError ? 'critical' : 'success');
+  const targetAction = resolveCollectorTargetAction(deleteTarget);
+  const mutationPending = Boolean(isDeletePending);
+  const tableLoading = Boolean(isLoadPending || mutationPending);
+  const resolvedActionFeedbackContract =
+    actionFeedbackContract ??
+    (resolvedActionTone === 'warning' ? 'angular-no-select-warning' : resolvedActionTone === 'critical' ? 'angular-delete-fail' : 'angular-delete-notify');
+  const [openRowActionMenuId, setOpenRowActionMenuId] = React.useState<string | null>(null);
+  const trimmedDeployName = deployName?.trim() ?? '';
+  const resolvedDeployIdentity = deployIdentity ?? '';
+  const showDeployNameValidation = Boolean(deployNameValidationVisible && !trimmedDeployName && !resolvedDeployIdentity);
+  const paginationSummary = t('collector.pagination.summary', {
+    page: currentPage,
+    totalPages,
+    from: pageStart,
+    to: pageEnd,
+    total: totalElements
+  });
+
+  function handlePageJumpChange(value: string) {
+    const parsed = Number.parseInt(value, 10);
+    if (!Number.isFinite(parsed)) return;
+    const nextPageIndex = Math.min(Math.max(parsed, 1), totalPages) - 1;
+    onPageIndexChange?.(nextPageIndex);
+  }
 
   return (
-    <div
-      data-collector-manage-surface="otlp-cold-collector-console"
-      data-collector-manage-style-baseline={coldCollectorVisual.canvasName}
-      className={coldCollectorVisual.canvas.root}
-      style={coldCollectorVisual.canvas.backgroundStyle}
-    >
-      <section className={coldCollectorVisual.layout.pageSection}>
+    <>
+      <div
+        data-collector-manage-surface="otlp-cold-collector-console"
+        data-collector-manage-style-baseline={coldCollectorVisual.canvasName}
+        className={coldCollectorVisual.canvas.root}
+        style={coldCollectorVisual.canvas.backgroundStyle}
+      >
+        <section className={coldCollectorVisual.layout.pageSection}>
         <div className="mx-auto max-w-[1480px]">
           <div className="mb-5">
             <div data-collector-header="cold-compact-header" className={coldCollectorVisual.panel.hero}>
@@ -96,26 +221,50 @@ export function CollectorManageSurface({
                   {t('menu.advanced.collector')}
                 </h1>
                 <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">
-                  管理已注册采集器节点、运行状态和任务分布,保持部署、搜索、上下线和删除操作在同一个高密度后台工作面中完成。
+                  {t('setting.collector.copy')}
                 </p>
                 <div data-collector-command-row="standard-equal-buttons" className={coldCollectorVisual.button.row}>
                   <Button size="sm" variant="default" className={coldButtonClassName} onClick={onRefresh}>
                     <RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />
                     {t('common.refresh')}
                   </Button>
-                  <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onDeploy}>
+                  <Button
+                    size="sm"
+                    variant="default"
+                    className={coldPrimaryButtonClassName}
+                    onClick={onDeploy}
+                    data-collector-deploy-open="angular-deploy-modal"
+                  >
                     <UploadCloud className="h-3.5 w-3.5" aria-hidden="true" />
                     {t('collector.deploy')}
                   </Button>
-                  <Button size="sm" variant="default" className={coldButtonClassName} onClick={onGoOnline}>
+                  <Button
+                    size="sm"
+                    variant="default"
+                    className={coldButtonClassName}
+                    onClick={onGoOnline}
+                    data-collector-online-selected="angular-batch-online-entry"
+                  >
                     <Power className="h-3.5 w-3.5" aria-hidden="true" />
                     {t('collector.online')}
                   </Button>
-                  <Button size="sm" variant="default" className={coldButtonClassName} onClick={onGoOffline}>
+                  <Button
+                    size="sm"
+                    variant="default"
+                    className={coldButtonClassName}
+                    onClick={onGoOffline}
+                    data-collector-offline-selected="angular-batch-offline-entry"
+                  >
                     <PowerOff className="h-3.5 w-3.5" aria-hidden="true" />
                     {t('collector.offline')}
                   </Button>
-                  <Button size="sm" variant="default" className={coldButtonClassName} onClick={onDelete}>
+                  <Button
+                    size="sm"
+                    variant="default"
+                    className={coldButtonClassName}
+                    onClick={onDelete}
+                    data-collector-delete-selected="angular-batch-delete-entry"
+                  >
                     <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
                     {t('collector.delete')}
                   </Button>
@@ -146,7 +295,55 @@ export function CollectorManageSurface({
             </div>
           </div>
 
-          <div data-collector-admin-layout="full-width-admin-list" className="space-y-5">
+          <div
+            data-collector-admin-layout="full-width-admin-list"
+            data-collector-delete-warning-contract="angular-no-select-warning"
+            data-collector-delete-confirm-contract="angular-modal-confirm"
+            data-collector-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"
+            data-collector-delete-feedback-contract="angular-delete-notify"
+            data-collector-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+            data-collector-delete-api-contract="angular-repeated-collectors-query"
+            data-collector-delete-page-clamp-contract="angular-update-page-index"
+            data-collector-delete-page-clamp-owner="route-state-contract"
+            data-collector-selection-reset-contract="angular-load-clears-selection"
+            data-collector-selection-reset-owner="route-state-contract"
+            data-collector-search-submit-contract="angular-enter-and-clear"
+            data-collector-search-clear-contract="angular-cleared-load"
+            data-collector-search-clear-owner="shared-search-row"
+            data-collector-operate-warning-contract="angular-no-select-online-offline"
+            data-collector-operate-confirm-contract="angular-modal-confirm"
+            data-collector-operate-confirm-contract-owner="hertzbeat-ui-confirm-dialog"
+            data-collector-operate-feedback-contract="angular-operate-notify"
+            data-collector-operate-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+            data-collector-operate-api-contract="angular-put-repeated-collectors-query"
+            data-collector-mutation-loading-contract="angular-nz-table-loading"
+            data-collector-load-failure-contract="angular-console-only-shell"
+            data-collector-load-failure-owner="collector-route-controller"
+            data-collector-load-failure={loadError ? 'angular-console-only-shell' : 'none'}
+            data-collector-deploy-modal-contract="angular-generate-identity-modal"
+            data-collector-deploy-api-contract="angular-post-generate-identity"
+            data-collector-deploy-failure-contract="angular-apply-fail-notification"
+            data-collector-deploy-validation-contract="angular-submit-marks-required"
+            data-collector-deploy-validation-raw-contract="angular-required-before-trim"
+            data-collector-deploy-validation-raw-owner="collector-route-controller"
+            data-collector-deploy-loading-contract="angular-nz-ok-loading"
+            data-collector-deploy-mask-contract="angular-mask-closable-false"
+            data-collector-deploy-width-contract="angular-width-45-percent"
+            data-collector-deploy-field-layout-contract="angular-label-7-control-12"
+            data-collector-deploy-field-layout-owner="route-form-field-grid"
+            data-collector-deploy-command-contract="angular-docker-package-shells"
+            data-collector-deploy-command-owner="hertzbeat-ui-code-editor"
+            data-collector-deploy-package-link-contract="angular-github-releases-link"
+            data-collector-deploy-copy-contract="angular-copy-identity"
+            data-collector-deploy-copy-feedback-contract="angular-copy-success-duration-800"
+            data-collector-deploy-result-close-contract="angular-danger-close-after-identity"
+            data-collector-deploy-result-close-owner="hertzbeat-ui-button"
+            data-collector-deploy-close-reset-contract="angular-close-clears-name"
+            data-collector-deploy-close-reset-owner="collector-route-controller"
+            data-collector-deploy-close-pending-result-contract="angular-close-ignores-late-generate"
+            data-collector-deploy-close-pending-result-owner="collector-route-controller"
+            className="space-y-5"
+          >
             <section className="min-w-0">
               <SearchRow
                 data-collector-toolbar="cold-table-toolbar"
@@ -154,18 +351,75 @@ export function CollectorManageSurface({
                 value={search}
                 placeholder={t('collector.name')}
                 searchLabel={t('common.search')}
+                clearLabel={t('common.clear')}
                 inputWidthClassName="w-[360px]"
                 onValueChange={onSearchChange}
                 onSearch={onSearch}
+                onClear={onSearchClear}
+                data-collector-search-submit-contract="angular-enter-and-clear"
+                data-collector-search-clear-contract="angular-cleared-load"
+                data-collector-search-clear-owner="shared-search-row"
+                trailingActions={
+                  <div className="text-[12px] font-semibold text-[#858d9a]">
+                    {t('setting.collector.selected-count', { count: resolvedSelectedCollectors.length })}
+                  </div>
+                }
               />
+              {actionFeedbackTitle ? (
+                <HzInlineFeedback
+                  tone={resolvedActionTone}
+                  title={actionFeedbackTitle}
+                  meta={actionMeta ?? undefined}
+                  variant="embedded"
+                  data-collector-action-feedback={resolvedActionTone}
+                  data-collector-action-feedback-contract={resolvedActionFeedbackContract}
+                  data-collector-delete-feedback={
+                    resolvedActionFeedbackContract.startsWith('angular-delete') || resolvedActionFeedbackContract === 'angular-no-select-warning'
+                      ? resolvedActionFeedbackContract
+                      : undefined
+                  }
+                  data-collector-delete-feedback-owner="hertzbeat-ui-inline-feedback"
+                  data-collector-delete-feedback-title={resolvedActionFeedbackContract === 'angular-delete-fail' ? 'common.notify.delete-fail' : undefined}
+                  data-collector-delete-feedback-detail={resolvedActionFeedbackContract === 'angular-delete-fail' && actionMeta ? 'backend-message' : undefined}
+                  data-collector-operate-feedback={
+                    resolvedActionFeedbackContract.startsWith('angular-operate') || resolvedActionFeedbackContract.startsWith('angular-no-select-o')
+                      ? resolvedActionFeedbackContract
+                      : undefined
+                  }
+                  data-collector-operate-feedback-owner="hertzbeat-ui-inline-feedback"
+                  data-collector-operate-feedback-title={resolvedActionFeedbackContract === 'angular-operate-fail' ? 'common.notify.operate-fail' : undefined}
+                  data-collector-operate-feedback-detail={resolvedActionFeedbackContract === 'angular-operate-fail' && actionMeta ? 'backend-message' : undefined}
+                />
+              ) : null}
 
               <div
                 data-collector-table-shell="cold-dense-table"
-                className="overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
+                data-collector-table-loading={tableLoading ? 'true' : 'false'}
+                data-collector-table-loading-contract="angular-load-collectors-table"
+                data-collector-table-loading-owner="angular-nz-table-loading"
+                data-collector-table-loading-scope="load-search-refresh-pagination-mutation"
+                data-collector-table-loading-source={tableLoading ? 'route-load-or-mutation' : 'idle'}
+                aria-busy={tableLoading ? 'true' : 'false'}
+                className="overflow-visible rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
               >
                 <table data-collector-manage-table="cold-collector-table" className="w-full table-fixed border-collapse text-left text-[12px] text-[#a9b0bb]">
                   <thead className="border-b border-[#252b34] bg-[#101217] text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">
                     <tr>
+                      <th className="w-[44px] px-3 py-2.5">
+                        <Checkbox
+                          data-collector-select-all="cold-checkbox"
+                          data-collector-select-all-contract="angular-header-includes-disabled-default"
+                          data-collector-selectable-names={collectorNames.join(',')}
+                          data-collector-immutable-names={immutableCollectorNames.join(',')}
+                          aria-label={t('common.select')}
+                          containerClassName="min-h-0"
+                          checked={allSelected}
+                          disabled={collectorNames.length === 0}
+                          onChange={event => {
+                            onSelectedCollectorsChange?.(event.target.checked ? collectorNames : []);
+                          }}
+                        />
+                      </th>
                       <th className="w-[18%] px-3 py-2.5">{t('collector.name')}</th>
                       <th className="w-[11%] px-3 py-2.5">{t('collector.status')}</th>
                       <th className="w-[14%] px-3 py-2.5">{t('collector.mode')}</th>
@@ -179,8 +433,29 @@ export function CollectorManageSurface({
                     </tr>
                   </thead>
                   <tbody>
-                    {rows.length > 0 ? rows.map(row => (
-                      <tr key={row.key} data-collector-row={row.key} className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]">
+                    {rows.length > 0 ? rows.map(row => {
+                      const rowActionMenuId = row.name;
+                      const isRowActionMenuOpen = openRowActionMenuId === rowActionMenuId;
+
+                      return (
+                        <tr key={row.key} data-collector-row={row.key} className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]">
+                          <td className="px-3 py-2.5">
+                            <Checkbox
+                              aria-label={t('common.select')}
+                              data-collector-row-select={row.name}
+                              data-collector-row-select-contract={row.canMutate ? 'angular-selectable' : 'angular-disabled-visual-only'}
+                              containerClassName="min-h-0"
+                              checked={row.canMutate && resolvedSelectedCollectors.includes(row.name)}
+                              disabled={!row.canMutate}
+                              onChange={event => {
+                                onSelectedCollectorsChange?.(
+                                  event.target.checked
+                                    ? [...resolvedSelectedCollectors, row.name]
+                                    : resolvedSelectedCollectors.filter(value => value !== row.name)
+                                );
+                              }}
+                            />
+                          </td>
                         <td className="px-3 py-2.5 font-semibold text-[#eef2f7]">
                           <div className="min-w-0">
                             <span className="inline-flex max-w-full items-center gap-1.5">
@@ -200,7 +475,10 @@ export function CollectorManageSurface({
                           </div>
                         </td>
                         <td className="px-3 py-2.5">
-                          <span className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${statusClassName(row.statusLabel)}`}>
+                          <span
+                            data-collector-status-tone={row.statusTone}
+                            className={`rounded-[3px] border px-2 py-0.5 text-[11px] font-semibold leading-4 ${statusClassName(row.statusTone)}`}
+                          >
                             {row.statusLabel}
                           </span>
                         </td>
@@ -223,8 +501,12 @@ export function CollectorManageSurface({
                                 variant="default"
                                 className={coldIconButtonClassName}
                                 title={t('collector.online')}
-                                onClick={() => onRowGoOnline(row.name)}
+                                onClick={() => {
+                                  setOpenRowActionMenuId(null);
+                                  onRowGoOnline(row.name);
+                                }}
                                 disabled={!row.canMutate}
+                                data-collector-online-one={row.name}
                               >
                                 <Power className="h-3.5 w-3.5" aria-hidden="true" />
                                 <span className="sr-only">{t('collector.online')}</span>
@@ -235,35 +517,322 @@ export function CollectorManageSurface({
                                 variant="default"
                                 className={coldIconButtonClassName}
                                 title={t('collector.offline')}
-                                onClick={() => onRowGoOffline(row.name)}
+                                onClick={() => {
+                                  setOpenRowActionMenuId(null);
+                                  onRowGoOffline(row.name);
+                                }}
                                 disabled={!row.canMutate}
+                                data-collector-offline-one={row.name}
                               >
                                 <PowerOff className="h-3.5 w-3.5" aria-hidden="true" />
                                 <span className="sr-only">{t('collector.offline')}</span>
                               </Button>
                             )}
-                            <Button
-                              size="icon"
-                              variant="default"
-                              className={coldIconButtonClassName}
-                              title={t('collector.delete')}
-                              onClick={() => onRowDelete(row.name)}
-                              disabled={!row.canMutate}
+                            <div
+                              className={collectorActionMenuRootClassName}
+                              data-collector-row-delete-menu={rowActionMenuId}
+                              data-collector-row-delete-menu-contract="angular-ellipsis-dropdown-delete"
+                              data-collector-row-delete-menu-layer="overlay-visible-above-panel"
+                              data-collector-row-delete-menu-clearance="floating-overlay-no-panel-crop"
+                              data-collector-row-delete-menu-open={isRowActionMenuOpen ? 'true' : 'false'}
                             >
-                              <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
-                              <span className="sr-only">{t('collector.delete')}</span>
-                            </Button>
+                              <button
+                                type="button"
+                                aria-expanded={isRowActionMenuOpen}
+                                aria-label={t('common.edit')}
+                                title={t('common.edit')}
+                                className={`${coldIconButtonClassName} inline-flex cursor-pointer list-none items-center justify-center [&::-webkit-details-marker]:hidden`}
+                                onClick={() => {
+                                  setOpenRowActionMenuId(current => (current === rowActionMenuId ? null : rowActionMenuId));
+                                }}
+                                data-collector-row-delete-menu-trigger={rowActionMenuId}
+                              >
+                                <MoreHorizontal className="h-3.5 w-3.5" aria-hidden="true" />
+                                <span className="sr-only">{t('common.edit')}</span>
+                              </button>
+                              <div
+                                role="menu"
+                                hidden={!isRowActionMenuOpen}
+                                className={`${collectorActionMenuPanelBaseClassName} right-0`}
+                                data-collector-row-delete-menu-panel={rowActionMenuId}
+                                data-collector-row-delete-menu-layer-panel="overlay-visible-above-panel"
+                                data-collector-row-delete-menu-clearance-panel="floating-overlay-no-panel-crop"
+                                data-collector-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"
+                                data-collector-row-delete-menu-panel-open={isRowActionMenuOpen ? 'true' : 'false'}
+                              >
+                                <HzTableRowActionButton
+                                  width="root-span"
+                                  intent="ghost"
+                                  onClick={() => {
+                                    setOpenRowActionMenuId(null);
+                                    onRowDelete(row.name);
+                                  }}
+                                  disabled={!row.canMutate}
+                                  data-collector-delete-one={row.name}
+                                  data-collector-delete-one-owner="hertzbeat-ui-table-row-action-button"
+                                  className="w-full text-[#fecaca] hover:text-white"
+                                >
+                                  <Trash2 className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
+                                  <span className="truncate">{t('collector.delete')}</span>
+                                </HzTableRowActionButton>
+                              </div>
+                            </div>
                           </div>
                         </td>
                       </tr>
-                    )) : <EmptyTableRow colSpan={10} />}
+                      );
+                    }) : <EmptyTableRow colSpan={11} t={t} />}
                   </tbody>
                 </table>
+                <div
+                  data-collector-pagination="cold-dense-pagination"
+                  data-collector-pagination-owner="hertzbeat-ui-pagination-bar"
+                  data-collector-pagination-contract="angular-search-pagination"
+                >
+                  <HzPaginationBar
+                    summary={paginationSummary}
+                    pageSizeLabel={t('collector.pagination.page-size')}
+                    pageSizeValue={String(currentPageSize)}
+                    pageSizeOptions={pageSizeOptions.map(value => ({ value: String(value), label: String(value) }))}
+                    pageJumpLabel={t('collector.pagination.page')}
+                    pageJumpValue={String(currentPage)}
+                    pageJumpMax={totalPages}
+                    previousLabel={t('common.previous-page')}
+                    nextLabel={t('common.next-page')}
+                    previousDisabled={currentPageIndex <= 0}
+                    nextDisabled={currentPage >= totalPages}
+                    onPrevious={() => onPageIndexChange?.(Math.max(currentPageIndex - 1, 0))}
+                    onNext={() => onPageIndexChange?.(Math.min(currentPageIndex + 1, totalPages - 1))}
+                    onPageSizeChange={value => onPageSizeChange?.(Number.parseInt(value, 10))}
+                    onPageJumpChange={handlePageJumpChange}
+                    pageJumpInputProps={
+                      {
+                        'data-collector-pagination-page-jump-owner': 'hertzbeat-ui-input'
+                      } as React.ComponentProps<typeof HzPaginationBar>['pageJumpInputProps']
+                    }
+                    pageSizeSelectProps={
+                      {
+                        'data-collector-pagination-page-size-owner': 'hertzbeat-ui-select'
+                      } as React.ComponentProps<typeof HzPaginationBar>['pageSizeSelectProps']
+                    }
+                    className="border-x-0"
+                  />
+                </div>
               </div>
             </section>
           </div>
         </div>
-      </section>
-    </div>
+        </section>
+      </div>
+      <HzConfirmDialog
+        open={Boolean(deleteTarget)}
+        tone="critical"
+        kicker={t('menu.advanced.collector')}
+        title={resolveCollectorConfirmTitle(deleteTarget, t)}
+        cancelLabel={t('common.button.cancel')}
+        confirmLabel={t('common.button.ok')}
+        confirmDisabled={mutationPending}
+        onCancel={onDeleteCancel ?? (() => {})}
+        onConfirm={onDeleteConfirm ?? (() => {})}
+        data-collector-delete-confirm="angular-modal-confirm"
+        data-collector-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+        data-collector-operate-confirm={targetAction === 'online' || targetAction === 'offline' ? 'angular-modal-confirm' : undefined}
+        data-collector-operate-confirm-owner="hertzbeat-ui-confirm-dialog"
+        confirmButtonProps={{
+          'data-collector-delete-confirm-submit': 'angular-modal-confirm',
+          'data-collector-operate-confirm-submit': targetAction === 'online' || targetAction === 'offline' ? 'angular-modal-confirm' : undefined
+        } as React.ComponentProps<typeof HzConfirmDialog>['confirmButtonProps']}
+      >
+        <div data-collector-delete-confirm-target={deleteTarget?.label ?? ''}>
+          {deleteTarget?.label ?? t('common.none')}
+        </div>
+      </HzConfirmDialog>
+      {deployOpen ? (
+        <div
+          className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6"
+          data-collector-deploy-dialog="angular-generate-identity-modal"
+          data-collector-deploy-dialog-owner="collector-manage-surface"
+          data-collector-deploy-mask-closable="angular-mask-closable-false"
+          data-collector-deploy-mask-closable-owner="angular-nz-modal"
+          data-collector-deploy-close-reset="angular-close-clears-name"
+          data-collector-deploy-close-reset-owner="collector-route-controller"
+          data-collector-deploy-close-pending-result="angular-close-ignores-late-generate"
+          data-collector-deploy-close-pending-result-owner="collector-route-controller"
+        >
+          <section
+            className="grid max-h-[88vh] w-[min(92vw,920px)] overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_28px_70px_rgba(0,0,0,0.48)] md:w-[45vw] md:max-w-[45vw]"
+            data-collector-deploy-width="angular-width-45-percent"
+            data-collector-deploy-width-owner="angular-nz-modal"
+          >
+            <header className="grid min-h-[56px] grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-b border-[#252b34] bg-[#101217] px-5">
+              <div className="min-w-0">
+                <div className="text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">{t('menu.advanced.collector')}</div>
+                <h2 className="truncate text-[18px] font-semibold text-[#eef2f7]">{t('collector.deploy')}</h2>
+              </div>
+              <button
+                type="button"
+                className="inline-flex h-8 w-8 items-center justify-center rounded-[3px] border border-[#2b3039] bg-[#101217] text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151b28]"
+                onClick={onDeployClose}
+                data-collector-deploy-close="angular-modal-cancel"
+                data-collector-deploy-close-reset="angular-close-clears-name"
+                data-collector-deploy-close-reset-owner="collector-route-controller"
+                aria-label={t('collector.deploy.close')}
+              >
+                <X className="h-4 w-4" aria-hidden="true" />
+              </button>
+            </header>
+            <div className="min-h-0 overflow-y-auto p-5">
+              <div
+                className="grid gap-2 md:grid-cols-[7fr_12fr] md:items-start"
+                data-collector-deploy-field="name"
+                data-collector-deploy-field-layout="angular-label-7-control-12"
+              >
+                <label
+                  htmlFor="collector-deploy-name"
+                  className="pt-2 text-[12px] font-semibold text-[#dbe4f0] md:text-right"
+                  data-collector-deploy-label-span="7"
+                >
+                  {t('collector.name')} <span className="text-[#f97316]">*</span>
+                </label>
+                <div className="min-w-0" data-collector-deploy-control-span="12">
+                  <input
+                    id="collector-deploy-name"
+                    value={deployName ?? ''}
+                    disabled={Boolean(resolvedDeployIdentity)}
+                    onChange={event => onDeployNameChange?.(event.target.value)}
+                    placeholder={t('collector.name.placeholder')}
+                    className="h-8 w-full rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] text-[#dbe4f0] outline-none transition placeholder:text-[#6f7788] focus:border-[#4e74f8] focus:ring-2 focus:ring-[#4e74f8]/10 disabled:opacity-60"
+                    data-collector-deploy-name-input="angular-required-name"
+                    data-collector-deploy-name-validation-raw="angular-required-before-trim"
+                    data-collector-deploy-name-validation-raw-owner="collector-route-controller"
+                    data-collector-deploy-name-validation-state={showDeployNameValidation ? 'dirty-invalid' : 'pristine'}
+                    data-collector-deploy-name-lock={resolvedDeployIdentity ? 'angular-disable-after-identity' : 'editable-before-identity'}
+                    data-collector-deploy-name-lock-owner="angular-ngmodel-input"
+                    data-collector-deploy-name-disabled={resolvedDeployIdentity ? 'true' : 'false'}
+                  />
+                  {showDeployNameValidation ? (
+                    <div
+                      className="mt-1 text-[11px] font-medium text-[#fca5a5]"
+                      data-collector-deploy-name-validation="validation.required"
+                      data-collector-deploy-name-validation-contract="angular-submit-marks-required"
+                    >
+                      {t('validation.required')}
+                    </div>
+                  ) : null}
+                </div>
+              </div>
+              {deployError ? (
+                <div className="mt-4">
+                  <HzInlineFeedback
+                    tone="critical"
+                    title={t('common.notify.apply-fail')}
+                    meta={deployError}
+                    variant="embedded"
+                    data-collector-deploy-feedback="angular-apply-fail-notification"
+                    data-collector-deploy-feedback-owner="hertzbeat-ui-inline-feedback"
+                    data-collector-deploy-feedback-title="common.notify.apply-fail"
+                    data-collector-deploy-feedback-detail="backend-message"
+                  />
+                </div>
+              ) : null}
+              {deployCopyMessage ? (
+                <div className="mt-4">
+                  <HzInlineFeedback
+                    tone="success"
+                    title={deployCopyMessage}
+                    variant="embedded"
+                    data-collector-deploy-copy-feedback="angular-copy-success"
+                    data-collector-deploy-copy-feedback-owner="hertzbeat-ui-inline-feedback"
+                    data-collector-deploy-copy-feedback-duration-ms="800"
+                  />
+                </div>
+              ) : null}
+              {resolvedDeployIdentity ? (
+                <div className="mt-5 grid gap-4" data-collector-deploy-result="angular-identity-shells">
+                  <div className="grid gap-2 text-center">
+                    <div className="text-[14px] font-semibold text-[#eef2f7]">{t('collector.deploy.identity')}</div>
+                    <div className="text-[12px] text-[#858d9a]">{t('collector.deploy.identity.tip')}</div>
+                    <button
+                      type="button"
+                      className="mx-auto inline-flex max-w-full items-center gap-2 rounded-[3px] border border-[#553d1f] bg-[#241711] px-3 py-2 font-mono text-[12px] font-semibold text-[#fed7aa] hover:border-[#f97316]"
+                      onClick={() => onDeployCopy?.(resolvedDeployIdentity)}
+                      data-collector-deploy-copy-identity="angular-copy-identity"
+                      title={t('common.button.copy.tip')}
+                    >
+                      <span className="truncate">{resolvedDeployIdentity}</span>
+                      <Copy className="h-3.5 w-3.5" aria-hidden="true" />
+                      <span>{t('common.copy.button')}</span>
+                    </button>
+                  </div>
+                  <div className="grid gap-4 lg:grid-cols-2">
+                    <HzCodeEditor
+                      readOnly
+                      language="shell"
+                      value={deployDockerShell ?? ''}
+                      title={t('collector.deploy.docker')}
+                      meta={deployHost ?? 'MANAGER_HOST'}
+                      minHeight="240px"
+                      data-collector-deploy-docker-shell="angular-docker-shell"
+                      data-collector-deploy-code-owner="hertzbeat-ui-code-editor"
+                    />
+                    <HzCodeEditor
+                      readOnly
+                      language="yaml"
+                      value={deployPackageShell ?? ''}
+                      title={t('collector.deploy.package')}
+                      meta={
+                        <a
+                          href="https://github.com/apache/hertzbeat/releases"
+                          target="_blank"
+                          rel="noreferrer"
+                          data-collector-deploy-package-link="angular-github-releases-link"
+                          data-collector-deploy-package-link-owner="angular-nz-modal"
+                        >
+                          {t('collector.deploy.package.github')}
+                        </a>
+                      }
+                      minHeight="260px"
+                      data-collector-deploy-package-shell="angular-package-shell"
+                      data-collector-deploy-code-owner="hertzbeat-ui-code-editor"
+                    />
+                  </div>
+                </div>
+              ) : null}
+            </div>
+            <footer className="flex justify-end gap-2 border-t border-[#252b34] bg-[#101217] px-5 py-3">
+              {resolvedDeployIdentity ? (
+                <HzButton
+                  size="md"
+                  intent="danger"
+                  onClick={onDeployClose}
+                  data-collector-deploy-result-close="angular-close-saved"
+                  data-collector-deploy-result-close-contract="angular-danger-close-after-identity"
+                  data-collector-deploy-result-close-owner="hertzbeat-ui-button"
+                  data-collector-deploy-result-close-reset="angular-close-clears-name"
+                  data-collector-deploy-result-close-reset-owner="collector-route-controller"
+                >
+                  {t('collector.deploy.close')}
+                </HzButton>
+              ) : (
+                <Button
+                  size="sm"
+                  variant="default"
+                  className={coldPrimaryButtonClassName}
+                  onClick={onDeployGenerate}
+                  disabled={Boolean(isDeployPending || resolvedDeployIdentity)}
+                  data-collector-deploy-generate="angular-generate-identity"
+                  data-collector-deploy-generate-validation="angular-click-before-required"
+                  data-collector-deploy-ok-loading={isDeployPending ? 'true' : 'false'}
+                  data-collector-deploy-ok-loading-owner="angular-nz-ok-loading"
+                  aria-busy={isDeployPending ? 'true' : 'false'}
+                >
+                  {isDeployPending ? t('common.loading') : t('collector.deploy.ok')}
+                </Button>
+              )}
+            </footer>
+          </section>
+        </div>
+      ) : null}
+    </>
   );
 }
diff --git a/web-next/components/pages/label-manage-surface.test.tsx b/web-next/components/pages/label-manage-surface.test.tsx
index 4a5b00d029..8665493263 100644
--- a/web-next/components/pages/label-manage-surface.test.tsx
+++ b/web-next/components/pages/label-manage-surface.test.tsx
@@ -4,10 +4,10 @@ import { resolve } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
+import { LabelManageSurface } from './label-manage-surface';
 
 describe('label manage surface', () => {
-  it('renders the OTLP cold-matte label console, dense table, and authoring dialog shell', async () => {
-    const { LabelManageSurface } = await import('./label-manage-surface');
+  it('renders the OTLP cold-matte label console, Angular card grid, and authoring dialog shell', () => {
     const t = createTranslatorMock({
       locale: 'zh-CN',
       overrides: {
@@ -15,11 +15,13 @@ describe('label manage surface', () => {
         'common.refresh': '刷新',
         'common.button.new': '新增',
         'common.search': '搜索',
+        'common.clear': '清除',
         'common.copy.button': '复制',
         'common.button.edit': '编辑',
         'common.button.delete': '删除选中项',
         'common.button.cancel': '取消',
         'common.button.save': '保存',
+        'common.none': '无标签预览',
         'label.search': '搜索标签',
         'label.new': '新增标签',
         'label.edit': '编辑标签',
@@ -50,9 +52,17 @@ describe('label manage surface', () => {
                 id: 2,
                 name: 'source',
                 tagValue: 'system',
-                description: 'preset label',
+                description: '',
                 type: 2,
                 gmtUpdate: '2026-04-09T10:00:00Z'
+              },
+              {
+                id: 3,
+                name: 'legacy',
+                tagValue: 'custom',
+                description: 'unknown type',
+                type: 99,
+                gmtUpdate: '2026-04-08T10:00:00Z'
               }
             ],
             totalElements: 2,
@@ -69,14 +79,21 @@ describe('label manage surface', () => {
           type: 1
         }}
         isManageModalAdd
+        deleteTarget={null}
+        isDeletePending={false}
+        isSavePending
+        actionMessage="新增成功"
         formatTime={() => '2026-04-10 18:00:00'}
         onSearchChange={() => {}}
         onSearch={() => {}}
+        onSearchClear={() => {}}
         onRefresh={() => {}}
         onNew={() => {}}
         onCopy={() => {}}
         onEdit={() => {}}
-        onDelete={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
         onDraftChange={() => {}}
         onCloseDialog={() => {}}
         onSaveDialog={() => {}}
@@ -88,32 +105,144 @@ describe('label manage surface', () => {
     expect(html).toContain('data-label-header="cold-compact-header"');
     expect(html).toContain('data-label-command-row="standard-equal-buttons"');
     expect(html).toContain('data-label-admin-layout="full-width-admin-list"');
+    expect(html).toContain('data-label-save-feedback-contract="angular-new-edit-notify"');
+    expect(html).toContain('data-label-save-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-save-failure-contract="angular-new-edit-fail-notification"');
+    expect(html).toContain('data-label-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-label-load-failure-owner="label-route-controller"');
+    expect(html).toContain('data-label-load-failure="none"');
+    expect(html).toContain('data-label-copy-feedback-contract="angular-copy-notify"');
+    expect(html).toContain('data-label-copy-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-delete-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-label-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-label-delete-confirm-state-contract="angular-modal-confirm-state"');
+    expect(html).toContain('data-label-delete-confirm-state-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-label-delete-confirm-state="closed"');
+    expect(html).toContain('data-label-delete-confirm-state-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-label-delete-confirm-pending="false"');
+    expect(html).toContain('data-label-delete-feedback-contract="angular-delete-notify"');
+    expect(html).toContain('data-label-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-delete-failure-contract="angular-delete-fail-notification"');
+    expect(html).toContain('data-label-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-delete-query-contract="angular-repeated-ids-query"');
+    expect(html).toContain('data-label-delete-query-owner="route-mutation-contract"');
+    expect(html).toContain('data-label-card-loading-contract="angular-table-loading"');
+    expect(html).toContain('data-label-card-loading-owner="label-route-controller"');
+    expect(html).toContain('data-label-card-loading="false"');
+    expect(html).toContain('data-label-save-loading-contract="angular-nz-ok-loading"');
+    expect(html).toContain('data-label-save-loading-contract-owner="angular-nz-ok-loading"');
+    expect(html).toContain('data-label-dialog-mask-closable-contract="angular-mask-closable-false"');
+    expect(html).toContain('data-label-dialog-mask-closable-owner="angular-nz-modal"');
+    expect(html).toContain('data-label-dialog-width-contract="angular-width-30-percent"');
+    expect(html).toContain('data-label-dialog-width-owner="angular-nz-modal"');
+    expect(html).toContain('data-label-dialog-field-layout-contract="angular-label-7-control-12"');
+    expect(html).toContain('data-label-dialog-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-label-dialog-preview-visibility-contract="angular-name-defined-preview"');
+    expect(html).toContain('data-label-dialog-preview-visibility-owner="route-form-state"');
+    expect(html).toContain('data-label-dialog-preview-frame-contract="angular-inline-tag-no-extra-frame"');
+    expect(html).toContain('data-label-dialog-preview-frame-owner="route-form-field-grid"');
+    expect(html).toContain('data-label-dialog-preview-chrome-contract="angular-tag-only-no-extra-frame"');
+    expect(html).toContain('data-label-dialog-preview-chrome-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-label-edit-reference-contract="angular-edit-direct-reference"');
+    expect(html).toContain('data-label-edit-reference-owner="route-form-state"');
+    expect(html).toContain('data-label-name-validation-contract="angular-required-before-submit"');
+    expect(html).toContain('data-label-name-validation-trigger-contract="angular-ok-marks-dirty"');
+    expect(html).toContain('data-label-name-validation-trigger-owner="route-form-contract"');
+    expect(html).toContain('data-label-name-validation-raw-contract="angular-required-before-trim"');
+    expect(html).toContain('data-label-name-validation-raw-owner="route-form-contract"');
+    expect(html).toContain('data-label-query-contract="angular-load-all-labels"');
+    expect(html).toContain('data-label-query-owner="label-query-state"');
+    expect(html).toContain('data-label-query-param-order-contract="angular-page-index-size-type-search"');
+    expect(html).toContain('data-label-query-param-order-owner="label-query-state"');
+    expect(html).toContain('data-label-card-grid-contract="angular-card-grid"');
+    expect(html).toContain('data-label-card-grid-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('aria-busy="false"');
+    expect(html).toContain('data-label-monitor-handoff-contract="angular-routerlink-monitors-labels"');
+    expect(html).toContain('data-label-monitor-handoff-owner="next-monitor-query-link"');
+    expect(html).toContain('data-hz-ui="label-tag"');
+    expect(html).toContain('data-hz-label-tag-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-hz-label-color-token="geekblue"');
+    expect(html).toContain('data-label-color-token="geekblue"');
+    expect(html).toContain('data-label-color-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-label-dialog-preview-color="angular-render-label-color"');
+    expect(html).toContain('data-label-dialog-preview-chrome="angular-nz-tag-inline"');
+    expect(html).toContain('data-label-dialog-preview-chrome-mode="angular-tag-only-no-extra-frame"');
+    expect(html).toContain('data-overlay-dialog-mask-closable="false"');
+    expect(html).toContain('data-label-dialog-mask-closable="false"');
+    expect(html).toContain('data-label-dialog-width="angular-width-30-percent"');
+    expect(html).toContain('data-label-dialog-field-layout="angular-label-7-control-12"');
+    expect(html).toContain('data-label-dialog-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-label-dialog-field-layout-row="angular-label-7-control-12"');
+    expect(html).toContain('data-label-dialog-label-span="7"');
+    expect(html).toContain('data-label-dialog-control-span="12"');
+    expect(html).toContain('sm:grid-cols-[minmax(96px,7fr)_minmax(0,12fr)]');
+    expect(html).toContain('lg:max-w-[30vw]');
+    expect(html).toContain('data-label-save-ok-loading="true"');
+    expect(html).toContain('data-label-save-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('aria-busy="true"');
     expect(html).toContain('data-label-toolbar="cold-table-toolbar"');
     expect(html).toContain('data-label-search-owner="shared-search-row"');
+    expect(html).toContain('data-label-search-submit="angular-enter-and-clear"');
+    expect(html).toContain('data-label-search-submit-owner="cold-search-row"');
+    expect(html).toContain('data-label-action-feedback="angular-action-success"');
+    expect(html).toContain('data-label-action-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-save-feedback="angular-save-success"');
+    expect(html).toContain('data-label-save-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-save-trim-contract="angular-trim-before-save"');
+    expect(html).toContain('data-label-save-trim-owner="label-manage-controller"');
+    expect(html).toContain('data-label-optional-save-payload-contract="angular-preserve-undefined-optional-fields"');
+    expect(html).toContain('data-label-optional-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-label-type-save-payload-contract="angular-preserve-implicit-type"');
+    expect(html).toContain('data-label-type-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-label-display-format-contract="angular-format-label-name-raw-tag-value"');
+    expect(html).toContain('data-label-display-format-owner="label-view-model"');
+    expect(html).toContain('data-label-description-display-contract="angular-truthy-description-raw"');
+    expect(html).toContain('data-label-description-display-owner="label-manage-surface"');
+    expect(html).toContain('data-hz-ui="inline-feedback"');
+    expect(html).toContain('data-hz-feedback-tone="success"');
+    expect(html).toContain('新增成功');
     expect(html).toContain('data-cold-search-row-owner="cold-search-row"');
     expect(html).toContain('data-cold-search-input="fixed-width-direct"');
     expect(html).toContain('data-cold-search-control="direct-input"');
     expect(html).toContain('data-cold-search-chrome="no-extra-input-shell"');
     expect(html).toContain('data-cold-search-action="submit"');
-    expect(html).toContain('data-label-table-shell="cold-dense-table"');
-    expect(html).toContain('data-label-table="cold-label-table"');
+    expect(html).toContain('data-cold-search-action="clear"');
+    expect(html).toContain('data-label-card-grid="angular-card-grid"');
+    expect(html).toContain('data-label-card-grid-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-label-card-column-contract="nz-xs-12-sm-8-md-6-lg-4"');
     expect(html).toContain('data-label-row="1"');
-    expect(html).toContain('data-label-row-actions="cold-icon-actions"');
+    expect(html).toContain('data-label-card-shell="angular-card"');
+    expect(html).toContain('data-label-card-size="small"');
+    expect(html).toContain('data-label-card-content="angular-label-content"');
+    expect(html).toContain('data-label-card-description="angular-description"');
+    expect(html).toContain('data-label-card-description-display="angular-truthy-description-raw"');
+    expect(html).toContain('data-label-card-description-display-owner="label-manage-surface"');
+    expect(html).toContain('data-label-card-actions="angular-card-actions"');
+    expect(html).toContain('data-label-monitor-handoff="angular-routerlink-monitors-labels"');
+    expect(html).toContain('data-label-monitor-handoff-query="team:ops"');
+    expect(html).toContain('data-label-monitor-handoff-owner="next-monitor-query-link"');
+    expect(html).toContain('data-label-row-actions="angular-card-actions-contextual"');
+    expect(html).toContain('data-label-row-actions-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-label-row-action="copy"');
+    expect(html).toContain('data-label-row-action="edit"');
+    expect(html).toContain('data-label-row-action="delete"');
+    expect(html).toContain('data-label-row-action-owner="row-contextual-icon-button"');
+    expect(html).toContain('data-label-row-action-label="team:ops"');
     expect(html).not.toContain('data-label-summary-rail=');
     expect(html).not.toContain('data-cold-search-input-shell');
     expect(html).toContain('标签管理');
     expect(html).toContain('新增');
     expect(html).toContain('搜索标签');
     expect(html).toContain('搜索');
-    expect(html).toContain('aria-label="复制"');
-    expect(html).toContain('aria-label="编辑"');
-    expect(html).toContain('aria-label="删除选中项"');
+    expect(html).toContain('aria-label="复制标签 team:ops"');
+    expect(html).toContain('aria-label="编辑标签 team:ops"');
+    expect(html).toContain('aria-label="删除标签 team:ops"');
+    expect(html).toContain('href="/monitors?labels=team%3Aops"');
     expect(html).toContain('team:ops');
     expect(html).toContain('source:system');
     expect(html).toContain('ops team');
-    expect(html).toContain('preset label');
-    expect(html).toContain('用户标签');
-    expect(html).toContain('预置标签');
+    expect(html).not.toContain('用户标签');
     expect(html).toContain('data-overlay-dialog="true"');
     expect(html).toContain('新增标签');
     expect(html).toContain('标签名');
@@ -122,13 +251,11 @@ describe('label manage surface', () => {
     expect(html).toContain('效果');
     expect(html).toContain('取消');
     expect(html).toContain('保存');
-    expect(html).not.toContain('data-label-manage-route="angular-label-cards"');
-    expect(html).not.toContain('data-label-card-shell="angular-card"');
+    expect(html).not.toContain('data-label-table="cold-label-table"');
     expect(html).not.toContain('setting.labels.subtitle');
-  });
+  }, 30000);
 
-  it('renders a cold table empty state with Chinese fallback copy when no labels exist', async () => {
-    const { LabelManageSurface } = await import('./label-manage-surface');
+  it('renders a cold card empty state with Chinese fallback copy when no labels exist', () => {
     const t = createTranslatorMock({
       locale: 'zh-CN',
       overrides: {
@@ -136,6 +263,7 @@ describe('label manage surface', () => {
         'common.refresh': '刷新',
         'common.button.new': '新增',
         'common.search': '搜索',
+        'common.clear': '清除',
         'label.search': '搜索标签'
       }
     });
@@ -154,21 +282,26 @@ describe('label manage surface', () => {
         search=""
         draftLabel={null}
         isManageModalAdd={false}
+        deleteTarget={null}
+        isDeletePending={false}
         formatTime={() => '-'}
         onSearchChange={() => {}}
         onSearch={() => {}}
+        onSearchClear={() => {}}
         onRefresh={() => {}}
         onNew={() => {}}
         onCopy={() => {}}
         onEdit={() => {}}
-        onDelete={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
         onDraftChange={() => {}}
         onCloseDialog={() => {}}
         onSaveDialog={() => {}}
       />
     );
 
-    expect(html).toContain('data-label-empty-state="cold-table-empty"');
+    expect(html).toContain('data-label-empty-state="cold-card-empty"');
     expect(html).toContain('data-label-empty-icon="cold-empty-box"');
     expect(html).toContain('暂无标签');
     expect(html).toContain('请先新增标签。');
@@ -176,22 +309,724 @@ describe('label manage surface', () => {
     expect(html).not.toContain('setting.labels.empty.copy');
   });
 
+  it('keeps Angular formatLabelName raw tag value spacing for display, copy, and monitor handoff', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [
+              {
+                id: 9,
+                name: 'team',
+                tagValue: ' ops ',
+                description: '',
+                type: 1
+              }
+            ],
+            totalElements: 1,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        draftLabel={null}
+        isManageModalAdd={false}
+        deleteTarget={null}
+        isDeletePending={false}
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-display-format-contract="angular-format-label-name-raw-tag-value"');
+    expect(html).toContain('data-label-monitor-handoff-query="team: ops "');
+    expect(html).toContain('data-label-row-action-label="team: ops "');
+    expect(html).toContain('href="/monitors?labels=team%3A%20ops%20"');
+    expect(html).toContain('team: ops ');
+    expect(html).not.toContain('team:ops');
+  });
+
+  it('renders whitespace-only descriptions because Angular only checks description truthiness', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [
+              {
+                id: 10,
+                name: 'team',
+                tagValue: 'ops',
+                description: '   ',
+                type: 1
+              }
+            ],
+            totalElements: 1,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        draftLabel={null}
+        isManageModalAdd={false}
+        deleteTarget={null}
+        isDeletePending={false}
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-description-display-contract="angular-truthy-description-raw"');
+    expect(html).toContain('data-label-description-display-owner="label-manage-surface"');
+    expect(html).toContain('data-label-card-description="angular-description"');
+    expect(html).toContain('data-label-card-description-display="angular-truthy-description-raw"');
+    expect(html).toContain('data-label-card-description-display-owner="label-manage-surface"');
+    expect(html).toContain('title="   "');
+  });
+
+  it('keeps the new-label dialog preview hidden until the Angular name-defined path runs', () => {
+    const t = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'menu.advanced.labels': '标签管理',
+        'common.refresh': '刷新',
+        'common.button.new': '新增',
+        'common.search': '搜索',
+        'common.button.cancel': '取消',
+        'common.button.save': '保存',
+        'common.none': '无标签预览',
+        'label.search': '搜索标签',
+        'label.new': '新增标签',
+        'label.edit': '编辑标签',
+        'label.name': '标签名',
+        'label.value': '标签值',
+        'label.description': '标签描述',
+        'label.display': '效果',
+        'setting.labels.empty.title': '暂无标签',
+        'setting.labels.empty.copy': '请先新增标签。'
+      }
+    });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        draftLabel={{
+          id: 0,
+          name: undefined as unknown as string,
+          tagValue: undefined,
+          description: undefined,
+          type: 1
+        }}
+        isManageModalAdd
+        deleteTarget={null}
+        isDeletePending={false}
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-dialog-preview-visibility="hidden"');
+    expect(html).toContain('data-label-dialog-preview-visibility-owner="route-form-state"');
+    expect(html).not.toContain('data-label-dialog-preview="cold-preview"');
+    expect(html).toContain('data-label-dialog-name-input="angular-required-name"');
+    expect(html).toContain('data-label-dialog-name-state="pristine"');
+    expect(html).toContain('data-label-save-submit-state="pristine"');
+    expect(html).not.toContain('data-label-dialog-name-validation="validation.required"');
+    expect(html).toContain('data-label-save-submit="angular-required-before-submit"');
+    expect(html).toContain('data-label-name-validation-trigger="angular-ok-marks-dirty"');
+    expect(html).toContain('data-label-name-validation-trigger-owner="route-form-contract"');
+    expect(html).not.toContain('disabled=""');
+    expect(html).toContain('data-label-save-trim="angular-trim-before-save"');
+    expect(html).toContain('data-label-save-trim-owner="label-manage-controller"');
+    expect(html).toContain('data-label-optional-save-payload-contract="angular-preserve-undefined-optional-fields"');
+    expect(html).toContain('data-label-optional-save-payload-owner="label-manage-controller"');
+    expect(html).toContain('data-label-type-save-payload-contract="angular-preserve-implicit-type"');
+    expect(html).toContain('data-label-type-save-payload-owner="label-manage-controller"');
+    expect(html).not.toContain('无标签预览');
+  });
+
+  it('shows the Angular display preview row once the label name is defined', () => {
+    const t = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'menu.advanced.labels': '标签管理',
+        'common.refresh': '刷新',
+        'common.button.new': '新增',
+        'common.search': '搜索',
+        'common.button.cancel': '取消',
+        'common.button.save': '保存',
+        'common.none': '无标签预览',
+        'label.search': '搜索标签',
+        'label.new': '新增标签',
+        'label.edit': '编辑标签',
+        'label.name': '标签名',
+        'label.value': '标签值',
+        'label.description': '标签描述',
+        'label.display': '效果',
+        'setting.labels.empty.title': '暂无标签',
+        'setting.labels.empty.copy': '请先新增标签。'
+      }
+    });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        draftLabel={{ id: 0, name: 'team', tagValue: '', description: '', type: 1 }}
+        isManageModalAdd
+        deleteTarget={null}
+        isDeletePending={false}
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-dialog-preview="cold-preview"');
+    expect(html).toContain('data-label-dialog-preview-visibility="visible"');
+    expect(html).toContain('data-label-dialog-preview-frame="angular-inline-tag-no-extra-frame"');
+    expect(html).toContain('data-label-dialog-preview-frame-owner="route-form-field-grid"');
+    expect(html).toContain('data-label-dialog-preview-chrome="angular-nz-tag-inline"');
+    expect(html).toContain('data-label-dialog-preview-chrome-mode="angular-tag-only-no-extra-frame"');
+    expect(html).toContain('data-label-dialog-preview-chrome-owner="hertzbeat-ui-label-tag"');
+    expect(html).toContain('data-label-dialog-field="display"');
+    expect(html).toContain('data-label-dialog-label-span="7"');
+    expect(html).toContain('data-label-dialog-control-span="12"');
+    expect(html).not.toContain('data-label-dialog-preview=&quot;cold-preview&quot; data-label-dialog-preview-visibility=&quot;visible&quot; data-label-dialog-preview-visibility-owner=&quot;route-form-state&quot; data-label-dialog-field=&quot;display&quot; data-label-dialog-field-layout-row=&quot;angular-label-7-control-12&quot; class=&quot;grid gap-2 text-[12px] font-semibold text-[#a9b0bb] sm:grid-cols-[minmax(96px,7fr)_minmax(0,12fr)] sm:items-start rounded-[4px] border');
+    expect(html).toContain('team');
+  });
+
+  it('marks the missing label name dirty only after the Angular OK-submit validation path runs', () => {
+    const t = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'menu.advanced.labels': '标签管理',
+        'common.refresh': '刷新',
+        'common.button.new': '新增',
+        'common.search': '搜索',
+        'common.button.cancel': '取消',
+        'common.button.save': '保存',
+        'common.none': '无标签预览',
+        'validation.required': '必填项',
+        'label.search': '搜索标签',
+        'label.new': '新增标签',
+        'label.name': '标签名',
+        'label.value': '标签值',
+        'label.description': '标签描述',
+        'label.display': '效果',
+        'setting.labels.empty.title': '暂无标签',
+        'setting.labels.empty.copy': '请先新增标签。'
+      }
+    });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        draftLabel={{ id: 0, name: '', tagValue: '', description: '', type: 1 }}
+        isManageModalAdd
+        isNameValidationVisible
+        deleteTarget={null}
+        isDeletePending={false}
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-dialog-name-state="dirty-invalid"');
+    expect(html).toContain('data-label-save-submit-state="dirty-invalid"');
+    expect(html).toContain('data-label-dialog-name-validation="validation.required"');
+    expect(html).toContain('必填项');
+    expect(html).not.toContain('disabled=""');
+  });
+
+  it('does not mark a whitespace-only label name invalid before Angular trim-before-save runs', () => {
+    const t = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'menu.advanced.labels': '标签管理',
+        'common.refresh': '刷新',
+        'common.button.new': '新增',
+        'common.search': '搜索',
+        'common.button.cancel': '取消',
+        'common.button.save': '保存',
+        'common.none': '无标签预览',
+        'validation.required': '必填项',
+        'label.search': '搜索标签',
+        'label.new': '新增标签',
+        'label.name': '标签名',
+        'label.value': '标签值',
+        'label.description': '标签描述',
+        'label.display': '效果',
+        'setting.labels.empty.title': '暂无标签',
+        'setting.labels.empty.copy': '请先新增标签。'
+      }
+    });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        draftLabel={{ id: 0, name: '   ', tagValue: undefined, description: undefined, type: 1 } as any}
+        isManageModalAdd
+        isNameValidationVisible
+        deleteTarget={null}
+        isDeletePending={false}
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-name-validation-raw-contract="angular-required-before-trim"');
+    expect(html).toContain('data-label-name-validation-raw-owner="route-form-contract"');
+    expect(html).toContain('data-label-dialog-name-state="pristine"');
+    expect(html).toContain('data-label-save-submit-state="pristine"');
+    expect(html).not.toContain('data-label-dialog-name-validation="validation.required"');
+    expect(html).not.toContain('必填项');
+  });
+
+  it('renders the shared Angular-style delete confirmation dialog when a label is targeted', () => {
+    const t = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'menu.advanced.labels': '标签管理',
+        'common.confirm.delete': '请确认是否删除!',
+        'common.button.cancel': '取消',
+        'common.button.ok': '确定',
+        'common.refresh': '刷新',
+        'common.button.new': '新增',
+        'common.search': '搜索',
+        'label.search': '搜索标签',
+        'setting.labels.empty.title': '暂无标签',
+        'setting.labels.empty.copy': '请先新增标签。'
+      }
+    });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        draftLabel={null}
+        isManageModalAdd={false}
+        deleteTarget={{
+          id: 7,
+          name: 'team',
+          tagValue: 'ops',
+          description: '',
+          type: 1
+        }}
+        isDeletePending
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-hz-ui="confirm-dialog"');
+    expect(html).toContain('data-hz-confirm-tone="critical"');
+    expect(html).toContain('data-label-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-label-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-label-delete-confirm-state="open"');
+    expect(html).toContain('data-label-delete-confirm-pending="true"');
+    expect(html).toContain('data-label-delete-confirm-state-target="team:ops"');
+    expect(html).toContain('data-label-delete-confirm-submit="angular-modal-confirm"');
+    expect(html).toContain('data-label-delete-confirm-close="angular-close-before-delete-result"');
+    expect(html).toContain('data-label-delete-confirm-close-owner="route-state-contract"');
+    expect(html).toContain('data-label-delete-confirm-target="team:ops"');
+    expect(html).toContain('请确认是否删除!');
+    expect(html).toContain('确定');
+  });
+
+  it('renders Angular label delete failure title with backend detail', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        draftLabel={null}
+        isManageModalAdd={false}
+        deleteTarget={null}
+        actionError="删除失败!"
+        actionMeta="backend refused label delete"
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-delete-feedback="angular-delete-fail"');
+    expect(html).toContain('data-label-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-label-delete-feedback-detail="backend-message"');
+    expect(html).toContain('删除失败!');
+    expect(html).toContain('backend refused label delete');
+  });
+
+  it('renders Angular label save failure title with backend detail', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        draftLabel={null}
+        isManageModalAdd={false}
+        deleteTarget={null}
+        actionError="修改失败!"
+        actionMeta="backend refused label edit"
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-save-failure="angular-save-fail"');
+    expect(html).toContain('data-label-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-label-save-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-label-save-feedback-detail="backend-message"');
+    expect(html).toContain('修改失败!');
+    expect(html).toContain('backend refused label edit');
+  });
+
+  it('keeps the Angular label shell visible without action feedback on load failure', () => {
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <LabelManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        draftLabel={null}
+        isManageModalAdd={false}
+        deleteTarget={null}
+        loadError="backend refused label load"
+        formatTime={() => '-'}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onSearchClear={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onCopy={() => {}}
+        onEdit={() => {}}
+        onDeleteRequest={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-label-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-label-load-failure-owner="label-route-controller"');
+    expect(html).toContain('data-label-card-loading="false"');
+    expect(html).not.toContain('backend refused label load');
+    expect(html).not.toContain('data-label-action-feedback="angular-action-error"');
+  });
+
   it('keeps the labels page on the cold visual owner instead of WorkbenchPage or alert primitives', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/label-manage-surface.tsx'), 'utf8');
 
     expect(source).toContain('coldOpsCatalogVisual');
     expect(source).toContain("from '../ui/search-row'");
+    expect(source).toContain("from '@hertzbeat/ui'");
+    expect(source).toContain('HzInlineFeedback');
+    expect(source).toContain('HzConfirmDialog');
+    expect(source).toContain('HzLabelTag');
+    expect(source).toContain('renderAngularLabelColor');
+    expect(source).toContain('data-label-color-token');
+    expect(source).toContain('data-label-color-owner="hertzbeat-ui-label-tag"');
+    expect(source).toContain('data-label-dialog-preview-color="angular-render-label-color"');
+    expect(source).toContain('maskClosable={false}');
+    expect(source).toContain('data-label-dialog-mask-closable');
+    expect(source).toContain('data-label-dialog-width');
+    expect(source).toContain('data-label-dialog-field-layout');
+    expect(source).toContain('lg:max-w-[30vw]');
+    expect(source).toContain('data-label-delete-confirm="angular-modal-confirm"');
+    expect(source).toContain('data-label-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain("data-label-delete-confirm-state={deleteTarget ? 'open' : 'closed'}");
+    expect(source).toContain('data-label-delete-confirm-state-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain("data-label-delete-confirm-pending={isDeletePending ? 'true' : 'false'}");
+    expect(source).toContain('data-label-delete-confirm-submit');
+    expect(source).toContain('data-label-delete-confirm-close-contract="angular-close-before-delete-result"');
+    expect(source).toContain('data-label-delete-confirm-close-owner="route-state-contract"');
+    expect(source).toContain("'data-label-delete-confirm-close': 'angular-close-before-delete-result'");
+    expect(source).toContain('data-label-delete-confirm-target');
+    expect(source).toContain("data-label-action-feedback={actionError ? 'angular-action-error' : 'angular-action-success'}");
+    expect(source).toContain('data-label-action-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain("data-label-save-feedback={actionError ? 'angular-save-error' : 'angular-save-success'}");
+    expect(source).toContain('data-label-save-feedback-owner="hertzbeat-ui-inline-feedback"');
     expect(source).toContain('inputWidthClassName="w-[360px]"');
     expect(source).toContain('data-label-manage-surface="otlp-cold-label-console"');
     expect(source).toContain('data-label-manage-style-baseline={coldLabelVisual.canvasName}');
     expect(source).toContain('data-label-header="cold-compact-header"');
     expect(source).toContain('data-label-command-row="standard-equal-buttons"');
     expect(source).toContain('data-label-admin-layout="full-width-admin-list"');
+    expect(source).toContain('data-label-save-feedback-contract="angular-new-edit-notify"');
+    expect(source).toContain('data-label-save-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-label-optional-save-payload-contract="angular-preserve-undefined-optional-fields"');
+    expect(source).toContain('data-label-optional-save-payload-owner="label-manage-controller"');
+    expect(source).toContain('data-label-type-save-payload-contract="angular-preserve-implicit-type"');
+    expect(source).toContain('data-label-type-save-payload-owner="label-manage-controller"');
+    expect(source).toContain('data-label-display-format-contract="angular-format-label-name-raw-tag-value"');
+    expect(source).toContain('data-label-display-format-owner="label-view-model"');
+    expect(source).toContain('data-label-description-display-contract="angular-truthy-description-raw"');
+    expect(source).toContain('data-label-description-display-owner="label-manage-surface"');
+    expect(source).toContain('data-label-save-failure-contract="angular-new-edit-fail-notification"');
+    expect(source).toContain('data-label-save-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-label-load-failure-contract="angular-console-only-shell"');
+    expect(source).toContain('data-label-load-failure-owner="label-route-controller"');
+    expect(source).toContain("data-label-load-failure={loadError ? 'angular-console-only-shell' : 'none'}");
+    expect(source).toContain("actionError === t('common.notify.new-fail')");
+    expect(source).toContain("actionError === t('common.notify.edit-fail')");
+    expect(source).toContain("data-label-save-failure={saveFailureKey ? 'angular-save-fail' : undefined}");
+    expect(source).toContain('data-label-save-feedback-title={saveFailureKey}');
+    expect(source).toContain("data-label-save-feedback-detail={saveFailureKey && actionMeta ? 'backend-message' : undefined}");
+    expect(source).toContain('data-label-copy-feedback-contract="angular-copy-notify"');
+    expect(source).toContain('data-label-copy-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-label-delete-confirm-contract="angular-modal-confirm"');
+    expect(source).toContain('data-label-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-label-delete-confirm-state-contract="angular-modal-confirm-state"');
+    expect(source).toContain('data-label-delete-confirm-state-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-label-delete-confirm-close-contract="angular-close-before-delete-result"');
+    expect(source).toContain('data-label-delete-confirm-close-owner="route-state-contract"');
+    expect(source).toContain('data-label-delete-feedback-contract="angular-delete-notify"');
+    expect(source).toContain('data-label-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-label-delete-failure-contract="angular-delete-fail-notification"');
+    expect(source).toContain('data-label-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-label-delete-feedback-title={isDeleteFailureFeedback ?');
+    expect(source).toContain("data-label-delete-feedback-detail={isDeleteFailureFeedback && actionMeta ? 'backend-message' : undefined}");
+    expect(source).toContain('data-label-delete-query-contract="angular-repeated-ids-query"');
+    expect(source).toContain('data-label-delete-query-owner="route-mutation-contract"');
+    expect(source).toContain('data-label-card-loading-contract="angular-table-loading"');
+    expect(source).toContain('data-label-card-loading-owner="label-route-controller"');
+    expect(source).toContain("data-label-card-loading={isCardGridLoading ? 'true' : 'false'}");
+    expect(source).toContain("aria-busy={isCardGridLoading ? 'true' : 'false'}");
+    expect(source).toContain('data-label-save-loading-contract="angular-nz-ok-loading"');
+    expect(source).toContain('data-label-save-loading-contract-owner="angular-nz-ok-loading"');
+    expect(source).toContain('data-label-dialog-mask-closable-contract="angular-mask-closable-false"');
+    expect(source).toContain('data-label-dialog-width-contract="angular-width-30-percent"');
+    expect(source).toContain('data-label-dialog-field-layout-contract="angular-label-7-control-12"');
+    expect(source).toContain('data-label-dialog-field-layout-owner="route-form-field-grid"');
+    expect(source).toContain('data-label-dialog-preview-visibility-contract="angular-name-defined-preview"');
+    expect(source).toContain('data-label-dialog-preview-visibility-owner="route-form-state"');
+    expect(source).toContain('data-label-dialog-preview-frame-contract="angular-inline-tag-no-extra-frame"');
+    expect(source).toContain('data-label-dialog-preview-frame-owner="route-form-field-grid"');
+    expect(source).toContain('data-label-dialog-preview-chrome-contract="angular-tag-only-no-extra-frame"');
+    expect(source).toContain('data-label-dialog-preview-chrome-owner="hertzbeat-ui-label-tag"');
+    expect(source).toContain('data-label-edit-reference-contract="angular-edit-direct-reference"');
+    expect(source).toContain('data-label-edit-reference-owner="route-form-state"');
+    expect(source).toContain('data-label-name-validation-contract="angular-required-before-submit"');
+    expect(source).toContain('data-label-name-validation-trigger-contract="angular-ok-marks-dirty"');
+    expect(source).toContain('data-label-name-validation-trigger-owner="route-form-contract"');
+    expect(source).toContain('data-label-name-validation-raw-contract="angular-required-before-trim"');
+    expect(source).toContain('data-label-name-validation-raw-owner="route-form-contract"');
+    expect(source).toContain('data-label-query-contract="angular-load-all-labels"');
+    expect(source).toContain('data-label-query-owner="label-query-state"');
+    expect(source).toContain('data-label-query-param-order-contract="angular-page-index-size-type-search"');
+    expect(source).toContain('data-label-query-param-order-owner="label-query-state"');
+    expect(source).toContain('data-label-card-grid-contract="angular-card-grid"');
+    expect(source).toContain('data-label-card-grid-owner="hertzbeat-ui-label-tag"');
+    expect(source).toContain('data-label-monitor-handoff-contract="angular-routerlink-monitors-labels"');
+    expect(source).toContain('data-label-monitor-handoff-owner="next-monitor-query-link"');
+    expect(source).toContain('data-label-dialog-name-input="angular-required-name"');
+    expect(source).toContain('data-label-dialog-field-layout-row="angular-label-7-control-12"');
+    expect(source).toContain('data-label-dialog-label-span="7"');
+    expect(source).toContain('data-label-dialog-control-span="12"');
+    expect(source).toContain('sm:grid-cols-[minmax(96px,7fr)_minmax(0,12fr)]');
+    expect(source).toContain('isLabelPreviewVisible');
+    expect(source).toContain("draftLabel?.name !== undefined");
+    expect(source).toContain('data-label-dialog-preview-frame="angular-inline-tag-no-extra-frame"');
+    expect(source).toContain('data-label-dialog-preview-chrome="angular-nz-tag-inline"');
+    expect(source).toContain('data-label-dialog-preview-chrome-mode="angular-tag-only-no-extra-frame"');
+    expect(source).toContain('className={coldDialogFieldClassName}');
+    expect(source).not.toContain('className={`${coldDialogFieldClassName} rounded-[4px] border border-[#2b3039] bg-[#101217] p-3`}');
+    expect(source).toContain('data-label-dialog-name-state');
+    expect(source).toContain('data-label-dialog-name-validation="validation.required"');
+    expect(source).toContain('data-label-save-submit="angular-required-before-submit"');
+    expect(source).toContain('data-label-name-validation-trigger="angular-ok-marks-dirty"');
+    expect(source).toContain('data-label-save-submit-state');
+    expect(source).toContain('disabled={Boolean(isSavePending)}');
+    expect(source).toContain("data-label-save-ok-loading={isSavePending ? 'true' : 'false'}");
+    expect(source).toContain('data-label-save-ok-loading-owner="angular-nz-ok-loading"');
     expect(source).toContain('data-label-toolbar="cold-table-toolbar"');
     expect(source).toContain('data-label-search-owner="shared-search-row"');
-    expect(source).toContain('data-label-table-shell="cold-dense-table"');
-    expect(source).toContain('data-label-table="cold-label-table"');
-    expect(source).toContain('data-label-empty-state="cold-table-empty"');
+    expect(source).toContain('data-label-search-submit="angular-enter-and-clear"');
+    expect(source).toContain('data-label-search-submit-owner="cold-search-row"');
+    expect(source).toContain("clearLabel={t('common.clear')}");
+    expect(source).toContain('onClear={onSearchClear}');
+    expect(source).toContain('data-label-card-grid="angular-card-grid"');
+    expect(source).toContain('data-label-card-column-contract="nz-xs-12-sm-8-md-6-lg-4"');
+    expect(source).toContain('data-label-card-shell="angular-card"');
+    expect(source).toContain('data-label-card-size="small"');
+    expect(source).toContain('data-label-card-content="angular-label-content"');
+    expect(source).toContain('data-label-card-description="angular-description"');
+    expect(source).toContain('data-label-card-description-display="angular-truthy-description-raw"');
+    expect(source).toContain('data-label-card-description-display-owner="label-manage-surface"');
+    expect(source).toContain('data-label-card-actions="angular-card-actions"');
+    expect(source).toContain('data-label-monitor-handoff="angular-routerlink-monitors-labels"');
+    expect(source).toContain('data-label-monitor-handoff-query={labelText}');
+    expect(source).toContain('data-label-monitor-handoff-owner="next-monitor-query-link"');
+    expect(source).toContain("t('setting.labels.action.copy-aria', actionLabelParams)");
+    expect(source).toContain("t('setting.labels.action.edit-aria', actionLabelParams)");
+    expect(source).toContain("t('setting.labels.action.delete-aria', actionLabelParams)");
+    expect(source).toContain('data-label-row-actions="angular-card-actions-contextual"');
+    expect(source).toContain('data-label-row-actions-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-label-row-action-owner="row-contextual-icon-button"');
+    expect(source).toContain('data-label-row-action-label={labelText}');
+    expect(source).toContain('data-label-empty-state="cold-card-empty"');
     expect(source).not.toContain('data-label-summary-rail');
     expect(source).not.toContain('coldLabelVisual.search.row');
     expect(source).not.toContain('coldLabelVisual.search.input');
@@ -202,11 +1037,7 @@ describe('label manage surface', () => {
     expect(source).not.toContain('WorkbenchPage');
     expect(source).not.toContain("from './alert-surface-primitives'");
     expect(source).not.toContain('AlertSurfacePanel');
-    expect(source).not.toContain('data-label-manage-route="angular-label-cards"');
-    expect(source).not.toContain('data-label-toolbar="angular-card-toolbar"');
-    expect(source).not.toContain('data-label-card-grid="angular-card-grid"');
-    expect(source).not.toContain('data-label-card-shell="angular-card"');
-    expect(source).not.toContain('data-label-card-actions="angular-card-actions"');
+    expect(source).not.toContain('data-label-table="cold-label-table"');
     expect(source).not.toContain('SurfaceSection');
     expect(source).not.toContain('StatusState');
     expect(source).not.toContain('ToolbarField');
diff --git a/web-next/components/pages/label-manage-surface.tsx b/web-next/components/pages/label-manage-surface.tsx
index 60ecfec966..e402e4ad9e 100644
--- a/web-next/components/pages/label-manage-surface.tsx
+++ b/web-next/components/pages/label-manage-surface.tsx
@@ -1,13 +1,15 @@
 'use client';
 
 import React from 'react';
+import Link from 'next/link';
 import { Copy, Inbox, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react';
 import { OverlayDialog } from '../workbench/overlay-dialog';
 import { Button } from '../ui/button';
 import { Input } from '../ui/input';
 import { SearchRow } from '../ui/search-row';
+import { HzActionGroup, HzConfirmDialog, HzIconButton, HzInlineFeedback, HzLabelTag } from '@hertzbeat/ui';
 import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
-import { buildLabelDisplayName } from '../../lib/label-manage/view-model';
+import { buildLabelDisplayName, renderAngularLabelColor } from '../../lib/label-manage/view-model';
 import type { LabelManagePageData } from '../../lib/label-manage/controller';
 import type { Label } from '../../lib/types';
 
@@ -19,14 +21,26 @@ type LabelManageSurfaceProps = {
   search: string;
   draftLabel: Label | null;
   isManageModalAdd: boolean;
+  isNameValidationVisible?: boolean;
+  deleteTarget: Label | null;
+  isLoadPending?: boolean;
+  isDeletePending?: boolean;
+  isSavePending?: boolean;
+  actionMessage?: string | null;
+  actionError?: string | null;
+  actionMeta?: string | null;
+  loadError?: string | null;
   formatTime: (value?: number | string | null) => string;
   onSearchChange: (value: string) => void;
   onSearch: () => void;
+  onSearchClear: () => void;
   onRefresh: () => void;
   onNew: () => void;
   onCopy: (label: Label) => void;
   onEdit: (label: Label) => void;
-  onDelete: (label: Label) => void;
+  onDeleteRequest: (label: Label) => void;
+  onDeleteCancel: () => void;
+  onDeleteConfirm: () => void;
   onDraftChange: (patch: Partial<Label>) => void;
   onCloseDialog: () => void;
   onSaveDialog: () => void;
@@ -40,28 +54,13 @@ const coldButtonClassName =
 const coldPrimaryButtonClassName =
   'h-8 min-w-[104px] rounded-[3px] border-[#31405c] bg-[#182238] px-3 text-[12px] font-semibold text-[#d8e4ff] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:border-[#4e74f8] hover:bg-[#202a42] hover:text-white';
 
-const coldIconButtonClassName =
-  'h-8 w-8 min-w-0 rounded-[3px] border-[#2b3039] bg-[#101217] text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white';
-
 const coldDialogInputClassName =
   'h-8 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#eef2f7] outline-none placeholder:text-[#6f7788] focus:border-[#4e74f8] focus:ring-2 focus:ring-[rgba(78,116,248,0.16)]';
 
-function isChineseTranslator(t: Translator): boolean {
-  return t('common.refresh') === '刷新' || t('menu.advanced.labels') === '标签管理';
-}
+const coldDialogFieldClassName =
+  'grid gap-2 text-[12px] font-semibold text-[#a9b0bb] sm:grid-cols-[minmax(96px,7fr)_minmax(0,12fr)] sm:items-start';
 
-function resolveCopy(t: Translator, key: string, fallback: string): string {
-  const value = t(key);
-  return value === key ? fallback : value;
-}
-
-function resolveLabelType(type: number | null | undefined, t: Translator): string {
-  const chinese = isChineseTranslator(t);
-  if (type === 0) return chinese ? '自动标签' : 'Auto label';
-  if (type === 1) return chinese ? '用户标签' : 'User label';
-  if (type === 2) return chinese ? '预置标签' : 'Preset label';
-  return '-';
-}
+const coldDialogLabelClassName = 'pt-2 leading-4 sm:text-right';
 
 export function LabelManageSurface({
   t,
@@ -69,14 +68,25 @@ export function LabelManageSurface({
   search,
   draftLabel,
   isManageModalAdd,
-  formatTime,
+  isNameValidationVisible,
+  deleteTarget,
+  isLoadPending,
+  isDeletePending,
+  isSavePending,
+  actionMessage,
+  actionError,
+  actionMeta,
+  loadError,
   onSearchChange,
   onSearch,
+  onSearchClear,
   onRefresh,
   onNew,
   onCopy,
   onEdit,
-  onDelete,
+  onDeleteRequest,
+  onDeleteCancel,
+  onDeleteConfirm,
   onDraftChange,
   onCloseDialog,
   onSaveDialog
@@ -84,8 +94,20 @@ export function LabelManageSurface({
   const labels = data.list.content ?? [];
   const dialogTitle = isManageModalAdd ? t('label.new') : t('label.edit');
   const displayName = draftLabel ? buildLabelDisplayName(draftLabel) : '';
-  const emptyTitle = resolveCopy(t, 'setting.labels.empty.title', '暂无标签');
-  const emptyCopy = resolveCopy(t, 'setting.labels.empty.copy', '请先新增标签。');
+  const previewDisplayName = displayName || t('common.none');
+  const isLabelPreviewVisible = Boolean(draftLabel) && draftLabel?.name !== undefined;
+  const isDraftNameMissing = Boolean(draftLabel) && (draftLabel.name == undefined || draftLabel.name.length === 0);
+  const shouldShowNameValidation = isDraftNameMissing && Boolean(isNameValidationVisible);
+  const isCardGridLoading = Boolean(isLoadPending || isDeletePending);
+  const isDeleteFailureFeedback = actionError === t('common.notify.delete-fail');
+  const saveFailureKey =
+    actionError === t('common.notify.new-fail')
+      ? 'common.notify.new-fail'
+      : actionError === t('common.notify.edit-fail')
+        ? 'common.notify.edit-fail'
+        : undefined;
+  const emptyTitle = t('setting.labels.empty.title');
+  const emptyCopy = t('setting.labels.empty.copy');
 
   return (
     <>
@@ -104,7 +126,7 @@ export function LabelManageSurface({
                     {t('menu.advanced.labels')}
                   </h1>
                   <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">
-                    统一管理监控标签名称、标签值和描述,用紧凑表格保持检索、复制和编辑动作在同一工作面里完成。
+                    {t('setting.labels.copy')}
                   </p>
                   <div data-label-command-row="standard-equal-buttons" className={coldLabelVisual.button.row}>
                     <Button size="sm" variant="default" className={coldButtonClassName} onClick={onRefresh}>
@@ -120,94 +142,226 @@ export function LabelManageSurface({
               </div>
             </div>
 
-            <div data-label-admin-layout="full-width-admin-list" className="space-y-5">
+            <div
+              data-label-admin-layout="full-width-admin-list"
+              data-label-save-feedback-contract="angular-new-edit-notify"
+              data-label-save-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+              data-label-save-failure-contract="angular-new-edit-fail-notification"
+              data-label-save-failure-owner="hertzbeat-ui-inline-feedback"
+              data-label-load-failure-contract="angular-console-only-shell"
+              data-label-load-failure-owner="label-route-controller"
+              data-label-load-failure={loadError ? 'angular-console-only-shell' : 'none'}
+              data-label-copy-feedback-contract="angular-copy-notify"
+              data-label-copy-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+              data-label-delete-confirm-contract="angular-modal-confirm"
+              data-label-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"
+              data-label-delete-confirm-state-contract="angular-modal-confirm-state"
+              data-label-delete-confirm-state-contract-owner="hertzbeat-ui-confirm-dialog"
+              data-label-delete-confirm-close-contract="angular-close-before-delete-result"
+              data-label-delete-confirm-close-owner="route-state-contract"
+              data-label-delete-feedback-contract="angular-delete-notify"
+              data-label-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+              data-label-delete-failure-contract="angular-delete-fail-notification"
+              data-label-delete-failure-owner="hertzbeat-ui-inline-feedback"
+              data-label-delete-query-contract="angular-repeated-ids-query"
+              data-label-delete-query-owner="route-mutation-contract"
+              data-label-card-loading-contract="angular-table-loading"
+              data-label-card-loading-owner="label-route-controller"
+              data-label-card-loading={isCardGridLoading ? 'true' : 'false'}
+              data-label-save-loading-contract="angular-nz-ok-loading"
+              data-label-save-loading-contract-owner="angular-nz-ok-loading"
+              data-label-save-trim-contract="angular-trim-before-save"
+              data-label-save-trim-owner="label-manage-controller"
+              data-label-optional-save-payload-contract="angular-preserve-undefined-optional-fields"
+              data-label-optional-save-payload-owner="label-manage-controller"
+              data-label-type-save-payload-contract="angular-preserve-implicit-type"
+              data-label-type-save-payload-owner="label-manage-controller"
+              data-label-display-format-contract="angular-format-label-name-raw-tag-value"
+              data-label-display-format-owner="label-view-model"
+              data-label-description-display-contract="angular-truthy-description-raw"
+              data-label-description-display-owner="label-manage-surface"
+              data-label-dialog-mask-closable-contract="angular-mask-closable-false"
+              data-label-dialog-mask-closable-owner="angular-nz-modal"
+              data-label-dialog-width-contract="angular-width-30-percent"
+              data-label-dialog-width-owner="angular-nz-modal"
+              data-label-dialog-field-layout-contract="angular-label-7-control-12"
+              data-label-dialog-field-layout-owner="route-form-field-grid"
+              data-label-dialog-preview-visibility-contract="angular-name-defined-preview"
+              data-label-dialog-preview-visibility-owner="route-form-state"
+              data-label-dialog-preview-frame-contract="angular-inline-tag-no-extra-frame"
+              data-label-dialog-preview-frame-owner="route-form-field-grid"
+              data-label-dialog-preview-chrome-contract="angular-tag-only-no-extra-frame"
+              data-label-dialog-preview-chrome-owner="hertzbeat-ui-label-tag"
+              data-label-edit-reference-contract="angular-edit-direct-reference"
+              data-label-edit-reference-owner="route-form-state"
+              data-label-name-validation-contract="angular-required-before-submit"
+              data-label-name-validation-trigger-contract="angular-ok-marks-dirty"
+              data-label-name-validation-trigger-owner="route-form-contract"
+              data-label-name-validation-raw-contract="angular-required-before-trim"
+              data-label-name-validation-raw-owner="route-form-contract"
+              data-label-query-contract="angular-load-all-labels"
+              data-label-query-owner="label-query-state"
+              data-label-query-param-order-contract="angular-page-index-size-type-search"
+              data-label-query-param-order-owner="label-query-state"
+              data-label-card-grid-contract="angular-card-grid"
+              data-label-card-grid-owner="hertzbeat-ui-label-tag"
+              data-label-monitor-handoff-contract="angular-routerlink-monitors-labels"
+              data-label-monitor-handoff-owner="next-monitor-query-link"
+              className="space-y-5"
+            >
               <section className="min-w-0">
                 <SearchRow
                   data-label-toolbar="cold-table-toolbar"
                   data-label-search-owner="shared-search-row"
+                  data-label-search-submit="angular-enter-and-clear"
+                  data-label-search-submit-owner="cold-search-row"
                   value={search}
                   placeholder={t('label.search')}
                   searchLabel={t('common.search')}
+                  clearLabel={t('common.clear')}
                   inputWidthClassName="w-[360px]"
                   onValueChange={onSearchChange}
                   onSearch={onSearch}
+                  onClear={onSearchClear}
                 />
 
+                {actionMessage || actionError ? (
+                  <HzInlineFeedback
+                    tone={actionError ? 'warning' : 'success'}
+                    title={actionError || actionMessage}
+                    meta={actionMeta ?? undefined}
+                    variant="embedded"
+                    data-label-action-feedback={actionError ? 'angular-action-error' : 'angular-action-success'}
+                    data-label-action-feedback-owner="hertzbeat-ui-inline-feedback"
+                    data-label-save-feedback={actionError ? 'angular-save-error' : 'angular-save-success'}
+                    data-label-save-feedback-owner="hertzbeat-ui-inline-feedback"
+                    data-label-save-failure={saveFailureKey ? 'angular-save-fail' : undefined}
+                    data-label-save-failure-owner={saveFailureKey ? 'hertzbeat-ui-inline-feedback' : undefined}
+                    data-label-save-feedback-title={saveFailureKey}
+                    data-label-save-feedback-detail={saveFailureKey && actionMeta ? 'backend-message' : undefined}
+                    data-label-delete-feedback={
+                      isDeleteFailureFeedback
+                        ? 'angular-delete-fail'
+                        : actionMessage === t('common.notify.delete-success')
+                          ? 'angular-delete-notify'
+                          : undefined
+                    }
+                    data-label-delete-feedback-owner={isDeleteFailureFeedback ? 'hertzbeat-ui-inline-feedback' : undefined}
+                    data-label-delete-feedback-title={isDeleteFailureFeedback ? 'common.notify.delete-fail' : undefined}
+                    data-label-delete-feedback-detail={isDeleteFailureFeedback && actionMeta ? 'backend-message' : undefined}
+                  />
+                ) : null}
+
                 <div
-                  data-label-table-shell="cold-dense-table"
-                  className="overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
+                  data-label-card-grid="angular-card-grid"
+                  data-label-card-grid-owner="hertzbeat-ui-label-tag"
+                  data-label-card-column-contract="nz-xs-12-sm-8-md-6-lg-4"
+                  data-label-card-loading={isCardGridLoading ? 'true' : 'false'}
+                  data-label-card-loading-owner="label-route-controller"
+                  aria-busy={isCardGridLoading ? 'true' : 'false'}
+                  className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6"
                 >
-                  <table data-label-table="cold-label-table" className="w-full table-fixed border-collapse text-left text-[12px] text-[#a9b0bb]">
-                    <thead className="border-b border-[#252b34] bg-[#101217] text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">
-                      <tr>
-                        <th className="w-[24%] px-3 py-2.5">标签名称</th>
-                        <th className="w-[18%] px-3 py-2.5">标签类型</th>
-                        <th className="w-[28%] px-3 py-2.5">标签描述</th>
-                        <th className="w-[18%] px-3 py-2.5">{t('common.edit-time')}</th>
-                        <th className="w-[96px] px-3 py-2.5">{t('common.edit')}</th>
-                      </tr>
-                    </thead>
-                    <tbody>
-                      {labels.length > 0 ? labels.map(label => {
-                        const labelText = buildLabelDisplayName(label);
-                        const copyLabel = t('common.copy.button');
-                        const editLabel = t('common.button.edit');
-                        const deleteLabel = t('common.button.delete');
+                  {labels.length > 0 ? labels.map(label => {
+                    const labelText = buildLabelDisplayName(label);
+                    const description = label.description ? label.description : '';
+                    const actionLabelParams = { name: labelText };
+                    const copyLabel = t('setting.labels.action.copy-aria', actionLabelParams);
+                    const editLabel = t('setting.labels.action.edit-aria', actionLabelParams);
+                    const deleteLabel = t('setting.labels.action.delete-aria', actionLabelParams);
 
-                        return (
-                          <tr key={label.id} data-label-row={label.id} className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]">
-                            <td className="px-3 py-2.5 font-semibold text-[#eef2f7]">
-                              <a
-                                href={`/monitors?labels=${encodeURIComponent(labelText)}`}
-                                className="inline-flex max-w-full rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[12px] leading-5 text-[#dbe4f0] hover:border-[#4e74f8] hover:text-white"
-                              >
-                                <span className="truncate">{labelText}</span>
-                              </a>
-                            </td>
-                            <td className="px-3 py-2.5">
-                              <span className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] leading-4 text-[#cbd5e1]">
-                                {resolveLabelType(label.type, t)}
-                              </span>
-                            </td>
-                            <td className="truncate px-3 py-2.5" title={label.description || '-'}>
-                              {label.description || '-'}
-                            </td>
-                            <td className="px-3 py-2.5 text-[#858d9a]">
-                              {formatTime(label.gmtUpdate || label.gmtCreate || null)}
-                            </td>
-                            <td className="px-3 py-2.5">
-                              <div data-label-row-actions="cold-icon-actions" className="flex gap-1.5">
-                                <Button size="icon" variant="default" className={coldIconButtonClassName} aria-label={copyLabel} title={copyLabel} onClick={() => onCopy(label)}>
-                                  <Copy className="h-3.5 w-3.5" aria-hidden="true" />
-                                </Button>
-                                <Button size="icon" variant="default" className={coldIconButtonClassName} aria-label={editLabel} title={editLabel} onClick={() => onEdit(label)}>
-                                  <Pencil className="h-3.5 w-3.5" aria-hidden="true" />
-                                </Button>
-                                <Button size="icon" variant="default" className={coldIconButtonClassName} aria-label={deleteLabel} title={deleteLabel} onClick={() => onDelete(label)}>
-                                  <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
-                                </Button>
-                              </div>
-                            </td>
-                          </tr>
-                        );
-                      }) : (
-                        <tr data-label-empty-state="cold-table-empty" className="border-t border-[#252b34] bg-[#0b0c0e]">
-                          <td colSpan={5} className="h-[240px] px-3 text-center text-[#a9b0bb]">
-                            <div className="inline-flex flex-col items-center gap-2.5">
-                              <span
-                                data-label-empty-icon="cold-empty-box"
-                                className="inline-flex h-10 w-10 items-center justify-center rounded-[4px] border border-[#303743] bg-[#101217] text-[#cbd5e1]"
-                              >
-                                <Inbox className="h-5 w-5" aria-hidden="true" />
-                              </span>
-                              <div className="text-[13px] font-semibold text-[#eef2f7]">{emptyTitle}</div>
-                              <div className="text-[12px] leading-5 text-[#8f99ab]">{emptyCopy}</div>
+                    return (
+                      <article
+                        key={label.id}
+                        data-label-row={label.id}
+                        data-label-card-shell="angular-card"
+                        data-label-card-size="small"
+                        className="flex min-h-[112px] min-w-0 flex-col overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_12px_32px_rgba(0,0,0,0.28)] transition hover:border-[#4e74f8] hover:bg-[#111721]"
+                      >
+                        <div data-label-card-content="angular-label-content" className="flex min-h-[72px] flex-1 flex-col gap-2 p-3">
+                          <Link
+                            href={`/monitors?labels=${encodeURIComponent(labelText)}`}
+                            title={labelText}
+                            className="inline-flex max-w-full"
+                            data-label-monitor-handoff="angular-routerlink-monitors-labels"
+                            data-label-monitor-handoff-query={labelText}
+                            data-label-monitor-handoff-owner="next-monitor-query-link"
+                          >
+                            <HzLabelTag
+                              colorToken={renderAngularLabelColor(label.name || '')}
+                              className="hover:border-[#4e74f8]"
+                              data-label-color-token={renderAngularLabelColor(label.name || '')}
+                              data-label-color-owner="hertzbeat-ui-label-tag"
+                            >
+                              <span className="truncate">{labelText}</span>
+                            </HzLabelTag>
+                          </Link>
+                          {description ? (
+                            <div
+                              data-label-card-description="angular-description"
+                              data-label-card-description-display="angular-truthy-description-raw"
+                              data-label-card-description-display-owner="label-manage-surface"
+                              className="line-clamp-2 min-h-[34px] text-[12px] leading-[17px] text-[#8f99ab]"
+                              title={description}
+                            >
+                              {description}
                             </div>
-                          </td>
-                        </tr>
-                      )}
-                    </tbody>
-                  </table>
+                          ) : null}
+                        </div>
+                        <div data-label-card-actions="angular-card-actions" className="border-t border-[#252b34] bg-[#101217] px-2 py-1.5">
+                          <HzActionGroup
+                            density="compact-icons"
+                            layout="center"
+                            data-label-row-actions="angular-card-actions-contextual"
+                            data-label-row-actions-owner="hertzbeat-ui-action-group"
+                          >
+                            <HzIconButton
+                              label={copyLabel}
+                              intent="ghost"
+                              onClick={() => onCopy(label)}
+                              data-label-row-action="copy"
+                              data-label-row-action-owner="row-contextual-icon-button"
+                              data-label-row-action-label={labelText}
+                            >
+                              <Copy size={13} />
+                            </HzIconButton>
+                            <HzIconButton
+                              label={editLabel}
+                              intent="ghost"
+                              onClick={() => onEdit(label)}
+                              data-label-row-action="edit"
+                              data-label-row-action-owner="row-contextual-icon-button"
+                              data-label-row-action-label={labelText}
+                            >
+                              <Pencil size={13} />
+                            </HzIconButton>
+                            <HzIconButton
+                              label={deleteLabel}
+                              intent="ghost"
+                              onClick={() => onDeleteRequest(label)}
+                              data-label-row-action="delete"
+                              data-label-row-action-owner="row-contextual-icon-button"
+                              data-label-row-action-label={labelText}
+                            >
+                              <Trash2 size={13} />
+                            </HzIconButton>
+                          </HzActionGroup>
+                        </div>
+                      </article>
+                    );
+                  }) : (
+                    <div data-label-empty-state="cold-card-empty" className="col-span-full h-[240px] rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] px-3 text-center text-[#a9b0bb]">
+                      <div className="flex h-full flex-col items-center justify-center gap-2.5">
+                        <span
+                          data-label-empty-icon="cold-empty-box"
+                          className="inline-flex h-10 w-10 items-center justify-center rounded-[4px] border border-[#303743] bg-[#101217] text-[#cbd5e1]"
+                        >
+                          <Inbox className="h-5 w-5" aria-hidden="true" />
+                        </span>
+                        <div className="text-[13px] font-semibold text-[#eef2f7]">{emptyTitle}</div>
+                        <div className="text-[12px] leading-5 text-[#8f99ab]">{emptyCopy}</div>
+                      </div>
+                    </div>
+                  )}
                 </div>
               </section>
             </div>
@@ -219,55 +373,168 @@ export function LabelManageSurface({
         open={Boolean(draftLabel)}
         title={dialogTitle}
         onClose={onCloseDialog}
-        maxWidthClassName="max-w-2xl"
+        maskClosable={false}
+        maxWidthClassName="max-w-[min(92vw,30rem)] lg:max-w-[30vw]"
+        overlayProps={{
+          'data-label-dialog-mask-closable': 'false',
+          'data-label-dialog-mask-closable-owner': 'angular-nz-modal',
+          'data-label-dialog-width': 'angular-width-30-percent',
+          'data-label-dialog-width-owner': 'angular-nz-modal',
+          'data-label-dialog-field-layout': 'angular-label-7-control-12',
+          'data-label-dialog-field-layout-owner': 'route-form-field-grid',
+          'data-label-dialog-preview-visibility': isLabelPreviewVisible ? 'visible' : 'hidden',
+          'data-label-dialog-preview-visibility-owner': 'route-form-state'
+        }}
         footer={
           <div className="flex flex-wrap justify-end gap-2">
             <Button size="sm" variant="default" className={coldButtonClassName} onClick={onCloseDialog}>
               {t('common.button.cancel')}
             </Button>
-            <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onSaveDialog}>
+            <Button
+              size="sm"
+              variant="default"
+              className={coldPrimaryButtonClassName}
+              onClick={onSaveDialog}
+              disabled={Boolean(isSavePending)}
+              aria-busy={isSavePending ? 'true' : 'false'}
+              data-label-save-submit="angular-required-before-submit"
+              data-label-name-validation-trigger="angular-ok-marks-dirty"
+              data-label-name-validation-trigger-owner="route-form-contract"
+              data-label-save-submit-state={shouldShowNameValidation ? 'dirty-invalid' : 'pristine'}
+              data-label-save-trim="angular-trim-before-save"
+              data-label-save-trim-owner="label-manage-controller"
+              data-label-save-ok-loading={isSavePending ? 'true' : 'false'}
+              data-label-save-ok-loading-owner="angular-nz-ok-loading"
+            >
               {t('common.button.save')}
             </Button>
           </div>
         }
       >
         {draftLabel ? (
-          <div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]">
-            <div className="space-y-3">
-              <label className="flex flex-col gap-2 text-[12px] font-semibold text-[#a9b0bb]">
-                <span>{t('label.name')}</span>
+          <div className="grid gap-3" data-label-dialog-field-layout="angular-label-7-control-12" data-label-dialog-field-layout-owner="route-form-field-grid">
+            <div
+              className={coldDialogFieldClassName}
+              data-label-dialog-field="name"
+              data-label-dialog-field-layout-row="angular-label-7-control-12"
+            >
+              <label htmlFor="label-name" className={coldDialogLabelClassName} data-label-dialog-label-span="7">
+                {t('label.name')}
+              </label>
+              <div className="min-w-0" data-label-dialog-control-span="12">
                 <Input
+                  id="label-name"
                   className={coldDialogInputClassName}
                   value={draftLabel.name || ''}
                   onChange={event => onDraftChange({ name: event.target.value })}
+                  data-label-dialog-name-input="angular-required-name"
+                  data-label-dialog-name-state={shouldShowNameValidation ? 'dirty-invalid' : 'pristine'}
                 />
+                {shouldShowNameValidation ? (
+                  <span className="mt-1 block text-[11px] font-medium text-[#fca5a5]" data-label-dialog-name-validation="validation.required">
+                    {t('validation.required')}
+                  </span>
+                ) : null}
+              </div>
+            </div>
+            <div
+              className={coldDialogFieldClassName}
+              data-label-dialog-field="value"
+              data-label-dialog-field-layout-row="angular-label-7-control-12"
+            >
+              <label htmlFor="label-value" className={coldDialogLabelClassName} data-label-dialog-label-span="7">
+                {t('label.value')}
               </label>
-              <label className="flex flex-col gap-2 text-[12px] font-semibold text-[#a9b0bb]">
-                <span>{t('label.value')}</span>
+              <div className="min-w-0" data-label-dialog-control-span="12">
                 <Input
+                  id="label-value"
                   className={coldDialogInputClassName}
                   value={draftLabel.tagValue || ''}
                   onChange={event => onDraftChange({ tagValue: event.target.value })}
                 />
+              </div>
+            </div>
+            <div
+              className={coldDialogFieldClassName}
+              data-label-dialog-field="description"
+              data-label-dialog-field-layout-row="angular-label-7-control-12"
+            >
+              <label htmlFor="label-description" className={coldDialogLabelClassName} data-label-dialog-label-span="7">
+                {t('label.description')}
               </label>
-              <label className="flex flex-col gap-2 text-[12px] font-semibold text-[#a9b0bb]">
-                <span>{t('label.description')}</span>
+              <div className="min-w-0" data-label-dialog-control-span="12">
                 <Input
+                  id="label-description"
                   className={coldDialogInputClassName}
                   value={draftLabel.description || ''}
                   onChange={event => onDraftChange({ description: event.target.value })}
                 />
-              </label>
-            </div>
-            <div data-label-dialog-preview="cold-preview" className="rounded-[4px] border border-[#2b3039] bg-[#101217] p-3">
-              <div className="mb-3 text-[13px] font-semibold text-[#eef2f7]">{t('label.display')}</div>
-              <div className="inline-flex min-h-8 max-w-full items-center rounded-[3px] border border-[#303743] bg-[#0b0c0e] px-2.5 text-[12px] font-semibold text-[#dbe4f0]">
-                <span className="truncate">{displayName || '-'}</span>
               </div>
             </div>
+            {isLabelPreviewVisible ? (
+              <div
+                data-label-dialog-preview="cold-preview"
+                data-label-dialog-preview-visibility="visible"
+                data-label-dialog-preview-visibility-owner="route-form-state"
+                data-label-dialog-preview-frame="angular-inline-tag-no-extra-frame"
+                data-label-dialog-preview-frame-owner="route-form-field-grid"
+                data-label-dialog-field="display"
+                data-label-dialog-field-layout-row="angular-label-7-control-12"
+                className={coldDialogFieldClassName}
+              >
+                <div className={coldDialogLabelClassName} data-label-dialog-label-span="7">
+                  {t('label.display')}
+                </div>
+                <HzLabelTag
+                  colorToken={renderAngularLabelColor(draftLabel.name || '')}
+                  className="max-w-full align-middle"
+                  data-label-dialog-preview-color="angular-render-label-color"
+                  data-label-dialog-preview-chrome="angular-nz-tag-inline"
+                  data-label-dialog-preview-chrome-mode="angular-tag-only-no-extra-frame"
+                  data-label-dialog-preview-chrome-owner="hertzbeat-ui-label-tag"
+                  data-label-color-owner="hertzbeat-ui-label-tag"
+                  data-label-dialog-control-span="12"
+                >
+                  <span className="truncate">{previewDisplayName}</span>
+                </HzLabelTag>
+              </div>
+            ) : (
+              <div hidden data-label-dialog-preview-visibility="hidden" data-label-dialog-preview-visibility-owner="route-form-state" />
+            )}
           </div>
         ) : null}
       </OverlayDialog>
+      <div
+        hidden
+        data-label-delete-confirm-state={deleteTarget ? 'open' : 'closed'}
+        data-label-delete-confirm-state-owner="hertzbeat-ui-confirm-dialog"
+        data-label-delete-confirm-pending={isDeletePending ? 'true' : 'false'}
+        data-label-delete-confirm-state-target={deleteTarget ? buildLabelDisplayName(deleteTarget) : ''}
+      />
+      <HzConfirmDialog
+        open={Boolean(deleteTarget)}
+        tone="critical"
+        kicker={t('menu.advanced.labels')}
+        title={t('common.confirm.delete')}
+        cancelLabel={t('common.button.cancel')}
+        confirmLabel={t('common.button.ok')}
+        confirmDisabled={Boolean(isDeletePending)}
+        onClose={onDeleteCancel}
+        onConfirm={onDeleteConfirm}
+        bodyRhythm="stack"
+        data-label-delete-confirm="angular-modal-confirm"
+        data-label-delete-confirm-owner="hertzbeat-ui-confirm-dialog"
+        data-label-delete-confirm-state={deleteTarget ? 'open' : 'closed'}
+        confirmButtonProps={{
+          'data-label-delete-confirm-submit': 'angular-modal-confirm',
+          'data-label-delete-confirm-close': 'angular-close-before-delete-result',
+          'data-label-delete-confirm-close-owner': 'route-state-contract'
+        } as React.ComponentProps<typeof HzConfirmDialog>['confirmButtonProps']}
+      >
+        <div data-label-delete-confirm-target={deleteTarget ? buildLabelDisplayName(deleteTarget) : ''}>
+          {deleteTarget ? buildLabelDisplayName(deleteTarget) : t('common.none')}
+        </div>
+      </HzConfirmDialog>
     </>
   );
 }
diff --git a/web-next/components/pages/plugin-manage-surface.test.tsx b/web-next/components/pages/plugin-manage-surface.test.tsx
index e36d828059..59f980c6b8 100644
--- a/web-next/components/pages/plugin-manage-surface.test.tsx
+++ b/web-next/components/pages/plugin-manage-surface.test.tsx
@@ -11,6 +11,7 @@ describe('plugin manage surface', () => {
     const t = createTranslatorMock({
       locale: 'zh-CN',
       overrides: {
+        'common.none': '无类型',
         'common.file.select': '选择文件'
       }
     });
@@ -32,7 +33,7 @@ describe('plugin manage surface', () => {
                 id: 2,
                 name: 'slack',
                 enableStatus: false,
-                items: [{ type: 'POST_ALERT' }],
+                items: [],
                 paramCount: 0
               }
             ],
@@ -48,8 +49,13 @@ describe('plugin manage surface', () => {
           jarFileName: 'smtp.jar',
           enableStatus: true
         }}
+        actionMessage="Edit Success!"
+        actionKind="enable"
+        isTogglePending
+        optimisticEnableStatus={{ 2: false }}
         onSearchChange={() => {}}
         onSearch={() => {}}
+        onSearchClear={() => {}}
         onRefresh={() => {}}
         onNew={() => {}}
         onDeleteSelected={() => {}}
@@ -67,20 +73,96 @@ describe('plugin manage surface', () => {
     expect(html).toContain('data-plugin-header="cold-compact-header"');
     expect(html).toContain('data-plugin-command-row="standard-equal-buttons"');
     expect(html).toContain('data-plugin-admin-layout="full-width-admin-list"');
+    expect(html).toContain('data-plugin-enable-feedback-contract="angular-edit-notify"');
+    expect(html).toContain('data-plugin-enable-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-enable-optimistic-contract="angular-toggle-mutates-row-before-put"');
+    expect(html).toContain('data-plugin-enable-optimistic-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-enable-optimistic-state="overridden"');
+    expect(html).toContain('data-plugin-enable-optimistic-next-status="false"');
+    expect(html).toContain('data-plugin-table-loading-contract="angular-load-plugins-table"');
+    expect(html).toContain('data-plugin-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(html).toContain('data-plugin-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-plugin-load-failure-owner="plugin-route-controller"');
+    expect(html).toContain('data-plugin-load-failure="none"');
+    expect(html).toContain('data-plugin-enable-feedback="angular-edit-notify"');
+    expect(html).toContain('data-plugin-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-toggle-loading-contract="angular-nz-table-loading"');
+    expect(html).toContain('data-plugin-toggle-loading-contract-owner="angular-nz-table-loading"');
+    expect(html).toContain('data-plugin-selection-reset-contract="angular-load-clears-selection"');
+    expect(html).toContain('data-plugin-selection-reset-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-table-columns-contract="angular-five-column-edit-actions"');
+    expect(html).toContain('data-plugin-table-columns-owner="angular-nz-table"');
+    expect(html).toContain('data-plugin-table-stable-height-contract="viewport-fill-on-empty-or-short"');
+    expect(html).toContain('data-plugin-table-stable-height-owner="route-layout-contract"');
+    expect(html).toContain('data-plugin-table-loading="true"');
+    expect(html).toContain('data-plugin-table-loading-owner="angular-nz-table-loading"');
+    expect(html).toContain('aria-busy="true"');
+    expect(html).toContain('Edit Success!');
     expect(html).toContain('data-plugin-manage-toolbar="cold-table-toolbar"');
     expect(html).toContain('data-plugin-manage-search-owner="shared-search-row"');
+    expect(html).toContain('data-plugin-search-submit-contract="angular-enter-and-clear"');
+    expect(html).toContain('data-plugin-search-clear-contract="angular-cleared-load"');
+    expect(html).toContain('data-plugin-search-clear-owner="shared-search-row"');
     expect(html).toContain('data-cold-search-row-owner="cold-search-row"');
     expect(html).toContain('data-cold-search-input="fixed-width-direct"');
     expect(html).toContain('data-cold-search-control="direct-input"');
     expect(html).toContain('data-cold-search-chrome="no-extra-input-shell"');
     expect(html).toContain('data-cold-search-action="submit"');
+    expect(html).toContain('data-cold-search-action="clear"');
     expect(html).toContain('data-cold-search-trailing-actions="detached-secondary"');
     expect(html).toContain('data-plugin-manage-table-shell="cold-dense-table"');
+    expect(html).toContain('data-plugin-table-stable-height="viewport-fill-on-empty-or-short"');
+    expect(html).toContain('data-plugin-table-fill-stage="viewport-fill-on-empty-or-short"');
     expect(html).toContain('data-plugin-manage-table="cold-plugin-table"');
+    expect(html).toContain('data-plugin-table-columns="angular-five-column-edit-actions"');
+    expect(html).not.toContain('>参数</th>');
+    expect(html).toContain('data-plugin-pagination="cold-dense-pagination"');
+    expect(html).toContain('data-plugin-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(html).toContain('data-plugin-pagination-contract="angular-search-pagination"');
+    expect(html).toContain('data-plugin-query-param-order-contract="angular-page-index-size-search"');
+    expect(html).toContain('data-plugin-query-param-order-owner="plugin-query-state"');
+    expect(html).toContain('data-hz-pagination-page-size="select-menu"');
+    expect(html).toContain('data-hz-pagination-page-jump="number-input"');
+    expect(html).toContain('data-plugin-pagination-page-jump-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-pagination-page-size-owner="hertzbeat-ui-select"');
     expect(html).toContain('data-plugin-select-all="cold-checkbox"');
+    expect(html).toContain('data-plugin-action-menu-clipping="none"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu="angular-toolbar-ellipsis-delete"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-contract="angular-toolbar-ellipsis-delete"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-open="false"');
+    expect(html).toContain('aria-expanded="false"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-trigger="angular-toolbar-ellipsis-delete"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-panel="angular-toolbar-ellipsis-delete"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-clearance-panel="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-plugin-toolbar-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-plugin-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-row-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-plugin-row-delete-menu-open="false"');
+    expect(html).toContain('data-plugin-row-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(html).toContain('data-plugin-row-delete-menu-clearance-panel="floating-overlay-no-panel-crop"');
+    expect(html).toContain('data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-row-delete-menu-panel-open="false"');
+    expect(html).toContain('data-plugin-delete-selected="angular-batch-delete-entry"');
+    expect(html).toContain('data-plugin-delete-selected-owner="hertzbeat-ui-table-row-action-button"');
     expect(html).toContain('data-plugin-row="1"');
     expect(html).toContain('data-plugin-row="2"');
-    expect(html).toContain('data-plugin-row-actions="cold-icon-actions"');
+    expect(html).toContain('data-plugin-row-delete-menu="1"');
+    expect(html).toContain('data-plugin-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(html).toContain('data-plugin-row-delete-menu-trigger="1"');
+    expect(html).toContain('data-plugin-row-delete-menu-panel="1"');
+    expect(html).toContain('data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-delete-one="1"');
+    expect(html).toContain('data-plugin-delete-one-owner="hertzbeat-ui-table-row-action-button"');
+    expect(html).toContain('data-plugin-row-actions="angular-row-actions-contextual"');
+    expect(html).toContain('data-plugin-row-actions-owner="cold-icon-actions"');
+    expect(html).toContain('data-plugin-row-action="params"');
+    expect(html).toContain('data-plugin-row-action="delete"');
+    expect(html).toContain('data-plugin-row-action-owner="row-contextual-icon-button"');
+    expect(html).toContain('data-plugin-row-action-label="smtp"');
     expect(html).not.toContain('data-plugin-summary-rail=');
     expect(html).not.toContain('data-cold-search-input-shell');
     expect(html).toContain('插件管理');
@@ -94,13 +176,29 @@ describe('plugin manage surface', () => {
     expect(html).toContain('slack');
     expect(html).toContain('告警后');
     expect(html).toContain('采集后');
+    expect(html).toContain('无类型');
     expect(html).toContain('已启用');
     expect(html).toContain('已停用');
     expect(html).toContain('已选择');
+    expect(html).toContain('第 1 / 1 页 · 1-2 / 2');
     expect(html).not.toContain('POST_ALERT');
     expect(html).not.toContain('POST_COLLECT');
-    expect(html).toContain('编辑参数');
+    expect(html).toContain('编辑插件参数 smtp');
+    expect(html).toContain('删除插件 smtp');
     expect(html).toContain('data-overlay-dialog="true"');
+    expect(html).toContain('data-overlay-dialog-mask-closable="false"');
+    expect(html).toContain('data-plugin-upload-mask-closable="false"');
+    expect(html).toContain('data-plugin-upload-width="angular-width-30-percent"');
+    expect(html).toContain('data-plugin-upload-modal-owner="angular-nz-modal"');
+    expect(html).toContain('data-plugin-upload-field-layout-contract="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-plugin-upload-field-layout="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-label-span="8"');
+    expect(html).toContain('data-plugin-upload-control-span="14"');
+    expect(html).toContain('data-plugin-upload-status-control="angular-nz-switch"');
+    expect(html).toContain('data-plugin-upload-status-control-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-hz-ui="switch"');
+    expect(html).toContain('role="switch"');
     expect(html).toContain('Jar包');
     expect(html).toContain('选择文件');
     expect(html).toContain('取消');
@@ -143,12 +241,62 @@ describe('plugin manage surface', () => {
     );
 
     expect(html).toContain('data-plugin-manage-table-shell="cold-dense-table"');
+    expect(html).toContain('data-plugin-table-stable-height="viewport-fill-on-empty-or-short"');
+    expect(html).toContain('data-plugin-table-stable-height-owner="route-layout-contract"');
+    expect(html).toContain('data-plugin-table-fill-stage="viewport-fill-on-empty-or-short"');
     expect(html).toContain('data-plugin-manage-table="cold-plugin-table"');
+    expect(html).toContain('colSpan="5"');
     expect(html).toContain('data-plugin-manage-empty-state="cold-table-empty"');
     expect(html).toContain('data-plugin-manage-empty-icon="cold-empty-box"');
     expect(html).toContain('暂无数据');
   });
 
+  it('keeps the Angular plugin management shell mounted without showing backend load errors', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={null}
+        loadError="backend refused plugin load"
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-manage-surface="otlp-cold-plugin-console"');
+    expect(html).toContain('data-plugin-load-failure-contract="angular-console-only-shell"');
+    expect(html).toContain('data-plugin-load-failure-owner="plugin-route-controller"');
+    expect(html).toContain('data-plugin-load-failure="angular-console-only-shell"');
+    expect(html).toContain('data-plugin-manage-toolbar="cold-table-toolbar"');
+    expect(html).toContain('data-plugin-manage-table-shell="cold-dense-table"');
+    expect(html).toContain('data-plugin-manage-empty-state="cold-table-empty"');
+    expect(html).toContain('插件管理');
+    expect(html).not.toContain('backend refused plugin load');
+    expect(html).not.toContain('data-observability-status');
+  });
+
   it('keeps the plugin page on the cold visual owner instead of WorkbenchPage or alert primitives', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/plugin-manage-surface.tsx'), 'utf8');
 
@@ -160,10 +308,240 @@ describe('plugin manage surface', () => {
     expect(source).toContain('data-plugin-header="cold-compact-header"');
     expect(source).toContain('data-plugin-command-row="standard-equal-buttons"');
     expect(source).toContain('data-plugin-admin-layout="full-width-admin-list"');
+    expect(source).toContain('data-plugin-enable-feedback-contract="angular-edit-notify"');
+    expect(source).toContain('data-plugin-enable-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-plugin-enable-optimistic-contract="angular-toggle-mutates-row-before-put"');
+    expect(source).toContain('data-plugin-enable-optimistic-owner="route-state-contract"');
+    expect(source).toContain('const resolveEnableStatus = (plugin: Plugin) => (');
+    expect(source).toContain("data-plugin-enable-optimistic-state={hasOptimisticEnableStatus ? 'overridden' : 'source'}");
+    expect(source).toContain("data-plugin-enable-optimistic-next-status={displayEnableStatus ? 'true' : 'false'}");
+    expect(source).toContain('data-plugin-table-loading-contract="angular-load-plugins-table"');
+    expect(source).toContain('data-plugin-table-loading-scope="load-search-refresh-pagination-mutation"');
+    expect(source).toContain('data-plugin-load-failure-contract="angular-console-only-shell"');
+    expect(source).toContain('data-plugin-load-failure-owner="plugin-route-controller"');
+    expect(source).toContain("data-plugin-load-failure={loadError ? 'angular-console-only-shell' : 'none'}");
+    expect(source).toContain("actionKind === 'enable'");
+    expect(source).toContain('data-plugin-enable-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-plugin-toggle-loading-contract="angular-nz-table-loading"');
+    expect(source).toContain('data-plugin-toggle-loading-contract-owner="angular-nz-table-loading"');
+    expect(source).toContain('data-plugin-selection-reset-contract="angular-load-clears-selection"');
+    expect(source).toContain('data-plugin-selection-reset-owner="route-state-contract"');
+    expect(source).toContain('data-plugin-table-columns-contract="angular-five-column-edit-actions"');
+    expect(source).toContain('data-plugin-table-columns-owner="angular-nz-table"');
+    expect(source).toContain('const tableLoading = Boolean(isLoadPending || isDeletePending || isTogglePending);');
+    expect(source).toContain("data-plugin-table-loading={tableLoading ? 'true' : 'false'}");
+    expect(source).toContain('data-plugin-table-loading-owner="angular-nz-table-loading"');
+    expect(source).toContain('data-plugin-delete-warning-contract="angular-no-select-warning"');
+    expect(source).toContain('data-plugin-delete-confirm-contract="angular-modal-confirm"');
+    expect(source).toContain('data-plugin-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(source).toContain('data-plugin-delete-feedback-contract="angular-delete-notify"');
+    expect(source).toContain('data-plugin-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-plugin-delete-failure-contract="angular-delete-fail-notification"');
+    expect(source).toContain('data-plugin-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-plugin-enable-failure-contract="angular-edit-fail-notification"');
+    expect(source).toContain('data-plugin-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-plugin-delete-failure={isDeleteFailure ? \'angular-delete-fail-notification\' : undefined}');
+    expect(source).toContain('data-plugin-delete-feedback-title={');
+    expect(source).toContain("data-plugin-delete-feedback-detail={isDeleteFailure ? 'backend-message' : undefined}");
+    expect(source).toContain('data-plugin-enable-failure={isEnableFailure ? \'angular-edit-fail-notification\' : undefined}');
+    expect(source).toContain('data-plugin-enable-failure-owner={isEnableFailure ? \'hertzbeat-ui-inline-feedback\' : undefined}');
+    expect(source).toContain('data-plugin-enable-feedback-title={');
+    expect(source).toContain("data-plugin-enable-feedback-detail={isEnableFailure ? 'backend-message' : undefined}");
+    expect(source).toContain('data-plugin-delete-query-contract="angular-repeated-ids-query"');
+    expect(source).toContain('data-plugin-delete-query-owner="route-mutation-contract"');
+    expect(source).toContain('data-plugin-delete-page-clamp-contract="angular-update-page-index"');
+    expect(source).toContain('data-plugin-table-stable-height-contract="viewport-fill-on-empty-or-short"');
+    expect(source).toContain('data-plugin-table-stable-height-owner="route-layout-contract"');
+    expect(source).toContain('data-plugin-upload-validation-contract="angular-required-before-submit"');
+    expect(source).toContain('data-plugin-upload-invalid-submit-contract="angular-mark-dirty-keep-open"');
+    expect(source).toContain('data-plugin-upload-invalid-submit-owner="route-validation-contract"');
+    expect(source).toContain('data-plugin-upload-loading-contract="angular-nz-ok-loading"');
+    expect(source).toContain('data-plugin-upload-payload-contract="angular-form-data"');
+    expect(source).toContain('data-plugin-upload-name-payload-contract="angular-raw-name-no-trim"');
+    expect(source).toContain('data-plugin-upload-name-payload-owner="plugin-manage-controller"');
+    expect(source).toContain('data-plugin-upload-save-lifecycle-contract="angular-close-success-keep-open-fail"');
+    expect(source).toContain('data-plugin-upload-save-lifecycle-owner="route-state-contract"');
+    expect(source).toContain('data-plugin-upload-success-reset-contract="angular-reset-form-after-success"');
+    expect(source).toContain('data-plugin-upload-success-reset-owner="route-state-contract"');
+    expect(source).toContain('data-plugin-upload-cancel-contract="angular-cancel-preserves-form"');
+    expect(source).toContain('data-plugin-upload-cancel-contract-owner="route-state-contract"');
+    expect(source).toContain('data-plugin-upload-cancel-pending-contract="angular-cancel-allowed-during-ok-loading"');
+    expect(source).toContain('data-plugin-upload-cancel-pending-owner="angular-nz-modal"');
+    expect(source).toContain('data-plugin-upload-mask-closable-contract="angular-mask-closable-false"');
+    expect(source).toContain('data-plugin-upload-width-contract="angular-width-30-percent"');
+    expect(source).toContain('data-plugin-upload-field-layout-contract="angular-label-8-control-14"');
+    expect(source).toContain('data-plugin-upload-field-layout-owner="route-form-field-grid"');
+    expect(source).toContain('data-plugin-upload-field-layout="angular-label-8-control-14"');
+    expect(source).toContain('data-plugin-upload-label-span="8"');
+    expect(source).toContain('data-plugin-upload-control-span="14"');
+    expect(source).toContain('sm:grid-cols-[minmax(96px,8fr)_minmax(0,14fr)]');
+    expect(source).toContain('data-plugin-upload-status-control-contract="angular-nz-switch"');
+    expect(source).toContain('data-plugin-upload-status-control-owner="hertzbeat-ui-switch"');
+    expect(source).toContain('data-plugin-upload-file-remove-contract="angular-nz-remove-clears-jar"');
+    expect(source).toContain('data-plugin-upload-file-remove-owner="hertzbeat-ui-file-input"');
+    expect(source).toContain('data-plugin-upload-modal-owner="angular-nz-modal"');
+    expect(source).toContain('data-plugin-upload-feedback-contract="angular-new-notify"');
+    expect(source).toContain('data-plugin-upload-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-plugin-upload-failure-contract="angular-new-fail-notification"');
+    expect(source).toContain('data-plugin-upload-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain("const isUploadFailure = actionKind === 'upload' && Boolean(actionError);");
+    expect(source).toContain("const isParamFailure = actionKind === 'params' && Boolean(actionError);");
+    expect(source).toContain("? t('common.notify.edit-fail')");
+    expect(source).toContain('const actionFeedbackMeta = isUploadFailure || isParamFailure || isEnableFailure || isDeleteFailure ? actionMeta ?? actionError ?? undefined : undefined;');
+    expect(source).toContain('data-plugin-upload-feedback-title={');
+    expect(source).toContain('data-plugin-upload-feedback-detail={');
+    expect(source).toContain('HzFileInput');
+    expect(source).toContain('data-plugin-upload-open="angular-upload-modal"');
+    expect(source).toContain('data-plugin-upload-file-input="angular-jar-before-upload"');
+    expect(source).toContain('data-plugin-upload-name-payload="angular-raw-name-no-trim"');
+    expect(source).toContain('data-plugin-upload-file-list="angular-before-upload-single-replace"');
+    expect(source).toContain('data-plugin-upload-cancel-pending="angular-cancel-allowed-during-ok-loading"');
+    expect(source).toContain('event.target.files?.[0]');
+    expect(source).toContain("pluginJarInputRef.current.value = '';");
+    expect(source).toContain('data-plugin-upload-file-remove="angular-nz-remove-clears-jar"');
+    expect(source).toContain("jarFileName: ''");
+    expect(source).toContain('data-plugin-upload-save="angular-form-data"');
+    expect(source).toContain('open={Boolean((isUploadDialogOpen ?? Boolean(draftPlugin)) && draftPlugin)}');
+    expect(source).toContain('data-plugin-upload-dialog="angular-cancel-preserves-form"');
+    expect(source).toContain('data-plugin-upload-mask-closable');
+    expect(source).toContain('data-plugin-upload-width');
+    expect(source).toContain('max-w-[min(92vw,30rem)] lg:max-w-[30vw]');
+    expect(source).toContain("data-plugin-upload-ok-loading={isUploadPending ? 'true' : 'false'}");
+    expect(source).toContain('data-plugin-upload-ok-loading-owner="angular-nz-ok-loading"');
+    expect(source).toContain('data-plugin-param-edit-contract="angular-params-define-modal"');
+    expect(source).toContain('data-plugin-param-action-visibility-contract="angular-paramcount-zero-only"');
+    expect(source).toContain('data-plugin-param-action-visibility-owner="plugin-view-model"');
+    expect(source).toContain('data-plugin-param-mask-closable-contract="angular-mask-closable-false"');
+    expect(source).toContain('data-plugin-param-modal-owner="angular-nz-modal"');
+    expect(source).toContain('data-plugin-param-mask-closable');
+    expect(source).toContain('data-plugin-param-save-contract="angular-params-post"');
+    expect(source).toContain('data-plugin-param-payload-contract="angular-object-values-payload"');
+    expect(source).toContain('data-plugin-param-save-lifecycle-contract="angular-close-success-keep-open-fail"');
+    expect(source).toContain('data-plugin-param-save-lifecycle-owner="route-state-contract"');
+    expect(source).toContain('data-plugin-param-save-loading-contract="angular-no-ok-loading"');
+    expect(source).toContain('data-plugin-param-save-loading-owner="angular-modal-ok-contract"');
+    expect(source).toContain('data-plugin-param-save-loading="angular-no-ok-loading"');
+    expect(source).toContain("data-plugin-param-save-pending={isParamPending ? 'true' : 'false'}");
+    expect(source).toContain('data-plugin-param-payload="angular-object-values-payload"');
+    expect(source).toContain('data-plugin-param-save-lifecycle="angular-close-success-keep-open-fail"');
+    expect(source).toContain('data-plugin-param-feedback-contract="angular-edit-notify"');
+    expect(source).toContain('data-plugin-param-failure-contract="angular-edit-fail-notification"');
+    expect(source).toContain('data-plugin-param-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain("data-plugin-param-failure={isParamFailure ? 'angular-edit-fail-notification' : undefined}");
+    expect(source).toContain("data-plugin-param-feedback-title={");
+    expect(source).toContain("data-plugin-param-feedback-detail={isParamFailure ? 'backend-message' : undefined}");
+    expect(source).toContain('data-plugin-param-pending-editable-contract="angular-controls-remain-enabled"');
+    expect(source).toContain('data-plugin-param-pending-editable-owner="angular-param-modal"');
+    expect(source).toContain("data-plugin-param-pending-editable={isParamPending ? 'angular-controls-remain-enabled' : 'idle'}");
+    expect(source).not.toContain('disabled={isParamPending}');
+    expect(source).toContain('data-plugin-param-width-contract="angular-default-modal-width"');
+    expect(source).toContain('data-plugin-param-width-owner="angular-nz-modal"');
+    expect(source).toContain('maxWidthClassName="max-w-[min(92vw,520px)]"');
+    expect(source).toContain("'data-plugin-param-width': 'angular-default-modal-width'");
+    expect(source).toContain("'data-plugin-param-width-owner': 'angular-nz-modal'");
+    expect(source).not.toContain('maxWidthClassName="max-w-3xl"');
+    expect(source).toContain('HzRadioButtonGroup');
+    expect(source).toContain('data-plugin-param-radio-contract="angular-nz-radio-group"');
+    expect(source).toContain('data-plugin-param-radio-owner="hertzbeat-ui-radio-button-group"');
+    expect(source).toContain("define.type === 'radio' && (define.options ?? []).length > 0");
+    expect(source).toContain('data-plugin-param-radio="angular-nz-radio-group"');
+    expect(source).toContain('data-plugin-param-radio-field={define.field}');
+    expect(source).toContain('data-plugin-param-textarea-contract="angular-textarea-rows-8"');
+    expect(source).toContain('data-plugin-param-textarea-owner="hertzbeat-ui-textarea"');
+    expect(source).toContain('function getPluginParamTextareaRows(define: ParamDefine): 4 | 8');
+    expect(source).toContain("return define.type === 'json' || define.type === 'textarea' ? 8 : 4;");
+    expect(source).toContain('rows={getPluginParamTextareaRows(define)}');
+    expect(source).toContain("data-plugin-param-textarea={define.type === 'textarea' ? 'angular-textarea-rows-8' : undefined}");
+    expect(source).toContain("data-plugin-param-textarea-rows={define.type === 'textarea' ? '8' : undefined}");
+    expect(source).toContain('data-plugin-param-field-layout-contract="angular-label-7-control-8"');
+    expect(source).toContain('data-plugin-param-field-layout-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(source).toContain('data-plugin-param-form-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(source).toContain("t('setting.plugins.action.params-aria', actionLabelParams)");
+    expect(source).toContain("t('setting.plugins.action.delete-aria', actionLabelParams)");
+    expect(source).toContain('data-plugin-param-edit-open={String(original.id)}');
+    expect(source).toContain('data-plugin-row-actions="angular-row-actions-contextual"');
+    expect(source).toContain('data-plugin-row-actions-owner="cold-icon-actions"');
+    expect(source).toContain('data-plugin-row-action-owner="row-contextual-icon-button"');
+    expect(source).toContain('data-plugin-row-action-label={row.name}');
+    expect(source).toContain('const [openRowActionMenuId, setOpenRowActionMenuId] = React.useState<string | null>(null)');
+    expect(source).toContain('const rowActionMenuId = String(original.id);');
+    expect(source).toContain('const isRowActionMenuOpen = openRowActionMenuId === rowActionMenuId;');
+    expect(source).toContain('data-plugin-row-delete-menu={rowActionMenuId}');
+    expect(source).toContain('data-plugin-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(source).toContain('data-plugin-row-delete-menu-layer="overlay-visible-above-panel"');
+    expect(source).toContain("data-plugin-row-delete-menu-open={isRowActionMenuOpen ? 'true' : 'false'}");
+    expect(source).toContain('aria-expanded={isRowActionMenuOpen}');
+    expect(source).toContain('setOpenRowActionMenuId(current => (current === rowActionMenuId ? null : rowActionMenuId))');
+    expect(source).toContain('data-plugin-row-delete-menu-trigger={rowActionMenuId}');
+    expect(source).toContain('hidden={!isRowActionMenuOpen}');
+    expect(source).toContain('data-plugin-row-delete-menu-panel={rowActionMenuId}');
+    expect(source).toContain('data-plugin-row-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(source).toContain('data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain("data-plugin-row-delete-menu-panel-open={isRowActionMenuOpen ? 'true' : 'false'}");
+    expect(source).toContain('setOpenRowActionMenuId(null);');
+    expect(source).not.toContain('<details');
+    expect(source).not.toContain('</details>');
+    expect(source).toContain('data-plugin-param-dialog="angular-params-define-modal"');
+    expect(source).toContain('data-plugin-param-form="angular-dynamic-form-field"');
+    expect(source).toContain('data-plugin-param-field-layout="angular-label-7-control-8"');
+    expect(source).toContain('data-plugin-param-label-span="7"');
+    expect(source).toContain('data-plugin-param-control-span="8"');
+    expect(source).toContain('data-plugin-param-save="angular-params-post"');
+    expect(source).toContain('HzMonitorEditorFieldGrid');
+    expect(source).toContain('HzTextarea');
+    expect(source).toContain('HzPaginationBar');
+    expect(source).toContain('pageSizeOptions = [8, 15, 25]');
+    expect(source).toContain('data-plugin-pagination="cold-dense-pagination"');
+    expect(source).toContain('data-plugin-pagination-owner="hertzbeat-ui-pagination-bar"');
+    expect(source).toContain('data-plugin-pagination-contract="angular-search-pagination"');
+    expect(source).toContain('data-plugin-query-param-order-contract="angular-page-index-size-search"');
+    expect(source).toContain('data-plugin-query-param-order-owner="plugin-query-state"');
+    expect(source).toContain("t('setting.plugins.pagination.summary'");
+    expect(source).toContain("t('setting.plugins.pagination.page-size')");
+    expect(source).toContain("t('setting.plugins.pagination.page')");
+    expect(source).toContain('data-plugin-pagination-page-jump-owner');
+    expect(source).toContain('data-plugin-pagination-page-size-owner');
+    expect(source).toContain('data-plugin-delete-feedback={');
+    expect(source).toContain('HzTableRowActionButton');
+    expect(source).toContain('data-plugin-toolbar-delete-menu="angular-toolbar-ellipsis-delete"');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-contract="angular-toolbar-ellipsis-delete"');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-layer="overlay-visible-above-panel"');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-open={isToolbarActionMenuOpen ?');
+    expect(source).toContain('setToolbarActionMenuOpen(open => !open)');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-trigger="angular-toolbar-ellipsis-delete"');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-panel="angular-toolbar-ellipsis-delete"');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-layer-panel="overlay-visible-above-panel"');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-clearance-panel="floating-overlay-no-panel-crop"');
+    expect(source).toContain('data-plugin-toolbar-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('data-plugin-row-delete-menu-contract="angular-ellipsis-dropdown-delete"');
+    expect(source).toContain('data-plugin-row-delete-menu-clearance="floating-overlay-no-panel-crop"');
+    expect(source).toContain('data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('pluginActionMenuRootClassName');
+    expect(source).toContain('pluginActionMenuPanelBaseClassName');
+    expect(source).toContain('data-plugin-action-menu-clipping="none"');
+    expect(source).toContain("style={{ overflow: 'visible' }}");
+    expect(source).toContain('z-[9999] inline-block overflow-visible');
+    expect(source).toContain('mt-2 z-[9999] min-w-[132px]');
+    expect(source).toContain('data-plugin-delete-selected="angular-batch-delete-entry"');
+    expect(source).toContain('data-plugin-delete-selected-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('data-plugin-delete-one={String(original.id)}');
+    expect(source).toContain('data-plugin-delete-one-owner="hertzbeat-ui-table-row-action-button"');
+    expect(source).toContain('HzConfirmDialog');
+    expect(source).toContain('HzInlineFeedback');
     expect(source).toContain('data-plugin-manage-toolbar="cold-table-toolbar"');
     expect(source).toContain('data-plugin-manage-search-owner="shared-search-row"');
+    expect(source).toContain('data-plugin-search-submit-contract="angular-enter-and-clear"');
+    expect(source).toContain('data-plugin-search-clear-contract="angular-cleared-load"');
+    expect(source).toContain('data-plugin-search-clear-owner="shared-search-row"');
+    expect(source).toContain('onClear={onSearchClear}');
     expect(source).toContain('data-plugin-manage-table-shell="cold-dense-table"');
+    expect(source).toContain('data-plugin-table-stable-height="viewport-fill-on-empty-or-short"');
+    expect(source).toContain('data-plugin-table-fill-stage="viewport-fill-on-empty-or-short"');
     expect(source).toContain('data-plugin-manage-table="cold-plugin-table"');
+    expect(source).toContain('data-plugin-table-columns="angular-five-column-edit-actions"');
+    expect(source).toContain('colSpan={5}');
+    expect(source).not.toContain("t('setting.plugins.table.params')");
     expect(source).toContain('data-plugin-manage-empty-state="cold-table-empty"');
     expect(source).not.toContain('data-plugin-summary-rail');
     expect(source).not.toContain('coldPluginVisual.search.row');
@@ -207,7 +585,7 @@ describe('plugin manage surface', () => {
     expect(source).not.toContain('className="inline-flex min-h-7 items-center rounded-[2px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-2 text-[12px] text-[var(--ops-text-primary)]"');
   });
 
-  it('uses the shared cold checkbox for table selection and draft enable state', async () => {
+  it('uses cold checkboxes for table selection and the shared switch for draft enable state', async () => {
     const { PluginManageSurface } = await import('./plugin-manage-surface');
     const t = createTranslatorMock({ locale: 'zh-CN' });
 
@@ -262,17 +640,656 @@ describe('plugin manage surface', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/pages/plugin-manage-surface.tsx'), 'utf8');
 
     expect(source).toContain("from '../ui/checkbox'");
+    expect(source).toContain('HzSwitch');
     expect(source).toContain('data-plugin-select-all="cold-checkbox"');
     expect(source).toContain('data-plugin-row-select={String(original.id)}');
-    expect(source).toContain('data-plugin-draft-enabled-checkbox="cold-checkbox"');
+    expect(source).toContain('data-plugin-upload-status-control="angular-nz-switch"');
     expect(source).not.toContain('className="h-3.5 w-3.5 accent-[#4e74f8]"');
     expect(html).toContain('data-plugin-select-all="cold-checkbox"');
     expect(html).toContain('data-plugin-row-select="11"');
     expect(html).toContain('data-plugin-row-select="12"');
-    expect(html).toContain('data-plugin-draft-enabled-checkbox="cold-checkbox"');
-    expect(html.match(/data-cold-checkbox-owner="cold-checkbox"/g)?.length ?? 0).toBe(4);
-    expect(html.match(/data-cold-checkbox-control="native-hidden"/g)?.length ?? 0).toBe(4);
-    expect(html.match(/data-cold-checkbox-box="indicator"/g)?.length ?? 0).toBe(4);
-    expect(html).toContain('data-cold-checkbox-label="true"');
+    expect(html).toContain('data-plugin-upload-status-control="angular-nz-switch"');
+    expect(html).toContain('data-plugin-upload-status-control-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-hz-ui="switch"');
+    expect(html.match(/data-cold-checkbox-owner="cold-checkbox"/g)?.length ?? 0).toBe(3);
+    expect(html.match(/data-cold-checkbox-control="native-hidden"/g)?.length ?? 0).toBe(3);
+    expect(html.match(/data-cold-checkbox-box="indicator"/g)?.length ?? 0).toBe(3);
+    expect(html).toContain('data-hz-switch-owner="hertzbeat-ui-switch"');
+  });
+
+  it('renders Angular plugin delete warning, confirmation, feedback, and page-clamp contracts through shared UI', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [
+              {
+                id: 21,
+                name: 'webhook',
+                enableStatus: true,
+                items: [{ type: 'POST_ALERT' }],
+                paramCount: 0
+              }
+            ],
+            totalElements: 1,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={null}
+        actionError="请先选择要删除的监控"
+        actionTone="warning"
+        actionKind="delete"
+        deleteTarget={{
+          ids: [21],
+          label: 'webhook',
+          mode: 'single'
+        }}
+        isDeletePending
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-delete-warning-contract="angular-no-select-warning"');
+    expect(html).toContain('data-plugin-delete-confirm-contract="angular-modal-confirm"');
+    expect(html).toContain('data-plugin-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-plugin-delete-feedback-contract="angular-delete-notify"');
+    expect(html).toContain('data-plugin-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-delete-query-contract="angular-repeated-ids-query"');
+    expect(html).toContain('data-plugin-delete-query-owner="route-mutation-contract"');
+    expect(html).toContain('data-plugin-delete-page-clamp-contract="angular-update-page-index"');
+    expect(html).toContain('data-plugin-query-param-order-contract="angular-page-index-size-search"');
+    expect(html).toContain('data-plugin-query-param-order-owner="plugin-query-state"');
+    expect(html).toContain('data-plugin-table-stable-height-contract="viewport-fill-on-empty-or-short"');
+    expect(html).toContain('data-plugin-table-stable-height-owner="route-layout-contract"');
+    expect(html).toContain('data-plugin-delete-feedback="angular-no-select-warning"');
+    expect(html).toContain('data-plugin-delete-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-delete-confirm="angular-modal-confirm"');
+    expect(html).toContain('data-plugin-delete-confirm-owner="hertzbeat-ui-confirm-dialog"');
+    expect(html).toContain('data-plugin-delete-confirm-submit="angular-modal-confirm"');
+    expect(html).toContain('data-plugin-delete-confirm-target="webhook"');
+    expect(html).toContain('请先选择要删除的监控');
+    expect(html).toContain('请确认是否删除!');
+  });
+
+  it('renders Angular plugin upload required validation, jar file input, and save feedback contracts', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={{
+          name: '',
+          jarFileName: '',
+          jarFile: null,
+          enableStatus: true
+        }}
+        actionMessage="Add Success!"
+        actionKind="upload"
+        uploadValidation={{ name: true, jarFile: true }}
+        isUploadPending
+        deleteTarget={null}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-upload-validation-contract="angular-required-before-submit"');
+    expect(html).toContain('data-plugin-upload-invalid-submit-contract="angular-mark-dirty-keep-open"');
+    expect(html).toContain('data-plugin-upload-invalid-submit-owner="route-validation-contract"');
+    expect(html).toContain('data-plugin-upload-loading-contract="angular-nz-ok-loading"');
+    expect(html).toContain('data-plugin-upload-payload-contract="angular-form-data"');
+    expect(html).toContain('data-plugin-upload-name-payload-contract="angular-raw-name-no-trim"');
+    expect(html).toContain('data-plugin-upload-name-payload-owner="plugin-manage-controller"');
+    expect(html).toContain('data-plugin-upload-save-lifecycle-contract="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-upload-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-upload-success-reset-contract="angular-reset-form-after-success"');
+    expect(html).toContain('data-plugin-upload-success-reset-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-upload-cancel-contract="angular-cancel-preserves-form"');
+    expect(html).toContain('data-plugin-upload-cancel-contract-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-upload-cancel-pending-contract="angular-cancel-allowed-during-ok-loading"');
+    expect(html).toContain('data-plugin-upload-cancel-pending-owner="angular-nz-modal"');
+    expect(html).toContain('data-plugin-upload-feedback-contract="angular-new-notify"');
+    expect(html).toContain('data-plugin-upload-feedback-contract-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-upload-failure-contract="angular-new-fail-notification"');
+    expect(html).toContain('data-plugin-upload-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-upload-field-layout-contract="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-field-layout-owner="route-form-field-grid"');
+    expect(html).toContain('data-plugin-upload-field="name"');
+    expect(html).toContain('data-plugin-upload-field="jarFile"');
+    expect(html).toContain('data-plugin-upload-cancel-pending="angular-cancel-allowed-during-ok-loading"');
+    expect(html).toContain('data-plugin-upload-field="enableStatus"');
+    expect(html).toContain('data-plugin-upload-field-layout="angular-label-8-control-14"');
+    expect(html).toContain('data-plugin-upload-label-span="8"');
+    expect(html).toContain('data-plugin-upload-control-span="14"');
+    expect(html).toContain('data-plugin-upload-feedback="angular-new-notify"');
+    expect(html).toContain('data-plugin-upload-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-upload-name-input="angular-required"');
+    expect(html).toContain('data-plugin-upload-name-payload="angular-raw-name-no-trim"');
+    expect(html).toContain('data-plugin-upload-file-input="angular-jar-before-upload"');
+    expect(html).toContain('data-plugin-upload-file-input-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-plugin-upload-file-list="angular-before-upload-single-replace"');
+    expect(html).toContain('data-plugin-upload-file-list-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-plugin-upload-file-remove-contract="angular-nz-remove-clears-jar"');
+    expect(html).toContain('data-plugin-upload-file-remove-owner="hertzbeat-ui-file-input"');
+    expect(html).toContain('data-plugin-upload-file-remove="angular-nz-remove-clears-jar"');
+    expect(html).toContain('data-plugin-upload-file-remove-state="empty"');
+    expect(html).toContain('data-plugin-upload-file-trigger="angular-jar-before-upload"');
+    expect(html).toContain('data-plugin-upload-file-name="angular-jar-before-upload"');
+    expect(html).toContain('data-plugin-upload-status-field="angular-nz-switch"');
+    expect(html).toContain('data-plugin-upload-status-control="angular-nz-switch"');
+    expect(html).toContain('data-plugin-upload-status-control-owner="hertzbeat-ui-switch"');
+    expect(html).toContain('data-plugin-upload-dialog="angular-cancel-preserves-form"');
+    expect(html).toContain('data-plugin-upload-invalid-submit="angular-mark-dirty-keep-open"');
+    expect(html).toContain('data-plugin-upload-invalid-submit-owner="route-validation-contract"');
+    expect(html).toContain('data-plugin-upload-save="angular-form-data"');
+    expect(html).toContain('data-plugin-upload-save-lifecycle="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-upload-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-upload-ok-loading="true"');
+    expect(html).toContain('data-plugin-upload-ok-loading-owner="angular-nz-ok-loading"');
+    expect(html).toContain('aria-busy="true"');
+    expect(html).toContain('data-plugin-upload-validation="name-required"');
+    expect(html).toContain('data-plugin-upload-validation="jar-required"');
+    expect(html).toContain('Add Success!');
+    expect(html).toContain('请填充必填项!');
+  });
+
+  it('renders Angular plugin upload failure title with backend detail', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={null}
+        actionError="backend rejected duplicate plugin"
+        actionKind="upload"
+        deleteTarget={null}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-upload-feedback="angular-new-fail"');
+    expect(html).toContain('data-plugin-upload-feedback-title="common.notify.new-fail"');
+    expect(html).toContain('data-plugin-upload-feedback-detail="backend-message"');
+    expect(html).toContain('新增失败!');
+    expect(html).toContain('backend rejected duplicate plugin');
+  });
+
+  it('renders Angular plugin delete failure title with backend detail', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={null}
+        actionError="删除失败!"
+        actionMeta="backend refused plugin delete"
+        actionKind="delete"
+        deleteTarget={null}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-delete-feedback="angular-delete-fail"');
+    expect(html).toContain('data-plugin-delete-failure="angular-delete-fail-notification"');
+    expect(html).toContain('data-plugin-delete-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-delete-feedback-title="common.notify.delete-fail"');
+    expect(html).toContain('data-plugin-delete-feedback-detail="backend-message"');
+    expect(html).toContain('删除失败!');
+    expect(html).toContain('backend refused plugin delete');
+  });
+
+  it('renders Angular plugin enable failure title with backend detail', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={null}
+        actionError="修改失败!"
+        actionMeta="backend refused plugin toggle"
+        actionKind="enable"
+        deleteTarget={null}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-enable-feedback="angular-edit-fail"');
+    expect(html).toContain('data-plugin-enable-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-plugin-enable-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-enable-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-plugin-enable-feedback-detail="backend-message"');
+    expect(html).toContain('修改失败!');
+    expect(html).toContain('backend refused plugin toggle');
+  });
+
+  it('renders Angular plugin parameter failure title with backend detail', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [],
+            totalElements: 0,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={null}
+        actionError="backend rejected plugin params"
+        actionKind="params"
+        deleteTarget={null}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-param-feedback="angular-edit-fail"');
+    expect(html).toContain('data-plugin-param-failure="angular-edit-fail-notification"');
+    expect(html).toContain('data-plugin-param-failure-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-param-feedback-title="common.notify.edit-fail"');
+    expect(html).toContain('data-plugin-param-feedback-detail="backend-message"');
+    expect(html).toContain('修改失败!');
+    expect(html).toContain('backend rejected plugin params');
+  });
+
+  it('renders Angular plugin parameter define modal and save feedback contracts', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({ locale: 'zh-CN' });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{
+          list: {
+            content: [
+              {
+                id: 31,
+                name: 'webhook',
+                enableStatus: true,
+                items: [{ type: 'POST_ALERT' }],
+                paramCount: 2
+              }
+            ],
+            totalElements: 1,
+            pageIndex: 0,
+            pageSize: 8
+          }
+        }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={null}
+        paramDraft={{
+          plugin: { id: 31, name: 'webhook', enableStatus: true } as any,
+          paramDefines: [
+            { field: 'endpoint', type: 'text', name: '端点', required: true },
+            { field: 'authType', type: 'radio', name: '认证类型', options: [{ label: 'Basic', value: 'basic' }, { label: 'Bearer', value: 'bearer' }] },
+            { field: 'script', type: 'textarea', name: '脚本' },
+            { field: 'payload', type: 'json', name: 'JSON' },
+            { field: 'headers', type: 'key-value', name: '请求头' },
+            { field: 'labels', type: 'labels', name: '标签' },
+            { field: 'selector', type: 'label-selector', name: '标签选择器' },
+            { field: 'metrics', type: 'metrics-field', name: '指标字段' },
+            { field: 'recipients', type: 'array', name: '收件人' }
+          ],
+          params: {
+            endpoint: { pluginMetadataId: 31, type: 1, field: 'endpoint', paramValue: 'https://hooks.example' },
+            authType: { pluginMetadataId: 31, type: 2, field: 'authType', paramValue: 'basic' },
+            script: { pluginMetadataId: 31, type: 2, field: 'script', paramValue: 'line one\nline two' },
+            payload: { pluginMetadataId: 31, type: 3, field: 'payload', paramValue: '{"ok":true}' },
+            headers: { pluginMetadataId: 31, type: 2, field: 'headers', paramValue: '{"Authorization":"Bearer token"}' },
+            labels: { pluginMetadataId: 31, type: 2, field: 'labels', paramValue: { env: 'prod' } },
+            selector: { pluginMetadataId: 31, type: 2, field: 'selector', paramValue: { service: 'checkout', severity: 'critical' } },
+            metrics: { pluginMetadataId: 31, type: 2, field: 'metrics', paramValue: [{ field: 'usage', unit: '%', type: 'number' }] },
+            recipients: { pluginMetadataId: 31, type: 2, field: 'recipients', paramValue: 'alpha,beta' }
+          }
+        }}
+        actionMessage="Edit Success!"
+        actionKind="params"
+        isParamPending
+        deleteTarget={null}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onParamChange={() => {}}
+        onCloseParamDialog={() => {}}
+        onSaveParamDialog={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-param-edit-contract="angular-params-define-modal"');
+    expect(html).toContain('data-plugin-param-action-visibility-contract="angular-paramcount-zero-only"');
+    expect(html).toContain('data-plugin-param-action-visibility-owner="plugin-view-model"');
+    expect(html).toContain('data-plugin-param-save-contract="angular-params-post"');
+    expect(html).toContain('data-plugin-param-payload-contract="angular-object-values-payload"');
+    expect(html).toContain('data-plugin-param-save-lifecycle-contract="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-param-save-lifecycle-owner="route-state-contract"');
+    expect(html).toContain('data-plugin-param-save-loading-contract="angular-no-ok-loading"');
+    expect(html).toContain('data-plugin-param-save-loading="angular-no-ok-loading"');
+    expect(html).toContain('data-plugin-param-save-loading-owner="angular-modal-ok-contract"');
+    expect(html).toContain('data-plugin-param-save-pending="true"');
+    expect(html).toContain('data-plugin-param-payload="angular-object-values-payload"');
+    expect(html).toContain('data-plugin-param-save-lifecycle="angular-close-success-keep-open-fail"');
+    expect(html).toContain('data-plugin-param-feedback-contract="angular-edit-notify"');
+    expect(html).toContain('data-plugin-param-pending-editable-contract="angular-controls-remain-enabled"');
+    expect(html).toContain('data-plugin-param-pending-editable="angular-controls-remain-enabled"');
+    expect(html).toContain('data-plugin-param-pending-editable-owner="angular-param-modal"');
+    expect(html).toContain('data-plugin-param-empty-contract="angular-empty-params-modal"');
+    expect(html).toContain('data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-plugin-param-width-contract="angular-default-modal-width"');
+    expect(html).toContain('data-plugin-param-width="angular-default-modal-width"');
+    expect(html).toContain('data-plugin-param-width-owner="angular-nz-modal"');
+    expect(html).toContain('max-w-[min(92vw,520px)]');
+    expect(html).not.toContain('max-w-3xl');
+    expect(html).toContain('data-plugin-param-radio-contract="angular-nz-radio-group"');
+    expect(html).toContain('data-plugin-param-radio-owner="hertzbeat-ui-radio-button-group"');
+    expect(html).toContain('data-plugin-param-textarea-contract="angular-textarea-rows-8"');
+    expect(html).toContain('data-plugin-param-textarea-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-plugin-param-advanced-fields-contract="angular-configurable-and-multi-func"');
+    expect(html).toContain('data-plugin-param-advanced-fields-owner="hertzbeat-ui-advanced-param-controls"');
+    expect(html).toContain('data-plugin-param-configurable-field-contract="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-configurable-field-owner="hertzbeat-ui-configurable-field"');
+    expect(html).toContain('data-plugin-param-key-value-contract="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-key-value-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-plugin-param-labels-contract="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-labels-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-plugin-param-label-selector-contract="angular-app-label-selector"');
+    expect(html).toContain('data-plugin-param-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-plugin-param-metrics-field-contract="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-metrics-field-owner="hertzbeat-ui-configurable-field-editor"');
+    expect(html).toContain('data-plugin-param-array-contract="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-array-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-param-field-layout-contract="angular-label-7-control-8"');
+    expect(html).toContain('data-plugin-param-field-layout-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-plugin-param-form-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-plugin-param-feedback="angular-edit-notify"');
+    expect(html).toContain('data-plugin-param-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-plugin-param-edit-open="31"');
+    expect(html).toContain('data-plugin-param-dialog="angular-params-define-modal"');
+    expect(html).toContain('data-plugin-param-dialog-plugin="webhook"');
+    expect(html).toContain('data-plugin-param-form="angular-dynamic-form-field"');
+    expect(html).toContain('data-plugin-param-field-layout="angular-label-7-control-8"');
+    expect(html).toContain('data-hz-ui="monitor-editor-field-grid"');
+    expect(html).toContain('data-plugin-param-field="endpoint"');
+    expect(html).toContain('data-plugin-param-label-span="7"');
+    expect(html).toContain('data-plugin-param-control-span="8"');
+    expect(html).toContain('data-plugin-param-field-type="text"');
+    expect(html).toContain('data-plugin-param-field-required="true"');
+    expect(html).toContain('data-plugin-param-input="endpoint"');
+    expect(html).toContain('data-plugin-param-input-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-param-multi-func-input="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-multi-func-type="text"');
+    expect(html).toContain('data-plugin-param-multi-func-clear="angular-allow-clear"');
+    expect(html).toContain('data-plugin-param-multi-func-clear-field="endpoint"');
+    expect(html).toContain('data-plugin-param-multi-func-clear-owner="hertzbeat-ui-input-affordance"');
+    expect(html).toContain('data-plugin-param-field="authType"');
+    expect(html).toContain('data-plugin-param-field-type="radio"');
+    expect(html).toContain('data-plugin-param-radio="angular-nz-radio-group"');
+    expect(html).toContain('data-plugin-param-radio-field="authType"');
+    expect(html).toContain('data-hz-ui="radio-button-group"');
+    expect(html).toContain('data-hz-radio-button-option="basic"');
+    expect(html).toContain('data-hz-radio-button-option="bearer"');
+    expect(html).toContain('data-hz-radio-button-checked="true"');
+    expect(html).toContain('data-plugin-param-field="script"');
+    expect(html).toContain('data-plugin-param-field-type="textarea"');
+    expect(html).toContain('data-plugin-param-textarea="angular-textarea-rows-8"');
+    expect(html).toContain('data-plugin-param-textarea-rows="8"');
+    expect(html).toContain('rows="8"');
+    expect(html).toContain('data-plugin-param-field="payload"');
+    expect(html).toContain('data-plugin-param-input-owner="hertzbeat-ui-textarea"');
+    expect(html).toContain('data-plugin-param-field="headers"');
+    expect(html).toContain('data-plugin-param-field-type="key-value"');
+    expect(html).toContain('data-plugin-param-configurable-field="angular-app-configurable-field"');
+    expect(html).toContain('data-plugin-param-key-value-editor="headers"');
+    expect(html).toContain('data-plugin-param-key-value-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-hz-ui="key-value-editor"');
+    expect(html).toContain('data-hz-key-value-input="key"');
+    expect(html).toContain('data-hz-key-value-input="value"');
+    expect(html).toContain('data-plugin-param-key-value-input="key"');
+    expect(html).toContain('data-plugin-param-key-value-input="value"');
+    expect(html).toContain('data-plugin-param-field="labels"');
+    expect(html).toContain('data-plugin-param-field-type="labels"');
+    expect(html).toContain('data-plugin-param-labels-editor="labels"');
+    expect(html).toContain('data-plugin-param-labels-owner="hertzbeat-ui-key-value-editor"');
+    expect(html).toContain('data-plugin-param-labels-input="key"');
+    expect(html).toContain('data-plugin-param-labels-input="value"');
+    expect(html).toContain('data-plugin-param-field="selector"');
+    expect(html).toContain('data-plugin-param-field-type="label-selector"');
+    expect(html).toContain('data-plugin-param-label-selector="angular-app-label-selector"');
+    expect(html).toContain('data-plugin-param-label-selector-field="selector"');
+    expect(html).toContain('data-plugin-param-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-cold-label-selector-owner="cold-label-selector"');
+    expect(html).toContain('data-cold-label-selector-record-row="service:checkout"');
+    expect(html).toContain('data-cold-label-selector-record-row="severity:critical"');
+    expect(html).toContain('data-cold-label-selector-key-input="searchable-key"');
+    expect(html).toContain('data-cold-label-selector-value-input="searchable-value"');
+    expect(html).toContain('data-cold-label-selector-value="hidden"');
+    expect(html).toContain('data-plugin-param-field="metrics"');
+    expect(html).toContain('data-plugin-param-field-type="metrics-field"');
+    expect(html).toContain('data-plugin-param-metrics-field-editor="metrics"');
+    expect(html).toContain('data-plugin-param-metrics-field-owner="hertzbeat-ui-configurable-field-editor"');
+    expect(html).toContain('data-hz-ui="configurable-field-editor"');
+    expect(html).toContain('data-hz-configurable-field-input="field"');
+    expect(html).toContain('data-hz-configurable-field-input="unit"');
+    expect(html).toContain('data-hz-configurable-field-input="type"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="field"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="unit"');
+    expect(html).toContain('data-plugin-param-metrics-field-input="type"');
+    expect(html).toContain('data-plugin-param-field="recipients"');
+    expect(html).toContain('data-plugin-param-field-type="array"');
+    expect(html).toContain('data-plugin-param-input="recipients"');
+    expect(html).toContain('data-plugin-param-array="angular-app-multi-func-input"');
+    expect(html).toContain('data-plugin-param-array-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-plugin-param-multi-func-type="array"');
+    expect(html).toContain('data-plugin-param-multi-func-clear-field="recipients"');
+    expect(html).toContain('data-plugin-param-save="angular-params-post"');
+    expect(html).toContain('data-plugin-param-cancel-loading="angular-no-ok-loading"');
+    expect(html).not.toContain('name="selector" disabled');
+    expect(html).toContain('Edit Success!');
+    expect(html).toContain('https://hooks.example');
+    expect(html).toContain('{&quot;ok&quot;:true}');
+    expect(html).toContain('Authorization');
+    expect(html).toContain('Bearer token');
+    expect(html).toContain('env');
+    expect(html).toContain('prod');
+    expect(html).toContain('usage');
+    expect(html).toContain('number');
+    expect(html).toContain('alpha,beta');
+  });
+
+  it('keeps the Angular plugin params modal open with an empty field set', async () => {
+    const { PluginManageSurface } = await import('./plugin-manage-surface');
+    const t = createTranslatorMock({
+      locale: 'zh-CN',
+      overrides: {
+        'common.no-data': '暂无数据',
+        'plugin.edit': '编辑插件'
+      }
+    });
+
+    const html = renderToStaticMarkup(
+      <PluginManageSurface
+        t={t}
+        data={{ list: { content: [], totalElements: 0, pageIndex: 0, pageSize: 8 } }}
+        search=""
+        selectedIds={[]}
+        draftPlugin={null}
+        paramDraft={{
+          plugin: { id: 42, name: 'empty-param-plugin', enableStatus: true } as any,
+          paramDefines: [],
+          params: {}
+        }}
+        deleteTarget={null}
+        onSearchChange={() => {}}
+        onSearch={() => {}}
+        onRefresh={() => {}}
+        onNew={() => {}}
+        onDeleteSelected={() => {}}
+        onDeleteCancel={() => {}}
+        onDeleteConfirm={() => {}}
+        onSelectedIdsChange={() => {}}
+        onEditParams={() => {}}
+        onParamChange={() => {}}
+        onCloseParamDialog={() => {}}
+        onSaveParamDialog={() => {}}
+        onToggleEnabled={() => {}}
+        onDeleteOne={() => {}}
+        onDraftChange={() => {}}
+        onCloseDialog={() => {}}
+        onSaveDialog={() => {}}
+      />
+    );
+
+    expect(html).toContain('data-plugin-param-empty-contract="angular-empty-params-modal"');
+    expect(html).toContain('data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('data-plugin-param-dialog="angular-params-define-modal"');
+    expect(html).toContain('data-plugin-param-dialog-plugin="empty-param-plugin"');
+    expect(html).toContain('data-plugin-param-form="angular-dynamic-form-field"');
+    expect(html).toContain('data-plugin-param-empty="angular-empty-params-modal"');
+    expect(html).toContain('data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"');
+    expect(html).toContain('暂无数据');
+    expect(html).not.toContain('data-plugin-param-field=');
   });
 });
diff --git a/web-next/components/pages/plugin-manage-surface.tsx b/web-next/components/pages/plugin-manage-surface.tsx
index 3beb70ac70..2ba41f981c 100644
--- a/web-next/components/pages/plugin-manage-surface.tsx
+++ b/web-next/components/pages/plugin-manage-surface.tsx
@@ -1,18 +1,27 @@
 'use client';
 
 import React from 'react';
-import { Inbox, Pencil, RefreshCw, Trash2, Upload } from 'lucide-react';
+import { Inbox, MoreHorizontal, Pencil, RefreshCw, Trash2, Upload, X } from 'lucide-react';
+import { HzConfigurableFieldEditor, HzConfirmDialog, HzFileInput, HzInlineFeedback, HzInput, HzKeyValueEditor, HzMonitorEditorFieldGrid, HzPaginationBar, HzRadioButtonGroup, HzSwitch, HzTableRowActionButton, HzTextarea } from '@hertzbeat/ui';
 import { OverlayDialog } from '../workbench/overlay-dialog';
 import { Button } from '../ui/button';
 import { Checkbox } from '../ui/checkbox';
 import { Input } from '../ui/input';
+import { LabelRecordInput } from '../ui/label-record-input';
 import { SearchRow } from '../ui/search-row';
 import { coldOpsCatalogVisual } from '../../lib/cold-ops-visual';
 import { buildPluginTableRows } from '../../lib/plugin-manage/view-model';
-import type { PluginManagePageData, PluginUploadDraft } from '../../lib/plugin-manage/controller';
-import type { Plugin } from '../../lib/types';
+import type { PluginManagePageData, PluginParamDraft, PluginUploadDraft } from '../../lib/plugin-manage/controller';
+import type { ParamDefine, Plugin } from '../../lib/types';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
+type PluginActionTone = 'success' | 'warning' | 'critical';
+type PluginActionKind = 'enable' | 'delete' | 'upload' | 'params';
+export type PluginDeleteTarget = {
+  ids: number[];
+  label: string;
+  mode: 'single' | 'batch';
+};
 
 type PluginManageSurfaceProps = {
   t: Translator;
@@ -20,13 +29,38 @@ type PluginManageSurfaceProps = {
   search: string;
   selectedIds: number[];
   draftPlugin: PluginUploadDraft | null;
+  isUploadDialogOpen?: boolean;
+  paramDraft?: PluginParamDraft | null;
+  actionMessage?: string | null;
+  actionError?: string | null;
+  actionMeta?: string | null;
+  actionTone?: PluginActionTone;
+  actionKind?: PluginActionKind;
+  loadError?: string | null;
+  uploadValidation?: { name?: boolean; jarFile?: boolean };
+  isUploadPending?: boolean;
+  isParamPending?: boolean;
+  deleteTarget: PluginDeleteTarget | null;
+  isLoadPending?: boolean;
+  isDeletePending?: boolean;
+  isTogglePending?: boolean;
+  optimisticEnableStatus?: Record<number, boolean>;
   onSearchChange: (value: string) => void;
   onSearch: () => void;
+  onSearchClear?: () => void;
   onRefresh: () => void;
   onNew: () => void;
   onDeleteSelected: () => void;
+  onDeleteCancel: () => void;
+  onDeleteConfirm: () => void;
   onSelectedIdsChange: (ids: number[]) => void;
+  pageSizeOptions?: number[];
+  onPageIndexChange?: (pageIndex: number) => void;
+  onPageSizeChange?: (pageSize: number) => void;
   onEditParams: (plugin: Plugin) => void;
+  onParamChange?: (field: string, value: unknown) => void;
+  onCloseParamDialog?: () => void;
+  onSaveParamDialog?: () => void;
   onToggleEnabled: (plugin: Plugin) => void;
   onDeleteOne: (plugin: Plugin) => void;
   onDraftChange: (patch: Partial<PluginUploadDraft>) => void;
@@ -48,19 +82,149 @@ const coldIconButtonClassName =
 const coldDialogInputClassName =
   'h-8 rounded-[3px] border border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#eef2f7] outline-none placeholder:text-[#6f7788] focus:border-[#4e74f8] focus:ring-2 focus:ring-[rgba(78,116,248,0.16)]';
 
-function isChineseTranslator(t: Translator): boolean {
-  return t('common.refresh') === '刷新' || t('menu.advanced.plugins') === '插件管理';
-}
+const pluginActionMenuRootClassName = 'group relative z-[9999] inline-block overflow-visible open:z-[9999]';
+const pluginActionMenuPanelBaseClassName =
+  'mt-2 z-[9999] min-w-[132px] rounded-[3px] border border-[#2b3039] bg-[#101217] p-1 shadow-[0_18px_42px_rgba(0,0,0,0.42)]';
 
 function pluginTypeLabel(t: Translator, type?: string) {
   return type ? t(`plugin.type.${type}`) : t('common.none');
 }
 
 function pluginStatusLabel(enabled: boolean, t: Translator): string {
-  if (isChineseTranslator(t)) {
-    return enabled ? '已启用' : '已停用';
+  return enabled ? t('common.enabled') : t('common.disabled');
+}
+
+function stringifyPluginParamValue(value: unknown): string {
+  if (value === null || value === undefined) return '';
+  return typeof value === 'string' ? value : String(value);
+}
+
+type PluginParamKeyValueRow = {
+  key: string;
+  value: string;
+};
+
+type PluginParamConfigurableRow = Record<string, string>;
+
+function parsePluginParamKeyValueRows(value: unknown): PluginParamKeyValueRow[] {
+  if (Array.isArray(value)) {
+    const rows = value.map(item => {
+      if (item && typeof item === 'object') {
+        const record = item as Record<string, unknown>;
+        return {
+          key: String(record.key ?? ''),
+          value: String(record.value ?? '')
+        };
+      }
+      return { key: String(item ?? ''), value: '' };
+    });
+    return rows.length > 0 ? rows : [{ key: '', value: '' }];
+  }
+
+  if (value && typeof value === 'object') {
+    const entries = Object.entries(value as Record<string, unknown>);
+    return entries.length > 0
+      ? entries.map(([key, entryValue]) => ({ key, value: String(entryValue ?? '') }))
+      : [{ key: '', value: '' }];
   }
-  return enabled ? 'Enabled' : 'Disabled';
+
+  if (typeof value !== 'string') return [{ key: '', value: '' }];
+  const trimmed = value.trim();
+  if (!trimmed || trimmed === '""') return [{ key: '', value: '' }];
+
+  try {
+    const parsed = JSON.parse(trimmed);
+    return parsePluginParamKeyValueRows(parsed);
+  } catch {
+    return [{ key: '', value: trimmed }];
+  }
+}
+
+function stringifyPluginParamKeyValueRows(rows: PluginParamKeyValueRow[]): string {
+  return JSON.stringify(
+    rows.reduce<Record<string, string>>((record, row) => {
+      const key = row.key.trim();
+      if (!key) return record;
+      record[key] = row.value.trim();
+      return record;
+    }, {})
+  );
+}
+
+function objectifyPluginParamLabelRows(rows: PluginParamKeyValueRow[]): Record<string, string> {
+  return rows.reduce<Record<string, string>>((record, row) => {
+    const key = row.key.trim();
+    const value = row.value.trim();
+    if (!key || !value) return record;
+    record[key] = value;
+    return record;
+  }, {});
+}
+
+function stringifyPluginParamLabelSelectorValue(value: unknown): string {
+  if (typeof value === 'string') {
+    const trimmed = value.trim();
+    if (!trimmed || trimmed === '""') return '';
+    try {
+      const parsed = JSON.parse(trimmed);
+      return stringifyPluginParamLabelSelectorValue(parsed);
+    } catch {
+      return trimmed;
+    }
+  }
+
+  return parsePluginParamKeyValueRows(value)
+    .filter(row => row.key.trim())
+    .map(row => {
+      const key = row.key.trim();
+      const rowValue = row.value.trim();
+      return rowValue ? `${key}:${rowValue}` : key;
+    })
+    .join(', ');
+}
+
+function objectifyPluginParamLabelSelectorValue(value: string): Record<string, string> {
+  return value.split(',').reduce<Record<string, string>>((record, item) => {
+    const [keyPart, ...valueParts] = item.split(':');
+    const key = keyPart.trim();
+    const rowValue = valueParts.join(':').trim();
+    if (!key || !rowValue) return record;
+    record[key] = rowValue;
+    return record;
+  }, {});
+}
+
+function parsePluginParamConfigurableRows(value: unknown, keys: string[]): PluginParamConfigurableRow[] {
+  if (Array.isArray(value)) {
+    const rows = value.map(item => {
+      if (item && typeof item === 'object') {
+        const record = item as Record<string, unknown>;
+        return Object.fromEntries(keys.map(key => [key, String(record[key] ?? '')]));
+      }
+      return Object.fromEntries(keys.map((key, index) => [key, index === 0 ? String(item ?? '') : '']));
+    });
+    return rows.length > 0 ? rows : [{}];
+  }
+  return [{}];
+}
+
+function isPluginParamTextarea(define: ParamDefine): boolean {
+  return define.type === 'json' || define.type === 'textarea' || define.type === 'textarea-limit';
+}
+
+function isPluginParamMultiFuncInput(define: ParamDefine): boolean {
+  return define.type === 'text' || define.type === 'string' || define.type === 'array' || define.type === 'password';
+}
+
+function getPluginParamTextareaRows(define: ParamDefine): 4 | 8 {
+  return define.type === 'json' || define.type === 'textarea' ? 8 : 4;
+}
+
+function buildPluginParamRadioOptions(define: ParamDefine) {
+  return (define.options ?? []).map(option => ({
+    value: option.value ?? '',
+    label: typeof option.label === 'string' ? option.label : option.value ?? ''
+  }));
 }
 
 function PluginSwitch({
@@ -99,13 +263,38 @@ export function PluginManageSurface({
   search,
   selectedIds,
   draftPlugin,
+  isUploadDialogOpen,
+  paramDraft,
+  actionMessage,
+  actionError,
+  actionMeta,
+  actionTone,
+  actionKind,
+  loadError,
+  uploadValidation,
+  isUploadPending,
+  isParamPending,
+  deleteTarget,
+  isLoadPending,
+  isDeletePending,
+  isTogglePending,
+  optimisticEnableStatus,
   onSearchChange,
   onSearch,
+  onSearchClear,
   onRefresh,
   onNew,
   onDeleteSelected,
+  onDeleteCancel,
+  onDeleteConfirm,
   onSelectedIdsChange,
+  pageSizeOptions = [8, 15, 25],
+  onPageIndexChange,
+  onPageSizeChange,
   onEditParams,
+  onParamChange,
+  onCloseParamDialog,
+  onSaveParamDialog,
   onToggleEnabled,
   onDeleteOne,
   onDraftChange,
@@ -114,8 +303,53 @@ export function PluginManageSurface({
 }: PluginManageSurfaceProps) {
   const resolvedSelectedIds = selectedIds ?? [];
   const plugins = data.list.content ?? [];
-  const rows = buildPluginTableRows(plugins);
+  const rows = buildPluginTableRows(plugins, t);
+  const currentPageIndex = Math.max(0, data.list.pageIndex ?? 0);
+  const currentPageSize = Math.max(1, data.list.pageSize ?? pageSizeOptions[0] ?? 8);
+  const totalElements = data.list.totalElements || 0;
+  const totalPages = Math.max(1, Math.ceil(totalElements / currentPageSize));
+  const currentPage = Math.min(currentPageIndex + 1, totalPages);
+  const pageStart = totalElements === 0 || rows.length === 0 ? 0 : currentPageIndex * currentPageSize + 1;
+  const pageEnd = totalElements === 0 ? 0 : Math.min(totalElements, currentPageIndex * currentPageSize + rows.length);
   const allSelected = plugins.length > 0 && plugins.every(plugin => resolvedSelectedIds.includes(plugin.id));
+  const resolvedActionTone = actionTone ?? (actionError ? 'critical' : 'success');
+  const isUploadFailure = actionKind === 'upload' && Boolean(actionError);
+  const isParamFailure = actionKind === 'params' && Boolean(actionError);
+  const isEnableFailure = actionKind === 'enable' && Boolean(actionError);
+  const isDeleteFailure = actionKind === 'delete' && resolvedActionTone === 'critical' && Boolean(actionError);
+  const actionFeedbackTitle = isUploadFailure
+    ? t('common.notify.new-fail')
+    : isParamFailure
+      ? t('common.notify.edit-fail')
+      : isEnableFailure
+        ? t('common.notify.edit-fail')
+        : isDeleteFailure
+          ? t('common.notify.delete-fail')
+          : actionError ?? actionMessage ?? '';
+  const actionFeedbackMeta = isUploadFailure || isParamFailure || isEnableFailure || isDeleteFailure ? actionMeta ?? actionError ?? undefined : undefined;
+  const tableLoading = Boolean(isLoadPending || isDeletePending || isTogglePending);
+  const resolveEnableStatus = (plugin: Plugin) => (
+    optimisticEnableStatus && Object.prototype.hasOwnProperty.call(optimisticEnableStatus, plugin.id)
+      ? Boolean(optimisticEnableStatus[plugin.id])
+      : Boolean(plugin.enableStatus)
+  );
+  const pluginJarInputRef = React.useRef<HTMLInputElement>(null);
+  const [isToolbarActionMenuOpen, setToolbarActionMenuOpen] = React.useState(false);
+  const [openRowActionMenuId, setOpenRowActionMenuId] = React.useState<string | null>(null);
+  const paginationSummary = t('setting.plugins.pagination.summary', {
+    page: currentPage,
+    totalPages,
+    from: pageStart,
+    to: pageEnd,
+    total: totalElements
+  });
+
+  function handlePageJumpChange(value: string) {
+    const parsed = Number.parseInt(value, 10);
+    if (!Number.isFinite(parsed)) return;
+    const nextPageIndex = Math.min(Math.max(parsed, 1), totalPages) - 1;
+    onPageIndexChange?.(nextPageIndex);
+  }
 
   return (
     <>
@@ -128,159 +362,520 @@ export function PluginManageSurface({
         <section className={coldPluginVisual.layout.pageSection}>
           <div className="mx-auto max-w-[1480px]">
             <div className="mb-5">
-              <div data-plugin-header="cold-compact-header" className={coldPluginVisual.panel.hero}>
+              <div
+                data-plugin-header="cold-compact-header"
+                data-plugin-action-menu-clipping="none"
+                className={`${coldPluginVisual.panel.hero} z-[80] overflow-visible`}
+                style={{ overflow: 'visible' }}
+              >
                 <div className="max-w-[820px]">
                   <h1 className="text-[30px] font-semibold leading-tight text-[#f5f7fb]">
                     {t('menu.advanced.plugins')}
                   </h1>
                   <p className="mt-4 max-w-[780px] text-[13px] leading-6 text-[#a9b0bb]">
-                    管理自定义插件、执行类型和启用状态,保持上传、筛选、批量删除和参数编辑在同一个紧凑工作面中完成。
+                    {t('setting.plugins.copy')}
                   </p>
                   <div data-plugin-command-row="standard-equal-buttons" className={coldPluginVisual.button.row}>
                     <Button size="sm" variant="default" className={coldButtonClassName} onClick={onRefresh}>
                       <RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />
                       {t('common.refresh')}
                     </Button>
-                    <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onNew}>
-                      <Upload className="h-3.5 w-3.5" aria-hidden="true" />
-                      {t('plugin.upload')}
-                    </Button>
                     <Button
                       size="sm"
                       variant="default"
-                      className={coldButtonClassName}
-                      onClick={onDeleteSelected}
-                      disabled={resolvedSelectedIds.length === 0}
+                      className={coldPrimaryButtonClassName}
+                      onClick={onNew}
+                      data-plugin-upload-open="angular-upload-modal"
                     >
-                      <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
-                      {t('plugin.delete')}
+                      <Upload className="h-3.5 w-3.5" aria-hidden="true" />
+                      {t('plugin.upload')}
                     </Button>
+                    <div
+                      className={pluginActionMenuRootClassName}
+                      data-plugin-toolbar-delete-menu="angular-toolbar-ellipsis-delete"
+                      data-plugin-toolbar-delete-menu-contract="angular-toolbar-ellipsis-delete"
+                      data-plugin-toolbar-delete-menu-layer="overlay-visible-above-panel"
+                      data-plugin-toolbar-delete-menu-clearance="floating-overlay-no-panel-crop"
+                      data-plugin-toolbar-delete-menu-open={isToolbarActionMenuOpen ? 'true' : 'false'}
+                    >
+                      <button
+                        type="button"
+                        aria-expanded={isToolbarActionMenuOpen}
+                        aria-label={t('common.edit')}
+                        title={t('common.edit')}
+                        className={`${coldIconButtonClassName} inline-flex cursor-pointer list-none items-center justify-center [&::-webkit-details-marker]:hidden`}
+                        onClick={() => setToolbarActionMenuOpen(open => !open)}
+                        data-plugin-toolbar-delete-menu-trigger="angular-toolbar-ellipsis-delete"
+                      >
+                        <MoreHorizontal className="h-3.5 w-3.5" aria-hidden="true" />
+                        <span className="sr-only">{t('common.edit')}</span>
+                      </button>
+                      <div
+                        role="menu"
+                        hidden={!isToolbarActionMenuOpen}
+                        className={`${pluginActionMenuPanelBaseClassName} left-0`}
+                        data-plugin-toolbar-delete-menu-panel="angular-toolbar-ellipsis-delete"
+                        data-plugin-toolbar-delete-menu-layer-panel="overlay-visible-above-panel"
+                        data-plugin-toolbar-delete-menu-clearance-panel="floating-overlay-no-panel-crop"
+                        data-plugin-toolbar-delete-menu-owner="hertzbeat-ui-table-row-action-button"
+                      >
+                        <HzTableRowActionButton
+                          width="root-span"
+                          intent="ghost"
+                          onClick={() => {
+                            setToolbarActionMenuOpen(false);
+                            onDeleteSelected();
+                          }}
+                          data-plugin-delete-selected="angular-batch-delete-entry"
+                          data-plugin-delete-selected-owner="hertzbeat-ui-table-row-action-button"
+                          className="w-full text-[#fecaca] hover:text-white"
+                        >
+                          <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
+                          <span className="truncate">{t('plugin.delete')}</span>
+                        </HzTableRowActionButton>
+                      </div>
+                    </div>
                   </div>
                 </div>
               </div>
             </div>
 
-            <div data-plugin-admin-layout="full-width-admin-list" className="space-y-5">
+            <div
+              data-plugin-admin-layout="full-width-admin-list"
+              data-plugin-enable-feedback-contract="angular-edit-notify"
+              data-plugin-enable-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+              data-plugin-enable-optimistic-contract="angular-toggle-mutates-row-before-put"
+              data-plugin-enable-optimistic-owner="route-state-contract"
+              data-plugin-table-loading-contract="angular-load-plugins-table"
+              data-plugin-table-loading-scope="load-search-refresh-pagination-mutation"
+              data-plugin-load-failure-contract="angular-console-only-shell"
+              data-plugin-load-failure-owner="plugin-route-controller"
+              data-plugin-load-failure={loadError ? 'angular-console-only-shell' : 'none'}
+              data-plugin-toggle-loading-contract="angular-nz-table-loading"
+              data-plugin-toggle-loading-contract-owner="angular-nz-table-loading"
+              data-plugin-selection-reset-contract="angular-load-clears-selection"
+              data-plugin-selection-reset-owner="route-state-contract"
+              data-plugin-query-param-order-contract="angular-page-index-size-search"
+              data-plugin-query-param-order-owner="plugin-query-state"
+              data-plugin-delete-warning-contract="angular-no-select-warning"
+              data-plugin-delete-confirm-contract="angular-modal-confirm"
+              data-plugin-delete-confirm-contract-owner="hertzbeat-ui-confirm-dialog"
+              data-plugin-delete-feedback-contract="angular-delete-notify"
+              data-plugin-delete-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+              data-plugin-delete-failure-contract="angular-delete-fail-notification"
+              data-plugin-delete-failure-owner="hertzbeat-ui-inline-feedback"
+              data-plugin-enable-failure-contract="angular-edit-fail-notification"
+              data-plugin-enable-failure-owner="hertzbeat-ui-inline-feedback"
+              data-plugin-delete-query-contract="angular-repeated-ids-query"
+              data-plugin-delete-query-owner="route-mutation-contract"
+              data-plugin-delete-page-clamp-contract="angular-update-page-index"
+              data-plugin-table-columns-contract="angular-five-column-edit-actions"
+              data-plugin-table-columns-owner="angular-nz-table"
+              data-plugin-table-stable-height-contract="viewport-fill-on-empty-or-short"
+              data-plugin-table-stable-height-owner="route-layout-contract"
+              data-plugin-row-delete-menu-contract="angular-ellipsis-dropdown-delete"
+              data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"
+              data-plugin-upload-validation-contract="angular-required-before-submit"
+              data-plugin-upload-invalid-submit-contract="angular-mark-dirty-keep-open"
+              data-plugin-upload-invalid-submit-owner="route-validation-contract"
+              data-plugin-upload-loading-contract="angular-nz-ok-loading"
+              data-plugin-upload-payload-contract="angular-form-data"
+              data-plugin-upload-name-payload-contract="angular-raw-name-no-trim"
+              data-plugin-upload-name-payload-owner="plugin-manage-controller"
+              data-plugin-upload-save-lifecycle-contract="angular-close-success-keep-open-fail"
+              data-plugin-upload-save-lifecycle-owner="route-state-contract"
+              data-plugin-upload-success-reset-contract="angular-reset-form-after-success"
+              data-plugin-upload-success-reset-owner="route-state-contract"
+              data-plugin-upload-cancel-contract="angular-cancel-preserves-form"
+              data-plugin-upload-cancel-contract-owner="route-state-contract"
+              data-plugin-upload-cancel-pending-contract="angular-cancel-allowed-during-ok-loading"
+              data-plugin-upload-cancel-pending-owner="angular-nz-modal"
+              data-plugin-upload-mask-closable-contract="angular-mask-closable-false"
+              data-plugin-upload-width-contract="angular-width-30-percent"
+              data-plugin-upload-modal-owner="angular-nz-modal"
+              data-plugin-upload-field-layout-contract="angular-label-8-control-14"
+              data-plugin-upload-field-layout-owner="route-form-field-grid"
+              data-plugin-upload-status-control-contract="angular-nz-switch"
+              data-plugin-upload-status-control-owner="hertzbeat-ui-switch"
+              data-plugin-upload-file-list-contract="angular-before-upload-single-replace"
+              data-plugin-upload-file-list-owner="hertzbeat-ui-file-input"
+              data-plugin-upload-file-remove-contract="angular-nz-remove-clears-jar"
+              data-plugin-upload-file-remove-owner="hertzbeat-ui-file-input"
+              data-plugin-upload-feedback-contract="angular-new-notify"
+              data-plugin-upload-feedback-contract-owner="hertzbeat-ui-inline-feedback"
+              data-plugin-upload-failure-contract="angular-new-fail-notification"
+              data-plugin-upload-failure-owner="hertzbeat-ui-inline-feedback"
+              data-plugin-param-edit-contract="angular-params-define-modal"
+              data-plugin-param-action-visibility-contract="angular-paramcount-zero-only"
+              data-plugin-param-action-visibility-owner="plugin-view-model"
+              data-plugin-param-mask-closable-contract="angular-mask-closable-false"
+              data-plugin-param-modal-owner="angular-nz-modal"
+              data-plugin-param-save-contract="angular-params-post"
+              data-plugin-param-payload-contract="angular-object-values-payload"
+              data-plugin-param-save-lifecycle-contract="angular-close-success-keep-open-fail"
+              data-plugin-param-save-lifecycle-owner="route-state-contract"
+              data-plugin-param-save-loading-contract="angular-no-ok-loading"
+              data-plugin-param-save-loading-owner="angular-modal-ok-contract"
+              data-plugin-param-feedback-contract="angular-edit-notify"
+              data-plugin-param-failure-contract="angular-edit-fail-notification"
+              data-plugin-param-failure-owner="hertzbeat-ui-inline-feedback"
+              data-plugin-param-empty-contract="angular-empty-params-modal"
+              data-plugin-param-empty-owner="hertzbeat-ui-monitor-editor-field-grid"
+              data-plugin-param-pending-editable-contract="angular-controls-remain-enabled"
+              data-plugin-param-pending-editable-owner="angular-param-modal"
+              data-plugin-param-width-contract="angular-default-modal-width"
+              data-plugin-param-width-owner="angular-nz-modal"
+              data-plugin-param-radio-contract="angular-nz-radio-group"
+              data-plugin-param-radio-owner="hertzbeat-ui-radio-button-group"
+              data-plugin-param-textarea-contract="angular-textarea-rows-8"
+              data-plugin-param-textarea-owner="hertzbeat-ui-textarea"
+              data-plugin-param-advanced-fields-contract="angular-configurable-and-multi-func"
+              data-plugin-param-advanced-fields-owner="hertzbeat-ui-advanced-param-controls"
+              data-plugin-param-configurable-field-contract="angular-app-configurable-field"
+              data-plugin-param-configurable-field-owner="hertzbeat-ui-configurable-field"
+              data-plugin-param-key-value-contract="angular-app-configurable-field"
+              data-plugin-param-key-value-owner="hertzbeat-ui-key-value-editor"
+              data-plugin-param-labels-contract="angular-app-configurable-field"
+              data-plugin-param-labels-owner="hertzbeat-ui-key-value-editor"
+              data-plugin-param-label-selector-contract="angular-app-label-selector"
+              data-plugin-param-label-selector-owner="cold-label-selector"
+              data-plugin-param-metrics-field-contract="angular-app-configurable-field"
+              data-plugin-param-metrics-field-owner="hertzbeat-ui-configurable-field-editor"
+              data-plugin-param-array-contract="angular-app-multi-func-input"
+              data-plugin-param-array-owner="hertzbeat-ui-input"
+              data-plugin-param-field-layout-contract="angular-label-7-control-8"
+              data-plugin-param-field-layout-owner="hertzbeat-ui-monitor-editor-field-grid"
+              data-plugin-param-form-owner="hertzbeat-ui-monitor-editor-field-grid"
+              className="space-y-5"
+            >
               <section className="min-w-0">
                 <SearchRow
                   data-plugin-manage-toolbar="cold-table-toolbar"
                   data-plugin-manage-search-owner="shared-search-row"
+                  data-plugin-search-submit-contract="angular-enter-and-clear"
+                  data-plugin-search-clear-contract="angular-cleared-load"
+                  data-plugin-search-clear-owner="shared-search-row"
                   value={search}
                   placeholder={t('plugin.search')}
                   searchLabel={t('common.search')}
+                  clearLabel={t('common.clear')}
                   inputWidthClassName="w-[360px]"
                   onValueChange={onSearchChange}
                   onSearch={onSearch}
+                  onClear={onSearchClear}
                   trailingActions={
                     <div className="text-[12px] font-semibold text-[#858d9a]">
-                      已选择 <span className="text-[#dbe4f0]">{resolvedSelectedIds.length}</span>
+                      {t('setting.plugins.selected-count', { count: resolvedSelectedIds.length })}
                     </div>
                   }
                 />
+                {actionFeedbackTitle ? (
+                  <HzInlineFeedback
+                    tone={resolvedActionTone}
+                    title={actionFeedbackTitle}
+                    meta={actionFeedbackMeta}
+                    variant="embedded"
+                    data-plugin-action-feedback={resolvedActionTone}
+                    data-plugin-enable-feedback={
+                      actionKind === 'enable'
+                        ? resolvedActionTone === 'critical'
+                          ? 'angular-edit-fail'
+                          : 'angular-edit-notify'
+                        : undefined
+                    }
+                    data-plugin-enable-feedback-owner="hertzbeat-ui-inline-feedback"
+                    data-plugin-enable-failure={isEnableFailure ? 'angular-edit-fail-notification' : undefined}
+                    data-plugin-enable-failure-owner={isEnableFailure ? 'hertzbeat-ui-inline-feedback' : undefined}
+                    data-plugin-enable-feedback-title={
+                      actionKind === 'enable'
+                        ? resolvedActionTone === 'critical'
+                          ? 'common.notify.edit-fail'
+                          : 'common.notify.edit-success'
+                        : undefined
+                    }
+                    data-plugin-enable-feedback-detail={isEnableFailure ? 'backend-message' : undefined}
+                    data-plugin-delete-feedback={
+                      actionKind === 'delete'
+                        ? resolvedActionTone === 'warning'
+                          ? 'angular-no-select-warning'
+                          : resolvedActionTone === 'critical'
+                            ? 'angular-delete-fail'
+                            : 'angular-delete-notify'
+                        : undefined
+                    }
+                    data-plugin-delete-feedback-owner="hertzbeat-ui-inline-feedback"
+                    data-plugin-delete-failure={isDeleteFailure ? 'angular-delete-fail-notification' : undefined}
+                    data-plugin-delete-failure-owner={isDeleteFailure ? 'hertzbeat-ui-inline-feedback' : undefined}
+                    data-plugin-delete-feedback-title={
+                      actionKind === 'delete'
+                        ? resolvedActionTone === 'critical'
+                          ? 'common.notify.delete-fail'
+                          : resolvedActionTone === 'warning'
+                            ? 'common.notify.no-select-delete'
+                            : 'common.notify.delete-success'
+                        : undefined
+                    }
+                    data-plugin-delete-feedback-detail={isDeleteFailure ? 'backend-message' : undefined}
+                    data-plugin-upload-feedback={
+                      actionKind === 'upload'
+                        ? resolvedActionTone === 'critical'
+                          ? 'angular-new-fail'
+                          : 'angular-new-notify'
+                        : undefined
+                    }
+                    data-plugin-upload-feedback-owner="hertzbeat-ui-inline-feedback"
+                    data-plugin-upload-feedback-title={
+                      actionKind === 'upload'
+                        ? resolvedActionTone === 'critical'
+                          ? 'common.notify.new-fail'
+                          : 'common.notify.new-success'
+                        : undefined
+                    }
+                    data-plugin-upload-feedback-detail={
+                      actionKind === 'upload' && resolvedActionTone === 'critical' ? 'backend-message' : undefined
+                    }
+                    data-plugin-param-feedback={
+                      actionKind === 'params'
+                        ? resolvedActionTone === 'critical'
+                          ? 'angular-edit-fail'
+                          : 'angular-edit-notify'
+                        : undefined
+                    }
+                    data-plugin-param-feedback-owner="hertzbeat-ui-inline-feedback"
+                    data-plugin-param-failure={isParamFailure ? 'angular-edit-fail-notification' : undefined}
+                    data-plugin-param-failure-owner={
+                      isParamFailure ? 'hertzbeat-ui-inline-feedback' : undefined
+                    }
+                    data-plugin-param-feedback-title={
+                      actionKind === 'params'
+                        ? resolvedActionTone === 'critical'
+                          ? 'common.notify.edit-fail'
+                          : 'common.notify.edit-success'
+                        : undefined
+                    }
+                    data-plugin-param-feedback-detail={isParamFailure ? 'backend-message' : undefined}
+                  />
+                ) : null}
 
                 <div
                   data-plugin-manage-table-shell="cold-dense-table"
-                  className="overflow-hidden rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)]"
+                  data-plugin-table-stable-height="viewport-fill-on-empty-or-short"
+                  data-plugin-table-stable-height-owner="route-layout-contract"
+                  data-plugin-table-loading={tableLoading ? 'true' : 'false'}
+                  data-plugin-table-loading-owner="angular-nz-table-loading"
+                  aria-busy={tableLoading ? 'true' : 'false'}
+                  className="flex min-h-[360px] flex-col overflow-visible rounded-[4px] border border-[#2b3039] bg-[#0b0c0e] shadow-[0_20px_56px_rgba(0,0,0,0.32)] lg:min-h-[calc(100vh-390px)]"
                 >
-                  <table data-plugin-manage-table="cold-plugin-table" className="w-full table-fixed border-collapse text-left text-[12px] text-[#a9b0bb]">
-                    <thead className="border-b border-[#252b34] bg-[#101217] text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">
-                      <tr>
-                        <th className="w-[44px] px-3 py-2.5">
-                          <Checkbox
-                            data-plugin-select-all="cold-checkbox"
-                            aria-label={t('common.select')}
-                            containerClassName="min-h-0"
-                            checked={allSelected}
-                            disabled={plugins.length === 0}
-                            onChange={event => {
-                              onSelectedIdsChange(event.target.checked ? plugins.map(plugin => plugin.id) : []);
-                            }}
-                          />
-                        </th>
-                        <th className="w-[24%] px-3 py-2.5">{t('plugin.name')}</th>
-                        <th className="w-[28%] px-3 py-2.5">{t('plugin.type')}</th>
-                        <th className="w-[14%] px-3 py-2.5">{t('plugin.status')}</th>
-                        <th className="w-[12%] px-3 py-2.5">参数</th>
-                        <th className="w-[96px] px-3 py-2.5">{t('common.edit')}</th>
-                      </tr>
-                    </thead>
-                    <tbody>
-                      {rows.length > 0 ? rows.map((row, index) => {
-                        const original = plugins[index];
-                        const checked = resolvedSelectedIds.includes(original.id);
-                        return (
-                          <tr key={row.key} data-plugin-row={row.key} className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]">
-                            <td className="px-3 py-2.5">
-                              <Checkbox
-                                aria-label={t('common.select')}
-                                data-plugin-row-select={String(original.id)}
-                                containerClassName="min-h-0"
-                                checked={checked}
-                                onChange={event => {
-                                  onSelectedIdsChange(
-                                    event.target.checked
-                                      ? [...resolvedSelectedIds, original.id]
-                                      : resolvedSelectedIds.filter(value => value !== original.id)
-                                  );
-                                }}
-                              />
-                            </td>
-                            <td className="px-3 py-2.5 font-semibold text-[#eef2f7]">{row.name}</td>
-                            <td className="px-3 py-2.5">
-                              <div className="flex flex-wrap gap-1.5">
-                                {row.typeLabels.length > 0 ? row.typeLabels.map(typeLabel => (
-                                  <span key={`${row.key}-${typeLabel}`} className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] leading-4 text-[#cbd5e1]">
-                                    {pluginTypeLabel(t, typeLabel)}
-                                  </span>
-                                )) : <span className="text-[#6f7681]">-</span>}
-                              </div>
-                            </td>
-                            <td className="px-3 py-2.5">
-                              <div className="flex items-center gap-2">
-                                <PluginSwitch
-                                  checked={Boolean(original.enableStatus)}
-                                  label={pluginStatusLabel(Boolean(original.enableStatus), t)}
-                                  onChange={() => onToggleEnabled(original)}
+                  <div className="min-h-0 flex-1 bg-[#0b0c0e]" data-plugin-table-fill-stage="viewport-fill-on-empty-or-short">
+                    <table
+                      data-plugin-manage-table="cold-plugin-table"
+                      data-plugin-table-columns="angular-five-column-edit-actions"
+                      data-plugin-table-columns-owner="angular-nz-table"
+                      className="w-full table-fixed border-collapse text-left text-[12px] text-[#a9b0bb]"
+                    >
+                      <thead className="border-b border-[#252b34] bg-[#101217] text-[11px] font-semibold uppercase tracking-[0.12em] text-[#7e8494]">
+                        <tr>
+                          <th className="w-[44px] px-3 py-2.5">
+                            <Checkbox
+                              data-plugin-select-all="cold-checkbox"
+                              aria-label={t('common.select')}
+                              containerClassName="min-h-0"
+                              checked={allSelected}
+                              disabled={plugins.length === 0}
+                              onChange={event => {
+                                onSelectedIdsChange(event.target.checked ? plugins.map(plugin => plugin.id) : []);
+                              }}
+                            />
+                          </th>
+                          <th className="w-[26%] px-3 py-2.5">{t('plugin.name')}</th>
+                          <th className="w-[34%] px-3 py-2.5">{t('plugin.type')}</th>
+                          <th className="w-[16%] px-3 py-2.5">{t('plugin.status')}</th>
+                          <th className="w-[96px] px-3 py-2.5">{t('common.edit')}</th>
+                        </tr>
+                      </thead>
+                      <tbody>
+                        {rows.length > 0 ? rows.map((row, index) => {
+                          const original = plugins[index];
+                          const checked = resolvedSelectedIds.includes(original.id);
+                          const hasOptimisticEnableStatus = Boolean(
+                            optimisticEnableStatus && Object.prototype.hasOwnProperty.call(optimisticEnableStatus, original.id)
+                          );
+                          const displayEnableStatus = resolveEnableStatus(original);
+                          const rowActionMenuId = String(original.id);
+                          const isRowActionMenuOpen = openRowActionMenuId === rowActionMenuId;
+                          const actionLabelParams = { name: row.name };
+                          const editParamsLabel = t('setting.plugins.action.params-aria', actionLabelParams);
+                          const deleteLabel = t('setting.plugins.action.delete-aria', actionLabelParams);
+                          return (
+                            <tr key={row.key} data-plugin-row={row.key} className="border-t border-[#252b34] bg-[#0b0c0e] transition hover:bg-[#111721]">
+                              <td className="px-3 py-2.5">
+                                <Checkbox
+                                  aria-label={t('common.select')}
+                                  data-plugin-row-select={String(original.id)}
+                                  containerClassName="min-h-0"
+                                  checked={checked}
+                                  onChange={event => {
+                                    onSelectedIdsChange(
+                                      event.target.checked
+                                        ? [...resolvedSelectedIds, original.id]
+                                        : resolvedSelectedIds.filter(value => value !== original.id)
+                                    );
+                                  }}
                                 />
-                                <span className="text-[11px] font-semibold text-[#cbd5e1]">
-                                  {pluginStatusLabel(Boolean(original.enableStatus), t)}
+                              </td>
+                              <td className="px-3 py-2.5 font-semibold text-[#eef2f7]">{row.name}</td>
+                              <td className="px-3 py-2.5">
+                                <div className="flex flex-wrap gap-1.5">
+                                  {row.typeLabels.length > 0 ? row.typeLabels.map(typeLabel => (
+                                    <span key={`${row.key}-${typeLabel}`} className="rounded-[3px] border border-[#303743] bg-[#101217] px-2 py-0.5 text-[11px] leading-4 text-[#cbd5e1]">
+                                      {pluginTypeLabel(t, typeLabel)}
+                                    </span>
+                                  )) : <span className="text-[#6f7681]">{t('common.none')}</span>}
+                                </div>
+                              </td>
+                              <td className="px-3 py-2.5">
+                                <div
+                                  className="flex items-center gap-2"
+                                  data-plugin-enable-optimistic-state={hasOptimisticEnableStatus ? 'overridden' : 'source'}
+                                  data-plugin-enable-optimistic-next-status={displayEnableStatus ? 'true' : 'false'}
+                                >
+                                  <PluginSwitch
+                                    checked={displayEnableStatus}
+                                    label={pluginStatusLabel(displayEnableStatus, t)}
+                                    onChange={() => onToggleEnabled(original)}
+                                  />
+                                  <span className="text-[11px] font-semibold text-[#cbd5e1]">
+                                    {pluginStatusLabel(displayEnableStatus, t)}
+                                  </span>
+                                </div>
+                              </td>
+                              <td className="px-3 py-2.5">
+                                <div data-plugin-row-actions="angular-row-actions-contextual" data-plugin-row-actions-owner="cold-icon-actions" className="flex gap-1.5">
+                                  {row.canEditParams ? (
+                                    <Button
+                                      size="icon"
+                                      variant="default"
+                                      className={coldIconButtonClassName}
+                                      onClick={() => {
+                                        setOpenRowActionMenuId(null);
+                                        onEditParams(original);
+                                      }}
+                                      aria-label={editParamsLabel}
+                                      title={editParamsLabel}
+                                      data-plugin-param-edit-open={String(original.id)}
+                                      data-plugin-row-action="params"
+                                      data-plugin-row-action-owner="row-contextual-icon-button"
+                                      data-plugin-row-action-label={row.name}
+                                    >
+                                      <Pencil className="h-3.5 w-3.5" aria-hidden="true" />
+                                      <span className="sr-only">{editParamsLabel}</span>
+                                    </Button>
+                                  ) : null}
+                                  <div
+                                    className={pluginActionMenuRootClassName}
+                                    data-plugin-row-delete-menu={rowActionMenuId}
+                                    data-plugin-row-delete-menu-contract="angular-ellipsis-dropdown-delete"
+                                    data-plugin-row-delete-menu-layer="overlay-visible-above-panel"
+                                    data-plugin-row-delete-menu-clearance="floating-overlay-no-panel-crop"
+                                    data-plugin-row-delete-menu-open={isRowActionMenuOpen ? 'true' : 'false'}
+                                  >
+                                    <button
+                                      type="button"
+                                      aria-expanded={isRowActionMenuOpen}
+                                      aria-label={t('common.edit')}
+                                      title={t('common.edit')}
+                                      className={`${coldIconButtonClassName} inline-flex cursor-pointer list-none items-center justify-center [&::-webkit-details-marker]:hidden`}
+                                      onClick={() => setOpenRowActionMenuId(current => (current === rowActionMenuId ? null : rowActionMenuId))}
+                                      data-plugin-row-delete-menu-trigger={rowActionMenuId}
+                                      data-plugin-row-action-label={row.name}
+                                    >
+                                      <MoreHorizontal className="h-3.5 w-3.5" aria-hidden="true" />
+                                      <span className="sr-only">{deleteLabel}</span>
+                                    </button>
+                                    <div
+                                      role="menu"
+                                      hidden={!isRowActionMenuOpen}
+                                      className={`${pluginActionMenuPanelBaseClassName} right-0`}
+                                      data-plugin-row-delete-menu-panel={rowActionMenuId}
+                                      data-plugin-row-delete-menu-layer-panel="overlay-visible-above-panel"
+                                      data-plugin-row-delete-menu-clearance-panel="floating-overlay-no-panel-crop"
+                                      data-plugin-row-delete-menu-owner="hertzbeat-ui-table-row-action-button"
+                                      data-plugin-row-delete-menu-panel-open={isRowActionMenuOpen ? 'true' : 'false'}
+                                    >
+                                      <HzTableRowActionButton
+                                        width="root-span"
+                                        intent="ghost"
+                                        onClick={() => {
+                                          setOpenRowActionMenuId(null);
+                                          onDeleteOne(original);
+                                        }}
+                                        data-plugin-delete-one={String(original.id)}
+                                        data-plugin-delete-one-owner="hertzbeat-ui-table-row-action-button"
+                                        data-plugin-row-action="delete"
+                                        data-plugin-row-action-label={row.name}
+                                        className="w-full text-[#fecaca] hover:text-white"
+                                      >
+                                        <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
+                                        <span className="truncate">{t('plugin.delete')}</span>
+                                      </HzTableRowActionButton>
+                                    </div>
+                                  </div>
+                                </div>
+                              </td>
+                            </tr>
+                          );
+                        }) : (
+                          <tr data-plugin-manage-empty-state="cold-table-empty" className="border-t border-[#252b34] bg-[#0b0c0e]">
+                            <td colSpan={5} className="h-[320px] px-3 text-center text-[#a9b0bb]">
+                              <div className="inline-flex flex-col items-center gap-2.5">
+                                <span
+                                  data-plugin-manage-empty-icon="cold-empty-box"
+                                  className="inline-flex h-10 w-10 items-center justify-center rounded-[4px] border border-[#303743] bg-[#101217] text-[#cbd5e1]"
+                                >
+                                  <Inbox className="h-5 w-5" aria-hidden="true" />
                                 </span>
-                              </div>
-                            </td>
-                            <td className="px-3 py-2.5 tabular-nums">{row.paramCount}</td>
-                            <td className="px-3 py-2.5">
-                              <div data-plugin-row-actions="cold-icon-actions" className="flex gap-1.5">
-                                {row.canEditParams ? (
-                                  <Button size="icon" variant="default" className={coldIconButtonClassName} onClick={() => onEditParams(original)} title={t('plugin.param.edit')}>
-                                    <Pencil className="h-3.5 w-3.5" aria-hidden="true" />
-                                    <span className="sr-only">{t('plugin.param.edit')}</span>
-                                  </Button>
-                                ) : null}
-                                <Button size="icon" variant="default" className={coldIconButtonClassName} onClick={() => onDeleteOne(original)} title={t('plugin.delete')}>
-                                  <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
-                                  <span className="sr-only">{t('plugin.delete')}</span>
-                                </Button>
+                                <div className="text-[13px] font-semibold text-[#eef2f7]">{t('common.no-data')}</div>
                               </div>
                             </td>
                           </tr>
-                        );
-                      }) : (
-                        <tr data-plugin-manage-empty-state="cold-table-empty" className="border-t border-[#252b34] bg-[#0b0c0e]">
-                          <td colSpan={6} className="h-[240px] px-3 text-center text-[#a9b0bb]">
-                            <div className="inline-flex flex-col items-center gap-2.5">
-                              <span
-                                data-plugin-manage-empty-icon="cold-empty-box"
-                                className="inline-flex h-10 w-10 items-center justify-center rounded-[4px] border border-[#303743] bg-[#101217] text-[#cbd5e1]"
-                              >
-                                <Inbox className="h-5 w-5" aria-hidden="true" />
-                              </span>
-                              <div className="text-[13px] font-semibold text-[#eef2f7]">暂无数据</div>
-                            </div>
-                          </td>
-                        </tr>
-                      )}
-                    </tbody>
-                  </table>
+                        )}
+                      </tbody>
+                    </table>
+                  </div>
+                  <div
+                    data-plugin-pagination="cold-dense-pagination"
+                    data-plugin-pagination-owner="hertzbeat-ui-pagination-bar"
+                    data-plugin-pagination-contract="angular-search-pagination"
+                  >
+                    <HzPaginationBar
+                      summary={paginationSummary}
+                      pageSizeLabel={t('setting.plugins.pagination.page-size')}
+                      pageSizeValue={String(currentPageSize)}
+                      pageSizeOptions={pageSizeOptions.map(value => ({ value: String(value), label: String(value) }))}
+                      pageJumpLabel={t('setting.plugins.pagination.page')}
+                      pageJumpValue={String(currentPage)}
+                      pageJumpMax={totalPages}
+                      previousLabel={t('common.previous-page')}
+                      nextLabel={t('common.next-page')}
+                      previousDisabled={currentPageIndex <= 0}
+                      nextDisabled={currentPage >= totalPages}
+                      onPrevious={() => onPageIndexChange?.(Math.max(currentPageIndex - 1, 0))}
+                      onNext={() => onPageIndexChange?.(Math.min(currentPageIndex + 1, totalPages - 1))}
+                      onPageSizeChange={value => onPageSizeChange?.(Number.parseInt(value, 10))}
+                      onPageJumpChange={handlePageJumpChange}
+                      pageJumpInputProps={
+                        {
+                          'data-plugin-pagination-page-jump-owner': 'hertzbeat-ui-input'
+                        } as React.ComponentProps<typeof HzPaginationBar>['pageJumpInputProps']
+                      }
+                      pageSizeSelectProps={
+                        {
+                          'data-plugin-pagination-page-size-owner': 'hertzbeat-ui-select'
+                        } as React.ComponentProps<typeof HzPaginationBar>['pageSizeSelectProps']
+                      }
+                      className="border-x-0"
+                    />
+                  </div>
                 </div>
               </section>
             </div>
@@ -289,55 +884,427 @@ export function PluginManageSurface({
       </div>
 
       <OverlayDialog
-        open={Boolean(draftPlugin)}
+        open={Boolean((isUploadDialogOpen ?? Boolean(draftPlugin)) && draftPlugin)}
         title={t('plugin.upload')}
         onClose={onCloseDialog}
-        maxWidthClassName="max-w-2xl"
+        maskClosable={false}
+        maxWidthClassName="max-w-[min(92vw,30rem)] lg:max-w-[30vw]"
+        overlayProps={{
+          'data-plugin-upload-mask-closable': 'false',
+          'data-plugin-upload-width': 'angular-width-30-percent',
+          'data-plugin-upload-modal-owner': 'angular-nz-modal'
+        }}
         footer={
           <div className="flex flex-wrap justify-end gap-2">
-            <Button size="sm" variant="default" className={coldButtonClassName} onClick={onCloseDialog}>
+            <Button
+              size="sm"
+              variant="default"
+              className={coldButtonClassName}
+              onClick={onCloseDialog}
+              data-plugin-upload-cancel="angular-cancel-preserves-form"
+              data-plugin-upload-cancel-pending="angular-cancel-allowed-during-ok-loading"
+              data-plugin-upload-cancel-pending-owner="angular-nz-modal"
+            >
               {t('common.button.cancel')}
             </Button>
-            <Button size="sm" variant="default" className={coldPrimaryButtonClassName} onClick={onSaveDialog}>
+            <Button
+              size="sm"
+              variant="default"
+              className={coldPrimaryButtonClassName}
+              onClick={onSaveDialog}
+              disabled={Boolean(isUploadPending)}
+              data-plugin-upload-save="angular-form-data"
+              data-plugin-upload-ok-loading={isUploadPending ? 'true' : 'false'}
+              data-plugin-upload-ok-loading-owner="angular-nz-ok-loading"
+              data-plugin-upload-save-lifecycle="angular-close-success-keep-open-fail"
+              data-plugin-upload-save-lifecycle-owner="route-state-contract"
+              aria-busy={isUploadPending ? 'true' : 'false'}
+            >
               {t('common.button.save')}
             </Button>
           </div>
         }
       >
         {draftPlugin ? (
-          <div className="space-y-3">
-            <label className="flex flex-col gap-2 text-[12px] font-semibold text-[#a9b0bb]">
-              <span>{t('plugin.name')}</span>
-              <Input
-                className={coldDialogInputClassName}
-                value={draftPlugin.name}
-                onChange={event => onDraftChange({ name: event.target.value })}
-              />
+          <div
+            className="space-y-3"
+            data-plugin-upload-dialog="angular-cancel-preserves-form"
+            data-plugin-upload-field-layout="angular-label-8-control-14"
+            data-plugin-upload-invalid-submit={uploadValidation?.name || uploadValidation?.jarFile ? 'angular-mark-dirty-keep-open' : undefined}
+            data-plugin-upload-invalid-submit-owner={uploadValidation?.name || uploadValidation?.jarFile ? 'route-validation-contract' : undefined}
+          >
+            <label
+              className="grid gap-2 text-[12px] font-semibold text-[#a9b0bb] sm:grid-cols-[minmax(96px,8fr)_minmax(0,14fr)] sm:items-start"
+              data-plugin-upload-field="name"
+              data-plugin-upload-field-layout="angular-label-8-control-14"
+            >
+              <span className="pt-2 text-right max-sm:text-left" data-plugin-upload-label-span="8">{t('plugin.name')}</span>
+              <span className="min-w-0" data-plugin-upload-control-span="14">
+                <Input
+                  className={coldDialogInputClassName}
+                  value={draftPlugin.name}
+                  onChange={event => onDraftChange({ name: event.target.value })}
+                  data-plugin-upload-name-input="angular-required"
+                  data-plugin-upload-name-payload="angular-raw-name-no-trim"
+                  data-plugin-upload-name-payload-owner="plugin-manage-controller"
+                />
+              </span>
+              {uploadValidation?.name ? (
+                <span className="text-[11px] font-semibold text-[#ffb4a8] sm:col-start-2" data-plugin-upload-validation="name-required">
+                  {t('validation.required')}
+                </span>
+              ) : null}
             </label>
-            <label className="flex flex-col gap-2 text-[12px] font-semibold text-[#a9b0bb]">
-              <span>{t('plugin.jar.file')}</span>
-              <div className="flex flex-wrap items-center gap-2">
+            <label
+              className="grid gap-2 text-[12px] font-semibold text-[#a9b0bb] sm:grid-cols-[minmax(96px,8fr)_minmax(0,14fr)] sm:items-start"
+              data-plugin-upload-field="jarFile"
+              data-plugin-upload-field-layout="angular-label-8-control-14"
+            >
+              <span className="pt-2 text-right max-sm:text-left" data-plugin-upload-label-span="8">{t('plugin.jar.file')}</span>
+              <span className="flex min-w-0 flex-wrap items-center gap-2" data-plugin-upload-control-span="14">
+                <HzFileInput
+                  ref={pluginJarInputRef}
+                  accept=".jar"
+                  aria-label={t('plugin.jar.file')}
+                  data-plugin-upload-file-input="angular-jar-before-upload"
+                  data-plugin-upload-file-input-owner="hertzbeat-ui-file-input"
+                  data-plugin-upload-file-list="angular-before-upload-single-replace"
+                  data-plugin-upload-file-list-owner="hertzbeat-ui-file-input"
+                  onChange={event => {
+                    const file = event.target.files?.[0] ?? null;
+                    onDraftChange({
+                      jarFile: file,
+                      jarFileName: file?.name ?? ''
+                    });
+                  }}
+                />
                 <Input
                   className={`${coldDialogInputClassName} min-w-0 flex-1`}
                   value={draftPlugin.jarFileName}
-                  onChange={event => onDraftChange({ jarFileName: event.target.value })}
+                  readOnly
+                  placeholder={t('plugin.jar.file')}
+                  data-plugin-upload-file-name="angular-jar-before-upload"
                 />
-                <Button size="sm" variant="default" className={coldButtonClassName} type="button">
+                <Button
+                  size="sm"
+                  variant="default"
+                  className={coldButtonClassName}
+                  type="button"
+                  data-plugin-upload-file-trigger="angular-jar-before-upload"
+                  onClick={() => pluginJarInputRef.current?.click()}
+                >
                   <Upload className="h-3.5 w-3.5" aria-hidden="true" />
                   {t('common.file.select')}
                 </Button>
-              </div>
+                <Button
+                  size="sm"
+                  variant="default"
+                  className={coldIconButtonClassName}
+                  type="button"
+                  aria-label={t('common.remove')}
+                  disabled={!draftPlugin.jarFileName}
+                  data-plugin-upload-file-remove="angular-nz-remove-clears-jar"
+                  data-plugin-upload-file-remove-owner="hertzbeat-ui-file-input"
+                  data-plugin-upload-file-remove-state={draftPlugin.jarFileName ? 'selected' : 'empty'}
+                  onClick={() => {
+                    if (pluginJarInputRef.current) {
+                      pluginJarInputRef.current.value = '';
+                    }
+                    onDraftChange({
+                      jarFile: null,
+                      jarFileName: ''
+                    });
+                  }}
+                >
+                  <X className="h-3.5 w-3.5" aria-hidden="true" />
+                </Button>
+              </span>
+              {uploadValidation?.jarFile ? (
+                <span className="text-[11px] font-semibold text-[#ffb4a8] sm:col-start-2" data-plugin-upload-validation="jar-required">
+                  {t('validation.required')}
+                </span>
+              ) : null}
             </label>
-            <Checkbox
-              data-plugin-draft-enabled-checkbox="cold-checkbox"
-              checked={draftPlugin.enableStatus}
-              onChange={event => onDraftChange({ enableStatus: event.target.checked })}
-              label={t('plugin.status')}
-              containerClassName="text-[#a9b0bb]"
-            />
+            <div
+              className="grid gap-2 text-[12px] font-semibold text-[#a9b0bb] sm:grid-cols-[minmax(96px,8fr)_minmax(0,14fr)] sm:items-center"
+              data-plugin-upload-field="enableStatus"
+              data-plugin-upload-field-layout="angular-label-8-control-14"
+              data-plugin-upload-status-field="angular-nz-switch"
+            >
+              <span className="text-right max-sm:text-left" data-plugin-upload-label-span="8">{t('plugin.status')}</span>
+              <span className="min-w-0" data-plugin-upload-control-span="14">
+                <HzSwitch
+                  checked={draftPlugin.enableStatus}
+                  onCheckedChange={checked => onDraftChange({ enableStatus: checked })}
+                  aria-label={t('plugin.status')}
+                  data-plugin-upload-status-control="angular-nz-switch"
+                  data-plugin-upload-status-control-owner="hertzbeat-ui-switch"
+                />
+              </span>
+            </div>
           </div>
         ) : null}
       </OverlayDialog>
+      <OverlayDialog
+        open={Boolean(paramDraft)}
+        title={t('plugin.edit')}
+        onClose={onCloseParamDialog ?? (() => {})}
+        maskClosable={false}
+        maxWidthClassName="max-w-[min(92vw,520px)]"
+        overlayProps={{
+          'data-plugin-param-mask-closable': 'false',
+          'data-plugin-param-modal-owner': 'angular-nz-modal',
+          'data-plugin-param-width': 'angular-default-modal-width',
+          'data-plugin-param-width-owner': 'angular-nz-modal'
+        }}
+        footer={
+          <div
+            className="flex flex-wrap justify-end gap-2"
+            data-plugin-param-save-loading-contract="angular-no-ok-loading"
+            data-plugin-param-save-loading="angular-no-ok-loading"
+            data-plugin-param-save-loading-owner="angular-modal-ok-contract"
+            data-plugin-param-save-pending={isParamPending ? 'true' : 'false'}
+            data-plugin-param-payload="angular-object-values-payload"
+            data-plugin-param-save-lifecycle="angular-close-success-keep-open-fail"
+            data-plugin-param-save-lifecycle-owner="route-state-contract"
+            data-plugin-param-pending-editable={isParamPending ? 'angular-controls-remain-enabled' : 'idle'}
+            data-plugin-param-pending-editable-owner="angular-param-modal"
+          >
+            <Button
+              size="sm"
+              variant="default"
+              className={coldButtonClassName}
+              onClick={onCloseParamDialog}
+              data-plugin-param-cancel-loading="angular-no-ok-loading"
+            >
+              {t('common.button.cancel')}
+            </Button>
+            <Button
+              size="sm"
+              variant="default"
+              className={coldPrimaryButtonClassName}
+              onClick={onSaveParamDialog}
+              data-plugin-param-save="angular-params-post"
+              data-plugin-param-save-loading="angular-no-ok-loading"
+              data-plugin-param-save-loading-owner="angular-modal-ok-contract"
+            >
+              {t('common.button.save')}
+            </Button>
+          </div>
+        }
+      >
+        {paramDraft ? (
+          <div
+            className="space-y-3"
+            data-plugin-param-dialog="angular-params-define-modal"
+            data-plugin-param-dialog-plugin={paramDraft.plugin.name}
+          >
+            <HzMonitorEditorFieldGrid
+              columns={1}
+              data-plugin-param-form="angular-dynamic-form-field"
+              data-plugin-param-form-owner="hertzbeat-ui-monitor-editor-field-grid"
+              data-plugin-param-field-layout="angular-label-7-control-8"
+              data-plugin-param-field-layout-owner="hertzbeat-ui-monitor-editor-field-grid"
+            >
+              {paramDraft.paramDefines.length > 0 ? paramDraft.paramDefines.map(define => {
+                const current = paramDraft.params[define.field];
+                const value = current?.paramValue;
+                const stringValue = stringifyPluginParamValue(value);
+                const label = typeof define.name === 'string' ? define.name : define.field;
+                const isMultiFuncInput = isPluginParamMultiFuncInput(define);
+                return (
+                  <label
+                    key={define.field}
+                    className="grid gap-2 text-[12px] font-semibold text-[#a9b0bb] sm:grid-cols-[minmax(112px,7fr)_minmax(0,8fr)] sm:items-start"
+                    data-plugin-param-field={define.field}
+                    data-plugin-param-field-layout="angular-label-7-control-8"
+                    data-plugin-param-field-type={define.type ?? 'password'}
+                    data-plugin-param-field-required={define.required ? 'true' : 'false'}
+                  >
+                    <span className="pt-2 text-right max-sm:text-left" data-plugin-param-label-span="7">
+                      {label}
+                      {define.required ? <span className="ml-1 text-[#ffb4a8]">*</span> : null}
+                    </span>
+                    <span className="min-w-0" data-plugin-param-control-span="8">
+                      {define.type === 'radio' && (define.options ?? []).length > 0 ? (
+                        <HzRadioButtonGroup
+                          name={define.field}
+                          value={stringifyPluginParamValue(value)}
+                          options={buildPluginParamRadioOptions(define)}
+                          onChange={nextValue => onParamChange?.(define.field, nextValue)}
+                          data-plugin-param-radio="angular-nz-radio-group"
... 4381 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 40/50: fix(web-next): source topology node icons from catalog

Posted by zh...@apache.org.
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 c0eb3a0814c6c9e3b4fe7cb99d9f8c9ce73fd6ca
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 09:04:54 2026 +0800

    fix(web-next): source topology node icons from catalog
---
 web-next/app/topology/page.test.tsx                     |  2 +-
 web-next/app/topology/topology-page.tsx                 |  3 ++-
 web-next/app/ui-lab/page.test.tsx                       |  8 +++++++-
 web-next/app/ui-lab/page.tsx                            |  4 ++--
 web-next/lib/i18n-runtime-messages.ts                   |  4 ++++
 web-next/packages/hertzbeat-ui/src/index.test.tsx       |  4 ++--
 web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx | 14 +++++++++-----
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx      | 16 ++++++++++++----
 web-next/test/i18n-test-helper.ts                       |  4 ++++
 9 files changed, 43 insertions(+), 16 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx b/web-next/app/topology/page.test.tsx
index 559e979481..9d41367f09 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -971,7 +971,7 @@ describe('topology page', () => {
 	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
 	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
 	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="blocks"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
 	    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
 	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
 	    expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
diff --git a/web-next/app/topology/topology-page.tsx b/web-next/app/topology/topology-page.tsx
index c17a439823..81f7f9e5dd 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -74,9 +74,10 @@ function findEdge(edges: TopologyServiceEdge[], id: string) {
 }
 
 function resolveTopologyG6EntityType(node: Pick<TopologyServiceNode, 'entityType' | 'id' | 'label' | 'source'>) {
+  const inferredIcon = getHzTopologyG6NodeIcon(`${node.entityType} ${node.label} ${node.source} ${node.id}`);
+  if (inferredIcon.kind === 'cache' || inferredIcon.kind === 'queue') return inferredIcon.kind;
   const directIcon = getHzTopologyG6NodeIcon(node.entityType);
   if (directIcon.kind !== 'unknown') return directIcon.kind;
-  const inferredIcon = getHzTopologyG6NodeIcon(`${node.label} ${node.source} ${node.id}`);
   return inferredIcon.kind === 'unknown' ? node.entityType : inferredIcon.kind;
 }
 
diff --git a/web-next/app/ui-lab/page.test.tsx b/web-next/app/ui-lab/page.test.tsx
index dbe95c950a..3093c7c6d8 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2703,13 +2703,19 @@ describe('HertzBeat UI lab page', () => {
 	    expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
 	    expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
 	    expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
-	    expect(html).toContain('data-hz-topology-legend-item="node-type-middleware"');
+	    expect(html).toContain('data-hz-topology-legend-item="node-type-cache"');
+	    expect(html).toContain('data-hz-topology-legend-item="node-type-queue"');
 	    expect(html).toContain('data-hz-topology-legend-item="node-type-monitor"');
+	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-middleware"');
 	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-application"');
 	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
 	    expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
 	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
 	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="database"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="memory-stick"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="inbox"');
 	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
 	    expect(html).toContain('data-hz-topology-legend-visual-source="lucide-react"');
 	    expect(html).toContain('data-hz-topology-legend-visual-mode="source-backed-text"');
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
index 0693307083..4bf30d9a9c 100644
--- a/web-next/app/ui-lab/page.tsx
+++ b/web-next/app/ui-lab/page.tsx
@@ -361,7 +361,7 @@ const topologyG6LabGraph: HzTopologyG6GraphInput = {
     {
       id: 'queue-events',
       label: 'Events Queue',
-      entityType: 'middleware',
+      entityType: 'queue',
       health: 'healthy',
       tone: 'blue',
       focus: 'normal',
@@ -383,7 +383,7 @@ const topologyG6LabGraph: HzTopologyG6GraphInput = {
     {
       id: 'cache-cart',
       label: 'Cart Cache',
-      entityType: 'middleware',
+      entityType: 'cache',
       health: 'warning',
       tone: 'orange',
       focus: 'normal',
diff --git a/web-next/lib/i18n-runtime-messages.ts b/web-next/lib/i18n-runtime-messages.ts
index 74fe659faf..c1e4754789 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -1020,6 +1020,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.legend.node-type.application': 'Application',
     'topology.legend.node-type.service': 'Service',
     'topology.legend.node-type.database': 'Database',
+    'topology.legend.node-type.cache': 'Cache',
+    'topology.legend.node-type.queue': 'Queue',
     'topology.legend.node-type.middleware': 'Middleware',
     'topology.legend.node-type.k8s-workload': 'Workload',
     'topology.legend.node-type.monitor': 'Monitor',
@@ -4537,6 +4539,8 @@ export const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.legend.node-type.application': '应用',
     'topology.legend.node-type.service': '服务',
     'topology.legend.node-type.database': '数据库',
+    'topology.legend.node-type.cache': '缓存',
+    'topology.legend.node-type.queue': '队列',
     'topology.legend.node-type.middleware': '中间件',
     'topology.legend.node-type.k8s-workload': '工作负载',
     'topology.legend.node-type.monitor': '监控',
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index 1613acd63f..4de18901b4 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -5208,7 +5208,7 @@ describe('@hertzbeat/ui', () => {
 	                label: 'Service node',
 	                iconSrc: 'data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%3E%3C/svg%3E',
 	                iconLibrary: 'lucide-react',
-	                iconName: 'blocks',
+	                iconName: 'component',
 	                iconSource: 'entity-type-catalog',
 	                visualSource: 'lucide-react'
 	              },
@@ -5241,7 +5241,7 @@ describe('@hertzbeat/ui', () => {
 	    expect(html).toContain('data-hz-topology-legend-source-label="lucide-react"');
 	    expect(html).toContain('data-hz-topology-legend-icon-owner="hertzbeat-ui-legend-source-icon"');
 	    expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
-	    expect(html).toContain('data-hz-topology-legend-icon-name="blocks"');
+	    expect(html).toContain('data-hz-topology-legend-icon-name="component"');
 	    expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
 	    expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
 	    expect(html).toContain('data-hz-topology-legend-visual-source="hertzbeat-status-token"');
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
index 04a813fb86..f8f730f6ec 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
@@ -79,7 +79,7 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
         entityType: 'service',
         iconLibrary: 'lucide-react',
         iconLabel: 'Service',
-        iconName: 'blocks',
+        iconName: 'component',
         iconSource: 'entity-type-catalog',
         nodeShape: 'circle-icon-label',
         requestRatePerSecond: 12.34,
@@ -127,8 +127,10 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
   it('uses a shared Lucide node icon catalog for supported topology entity types', () => {
     expect(HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.map(item => [item.kind, item.iconName, item.label])).toEqual([
       ['application', 'app-window', 'Application'],
-      ['service', 'blocks', 'Service'],
+      ['service', 'component', 'Service'],
       ['database', 'database', 'Database'],
+      ['cache', 'memory-stick', 'Cache'],
+      ['queue', 'inbox', 'Queue'],
       ['middleware', 'workflow', 'Middleware'],
       ['k8s-workload', 'container', 'Workload'],
       ['monitor', 'activity', 'Monitor'],
@@ -137,10 +139,12 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
       ['unknown', 'circle-help', 'Unknown']
     ]);
 
-    expect(getHzTopologyG6NodeIcon('service')).toMatchObject({ kind: 'service', iconName: 'blocks' });
-    expect(getHzTopologyG6NodeIcon('payment-api')).toMatchObject({ kind: 'service', iconName: 'blocks' });
+    expect(getHzTopologyG6NodeIcon('service')).toMatchObject({ kind: 'service', iconName: 'component' });
+    expect(getHzTopologyG6NodeIcon('payment-api')).toMatchObject({ kind: 'service', iconName: 'component' });
     expect(getHzTopologyG6NodeIcon('orders-db')).toMatchObject({ kind: 'database', iconName: 'database' });
-    expect(getHzTopologyG6NodeIcon('redis-cache')).toMatchObject({ kind: 'middleware', iconName: 'workflow' });
+    expect(getHzTopologyG6NodeIcon('redis-cache')).toMatchObject({ kind: 'cache', iconName: 'memory-stick' });
+    expect(getHzTopologyG6NodeIcon('kafka-events-queue')).toMatchObject({ kind: 'queue', iconName: 'inbox' });
+    expect(getHzTopologyG6NodeIcon('middleware')).toMatchObject({ kind: 'middleware', iconName: 'workflow' });
     expect(getHzTopologyG6NodeIcon('k8s-workload')).toMatchObject({ kind: 'k8s-workload', iconName: 'container' });
     expect(getHzTopologyG6NodeIcon('monitor-bind')).toMatchObject({ kind: 'monitor', iconName: 'activity' });
     expect(getHzTopologyG6NodeIcon('linux-host')).toMatchObject({ kind: 'resource', iconName: 'server' });
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
index d638b35501..e913a7de1f 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
@@ -4,10 +4,12 @@ import * as React from 'react';
 import { Crosshair, Maximize2, RotateCcw, Search, ZoomIn, ZoomOut } from 'lucide-react';
 import { __iconNode as activityIconNode } from 'lucide-react/dist/esm/icons/activity.js';
 import { __iconNode as appWindowIconNode } from 'lucide-react/dist/esm/icons/app-window.js';
-import { __iconNode as blocksIconNode } from 'lucide-react/dist/esm/icons/blocks.js';
 import { __iconNode as circleHelpIconNode } from 'lucide-react/dist/esm/icons/circle-help.js';
+import { __iconNode as componentIconNode } from 'lucide-react/dist/esm/icons/component.js';
 import { __iconNode as containerIconNode } from 'lucide-react/dist/esm/icons/container.js';
 import { __iconNode as databaseIconNode } from 'lucide-react/dist/esm/icons/database.js';
+import { __iconNode as inboxIconNode } from 'lucide-react/dist/esm/icons/inbox.js';
+import { __iconNode as memoryStickIconNode } from 'lucide-react/dist/esm/icons/memory-stick.js';
 import { __iconNode as serverIconNode } from 'lucide-react/dist/esm/icons/server.js';
 import { __iconNode as triangleAlertIconNode } from 'lucide-react/dist/esm/icons/triangle-alert.js';
 import { __iconNode as workflowIconNode } from 'lucide-react/dist/esm/icons/workflow.js';
@@ -79,6 +81,8 @@ export type HzTopologyG6NodeIconKind =
   | 'application'
   | 'service'
   | 'database'
+  | 'cache'
+  | 'queue'
   | 'middleware'
   | 'k8s-workload'
   | 'monitor'
@@ -88,8 +92,10 @@ export type HzTopologyG6NodeIconKind =
 
 export type HzTopologyG6NodeIconName =
   | 'app-window'
-  | 'blocks'
+  | 'component'
   | 'database'
+  | 'memory-stick'
+  | 'inbox'
   | 'workflow'
   | 'container'
   | 'activity'
@@ -360,13 +366,15 @@ function topologyNodeIconSpec(
 
 export const HZ_TOPOLOGY_G6_NODE_ICON_CATALOG: readonly HzTopologyG6NodeIconSpec[] = [
   topologyNodeIconSpec('application', 'Application', 'app-window', ['application', 'app'], appWindowIconNode),
-  topologyNodeIconSpec('service', 'Service', 'blocks', ['service', 'api', 'endpoint'], blocksIconNode),
+  topologyNodeIconSpec('service', 'Service', 'component', ['service', 'api', 'endpoint'], componentIconNode),
   topologyNodeIconSpec('database', 'Database', 'database', ['database', 'db', 'mysql', 'postgres', 'postgresql', 'mongo'], databaseIconNode),
+  topologyNodeIconSpec('cache', 'Cache', 'memory-stick', ['cache', 'redis', 'memcached'], memoryStickIconNode),
+  topologyNodeIconSpec('queue', 'Queue', 'inbox', ['queue', 'mq', 'broker', 'topic', 'messaging', 'kafka', 'rabbit'], inboxIconNode),
   topologyNodeIconSpec(
     'middleware',
     'Middleware',
     'workflow',
-    ['middleware', 'queue', 'mq', 'broker', 'topic', 'messaging', 'cache', 'redis', 'kafka', 'rabbit'],
+    ['middleware'],
     workflowIconNode
   ),
   topologyNodeIconSpec(
diff --git a/web-next/test/i18n-test-helper.ts b/web-next/test/i18n-test-helper.ts
index 1f955f4fdd..068a2c9301 100644
--- a/web-next/test/i18n-test-helper.ts
+++ b/web-next/test/i18n-test-helper.ts
@@ -621,6 +621,8 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.legend.node-type.application': 'Application',
     'topology.legend.node-type.service': 'Service',
     'topology.legend.node-type.database': 'Database',
+    'topology.legend.node-type.cache': 'Cache',
+    'topology.legend.node-type.queue': 'Queue',
     'topology.legend.node-type.middleware': 'Middleware',
     'topology.legend.node-type.k8s-workload': 'Workload',
     'topology.legend.node-type.monitor': 'Monitor',
@@ -3411,6 +3413,8 @@ const SUPPLEMENTAL_MESSAGES: Partial<Record<LocaleCode, Messages>> = {
     'topology.legend.node-type.application': '应用',
     'topology.legend.node-type.service': '服务',
     'topology.legend.node-type.database': '数据库',
+    'topology.legend.node-type.cache': '缓存',
+    'topology.legend.node-type.queue': '队列',
     'topology.legend.node-type.middleware': '中间件',
     'topology.legend.node-type.k8s-workload': '工作负载',
     'topology.legend.node-type.monitor': '监控',


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 24/50: fix(ai): align tools with backend API changes

Posted by zh...@apache.org.
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 66ae2f6c88335c3759bb7bbdc4465dd8ebeac0ce
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 09:46:56 2026 +0800

    fix(ai): align tools with backend API changes
    
    Validation:
    
    - ./mvnw -pl hertzbeat-ai -Dcheckstyle.skip=true -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests clean compile
    
    - git diff --cached --check
    
    - cached trailing-whitespace scan
---
 .../hertzbeat/ai/tools/impl/AlertToolsImpl.java    | 26 +++++++++++-----------
 .../hertzbeat/ai/tools/impl/MonitorToolsImpl.java  | 10 +++++----
 2 files changed, 19 insertions(+), 17 deletions(-)

diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertToolsImpl.java
index 2e8df751c2..4b461b0888 100644
--- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertToolsImpl.java
+++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertToolsImpl.java
@@ -44,26 +44,26 @@ public class AlertToolsImpl implements AlertTools {
     @Override
     @Tool(name = "query_alerts", description = """
             HertzBeat: Query alerts with comprehensive filtering and pagination options.
-            
+
             ALERT TYPES:
             - Pass alertType='single' for individual alert instances
             - Pass alertType='group' for grouped/aggregated alerts
             - Pass alertType='both' to get both types (separate sections)
-            
+
             STATUS FILTERING:
             - 'firing': Currently active alerts requiring attention
             - 'resolved': Previously active alerts that have been cleared
             - 'all': Both firing and resolved alerts (default)
-            
+
             SEARCH & FILTERING:
             - search: Search in alert content, labels, or descriptions
             - sort: Order by 'startAt' (trigger time), 'triggerTimes' (frequency), 'status'
             - order: 'asc' (oldest first) or 'desc' (newest first, default)
-            
+
             PAGINATION:
             - pageIndex: Page number starting from 0
             - pageSize: Number of alerts per page (default: 10, max recommended: 50)
-            
+
             EXAMPLE AND COMMON USE CASES:
             - Recent active alerts: alertType='single', status='firing', sort='startAt', order='desc'
             - Historical analysis: alertType='single', status='resolved', pageSize=50
@@ -115,7 +115,7 @@ public class AlertToolsImpl implements AlertTools {
             // Handle different alert types
             if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
                 Page<SingleAlert> singleResult = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
-                
+
                 response.append("SINGLE ALERTS:\n");
                 response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
 
@@ -124,7 +124,7 @@ public class AlertToolsImpl implements AlertTools {
                     response.append("Status: ").append(alert.getStatus()).append("\n");
                     response.append("Content: ").append(alert.getContent() != null ? alert.getContent() : "No content").append("\n");
                     response.append("Trigger Times: ").append(alert.getTriggerTimes()).append("\n");
-                    
+
                     if (alert.getStartAt() != null) {
                         response.append("Started At: ").append(UtilityClass.formatTimestamp(alert.getStartAt())).append("\n");
                     }
@@ -134,7 +134,7 @@ public class AlertToolsImpl implements AlertTools {
                     if (alert.getEndAt() != null) {
                         response.append("Ended At: ").append(UtilityClass.formatTimestamp(alert.getEndAt())).append("\n");
                     }
-                    
+
                     if (alert.getLabels() != null && !alert.getLabels().isEmpty()) {
                         response.append("Labels: ").append(alert.getLabels()).append("\n");
                     }
@@ -151,9 +151,9 @@ public class AlertToolsImpl implements AlertTools {
                 if ("both".equalsIgnoreCase(alertType)) {
                     response.append("\n");
                 }
-                
-                Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
-                
+
+                Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, null, sort, order, pageIndex, pageSize);
+
                 response.append("GROUP ALERTS:\n");
                 response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
 
@@ -161,14 +161,14 @@ public class AlertToolsImpl implements AlertTools {
                     response.append("Group Alert ID: ").append(alert.getId()).append("\n");
                     response.append("Status: ").append(alert.getStatus()).append("\n");
                     response.append("Group Key: ").append(alert.getGroupKey() != null ? alert.getGroupKey() : "No group key").append("\n");
-                    
+
                     if (alert.getGmtCreate() != null) {
                         response.append("Created At: ").append(alert.getGmtCreate()).append("\n");
                     }
                     if (alert.getGmtUpdate() != null) {
                         response.append("Updated At: ").append(alert.getGmtUpdate()).append("\n");
                     }
-                    
+
                     if (alert.getCommonLabels() != null && !alert.getCommonLabels().isEmpty()) {
                         response.append("Common Labels: ").append(alert.getCommonLabels()).append("\n");
                     }
diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java
index 7495cf83eb..b89eaf11d4 100644
--- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java
+++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java
@@ -32,7 +32,7 @@ import org.springframework.ai.tool.annotation.ToolParam;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.apache.hertzbeat.common.entity.manager.Monitor;
 import org.apache.hertzbeat.common.entity.manager.Param;
-import org.apache.hertzbeat.common.entity.manager.ParamDefine;
+import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -280,7 +280,9 @@ public class MonitorToolsImpl implements MonitorTools {
 
             // Validate that all required parameters for this monitor type are provided
             try {
-                MonitorDto monitorDto = MonitorDto.builder().monitor(monitor).params(paramList).build();
+                MonitorDto monitorDto = new MonitorDto();
+                monitorDto.setMonitor(monitor);
+                monitorDto.setParams(paramList);
                 monitorService.validate(monitorDto, false);
             } catch (IllegalArgumentException argumentException) {
                 if (argumentException.getMessage().contains("required")) {
@@ -456,7 +458,7 @@ public class MonitorToolsImpl implements MonitorTools {
             }
 
             // Get parameter definitions from app service
-            List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
+            List<ParamDefineInfo> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
 
             if (paramDefines == null || paramDefines.isEmpty()) {
                 return String.format("No parameter definitions found for monitor type '%s'. "
@@ -468,7 +470,7 @@ public class MonitorToolsImpl implements MonitorTools {
             response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
                 app, paramDefines.size()));
 
-            for (ParamDefine paramDefine : paramDefines) {
+            for (ParamDefineInfo paramDefine : paramDefines) {
                 response.append("• Field: ").append(paramDefine.getField()).append("\n");
 
                 // Add display name if available


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 30/50: fix(web-next): stop topology first-paint overfit

Posted by zh...@apache.org.
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 202f407d2c094401fc42934adeafee3cf02618cc
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 21:33:24 2026 +0800

    fix(web-next): stop topology first-paint overfit
---
 .../packages/hertzbeat-ui/src/topology-g6.test.tsx | 22 ++++++++++++++++------
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx | 12 +++++++-----
 2 files changed, 23 insertions(+), 11 deletions(-)

diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
index 9e4bf33697..11ebfd8f5d 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import { readFileSync } from 'node:fs';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it } from 'vitest';
 import {
@@ -18,6 +19,8 @@ import {
   HzTopologyG6Canvas
 } from './topology-g6';
 
+const topologyG6Source = readFileSync(new URL('./topology-g6.tsx', import.meta.url), 'utf8');
+
 describe('@hertzbeat/ui topology G6 canvas', () => {
   const graph = {
     nodes: [
@@ -351,30 +354,37 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(html).toContain('data-topology-node-focus-href="/topology?entityId=501&amp;depth=1"');
   });
 
-  it('schedules a post-render fit view pass so the real graph is framed on first paint', () => {
-    const source = String(HzTopologyG6Canvas);
+  it('schedules a post-render overflow-only fit pass so small graphs are not magnified on first paint', () => {
+    const source = topologyG6Source;
     const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={graph} />);
 
     expect(source).toContain('scheduleInitialFitView');
     expect(source).toContain('fitAndCenterG6Viewport');
+    expect(source).toContain("fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, false)");
+    expect(source).toContain("fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, { duration: 120 })");
+    expect(source).not.toContain("fitAndCenterG6Viewport(runtimeGraph, { when: 'always' }, false)");
+    expect(source).not.toContain("fitAndCenterG6Viewport(runtimeGraph, { when: 'always' }, { duration: 120 })");
     expect(source).not.toContain("runtimeGraph.fitView?.({ when: 'always' }, false);\n        await runtimeGraph.fitCenter?.(false);");
     expect(html).toContain('data-hz-topology-g6-auto-fit-zoom-bounds="0.18-1.35"');
     expect(html).toContain('data-hz-topology-g6-auto-fit-max-zoom="1.35"');
+    expect(html).toContain('data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"');
+    expect(html).toContain('data-hz-topology-g6-fit-mode="overflow-only-center"');
     expect(clampHzTopologyG6AutoFitZoom(4.8)).toBe(HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM);
     expect(clampHzTopologyG6AutoFitZoom(0.72)).toBe(0.72);
   });
 
   it('centers the shared G6 canvas after fit and reset view actions', () => {
-    const source = String(HzTopologyG6Canvas);
+    const source = topologyG6Source;
     const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={buildHzTopologyG6ScaleFixture(8)} />);
 
     expect(source).toContain('centerGraphView');
     expect(source).toContain('centerGraphView("fit-view")');
     expect(source).toContain('centerGraphView("reset-view")');
-    expect(source).toContain('fitAndCenterG6Viewport(graphRef.current, { when: "always" }, { duration: 180 })');
+    expect(source).toContain("fitAndCenterG6Viewport(graphRef.current, { when: 'overflow' }, { duration: 180 })");
+    expect(source).not.toContain("fitAndCenterG6Viewport(graphRef.current, { when: 'always' }, { duration: 180 })");
     expect(html).toContain('data-hz-topology-g6-viewport-owner="hertzbeat-ui-g6-viewport"');
-    expect(html).toContain('data-hz-topology-g6-fit-behavior="fit-and-center"');
-    expect(html).toContain('data-hz-topology-g6-reset-behavior="zoom-fit-center"');
+    expect(html).toContain('data-hz-topology-g6-fit-behavior="overflow-fit-and-center"');
+    expect(html).toContain('data-hz-topology-g6-reset-behavior="zoom-one-overflow-fit-center"');
   });
 
   it('preserves operator wheel and pan zoom while hover or selection styling updates redraw the G6 graph', () => {
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
index 1f2a102286..57fe3556f6 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
@@ -1141,7 +1141,7 @@ async function fitAndCenterG6Viewport(
 function scheduleInitialFitView(runtimeGraph: G6GraphRuntime, shouldRun: () => boolean) {
   return window.setTimeout(() => {
     if (!shouldRun()) return;
-    void fitAndCenterG6Viewport(runtimeGraph, { when: 'always' }, { duration: 120 });
+    void fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, { duration: 120 });
   }, 180);
 }
 
@@ -1592,7 +1592,7 @@ export function HzTopologyG6Canvas({
           clearSharedHover();
         });
         await runtimeGraph.render();
-        await fitAndCenterG6Viewport(runtimeGraph, { when: 'always' }, false);
+        await fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, false);
         lastFitStructureKeyRef.current = graphStructureKey;
         lastDrawGraphKeyRef.current = latestG6RenderKeyRef.current;
         initialFitTimerRef.current = scheduleInitialFitView(runtimeGraph, () => !hasUserViewportInteractedRef.current);
@@ -1698,7 +1698,7 @@ export function HzTopologyG6Canvas({
 
   const centerGraphView = React.useCallback(async (action: 'fit-view' | 'reset-view') => {
     if (action === 'reset-view') await graphRef.current?.zoomTo?.(1, { duration: 180 });
-    await fitAndCenterG6Viewport(graphRef.current, { when: 'always' }, { duration: 180 });
+    await fitAndCenterG6Viewport(graphRef.current, { when: 'overflow' }, { duration: 180 });
   }, []);
 
   const fitView = React.useCallback(() => {
@@ -1830,6 +1830,8 @@ export function HzTopologyG6Canvas({
       data-hz-topology-g6-auto-fit-owner="hertzbeat-ui-g6-auto-fit"
       data-hz-topology-g6-auto-fit-zoom-bounds={`${HZ_TOPOLOGY_G6_MIN_ZOOM}-${HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM}`}
       data-hz-topology-g6-auto-fit-max-zoom={HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM}
+      data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"
+      data-hz-topology-g6-fit-mode="overflow-only-center"
       data-hz-topology-g6-viewport-interaction-state={viewportInteractionState}
       data-hz-topology-g6-viewport-interaction-owner="hertzbeat-ui-g6-viewport-interaction"
       data-hz-topology-g6-viewport-preservation="clamped-wheel-pan-zoom"
@@ -1877,8 +1879,8 @@ export function HzTopologyG6Canvas({
       data-hz-topology-g6-style-redraw-behavior="no-auto-fit"
       data-hz-topology-g6-style-redraw-skip="identical-render-key"
       data-hz-topology-g6-blank-hover-clear="no-op-without-hover"
-      data-hz-topology-g6-fit-behavior="fit-and-center"
-      data-hz-topology-g6-reset-behavior="zoom-fit-center"
+      data-hz-topology-g6-fit-behavior="overflow-fit-and-center"
+      data-hz-topology-g6-reset-behavior="zoom-one-overflow-fit-center"
       data-hz-topology-g6-node-selection={selectionMode}
       data-hz-topology-g6-edge-selection={selectionMode}
       data-hz-topology-g6-edge-hit-target="wide-pointer-band"


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 19/50: feat(web-next): align shared UI primitive parity

Posted by zh...@apache.org.
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 435cc244aa093921f698f31705b59d8431aaa015
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 02:08:31 2026 +0800

    feat(web-next): align shared UI primitive parity
    
    Validation: vitest run app/loading.test.tsx packages/design-tokens/src/tokens.test.ts components/ui/cold-code-editor.test.tsx components/ui/cold-confirm-dialog.test.tsx components/ui/date-time-range.test.tsx components/ui/label-record-input.test.tsx components/ui/number-stepper.test.tsx components/ui/search-row.test.tsx components/ui/select.test.tsx components/ui/tag-input.test.tsx components/workbench/overlay-dialog.test.tsx components/workbench/workspace-tab-strip.test.tsx app/ui-la [...]
---
 web-next/app/globals.css                           | 39 +++++++--
 web-next/app/loading.test.tsx                      | 12 +--
 web-next/app/loading.tsx                           | 25 ++----
 web-next/components/ui/cold-code-editor.test.tsx   | 76 ++++++++++++++++-
 web-next/components/ui/cold-code-editor.tsx        | 92 +++++++++++++++++++--
 .../components/ui/cold-confirm-dialog.test.tsx     | 44 ++++++++++
 web-next/components/ui/cold-confirm-dialog.tsx     |  9 +-
 web-next/components/ui/date-time-range.test.tsx    | 49 +++++++++++
 web-next/components/ui/date-time-range.tsx         | 95 ++++++++++++++++++----
 web-next/components/ui/label-record-input.test.tsx |  5 ++
 web-next/components/ui/label-record-input.tsx      | 13 ++-
 web-next/components/ui/number-stepper.test.tsx     |  2 +
 web-next/components/ui/number-stepper.tsx          |  9 +-
 web-next/components/ui/search-row.test.tsx         | 20 +++++
 web-next/components/ui/search-row.tsx              |  9 +-
 web-next/components/ui/select.test.tsx             | 25 ++++++
 web-next/components/ui/select.tsx                  | 39 ++++++++-
 web-next/components/ui/tag-input.test.tsx          |  3 +
 web-next/components/ui/tag-input.tsx               |  9 +-
 .../components/workbench/overlay-dialog.test.tsx   | 32 ++++++++
 web-next/components/workbench/overlay-dialog.tsx   | 44 ++++++----
 .../workbench/workspace-tab-strip.test.tsx         |  3 +
 22 files changed, 570 insertions(+), 84 deletions(-)

diff --git a/web-next/app/globals.css b/web-next/app/globals.css
index 47c1c39363..9dd5cb7b76 100644
--- a/web-next/app/globals.css
+++ b/web-next/app/globals.css
@@ -1,3 +1,6 @@
+@import '../packages/design-tokens/src/tokens.css';
+@import '../packages/design-tokens/src/themes.css';
+
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
@@ -61,6 +64,30 @@
   --hb-shadow: var(--ops-panel-shadow);
   --hb-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
   --hb-mono: 'SFMono-Regular', ui-monospace, Menlo, Monaco, Consolas, monospace;
+
+  --hz-ui-canvas: #0b0d10;
+  --hz-ui-surface: #0d1014;
+  --hz-ui-surface-soft: #10141a;
+  --hz-ui-surface-raised: #131922;
+  --hz-ui-surface-graphite: #0c0e12;
+  --hz-ui-surface-muted: rgba(255, 255, 255, 0.018);
+  --hz-ui-control: #0e1218;
+  --hz-ui-code: #090b0e;
+  --hz-ui-line-strong: rgba(166, 178, 195, 0.135);
+  --hz-ui-line: rgba(166, 178, 195, 0.085);
+  --hz-ui-line-soft: rgba(166, 178, 195, 0.052);
+  --hz-ui-line-faint: rgba(166, 178, 195, 0.032);
+  --hz-ui-active: rgba(76, 92, 148, 0.22);
+  --hz-ui-active-soft: rgba(76, 92, 148, 0.095);
+  --hz-ui-accent: #7c93db;
+  --hz-ui-accent-muted: rgba(124, 147, 219, 0.32);
+  --hz-ui-action-primary: #4f6bdc;
+  --hz-ui-action-primary-hover: #5d78ee;
+  --hz-ui-action-danger: #7f232f;
+  --hz-ui-action-danger-hover: #9b2a39;
+  --hz-ui-scrollbar-size: 7px;
+  --hz-ui-scrollbar-thumb: rgba(166, 178, 195, 0.18);
+  --hz-ui-scrollbar-thumb-hover: rgba(166, 178, 195, 0.3);
 }
 
 html[data-theme='light-ops'],
@@ -499,15 +526,15 @@ body[data-theme='compact'] {
   body,
   .hb-scrollbar {
     scrollbar-width: thin;
-    scrollbar-color: hsl(var(--border)) transparent;
+    scrollbar-color: var(--hz-ui-scrollbar-thumb) transparent;
     scrollbar-gutter: stable;
   }
 
   html::-webkit-scrollbar,
   body::-webkit-scrollbar,
   .hb-scrollbar::-webkit-scrollbar {
-    width: 10px;
-    height: 10px;
+    width: var(--hz-ui-scrollbar-size);
+    height: var(--hz-ui-scrollbar-size);
   }
 
   html::-webkit-scrollbar-track,
@@ -519,16 +546,16 @@ body[data-theme='compact'] {
   html::-webkit-scrollbar-thumb,
   body::-webkit-scrollbar-thumb,
   .hb-scrollbar::-webkit-scrollbar-thumb {
-    border: 2px solid transparent;
+    border: 1px solid transparent;
     border-radius: 999px;
-    background: linear-gradient(180deg, hsl(var(--accent)), hsl(var(--secondary)));
+    background: var(--hz-ui-scrollbar-thumb);
     background-clip: padding-box;
   }
 
   html::-webkit-scrollbar-thumb:hover,
   body::-webkit-scrollbar-thumb:hover,
   .hb-scrollbar::-webkit-scrollbar-thumb:hover {
-    background: linear-gradient(180deg, hsl(var(--ring) / 0.5), hsl(var(--accent)));
+    background: var(--hz-ui-scrollbar-thumb-hover);
     background-clip: padding-box;
   }
 
diff --git a/web-next/app/loading.test.tsx b/web-next/app/loading.test.tsx
index a889d2339a..782034c37a 100644
--- a/web-next/app/loading.test.tsx
+++ b/web-next/app/loading.test.tsx
@@ -3,16 +3,18 @@ import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it } from 'vitest';
 
 describe('global app loading shell', () => {
-  it('renders a visible operator-facing route loading state instead of a blank screen', async () => {
+  it('keeps route pending feedback quiet instead of showing a full Loading workspace panel', async () => {
     const { default: Loading } = await import('./loading');
     const html = renderToStaticMarkup(<Loading />);
 
-    expect(html).toContain('data-app-route-loading="global-workbench-loading"');
-    expect(html).toContain('data-app-route-loading-spinner="true"');
+    expect(html).toContain('data-app-route-loading="quiet-route-pending"');
+    expect(html).toContain('data-app-route-loading-indicator="true"');
     expect(html).toContain('role="status"');
     expect(html).toContain('aria-busy="true"');
-    expect(html).toContain('正在加载工作台');
-    expect(html).toContain('正在准备页面数据');
+    expect(html).not.toContain('Loading workspace');
+    expect(html).not.toContain('Preparing page data');
+    expect(html).not.toContain('rounded-[4px]');
+    expect(html).not.toContain('min-h-[360px]');
     expect(html).not.toContain('/api/');
     expect(html).not.toContain('http://localhost');
   });
diff --git a/web-next/app/loading.tsx b/web-next/app/loading.tsx
index d0a8f5d27a..f447413db6 100644
--- a/web-next/app/loading.tsx
+++ b/web-next/app/loading.tsx
@@ -1,28 +1,21 @@
 import React from 'react';
-import { Loader2 } from 'lucide-react';
 
 export default function Loading() {
   return (
     <main
-      data-app-route-loading="global-workbench-loading"
+      data-app-route-loading="quiet-route-pending"
       role="status"
       aria-busy="true"
       aria-live="polite"
-      className="min-h-[calc(100vh-56px)] bg-[#07090b] px-6 py-6 text-[#e8edf5]"
+      aria-label="Route pending"
+      className="min-h-[calc(100vh-56px)] bg-[#07090b]"
     >
-      <section className="mx-auto flex min-h-[360px] w-full max-w-[1600px] items-center justify-center rounded-[4px] border border-[#252b35] bg-[#0d1015] px-6 py-12 shadow-[0_18px_60px_rgba(0,0,0,0.28)]">
-        <div className="flex max-w-[420px] flex-col items-center text-center">
-          <Loader2
-            data-app-route-loading-spinner="true"
-            className="h-8 w-8 animate-spin text-[#8fb3ff]"
-            aria-hidden="true"
-          />
-          <h1 className="mt-4 text-[18px] font-semibold tracking-normal text-[#f4f7fb]">正在加载工作台</h1>
-          <p className="mt-2 text-[13px] leading-6 text-[#9ca7ba]">
-            正在准备页面数据,请稍等。
-          </p>
-        </div>
-      </section>
+      <div className="h-px w-full overflow-hidden bg-[#11161d]">
+        <div
+          data-app-route-loading-indicator="true"
+          className="h-px w-1/3 animate-pulse bg-[#8fb3ff]"
+        />
+      </div>
     </main>
   );
 }
diff --git a/web-next/components/ui/cold-code-editor.test.tsx b/web-next/components/ui/cold-code-editor.test.tsx
index f6bbc08151..b20f4e1469 100644
--- a/web-next/components/ui/cold-code-editor.test.tsx
+++ b/web-next/components/ui/cold-code-editor.test.tsx
@@ -5,14 +5,15 @@ import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 
 vi.mock('@uiw/react-codemirror', () => ({
-  default: ({ value, readOnly, extensions, height, basicSetup, theme, onChange, ...props }: any) => (
+  default: ({ value, readOnly, editable, extensions, height, basicSetup, theme, onChange, ...props }: any) => (
     <div
       data-testid={props['data-testid']}
       data-mocked-codemirror="true"
       data-readonly={readOnly ? 'true' : 'false'}
+      data-editable={editable ? 'true' : 'false'}
       data-height={height}
       data-basic-setup={basicSetup ? 'true' : 'false'}
-      data-theme={theme ? 'custom-dark' : 'missing'}
+      data-theme={typeof theme === 'string' ? theme : theme ? 'custom-dark' : 'missing'}
       data-extension-count={Array.isArray(extensions) ? String(extensions.length) : '0'}
       onClick={() => onChange?.(`${value}\nnext`)}
     >
@@ -51,8 +52,13 @@ describe('ColdCodeEditor', () => {
     expect(source).toContain("from '@codemirror/lang-json'");
     expect(source).toContain("from '@codemirror/lang-html'");
     expect(source).toContain('EditorView.lineWrapping');
-    expect(source).toContain('theme={oneDark}');
+    expect(source).toContain("theme={theme === 'vs-dark' ? oneDark : 'light'}");
     expect(source).toContain('data-cold-code-editor="codemirror"');
+    expect(source).toContain('data-cold-code-editor-theme={theme}');
+    expect(source).toContain("data-cold-code-editor-loading={loading ? 'true' : 'false'}");
+    expect(source).toContain('data-cold-code-editor-loading-state="angular-nz-code-editor-loading"');
+    expect(source).toContain("data-cold-code-editor-folding={folding ? 'true' : 'false'}");
+    expect(source).toContain("data-cold-code-editor-automatic-layout={automaticLayout ? 'true' : 'false'}");
     expect(source).toContain('data-cold-code-editor-license="codemirror-mit"');
     expect(source).not.toContain('<textarea');
 
@@ -69,10 +75,74 @@ describe('ColdCodeEditor', () => {
 
     expect(html).toContain('data-cold-code-editor="codemirror"');
     expect(html).toContain('data-cold-code-editor-language="yaml"');
+    expect(html).toContain('data-cold-code-editor-theme="vs-dark"');
+    expect(html).toContain('data-cold-code-editor-loading="false"');
+    expect(html).toContain('data-cold-code-editor-loading-owner="cold-code-editor"');
+    expect(html).toContain('data-cold-code-editor-folding="true"');
+    expect(html).toContain('data-cold-code-editor-automatic-layout="true"');
     expect(html).toContain('data-cold-code-editor-readonly="true"');
     expect(html).toContain('data-cold-code-editor-license="codemirror-mit"');
     expect(html).toContain('data-mocked-codemirror="true"');
+    expect(html).toContain('data-editable="false"');
     expect(html).toContain('data-theme="custom-dark"');
     expect(html).toContain('apiVersion: hertzbeat/v1');
+    expect(html).not.toContain('data-cold-code-editor-loading-state="angular-nz-code-editor-loading"');
+  });
+
+  it('can expose the Angular light editor theme contract', async () => {
+    const { ColdCodeEditor } = await import('./cold-code-editor');
+    const html = renderToStaticMarkup(
+      <ColdCodeEditor
+        data-testid="cold-code-editor-light"
+        language="yaml"
+        theme="vs"
+        value="app: mysql"
+        onChange={vi.fn()}
+      />
+    );
+
+    expect(html).toContain('data-cold-code-editor-theme="vs"');
+    expect(html).toContain('data-theme="light"');
+  });
+
+  it('lets routes surface disabled folding and automatic layout contracts when needed', async () => {
+    const { ColdCodeEditor } = await import('./cold-code-editor');
+    const html = renderToStaticMarkup(
+      <ColdCodeEditor
+        data-testid="cold-code-editor-options"
+        language="yaml"
+        value="app: custom"
+        folding={false}
+        automaticLayout={false}
+        onChange={vi.fn()}
+      />
+    );
+
+    expect(html).toContain('data-cold-code-editor-folding="false"');
+    expect(html).toContain('data-cold-code-editor-automatic-layout="false"');
+  });
+
+  it('mirrors the Angular nz-code-editor loading contract', async () => {
+    const { ColdCodeEditor } = await import('./cold-code-editor');
+    const html = renderToStaticMarkup(
+      <ColdCodeEditor
+        data-testid="cold-code-editor-loading"
+        language="yaml"
+        value="app: loading"
+        loading
+        loadingLabel="Loading template YAML"
+        onChange={vi.fn()}
+      />
+    );
+
+    expect(html).toContain('data-cold-code-editor-loading="true"');
+    expect(html).toContain('data-cold-code-editor-loading-owner="cold-code-editor"');
+    expect(html).toContain('aria-busy="true"');
+    expect(html).toContain('data-cold-code-editor-readonly="true"');
+    expect(html).toContain('data-readonly="true"');
+    expect(html).toContain('data-editable="false"');
+    expect(html).toContain('data-cold-code-editor-loading-state="angular-nz-code-editor-loading"');
+    expect(html).toContain('data-cold-code-editor-loading-state-owner="cold-code-editor"');
+    expect(html).toContain('Loading template YAML');
   });
 });
diff --git a/web-next/components/ui/cold-code-editor.tsx b/web-next/components/ui/cold-code-editor.tsx
index 0f33b69ea6..734139342f 100644
--- a/web-next/components/ui/cold-code-editor.tsx
+++ b/web-next/components/ui/cold-code-editor.tsx
@@ -14,12 +14,18 @@ import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
 export type ColdCodeEditorLanguage = 'yaml' | 'json' | 'html' | 'javascript' | 'shell' | 'text';
+export type ColdCodeEditorTheme = 'vs' | 'vs-dark';
 
 export type ColdCodeEditorProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> & {
   value: string;
   onChange?: (value: string) => void;
   language?: ColdCodeEditorLanguage;
+  theme?: ColdCodeEditorTheme;
   readOnly?: boolean;
+  loading?: boolean;
+  loadingLabel?: React.ReactNode;
+  folding?: boolean;
+  automaticLayout?: boolean;
   height?: string;
   minHeight?: string;
   placeholder?: string;
@@ -76,6 +82,55 @@ const coldCodeEditorTheme = EditorView.theme({
   dark: true
 });
 
+const coldCodeEditorLightTheme = EditorView.theme({
+  '&': {
+    backgroundColor: '#f8fafc',
+    color: '#111827',
+    border: '1px solid #cbd5e1',
+    borderRadius: '3px',
+    fontSize: '12px',
+    minHeight: 'inherit'
+  },
+  '&.cm-focused': {
+    borderColor: '#4e74f8',
+    outline: '2px solid rgba(78,116,248,0.14)'
+  },
+  '.cm-scroller': {
+    fontFamily:
+      'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
+    lineHeight: '1.6',
+    minHeight: 'inherit'
+  },
+  '.cm-content': {
+    padding: '8px 0',
+    caretColor: '#1f2937',
+    minHeight: 'inherit'
+  },
+  '.cm-line': {
+    padding: '0 12px'
+  },
+  '.cm-gutters': {
+    backgroundColor: '#eef2f7',
+    borderRight: '1px solid #cbd5e1',
+    color: '#64748b'
+  },
+  '.cm-activeLine': {
+    backgroundColor: '#e8f0ff'
+  },
+  '.cm-activeLineGutter': {
+    backgroundColor: '#e8f0ff',
+    color: '#1f2937'
+  },
+  '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
+    backgroundColor: 'rgba(78,116,248,0.24)'
+  },
+  '.cm-placeholder': {
+    color: '#64748b'
+  }
+}, {
+  dark: false
+});
+
 function getLanguageExtension(language: ColdCodeEditorLanguage): Extension | null {
   switch (language) {
     case 'yaml':
@@ -97,7 +152,12 @@ export function ColdCodeEditor({
   value,
   onChange,
   language = 'text',
+  theme = 'vs-dark',
   readOnly = false,
+  loading = false,
+  loadingLabel = 'Loading editor',
+  folding = true,
+  automaticLayout = true,
   height,
   minHeight = '220px',
   placeholder,
@@ -109,23 +169,30 @@ export function ColdCodeEditor({
 }: ColdCodeEditorProps) {
   const extensions = useMemo(() => {
     const languageExtension = getLanguageExtension(language);
+    const themeExtension = theme === 'vs-dark' ? coldCodeEditorTheme : coldCodeEditorLightTheme;
     return [
       basicSetup,
-      coldCodeEditorTheme,
+      themeExtension,
       EditorView.lineWrapping,
       ...(languageExtension ? [languageExtension] : []),
-      ...(readOnly ? [EditorState.readOnly.of(true)] : [])
+      ...(readOnly || loading ? [EditorState.readOnly.of(true)] : [])
     ];
-  }, [language, readOnly]);
+  }, [language, loading, readOnly, theme]);
 
   return (
     <div
       {...props}
       data-cold-code-editor="codemirror"
       data-cold-code-editor-language={language}
-      data-cold-code-editor-readonly={readOnly ? 'true' : undefined}
+      data-cold-code-editor-theme={theme}
+      data-cold-code-editor-loading={loading ? 'true' : 'false'}
+      data-cold-code-editor-loading-owner="cold-code-editor"
+      data-cold-code-editor-folding={folding ? 'true' : 'false'}
+      data-cold-code-editor-automatic-layout={automaticLayout ? 'true' : 'false'}
+      data-cold-code-editor-readonly={readOnly || loading ? 'true' : undefined}
       data-cold-code-editor-license="codemirror-mit"
-      className={cn('min-w-0 overflow-hidden rounded-[3px]', className)}
+      aria-busy={loading ? 'true' : undefined}
+      className={cn('relative min-w-0 overflow-hidden rounded-[3px]', className)}
       style={{ minHeight, ...style }}
     >
       {name ? <HiddenInput name={name} value={value} data-cold-code-editor-value="hidden" /> : null}
@@ -135,13 +202,22 @@ export function ColdCodeEditor({
         minHeight={minHeight}
         placeholder={placeholder}
         basicSetup={false}
-        theme={oneDark}
+        theme={theme === 'vs-dark' ? oneDark : 'light'}
         extensions={extensions}
-        readOnly={readOnly}
-        editable={!readOnly}
+        readOnly={readOnly || loading}
+        editable={!readOnly && !loading}
         aria-label={ariaLabel}
         onChange={nextValue => onChange?.(nextValue)}
       />
+      {loading ? (
+        <div
+          data-cold-code-editor-loading-state="angular-nz-code-editor-loading"
+          data-cold-code-editor-loading-state-owner="cold-code-editor"
+          className="absolute inset-0 flex items-center justify-center bg-[#0b0c0e]/72 text-[12px] font-semibold text-[#dbe4f0] backdrop-blur-[1px]"
+        >
+          {loadingLabel}
+        </div>
+      ) : null}
     </div>
   );
 }
diff --git a/web-next/components/ui/cold-confirm-dialog.test.tsx b/web-next/components/ui/cold-confirm-dialog.test.tsx
new file mode 100644
index 0000000000..b1a6ef5540
--- /dev/null
+++ b/web-next/components/ui/cold-confirm-dialog.test.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it } from 'vitest';
+
+import { ColdConfirmDialog } from './cold-confirm-dialog';
+
+describe('ColdConfirmDialog', () => {
+  it('renders shared confirmation chrome from runtime messages while preserving caller labels', () => {
+    const idleHtml = renderToStaticMarkup(
+      <ColdConfirmDialog
+        open
+        title="确认删除"
+        copy="删除后不可恢复。"
+        confirmLabel="确认"
+        cancelLabel="取消"
+        onCancel={() => undefined}
+        onConfirm={() => undefined}
+      />
+    );
+
+    expect(idleHtml).toContain('data-cold-confirm-dialog="cold-confirm-dialog"');
+    expect(idleHtml).toContain('Confirm operation');
+    expect(idleHtml).toContain('确认删除');
+    expect(idleHtml).toContain('删除后不可恢复。');
+    expect(idleHtml).toContain('确认');
+    expect(idleHtml).toContain('取消');
+
+    const pendingHtml = renderToStaticMarkup(
+      <ColdConfirmDialog
+        open
+        title="确认删除"
+        copy="删除后不可恢复。"
+        confirmLabel="确认"
+        cancelLabel="取消"
+        pending
+        onCancel={() => undefined}
+        onConfirm={() => undefined}
+      />
+    );
+
+    expect(pendingHtml).toContain('Processing');
+    expect(pendingHtml).toContain('disabled=""');
+  });
+});
diff --git a/web-next/components/ui/cold-confirm-dialog.tsx b/web-next/components/ui/cold-confirm-dialog.tsx
index 0f33abb76a..abab8ef957 100644
--- a/web-next/components/ui/cold-confirm-dialog.tsx
+++ b/web-next/components/ui/cold-confirm-dialog.tsx
@@ -4,6 +4,11 @@ import React from 'react';
 import { AlertTriangle } from 'lucide-react';
 import { Button } from './button';
 import { OverlayDialog } from '../workbench/overlay-dialog';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
+
+function translateColdConfirm(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
 
 type ColdConfirmDialogProps = {
   open: boolean;
@@ -30,7 +35,7 @@ export function ColdConfirmDialog({
     <OverlayDialog
       open={open}
       title={title}
-      kicker="确认操作"
+      kicker={translateColdConfirm('common.confirm.operation')}
       onClose={pending ? () => undefined : onCancel}
       maxWidthClassName="max-w-md"
       contentClassName="py-4"
@@ -40,7 +45,7 @@ export function ColdConfirmDialog({
             {cancelLabel}
           </Button>
           <Button size="sm" variant="primary" onClick={onConfirm} disabled={pending}>
-            {pending ? '处理中' : confirmLabel}
+            {pending ? translateColdConfirm('common.processing') : confirmLabel}
           </Button>
         </div>
       }
diff --git a/web-next/components/ui/date-time-range.test.tsx b/web-next/components/ui/date-time-range.test.tsx
index 6e9333e294..d4a298ca3b 100644
--- a/web-next/components/ui/date-time-range.test.tsx
+++ b/web-next/components/ui/date-time-range.test.tsx
@@ -118,4 +118,53 @@ describe('DateTimeRange', () => {
     expect(html).not.toContain('今天');
     expect(html).not.toContain('type="time"');
   });
+
+  it('renders English fallback default trigger labels', () => {
+    const html = renderToStaticMarkup(
+      <DateTimeRange
+        mode="time"
+        startName="periodStart"
+        endName="periodEnd"
+        startValue=""
+        endValue=""
+        onStartChange={vi.fn()}
+        onEndChange={vi.fn()}
+      />
+    );
+
+    expect(html.match(/>Not set</g)).toHaveLength(2);
+    expect(html).toContain('aria-label="Start"');
+    expect(html).toContain('aria-label="End"');
+    expect(html).not.toContain('未设置');
+    expect(html).not.toContain('aria-label="开始"');
+    expect(html).not.toContain('aria-label="结束"');
+  });
+
+  it('keeps empty-state picker chrome caller-owned', () => {
+    const html = renderToStaticMarkup(
+      <DateTimeRange
+        mode="time"
+        startName="periodStart"
+        endName="periodEnd"
+        startValue=""
+        endValue=""
+        onStartChange={vi.fn()}
+        onEndChange={vi.fn()}
+        startLabel="Start time"
+        endLabel="End time"
+        emptyLabel="Not set"
+        hourLabel="Hour"
+        minuteLabel="Minute"
+        previousMonthLabel="Previous month"
+        nextMonthLabel="Next month"
+        clearLabel="Clear"
+        confirmLabel="OK"
+      />
+    );
+
+    expect(html.match(/Not set/g)).toHaveLength(2);
+    expect(html).toContain('aria-label="Start time"');
+    expect(html).toContain('aria-label="End time"');
+    expect(html).not.toContain('未设置');
+  });
 });
diff --git a/web-next/components/ui/date-time-range.tsx b/web-next/components/ui/date-time-range.tsx
index e87b4a7fd7..15f057cbf1 100644
--- a/web-next/components/ui/date-time-range.tsx
+++ b/web-next/components/ui/date-time-range.tsx
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
 import ReactDatePicker, { registerLocale } from 'react-datepicker';
 import { zhCN } from 'date-fns/locale/zh-CN';
 import { ArrowRight } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
@@ -16,6 +17,13 @@ export interface DateTimeRangeProps extends React.HTMLAttributes<HTMLDivElement>
   onEndChange: (value: string) => void;
   startLabel?: string;
   endLabel?: string;
+  emptyLabel?: string;
+  hourLabel?: string;
+  minuteLabel?: string;
+  previousMonthLabel?: string;
+  nextMonthLabel?: string;
+  clearLabel?: string;
+  confirmLabel?: string;
   reserveActionSpace?: boolean;
 }
 
@@ -34,6 +42,10 @@ const TIME_MINUTES = Array.from({ length: 60 }, (_, minute) => minute);
 
 registerLocale('zh-CN', zhCN);
 
+function translateDateTimeRange(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
+
 function pad2(value: number) {
   return String(value).padStart(2, '0');
 }
@@ -85,8 +97,8 @@ function formatDateChange(mode: DateTimeRangeProps['mode'], value: string, nextD
   return formatFromDate(mode, nextDate);
 }
 
-function formatValue(mode: DateTimeRangeProps['mode'], value: string) {
-  if (!value) return '未设置';
+function formatValue(mode: DateTimeRangeProps['mode'], value: string, emptyLabel: string) {
+  if (!value) return emptyLabel;
   return mode === 'time' ? value.slice(0, 5) : value.replace('T', ' ');
 }
 
@@ -188,11 +200,15 @@ function TimeColumn({
 function TimeColumns({
   mode,
   value,
-  onChange
+  onChange,
+  hourLabel,
+  minuteLabel
 }: {
   mode: DateTimeRangeProps['mode'];
   value: string;
   onChange: (value: string) => void;
+  hourLabel: string;
+  minuteLabel: string;
 }) {
   const { hour, minute } = getTimeParts(mode, value);
   return (
@@ -204,14 +220,14 @@ function TimeColumns({
       )}
     >
       <TimeColumn
-        label="时"
+        label={hourLabel}
         column="hour"
         values={TIME_HOURS}
         selectedValue={hour}
         onSelect={nextHour => onChange(formatWithTime(mode, value, nextHour, minute))}
       />
       <TimeColumn
-        label="分"
+        label={minuteLabel}
         column="minute"
         values={TIME_MINUTES}
         selectedValue={minute}
@@ -227,9 +243,29 @@ type ColdDateTimePickerProps = {
   value: string;
   label: string;
   onChange: (value: string) => void;
+  emptyLabel: string;
+  hourLabel: string;
+  minuteLabel: string;
+  previousMonthLabel: string;
+  nextMonthLabel: string;
+  clearLabel: string;
+  confirmLabel: string;
 };
 
-function ColdDateTimePicker({ mode, name, value, label, onChange }: ColdDateTimePickerProps) {
+function ColdDateTimePicker({
+  mode,
+  name,
+  value,
+  label,
+  onChange,
+  emptyLabel,
+  hourLabel,
+  minuteLabel,
+  previousMonthLabel,
+  nextMonthLabel,
+  clearLabel,
+  confirmLabel
+}: ColdDateTimePickerProps) {
   const triggerRef = React.useRef<HTMLButtonElement | null>(null);
   const [open, setOpen] = React.useState(false);
   const [draftValue, setDraftValue] = React.useState(value);
@@ -301,10 +337,10 @@ function ColdDateTimePicker({ mode, name, value, label, onChange }: ColdDateTime
               onChange={next => setDraftValue(formatDateChange(mode, draftValue, next))}
               inline
               locale="zh-CN"
-              previousMonthButtonLabel="上个月"
-              nextMonthButtonLabel="下个月"
-              previousMonthAriaLabel="上个月"
-              nextMonthAriaLabel="下个月"
+              previousMonthButtonLabel={previousMonthLabel}
+              nextMonthButtonLabel={nextMonthLabel}
+              previousMonthAriaLabel={previousMonthLabel}
+              nextMonthAriaLabel={nextMonthLabel}
               calendarClassName="hertzbeat-date-picker-calendar"
               shouldCloseOnSelect={false}
               dateFormat="yyyy-MM-dd HH:mm"
@@ -312,7 +348,13 @@ function ColdDateTimePicker({ mode, name, value, label, onChange }: ColdDateTime
             />
           </div>
         )}
-        <TimeColumns mode={mode} value={draftValue} onChange={setDraftValue} />
+        <TimeColumns
+          mode={mode}
+          value={draftValue}
+          onChange={setDraftValue}
+          hourLabel={hourLabel}
+          minuteLabel={minuteLabel}
+        />
       </div>
         <div data-cold-date-time-picker-panel="body-portal-clear-confirm" className="flex justify-end gap-2 border-t border-[#252b34] p-3">
           <button
@@ -321,7 +363,7 @@ function ColdDateTimePicker({ mode, name, value, label, onChange }: ColdDateTime
             className="h-7 min-w-[58px] rounded-[3px] border border-[#2b3039] bg-[#0d1015] px-2 text-[12px] font-semibold text-[#a9b0bb] hover:border-[#3b4454] hover:text-[#dbe4f0]"
             onClick={clearValue}
           >
-            清除
+            {clearLabel}
           </button>
           <button
             type="button"
@@ -329,7 +371,7 @@ function ColdDateTimePicker({ mode, name, value, label, onChange }: ColdDateTime
             className="h-7 min-w-[58px] rounded-[3px] border border-[#31405c] bg-[#182238] px-2 text-[12px] font-semibold text-[#d8e4ff] hover:border-[#4e74f8]"
             onClick={confirmValue}
           >
-            确定
+            {confirmLabel}
           </button>
         </div>
     </div>
@@ -344,7 +386,7 @@ function ColdDateTimePicker({ mode, name, value, label, onChange }: ColdDateTime
       <HiddenInput data-cold-date-time-picker-input="hidden-value" name={name} value={value} />
       <PickerTrigger
         ref={triggerRef}
-        displayValue={formatValue(mode, open ? draftValue : value)}
+        displayValue={formatValue(mode, open ? draftValue : value, emptyLabel)}
         empty={!(open ? draftValue : value)}
         expanded={open}
         aria-label={label}
@@ -363,8 +405,15 @@ export function DateTimeRange({
   endValue,
   onStartChange,
   onEndChange,
-  startLabel = '开始',
-  endLabel = '结束',
+  startLabel = translateDateTimeRange('time.range.start'),
+  endLabel = translateDateTimeRange('time.range.end'),
+  emptyLabel = translateDateTimeRange('time.range.unset'),
+  hourLabel = translateDateTimeRange('time.range.hour'),
+  minuteLabel = translateDateTimeRange('time.range.minute'),
+  previousMonthLabel = translateDateTimeRange('time.range.previous-month'),
+  nextMonthLabel = translateDateTimeRange('time.range.next-month'),
+  clearLabel = translateDateTimeRange('common.clear'),
+  confirmLabel = translateDateTimeRange('common.button.ok'),
   reserveActionSpace,
   className,
   ...props
@@ -392,6 +441,13 @@ export function DateTimeRange({
         value={startValue}
         label={startLabel}
         onChange={onStartChange}
+        emptyLabel={emptyLabel}
+        hourLabel={hourLabel}
+        minuteLabel={minuteLabel}
+        previousMonthLabel={previousMonthLabel}
+        nextMonthLabel={nextMonthLabel}
+        clearLabel={clearLabel}
+        confirmLabel={confirmLabel}
       />
       <ArrowRight className="mx-auto h-3.5 w-3.5 shrink-0 text-[#7e8494]" aria-hidden="true" />
       <ColdDateTimePicker
@@ -400,6 +456,13 @@ export function DateTimeRange({
         value={endValue}
         label={endLabel}
         onChange={onEndChange}
+        emptyLabel={emptyLabel}
+        hourLabel={hourLabel}
+        minuteLabel={minuteLabel}
+        previousMonthLabel={previousMonthLabel}
+        nextMonthLabel={nextMonthLabel}
+        clearLabel={clearLabel}
+        confirmLabel={confirmLabel}
       />
       {reserveActionSpace ? <span data-cold-date-time-range-reserved-action="true" aria-hidden="true" /> : null}
     </div>
diff --git a/web-next/components/ui/label-record-input.test.tsx b/web-next/components/ui/label-record-input.test.tsx
index 880e8f26e7..5925f3ed95 100644
--- a/web-next/components/ui/label-record-input.test.tsx
+++ b/web-next/components/ui/label-record-input.test.tsx
@@ -34,6 +34,11 @@ describe('LabelRecordInput', () => {
     expect(html).toContain('data-cold-label-selector-value-input="searchable-value"');
     expect(html).toContain('data-cold-label-selector-value="hidden"');
     expect(html).toContain('type="hidden"');
+    expect(html).toContain('placeholder="Label key"');
+    expect(html).toContain('placeholder="Label value"');
+    expect(html).toContain('>Add<');
+    expect(html).toContain('>Remove<');
+    expect(html).toContain('aria-label="Remove service:checkout"');
     expect(html).toContain('rounded-[3px]');
     expect(html).not.toContain('data-cold-label-selector-chip-list="true"');
     expect(html).not.toContain('data-cold-label-selector-chip=');
diff --git a/web-next/components/ui/label-record-input.tsx b/web-next/components/ui/label-record-input.tsx
index ae2af381c1..3d1e6683e3 100644
--- a/web-next/components/ui/label-record-input.tsx
+++ b/web-next/components/ui/label-record-input.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
 import { Plus, X } from 'lucide-react';
 import type { AlertLabelOptions } from '../../lib/alert-label-options';
 import { DEFAULT_ALERT_LABEL_OPTIONS } from '../../lib/alert-label-options';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
@@ -54,6 +55,10 @@ function filterSuggestions(values: string[], query: string, limit = 8) {
     .slice(0, limit);
 }
 
+function translateLabelRecordInput(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
+
 export interface LabelRecordInputProps {
   value: string;
   onValueChange: (value: string) => void;
@@ -75,10 +80,10 @@ export const LabelRecordInput = React.forwardRef<HTMLInputElement, LabelRecordIn
       name,
       disabled,
       labelOptions = DEFAULT_ALERT_LABEL_OPTIONS,
-      keyPlaceholder = '标签名',
-      valuePlaceholder = '标签值',
-      addLabel = '添加',
-      removeLabel = '删除',
+      keyPlaceholder = translateLabelRecordInput('common.label.key'),
+      valuePlaceholder = translateLabelRecordInput('common.label.value'),
+      addLabel = translateLabelRecordInput('common.add'),
+      removeLabel = translateLabelRecordInput('common.remove'),
       containerClassName
     },
     ref
diff --git a/web-next/components/ui/number-stepper.test.tsx b/web-next/components/ui/number-stepper.test.tsx
index de34123e57..0be0be91cd 100644
--- a/web-next/components/ui/number-stepper.test.tsx
+++ b/web-next/components/ui/number-stepper.test.tsx
@@ -11,6 +11,8 @@ describe('NumberStepper', () => {
     expect(html).toContain('data-cold-number-stepper-input="true"');
     expect(html).toContain('data-cold-number-stepper-action="decrement"');
     expect(html).toContain('data-cold-number-stepper-action="increment"');
+    expect(html).toContain('>Decrease<');
+    expect(html).toContain('>Increase<');
     expect(html).toContain('inputMode="numeric"');
     expect(html).toContain('rounded-[3px]');
     expect(html).not.toContain('type="number"');
diff --git a/web-next/components/ui/number-stepper.tsx b/web-next/components/ui/number-stepper.tsx
index c44026afdc..c769372199 100644
--- a/web-next/components/ui/number-stepper.tsx
+++ b/web-next/components/ui/number-stepper.tsx
@@ -1,5 +1,6 @@
 import * as React from 'react';
 import { Minus, Plus } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 
 function toNumber(value: string | number | undefined, fallback: number) {
@@ -18,6 +19,10 @@ function formatNumber(value: number) {
   return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(6)));
 }
 
+function translateNumberStepper(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
+
 export interface NumberStepperProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'value' | 'onChange'> {
   value: string;
   onValueChange: (value: string) => void;
@@ -37,8 +42,8 @@ const NumberStepper = React.forwardRef<HTMLInputElement, NumberStepperProps>(
       step = 1,
       value,
       onValueChange,
-      decrementLabel = '减少',
-      incrementLabel = '增加',
+      decrementLabel = translateNumberStepper('common.decrement'),
+      incrementLabel = translateNumberStepper('common.increment'),
       ...props
     },
     ref
diff --git a/web-next/components/ui/search-row.test.tsx b/web-next/components/ui/search-row.test.tsx
index ae2fd67397..c71b588dd0 100644
--- a/web-next/components/ui/search-row.test.tsx
+++ b/web-next/components/ui/search-row.test.tsx
@@ -24,6 +24,9 @@ describe('SearchRow', () => {
     expect(html).toContain('data-cold-search-chrome="no-extra-input-shell"');
     expect(html).toContain('data-cold-search-action="submit"');
     expect(html).toContain('data-cold-search-action="clear"');
+    expect(html).toContain('<form');
+    expect(html).toContain('type="search"');
+    expect(html).toContain('type="submit"');
     expect(html).toContain('w-[320px]');
     expect(html).toContain('w-fit');
     expect(html).toContain('max-w-full');
@@ -54,4 +57,21 @@ describe('SearchRow', () => {
       html.indexOf('data-cold-search-action="submit"')
     );
   });
+
+  it('uses runtime defaults for action labels when callers omit shared copy', () => {
+    const html = renderToStaticMarkup(
+      <SearchRow
+        value="weekday"
+        placeholder="Policy name"
+        onValueChange={vi.fn()}
+        onSearch={vi.fn()}
+        onClear={vi.fn()}
+      />
+    );
+
+    expect(html).toContain('data-cold-search-action="submit"');
+    expect(html).toContain('data-cold-search-action="clear"');
+    expect(html).toContain('Search');
+    expect(html).toContain('Clear');
+  });
 });
diff --git a/web-next/components/ui/search-row.tsx b/web-next/components/ui/search-row.tsx
index 2a341e4c9f..300c839d39 100644
--- a/web-next/components/ui/search-row.tsx
+++ b/web-next/components/ui/search-row.tsx
@@ -2,11 +2,12 @@ import * as React from 'react';
 import { Search, X } from 'lucide-react';
 import { Button } from './button';
 import { cn } from '../../lib/utils';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 
 export interface SearchRowProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'onChange'> {
   value: string;
   placeholder: string;
-  searchLabel: string;
+  searchLabel?: string;
   clearLabel?: string;
   filters?: React.ReactNode;
   inputWidthClassName?: string;
@@ -20,12 +21,14 @@ export interface SearchRowProps extends Omit<React.FormHTMLAttributes<HTMLFormEl
 
 const searchButtonClassName =
   'h-8 min-w-[72px] rounded-[3px] border-[#2b3039] bg-[#101217] px-3 text-[12px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] hover:bg-[#151b28] hover:text-white';
+const DEFAULT_SEARCH_ROW_SEARCH_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['common.search'] ?? 'common.search';
+const DEFAULT_SEARCH_ROW_CLEAR_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['common.clear'] ?? 'common.clear';
 
 export function SearchRow({
   value,
   placeholder,
-  searchLabel,
-  clearLabel,
+  searchLabel = DEFAULT_SEARCH_ROW_SEARCH_LABEL,
+  clearLabel = DEFAULT_SEARCH_ROW_CLEAR_LABEL,
   filters,
   inputWidthClassName = 'w-[320px]',
   searchDisabled = false,
diff --git a/web-next/components/ui/select.test.tsx b/web-next/components/ui/select.test.tsx
index 78aecac21a..314dc2d0e7 100644
--- a/web-next/components/ui/select.test.tsx
+++ b/web-next/components/ui/select.test.tsx
@@ -59,6 +59,31 @@ describe('Select', () => {
 
     expect(directNativeSelectFiles).toEqual([]);
   });
+
+  it('renders the catalog-backed empty trigger label when no option is available', () => {
+    const html = renderToStaticMarkup(<Select aria-label="empty select" />);
+
+    expect(html).toContain('data-cold-select-control="custom-trigger"');
+    expect(html).toContain('None');
+    expect(html).not.toContain('>-<');
+  });
+
+  it('can render the Angular searchable dropdown affordance for settings timezones', () => {
+    const html = renderToStaticMarkup(
+      <Select searchable searchPlaceholder="Search timezone" defaultOpen defaultValue="Asia/Shanghai" aria-label="Timezone">
+        <option value="Asia/Shanghai">Asia/Shanghai (+08:00) Shanghai</option>
+        <option value="UTC">UTC (+00:00) UTC</option>
+      </Select>
+    );
+    const source = readFileSync(resolve(process.cwd(), 'components/ui/select.tsx'), 'utf8');
+
+    expect(html).toContain('data-cold-select-search="angular-nz-show-search"');
+    expect(html).toContain('type="search"');
+    expect(html).toContain('placeholder="Search timezone"');
+    expect(html).toContain('data-cold-select-listbox="custom-menu"');
+    expect(source).toContain('searchable?: boolean');
+    expect(source).toContain('data-cold-select-empty="search-empty"');
+  });
 });
 
 function walkSourceFiles(dir: string): string[] {
diff --git a/web-next/components/ui/select.tsx b/web-next/components/ui/select.tsx
index 16d4064239..6bdfe8317a 100644
--- a/web-next/components/ui/select.tsx
+++ b/web-next/components/ui/select.tsx
@@ -1,6 +1,7 @@
 import * as React from 'react';
 import { createPortal } from 'react-dom';
 import { ChevronDown } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
@@ -15,6 +16,8 @@ export const coldSelectIconClassName =
 const coldSelectListboxClassName =
   'fixed z-[90] min-w-[220px] overflow-y-auto rounded-[4px] border border-[#2b3039] bg-[#111318] p-1 text-[12px] font-semibold text-[#dbe4f0] shadow-[0_22px_60px_rgba(0,0,0,0.55)]';
 
+const DEFAULT_SELECT_EMPTY_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['common.none'] ?? 'common.none';
+
 type SelectOption = {
   value: string;
   label: React.ReactNode;
@@ -24,6 +27,8 @@ type SelectOption = {
 export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
   containerClassName?: string;
   defaultOpen?: boolean;
+  searchable?: boolean;
+  searchPlaceholder?: string;
   onValueChange?: (value: string) => void;
 }
 
@@ -73,6 +78,8 @@ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
       id,
       'aria-label': ariaLabel,
       defaultOpen = false,
+      searchable = false,
+      searchPlaceholder,
       ...props
     },
     ref
@@ -82,6 +89,7 @@ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
     const isControlled = value !== undefined;
     const [internalValue, setInternalValue] = React.useState(String(defaultValue ?? fallbackValue));
     const [open, setOpen] = React.useState(defaultOpen);
+    const [searchQuery, setSearchQuery] = React.useState('');
     const [listboxStyle, setListboxStyle] = React.useState<React.CSSProperties>({ position: 'fixed' });
     const shellRef = React.useRef<HTMLSpanElement>(null);
     const triggerRef = React.useRef<HTMLButtonElement>(null);
@@ -91,6 +99,10 @@ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
     const listboxId = `${triggerId}-listbox`;
     const selectedValue = String(isControlled ? value : internalValue);
     const selectedOption = options.find(option => option.value === selectedValue) ?? options.find(option => !option.disabled);
+    const normalizedSearchQuery = searchQuery.trim().toLowerCase();
+    const visibleOptions = searchable && normalizedSearchQuery
+      ? options.filter(option => React.Children.toArray(option.label).join(' ').toLowerCase().includes(normalizedSearchQuery))
+      : options;
 
     const updateListboxGeometry = React.useCallback(() => {
       if (typeof window === 'undefined' || !triggerRef.current) return;
@@ -134,6 +146,10 @@ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
       return () => document.removeEventListener('pointerdown', onPointerDown);
     }, [open]);
 
+    React.useEffect(() => {
+      if (!open) setSearchQuery('');
+    }, [open]);
+
     React.useEffect(() => {
       if (!open || typeof window === 'undefined') return;
       window.addEventListener('resize', updateListboxGeometry);
@@ -172,7 +188,21 @@ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
         className={cn(coldSelectListboxClassName)}
         style={listboxStyle}
       >
-        {options.map(option => {
+        {searchable ? (
+          <input
+            type="search"
+            value={searchQuery}
+            placeholder={searchPlaceholder}
+            aria-label={searchPlaceholder}
+            data-cold-select-search="angular-nz-show-search"
+            className="mb-1 h-8 w-full rounded-[3px] border border-[#2b3039] bg-[#0b0c0e] px-2.5 text-[12px] font-semibold text-[#dbe4f0] outline-none placeholder:text-[#858d9a] focus:border-[#4e74f8] focus:ring-2 focus:ring-[rgba(78,116,248,0.12)]"
+            onChange={event => setSearchQuery(event.target.value)}
+            onKeyDown={event => {
+              if (event.key === 'Escape') setOpen(false);
+            }}
+          />
+        ) : null}
+        {visibleOptions.map(option => {
           const selected = option.value === selectedOption?.value;
           return (
             <button
@@ -194,6 +224,11 @@ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
             </button>
           );
         })}
+        {visibleOptions.length === 0 ? (
+          <div data-cold-select-empty="search-empty" className="px-2.5 py-2 text-[12px] text-[#8f99ab]">
+            {DEFAULT_SELECT_EMPTY_LABEL}
+          </div>
+        ) : null}
       </div>
     ) : null;
 
@@ -257,7 +292,7 @@ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
             }
           }}
         >
-          <span className="min-w-0 truncate">{selectedOption?.label ?? '-'}</span>
+          <span className="min-w-0 truncate">{selectedOption?.label ?? DEFAULT_SELECT_EMPTY_LABEL}</span>
         </button>
         <ChevronDown data-cold-select-icon="chevron" className={cn(coldSelectIconClassName, open ? 'text-[#dbe4f0]' : null)} aria-hidden="true" />
         {listbox ? (canUsePortal ? createPortal(listbox, document.body) : listbox) : null}
diff --git a/web-next/components/ui/tag-input.test.tsx b/web-next/components/ui/tag-input.test.tsx
index 3fbc927fdb..f9a4d4871e 100644
--- a/web-next/components/ui/tag-input.test.tsx
+++ b/web-next/components/ui/tag-input.test.tsx
@@ -27,6 +27,9 @@ describe('TagInput', () => {
     expect(html).toContain('data-cold-tag-input-control="draft"');
     expect(html).toContain('data-cold-tag-input-value="hidden"');
     expect(html).toContain('type="hidden"');
+    expect(html).toContain('placeholder="Add tag"');
+    expect(html).toContain('aria-label="Remove alertname"');
+    expect(html).toContain('aria-label="Remove service"');
     expect(html).toContain('rounded-[3px]');
     expect(html).not.toContain('data-cold-tag-suggestion=');
     expect(html).not.toContain('data-cold-tag-suggestions-owner=');
diff --git a/web-next/components/ui/tag-input.tsx b/web-next/components/ui/tag-input.tsx
index e0eadcde2d..ebb31b04eb 100644
--- a/web-next/components/ui/tag-input.tsx
+++ b/web-next/components/ui/tag-input.tsx
@@ -1,5 +1,6 @@
 import * as React from 'react';
 import { X } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
@@ -20,6 +21,10 @@ type PopoverMetrics = {
   width: number;
 };
 
+function translateTagInput(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
+
 export interface TagInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
   value: string;
   onValueChange: (value: string) => void;
@@ -118,7 +123,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
               disabled={disabled}
               data-cold-tag-remove={tag}
               className="grid h-4 w-4 place-items-center rounded-[2px] text-[#8f99ab] transition hover:bg-[#202838] hover:text-[#f5f7fb] disabled:pointer-events-none"
-              aria-label={`删除 ${tag}`}
+              aria-label={`${translateTagInput('common.remove')} ${tag}`}
               onClick={() => removeTag(tag)}
             >
               <X className="h-3 w-3" aria-hidden="true" />
@@ -135,7 +140,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
             'h-6 min-w-[120px] flex-1 border-0 bg-transparent px-1 text-[12px] font-semibold text-[#dbe4f0] outline-none placeholder:text-[#858d9a]',
             className
           )}
-          placeholder={tags.length === 0 ? placeholder : '添加标签'}
+          placeholder={tags.length === 0 ? placeholder : translateTagInput('common.tag.add')}
           onChange={event => {
             setDraft(event.target.value);
             positionPopover(event.currentTarget);
diff --git a/web-next/components/workbench/overlay-dialog.test.tsx b/web-next/components/workbench/overlay-dialog.test.tsx
index e1245bc632..9361d23e41 100644
--- a/web-next/components/workbench/overlay-dialog.test.tsx
+++ b/web-next/components/workbench/overlay-dialog.test.tsx
@@ -39,6 +39,7 @@ describe('overlay dialog', () => {
 
     const overlay = container.querySelector('[data-overlay-dialog="true"]') as HTMLElement | null;
     expect(overlay).not.toBeNull();
+    expect(overlay?.getAttribute('data-overlay-dialog-mask-closable')).toBe('true');
 
     await act(async () => {
       overlay?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
@@ -86,6 +87,37 @@ describe('overlay dialog', () => {
 
     const overlay = container.querySelector('[data-overlay-dialog="true"]') as HTMLElement | null;
     expect(overlay).not.toBeNull();
+    expect(overlay?.getAttribute('data-overlay-dialog-mask-closable')).toBe('false');
+
+    await act(async () => {
+      overlay?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(onClose).not.toHaveBeenCalled();
+  });
+
+  it('allows centered dialogs to explicitly preserve non-click-away modal semantics with route markers', async () => {
+    const onClose = vi.fn();
+
+    await act(async () => {
+      root.render(
+        <OverlayDialog
+          open={true}
+          title="标签管理"
+          onClose={onClose}
+          maskClosable={false}
+          overlayProps={{ 'data-label-dialog-mask-closable': 'false' }}
+        >
+          <input aria-label="标签名称" />
+        </OverlayDialog>
+      );
+      await Promise.resolve();
+    });
+
+    const overlay = container.querySelector('[data-overlay-dialog="true"]') as HTMLElement | null;
+    expect(overlay?.getAttribute('data-overlay-dialog-mask-closable')).toBe('false');
+    expect(overlay?.getAttribute('data-label-dialog-mask-closable')).toBe('false');
 
     await act(async () => {
       overlay?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
diff --git a/web-next/components/workbench/overlay-dialog.tsx b/web-next/components/workbench/overlay-dialog.tsx
index 780e27eeb5..3e5be5d21f 100644
--- a/web-next/components/workbench/overlay-dialog.tsx
+++ b/web-next/components/workbench/overlay-dialog.tsx
@@ -2,20 +2,12 @@
 
 import React from 'react';
 import { X } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 
-export function OverlayDialog({
-  open,
-  title,
-  kicker,
-  footer,
-  onClose,
-  children,
-  className,
-  contentClassName,
-  maxWidthClassName = 'max-w-5xl',
-  placement = 'center'
-}: {
+const DEFAULT_OVERLAY_DIALOG_CLOSE_LABEL = SUPPLEMENTAL_MESSAGES['en-US']?.['common.dialog.close'] ?? 'common.dialog.close';
+
+type OverlayDialogProps = {
   open: boolean;
   title: React.ReactNode;
   kicker?: React.ReactNode;
@@ -26,19 +18,40 @@ export function OverlayDialog({
   contentClassName?: string;
   maxWidthClassName?: string;
   placement?: 'center' | 'right';
-}) {
+  closeLabel?: string;
+  maskClosable?: boolean;
+  overlayProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'children' | 'className' | 'onClick' | 'title'>;
+};
+
+export function OverlayDialog({
+  open,
+  title,
+  kicker,
+  footer,
+  onClose,
+  children,
+  className,
+  contentClassName,
+  maxWidthClassName = 'max-w-5xl',
+  placement = 'center',
+  closeLabel = DEFAULT_OVERLAY_DIALOG_CLOSE_LABEL,
+  maskClosable,
+  overlayProps
+}: OverlayDialogProps) {
   if (!open) {
     return null;
   }
   const isSideDrawer = placement === 'right';
+  const isMaskClosable = maskClosable ?? isSideDrawer;
   const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
-    if (isSideDrawer && event.target === event.currentTarget) {
+    if (isMaskClosable && event.target === event.currentTarget) {
       onClose();
     }
   };
 
   return (
     <div
+      {...overlayProps}
       className={cn(
         'fixed inset-0 z-50 flex bg-[rgba(11,12,14,0.88)]',
         isSideDrawer ? 'items-stretch justify-end p-0' : 'items-center justify-center p-4'
@@ -48,6 +61,7 @@ export function OverlayDialog({
       aria-label={typeof title === 'string' ? title : undefined}
       data-overlay-dialog="true"
       data-overlay-dialog-placement={placement}
+      data-overlay-dialog-mask-closable={isMaskClosable ? 'true' : 'false'}
       onClick={handleOverlayClick}
     >
       <div
@@ -67,7 +81,7 @@ export function OverlayDialog({
           <button
             type="button"
             onClick={onClose}
-            aria-label="Close dialog"
+            aria-label={closeLabel}
             className="inline-flex h-8 w-8 items-center justify-center rounded-[2px] border border-[var(--ops-border-color)] bg-transparent text-[var(--ops-text-secondary)] transition hover:border-[var(--ops-primary)] hover:bg-[var(--ops-surface-raised)] hover:text-[var(--ops-text-primary)]"
           >
             <X size={16} />
diff --git a/web-next/components/workbench/workspace-tab-strip.test.tsx b/web-next/components/workbench/workspace-tab-strip.test.tsx
index 74b6d9f4c4..6748e11664 100644
--- a/web-next/components/workbench/workspace-tab-strip.test.tsx
+++ b/web-next/components/workbench/workspace-tab-strip.test.tsx
@@ -25,6 +25,7 @@ describe('workspace tab strip', () => {
 
     expect(html).toContain('Summary');
     expect(html).toContain('Details');
+    expect(html).toContain('aria-label="Workspace navigation"');
     expect(html).toContain('border-[var(--ops-border-color)]');
     expect(html).toContain('bg-[var(--ops-surface-panel)]');
     expect(html).toContain('bg-[var(--ops-surface-raised)]');
@@ -38,6 +39,7 @@ describe('workspace tab strip', () => {
   it('renders shared default tabs with ops-shell chrome and badge treatment', () => {
     const html = renderToStaticMarkup(
       <WorkspaceTabStrip
+        ariaLabel="Custom workspace sections"
         tabs={[
           { key: 'all', label: 'All', active: true, badge: '4' },
           { key: 'errors', label: 'Errors', onSelect: () => {} }
@@ -48,6 +50,7 @@ describe('workspace tab strip', () => {
     expect(html).toContain('All');
     expect(html).toContain('Errors');
     expect(html).toContain('>4<');
+    expect(html).toContain('aria-label="Custom workspace sections"');
     expect(html).toContain('border-[var(--ops-primary)]');
     expect(html).toContain('bg-[var(--ops-surface-raised)]');
     expect(html).toContain('text-[var(--ops-text-tertiary)]');


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 06/50: feat(web-next): restore monitor detail parity

Posted by zh...@apache.org.
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 457076998a1d19459156a8376f5a4ddd29d1f5b1
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 00:31:21 2026 +0800

    feat(web-next): restore monitor detail parity
---
 .../monitors/[monitorId]/monitor-detail-page.tsx   | 1191 ++++++++++++++++++++
 web-next/app/monitors/[monitorId]/page.test.tsx    |  471 +++++++-
 web-next/app/monitors/[monitorId]/page.tsx         |  741 +-----------
 .../monitor-detail/history-line-chart.test.tsx     |   33 +-
 .../monitor-detail/history-line-chart.tsx          |  155 ++-
 .../monitor-detail/monitor-detail-console.test.tsx |  441 ++++++--
 .../monitor-detail/monitor-detail-console.tsx      |  289 +++--
 .../monitor-detail-sections.test.tsx               |  406 +++++--
 .../monitor-detail/monitor-detail-sections.tsx     |  662 ++++++-----
 .../monitor-history-chart-grid.test.tsx            |  426 ++++++-
 .../monitor-detail/monitor-history-chart-grid.tsx  |  427 +++++--
 .../monitor-detail/monitor-history-panel.test.tsx  |  442 +++++++-
 .../monitor-detail/monitor-history-panel.tsx       |  452 ++++----
 .../monitor-detail/monitor-metric-table.test.tsx   |   27 +-
 .../monitor-detail/monitor-metric-table.tsx        |  103 +-
 .../monitor-panel-primitives.test.tsx              |   16 +
 .../monitor-detail/monitor-panel-primitives.tsx    |   12 +-
 .../monitor-detail/monitor-realtime-panel.test.tsx |  214 +++-
 .../monitor-detail/monitor-realtime-panel.tsx      |  289 ++---
 .../monitor-detail/monitor-summary-card.test.tsx   |   41 +-
 .../monitor-detail/monitor-summary-card.tsx        |   67 +-
 web-next/lib/monitor-detail/controller.test.ts     |  133 ++-
 web-next/lib/monitor-detail/controller.ts          |   81 +-
 .../lib/monitor-detail/detail-route-state.test.ts  |   99 +-
 web-next/lib/monitor-detail/detail-route-state.ts  |   40 +-
 web-next/lib/monitor-detail/history-chart.test.ts  |  158 ++-
 web-next/lib/monitor-detail/history-chart.ts       |   55 +-
 web-next/lib/monitor-detail/view-model.test.ts     |   13 +
 web-next/lib/monitor-detail/view-model.ts          |   13 +-
 29 files changed, 5379 insertions(+), 2118 deletions(-)

diff --git a/web-next/app/monitors/[monitorId]/monitor-detail-page.tsx b/web-next/app/monitors/[monitorId]/monitor-detail-page.tsx
new file mode 100644
index 0000000000..381bc75f3c
--- /dev/null
+++ b/web-next/app/monitors/[monitorId]/monitor-detail-page.tsx
@@ -0,0 +1,1191 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useQueryClient } from '@tanstack/react-query';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { HzButton, HzInlineFeedback, HzLoadingState } from '@hertzbeat/ui';
+import { ClientWorkbench } from '@/components/workbench/client-workbench';
+import { MonitorDetailConsole, type MonitorDetailConsoleTabKey } from '@/components/monitor-detail/monitor-detail-console';
+import { MonitorDetailSections } from '@/components/monitor-detail/monitor-detail-sections';
+import { useI18n } from '@/components/providers/i18n-provider';
+import { api } from '@/lib/monitor-api-facade';
+import { formatTime } from '@/lib/format';
+import { queryKeys } from '@/lib/query-keys';
+import {
+  buildMonitorDetailUrl,
+  buildMonitorFavoriteUrl,
+  buildMonitorGrafanaUrl,
+  addMonitorFavoriteFromFacade,
+  deleteMonitorGrafanaDashboardFromFacade,
+  loadMonitorDetailBundleFromFacade,
+  loadMonitorHistoryMetricCatalogFromFacade,
+  loadMonitorHistoryMetricDataFromFacade,
+  loadMonitorRealtimeMetricDataFromFacade,
+  removeMonitorFavoriteFromFacade,
+  type MonitorDetailResponse,
+  type MonitorHistoryMetricCatalogItem
+} from '@/lib/monitor-detail/controller';
+import {
+  buildMonitorFavoriteJumpRows,
+  filterMonitorHistoryMetricCatalog,
+  filterMonitorHistorySeriesRows,
+  filterMonitorMetricCatalog,
+  buildMonitorHistoryMetricRows,
+  buildMonitorHistorySeriesRows,
+  buildMonitorMetricCatalogRows,
+  resolveMonitorMetricTableMode,
+  buildMonitorMetricTableMatrix,
+  normalizeMonitorFavoriteNames
+} from '@/lib/monitor-detail/view-model';
+import {
+  readMonitorDetailNavigationContext,
+  type MonitorDetailRefreshableTab,
+  resolveActiveFavoriteRow,
+  resolveFavoriteSurfaceMode,
+  resolveFavoriteJumpTarget,
+  resolveMonitorDetailRefreshTarget,
+  shouldFallbackFromGrafanaTab
+} from '@/lib/monitor-detail/detail-route-state';
+import { buildMonitorEditHref, buildMonitorListReturnHref } from '@/lib/monitor-manage/navigation';
+import {
+  appendTimeContextParams,
+  parseTimeContextFromParams,
+  resolveAppliedTimeContext,
+  resolveTimeContextRefreshInterval,
+  sanitizeTimeContext,
+  TIME_CONTEXT_REFRESH_INTERVAL_SECONDS,
+  TIME_CONTEXT_QUERY_KEYS,
+  timeContextRefreshIntervalToContext,
+  timeRangeToExpressionRange,
+  timeRangeToTimeWindow,
+  timeWindowToTimeRange,
+  type TimeContext
+} from '@/lib/time-context';
+import type { GrafanaDashboard, Monitor, MonitorDetailMetric, MonitorHistoryData, MonitorRealtimeMetricData, Param } from '@/lib/types';
+
+type MonitorDetailData = {
+  monitor: Monitor;
+  params: Param[];
+  metrics: MonitorDetailMetric[];
+  favoriteMetrics: string[];
+  grafana: GrafanaDashboard;
+};
+
+type MetricCardPayloadState = {
+  payload: MonitorRealtimeMetricData | null;
+  loading: boolean;
+  error: string | null;
+};
+
+const HISTORY_WINDOWS = [
+  { value: '30m', labelKey: 'monitor.detail.history.range.last-30m' },
+  { value: '1h', labelKey: 'monitor.detail.history.range.last-1h' },
+  { value: '6h', labelKey: 'monitor.detail.history.range.last-6h' },
+  { value: '1d', labelKey: 'monitor.detail.history.range.last-1d' },
+  { value: '1W', labelKey: 'monitor.detail.history.range.last-1w' },
+  { value: '4W', labelKey: 'monitor.detail.history.range.last-4w' },
+  { value: '12W', labelKey: 'monitor.detail.history.range.last-12w' }
+] as const;
+
+const HISTORY_MODES = [
+  { value: false, labelKey: 'monitor.detail.history.mode.raw' },
+  { value: true, labelKey: 'monitor.detail.history.mode.aggregated' }
+] as const;
+
+const DEFAULT_HISTORY_TIME_CONTEXT: TimeContext = {
+  timeRange: 'last-1h',
+  from: 'now-1h',
+  to: 'now',
+  live: 'false'
+};
+const MONITOR_DETAIL_DEFAULT_REFRESH_INTERVAL_SECONDS = 90;
+const MONITOR_DETAIL_REALTIME_PAGE_SIZE = 10;
+const MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE = 6;
+const MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE = 6;
+
+function resolveMonitorHistoryWindow(value: string | undefined) {
+  const nextWindow = timeRangeToTimeWindow(value);
+  return HISTORY_WINDOWS.some(option => option.value === nextWindow) ? nextWindow : null;
+}
+
+function resolveMonitorHistoryTimeContextFallback(context: TimeContext, fallback: TimeContext) {
+  const nextFallback = { ...fallback };
+  if (Object.prototype.hasOwnProperty.call(context, 'live') && context.live == null) {
+    delete nextFallback.live;
+  }
+  if (Object.prototype.hasOwnProperty.call(context, 'refresh') && context.refresh == null) {
+    delete nextFallback.refresh;
+  }
+  if (context.refresh) {
+    delete nextFallback.live;
+  }
+  if (context.live === 'false') {
+    delete nextFallback.refresh;
+  }
+  return nextFallback;
+}
+
+function normalizeMonitorHistoryTimeContext(context: TimeContext, fallback: TimeContext = DEFAULT_HISTORY_TIME_CONTEXT) {
+  const sanitized = sanitizeTimeContext(context);
+  const fallbackContext = resolveMonitorHistoryTimeContextFallback(context, fallback);
+  const relativeRange = timeRangeToExpressionRange(sanitized.timeRange);
+  const expressionContext = (sanitized.from || relativeRange?.from) && !sanitized.start && !sanitized.end
+    ? { ...sanitized, from: sanitized.from || relativeRange?.from, to: sanitized.to || relativeRange?.to || 'now' }
+    : sanitized;
+  return resolveAppliedTimeContext(expressionContext, fallbackContext, 'last-1h');
+}
+
+function resolveMonitorDetailAppContext(monitor: Monitor, navigationContext: { app?: string | null }) {
+  const scrapeApp = monitor.scrape && monitor.scrape !== 'static' ? monitor.scrape : null;
+  return scrapeApp || navigationContext.app || monitor.app || null;
+}
+
+function buildMonitorHistoryTimeRoute(monitorId: string | number, searchParams: { toString(): string }, context: TimeContext) {
+  const next = new URLSearchParams(searchParams.toString());
+  TIME_CONTEXT_QUERY_KEYS.forEach(key => next.delete(key));
+  appendTimeContextParams(next, context);
+  const query = next.toString();
+  return query ? `/monitors/${encodeURIComponent(String(monitorId))}?${query}` : `/monitors/${encodeURIComponent(String(monitorId))}`;
+}
+
+function MonitorDetailWorkbench({ data }: { data: MonitorDetailData }) {
+  const { t } = useI18n();
+  const router = useRouter();
+  const searchParams = useSearchParams();
+  const queryClient = useQueryClient();
+  const searchParamString = searchParams.toString();
+  const { monitor, params, metrics, favoriteMetrics, grafana } = data;
+  const navigationContext = readMonitorDetailNavigationContext(searchParams);
+  const rawRouteTimeContext = useMemo(
+    () => parseTimeContextFromParams(new URLSearchParams(searchParamString)),
+    [searchParamString]
+  );
+  const routeHistoryTimeContext = useMemo(
+    () => normalizeMonitorHistoryTimeContext(rawRouteTimeContext),
+    [rawRouteTimeContext]
+  );
+  const [grafanaState, setGrafanaState] = useState<GrafanaDashboard>(grafana ?? { enabled: false });
+  const [grafanaMessage, setGrafanaMessage] = useState<string | null>(null);
+  const [grafanaError, setGrafanaError] = useState<string | null>(null);
+  const [selectedMetricKey, setSelectedMetricKey] = useState<string | null>(null);
+  const [favoriteNames, setFavoriteNames] = useState<string[]>(() => normalizeMonitorFavoriteNames(favoriteMetrics));
+  const [favoriteMessage, setFavoriteMessage] = useState<string | null>(null);
+  const [favoriteError, setFavoriteError] = useState<string | null>(null);
+  const [historyMetrics, setHistoryMetrics] = useState<MonitorHistoryMetricCatalogItem[]>([]);
+  const [metricSearch, setMetricSearch] = useState('');
+  const [historyMetricSearch, setHistoryMetricSearch] = useState('');
+  const [historySeriesSearch, setHistorySeriesSearch] = useState('');
+  const [selectedHistoryMetricKey, setSelectedHistoryMetricKey] = useState<string | null>(null);
+  const [historyLoading, setHistoryLoading] = useState(false);
+  const [historyError, setHistoryError] = useState<string | null>(null);
+  const [historyPayload, setHistoryPayload] = useState<MonitorHistoryData | null>(null);
+  const [historyChartPayloads, setHistoryChartPayloads] = useState<Record<string, MonitorHistoryData | null>>({});
+  const [historyChartLoadingKeys, setHistoryChartLoadingKeys] = useState<string[]>([]);
+  const [historyChartErrors, setHistoryChartErrors] = useState<Record<string, string>>({});
+  const [historyPayloadLoading, setHistoryPayloadLoading] = useState(false);
+  const [historyPayloadError, setHistoryPayloadError] = useState<string | null>(null);
+  const [selectedHistorySeriesKey, setSelectedHistorySeriesKey] = useState<string | null>(null);
+  const [selectedHistoryPointIndex, setSelectedHistoryPointIndex] = useState<number | null>(null);
+  const [historyWindow, setHistoryWindow] = useState<(typeof HISTORY_WINDOWS)[number]['value']>(
+    (resolveMonitorHistoryWindow(routeHistoryTimeContext.timeRange) as (typeof HISTORY_WINDOWS)[number]['value'] | null) ?? '1h'
+  );
+  const [historyTimeContext, setHistoryTimeContext] = useState<TimeContext>(routeHistoryTimeContext);
+  const [historyInterval, setHistoryInterval] = useState<(typeof HISTORY_MODES)[number]['value']>(false);
+  const [historyCatalogReloadToken, setHistoryCatalogReloadToken] = useState(0);
+  const [historyReloadToken, setHistoryReloadToken] = useState(0);
+  const [historyFullscreen, setHistoryFullscreen] = useState(false);
+  const [metricPayload, setMetricPayload] = useState<unknown>(null);
+  const [metricPayloadLoading, setMetricPayloadLoading] = useState(false);
+  const [metricPayloadError, setMetricPayloadError] = useState<string | null>(null);
+  const [metricCardPayloads, setMetricCardPayloads] = useState<Record<string, MetricCardPayloadState>>({});
+  const [metricReloadToken, setMetricReloadToken] = useState(0);
+  const [selectedMetricRowKey, setSelectedMetricRowKey] = useState<string | null>(null);
+  const [metricTableMode, setMetricTableMode] = useState<'table' | 'detail'>('table');
+  const [metricFullscreen, setMetricFullscreen] = useState(false);
+  const [currentTab, setCurrentTab] = useState<MonitorDetailConsoleTabKey>('realtime');
+  const [lastRefreshableTab, setLastRefreshableTab] = useState<MonitorDetailRefreshableTab>('realtime');
+  const [favoriteSurfaceMode, setFavoriteSurfaceMode] = useState<'realtime' | 'history'>('realtime');
+  const [visibleRealtimeMetricCount, setVisibleRealtimeMetricCount] = useState(MONITOR_DETAIL_REALTIME_PAGE_SIZE);
+  const [visibleFavoriteRealtimeMetricCount, setVisibleFavoriteRealtimeMetricCount] = useState(MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE);
+  const [visibleHistoryChartCount, setVisibleHistoryChartCount] = useState(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+  const [visibleFavoriteHistoryChartCount, setVisibleFavoriteHistoryChartCount] = useState(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+  const routeRefreshInterval = resolveTimeContextRefreshInterval(
+    rawRouteTimeContext,
+    TIME_CONTEXT_REFRESH_INTERVAL_SECONDS,
+    MONITOR_DETAIL_DEFAULT_REFRESH_INTERVAL_SECONDS
+  );
+  const [refreshInterval, setRefreshInterval] = useState<number>(() => routeRefreshInterval);
+  const [refreshCountdown, setRefreshCountdown] = useState<number>(() => (routeRefreshInterval > 0 ? routeRefreshInterval : -1));
+  const [favoriteReloadToken, setFavoriteReloadToken] = useState(0);
+  const [grafanaReloadToken, setGrafanaReloadToken] = useState(0);
+  const shouldLoadRealtimeMetrics = currentTab === 'realtime' || (currentTab === 'favorites' && favoriteSurfaceMode === 'realtime');
+  const shouldLoadHistoryCatalog = currentTab === 'history' || (currentTab === 'favorites' && favoriteSurfaceMode === 'history');
+  const shouldLoadHistoryPayloads = currentTab === 'history' || (currentTab === 'favorites' && favoriteSurfaceMode === 'history');
+
+  const invalidateMonitorDetailQuery = useCallback(
+    () => queryClient.invalidateQueries({ queryKey: queryKeys.monitors.detail(monitor.id) }),
+    [monitor.id, queryClient]
+  );
+
+  const filteredMetrics = useMemo(() => filterMonitorMetricCatalog(metrics, metricSearch), [metricSearch, metrics]);
+  const displayedMetrics = filteredMetrics.slice(0, visibleRealtimeMetricCount);
+  const displayedMetricNames = displayedMetrics.map(metric => metric.name).filter(Boolean);
+  const displayedMetricNameKey = displayedMetricNames.join('\u0000');
+  const metricRows = buildMonitorMetricCatalogRows(displayedMetrics, t);
+  const realtimeMetricHasMore = visibleRealtimeMetricCount < filteredMetrics.length;
+  const selectedMetric = filteredMetrics.find(metric => metric.name === selectedMetricKey) ?? filteredMetrics[0] ?? null;
+  const filteredHistoryMetrics = useMemo(
+    () => filterMonitorHistoryMetricCatalog(historyMetrics, historyMetricSearch),
+    [historyMetricSearch, historyMetrics]
+  );
+  const historyRows = buildMonitorHistoryMetricRows(filteredHistoryMetrics, t);
+  const displayedHistoryChartItems = useMemo(
+    () => filteredHistoryMetrics.slice(0, visibleHistoryChartCount),
+    [filteredHistoryMetrics, visibleHistoryChartCount]
+  );
+  const historyChartHasMore = visibleHistoryChartCount < filteredHistoryMetrics.length;
+  const selectedHistoryMetric = filteredHistoryMetrics.find(item => `${item.metrics}:${item.metric}` === selectedHistoryMetricKey) ?? filteredHistoryMetrics[0] ?? null;
+  const historySeriesRows = filterMonitorHistorySeriesRows(buildMonitorHistorySeriesRows(historyPayload, t, formatTime), historySeriesSearch);
+  const favoriteJumpRows = useMemo(() => buildMonitorFavoriteJumpRows(favoriteNames, metrics, historyMetrics, t), [favoriteNames, historyMetrics, metrics, t]);
+  const favoriteRealtimeRows = useMemo(() => favoriteJumpRows.filter(row => row.targetKind === 'realtime'), [favoriteJumpRows]);
+  const favoriteRealtimeMetricHasMore = visibleFavoriteRealtimeMetricCount < favoriteRealtimeRows.length;
+  const historyMetricByKey = useMemo(() => new Map(historyMetrics.map(item => [`${item.metrics}:${item.metric}`, item])), [historyMetrics]);
+  const favoriteHistoryCatalogItems = useMemo(
+    () =>
+      favoriteJumpRows
+        .filter(row => row.targetKind === 'history')
+        .map(row => historyMetricByKey.get(row.targetKey))
+        .filter((item): item is MonitorHistoryMetricCatalogItem => Boolean(item)),
+    [favoriteJumpRows, historyMetricByKey]
+  );
+  const favoriteHistoryCatalogItemKey = favoriteHistoryCatalogItems.map(item => `${item.metrics}:${item.metric}`).join('\u0000');
+  const favoriteHistoryChartItems = useMemo(
+    () => favoriteHistoryCatalogItems.slice(0, visibleFavoriteHistoryChartCount),
+    [favoriteHistoryCatalogItems, visibleFavoriteHistoryChartCount]
+  );
+  const favoriteHistoryChartHasMore = visibleFavoriteHistoryChartCount < favoriteHistoryCatalogItems.length;
+  const favoriteHistoryChartItemKey = favoriteHistoryChartItems.map(item => `${item.metrics}:${item.metric}`).join('\u0000');
+  const [selectedFavoriteKey, setSelectedFavoriteKey] = useState<string | null>(null);
+
+  const resetHistoryInvestigationSelection = useCallback((items: MonitorHistoryMetricCatalogItem[] = filteredHistoryMetrics) => {
+    const first = items[0];
+    setSelectedHistoryMetricKey(first ? `${first.metrics}:${first.metric}` : null);
+    setSelectedHistorySeriesKey(null);
+    setSelectedHistoryPointIndex(null);
+    setHistoryPayload(null);
+    setHistoryPayloadError(null);
+  }, [filteredHistoryMetrics]);
+
+  const readMonitorHistoryMetricPanelData = useCallback(
+    (item: MonitorHistoryMetricCatalogItem) =>
+      queryClient.fetchQuery({
+        queryKey: queryKeys.monitors.history(monitor.id, `${item.metrics}.${item.metric}`, {
+          monitorId: monitor.id,
+          metric: `${item.metrics}.${item.metric}`,
+          history: historyWindow,
+          interval: historyInterval,
+          start: historyTimeContext.start,
+          end: historyTimeContext.end
+        }),
+        queryFn: () =>
+          loadMonitorHistoryMetricDataFromFacade(api.monitors.historyMetric, monitor, item, {
+            history: historyWindow,
+            interval: historyInterval,
+            start: historyTimeContext.start,
+            end: historyTimeContext.end
+          }),
+        staleTime: 5000
+      }),
+    [historyInterval, historyTimeContext.end, historyTimeContext.start, historyWindow, monitor, queryClient]
+  );
+
+  function resetActiveHistoryInvestigationSelection() {
+    resetHistoryInvestigationSelection(currentTab === 'favorites' && favoriteSurfaceMode === 'history' ? favoriteHistoryCatalogItems : filteredHistoryMetrics);
+  }
+
+  useEffect(() => {
+    const nextWindow = resolveMonitorHistoryWindow(routeHistoryTimeContext.timeRange);
+    if (nextWindow) {
+      setHistoryWindow(nextWindow as (typeof HISTORY_WINDOWS)[number]['value']);
+    }
+    setHistoryTimeContext(routeHistoryTimeContext);
+  }, [routeHistoryTimeContext]);
+
+  useEffect(() => {
+    setRefreshInterval(routeRefreshInterval);
+  }, [routeRefreshInterval]);
+
+  useEffect(() => {
+    setVisibleRealtimeMetricCount(MONITOR_DETAIL_REALTIME_PAGE_SIZE);
+  }, [metricSearch, metrics]);
+
+  useEffect(() => {
+    setVisibleHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+  }, [historyMetricSearch, historyMetrics]);
+
+  useEffect(() => {
+    setVisibleFavoriteHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+  }, [favoriteNames, historyMetrics]);
+
+  useEffect(() => {
+    setVisibleFavoriteRealtimeMetricCount(MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE);
+  }, [favoriteNames, metrics]);
+
+  useEffect(() => {
+    if (currentTab !== 'favorites' || favoriteSurfaceMode !== 'history') return;
+    const first = favoriteHistoryCatalogItems[0];
+    setSelectedHistoryMetricKey(first ? `${first.metrics}:${first.metric}` : null);
+    setSelectedHistorySeriesKey(null);
+    setSelectedHistoryPointIndex(null);
+    setHistoryPayload(null);
+    setHistoryPayloadError(null);
+  }, [currentTab, favoriteHistoryCatalogItemKey, favoriteHistoryCatalogItems, favoriteSurfaceMode]);
+
+  function resolveHistoryFavoriteToken(item: MonitorHistoryMetricCatalogItem) {
+    const fullPath = `${item.metrics}.${item.metric}`;
+    return favoriteNames.find(name => name === fullPath || name === item.metrics || name === item.metric) ?? null;
+  }
+
+  useEffect(() => {
+    setGrafanaState(grafana ?? { enabled: false });
+  }, [grafana]);
+
+  useEffect(() => {
+    setFavoriteNames(normalizeMonitorFavoriteNames(favoriteMetrics));
+  }, [favoriteMetrics]);
+
+  useEffect(() => {
+    let cancelled = false;
+    api.monitors.favoriteMetrics(monitor.id)
+      .then(result => {
+        if (!cancelled) {
+          setFavoriteNames(normalizeMonitorFavoriteNames(result || []));
+        }
+      })
+      .catch(() => {
+        // Keep the current optimistic/local favorites state if refresh fails.
+      });
+
+    return () => {
+      cancelled = true;
+    };
+  }, [favoriteMetrics, favoriteReloadToken, monitor.id]);
+
+  useEffect(() => {
+    let cancelled = false;
+    api.monitors.grafanaDashboard<GrafanaDashboard | null>(monitor.id)
+      .then(result => {
+        if (!cancelled) {
+          setGrafanaState(result ?? { enabled: false });
+        }
+      })
+      .catch(() => {
+        if (!cancelled) {
+          setGrafanaState({ enabled: false });
+        }
+      });
+
+    return () => {
+      cancelled = true;
+    };
+  }, [grafana, grafanaReloadToken, monitor.id]);
+
+  useEffect(() => {
+    if (shouldFallbackFromGrafanaTab(grafanaState.enabled, currentTab)) {
+      setCurrentTab(lastRefreshableTab);
+    }
+  }, [currentTab, grafanaState.enabled, lastRefreshableTab]);
+
+  useEffect(() => {
+    setSelectedMetricKey(prev => (prev && filteredMetrics.some(metric => metric.name === prev) ? prev : filteredMetrics[0]?.name || null));
+  }, [filteredMetrics, metricSearch, metrics]);
+
+  useEffect(() => {
+    const metricNames = displayedMetricNameKey ? displayedMetricNameKey.split('\u0000').filter(Boolean) : [];
+    if (!shouldLoadRealtimeMetrics) {
+      setMetricCardPayloads({});
+      return;
+    }
+    if (metricNames.length === 0) {
+      setMetricCardPayloads({});
+      return;
+    }
+
+    let cancelled = false;
+    setMetricCardPayloads(previous => {
+      const next: Record<string, MetricCardPayloadState> = {};
+      metricNames.forEach(name => {
+        next[name] = {
+          payload: previous[name]?.payload ?? null,
+          loading: true,
+          error: null
+        };
+      });
+      return next;
+    });
+
+    Promise.all(
+      metricNames.map(name =>
+        loadMonitorRealtimeMetricDataFromFacade(api.monitors.realtimeMetric, monitor.id, name)
+          .then(payload => ({ name, payload: payload as MonitorRealtimeMetricData | null, error: null }))
+          .catch(error => ({
+            name,
+            payload: null,
+            error: error instanceof Error ? error.message : t('common.load-failed')
+          }))
+      )
+    ).then(results => {
+      if (cancelled) return;
+      setMetricCardPayloads(() => {
+        const next: Record<string, MetricCardPayloadState> = {};
+        results.forEach(result => {
+          next[result.name] = {
+            payload: result.payload,
+            loading: false,
+            error: result.error
+          };
+        });
+        return next;
+      });
+    });
+
+    return () => {
+      cancelled = true;
+    };
+  }, [displayedMetricNameKey, metricReloadToken, monitor.id, shouldLoadRealtimeMetrics, t]);
+
+  useEffect(() => {
+    if (!shouldLoadHistoryCatalog) return;
+
+    let cancelled = false;
+    setHistoryLoading(true);
+    setHistoryError(null);
+    loadMonitorHistoryMetricCatalogFromFacade(api.monitors.warehouseStorageStatus, api.monitors.historyMetricCatalogDefine, monitor)
+      .then(rows => {
+        if (!cancelled) {
+          setHistoryMetrics(rows);
+          setHistoryError(null);
+        }
+      })
+      .catch(error => {
+        if (!cancelled) {
+          setHistoryMetrics([]);
+          setHistoryError(error instanceof Error ? error.message : t('common.load-failed'));
+        }
+      })
+      .finally(() => {
+        if (!cancelled) {
+          setHistoryLoading(false);
+        }
+      });
+    return () => {
+      cancelled = true;
+    };
+  }, [historyCatalogReloadToken, monitor, shouldLoadHistoryCatalog, t]);
+
+  useEffect(() => {
+    setSelectedHistoryMetricKey(prev =>
+      prev && filteredHistoryMetrics.some(item => `${item.metrics}:${item.metric}` === prev)
+        ? prev
+        : filteredHistoryMetrics[0]
+          ? `${filteredHistoryMetrics[0].metrics}:${filteredHistoryMetrics[0].metric}`
+          : null
+    );
+  }, [filteredHistoryMetrics, historyMetricSearch, historyMetrics]);
+
+  useEffect(() => {
+    const seriesKeys = historySeriesRows.map(row => row.key);
+    setSelectedHistorySeriesKey(prev => (prev && seriesKeys.includes(prev) ? prev : seriesKeys[0] || null));
+  }, [historyPayload, historySeriesRows, historySeriesSearch]);
+
+  useEffect(() => {
+    if (!selectedHistorySeriesKey || !historyPayload?.values?.[selectedHistorySeriesKey]?.length) {
+      setSelectedHistoryPointIndex(null);
+      return;
+    }
+
+    const values = historyPayload.values[selectedHistorySeriesKey];
+    setSelectedHistoryPointIndex(prev => (prev != null && values[prev] ? prev : values.length - 1));
+  }, [historyPayload, selectedHistorySeriesKey]);
+
+  useEffect(() => {
+    setSelectedFavoriteKey(prev => resolveActiveFavoriteRow(favoriteJumpRows, prev, favoriteSurfaceMode)?.key ?? null);
+  }, [favoriteJumpRows, favoriteSurfaceMode]);
+
+  useEffect(() => {
+    setFavoriteSurfaceMode(current => resolveFavoriteSurfaceMode(current, favoriteJumpRows, selectedFavoriteKey));
+  }, [favoriteJumpRows, selectedFavoriteKey]);
+
+  useEffect(() => {
+    if (!selectedFavoriteKey) return;
+    const row = resolveActiveFavoriteRow(favoriteJumpRows, selectedFavoriteKey, favoriteSurfaceMode);
+    if (!row) return;
+    const target = resolveFavoriteJumpTarget(row);
+
+    setFavoriteSurfaceMode(row.targetKind);
+
+    if (row.targetKind === 'realtime') {
+      setSelectedMetricKey(current => (current === target.targetKey ? current : target.targetKey));
+      return;
+    }
+
+    setSelectedHistoryMetricKey(current => (current === target.targetKey ? current : target.targetKey));
+  }, [favoriteJumpRows, favoriteSurfaceMode, selectedFavoriteKey]);
+
+  useEffect(() => {
+    if (!shouldLoadHistoryPayloads || !selectedHistoryMetric) {
+      setHistoryPayload(null);
+      setHistoryPayloadError(null);
+      return;
+    }
+    let cancelled = false;
+    setHistoryPayloadLoading(true);
+    setHistoryPayloadError(null);
+    readMonitorHistoryMetricPanelData(selectedHistoryMetric)
+      .then(result => {
+        if (!cancelled) {
+          setHistoryPayload(result);
+          setHistoryPayloadError(null);
+        }
+      })
+      .catch(error => {
+        if (!cancelled) {
+          setHistoryPayload(null);
+          setHistoryPayloadError(error instanceof Error ? error.message : t('common.load-failed'));
+        }
+      })
+      .finally(() => {
+        if (!cancelled) {
+          setHistoryPayloadLoading(false);
+        }
+      });
+    return () => {
+      cancelled = true;
+    };
+  }, [
+    historyInterval,
+    historyReloadToken,
+    historyTimeContext.end,
+    historyTimeContext.from,
+    historyTimeContext.start,
+    historyTimeContext.timezone,
+    historyTimeContext.to,
+    historyWindow,
+    monitor,
+    readMonitorHistoryMetricPanelData,
+    selectedHistoryMetric,
+    shouldLoadHistoryPayloads,
+    t
+  ]);
+
+  useEffect(() => {
+    const shouldLoadChartGrid = currentTab === 'history' || (currentTab === 'favorites' && favoriteSurfaceMode === 'history');
+    if (!shouldLoadChartGrid) return;
+
+    const chartItems =
+      currentTab === 'favorites' && favoriteSurfaceMode === 'history'
+        ? favoriteHistoryChartItems
+        : displayedHistoryChartItems;
+    if (chartItems.length === 0) {
+      setHistoryChartPayloads({});
+      setHistoryChartLoadingKeys([]);
+      setHistoryChartErrors({});
+      return;
+    }
+
+    let cancelled = false;
+    const chartKeys = chartItems.map(item => `${item.metrics}:${item.metric}`);
+    setHistoryChartLoadingKeys(chartKeys);
+    setHistoryChartErrors({});
+
+    Promise.all(
+      chartItems.map(item =>
+        readMonitorHistoryMetricPanelData(item)
+          .then(payload => ({ key: `${item.metrics}:${item.metric}`, payload }))
+          .catch(error => ({
+            key: `${item.metrics}:${item.metric}`,
+            error: error instanceof Error ? error.message : t('common.load-failed')
+          }))
+      )
+    ).then(results => {
+      if (cancelled) return;
+
+      const nextPayloads: Record<string, MonitorHistoryData | null> = {};
+      const nextErrors: Record<string, string> = {};
+      results.forEach(result => {
+        if ('payload' in result) {
+          nextPayloads[result.key] = result.payload;
+          return;
+        }
+        nextPayloads[result.key] = null;
+        nextErrors[result.key] = result.error;
+      });
+      setHistoryChartPayloads(nextPayloads);
+      setHistoryChartErrors(nextErrors);
+      setHistoryChartLoadingKeys([]);
+    });
+
+    return () => {
+      cancelled = true;
+    };
+  }, [
+    currentTab,
+    favoriteHistoryChartItems,
+    favoriteHistoryChartItemKey,
+    favoriteSurfaceMode,
+    displayedHistoryChartItems,
+    historyInterval,
+    historyMetricSearch,
+    historyMetrics,
+    historyReloadToken,
+    historyTimeContext.end,
+    historyTimeContext.from,
+    historyTimeContext.start,
+    historyTimeContext.timezone,
+    historyTimeContext.to,
+    historyWindow,
+    monitor,
+    readMonitorHistoryMetricPanelData,
+    t
+  ]);
+
+  useEffect(() => {
+    if (!shouldLoadRealtimeMetrics || !selectedMetric?.name) {
+      setMetricPayload(null);
+      setMetricPayloadError(null);
+      return;
+    }
+    let cancelled = false;
+    setMetricPayloadLoading(true);
+    setMetricPayloadError(null);
+    loadMonitorRealtimeMetricDataFromFacade(api.monitors.realtimeMetric, monitor.id, selectedMetric.name)
+      .then(result => {
+        if (!cancelled) {
+          setMetricPayload(result);
+          setMetricPayloadError(null);
+        }
+      })
+      .catch(error => {
+        if (!cancelled) {
+          setMetricPayload(null);
+          setMetricPayloadError(error instanceof Error ? error.message : t('common.load-failed'));
+        }
+      })
+      .finally(() => {
+        if (!cancelled) {
+          setMetricPayloadLoading(false);
+        }
+      });
+    return () => {
+      cancelled = true;
+    };
+  }, [metricReloadToken, monitor.id, selectedMetric?.name, shouldLoadRealtimeMetrics, t]);
+
+  useEffect(() => {
+    const tableRows = buildMonitorMetricTableMatrix(metricPayload as any, t).rows;
+    setSelectedMetricRowKey(prev => (prev && tableRows.some(row => row.key === prev) ? prev : tableRows[0]?.key || null));
+  }, [metricPayload, t]);
+
+  useEffect(() => {
+    setMetricTableMode(prev => resolveMonitorMetricTableMode(metricPayload as any, prev));
+  }, [metricPayload]);
+
+  const resetRealtimeInvestigationSelection = useCallback(() => {
+    setMetricPayload(null);
+    setMetricPayloadError(null);
+    setSelectedMetricRowKey(null);
+    setMetricTableMode('table');
+  }, []);
+
+  const handleRefresh = useCallback(() => {
+    const refreshTarget = resolveMonitorDetailRefreshTarget(currentTab, lastRefreshableTab);
+    if (refreshInterval > 0) {
+      setRefreshCountdown(refreshInterval);
+    }
+    if (refreshTarget === 'history') {
+      setVisibleHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+      setHistoryReloadToken(prev => prev + 1);
+      return;
+    }
+    if (refreshTarget === 'favorites') {
+      setFavoriteReloadToken(prev => prev + 1);
+      if (favoriteSurfaceMode === 'history') {
+        resetHistoryInvestigationSelection(favoriteHistoryCatalogItems);
+        setVisibleFavoriteHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+        setHistoryReloadToken(prev => prev + 1);
+      } else {
+        setVisibleFavoriteRealtimeMetricCount(MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE);
+        resetRealtimeInvestigationSelection();
+        setMetricReloadToken(prev => prev + 1);
+      }
+      return;
+    }
+    if (refreshTarget === 'grafana') {
+      setGrafanaReloadToken(prev => prev + 1);
+      return;
+    }
+    setVisibleRealtimeMetricCount(MONITOR_DETAIL_REALTIME_PAGE_SIZE);
+    resetRealtimeInvestigationSelection();
+    setMetricReloadToken(prev => prev + 1);
+  }, [
+    currentTab,
+    favoriteHistoryCatalogItems,
+    favoriteSurfaceMode,
+    lastRefreshableTab,
+    refreshInterval,
+    resetHistoryInvestigationSelection,
+    resetRealtimeInvestigationSelection
+  ]);
+
+  useEffect(() => {
+    if (refreshInterval <= 0) return;
+
+    const timer = window.setInterval(() => {
+      handleRefresh();
+    }, refreshInterval * 1000);
+
+    return () => {
+      window.clearInterval(timer);
+    };
+  }, [handleRefresh, refreshInterval]);
+
+  useEffect(() => {
+    if (refreshInterval <= 0) {
+      setRefreshCountdown(-1);
+      return;
+    }
+
+    setRefreshCountdown(refreshInterval);
+    const timer = window.setInterval(() => {
+      setRefreshCountdown(current => (current <= 1 ? refreshInterval : current - 1));
+    }, 1000);
+
+    return () => {
+      window.clearInterval(timer);
+    };
+  }, [refreshInterval]);
+
+  async function handleToggleFavorite(metricName: string) {
+    const favorited = favoriteNames.includes(metricName);
+    try {
+      if (favorited) {
+        await removeMonitorFavoriteFromFacade(api.monitors.removeFavoriteMetric, monitor.id, metricName);
+        setFavoriteNames(prev => normalizeMonitorFavoriteNames(prev.filter(item => item !== metricName)));
+      } else {
+        await addMonitorFavoriteFromFacade(api.monitors.addFavoriteMetric, monitor.id, metricName);
+        setFavoriteNames(prev => normalizeMonitorFavoriteNames([...prev, metricName]));
+      }
+      setFavoriteMessage(t(favorited ? 'monitor.favorite.remove.success' : 'monitor.favorite.add.success'));
+      setFavoriteError(null);
+      void invalidateMonitorDetailQuery();
+    } catch (error) {
+      setFavoriteError(error instanceof Error ? error.message : t(favorited ? 'monitor.favorite.remove.failed' : 'monitor.favorite.add.failed'));
+    }
+  }
+
+  async function handleToggleHistoryFavorite(item: MonitorHistoryMetricCatalogItem) {
+    const favoriteToken = resolveHistoryFavoriteToken(item);
+    const nextToken = `${item.metrics}.${item.metric}`;
+    try {
+      if (favoriteToken) {
+        await removeMonitorFavoriteFromFacade(api.monitors.removeFavoriteMetric, monitor.id, favoriteToken);
+        setFavoriteNames(prev => normalizeMonitorFavoriteNames(prev.filter(name => name !== favoriteToken)));
+      } else {
+        await addMonitorFavoriteFromFacade(api.monitors.addFavoriteMetric, monitor.id, nextToken);
+        setFavoriteNames(prev => normalizeMonitorFavoriteNames([...prev, nextToken]));
+      }
+      setFavoriteMessage(t(favoriteToken ? 'monitor.favorite.remove.success' : 'monitor.favorite.add.success'));
+      setFavoriteError(null);
+      void invalidateMonitorDetailQuery();
+    } catch (error) {
+      setFavoriteError(error instanceof Error ? error.message : t(favoriteToken ? 'monitor.favorite.remove.failed' : 'monitor.favorite.add.failed'));
+    }
+  }
+
+  function handlePreviewFavorite(key: string) {
+    setSelectedFavoriteKey(key);
+    const row = favoriteJumpRows.find(item => item.key === key);
+    if (!row) return;
+    setFavoriteSurfaceMode(row.targetKind);
+    if (row.targetKind === 'realtime') {
+      const target = resolveFavoriteJumpTarget(row);
+      resetRealtimeInvestigationSelection();
+      setSelectedMetricKey(target.targetKey);
+    } else {
+      const target = resolveFavoriteJumpTarget(row);
+      setHistoryPayload(null);
+      setHistoryPayloadError(null);
+      setSelectedHistorySeriesKey(null);
+      setSelectedHistoryPointIndex(null);
+      setSelectedHistoryMetricKey(target.targetKey);
+    }
+  }
+
+  function handleSelectFavorite(key: string) {
+    handlePreviewFavorite(key);
+    const row = favoriteJumpRows.find(item => item.key === key);
+    if (!row) return;
+    const nextTab = resolveFavoriteJumpTarget(row).tab;
+    setLastRefreshableTab(nextTab);
+    setCurrentTab(nextTab);
+  }
+
+  function handleSelectDetailTab(tab: MonitorDetailConsoleTabKey) {
+    if (tab !== 'grafana') {
+      setLastRefreshableTab(tab);
+    }
+    if (tab === 'realtime') {
+      setVisibleRealtimeMetricCount(MONITOR_DETAIL_REALTIME_PAGE_SIZE);
+      resetRealtimeInvestigationSelection();
+      setMetricReloadToken(prev => prev + 1);
+    }
+    if (tab === 'history') {
+      setVisibleHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+      resetHistoryInvestigationSelection(filteredHistoryMetrics);
+      setHistoryCatalogReloadToken(prev => prev + 1);
+      setHistoryReloadToken(prev => prev + 1);
+    }
+    if (tab === 'favorites') {
+      setVisibleFavoriteRealtimeMetricCount(MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE);
+      setVisibleFavoriteHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+      setFavoriteReloadToken(prev => prev + 1);
+      if (favoriteSurfaceMode === 'history') {
+        resetHistoryInvestigationSelection(favoriteHistoryCatalogItems);
+        setHistoryReloadToken(prev => prev + 1);
+      } else {
+        setMetricReloadToken(prev => prev + 1);
+      }
+    }
+    setCurrentTab(tab);
+  }
+
+  function handleSetFavoriteSurfaceMode(mode: 'realtime' | 'history') {
+    setFavoriteSurfaceMode(mode);
+    if (mode === 'history') {
+      setVisibleFavoriteHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+    } else {
+      setVisibleFavoriteRealtimeMetricCount(MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE);
+    }
+    const rows = favoriteJumpRows.filter(row => row.targetKind === mode);
+    const nextRow = rows.find(row => row.key === selectedFavoriteKey) ?? rows[0];
+    if (nextRow) {
+      handlePreviewFavorite(nextRow.key);
+    }
+  }
+
+  function replaceMonitorHistoryTimeRoute(context: TimeContext) {
+    const route = buildMonitorHistoryTimeRoute(monitor.id, searchParamString, context);
+    router.replace(route);
+    if (typeof window !== 'undefined') {
+      window.history.replaceState(window.history.state, '', route);
+    }
+  }
+
+  function handleApplyHistoryTimeContext(context: TimeContext) {
+    const applied = normalizeMonitorHistoryTimeContext(context, historyTimeContext);
+    const nextWindow = resolveMonitorHistoryWindow(applied.timeRange);
+    if (nextWindow) {
+      setHistoryWindow(nextWindow as (typeof HISTORY_WINDOWS)[number]['value']);
+    }
+    setHistoryTimeContext(applied);
+    resetActiveHistoryInvestigationSelection();
+    replaceMonitorHistoryTimeRoute(applied);
+  }
+
+  function handleSelectRefreshInterval(value: number) {
+    setRefreshInterval(value);
+    setRefreshCountdown(value > 0 ? value : -1);
+    const nextRefreshContext = timeContextRefreshIntervalToContext(value);
+    const applied = normalizeMonitorHistoryTimeContext({ ...historyTimeContext, ...nextRefreshContext }, DEFAULT_HISTORY_TIME_CONTEXT);
+    setHistoryTimeContext(applied);
+    replaceMonitorHistoryTimeRoute(applied);
+  }
+
+  function handleHistoryWindowChange(value: string) {
+    const nextWindow = value as (typeof HISTORY_WINDOWS)[number]['value'];
+    const nextContext = normalizeMonitorHistoryTimeContext({ timeRange: timeWindowToTimeRange(nextWindow) }, historyTimeContext);
+    setHistoryWindow(nextWindow);
+    setHistoryTimeContext(nextContext);
+    resetActiveHistoryInvestigationSelection();
+    replaceMonitorHistoryTimeRoute(nextContext);
+  }
+
+  function handleHistoryModeChange(value: boolean) {
+    setHistoryInterval(value);
+    resetActiveHistoryInvestigationSelection();
+  }
+
+  function handleSelectHistoryMetricKey(key: string) {
+    setSelectedHistoryMetricKey(key);
+    setSelectedHistorySeriesKey(null);
+    setSelectedHistoryPointIndex(null);
+    setHistoryPayload(null);
+    setHistoryPayloadError(null);
+  }
+
+  function handleSelectMetricKey(key: string) {
+    setSelectedMetricKey(key);
+    resetRealtimeInvestigationSelection();
+  }
+
+  function handleMetricRefresh() {
+    resetRealtimeInvestigationSelection();
+    setMetricReloadToken(prev => prev + 1);
+  }
+
+  function handleHistoryRefresh() {
+    if (refreshInterval > 0) {
+      setRefreshCountdown(refreshInterval);
+    }
+    resetHistoryInvestigationSelection(filteredHistoryMetrics);
+    setVisibleHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+    setHistoryCatalogReloadToken(prev => prev + 1);
+    setHistoryReloadToken(prev => prev + 1);
+  }
+
+  function handleFavoriteHistoryRefresh() {
+    if (refreshInterval > 0) {
+      setRefreshCountdown(refreshInterval);
+    }
+    resetHistoryInvestigationSelection(favoriteHistoryCatalogItems);
+    setVisibleFavoriteHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);
+    setFavoriteReloadToken(prev => prev + 1);
+    setHistoryReloadToken(prev => prev + 1);
+  }
+
+  async function handleRemoveFavoriteToken(token: string) {
+    try {
+      await removeMonitorFavoriteFromFacade(api.monitors.removeFavoriteMetric, monitor.id, token);
+      setFavoriteNames(prev => normalizeMonitorFavoriteNames(prev.filter(name => name !== token)));
+      setFavoriteMessage(t('monitor.favorite.remove.success'));
+      setFavoriteError(null);
+      void invalidateMonitorDetailQuery();
+    } catch (error) {
+      setFavoriteError(error instanceof Error ? error.message : t('monitor.favorite.remove.failed'));
+    }
+  }
+
+  async function handleDeleteGrafanaDashboard() {
+    try {
+      await deleteMonitorGrafanaDashboardFromFacade(api.monitors.deleteGrafanaDashboard, monitor.id);
+      setGrafanaState({ enabled: false });
+      setGrafanaMessage(t('common.delete-success'));
+      setGrafanaError(null);
+      void invalidateMonitorDetailQuery();
+    } catch (error) {
+      setGrafanaError(error instanceof Error ? error.message : t('common.delete-failed'));
+    }
+  }
+
+  const appDefinitionKey = resolveMonitorDetailAppContext(monitor, navigationContext);
+  const detailNavigationContext = appDefinitionKey ? { ...navigationContext, app: appDefinitionKey } : navigationContext;
+  const backHref = buildMonitorListReturnHref({ ...navigationContext, app: appDefinitionKey });
+  const editHref = buildMonitorEditHref(monitor.id, navigationContext);
+  const helpHref = appDefinitionKey ? `https://hertzbeat.apache.org/docs/help/${encodeURIComponent(appDefinitionKey)}` : null;
+  const sections = MonitorDetailSections({
+    monitor,
+    editHref,
+    params,
+    selectedMetric,
+    metricSearch,
+    metricRows,
+    realtimeMetricTotalCount: filteredMetrics.length,
+    realtimeMetricHasMore,
+    metricCardPayloads,
+    metrics,
+    favoriteNames,
+    favoriteJumpRows,
+    selectedFavoriteKey,
+    favoriteSurfaceMode,
+    favoriteRealtimeMetricVisibleCount: visibleFavoriteRealtimeMetricCount,
+    favoriteRealtimeMetricTotalCount: favoriteRealtimeRows.length,
+    favoriteRealtimeMetricHasMore,
+    selectedMetricKey,
+    metricPayload,
+    metricPayloadLoading,
+    metricPayloadError,
+    selectedMetricRowKey,
+    metricTableMode,
+    metricFullscreen,
+    historyLoading,
+    historyError,
+    historyMetrics,
+    historyMetricSearch,
+    historyRows,
+    historyChartVisibleCount: visibleHistoryChartCount,
+    historyChartTotalCount: filteredHistoryMetrics.length,
+    historyChartHasMore,
+    selectedHistoryMetricKey,
+    historySeriesSearch,
+    historySeriesRows,
+    selectedHistorySeriesKey,
+    selectedHistoryPointIndex,
+    historyPayload,
+    historyChartPayloads,
+    historyChartLoadingKeys,
+    historyChartErrors,
+    historyPayloadLoading,
+    historyPayloadError,
+    historyInterval,
+    historyWindow,
+    historyTimeContext,
+    historyWindows: HISTORY_WINDOWS.map(option => ({ value: option.value, label: t(option.labelKey) })),
+    historyModes: HISTORY_MODES.map(option => ({ value: option.value, label: t(option.labelKey) })),
+    historyFullscreen,
+    favoriteHistoryChartVisibleCount: visibleFavoriteHistoryChartCount,
+    favoriteHistoryChartTotalCount: favoriteHistoryCatalogItems.length,
+    favoriteHistoryChartHasMore,
+    grafanaState,
+    grafanaMessage,
+    grafanaError,
+    favoriteMessage,
+    favoriteError,
+    onMetricSearchChange: setMetricSearch,
+    onSelectMetric: handleSelectMetricKey,
+    onToggleFavorite: handleToggleFavorite,
+    onLoadMoreRealtimeMetrics: () =>
+      setVisibleRealtimeMetricCount(prev => Math.min(prev + MONITOR_DETAIL_REALTIME_PAGE_SIZE, filteredMetrics.length)),
+    onSelectMetricRow: setSelectedMetricRowKey,
+    onMetricModeChange: setMetricTableMode,
+    onMetricRefresh: handleMetricRefresh,
+    onMetricFullscreenToggle: () => setMetricFullscreen(prev => !prev),
+    onHistoryMetricSearchChange: setHistoryMetricSearch,
+    onSelectHistoryMetric: handleSelectHistoryMetricKey,
+    onToggleHistoryFavorite: handleToggleHistoryFavorite,
+    onHistorySeriesSearchChange: setHistorySeriesSearch,
+    onSelectHistorySeries: setSelectedHistorySeriesKey,
+    onSelectHistoryPoint: setSelectedHistoryPointIndex,
+    onHistoryRefresh: handleHistoryRefresh,
+    onFavoriteHistoryRefresh: handleFavoriteHistoryRefresh,
+    onLoadMoreHistoryCharts: () =>
+      setVisibleHistoryChartCount(prev => Math.min(prev + MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE, filteredHistoryMetrics.length)),
+    onHistoryWindowChange: handleHistoryWindowChange,
+    onHistoryTimeContextApply: handleApplyHistoryTimeContext,
+    onApplyHistoryChartZoomTimeRange: handleApplyHistoryTimeContext,
+    onHistoryModeChange: handleHistoryModeChange,
+    onHistoryFullscreenToggle: () => setHistoryFullscreen(prev => !prev),
+    onSelectFavorite: handleSelectFavorite,
+    onSetFavoriteSurfaceMode: handleSetFavoriteSurfaceMode,
+    onLoadMoreFavoriteRealtimeMetrics: () =>
+      setVisibleFavoriteRealtimeMetricCount(prev =>
+        Math.min(prev + MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE, favoriteRealtimeRows.length)
+      ),
+    onLoadMoreFavoriteHistoryCharts: () =>
+      setVisibleFavoriteHistoryChartCount(prev =>
+        Math.min(prev + MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE, favoriteHistoryCatalogItems.length)
+      ),
+    onRemoveFavoriteToken: handleRemoveFavoriteToken,
+    onDeleteGrafanaDashboard: handleDeleteGrafanaDashboard,
+    t
+  });
+
+  return (
+    <MonitorDetailConsole
+      monitor={monitor}
+      currentTab={currentTab}
+      refreshTarget={resolveMonitorDetailRefreshTarget(currentTab, lastRefreshableTab)}
+      grafanaRefreshFallbackTab={lastRefreshableTab}
+      refreshInterval={refreshInterval}
+      refreshCountdown={refreshCountdown}
+      backHref={backHref}
+      editHref={editHref}
+      helpHref={helpHref}
+      appHref={appDefinitionKey ? `/setting/define?app=${encodeURIComponent(appDefinitionKey)}` : null}
+      grafanaUrl={grafanaState.enabled ? grafanaState.url : null}
+      navigationContext={detailNavigationContext}
+      onSelectTab={handleSelectDetailTab}
+      onSelectRefreshInterval={handleSelectRefreshInterval}
+      onRefresh={handleRefresh}
+      realtimeContent={sections.realtimeContent}
+      historyContent={sections.historyContent}
+      favoritesContent={sections.favoritesContent}
+      grafanaContent={sections.grafanaContent}
+      t={t}
+    />
+  );
+}
+
+export default function MonitorDetailPage({ monitorId }: { monitorId: string }) {
+  const { t } = useI18n();
+  const queryClient = useQueryClient();
+  const monitorDetailUrl = useMemo(() => buildMonitorDetailUrl(monitorId), [monitorId]);
+  const monitorDetailGrafanaUrl = useMemo(() => buildMonitorGrafanaUrl(monitorId), [monitorId]);
+  const monitorDetailFavoriteUrl = useMemo(() => buildMonitorFavoriteUrl(monitorId), [monitorId]);
+  const monitorDetailCacheKey = useMemo(
+    () => ['monitor-detail', monitorDetailUrl, monitorDetailGrafanaUrl, monitorDetailFavoriteUrl].join(':'),
+    [monitorDetailFavoriteUrl, monitorDetailGrafanaUrl, monitorDetailUrl]
+  );
+  const load = useCallback(async (): Promise<MonitorDetailData> => {
+    return queryClient.fetchQuery({
+      queryKey: queryKeys.monitors.detail(monitorId),
+      queryFn: () =>
+        loadMonitorDetailBundleFromFacade(
+          api.monitors.detail<MonitorDetailResponse>,
+          api.monitors.grafanaDashboard,
+          api.monitors.favoriteMetrics,
+          monitorId
+        ),
+      staleTime: 5000
+    });
+  }, [monitorId, queryClient]);
+
+  return (
+    <ClientWorkbench
+      load={load}
+      loadingCopy={t('monitor.detail.loading')}
+      cacheKey={monitorDetailCacheKey}
+      renderLoading={visible => (
+        <section
+          className="mx-auto w-full max-w-[1320px] px-3 py-6"
+          data-monitor-detail-route-state={visible ? 'loading' : 'deferred'}
+          data-monitor-detail-route-state-owner="hertzbeat-ui-loading-state"
+          data-monitor-detail-loading-state="angular-route-state"
+          aria-busy="true"
+        >
+          {visible ? (
+            <HzLoadingState
+              title={t('monitor.route-state.loading.title')}
+              description={t('monitor.route-state.loading.copy')}
+              rows={3}
+              data-monitor-detail-route-state-loading="true"
+            />
+          ) : null}
+        </section>
+      )}
+      renderError={(message, retry) => (
+        <section
+          className="mx-auto w-full max-w-[1320px] px-3 py-6"
+          data-monitor-detail-route-state="error"
+          data-monitor-detail-route-state-owner="hertzbeat-ui-inline-feedback"
+          data-monitor-detail-error-state="angular-route-state"
+        >
+          <HzInlineFeedback
+            tone="critical"
+            title={t('monitor.route-state.error.title')}
+            description={(
+              <>
+                {t('monitor.route-state.error.copy')}
+                <span className="sr-only"> {message}</span>
+              </>
+            )}
+            variant="embedded"
+            data-monitor-detail-route-state-feedback="error"
+          />
+          <div className="mt-3 flex justify-center">
+            <HzButton
+              size="sm"
+              intent="primary"
+              data-monitor-detail-route-state-retry="true"
+              data-monitor-detail-route-state-retry-owner="hertzbeat-ui-button"
+              onClick={retry}
+            >
+              {t('monitor.route-state.action.retry')}
+            </HzButton>
+          </div>
+        </section>
+      )}
+    >
+      {data => <MonitorDetailWorkbench data={data} />}
+    </ClientWorkbench>
+  );
+}
diff --git a/web-next/app/monitors/[monitorId]/page.test.tsx b/web-next/app/monitors/[monitorId]/page.test.tsx
index aafe5878d0..34fb3bb8f5 100644
--- a/web-next/app/monitors/[monitorId]/page.test.tsx
+++ b/web-next/app/monitors/[monitorId]/page.test.tsx
@@ -2,19 +2,33 @@ import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { describe, expect, it } from 'vitest';
 
-const source = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/page.tsx'), 'utf8');
+const source = readFileSync(resolve(process.cwd(), 'app/monitors/[monitorId]/monitor-detail-page.tsx'), 'utf8');
+const sectionsSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-sections.tsx'), 'utf8');
+const consoleSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
+const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
+const chartGridSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-chart-grid.tsx'), 'utf8');
 
 describe('MonitorDetailPage route contract', () => {
   it('routes monitor detail through the shared HertzBeat console and section owner', () => {
     expect(source).toContain('MonitorDetailConsole');
     expect(source).toContain('MonitorDetailSections');
-    expect(source).toContain('loadMonitorDetailBundle');
+    expect(source).toContain('loadMonitorDetailBundleFromFacade');
+    expect(source).toContain('useQueryClient');
+    expect(source).toContain('queryClient.fetchQuery({');
+    expect(source).toContain('queryKey: queryKeys.monitors.detail(monitorId)');
+    expect(source).toContain('staleTime: 5000');
+    expect(source).toContain('api.monitors.detail<MonitorDetailResponse>');
+    expect(source).toContain('api.monitors.grafanaDashboard');
+    expect(source).toContain('api.monitors.favoriteMetrics');
+    expect(source).not.toContain('loadMonitorDetailBundle(apiMessageGet, monitorId)');
+    expect(source).not.toContain("import { apiMessageDelete, apiMessageGet, apiMessagePost } from '@/lib/api-client';");
     expect(source).not.toContain('buildMonitorWorkbenchSummaryFacts');
     expect(source).not.toContain("const historyChartSummaryCount = currentTab === 'history' ? historyMetrics.length : 0;");
     expect(source).toContain('historyChartPayloads');
     expect(source).toContain('historyChartLoadingKeys');
     expect(source).toContain('historyChartErrors');
-    expect(source).toContain('filterMonitorHistoryMetricCatalog(historyMetrics, historyMetricSearch).slice(0, 6)');
+    expect(source).toContain('const displayedHistoryChartItems = useMemo(');
+    expect(source).toContain('filteredHistoryMetrics.slice(0, visibleHistoryChartCount)');
     expect(source).toContain('start: historyTimeContext.start');
     expect(source).toContain('end: historyTimeContext.end');
     expect(source).toContain('onApplyHistoryChartZoomTimeRange: handleApplyHistoryTimeContext');
@@ -24,17 +38,460 @@ describe('MonitorDetailPage route contract', () => {
     expect(source).not.toContain('buildMonitorDetailFacts(monitor');
   });
 
+  it('invalidates the monitor detail React Query data after mutable detail actions', () => {
+    expect(source).toContain('const invalidateMonitorDetailQuery = useCallback(');
+    expect(source).toContain('queryClient.invalidateQueries({ queryKey: queryKeys.monitors.detail(monitor.id) })');
+    expect(source).toContain('void invalidateMonitorDetailQuery();');
+    expect(source).toContain('addMonitorFavoriteFromFacade(api.monitors.addFavoriteMetric, monitor.id, metricName)');
+    expect(source).toContain('removeMonitorFavoriteFromFacade(api.monitors.removeFavoriteMetric, monitor.id, metricName)');
+    expect(source).toContain('removeMonitorFavoriteFromFacade(api.monitors.removeFavoriteMetric, monitor.id, token)');
+    expect(source).toContain('deleteMonitorGrafanaDashboardFromFacade(api.monitors.deleteGrafanaDashboard, monitor.id)');
+  });
+
+  it('keeps monitor history time range synchronized with the platform from/to route contract', () => {
+    expect(source).toContain('useRouter');
+    expect(source).toContain('const searchParamString = searchParams.toString();');
+    expect(source).toContain('parseTimeContextFromParams(new URLSearchParams(searchParamString))');
+    expect(source).toContain('[searchParamString]');
+    expect(source).not.toContain('parseTimeContextFromParams(searchParams)');
+    expect(source).toContain('resolveAppliedTimeContext');
+    expect(source).toContain('timeRangeToExpressionRange');
+    expect(source).toContain('timeRangeToTimeWindow');
+    expect(source).toContain('timeWindowToTimeRange');
+    expect(source).toContain('function resolveMonitorHistoryWindow(');
+    expect(source).not.toContain('function timeRangeToExpressionFrom(');
+    expect(source).not.toContain('function historyWindowToTimeRange(');
+    expect(source).not.toContain('function timeRangeToHistoryWindow(');
+    expect(source).toContain('buildMonitorHistoryTimeRoute');
+    expect(source).toContain('router.replace(route);');
+    expect(source).toContain('function replaceMonitorHistoryTimeRoute(context: TimeContext) {');
+    expect(source).toContain("window.history.replaceState(window.history.state, '', route);");
+    expect(source).toContain('historyTimeContext.from');
+    expect(source).toContain('historyTimeContext.to');
+    expect(source).toContain('historyTimeContext.timezone');
+  });
+
+  it('keeps hidden monitor detail tabs from loading unrelated metric payloads', () => {
+    expect(source).toContain(
+      "const shouldLoadRealtimeMetrics = currentTab === 'realtime' || (currentTab === 'favorites' && favoriteSurfaceMode === 'realtime');"
+    );
+    expect(source).toContain(
+      "const shouldLoadHistoryCatalog = currentTab === 'history' || (currentTab === 'favorites' && favoriteSurfaceMode === 'history');"
+    );
+    expect(source).toContain(
+      "const shouldLoadHistoryPayloads = currentTab === 'history' || (currentTab === 'favorites' && favoriteSurfaceMode === 'history');"
+    );
+    expect(source).toContain("if (!shouldLoadRealtimeMetrics) {");
+    expect(source).toContain('if (!shouldLoadHistoryCatalog) return;');
+    expect(source).toContain("if (!shouldLoadHistoryPayloads || !selectedHistoryMetric) {");
+    expect(source).toContain('[displayedMetricNameKey, metricReloadToken, monitor.id, shouldLoadRealtimeMetrics, t]');
+    expect(source).toContain('[historyCatalogReloadToken, monitor, shouldLoadHistoryCatalog, t]');
+    expect(source).toContain('shouldLoadHistoryPayloads,');
+    expect(consoleSource).toContain('data-monitor-detail-history-catalog-load="angular-tab-click-lazy"');
+    expect(consoleSource).toContain('data-monitor-detail-realtime-tab-load="angular-click-load-real-time-metric"');
+  });
+
+  it('reloads favorite tab payloads through the active Angular favorite subselector', () => {
+    expect(source).toContain("const shouldLoadChartGrid = currentTab === 'history' || (currentTab === 'favorites' && favoriteSurfaceMode === 'history');");
+    expect(source).toContain("currentTab === 'favorites' && favoriteSurfaceMode === 'history'");
+    expect(source).toContain('const favoriteHistoryCatalogItemKey = favoriteHistoryCatalogItems.map');
+    expect(source).toContain('? favoriteHistoryChartItems');
+    expect(source).toContain('const favoriteHistoryChartItemKey = favoriteHistoryChartItems.map');
+    expect(source).toContain("if (currentTab !== 'favorites' || favoriteSurfaceMode !== 'history') return;");
+    expect(source).toContain('setSelectedHistoryMetricKey(first ? `${first.metrics}:${first.metric}` : null);');
+    expect(source).toContain('setSelectedHistorySeriesKey(null);');
+    expect(source).toContain('setSelectedHistoryPointIndex(null);');
+    expect(source).toContain('setHistoryPayload(null);');
+    expect(source).toContain('setFavoriteReloadToken(prev => prev + 1);');
+    expect(source).toContain("if (favoriteSurfaceMode === 'history') {");
+    expect(source).toContain('resetHistoryInvestigationSelection(favoriteHistoryCatalogItems);');
+    expect(source).toContain('setHistoryReloadToken(prev => prev + 1);');
+    expect(source).toContain('setMetricReloadToken(prev => prev + 1);');
+    expect(source).toContain("if (tab === 'favorites') {");
+    expect(source).toContain('setVisibleFavoriteRealtimeMetricCount(MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE);');
+    expect(source).toContain('setVisibleFavoriteHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);');
+    expect(consoleSource).toContain('data-monitor-detail-favorite-tab-load="angular-click-load-favorite-metrics"');
+    expect(sectionsSource).toContain('data-monitor-detail-favorite-history-reload-reset="angular-chart-reload"');
+    expect(sectionsSource).toContain('data-monitor-detail-favorite-history-source="favorite-history-chart-payloads"');
+  });
+
+  it('keeps favorite history chart controls available like Angular monitor-data-chart toolboxes', () => {
+    expect(source).toContain('function handleFavoriteHistoryRefresh() {');
+    expect(source).toContain('setVisibleFavoriteHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);');
+    expect(source).toContain('setFavoriteReloadToken(prev => prev + 1);');
+    expect(source).toContain('setHistoryReloadToken(prev => prev + 1);');
+    expect(source).toContain('onFavoriteHistoryRefresh: handleFavoriteHistoryRefresh');
+    expect(sectionsSource).toContain('data-monitor-detail-favorite-history-controls="angular-chart-toolbox"');
+    expect(sectionsSource).toContain('onRefresh={onFavoriteHistoryRefresh}');
+  });
+
+  it('keeps history and favorite chart grids incrementally reachable like Angular sentinels', () => {
+    expect(source).toContain('const MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE = 6;');
+    expect(source).toContain('const [visibleHistoryChartCount, setVisibleHistoryChartCount] = useState(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);');
+    expect(source).toContain(
+      'const [visibleFavoriteHistoryChartCount, setVisibleFavoriteHistoryChartCount] = useState(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);'
+    );
+    expect(source).toContain('filteredHistoryMetrics.slice(0, visibleHistoryChartCount)');
+    expect(source).toContain('const favoriteHistoryCatalogItems = useMemo(');
+    expect(source).toContain('() => favoriteHistoryCatalogItems.slice(0, visibleFavoriteHistoryChartCount)');
+    expect(source).toContain('setVisibleHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);');
+    expect(source).toContain('setVisibleFavoriteHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);');
+    expect(source).toContain('historyChartVisibleCount: visibleHistoryChartCount');
+    expect(source).toContain('favoriteHistoryChartVisibleCount: visibleFavoriteHistoryChartCount');
+    expect(source).toContain('onLoadMoreHistoryCharts: () =>');
+    expect(source).toContain('onLoadMoreFavoriteHistoryCharts: () =>');
+    expect(source).not.toContain('filterMonitorHistoryMetricCatalog(historyMetrics, historyMetricSearch).slice(0, 6)');
+    expect(sectionsSource).toContain('data-monitor-detail-incremental-mode="angular-chart-sentinel"');
+    expect(sectionsSource).toContain('data-monitor-detail-incremental-mode="angular-favorite-chart-sentinel"');
+  });
+
+  it('keeps favorite realtime metrics incrementally reachable like Angular favoriteMetricsLoadSentinel', () => {
+    expect(source).toContain('const MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE = 6;');
+    expect(source).toContain(
+      'const [visibleFavoriteRealtimeMetricCount, setVisibleFavoriteRealtimeMetricCount] = useState(MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE);'
+    );
+    expect(source).toContain("const favoriteRealtimeRows = useMemo(() => favoriteJumpRows.filter(row => row.targetKind === 'realtime'), [favoriteJumpRows]);");
+    expect(source).toContain('const favoriteRealtimeMetricHasMore = visibleFavoriteRealtimeMetricCount < favoriteRealtimeRows.length;');
+    expect(source).toContain('setVisibleFavoriteRealtimeMetricCount(MONITOR_DETAIL_FAVORITE_REALTIME_PAGE_SIZE);');
+    expect(source).toContain('favoriteRealtimeMetricVisibleCount: visibleFavoriteRealtimeMetricCount');
+    expect(source).toContain('favoriteRealtimeMetricTotalCount: favoriteRealtimeRows.length');
+    expect(source).toContain('favoriteRealtimeMetricHasMore,');
+    expect(source).toContain('onLoadMoreFavoriteRealtimeMetrics: () =>');
+    expect(sectionsSource).toContain('visibleRealtimeFavoriteRows = realtimeFavoriteRows.slice(0, favoriteRealtimeMetricVisibleCount)');
+    expect(sectionsSource).toContain('incrementalMode="angular-favorite-sentinel"');
+    expect(sectionsSource).toContain('incrementalScope="favorite-realtime"');
+  });
+
+  it('resets history chart pagination when the shared history toolbar refreshes like Angular loadMetricChart', () => {
+    expect(source).toContain('api.monitors.warehouseStorageStatus');
+    expect(source).toContain('api.monitors.historyMetricCatalogDefine');
+    expect(source).toContain(
+      'loadMonitorHistoryMetricCatalogFromFacade(api.monitors.warehouseStorageStatus, api.monitors.historyMetricCatalogDefine, monitor)'
+    );
+    expect(source).not.toContain('loadMonitorHistoryMetricCatalog(apiMessageGet, monitor)');
+    expect(source).toContain('const [historyCatalogReloadToken, setHistoryCatalogReloadToken] = useState(0);');
+    expect(source).toContain('[historyCatalogReloadToken, monitor, shouldLoadHistoryCatalog, t]');
+    expect(source).toContain("if (tab === 'history') {");
+    expect(source).toContain('resetHistoryInvestigationSelection(filteredHistoryMetrics);');
+    expect(source).toContain('setHistoryCatalogReloadToken(prev => prev + 1);');
+    expect(source).toContain('function handleHistoryRefresh() {');
+    expect(source).toContain('setVisibleHistoryChartCount(MONITOR_DETAIL_HISTORY_CHART_PAGE_SIZE);');
+    expect(source).toContain('setHistoryReloadToken(prev => prev + 1);');
+    expect(source).toContain('onHistoryRefresh: handleHistoryRefresh');
+    expect(source).not.toContain('onHistoryRefresh: () => setHistoryReloadToken(prev => prev + 1)');
+    expect(consoleSource).toContain('data-monitor-detail-history-tab-load="angular-click-load-metric-chart"');
+  });
+
+  it('resets history investigation selection on chart reload and time changes like stateless Angular charts', () => {
+    expect(source).toContain('api.monitors.historyMetric');
+    expect(source).toContain('queryKeys.monitors.history');
+    expect(source).not.toContain('queryKeys.dashboard.panel');
+    expect(source).not.toContain('monitorDetailDashboardTemplate.id');
+    expect(source).toContain('const readMonitorHistoryMetricPanelData = useCallback(');
+    expect(source).toContain('readMonitorHistoryMetricPanelData(selectedHistoryMetric)');
+    expect(source).toContain('readMonitorHistoryMetricPanelData(item)');
+    expect(source).toContain('loadMonitorHistoryMetricDataFromFacade(api.monitors.historyMetric, monitor, item, {');
+    expect(source).not.toContain('loadMonitorHistoryMetricData(apiMessageGet, monitor');
+    expect(source).toContain('const resetHistoryInvestigationSelection = useCallback((items: MonitorHistoryMetricCatalogItem[] = filteredHistoryMetrics) => {');
+    expect(source).toContain('const first = items[0];');
+    expect(source).toContain('setSelectedHistoryMetricKey(first ? `${first.metrics}:${first.metric}` : null);');
+    expect(source).toContain('setSelectedHistorySeriesKey(null);');
+    expect(source).toContain('setSelectedHistoryPointIndex(null);');
+    expect(source).toContain('setHistoryPayload(null);');
+    expect(source).toContain('setHistoryPayloadError(null);');
+    expect(source).toContain('resetActiveHistoryInvestigationSelection();');
+    expect(source).toContain('function handleHistoryModeChange(value: boolean) {');
+    expect(source).toContain('function handleSelectHistoryMetricKey(key: string) {');
+    expect(source).toContain('resetHistoryInvestigationSelection(filteredHistoryMetrics);');
+    expect(source).toContain('resetHistoryInvestigationSelection(favoriteHistoryCatalogItems);');
+    expect(source).toContain('onSelectHistoryMetric: handleSelectHistoryMetricKey');
+    expect(source).toContain('onHistoryModeChange: handleHistoryModeChange');
+    expect(source).not.toContain('onSelectHistoryMetric: setSelectedHistoryMetricKey');
+    expect(source).not.toContain('onHistoryModeChange: setHistoryInterval');
+    expect(chartGridSource).toContain('data-monitor-history-selection-reset="angular-chart-reload"');
+  });
+
+  it('keeps realtime metrics incrementally reachable instead of truncating the Angular metric catalog', () => {
+    expect(source).toContain('api.monitors.realtimeMetric');
+    expect(source).toContain('loadMonitorRealtimeMetricDataFromFacade(api.monitors.realtimeMetric, monitor.id, name)');
+    expect(source).toContain('loadMonitorRealtimeMetricDataFromFacade(api.monitors.realtimeMetric, monitor.id, selectedMetric.name)');
+    expect(source).not.toContain('loadMonitorRealtimeMetricData(apiMessageGet, String(monitor.id)');
+    expect(source).toContain('const MONITOR_DETAIL_REALTIME_PAGE_SIZE = 10;');
+    expect(source).toContain('const [visibleRealtimeMetricCount, setVisibleRealtimeMetricCount] = useState(MONITOR_DETAIL_REALTIME_PAGE_SIZE);');
+    expect(source).toContain('const displayedMetrics = filteredMetrics.slice(0, visibleRealtimeMetricCount);');
+    expect(source).toContain('const realtimeMetricHasMore = visibleRealtimeMetricCount < filteredMetrics.length;');
+    expect(source).toContain('setVisibleRealtimeMetricCount(MONITOR_DETAIL_REALTIME_PAGE_SIZE);');
+    expect(source).toContain('realtimeMetricTotalCount: filteredMetrics.length');
+    expect(source).toContain('realtimeMetricHasMore,');
+    expect(source).toContain('onLoadMoreRealtimeMetrics: () =>');
+    expect(source).not.toContain('const displayedMetrics = filteredMetrics.slice(0, 10);');
+    expect(sectionsSource).toContain('HzMonitorIncrementalLoadFooter');
+    expect(sectionsSource).toContain('data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"');
+    expect(sectionsSource).toContain("incrementalMode = 'angular-sentinel'");
+  });
+
+  it('keeps realtime metric payload failures scoped to each Angular-style card', () => {
+    const realtimeStart = sectionsSource.indexOf('const realtimeNode = (');
+    const historyStart = sectionsSource.indexOf('const historyNode = (', realtimeStart);
+    const realtimeSource = sectionsSource.slice(realtimeStart, historyStart);
+    const signalListSource = sectionsSource.slice(
+      sectionsSource.indexOf('function MetricSignalList'),
+      sectionsSource.indexOf('function MonitorBasicSharedSurface')
+    );
+
+    expect(signalListSource).toContain('data-monitor-detail-realtime-payload-errors="card-local"');
+    expect(realtimeSource).toContain('metricCardPayloads');
+    expect(realtimeSource).not.toContain('metricPayloadError ?');
+    expect(realtimeSource).not.toContain('data-monitor-detail-state-scope="realtime"');
+  });
+
+  it('resets realtime table selection and detail mode on metric reload like stateless Angular data tables', () => {
+    expect(source).toContain('const resetRealtimeInvestigationSelection = useCallback(() => {');
+    expect(source).toContain('setMetricPayload(null);');
+    expect(source).toContain('setMetricPayloadError(null);');
+    expect(source).toContain('setSelectedMetricRowKey(null);');
+    expect(source).toContain("setMetricTableMode('table');");
+    expect(source).toContain('function handleSelectMetricKey(key: string) {');
+    expect(source).toContain('function handleMetricRefresh() {');
+    expect(source).toContain('resetRealtimeInvestigationSelection();');
+    expect(source).toContain('onSelectMetric: handleSelectMetricKey');
+    expect(source).toContain('onMetricRefresh: handleMetricRefresh');
+    expect(source).not.toContain('onSelectMetric: setSelectedMetricKey');
+    expect(source).not.toContain('onMetricRefresh: () => setMetricReloadToken(prev => prev + 1)');
+    expect(sectionsSource).toContain('data-monitor-detail-realtime-selection-reset="angular-table-reload"');
+  });
+
+  it('synchronizes the persistent monitor refresh toolbar with the platform refresh route context', () => {
+    expect(source).toContain('resolveTimeContextRefreshInterval');
+    expect(source).toContain('const MONITOR_DETAIL_DEFAULT_REFRESH_INTERVAL_SECONDS = 90;');
+    expect(source).toContain('TIME_CONTEXT_REFRESH_INTERVAL_SECONDS');
+    expect(source).not.toContain('function resolveMonitorDetailRefreshInterval(');
+    expect(source).toContain('TIME_CONTEXT_REFRESH_INTERVAL_SECONDS,');
+    expect(source).toContain('MONITOR_DETAIL_DEFAULT_REFRESH_INTERVAL_SECONDS');
+    expect(source).toContain('const rawRouteTimeContext = useMemo(');
+    expect(source).toContain('() => normalizeMonitorHistoryTimeContext(rawRouteTimeContext)');
+    expect(source).toContain('rawRouteTimeContext,');
+    expect(source).toContain('const [refreshInterval, setRefreshInterval] = useState<number>(() => routeRefreshInterval);');
+    expect(source).toContain('const [refreshCountdown, setRefreshCountdown] = useState<number>(() => (routeRefreshInterval > 0 ? routeRefreshInterval : -1));');
+    expect(source).toContain('setRefreshInterval(routeRefreshInterval);');
+    expect(consoleSource).toContain('data-monitor-detail-default-refresh-contract={refreshInterval === 90 ?');
+    expect(consoleSource).toContain('data-monitor-detail-refresh-copy-contract="angular-deadline-label"');
+  });
+
+  it('keeps Grafana refresh aligned to the previous Angular data tab', () => {
+    expect(source).toContain("const [lastRefreshableTab, setLastRefreshableTab] = useState<MonitorDetailRefreshableTab>('realtime');");
+    expect(source).toContain('resolveMonitorDetailRefreshTarget(currentTab, lastRefreshableTab)');
+    expect(source).toContain('function handleSelectDetailTab(tab: MonitorDetailConsoleTabKey) {');
+    expect(source).toContain("if (tab !== 'grafana') {");
+    expect(source).toContain('setLastRefreshableTab(tab);');
+    expect(source).toContain('setVisibleRealtimeMetricCount(MONITOR_DETAIL_REALTIME_PAGE_SIZE);');
+    expect(source).toContain('resetRealtimeInvestigationSelection();');
+    expect(source).toContain('setMetricReloadToken(prev => prev + 1);');
+    expect(source).toContain('grafanaRefreshFallbackTab={lastRefreshableTab}');
+    expect(consoleSource).toContain("data-monitor-detail-grafana-refresh-target={currentTab === 'grafana' ? grafanaRefreshFallbackTab : undefined}");
+    expect(source).toContain('setCurrentTab(lastRefreshableTab);');
+    expect(source).toContain('[currentTab, grafanaState.enabled, lastRefreshableTab]');
+    expect(consoleSource).toContain('data-monitor-detail-grafana-fallback-tab="angular-previous-data-tab"');
+    expect(sectionsSource).toContain('data-monitor-detail-grafana-action-contract="angular-edit-config-delete-dashboard"');
+    expect(sectionsSource).toContain('data-monitor-detail-grafana-config-action="monitor-edit-grafana"');
+    expect(sectionsSource).toContain('data-monitor-detail-grafana-delete-action="delete-dashboard"');
+    expect(sectionsSource).toContain('data-monitor-detail-grafana-delete-teardown="angular-hide-tab"');
+    expect(source).toContain('await deleteMonitorGrafanaDashboardFromFacade(api.monitors.deleteGrafanaDashboard, monitor.id);');
+    expect(source).toContain('setGrafanaState({ enabled: false });');
+  });
+
+  it('keeps favorite add/remove feedback aligned with Angular monitor copy', () => {
+    expect(source).toContain("import { api } from '@/lib/monitor-api-facade';");
+    expect(source).toContain('api.monitors.favoriteMetrics(monitor.id)');
+    expect(source).toContain('addMonitorFavoriteFromFacade(api.monitors.addFavoriteMetric, monitor.id, metricName)');
+    expect(source).toContain('removeMonitorFavoriteFromFacade(api.monitors.removeFavoriteMetric, monitor.id, metricName)');
+    expect(source).toContain('removeMonitorFavoriteFromFacade(api.monitors.removeFavoriteMetric, monitor.id, favoriteToken)');
+    expect(source).toContain('addMonitorFavoriteFromFacade(api.monitors.addFavoriteMetric, monitor.id, nextToken)');
+    expect(source).toContain('api.monitors.grafanaDashboard<GrafanaDashboard | null>(monitor.id)');
+    expect(source).not.toContain('loadMonitorFavoriteMetrics(apiMessageGet as any, String(monitor.id))');
+    expect(source).not.toContain('apiMessageGet<GrafanaDashboard | null>(buildMonitorGrafanaUrl(String(monitor.id)))');
+    expect(source).not.toContain('addMonitorFavorite(apiMessagePost');
+    expect(source).not.toContain('removeMonitorFavorite(apiMessageDelete');
+    expect(source).toContain('normalizeMonitorFavoriteNames');
+    expect(source).toContain('useState<string[]>(() => normalizeMonitorFavoriteNames(favoriteMetrics))');
+    expect(source).toContain('setFavoriteNames(normalizeMonitorFavoriteNames(favoriteMetrics));');
+    expect(source).toContain('setFavoriteNames(normalizeMonitorFavoriteNames(result || []));');
+    expect(source).toContain('setFavoriteNames(prev => normalizeMonitorFavoriteNames([...prev, metricName]));');
+    expect(source).toContain('setFavoriteNames(prev => normalizeMonitorFavoriteNames([...prev, nextToken]));');
+    expect(sectionsSource).toContain('data-monitor-detail-favorite-set-semantics="angular-set"');
+    expect(source).toContain("setFavoriteMessage(t(favorited ? 'monitor.favorite.remove.success' : 'monitor.favorite.add.success'))");
+    expect(source).toContain("setFavoriteError(error instanceof Error ? error.message : t(favorited ? 'monitor.favorite.remove.failed' : 'monitor.favorite.add.failed'))");
+    expect(source).toContain("setFavoriteMessage(t(favoriteToken ? 'monitor.favorite.remove.success' : 'monitor.favorite.add.success'))");
+    expect(source).toContain("setFavoriteMessage(t('monitor.favorite.remove.success'))");
+    expect(source).toContain("setFavoriteError(error instanceof Error ? error.message : t('monitor.favorite.remove.failed'))");
+    expect(sectionsSource).toContain('data-monitor-detail-favorite-feedback-copy="angular-favorite-copy"');
+    expect(source).not.toContain("setFavoriteMessage(t('common.save-success'))");
+    expect(source).not.toContain("setFavoriteError(error instanceof Error ? error.message : t('common.save-failed'))");
+  });
+
+  it('keeps favorite removal fallback scoped to the active Angular subselector', () => {
+    expect(source).toContain('setSelectedFavoriteKey(prev => resolveActiveFavoriteRow(favoriteJumpRows, prev, favoriteSurfaceMode)?.key ?? null);');
+    expect(source).toContain('if (!selectedFavoriteKey) return;');
+    expect(source).toContain('const row = resolveActiveFavoriteRow(favoriteJumpRows, selectedFavoriteKey, favoriteSurfaceMode);');
+    expect(source).toContain("setFavoriteSurfaceMode(current => resolveFavoriteSurfaceMode(current, favoriteJumpRows, selectedFavoriteKey));");
+    expect(sectionsSource).toContain('data-monitor-detail-favorite-active-fallback="angular-subselector-sticky"');
+  });
+
+  it('keeps favorite empty states scoped like Angular empty-favorite panes without sentinels', () => {
+    expect(sectionsSource).toContain('data-monitor-detail-favorite-empty-contract="angular-subselector-empty"');
+    expect(sectionsSource).toContain('data-monitor-detail-favorite-empty-teardown="angular-empty-no-sentinel"');
+    expect(sectionsSource).toContain('data-monitor-detail-empty-scope="favorite-realtime"');
+    expect(sectionsSource).toContain('data-monitor-detail-empty-scope="favorite-history"');
+    expect(sectionsSource).toContain("favoriteMetricRows.length > 0 ? (");
+    expect(sectionsSource).toContain("favoriteHistoryChartItems.length > 0 ? (");
+    expect(sectionsSource).toContain("title={t('monitor.detail.favorite.surface.empty.realtime.title')}");
+    expect(sectionsSource).toContain("title={t('monitor.detail.favorite.surface.empty.history.title')}");
+    expect(sectionsSource).not.toContain('data-monitor-detail-favorite-empty="realtime"');
+    expect(sectionsSource).not.toContain('data-monitor-detail-favorite-empty="history"');
+  });
+
+  it('keeps monitor history refresh manual by default to avoid overlapping chart reloads', () => {
+    expect(source).toContain("const DEFAULT_HISTORY_TIME_CONTEXT: TimeContext = {");
+    expect(source).toContain("live: 'false'");
+    expect(source).toContain("from: 'now-1h'");
+    expect(source).toContain("to: 'now'");
+    expect(source).toContain("if (context.refresh) {");
+    expect(source).toContain('delete nextFallback.live;');
+  });
+
+  it('writes persistent monitor refresh changes back to the platform time route context', () => {
+    expect(source).toContain('timeContextRefreshIntervalToContext');
+    expect(source).not.toContain('function monitorDetailRefreshIntervalToContextValue(');
+    expect(source).not.toContain('function monitorDetailRefreshIntervalToLiveValue(');
+    expect(source).toContain('function handleSelectRefreshInterval(value: number) {');
+    expect(source).toContain('setRefreshCountdown(value > 0 ? value : -1);');
+    expect(source).toContain('const nextRefreshContext = timeContextRefreshIntervalToContext(value);');
+    expect(source).toContain('{ ...historyTimeContext, ...nextRefreshContext }');
+    expect(source).toContain('replaceMonitorHistoryTimeRoute(applied);');
+    expect(source).toContain('onSelectRefreshInterval={handleSelectRefreshInterval}');
+    expect(source).not.toContain('onSelectRefreshInterval={setRefreshInterval}');
+  });
+
+  it('clears stale paused refresh state when applying a positive history refresh interval', () => {
+    expect(source).toContain('function resolveMonitorHistoryTimeContextFallback(');
+    expect(source).toContain("Object.prototype.hasOwnProperty.call(context, 'live')");
+    expect(source).toContain("Object.prototype.hasOwnProperty.call(context, 'refresh')");
+    expect(source).toContain('if (context.refresh) {');
+    expect(source).toContain('delete nextFallback.live;');
+    expect(source).toContain("if (context.live === 'false') {");
+    expect(source).toContain('delete nextFallback.refresh;');
+  });
+
   it('does not reintroduce the old pre-tab context card stack at route level', () => {
     expect(source).not.toContain('contextContent=');
     expect(source).not.toContain('SurfaceSection');
     expect(source).not.toContain('WorkbenchPanel');
   });
 
-  it('passes monitor signal handoff links from route state into the compact console toolbar', () => {
-    expect(source).toContain('buildMonitorSignalHandoffLinks');
-    expect(source).toContain('const signalHandoffLinks = buildMonitorSignalHandoffLinks(');
-    expect(source).toContain('signalHandoffLinks={signalHandoffLinks}');
+  it('does not pass monitor signal handoff links into the detail toolbar', () => {
+    expect(source).not.toContain('buildMonitorSignalHandoffLinks');
+    expect(source).not.toContain('const signalHandoffLinks = buildMonitorSignalHandoffLinks(');
+    expect(source).not.toContain('signalHandoffLinks={signalHandoffLinks}');
     expect(source).not.toContain('data-monitor-signal-fake-card');
     expect(source).not.toContain('data-monitor-signal-fake-count');
   });
+
+  it('keeps monitor detail free of toolbar-level signal handoff links', () => {
+    expect(consoleSource).toContain('HzMonitorRefreshToolbar');
+    expect(consoleSource).not.toContain('signalHandoffLinks');
+    expect(consoleSource).not.toContain("id: 'metrics'");
+    expect(consoleSource).not.toContain("id: 'logs'");
+    expect(consoleSource).not.toContain("id: 'traces'");
+    expect(consoleSource).toContain('HzIconLink');
+    expect(consoleSource).toContain('data-monitor-detail-tab-extra-contract="angular-refresh-help"');
+    expect(consoleSource).toContain('data-monitor-detail-help-action="angular-docs-help"');
+    expect(consoleSource).toContain('data-monitor-detail-help-owner="hertzbeat-ui-icon-link"');
+  });
+
+  it('keeps monitor detail breadcrumb context owned by shared underline text instead of chip chrome', () => {
+    expect(consoleSource).toContain('HzInlineContextMark');
+    expect(consoleSource).toContain('data-monitor-detail-context-mark="breadcrumb"');
+    expect(consoleSource).toContain('data-monitor-detail-context-mark-owner="hertzbeat-ui-inline-context-mark"');
+    expect(consoleSource).toContain('data-monitor-detail-list-return="angular-app-filter"');
+    expect(consoleSource).toContain('data-monitor-detail-list-return-target={backHref}');
+    expect(consoleSource).toContain('data-monitor-detail-app-definition-link="angular-monitor-app"');
+    expect(consoleSource).toContain('data-monitor-detail-app-definition-target={appHref}');
+    expect(source).toContain('function resolveMonitorDetailAppContext(monitor: Monitor, navigationContext: { app?: string | null })');
+    expect(source).toContain("const scrapeApp = monitor.scrape && monitor.scrape !== 'static' ? monitor.scrape : null;");
+    expect(source).toContain('return scrapeApp || navigationContext.app || monitor.app || null;');
+    expect(source).toContain('const appDefinitionKey = resolveMonitorDetailAppContext(monitor, navigationContext);');
+    expect(source).toContain('const detailNavigationContext = appDefinitionKey ? { ...navigationContext, app: appDefinitionKey } : navigationContext;');
+    expect(source).toContain('const backHref = buildMonitorListReturnHref({ ...navigationContext, app: appDefinitionKey });');
+    expect(source).toContain('navigationContext={detailNavigationContext}');
+    expect(source).not.toContain('const backHref = buildMonitorListReturnHref(navigationContext);');
+    expect(source).toContain('appHref={appDefinitionKey ? `/setting/define?app=${encodeURIComponent(appDefinitionKey)}` : null}');
+    expect(source).not.toContain("appHref={navigationContext.app ? `/setting/define?app=${encodeURIComponent(navigationContext.app)}` : null}");
+    expect(consoleSource).not.toContain('HzBreadcrumbChip');
+    expect(consoleSource).not.toContain('data-monitor-detail-app-chip="breadcrumb"');
+    expect(consoleSource).not.toContain('data-monitor-detail-app-chip-owner="hertzbeat-ui-breadcrumb-chip"');
+    expect(consoleSource).not.toContain('ObservabilityControlChip');
+  });
+
+  it('keeps monitor detail tabs owned by shared @hertzbeat/ui chrome', () => {
+    expect(consoleSource).toContain('HzMonitorDetailTabs');
+    expect(consoleSource).toContain('data-monitor-detail-tabs-owner="hertzbeat-ui-monitor-detail-tabs"');
+    expect(consoleSource).toContain('HzMonitorDetailWorkbenchFrame');
+    expect(uiSource).toContain('data-monitor-detail-tabset-type="bottom-underline-switch"');
+    expect(consoleSource).not.toContain('data-monitor-detail-tabset-type="left-accent-card-switch"');
+    expect(consoleSource).not.toContain('ObservabilityTabStrip');
+  });
+
+  it('keeps monitor detail transient states owned by shared @hertzbeat/ui primitives', () => {
+    expect(sectionsSource).toContain('HzLoadingState');
+    expect(sectionsSource).toContain('HzStateNotice');
+    expect(sectionsSource).toContain('HzEmptyState');
+    expect(sectionsSource).toContain('data-monitor-detail-state-owner="hertzbeat-ui-loading-state"');
+    expect(sectionsSource).toContain('data-monitor-detail-state-owner="hertzbeat-ui-state-notice"');
+    expect(sectionsSource).toContain('data-monitor-detail-empty-owner="hertzbeat-ui-empty-state"');
+    expect(sectionsSource).not.toContain('data-monitor-detail-favorite-empty="realtime"');
+    expect(sectionsSource).not.toContain('data-monitor-detail-favorite-empty="history"');
+    expect(sectionsSource).not.toContain(
+      'rounded-[3px] border border-dashed border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-5 py-10'
+    );
+  });
+
+  it('keeps monitor detail load failures in the Angular-style detail route-state surface', () => {
+    expect(source).toContain('renderLoading={visible =>');
+    expect(source).toContain('renderError={(message, retry) =>');
+    expect(source).toContain('data-monitor-detail-loading-state="angular-route-state"');
+    expect(source).toContain('data-monitor-detail-error-state="angular-route-state"');
+    expect(source).toContain('data-monitor-detail-route-state-owner="hertzbeat-ui-loading-state"');
+    expect(source).toContain('data-monitor-detail-route-state-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).toContain('data-monitor-detail-route-state-feedback="error"');
+    expect(source).toContain('data-monitor-detail-route-state-retry-owner="hertzbeat-ui-button"');
+    expect(source).toContain("t('monitor.route-state.loading.title')");
+    expect(source).toContain("t('monitor.route-state.error.title')");
+    expect(source).toContain("t('monitor.route-state.action.retry')");
+  });
+
+  it('keeps monitor detail refresh interval selection on shared @hertzbeat/ui chrome', () => {
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
+
+    expect(consoleSource).toContain('HzMonitorRefreshToolbar');
+    expect(consoleSource).not.toContain('HzSelect');
+    expect(uiSource).toContain('HzSelect');
+    expect(uiSource).toContain('data-monitor-refresh-select-owner="hertzbeat-ui-select"');
+    expect(uiSource).toContain('data-monitor-refresh-select="true"');
+    expect(consoleSource).not.toContain("import { Select } from '../ui/select'");
+    expect(consoleSource).not.toContain('<Select');
+  });
+
+  it('keeps monitor detail refresh toolbar badge and action on shared @hertzbeat/ui primitives', () => {
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
+
+    expect(consoleSource).toContain('HzMonitorRefreshToolbar');
+    expect(consoleSource).not.toContain('HzStatusBadge');
+    expect(consoleSource).not.toContain('HzButton');
+    expect(uiSource).toContain('HzStatusBadge');
+    expect(uiSource).toContain('HzButton');
+    expect(uiSource).toContain('data-monitor-refresh-toolbar-owner="hertzbeat-ui-refresh-toolbar"');
+    expect(uiSource).toContain('data-monitor-refresh-badge-owner="hertzbeat-ui-status-badge"');
+    expect(uiSource).toContain('data-monitor-refresh-action-owner="hertzbeat-ui-button"');
+    expect(consoleSource).not.toContain('ObservabilityBadge');
+  });
 });
diff --git a/web-next/app/monitors/[monitorId]/page.tsx b/web-next/app/monitors/[monitorId]/page.tsx
index b140635123..13b110e6f5 100644
--- a/web-next/app/monitors/[monitorId]/page.tsx
+++ b/web-next/app/monitors/[monitorId]/page.tsx
@@ -1,739 +1,8 @@
-'use client';
+import React from 'react';
 
-import { useSearchParams } from 'next/navigation';
-import { useCallback, useEffect, useState } from 'react';
-import { ClientWorkbench } from '@/components/workbench/client-workbench';
-import { MonitorDetailConsole, type MonitorDetailConsoleTabKey } from '@/components/monitor-detail/monitor-detail-console';
-import { MonitorDetailSections } from '@/components/monitor-detail/monitor-detail-sections';
-import { useI18n } from '@/components/providers/i18n-provider';
-import { apiMessageDelete, apiMessageGet, apiMessagePost } from '@/lib/api-client';
-import { formatTime } from '@/lib/format';
-import {
-  addMonitorFavorite,
-  buildMonitorGrafanaUrl,
-  deleteMonitorGrafanaDashboard,
-  loadMonitorDetailBundle,
-  loadMonitorFavoriteMetrics,
-  loadMonitorHistoryMetricCatalog,
-  loadMonitorHistoryMetricData,
-  loadMonitorRealtimeMetricData,
-  removeMonitorFavorite,
-  type MonitorHistoryMetricCatalogItem
-} from '@/lib/monitor-detail/controller';
-import {
-  buildMonitorFavoriteJumpRows,
-  filterMonitorHistoryMetricCatalog,
-  filterMonitorHistorySeriesRows,
-  filterMonitorMetricCatalog,
-  buildMonitorHistoryMetricRows,
-  buildMonitorHistorySeriesRows,
-  buildMonitorMetricCatalogRows,
-  resolveMonitorMetricTableMode,
-  buildMonitorMetricTableMatrix
-} from '@/lib/monitor-detail/view-model';
-import {
-  buildMonitorSignalHandoffLinks,
-  readMonitorDetailNavigationContext,
-  resolveActiveFavoriteRow,
-  resolveFavoriteSurfaceMode,
-  resolveFavoriteJumpTarget,
-  resolveMonitorDetailRefreshTarget,
-  shouldFallbackFromGrafanaTab
-} from '@/lib/monitor-detail/detail-route-state';
-import { buildMonitorDetailHref, buildMonitorEditHref, buildMonitorListReturnHref } from '@/lib/monitor-manage/navigation';
-import { sanitizeTimeContext, type TimeContext } from '@/lib/time-context';
-import type { GrafanaDashboard, Monitor, MonitorDetailMetric, MonitorHistoryData, MonitorRealtimeMetricData, Param } from '@/lib/types';
+import MonitorDetailPage from './monitor-detail-page';
 
-type MonitorDetailData = {
-  monitor: Monitor;
-  params: Param[];
-  metrics: MonitorDetailMetric[];
-  favoriteMetrics: string[];
-  grafana: GrafanaDashboard;
-};
-
-type MetricCardPayloadState = {
-  payload: MonitorRealtimeMetricData | null;
-  loading: boolean;
-  error: string | null;
-};
-
-const HISTORY_WINDOWS = [
-  { value: '30m', labelKey: 'monitor.detail.history.range.last-30m' },
-  { value: '1h', labelKey: 'monitor.detail.history.range.last-1h' },
-  { value: '6h', labelKey: 'monitor.detail.history.range.last-6h' },
-  { value: '1d', labelKey: 'monitor.detail.history.range.last-1d' },
-  { value: '1W', labelKey: 'monitor.detail.history.range.last-1w' },
-  { value: '4W', labelKey: 'monitor.detail.history.range.last-4w' },
-  { value: '12W', labelKey: 'monitor.detail.history.range.last-12w' }
-] as const;
-
-const HISTORY_MODES = [
-  { value: false, labelKey: 'monitor.detail.history.mode.raw' },
-  { value: true, labelKey: 'monitor.detail.history.mode.aggregated' }
-] as const;
-
-function historyWindowToTimeRange(value: string | undefined) {
-  if (!value) return undefined;
-  if (value.startsWith('last-')) return value.toLowerCase();
-  return `last-${value.toLowerCase()}`;
-}
-
-function timeRangeToHistoryWindow(value: string | undefined) {
-  if (!value) return null;
-  const normalized = value.replace(/^last-/, '');
-  const nextWindow = normalized.endsWith('w') ? normalized.replace('w', 'W') : normalized;
-  return HISTORY_WINDOWS.some(option => option.value === nextWindow) ? nextWindow : null;
-}
-
-function MonitorDetailWorkbench({ data }: { data: MonitorDetailData }) {
-  const { t } = useI18n();
-  const searchParams = useSearchParams();
-  const { monitor, params, metrics, favoriteMetrics, grafana } = data;
-  const navigationContext = readMonitorDetailNavigationContext(searchParams);
-  const [grafanaState, setGrafanaState] = useState<GrafanaDashboard>(grafana ?? { enabled: false });
-  const [grafanaMessage, setGrafanaMessage] = useState<string | null>(null);
-  const [grafanaError, setGrafanaError] = useState<string | null>(null);
-  const [selectedMetricKey, setSelectedMetricKey] = useState<string | null>(null);
-  const [favoriteNames, setFavoriteNames] = useState<string[]>(favoriteMetrics);
-  const [favoriteMessage, setFavoriteMessage] = useState<string | null>(null);
-  const [favoriteError, setFavoriteError] = useState<string | null>(null);
-  const [historyMetrics, setHistoryMetrics] = useState<MonitorHistoryMetricCatalogItem[]>([]);
-  const [metricSearch, setMetricSearch] = useState('');
-  const [historyMetricSearch, setHistoryMetricSearch] = useState('');
-  const [historySeriesSearch, setHistorySeriesSearch] = useState('');
-  const [selectedHistoryMetricKey, setSelectedHistoryMetricKey] = useState<string | null>(null);
-  const [historyLoading, setHistoryLoading] = useState(false);
-  const [historyError, setHistoryError] = useState<string | null>(null);
-  const [historyPayload, setHistoryPayload] = useState<MonitorHistoryData | null>(null);
-  const [historyChartPayloads, setHistoryChartPayloads] = useState<Record<string, MonitorHistoryData | null>>({});
-  const [historyChartLoadingKeys, setHistoryChartLoadingKeys] = useState<string[]>([]);
-  const [historyChartErrors, setHistoryChartErrors] = useState<Record<string, string>>({});
-  const [historyPayloadLoading, setHistoryPayloadLoading] = useState(false);
-  const [historyPayloadError, setHistoryPayloadError] = useState<string | null>(null);
-  const [selectedHistorySeriesKey, setSelectedHistorySeriesKey] = useState<string | null>(null);
-  const [selectedHistoryPointIndex, setSelectedHistoryPointIndex] = useState<number | null>(null);
-  const [historyWindow, setHistoryWindow] = useState<(typeof HISTORY_WINDOWS)[number]['value']>('1h');
-  const [historyTimeContext, setHistoryTimeContext] = useState<TimeContext>({ timeRange: 'last-1h' });
-  const [historyInterval, setHistoryInterval] = useState<(typeof HISTORY_MODES)[number]['value']>(false);
-  const [historyReloadToken, setHistoryReloadToken] = useState(0);
-  const [historyFullscreen, setHistoryFullscreen] = useState(false);
-  const [metricPayload, setMetricPayload] = useState<unknown>(null);
-  const [metricPayloadLoading, setMetricPayloadLoading] = useState(false);
-  const [metricPayloadError, setMetricPayloadError] = useState<string | null>(null);
-  const [metricCardPayloads, setMetricCardPayloads] = useState<Record<string, MetricCardPayloadState>>({});
-  const [metricReloadToken, setMetricReloadToken] = useState(0);
-  const [selectedMetricRowKey, setSelectedMetricRowKey] = useState<string | null>(null);
-  const [metricTableMode, setMetricTableMode] = useState<'table' | 'detail'>('table');
-  const [metricFullscreen, setMetricFullscreen] = useState(false);
-  const [currentTab, setCurrentTab] = useState<MonitorDetailConsoleTabKey>('realtime');
-  const [favoriteSurfaceMode, setFavoriteSurfaceMode] = useState<'realtime' | 'history'>('realtime');
-  const [refreshInterval, setRefreshInterval] = useState<number>(30);
-  const [refreshCountdown, setRefreshCountdown] = useState<number>(30);
-  const [favoriteReloadToken, setFavoriteReloadToken] = useState(0);
-  const [grafanaReloadToken, setGrafanaReloadToken] = useState(0);
-
-  const filteredMetrics = filterMonitorMetricCatalog(metrics, metricSearch);
-  const displayedMetrics = filteredMetrics.slice(0, 10);
-  const displayedMetricNames = displayedMetrics.map(metric => metric.name).filter(Boolean);
-  const displayedMetricNameKey = displayedMetricNames.join('\u0000');
-  const metricRows = buildMonitorMetricCatalogRows(displayedMetrics, t);
-  const selectedMetric = filteredMetrics.find(metric => metric.name === selectedMetricKey) ?? filteredMetrics[0] ?? null;
-  const filteredHistoryMetrics = filterMonitorHistoryMetricCatalog(historyMetrics, historyMetricSearch);
-  const historyRows = buildMonitorHistoryMetricRows(filteredHistoryMetrics, t);
-  const selectedHistoryMetric = filteredHistoryMetrics.find(item => `${item.metrics}:${item.metric}` === selectedHistoryMetricKey) ?? filteredHistoryMetrics[0] ?? null;
-  const historySeriesRows = filterMonitorHistorySeriesRows(buildMonitorHistorySeriesRows(historyPayload, t, formatTime), historySeriesSearch);
-  const favoriteJumpRows = buildMonitorFavoriteJumpRows(favoriteNames, metrics, historyMetrics, t);
-  const [selectedFavoriteKey, setSelectedFavoriteKey] = useState<string | null>(null);
-
-  function resolveHistoryFavoriteToken(item: MonitorHistoryMetricCatalogItem) {
-    const fullPath = `${item.metrics}.${item.metric}`;
-    return favoriteNames.find(name => name === fullPath || name === item.metrics || name === item.metric) ?? null;
-  }
-
-  useEffect(() => {
-    setGrafanaState(grafana ?? { enabled: false });
-  }, [grafana]);
-
-  useEffect(() => {
-    setFavoriteNames(favoriteMetrics);
-  }, [favoriteMetrics]);
-
-  useEffect(() => {
-    let cancelled = false;
-    loadMonitorFavoriteMetrics(apiMessageGet as any, String(monitor.id))
-      .then(result => {
-        if (!cancelled) {
-          setFavoriteNames(result || []);
-        }
-      })
-      .catch(() => {
-        // Keep the current optimistic/local favorites state if refresh fails.
-      });
-
-    return () => {
-      cancelled = true;
-    };
-  }, [favoriteMetrics, favoriteReloadToken, monitor.id]);
-
-  useEffect(() => {
-    let cancelled = false;
-    apiMessageGet<GrafanaDashboard | null>(buildMonitorGrafanaUrl(String(monitor.id)))
-      .then(result => {
-        if (!cancelled) {
-          setGrafanaState(result ?? { enabled: false });
-        }
-      })
-      .catch(() => {
-        if (!cancelled) {
-          setGrafanaState({ enabled: false });
-        }
-      });
-
-    return () => {
-      cancelled = true;
-    };
-  }, [grafana, grafanaReloadToken, monitor.id]);
-
-  useEffect(() => {
-    if (shouldFallbackFromGrafanaTab(grafanaState.enabled, currentTab)) {
-      setCurrentTab('realtime');
-    }
-  }, [currentTab, grafanaState.enabled]);
-
-  useEffect(() => {
-    setSelectedMetricKey(prev => (prev && filteredMetrics.some(metric => metric.name === prev) ? prev : filteredMetrics[0]?.name || null));
-  }, [filteredMetrics, metricSearch, metrics]);
-
-  useEffect(() => {
-    const metricNames = displayedMetricNameKey ? displayedMetricNameKey.split('\u0000').filter(Boolean) : [];
-    if (metricNames.length === 0) {
-      setMetricCardPayloads({});
-      return;
-    }
-
-    let cancelled = false;
-    setMetricCardPayloads(previous => {
-      const next: Record<string, MetricCardPayloadState> = {};
-      metricNames.forEach(name => {
-        next[name] = {
-          payload: previous[name]?.payload ?? null,
-          loading: true,
-          error: null
-        };
-      });
-      return next;
-    });
-
-    Promise.all(
-      metricNames.map(name =>
-        loadMonitorRealtimeMetricData(apiMessageGet, String(monitor.id), name)
-          .then(payload => ({ name, payload: payload as MonitorRealtimeMetricData | null, error: null }))
-          .catch(error => ({
-            name,
-            payload: null,
-            error: error instanceof Error ? error.message : t('common.load-failed')
-          }))
-      )
-    ).then(results => {
-      if (cancelled) return;
-      setMetricCardPayloads(() => {
-        const next: Record<string, MetricCardPayloadState> = {};
-        results.forEach(result => {
-          next[result.name] = {
-            payload: result.payload,
-            loading: false,
-            error: result.error
-          };
-        });
-        return next;
-      });
-    });
-
-    return () => {
-      cancelled = true;
-    };
-  }, [displayedMetricNameKey, metricReloadToken, monitor.id, t]);
-
-  useEffect(() => {
-    let cancelled = false;
-    setHistoryLoading(true);
-    setHistoryError(null);
-    loadMonitorHistoryMetricCatalog(apiMessageGet, monitor)
-      .then(rows => {
-        if (!cancelled) {
-          setHistoryMetrics(rows);
-          setHistoryError(null);
-        }
-      })
-      .catch(error => {
-        if (!cancelled) {
-          setHistoryMetrics([]);
-          setHistoryError(error instanceof Error ? error.message : t('common.load-failed'));
-        }
-      })
-      .finally(() => {
-        if (!cancelled) {
-          setHistoryLoading(false);
-        }
-      });
-    return () => {
-      cancelled = true;
-    };
-  }, [monitor, t]);
-
-  useEffect(() => {
-    setSelectedHistoryMetricKey(prev =>
-      prev && filteredHistoryMetrics.some(item => `${item.metrics}:${item.metric}` === prev)
-        ? prev
-        : filteredHistoryMetrics[0]
-          ? `${filteredHistoryMetrics[0].metrics}:${filteredHistoryMetrics[0].metric}`
-          : null
-    );
-  }, [filteredHistoryMetrics, historyMetricSearch, historyMetrics]);
-
-  useEffect(() => {
-    const seriesKeys = historySeriesRows.map(row => row.key);
-    setSelectedHistorySeriesKey(prev => (prev && seriesKeys.includes(prev) ? prev : seriesKeys[0] || null));
-  }, [historyPayload, historySeriesRows, historySeriesSearch]);
-
-  useEffect(() => {
-    if (!selectedHistorySeriesKey || !historyPayload?.values?.[selectedHistorySeriesKey]?.length) {
-      setSelectedHistoryPointIndex(null);
-      return;
-    }
-
-    const values = historyPayload.values[selectedHistorySeriesKey];
-    setSelectedHistoryPointIndex(prev => (prev != null && values[prev] ? prev : values.length - 1));
-  }, [historyPayload, selectedHistorySeriesKey]);
-
-  useEffect(() => {
-    setSelectedFavoriteKey(prev => resolveActiveFavoriteRow(favoriteJumpRows, prev)?.key ?? null);
-  }, [favoriteJumpRows]);
-
-  useEffect(() => {
-    setFavoriteSurfaceMode(current => resolveFavoriteSurfaceMode(current, favoriteJumpRows, selectedFavoriteKey));
-  }, [favoriteJumpRows, selectedFavoriteKey]);
-
-  useEffect(() => {
-    const row = resolveActiveFavoriteRow(favoriteJumpRows, selectedFavoriteKey);
-    if (!row) return;
-    const target = resolveFavoriteJumpTarget(row);
-
-    setFavoriteSurfaceMode(row.targetKind);
-
-    if (row.targetKind === 'realtime') {
-      setSelectedMetricKey(current => (current === target.targetKey ? current : target.targetKey));
-      return;
-    }
-
-    setSelectedHistoryMetricKey(current => (current === target.targetKey ? current : target.targetKey));
-  }, [favoriteJumpRows, selectedFavoriteKey]);
-
-  useEffect(() => {
-    if (!selectedHistoryMetric) {
-      setHistoryPayload(null);
-      setHistoryPayloadError(null);
-      return;
-    }
-    let cancelled = false;
-    setHistoryPayloadLoading(true);
-    setHistoryPayloadError(null);
-    loadMonitorHistoryMetricData(apiMessageGet, monitor, selectedHistoryMetric, {
-      history: historyWindow,
-      interval: historyInterval,
-      start: historyTimeContext.start,
-      end: historyTimeContext.end
-    })
-      .then(result => {
-        if (!cancelled) {
-          setHistoryPayload(result);
-          setHistoryPayloadError(null);
-        }
-      })
-      .catch(error => {
-        if (!cancelled) {
-          setHistoryPayload(null);
-          setHistoryPayloadError(error instanceof Error ? error.message : t('common.load-failed'));
-        }
-      })
-      .finally(() => {
-        if (!cancelled) {
-          setHistoryPayloadLoading(false);
-        }
-      });
-    return () => {
-      cancelled = true;
-    };
-  }, [historyInterval, historyReloadToken, historyTimeContext.end, historyTimeContext.start, historyWindow, monitor, selectedHistoryMetric, t]);
-
-  useEffect(() => {
-    if (currentTab !== 'history') return;
-
-    const chartItems = filterMonitorHistoryMetricCatalog(historyMetrics, historyMetricSearch).slice(0, 6);
-    if (chartItems.length === 0) {
-      setHistoryChartPayloads({});
-      setHistoryChartLoadingKeys([]);
-      setHistoryChartErrors({});
-      return;
-    }
-
-    let cancelled = false;
-    const chartKeys = chartItems.map(item => `${item.metrics}:${item.metric}`);
-    setHistoryChartLoadingKeys(chartKeys);
-    setHistoryChartErrors({});
-
-    Promise.all(
-      chartItems.map(item =>
-        loadMonitorHistoryMetricData(apiMessageGet, monitor, item, {
-          history: historyWindow,
-          interval: historyInterval,
-          start: historyTimeContext.start,
-          end: historyTimeContext.end
-        })
-          .then(payload => ({ key: `${item.metrics}:${item.metric}`, payload }))
-          .catch(error => ({
-            key: `${item.metrics}:${item.metric}`,
-            error: error instanceof Error ? error.message : t('common.load-failed')
-          }))
-      )
-    ).then(results => {
-      if (cancelled) return;
-
-      const nextPayloads: Record<string, MonitorHistoryData | null> = {};
-      const nextErrors: Record<string, string> = {};
-      results.forEach(result => {
-        if ('payload' in result) {
-          nextPayloads[result.key] = result.payload;
-          return;
-        }
-        nextPayloads[result.key] = null;
-        nextErrors[result.key] = result.error;
-      });
-      setHistoryChartPayloads(nextPayloads);
-      setHistoryChartErrors(nextErrors);
-      setHistoryChartLoadingKeys([]);
-    });
-
-    return () => {
-      cancelled = true;
-    };
-  }, [currentTab, historyInterval, historyMetricSearch, historyMetrics, historyReloadToken, historyTimeContext.end, historyTimeContext.start, historyWindow, monitor, t]);
-
-  useEffect(() => {
-    if (!selectedMetric?.name) {
-      setMetricPayload(null);
-      setMetricPayloadError(null);
-      return;
-    }
-    let cancelled = false;
-    setMetricPayloadLoading(true);
-    setMetricPayloadError(null);
-    loadMonitorRealtimeMetricData(apiMessageGet, String(monitor.id), selectedMetric.name)
-      .then(result => {
-        if (!cancelled) {
-          setMetricPayload(result);
-          setMetricPayloadError(null);
-        }
-      })
-      .catch(error => {
-        if (!cancelled) {
-          setMetricPayload(null);
-          setMetricPayloadError(error instanceof Error ? error.message : t('common.load-failed'));
-        }
-      })
-      .finally(() => {
-        if (!cancelled) {
-          setMetricPayloadLoading(false);
-        }
-      });
-    return () => {
-      cancelled = true;
-    };
-  }, [metricReloadToken, monitor.id, selectedMetric?.name, t]);
-
-  useEffect(() => {
-    const tableRows = buildMonitorMetricTableMatrix(metricPayload as any, t).rows;
-    setSelectedMetricRowKey(prev => (prev && tableRows.some(row => row.key === prev) ? prev : tableRows[0]?.key || null));
-  }, [metricPayload, t]);
-
-  useEffect(() => {
-    setMetricTableMode(prev => resolveMonitorMetricTableMode(metricPayload as any, prev));
-  }, [metricPayload]);
-
-  const handleRefresh = useCallback(() => {
-    const refreshTarget = resolveMonitorDetailRefreshTarget(currentTab);
-    if (refreshInterval > 0) {
-      setRefreshCountdown(refreshInterval);
-    }
-    if (refreshTarget === 'history') {
-      setHistoryReloadToken(prev => prev + 1);
-      return;
-    }
-    if (refreshTarget === 'favorites') {
-      setFavoriteReloadToken(prev => prev + 1);
-      return;
-    }
-    if (refreshTarget === 'grafana') {
-      setGrafanaReloadToken(prev => prev + 1);
-      return;
-    }
-    setMetricReloadToken(prev => prev + 1);
-  }, [currentTab, refreshInterval]);
-
-  useEffect(() => {
-    if (refreshInterval <= 0) return;
-
-    const timer = window.setInterval(() => {
-      handleRefresh();
-    }, refreshInterval * 1000);
-
-    return () => {
-      window.clearInterval(timer);
-    };
-  }, [handleRefresh, refreshInterval]);
-
-  useEffect(() => {
-    if (refreshInterval <= 0) {
-      setRefreshCountdown(-1);
-      return;
-    }
-
-    setRefreshCountdown(refreshInterval);
-    const timer = window.setInterval(() => {
-      setRefreshCountdown(current => (current <= 1 ? refreshInterval : current - 1));
-    }, 1000);
-
-    return () => {
-      window.clearInterval(timer);
-    };
-  }, [refreshInterval]);
-
-  async function handleToggleFavorite(metricName: string) {
-    try {
-      if (favoriteNames.includes(metricName)) {
-        await removeMonitorFavorite(apiMessageDelete as any, String(monitor.id), metricName);
-        setFavoriteNames(prev => prev.filter(item => item !== metricName));
-      } else {
-        await addMonitorFavorite(apiMessagePost as any, String(monitor.id), metricName);
-        setFavoriteNames(prev => [...prev, metricName]);
-      }
-      setFavoriteMessage(t('common.save-success'));
-      setFavoriteError(null);
-    } catch (error) {
-      setFavoriteError(error instanceof Error ? error.message : t('common.save-failed'));
-    }
-  }
-
-  async function handleToggleHistoryFavorite(item: MonitorHistoryMetricCatalogItem) {
-    const favoriteToken = resolveHistoryFavoriteToken(item);
-    const nextToken = `${item.metrics}.${item.metric}`;
-    try {
-      if (favoriteToken) {
-        await removeMonitorFavorite(apiMessageDelete as any, String(monitor.id), favoriteToken);
-        setFavoriteNames(prev => prev.filter(name => name !== favoriteToken));
-      } else {
-        await addMonitorFavorite(apiMessagePost as any, String(monitor.id), nextToken);
-        setFavoriteNames(prev => [...prev, nextToken]);
-      }
-      setFavoriteMessage(t('common.save-success'));
-      setFavoriteError(null);
-    } catch (error) {
-      setFavoriteError(error instanceof Error ? error.message : t('common.save-failed'));
-    }
-  }
-
-  function handlePreviewFavorite(key: string) {
-    setSelectedFavoriteKey(key);
-    const row = favoriteJumpRows.find(item => item.key === key);
-    if (!row) return;
-    setFavoriteSurfaceMode(row.targetKind);
-    if (row.targetKind === 'realtime') {
-      const target = resolveFavoriteJumpTarget(row);
-      setMetricPayload(null);
-      setMetricPayloadError(null);
-      setSelectedMetricRowKey(null);
-      setSelectedMetricKey(target.targetKey);
-    } else {
-      const target = resolveFavoriteJumpTarget(row);
-      setHistoryPayload(null);
-      setHistoryPayloadError(null);
-      setSelectedHistorySeriesKey(null);
-      setSelectedHistoryPointIndex(null);
-      setSelectedHistoryMetricKey(target.targetKey);
-    }
-  }
-
-  function handleSelectFavorite(key: string) {
-    handlePreviewFavorite(key);
-    const row = favoriteJumpRows.find(item => item.key === key);
-    if (!row) return;
-    setCurrentTab(resolveFavoriteJumpTarget(row).tab);
-  }
-
-  function handleSetFavoriteSurfaceMode(mode: 'realtime' | 'history') {
-    setFavoriteSurfaceMode(mode);
-    const rows = favoriteJumpRows.filter(row => row.targetKind === mode);
-    const nextRow = rows.find(row => row.key === selectedFavoriteKey) ?? rows[0];
-    if (nextRow) {
-      handlePreviewFavorite(nextRow.key);
-    }
-  }
-
-  function handleApplyHistoryTimeContext(context: TimeContext) {
-    const sanitized = sanitizeTimeContext(context);
-    const nextWindow = timeRangeToHistoryWindow(sanitized.timeRange);
-    if (nextWindow) {
-      setHistoryWindow(nextWindow as (typeof HISTORY_WINDOWS)[number]['value']);
-    }
-    setHistoryTimeContext(
-      sanitized.start && sanitized.end
-        ? sanitized
-        : { timeRange: sanitized.timeRange || historyWindowToTimeRange(historyWindow) }
-    );
-  }
-
-  function handleHistoryWindowChange(value: string) {
-    const nextWindow = value as (typeof HISTORY_WINDOWS)[number]['value'];
-    setHistoryWindow(nextWindow);
-    setHistoryTimeContext({ timeRange: historyWindowToTimeRange(nextWindow) });
-  }
-
-  async function handleRemoveFavoriteToken(token: string) {
-    try {
-      await removeMonitorFavorite(apiMessageDelete as any, String(monitor.id), token);
-      setFavoriteNames(prev => prev.filter(name => name !== token));
-      setFavoriteMessage(t('common.save-success'));
-      setFavoriteError(null);
-    } catch (error) {
-      setFavoriteError(error instanceof Error ? error.message : t('common.save-failed'));
-    }
-  }
-
-  async function handleDeleteGrafanaDashboard() {
-    try {
-      await deleteMonitorGrafanaDashboard(apiMessageDelete as any, String(monitor.id));
-      setGrafanaState({ enabled: false });
-      setGrafanaMessage(t('common.delete-success'));
-      setGrafanaError(null);
-    } catch (error) {
-      setGrafanaError(error instanceof Error ? error.message : t('common.delete-failed'));
-    }
-  }
-
-  const backHref = buildMonitorListReturnHref(navigationContext);
-  const editHref = buildMonitorEditHref(monitor.id, navigationContext);
-  const detailHref = buildMonitorDetailHref(monitor.id, navigationContext);
-  const signalHandoffLinks = buildMonitorSignalHandoffLinks(monitor, navigationContext, detailHref);
-  const helpHref = navigationContext.app || monitor.app ? `https://hertzbeat.apache.org/docs/help/${encodeURIComponent(navigationContext.app || monitor.app)}` : null;
-  const sections = MonitorDetailSections({
-    monitor,
-    editHref,
-    params,
-    selectedMetric,
-    metricSearch,
-    metricRows,
-    metricCardPayloads,
-    metrics,
-    favoriteNames,
-    favoriteJumpRows,
-    selectedFavoriteKey,
-    favoriteSurfaceMode,
-    selectedMetricKey,
-    metricPayload,
-    metricPayloadLoading,
-    metricPayloadError,
-    selectedMetricRowKey,
-    metricTableMode,
-    metricFullscreen,
-    historyLoading,
-    historyError,
-    historyMetrics,
-    historyMetricSearch,
-    historyRows,
-    selectedHistoryMetricKey,
-    historySeriesSearch,
-    historySeriesRows,
-    selectedHistorySeriesKey,
-    selectedHistoryPointIndex,
-    historyPayload,
-    historyChartPayloads,
-    historyChartLoadingKeys,
-    historyChartErrors,
-    historyPayloadLoading,
-    historyPayloadError,
-    historyInterval,
-    historyWindow,
-    historyTimeContext,
-    historyWindows: HISTORY_WINDOWS.map(option => ({ value: option.value, label: t(option.labelKey) })),
-    historyModes: HISTORY_MODES.map(option => ({ value: option.value, label: t(option.labelKey) })),
-    historyFullscreen,
-    grafanaState,
-    grafanaMessage,
-    grafanaError,
-    favoriteMessage,
-    favoriteError,
-    onMetricSearchChange: setMetricSearch,
-    onSelectMetric: setSelectedMetricKey,
-    onToggleFavorite: handleToggleFavorite,
-    onSelectMetricRow: setSelectedMetricRowKey,
-    onMetricModeChange: setMetricTableMode,
-    onMetricRefresh: () => setMetricReloadToken(prev => prev + 1),
-    onMetricFullscreenToggle: () => setMetricFullscreen(prev => !prev),
-    onHistoryMetricSearchChange: setHistoryMetricSearch,
-    onSelectHistoryMetric: setSelectedHistoryMetricKey,
-    onToggleHistoryFavorite: handleToggleHistoryFavorite,
-    onHistorySeriesSearchChange: setHistorySeriesSearch,
-    onSelectHistorySeries: setSelectedHistorySeriesKey,
-    onSelectHistoryPoint: setSelectedHistoryPointIndex,
-    onHistoryRefresh: () => setHistoryReloadToken(prev => prev + 1),
-    onHistoryWindowChange: handleHistoryWindowChange,
-    onHistoryTimeContextApply: handleApplyHistoryTimeContext,
-    onApplyHistoryChartZoomTimeRange: handleApplyHistoryTimeContext,
-    onHistoryModeChange: setHistoryInterval,
-    onHistoryFullscreenToggle: () => setHistoryFullscreen(prev => !prev),
-    onSelectFavorite: handleSelectFavorite,
-    onSetFavoriteSurfaceMode: handleSetFavoriteSurfaceMode,
-    onRemoveFavoriteToken: handleRemoveFavoriteToken,
-    onDeleteGrafanaDashboard: handleDeleteGrafanaDashboard,
-    t
-  });
-
-  return (
-    <MonitorDetailConsole
-      monitor={monitor}
-      currentTab={currentTab}
-      refreshInterval={refreshInterval}
-      refreshCountdown={refreshCountdown}
-      backHref={backHref}
-      editHref={editHref}
-      helpHref={helpHref}
-      appHref={navigationContext.app ? `/setting/define?app=${encodeURIComponent(navigationContext.app)}` : null}
-      grafanaUrl={grafanaState.enabled ? grafanaState.url : null}
-      navigationContext={navigationContext}
-      signalHandoffLinks={signalHandoffLinks}
-      onSelectTab={setCurrentTab}
-      onSelectRefreshInterval={setRefreshInterval}
-      onRefresh={handleRefresh}
-      realtimeContent={sections.realtimeContent}
-      historyContent={sections.historyContent}
-      favoritesContent={sections.favoritesContent}
-      grafanaContent={sections.grafanaContent}
-      t={t}
-    />
-  );
-}
-
-export default function MonitorDetailPage({ params }: { params: Promise<{ monitorId: string }> }) {
-  const { t } = useI18n();
-  const load = useCallback(async (): Promise<MonitorDetailData> => {
-    const resolved = await params;
-    return loadMonitorDetailBundle(apiMessageGet, resolved.monitorId);
-  }, [params]);
-
-  return (
-    <ClientWorkbench load={load} loadingCopy={t('monitor.detail.loading')}>
-      {data => <MonitorDetailWorkbench data={data} />}
-    </ClientWorkbench>
-  );
+export default async function MonitorDetailRoutePage({ params }: { params: Promise<{ monitorId: string }> }) {
+  const resolved = await params;
+  return <MonitorDetailPage monitorId={resolved.monitorId} />;
 }
diff --git a/web-next/components/monitor-detail/history-line-chart.test.tsx b/web-next/components/monitor-detail/history-line-chart.test.tsx
index a83f40e9c7..b0a4678df7 100644
--- a/web-next/components/monitor-detail/history-line-chart.test.tsx
+++ b/web-next/components/monitor-detail/history-line-chart.test.tsx
@@ -6,18 +6,41 @@ import { describe, expect, it } from 'vitest';
 import { HistoryLineChart } from './history-line-chart';
 
 describe('history line chart', () => {
-  it('moves chart quick actions and series toggles onto observability pill buttons', () => {
+  it('routes chart quick actions and series toggles through shared HertzBeat UI buttons', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/history-line-chart.tsx'), 'utf8');
 
-    expect(source).toContain('ObservabilityPillButton');
+    expect(source).toContain("from '@hertzbeat/ui'");
+    expect(source).toContain('HzActionGroup');
+    expect(source).toContain('HzButton');
+    expect(source).toContain('HzButtonLink');
+    expect(source).toContain('HzDetailRows');
+    expect(source).toContain('HzDataMetaText');
+    expect(source).toContain('layout="stack"');
+    expect(source).toContain('data-monitor-history-line-action-stack-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-monitor-history-line-action-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-monitor-history-line-control-owner="hertzbeat-ui-button"');
+    expect(source).toContain('data-monitor-history-download-owner="hertzbeat-ui-button-link"');
+    expect(source).toContain('data-monitor-history-line-point-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('data-monitor-history-line-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(source).not.toContain('text-[var(--ops-text-tertiary)]');
+    expect(source).not.toContain('ObservabilityPillButton');
+    expect(source).not.toContain('data-hz-ui="button"');
+    expect(source).not.toContain('bg-[var(--hz-ui-action-bg)]');
+    expect(source).not.toContain('rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)]');
+    expect(source).not.toContain('className="flex flex-wrap gap-2 text-[11px] text-[var(--ops-text-secondary)]"');
+    expect(source).not.toContain('className="flex flex-wrap gap-3 text-[11px] text-[var(--ops-text-secondary)]"');
+    expect(source).not.toContain('className="flex flex-wrap items-center gap-2 text-xs text-[var(--ops-text-secondary)]"');
+    expect(source).not.toContain('className="space-y-2"');
     expect(source).not.toContain('className="inline-flex items-center rounded-full border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-2.5 py-1 transition hover:border-[var(--ops-primary)] hover:bg-[var(--ops-surface-raised)] disabled:cursor-not-allowed disabled:opacity-40"');
     expect(source).not.toContain('className="inline-flex items-center gap-2 rounded-full border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-2.5 py-1 transition hover:border-[var(--ops-primary)] hover:bg-[var(--ops-surface-raised)]"');
   });
 
-  it('routes the outer chart shell through the observability inset-panel owner', () => {
+  it('routes the outer chart shell through the shared HertzBeat UI chart surface', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/history-line-chart.tsx'), 'utf8');
 
-    expect(source).toContain('ObservabilityInsetPanel');
+    expect(source).toContain('HzChartSurface');
+    expect(source).toContain('data-monitor-history-line-owner="hertzbeat-ui-chart-surface"');
+    expect(source).not.toContain('ObservabilityInsetPanel');
     expect(source).not.toContain('className="rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] p-3"');
     expect(source).not.toContain("from '../workbench/primitives'");
   });
@@ -50,6 +73,8 @@ describe('history line chart', () => {
     expect(html).toContain('monitor-history-aggregated.svg');
     expect(html).toContain('data:image/svg+xml;charset=utf-8,');
     expect(html).toContain('Click or hover the chart to inspect the nearest point');
+    expect(html).toContain('data-monitor-history-line-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-hz-ui="data-meta-text"');
   });
 
   it('renders aggregated chart quick presets for primary-only and full-stat views', () => {
diff --git a/web-next/components/monitor-detail/history-line-chart.tsx b/web-next/components/monitor-detail/history-line-chart.tsx
index 2e0fc6f8d2..98cc1cc8e7 100644
--- a/web-next/components/monitor-detail/history-line-chart.tsx
+++ b/web-next/components/monitor-detail/history-line-chart.tsx
@@ -1,7 +1,7 @@
 'use client';
 
 import React, { useState } from 'react';
-import { ObservabilityInsetPanel, ObservabilityPillButton } from '../observability';
+import { HzActionGroup, HzButton, HzButtonLink, HzChartSurface, HzDataMetaText, HzDetailRows } from '@hertzbeat/ui';
 import {
   buildHistoryChartDownloadName,
   buildHistoryChartPoints,
@@ -52,6 +52,22 @@ export function HistoryLineChart({
         }))
         .filter(item => item.point);
   const activePoint = activePointIndex == null ? null : points[activePointIndex] || null;
+  const activeDetailRows = activePoint
+    ? [
+        {
+          key: 'active-point',
+          title: hoveredPointIndex != null ? 'hovered point' : 'selected point',
+          copy: activePoint.rawValue,
+          meta: activePoint.label
+        },
+        ...activeSeriesValues.map(item => ({
+          key: item.key,
+          title: item.label,
+          copy: item.point?.rawValue ?? '-',
+          meta: item.key
+        }))
+      ]
+    : [];
   const downloadUrl = buildHistoryChartSvgDataUrl(buildHistoryChartSvgDocument(visibleSeries, selectedPointIndex));
   const downloadName = buildHistoryChartDownloadName(aggregated);
   const primarySeriesKey = aggregated ? 'mean' : 'origin';
@@ -59,37 +75,61 @@ export function HistoryLineChart({
   const canShowAllStats = aggregated && resolvedVisibleSeriesKeys.length !== seriesKeys.length;
 
   return (
-    <ObservabilityInsetPanel>
+    <HzChartSurface
+      heading={aggregated ? 'Aggregated history' : 'History'}
+      variant="inline"
+      data-monitor-history-line-owner="hertzbeat-ui-chart-surface"
+      data-monitor-history-line-legacy-svg="true"
+      contentClassName="px-3 py-2"
+    >
       <div className="mb-3 flex flex-wrap items-center justify-between gap-3">
         {aggregated && series.length > 1 ? (
-          <div className="space-y-2">
+          <HzActionGroup
+            layout="stack"
+            density="inline"
+            data-monitor-history-line-action-stack-owner="hertzbeat-ui-action-group"
+          >
             {onVisibleSeriesKeysChange ? (
-              <div className="flex flex-wrap gap-2 text-[11px] text-[var(--ops-text-secondary)]">
-                <ObservabilityPillButton
+              <HzActionGroup
+                density="inline"
+                data-monitor-history-line-action-owner="hertzbeat-ui-action-group"
+                data-monitor-history-line-action="quick-presets"
+              >
+                <HzButton
                   type="button"
-                  size="compact"
+                  size="xs"
+                  intent="ghost"
+                  data-monitor-history-line-control-owner="hertzbeat-ui-button"
                   disabled={!canFocusPrimarySeries}
                   onClick={() => onVisibleSeriesKeysChange([primarySeriesKey])}
                 >
                   Primary only
-                </ObservabilityPillButton>
-                <ObservabilityPillButton
+                </HzButton>
+                <HzButton
                   type="button"
-                  size="compact"
+                  size="xs"
+                  intent="ghost"
+                  data-monitor-history-line-control-owner="hertzbeat-ui-button"
                   disabled={!canShowAllStats}
                   onClick={() => onVisibleSeriesKeysChange(seriesKeys)}
                 >
                   Show all stats
-                </ObservabilityPillButton>
-              </div>
+                </HzButton>
+              </HzActionGroup>
             ) : null}
-            <div className="flex flex-wrap gap-3 text-[11px] text-[var(--ops-text-secondary)]">
+            <HzActionGroup
+              density="inline"
+              data-monitor-history-line-action-owner="hertzbeat-ui-action-group"
+              data-monitor-history-line-action="series-visibility"
+            >
               {series.map(item => (
-                <ObservabilityPillButton
+                <HzButton
                   key={item.key}
                   type="button"
-                  size="compact"
-                  active={resolvedVisibleSeriesKeys.includes(item.key)}
+                  size="xs"
+                  intent={resolvedVisibleSeriesKeys.includes(item.key) ? 'secondary' : 'ghost'}
+                  data-monitor-history-line-control-owner="hertzbeat-ui-button"
+                  data-series-selected={resolvedVisibleSeriesKeys.includes(item.key) ? 'true' : 'false'}
                   className="gap-2"
                   style={{ opacity: resolvedVisibleSeriesKeys.includes(item.key) ? 1 : 0.45 }}
                   onClick={() => {
@@ -100,26 +140,31 @@ export function HistoryLineChart({
                 >
                   <span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: item.color }} />
                   <span>{item.label}</span>
-                </ObservabilityPillButton>
-              ))}
-            </div>
-          </div>
+                </HzButton>
+                ))}
+              </HzActionGroup>
+          </HzActionGroup>
         ) : (
           <div />
         )}
-        <ObservabilityPillButton
-          as="a"
-          size="compact"
+        <HzButtonLink
           href={downloadUrl}
           download={downloadName}
+          size="xs"
+          intent="secondary"
+          data-monitor-history-line-control-owner="hertzbeat-ui-button"
+          data-monitor-history-download-owner="hertzbeat-ui-button-link"
         >
           Download SVG
-        </ObservabilityPillButton>
+        </HzButtonLink>
       </div>
       {onSelectPoint ? (
-        <div className="mb-2 text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">
+        <HzDataMetaText
+          className="mb-2 block tracking-[0.16em]"
+          data-monitor-history-line-meta-owner="hertzbeat-ui-data-meta-text"
+        >
           Click or hover the chart to inspect the nearest point
-        </div>
+        </HzDataMetaText>
       ) : null}
       <svg viewBox="0 0 100 100" className="h-40 w-full overflow-visible">
         <line x1="0" y1="100" x2="100" y2="100" stroke="rgba(255,255,255,0.12)" strokeWidth="1" />
@@ -183,52 +228,48 @@ export function HistoryLineChart({
       {activePoint ? (
         <div className="mt-3 space-y-2">
           {onSelectPoint ? (
-            <div className="flex flex-wrap items-center gap-2 text-xs text-[var(--ops-text-secondary)]">
-              <ObservabilityPillButton
+            <HzActionGroup
+              density="inline"
+              data-monitor-history-line-action-owner="hertzbeat-ui-action-group"
+              data-monitor-history-line-action="point-navigation"
+            >
+              <HzButton
                 type="button"
-                size="compact"
+                size="xs"
+                intent="ghost"
+                data-monitor-history-line-control-owner="hertzbeat-ui-button"
                 disabled={activePointIndex == null || activePointIndex <= 0}
                 onClick={() => activePointIndex != null && onSelectPoint(Math.max(0, activePointIndex - 1))}
               >
                 Previous
-              </ObservabilityPillButton>
+              </HzButton>
               <span>{`Point ${(activePointIndex ?? 0) + 1} / ${points.length}`}</span>
-              <ObservabilityPillButton
+              <HzButton
                 type="button"
-                size="compact"
+                size="xs"
+                intent="ghost"
+                data-monitor-history-line-control-owner="hertzbeat-ui-button"
                 disabled={activePointIndex == null || activePointIndex >= points.length - 1}
                 onClick={() => activePointIndex != null && onSelectPoint(Math.min(points.length - 1, activePointIndex + 1))}
               >
                 Next
-              </ObservabilityPillButton>
-            </div>
-          ) : null}
-          <div className="rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-3 py-2 text-xs text-[var(--ops-text-secondary)]">
-            <span className="font-medium text-[var(--ops-text-primary)]">{hoveredPointIndex != null ? 'hovered point' : 'selected point'}</span>
-            {' · '}
-            <span>{activePoint.label}</span>
-            {' · '}
-            <span>{activePoint.rawValue}</span>
-          </div>
-          {activeSeriesValues.length > 0 ? (
-            <div className="grid gap-2 sm:grid-cols-3">
-              {activeSeriesValues.map(item => (
-                <div key={item.key} className="rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-3 py-2 text-xs text-[var(--ops-text-secondary)]">
-                  <div className="inline-flex items-center gap-2">
-                    <span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: item.color }} />
-                    <span className="font-medium text-[var(--ops-text-primary)]">{item.label}</span>
-                  </div>
-                  <div className="mt-1">{item.point?.rawValue}</div>
-                </div>
-              ))}
-            </div>
+              </HzButton>
+            </HzActionGroup>
           ) : null}
+          <HzDetailRows
+            rows={activeDetailRows}
+            data-monitor-history-line-point-owner="hertzbeat-ui-detail-rows"
+          />
         </div>
       ) : null}
-      <div className="mt-2 flex flex-wrap justify-between gap-2 text-[11px] text-[var(--ops-text-tertiary)]">
-        <span>{points[0]?.label}</span>
-        <span>{points[points.length - 1]?.label}</span>
+      <div className="mt-2 flex flex-wrap justify-between gap-2">
+        <HzDataMetaText data-monitor-history-line-meta-owner="hertzbeat-ui-data-meta-text">
+          {points[0]?.label}
+        </HzDataMetaText>
+        <HzDataMetaText data-monitor-history-line-meta-owner="hertzbeat-ui-data-meta-text">
+          {points[points.length - 1]?.label}
+        </HzDataMetaText>
       </div>
-    </ObservabilityInsetPanel>
+    </HzChartSurface>
   );
 }
diff --git a/web-next/components/monitor-detail/monitor-detail-console.test.tsx b/web-next/components/monitor-detail/monitor-detail-console.test.tsx
index d6855bda19..2c8fd32a12 100644
--- a/web-next/components/monitor-detail/monitor-detail-console.test.tsx
+++ b/web-next/components/monitor-detail/monitor-detail-console.test.tsx
@@ -52,7 +52,9 @@ describe('monitor detail console', () => {
     expect(html).toContain('监控实时数据详情');
     expect(html).toContain('data-monitor-detail-header-mode="breadcrumb-only"');
     expect(html).toContain('data-monitor-detail-reference-source="apache-hertzbeat-master-monitor-detail"');
-    expect(html).toContain('data-monitor-detail-app-chip="breadcrumb"');
+    expect(html).toContain('data-monitor-detail-context-mark="breadcrumb"');
+    expect(html).toContain('data-monitor-detail-context-mark-owner="hertzbeat-ui-inline-context-mark"');
+    expect(html).not.toContain('data-monitor-detail-app-chip="breadcrumb"');
     expect(html).not.toContain('监控工作台');
     expect(html).not.toContain('Monitors');
     expect(html).not.toContain('Monitor detail');
@@ -118,36 +120,68 @@ describe('monitor detail console', () => {
     expect(html).toContain('data-monitor-detail-tab-icon="realtime"');
     expect(html).toContain('data-monitor-detail-tab-icon="history"');
     expect(html).toContain('data-monitor-detail-tab-icon="favorites"');
+    expect(html).toContain('data-hz-ui="monitor-detail-tab-label"');
+    expect(html).toContain('data-monitor-detail-tab-label-owner="hertzbeat-ui-detail-tab-label"');
+    expect(html).toContain('data-monitor-detail-tab-icon-owner="hertzbeat-ui-detail-tab-label"');
     expect(html).not.toContain('Monitor ID');
     expect(html).not.toContain('Status</div>');
     expect(html).not.toContain('Updated</div>');
-    expect(html).toContain('10s');
-    expect(html).toContain('30s');
-    expect(html).toContain('300s');
-    expect(html).toContain('Off');
+    expect(html).toContain('Set Auto Refresh For 10 s');
+    expect(html).toContain('Set Auto Refresh For 30 s');
+    expect(html).toContain('Set Auto Refresh For 300 s');
+    expect(html).toContain('Close Auto Refresh');
+    expect(html).toContain('data-monitor-refresh-select-owner="hertzbeat-ui-select"');
+    expect(html).toContain('data-monitor-refresh-select="true"');
+    expect(html).toContain('data-hz-ui="select"');
+    expect(html).toContain('data-hz-ui="select-menu"');
+    expect(html).toContain('data-monitor-detail-refresh-options-contract="angular-config-refresh-copy"');
+    expect(html).toContain('data-monitor-detail-refresh-copy-contract="angular-deadline-label"');
+    expect(html).toContain('data-monitor-detail-countdown-reset-contract="angular-deadline-reset"');
+    expect(html).not.toContain('<select');
     expect(html).toContain('Refresh');
-    expect(html).toContain('Refresh soon 18s');
+    expect(html).toContain('Auto Refresh After 18 s');
     expect(html).not.toContain('Back to monitors');
     expect(html).not.toContain('Edit monitor');
-    expect(html).not.toContain('Help');
+    expect(html).toContain('Help');
+    expect(html).toContain('data-monitor-detail-tab-extra-contract="angular-refresh-help"');
+    expect(html).toContain('data-monitor-detail-tab-extra-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-monitor-detail-help-action="angular-docs-help"');
+    expect(html).toContain('data-monitor-detail-help-owner="hertzbeat-ui-icon-link"');
+    expect(html).toContain('data-monitor-detail-help-target="https://hertzbeat.apache.org/docs/help/http"');
+    expect(html).toContain('data-monitor-detail-list-return="angular-app-filter"');
+    expect(html).toContain('data-monitor-detail-list-return-target="/monitors?app=http"');
+    expect(html).toContain('data-hz-ui="icon-link"');
     expect(html).not.toContain('Open Grafana');
     expect(html).toContain('grafana stage');
     expect(html).toContain('/setting/define?app=http');
-    expect(html).not.toContain('https://hertzbeat.apache.org/docs/help/http');
+    expect(html).toContain('https://hertzbeat.apache.org/docs/help/http');
     expect(html).toMatch(/data-monitor-console-layout=\"angular-workbench\"/);
     expect(html).toMatch(/data-monitor-console-tone=\"operator-sheet\"/);
+    expect(html).toMatch(/data-hz-ui=\"monitor-detail-console-shell\"/);
+    expect(html).toMatch(/data-monitor-detail-console-shell-owner=\"hertzbeat-ui-detail-console-shell\"/);
+    expect(html).toMatch(/data-hz-ui=\"monitor-detail-workbench-frame\"/);
+    expect(html).toMatch(/data-monitor-workbench-stage-owner=\"hertzbeat-ui-detail-workbench-frame\"/);
     expect(html).toMatch(/data-monitor-detail-completion-guard=\"reported-angular-no-card-stack\"/);
     expect(html).toMatch(/data-monitor-detail-body-stack-state=\"single-workbench-table-rows\"/);
     expect(html).toMatch(/data-monitor-detail-completion-header=\"breadcrumb-only\"/);
     expect(html).toMatch(/data-monitor-first-viewport-rhythm=\"angular-tight\"/);
     expect(html).toMatch(/data-monitor-detail-header-mode=\"breadcrumb-only\"/);
     expect(html).toMatch(/data-monitor-detail-reference-source=\"apache-hertzbeat-master-monitor-detail\"/);
-    expect(html).toMatch(/data-monitor-detail-app-chip=\"breadcrumb\"/);
+    expect(html).toMatch(/data-monitor-detail-breadcrumb-owner=\"hertzbeat-ui-monitor-breadcrumb\"/);
+    expect(html).toMatch(/data-hz-ui=\"monitor-breadcrumb\"/);
+    expect(html).toMatch(/data-monitor-detail-context-mark=\"breadcrumb\"/);
+    expect(html).toMatch(/data-monitor-detail-context-mark-owner=\"hertzbeat-ui-inline-context-mark\"/);
+    expect(html).toMatch(/data-monitor-detail-app-definition-link=\"angular-monitor-app\"/);
+    expect(html).toMatch(/data-monitor-detail-app-definition-target=\"\/setting\/define\?app=http\"/);
+    expect(html).not.toMatch(/data-monitor-detail-app-chip=\"breadcrumb\"/);
+    expect(html).toMatch(/data-hz-ui=\"monitor-detail-tab-panel\"/);
     expect(html).toMatch(/data-monitor-console-tab-panel=\"true\"/);
-    expect(html).toMatch(/data-monitor-console-tab-panel-rhythm=\"angular-tight\"/);
+    expect(html).toMatch(/data-monitor-console-tab-panel-owner=\"hertzbeat-ui-detail-tab-panel\"/);
+    expect(html).toMatch(/data-monitor-console-tab-panel-rhythm=\"shared-tight\"/);
     expect(html).not.toMatch(/data-monitor-detail-return-action=\"true\"/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-detail-page-frame[^\"]*-mx-4[^\"]*space-y-2[^\"]*px-3[^\"]*py-3[^\"]*sm:-mx-6[^\"]*sm:px-3/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-detail-workbench-breadcrumb[^\"]*border-b[^\"]*pb-2/);
+    expect(html).toMatch(/class=\"[^\"]*-mx-4[^\"]*space-y-2[^\"]*px-3[^\"]*pb-3[^\"]*pt-0[^\"]*sm:-mx-6[^\"]*sm:px-3/);
+    expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-page-frame/);
+    expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-workbench-breadcrumb/);
     expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-workbench-header/);
     expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-workbench-header__main/);
     expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-workbench-header__chips/);
@@ -165,32 +199,70 @@ describe('monitor detail console', () => {
     expect(html).not.toMatch(/class=\"[^\"]*platform-context-chip-bar--dense/);
     expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-workbench-header__context-stack/);
     expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-workbench-strip/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-detail-workbench-layout[^\"]*overflow-visible[^\"]*bg-transparent/);
+    expect(html).toMatch(/class=\"[^\"]*overflow-visible[^\"]*bg-transparent/);
+    expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-workbench-layout/);
     expect(html).toMatch(/data-monitor-workbench-stage=\"angular-layout\"/);
     expect(html).toMatch(/data-monitor-workbench-stage-chrome=\"angular-tabset-direct\"/);
     expect(html).toMatch(/data-monitor-workbench-stage-rhythm=\"direct-tab-body\"/);
-    expect(html).toMatch(/data-monitor-tab-body-surface=\"angular-tab-content-direct\"/);
+    expect(html).toMatch(/data-monitor-tab-body-surface=\"hertzbeat-ui-detail-tab-panel\"/);
     expect(html).not.toMatch(/data-monitor-workbench-stage-chrome=\"single-sheet\"/);
     expect(html).not.toMatch(/data-monitor-tab-body-surface=\"unified-stage\"/);
-    expect(html).toMatch(/data-monitor-detail-tabset-type=\"angular-card\"/);
-    expect(html).toMatch(/data-observability-tabstrip-variant=\"card\"/);
-    expect(html).toMatch(/data-observability-tabstrip-card=\"angular-nz-card\"/);
-    expect(html).toMatch(/data-observability-tab-card=\"true\"/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-detail-workbench-tabs[^\"]*pb-2/);
+    expect(html).toMatch(/data-monitor-detail-tabset-type=\"bottom-underline-switch\"/);
+    expect(html).toMatch(/data-hz-ui=\"monitor-detail-tabs\"/);
+    expect(html).toMatch(/data-monitor-detail-tabs-owner=\"hertzbeat-ui-monitor-detail-tabs\"/);
+    expect(html).toMatch(/data-monitor-detail-tabs-variant=\"bottom-underline-switch\"/);
+    expect(html).toMatch(/data-monitor-detail-tabs-control-baseline=\"underline-28\"/);
+    expect(html).toMatch(/data-monitor-detail-tabs-family=\"top-tab-underline\"/);
+    expect(html).toMatch(/data-monitor-detail-tab-underline=\"true\"/);
+    expect(html).toMatch(/data-monitor-detail-tab-underline-selected=\"true\"/);
+    expect(html).toMatch(/data-monitor-detail-tab-control-baseline=\"underline-28\"/);
+    expect(html).toMatch(/data-hz-control-edge=\"bottom-underline\"/);
+    expect(html).toMatch(/data-monitor-detail-tab-visual-family=\"top-tab-underline\"/);
+    expect(html).toMatch(/class=\"[^\"]*h-7[^\"]*after:absolute/);
+    expect(html).not.toMatch(/class=\"[^\"]*min-h-10/);
+    expect(html).not.toMatch(/sm:self-end/);
+    expect(html).not.toMatch(/data-monitor-detail-tabs-variant=\"compact-toolbar-buttons\"/);
+    expect(html).not.toMatch(/data-monitor-detail-tab-toolbar-button=\"true\"/);
+    expect(html).not.toMatch(/data-monitor-detail-tab-accent-card=\"true\"/);
+    expect(html).not.toMatch(/data-monitor-detail-tab-accent-card-selected=\"true\"/);
+    expect(html).not.toMatch(/data-monitor-detail-tab-card=\"true\"/);
+    expect(html).toMatch(/class=\"[^\"]*pb-2/);
+    expect(html).not.toMatch(/class=\"[^\"]*monitor-detail-workbench-tabs/);
     expect(html).not.toMatch(/data-monitor-header-context-rail=\"true\"/);
     expect(html).not.toMatch(/<p[^>]*>10\.0\.0\.1<\/p>/);
     expect(html.indexOf('data-monitor-detail-header-mode=\"breadcrumb-only\"')).toBeLessThan(
       html.indexOf('data-monitor-workbench-stage=\"angular-layout\"')
     );
-    expect(html).toMatch(/data-monitor-refresh-toolbar-variant=\"angular-tab-extra\"/);
+    expect(html).toMatch(/data-monitor-refresh-toolbar-variant=\"compact-toolbar-extra\"/);
     expect(html).toMatch(/data-monitor-refresh-toolbar-position=\"tabbar-extra\"/);
-    expect(html).toMatch(/data-monitor-refresh-toolbar-density=\"angular-bordered-controls\"/);
-    expect(html).toMatch(/data-monitor-refresh-badge-variant=\"bordered\"/);
-    expect(html).toMatch(/data-monitor-refresh-select-density=\"bordered\"/);
-    expect(html).toMatch(/data-monitor-refresh-action-density=\"bordered\"/);
-    expect(html).toMatch(/data-observability-tabstrip-extra=\"true\"/);
+    expect(html).toMatch(/data-hz-ui=\"monitor-refresh-toolbar\"/);
+    expect(html).toMatch(/data-hz-monitor-refresh-toolbar-layout=\"single-row-compact\"/);
+    expect(html).toMatch(/data-monitor-refresh-toolbar-density=\"inline-quiet-controls\"/);
+    expect(html).toMatch(/data-monitor-refresh-badge-variant=\"quiet\"/);
+    expect(html).toMatch(/data-monitor-refresh-select-density=\"quiet\"/);
+    expect(html).toMatch(/data-monitor-refresh-action-density=\"quiet\"/);
+    expect(html).toMatch(/data-monitor-refresh-control-baseline=\"control-32-lined\"/);
+    expect(html).not.toMatch(/data-monitor-signal-handoff=\"compact-actions\"/);
+    expect(html).not.toMatch(/data-monitor-signal-handoff-link=\"metrics\"/);
+    expect(html).not.toMatch(/data-monitor-signal-handoff-link=\"logs\"/);
+    expect(html).not.toMatch(/data-monitor-signal-handoff-link=\"traces\"/);
+    expect(html).toMatch(/data-hz-control-height=\"32\"/);
+    expect(html).toMatch(/data-hz-control-height=\"28\"/);
+    expect(html).toMatch(/data-hz-control-edge=\"lined\"/);
+    expect(html).toMatch(/class=\"[^\"]*h-8[^\"]*border-\[var\(--hz-ui-line-soft\)\]/);
+    expect(html).toMatch(/class=\"[^\"]*h-7[^\"]*border-\[var\(--hz-ui-line-soft\)\]/);
+    expect(html).not.toMatch(/!border-transparent/);
+    expect(html).not.toMatch(/data-monitor-refresh-toolbar-density=\"angular-bordered-controls\"/);
+    expect(html).not.toMatch(/data-monitor-refresh-badge-variant=\"bordered\"/);
+    expect(html).not.toMatch(/data-monitor-refresh-select-density=\"bordered\"/);
+    expect(html).not.toMatch(/data-monitor-refresh-action-density=\"bordered\"/);
+    expect(html).toMatch(/data-monitor-detail-tabs-extra=\"true\"/);
+    expect(html).toMatch(/data-monitor-detail-tabs-extra-slot=\"true\"/);
     expect(html).not.toMatch(/data-observability-tab-indicator=\"underline\"/);
-    expect(html.indexOf('role=\"tablist\"')).toBeLessThan(html.indexOf('data-monitor-refresh-toolbar-variant=\"angular-tab-extra\"'));
+    expect(html).not.toMatch(/data-monitor-detail-tabs-variant=\"angular-card\"/);
+    expect(html).not.toMatch(/data-observability-tabstrip-variant=\"card\"/);
+    expect(html).not.toMatch(/data-observability-tabstrip-card=\"angular-nz-card\"/);
+    expect(html.indexOf('role=\"tablist\"')).toBeLessThan(html.indexOf('data-monitor-refresh-toolbar-variant=\"compact-toolbar-extra\"'));
     expect(html).toMatch(/data-monitor-refresh-select=\"true\"/);
     expect(html).not.toMatch(/data-refresh-selected=\"30\"/);
     expect(html).toMatch(/data-selected-tab=\"grafana\"/);
@@ -206,6 +278,108 @@ describe('monitor detail console', () => {
     expect(html).not.toContain('Persistent context rail');
   });
 
+  it('links the breadcrumb app mark from monitor.app when no list query context is present', () => {
+    const html = renderToStaticMarkup(
+      <MonitorDetailConsole
+        monitor={{
+          id: 42,
+          name: 'checkout-core',
+          app: 'mysql',
+          status: 1,
+          instance: 'mysql:3306'
+        } as any}
+        currentTab="realtime"
+        refreshInterval={30}
+        refreshCountdown={18}
+        backHref="/monitors"
+        editHref="/monitors/42/edit"
+        appHref="/setting/define?app=mysql"
+        navigationContext={{}}
+        onSelectTab={() => {}}
+        onSelectRefreshInterval={() => {}}
+        onRefresh={() => {}}
+        realtimeContent={<div>realtime stage</div>}
+        historyContent={<div>history stage</div>}
+        favoritesContent={<div>favorites stage</div>}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-monitor-detail-app-definition-link="angular-monitor-app"');
+    expect(html).toContain('data-monitor-detail-app-definition-target="/setting/define?app=mysql"');
+    expect(html).toContain('data-monitor-detail-list-return-target="/monitors"');
+    expect(html).toContain('href="/setting/define?app=mysql"');
+  });
+
+  it('renders a scrape-derived app context passed by the detail route for non-static discovery monitors', () => {
+    const html = renderToStaticMarkup(
+      <MonitorDetailConsole
+        monitor={{
+          id: 42,
+          name: 'node-exporter',
+          app: 'website',
+          scrape: 'prometheus',
+          status: 1,
+          instance: 'node-1:9100'
+        } as any}
+        currentTab="realtime"
+        refreshInterval={90}
+        refreshCountdown={90}
+        backHref="/monitors?app=prometheus"
+        editHref="/monitors/42/edit"
+        helpHref="https://hertzbeat.apache.org/docs/help/prometheus"
+        appHref="/setting/define?app=prometheus"
+        navigationContext={{ app: 'prometheus' }}
+        onSelectTab={() => {}}
+        onSelectRefreshInterval={() => {}}
+        onRefresh={() => {}}
+        realtimeContent={<div>realtime stage</div>}
+        historyContent={<div>history stage</div>}
+        favoritesContent={<div>favorites stage</div>}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-monitor-detail-list-return-target="/monitors?app=prometheus"');
+    expect(html).toContain('data-monitor-detail-app-definition-target="/setting/define?app=prometheus"');
+    expect(html).toContain('data-monitor-detail-help-target="https://hertzbeat.apache.org/docs/help/prometheus"');
+    expect(html).toContain('>prometheus</a>');
+  });
+
+  it('routes the outer monitor detail console shell through shared @hertzbeat/ui ownership', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
+    const uiLabSource = readFileSync(resolve(process.cwd(), 'app/ui-lab/page.tsx'), 'utf8');
+
+    expect(uiSource).toContain('export function HzMonitorDetailConsoleShell');
+    expect(uiSource).toContain('data-hz-ui="monitor-detail-console-shell"');
+    expect(uiSource).toContain('data-monitor-detail-console-shell-owner="hertzbeat-ui-detail-console-shell"');
+    expect(uiLabSource).toContain('data-hz-ui-lab-monitor-detail-console-shell="shared"');
+    expect(source).toContain('HzMonitorDetailConsoleShell');
+    expect(source).toContain('data-monitor-detail-console-shell-owner="hertzbeat-ui-detail-console-shell"');
+    expect(source).toContain('data-monitor-console-layout="angular-workbench"');
+    expect(source).toContain('data-monitor-first-viewport-rhythm="angular-tight"');
+    expect(source).not.toContain('className="-mx-4 space-y-2 px-3 pb-3 pt-0 text-[var(--ops-text-primary)] sm:-mx-6 sm:px-3"');
+    expect(source).not.toContain('style={cockpitSansStyle}');
+  });
+
+  it('routes the monitor detail tab workbench frame through shared @hertzbeat/ui ownership', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
+    const uiLabSource = readFileSync(resolve(process.cwd(), 'app/ui-lab/page.tsx'), 'utf8');
+
+    expect(uiSource).toContain('export function HzMonitorDetailWorkbenchFrame');
+    expect(uiSource).toContain('data-hz-ui="monitor-detail-workbench-frame"');
+    expect(uiSource).toContain('data-monitor-workbench-stage-owner="hertzbeat-ui-detail-workbench-frame"');
+    expect(uiSource).toContain('data-monitor-detail-tabset-owner="hertzbeat-ui-detail-workbench-frame"');
+    expect(uiLabSource).toContain('data-hz-ui-lab-monitor-detail-workbench-frame="shared"');
+    expect(source).toContain('HzMonitorDetailWorkbenchFrame');
+    expect(source).toContain('data-monitor-workbench-stage-owner="hertzbeat-ui-detail-workbench-frame"');
+    expect(source).not.toContain('<section');
+    expect(source).not.toContain('className="overflow-visible bg-transparent"');
+    expect(source).not.toContain('className="pb-2"');
+  });
+
   it('hides the grafana tab when no dashboard is available', () => {
     const html = renderToStaticMarkup(
       <MonitorDetailConsole
@@ -246,36 +420,98 @@ describe('monitor detail console', () => {
     expect(html).not.toContain('Open Grafana');
     expect(html).not.toContain('data-tab="grafana"');
     expect(html).toMatch(/data-monitor-refresh-select=\"true\"/);
-    expect(html).toContain('Auto refresh off');
+    expect(html).toContain('Close Auto Refresh');
+  });
+
+  it('keeps the Angular default refresh deadline as the current toolbar value', () => {
+    const html = renderToStaticMarkup(
+      <MonitorDetailConsole
+        monitor={{
+          id: 42,
+          name: 'checkout-core',
+          app: 'http',
+          status: 1,
+          instance: '10.0.0.1'
+        } as any}
+        workbenchSummaryFacts={[{ key: 'realtime', label: 'Real-time Metrics', value: '2' }]}
+        currentTab="realtime"
+        refreshInterval={90}
+        refreshCountdown={90}
+        backHref="/monitors"
+        editHref="/monitors/42/edit"
+        helpHref={null}
+        appHref="/setting/define?app=http"
+        grafanaUrl={null}
+        navigationContext={{ app: 'http' }}
+        onSelectTab={() => {}}
+        onSelectRefreshInterval={() => {}}
+        onRefresh={() => {}}
+        realtimeContent={<div>realtime stage</div>}
+        historyContent={<div>history stage</div>}
+        favoritesContent={<div>favorites stage</div>}
+        grafanaContent={null}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('Auto Refresh After 90 s');
+    expect(html).toContain('data-monitor-detail-default-refresh-contract="angular-deadline-90"');
+    expect(html).toContain('data-monitor-detail-refresh-current-option="angular-default-deadline-current"');
+    expect(html).toContain('aria-label="Auto Refresh After 90 s"');
   });
 
   it('routes refresh controls through the compact tab-extra toolbar rhythm', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
 
-    expect(source).toContain('data-monitor-refresh-toolbar-variant="angular-tab-extra"');
+    expect(source).toContain('data-monitor-refresh-toolbar-variant="compact-toolbar-extra"');
     expect(source).toContain('data-monitor-refresh-toolbar-position="tabbar-extra"');
-    expect(source).toContain('extra={refreshToolbar}');
-    expect(source).toContain('variant="card"');
-    expect(source).toContain('data-monitor-detail-tabset-type="angular-card"');
-    expect(source).toContain('data-monitor-refresh-select="true"');
+    expect(source).toContain('HzMonitorRefreshToolbar');
+    expect(source).toContain('data-hz-monitor-refresh-toolbar-layout="single-row-compact"');
+    expect(source).toContain('extra={detailTabExtra}');
+    expect(source).toContain('HzMonitorDetailTabs');
+    expect(source).toContain('HzMonitorDetailTabLabel');
+    expect(source).toContain('data-monitor-detail-tabs-owner="hertzbeat-ui-monitor-detail-tabs"');
+    expect(source).toContain('data-monitor-detail-tab-label-owner="hertzbeat-ui-detail-tab-label"');
+    expect(uiSource).toContain('data-monitor-detail-tabset-type="bottom-underline-switch"');
+    expect(uiSource).toContain('data-hz-ui="monitor-detail-tab-label"');
+    expect(uiSource).toContain('data-monitor-detail-tab-icon-owner="hertzbeat-ui-detail-tab-label"');
+    expect(uiSource).toContain('data-monitor-refresh-select="true"');
     expect(source).toContain('RefreshCw');
+    expect(source).not.toContain('className="inline-flex items-center gap-2"');
+    expect(source).not.toContain('const refreshToolbar = (');
+    expect(source).not.toContain('HzStatusBadge');
+    expect(source).not.toContain('ObservabilityTabStrip');
     expect(source).not.toContain('data-refresh-selected=');
     expect(source).not.toContain('rounded-[2px] px-2 py-1 text-[11px] font-medium tracking-[0.03em] transition');
     expect(source).not.toContain("border-b border-[var(--ops-primary)] bg-[var(--ops-surface-panel)] text-[var(--ops-text-primary)]");
   });
 
-  it('routes the app badge through the breadcrumb owner without a pre-tab context rail', () => {
+  it('routes the app context through underline text without a pre-tab context rail', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
 
-    expect(source).toContain('ObservabilityControlChip');
+    expect(source).toContain('HzInlineContextMark');
+    expect(source).toContain('HzMonitorBreadcrumb');
+    expect(source).toContain('data-monitor-detail-breadcrumb-owner="hertzbeat-ui-monitor-breadcrumb"');
+    expect(source).toContain('data-monitor-detail-context-mark-owner="hertzbeat-ui-inline-context-mark"');
+    expect(source).toContain('placement="breadcrumb"');
+    expect(source).not.toContain('className="ml-1"');
+    expect(source).not.toContain('monitor-detail-workbench-breadcrumb flex flex-wrap items-center gap-2 border-b border-[var(--ops-border-color)] pb-2 text-[11px] text-[var(--ops-text-tertiary)]');
+    expect(source).not.toContain('HzBreadcrumbChip');
+    expect(source).not.toContain('data-monitor-detail-app-chip-owner="hertzbeat-ui-breadcrumb-chip"');
+    expect(source).not.toContain('ObservabilityControlChip');
     expect(source).not.toContain('ContextChipBar');
     expect(source).not.toContain('actions={');
     expect(source).not.toContain('data-monitor-detail-return-action="true"');
-    expect(source).not.toContain("t('common.button.help')");
+    expect(source).toContain("t('common.button.help')");
+    expect(source).toContain('data-monitor-detail-tab-extra-contract="angular-refresh-help"');
+    expect(source).toContain('data-monitor-detail-help-action="angular-docs-help"');
+    expect(source).toContain('data-monitor-detail-help-owner="hertzbeat-ui-icon-link"');
     expect(source).not.toContain("t('monitor.edit-monitor')");
     expect(source).toContain('data-monitor-detail-header-mode="breadcrumb-only"');
     expect(source).toContain('data-monitor-detail-reference-source="apache-hertzbeat-master-monitor-detail"');
-    expect(source).toContain('data-monitor-detail-app-chip="breadcrumb"');
+    expect(source).toContain('data-monitor-detail-context-mark="breadcrumb"');
+    expect(source).not.toContain('data-monitor-detail-app-chip="breadcrumb"');
     expect(source).not.toContain('data-monitor-header-layout="angular-context-rail"');
     expect(source).not.toContain('data-monitor-header-proportion="angular-reference"');
     expect(source).not.toContain('data-monitor-header-context-rail="true"');
@@ -307,24 +543,39 @@ describe('monitor detail console', () => {
 
   it('keeps monitor detail first viewport on tight vertical rhythm', () => {
     const consoleSource = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
 
     expect(consoleSource).toContain('data-monitor-first-viewport-rhythm="angular-tight"');
+    expect(consoleSource).toContain('data-monitor-detail-history-tab-load="angular-click-load-metric-chart"');
+    expect(consoleSource).toContain('data-monitor-detail-realtime-tab-load="angular-click-load-real-time-metric"');
+    expect(consoleSource).toContain('data-monitor-detail-favorite-tab-load="angular-click-load-favorite-metrics"');
+    expect(consoleSource).toContain('data-monitor-detail-grafana-fallback-tab="angular-previous-data-tab"');
     expect(consoleSource).toContain('data-monitor-detail-completion-guard="reported-angular-no-card-stack"');
     expect(consoleSource).toContain('data-monitor-detail-body-stack-state="single-workbench-table-rows"');
     expect(consoleSource).toContain('data-monitor-detail-completion-header="breadcrumb-only"');
     expect(consoleSource).toContain('data-monitor-detail-header-mode="breadcrumb-only"');
     expect(consoleSource).toContain('data-monitor-detail-reference-source="apache-hertzbeat-master-monitor-detail"');
-    expect(consoleSource).toContain('data-monitor-console-tab-panel-rhythm="angular-tight"');
+    expect(consoleSource).toContain('data-monitor-detail-breadcrumb-owner="hertzbeat-ui-monitor-breadcrumb"');
+    expect(consoleSource).toContain('HzMonitorDetailConsoleShell');
+    expect(consoleSource).toContain('data-monitor-detail-console-shell-owner="hertzbeat-ui-detail-console-shell"');
+    expect(consoleSource).toContain('HzMonitorDetailWorkbenchFrame');
+    expect(consoleSource).toContain('data-monitor-workbench-stage-owner="hertzbeat-ui-detail-workbench-frame"');
+    expect(consoleSource).not.toContain('monitor-detail-workbench-breadcrumb flex flex-wrap items-center gap-2 border-b border-[var(--ops-border-color)] pb-2 text-[11px] text-[var(--ops-text-tertiary)]');
+    expect(consoleSource).toContain('HzMonitorDetailTabPanel');
+    expect(consoleSource).toContain('data-monitor-console-tab-panel-owner="hertzbeat-ui-detail-tab-panel"');
+    expect(consoleSource).not.toContain('data-monitor-console-tab-panel-rhythm="angular-tight"');
     expect(consoleSource).not.toContain('data-monitor-header-proportion="angular-reference"');
     expect(consoleSource).not.toContain('data-monitor-header-rail-align="title-block"');
     expect(consoleSource).not.toContain('ObservabilityPageHeader');
-    expect(consoleSource).toContain('monitor-detail-page-frame -mx-4 space-y-2');
-    expect(consoleSource).toContain('-mx-4');
-    expect(consoleSource).toContain('px-3 py-3');
-    expect(consoleSource).toContain('sm:-mx-6');
-    expect(consoleSource).toContain('sm:px-3');
-    expect(consoleSource).toContain('monitor-detail-workbench-breadcrumb');
-    expect(consoleSource).toContain('border-b border-[var(--ops-border-color)] pb-2');
+    expect(consoleSource).not.toContain('monitor-detail-page-frame');
+    expect(uiSource).toContain('-mx-4 space-y-2 px-3 pb-3 pt-0');
+    expect(uiSource).toContain('sm:-mx-6 sm:px-3');
+    expect(consoleSource).not.toContain('-mx-4 space-y-2 px-3 pb-3 pt-0');
+    expect(consoleSource).not.toContain('px-3 py-3');
+    expect(consoleSource).not.toContain('monitor-detail-workbench-breadcrumb');
+    expect(consoleSource).toContain('HzMonitorBreadcrumb');
+    expect(consoleSource).toContain('data-monitor-detail-breadcrumb-owner="hertzbeat-ui-monitor-breadcrumb"');
+    expect(consoleSource).not.toContain('border-b border-[var(--ops-border-color)] pb-2');
     expect(consoleSource).not.toContain('monitor-detail-workbench-header grid gap-2');
     expect(consoleSource).not.toContain('pb-2 xl:grid-cols-[minmax(0,1fr)_minmax(168px,190px)]');
     expect(consoleSource).not.toContain('monitor-detail-workbench-header__chips');
@@ -332,14 +583,16 @@ describe('monitor detail console', () => {
     expect(consoleSource).toContain('data-monitor-workbench-stage="angular-layout"');
     expect(consoleSource).toContain('data-monitor-workbench-stage-chrome="angular-tabset-direct"');
     expect(consoleSource).toContain('data-monitor-workbench-stage-rhythm="direct-tab-body"');
-    expect(consoleSource).toContain('data-monitor-detail-tabset-type="angular-card"');
-    expect(consoleSource).toContain('monitor-detail-workbench-layout overflow-visible bg-transparent');
-    expect(consoleSource).not.toContain('monitor-detail-workbench-layout overflow-hidden');
-    expect(consoleSource).not.toContain('monitor-detail-workbench-layout overflow-hidden border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)]');
-    expect(consoleSource).toContain('data-monitor-tab-body-surface="angular-tab-content-direct"');
-    expect(consoleSource).toContain('monitor-detail-workbench-tabs pb-2');
-    expect(consoleSource).not.toContain('monitor-detail-workbench-tabs px-3 pb-2 pt-2');
-    expect(consoleSource).toContain('className="space-y-2"');
+    expect(uiSource).toContain('data-monitor-detail-tabset-type="bottom-underline-switch"');
+    expect(uiSource).toContain('overflow-visible bg-transparent');
+    expect(consoleSource).not.toContain('className="overflow-visible bg-transparent"');
+    expect(consoleSource).not.toContain('monitor-detail-workbench-layout');
+    expect(consoleSource).not.toContain('data-monitor-tab-body-surface="angular-tab-content-direct"');
+    expect(uiSource).toContain("cn('pb-2'");
+    expect(consoleSource).not.toContain('className="pb-2"');
+    expect(consoleSource).not.toContain('className="-mx-4 space-y-2 px-3 pb-3 pt-0 text-[var(--ops-text-primary)] sm:-mx-6 sm:px-3"');
+    expect(consoleSource).not.toContain('monitor-detail-workbench-tabs');
+    expect(consoleSource).not.toContain('className="space-y-2"');
     expect(consoleSource).not.toContain('className="space-y-2 px-3 pb-3"');
     expect(consoleSource).not.toContain('monitor-detail-page-frame space-y-4');
     expect(consoleSource).not.toContain('monitor-detail-page-frame -mx-4 space-y-3');
@@ -352,22 +605,48 @@ describe('monitor detail console', () => {
     expect(consoleSource).not.toContain('className="space-y-4"');
   });
 
-  it('routes the refresh countdown badge through the observability badge owner', () => {
+  it('routes the refresh countdown toolbar through shared @hertzbeat/ui primitives', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
+    const uiSource = readFileSync(resolve(process.cwd(), 'packages/hertzbeat-ui/src/index.tsx'), 'utf8');
 
-    expect(source).toContain('ObservabilityBadge');
-    expect(source).toContain('data-monitor-refresh-toolbar-density="angular-bordered-controls"');
-    expect(source).toContain('data-monitor-refresh-badge-variant="bordered"');
-    expect(source).toContain('data-monitor-refresh-select-density="bordered"');
-    expect(source).toContain('data-monitor-refresh-action-density="bordered"');
-    expect(source).toContain('border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)]');
+    expect(source).toContain('HzMonitorRefreshToolbar');
+    expect(source).toContain('data-monitor-refresh-toolbar-density="inline-quiet-controls"');
+    expect(source).toContain('data-hz-monitor-refresh-toolbar-layout="single-row-compact"');
+    expect(source).toContain('data-monitor-detail-refresh-copy-contract="angular-deadline-label"');
+    expect(source).toContain("t('monitor.detail.auto-refresh', { time: refreshCountdown })");
+    expect(source).toContain("t('monitor.detail.close-refresh')");
+    expect(source).not.toContain('monitor.detail.refresh-countdown');
+    expect(source).not.toContain('monitor.detail.refresh-disabled');
+    expect(source).not.toContain('HzStatusBadge');
+    expect(source).not.toContain('HzButton');
+    expect(uiSource).toContain('HzStatusBadge');
+    expect(uiSource).toContain('HzButton');
+    expect(uiSource).toContain('data-monitor-refresh-toolbar-owner="hertzbeat-ui-refresh-toolbar"');
+    expect(uiSource).toContain('data-monitor-refresh-badge-owner="hertzbeat-ui-status-badge"');
+    expect(uiSource).toContain('data-monitor-refresh-action-owner="hertzbeat-ui-button"');
+    expect(uiSource).toContain('data-monitor-refresh-badge-variant="quiet"');
+    expect(uiSource).toContain('data-monitor-refresh-select-density="quiet"');
+    expect(uiSource).toContain('data-monitor-refresh-action-density="quiet"');
+    expect(uiSource).toContain('data-monitor-refresh-control-baseline="control-32-lined"');
+    expect(uiSource).toContain('data-hz-control-height="32"');
+    expect(uiSource).toContain('data-hz-control-height="28"');
+    expect(uiSource).toContain('data-hz-control-edge="lined"');
+    expect(source).not.toContain('signalHandoffLinks');
+    expect(source).not.toContain("id: 'metrics'");
+    expect(source).not.toContain("id: 'logs'");
+    expect(source).not.toContain("id: 'traces'");
+    expect(uiSource).toContain('className="h-7 w-7');
+    expect(uiSource).not.toContain('border-transparent bg-transparent text-[#98a2b3]');
+    expect(uiSource).not.toContain('data-monitor-refresh-badge-variant="bordered"');
+    expect(uiSource).not.toContain('data-monitor-refresh-select-density="bordered"');
+    expect(uiSource).not.toContain('data-monitor-refresh-action-density="bordered"');
+    expect(source).not.toContain('ObservabilityBadge');
     expect(source).not.toContain('variant="underline"');
-    expect(source).not.toContain('border-transparent bg-transparent');
     expect(source).not.toContain('rounded-[2px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-2.5 py-1 text-[11px] font-medium tracking-[0.12em] text-[var(--ops-text-tertiary)]');
     expect(source).not.toContain("from '../workbench/primitives'");
   });
 
-  it('shows compact signal handoff actions without fake signal cards or counts', () => {
+  it('does not render monitor detail signal handoff actions in the tab toolbar', () => {
     const html = renderToStaticMarkup(
       <MonitorDetailConsole
         monitor={{
@@ -387,11 +666,6 @@ describe('monitor detail console', () => {
         appHref="/setting/define?app=http"
         grafanaUrl={null}
         navigationContext={{ app: 'http' }}
-        signalHandoffLinks={{
-          metricsHref: '/ingestion/otlp/metrics?timeRange=last-1h&monitorId=42&source=monitor',
-          logsHref: '/log/manage?timeRange=last-1h&monitorId=42&source=monitor',
-          tracesHref: '/trace/manage?timeRange=last-1h&monitorId=42&source=monitor'
-        }}
         onSelectTab={() => {}}
         onSelectRefreshInterval={() => {}}
         onRefresh={() => {}}
@@ -403,23 +677,36 @@ describe('monitor detail console', () => {
       />
     );
 
-    expect(html).toContain('data-monitor-signal-handoff="compact-actions"');
-    expect(html).toContain('data-monitor-signal-handoff-source="context-only"');
-    expect(html).toContain('data-monitor-signal-handoff-link="metrics"');
-    expect(html).toContain('data-monitor-signal-handoff-link="logs"');
-    expect(html).toContain('data-monitor-signal-handoff-link="traces"');
-    expect(html).toContain('/ingestion/otlp/metrics?timeRange=last-1h&amp;monitorId=42&amp;source=monitor');
-    expect(html).toContain('/log/manage?timeRange=last-1h&amp;monitorId=42&amp;source=monitor');
-    expect(html).toContain('/trace/manage?timeRange=last-1h&amp;monitorId=42&amp;source=monitor');
-    expect(html).toContain('Metrics Workbench');
-    expect(html).toContain('Logs Workbench');
-    expect(html).toContain('Traces Workbench');
+    expect(html).not.toContain('data-monitor-signal-handoff="compact-actions"');
+    expect(html).not.toContain('data-monitor-signal-handoff-owner="hertzbeat-ui-icon-link-group"');
+    expect(html).not.toContain('data-monitor-signal-handoff-link-owner="hertzbeat-ui-icon-link"');
+    expect(html).not.toContain('data-monitor-signal-handoff-link="metrics"');
+    expect(html).not.toContain('data-monitor-signal-handoff-link="logs"');
+    expect(html).not.toContain('data-monitor-signal-handoff-link="traces"');
+    expect(html).not.toContain('/ingestion/otlp/metrics?timeRange=last-1h&amp;monitorId=42&amp;source=monitor');
+    expect(html).not.toContain('/log/manage?timeRange=last-1h&amp;monitorId=42&amp;source=monitor');
+    expect(html).not.toContain('/trace/manage?timeRange=last-1h&amp;monitorId=42&amp;source=monitor');
+    expect(html).not.toContain('Metrics Workbench');
+    expect(html).not.toContain('Logs Workbench');
+    expect(html).not.toContain('Traces Workbench');
     expect(html).not.toContain('data-monitor-signal-fake-card');
     expect(html).not.toContain('data-monitor-signal-fake-count');
     expect(html).not.toContain('0 signals');
     expect(html).not.toContain('No signal data');
   });
 
+  it('does not route monitor signal handoff buttons into the detail toolbar', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-console.tsx'), 'utf8');
+
+    expect(source).toContain('HzMonitorRefreshToolbar');
+    expect(source).not.toContain('signalHandoffLinks');
+    expect(source).not.toContain("id: 'metrics'");
+    expect(source).not.toContain("id: 'logs'");
+    expect(source).not.toContain("id: 'traces'");
+    expect(source).toContain('HzIconLink');
+    expect(source).toContain('data-monitor-detail-help-action="angular-docs-help"');
+  });
+
   it('does not render a pre-tab overview slot on monitor detail pages', () => {
     const html = renderToStaticMarkup(
       <MonitorDetailConsole
diff --git a/web-next/components/monitor-detail/monitor-detail-console.tsx b/web-next/components/monitor-detail/monitor-detail-console.tsx
index 351e3d72c6..435f754114 100644
--- a/web-next/components/monitor-detail/monitor-detail-console.tsx
+++ b/web-next/components/monitor-detail/monitor-detail-console.tsx
@@ -1,14 +1,20 @@
 'use client';
 
 import Link from 'next/link';
-import { Activity, BarChart3, FileText, GitBranch, LineChart, RefreshCw, Star } from 'lucide-react';
+import { Activity, HelpCircle, LineChart, RefreshCw, Star } from 'lucide-react';
 import React from 'react';
 import {
-  ObservabilityBadge,
-  ObservabilityControlChip,
-  ObservabilityTabStrip
-} from '../observability';
-import { Select } from '../ui/select';
+  HzActionGroup,
+  HzIconLink,
+  HzInlineContextMark,
+  HzMonitorBreadcrumb,
+  HzMonitorDetailConsoleShell,
+  HzMonitorDetailTabLabel,
+  HzMonitorDetailTabPanel,
+  HzMonitorDetailTabs,
+  HzMonitorDetailWorkbenchFrame,
+  HzMonitorRefreshToolbar
+} from '@hertzbeat/ui';
 import type { Monitor } from '../../lib/types';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
@@ -25,38 +31,26 @@ type NavigationContext = {
   app?: string | null;
 };
 
-type SignalHandoffLinks = {
-  metricsHref: string;
-  logsHref: string;
-  tracesHref: string;
-};
-
 const REFRESH_OPTIONS = [
-  { value: 10, label: '10s' },
-  { value: 30, label: '30s' },
-  { value: 60, label: '60s' },
-  { value: 300, label: '300s' },
-  { value: -1, label: 'Off' }
+  { value: 10 },
+  { value: 30 },
+  { value: 60 },
+  { value: 300 },
+  { value: -1 }
 ] as const;
 
-const cockpitSansStyle = {
-  fontFamily: '"IBM Plex Sans", "Avenir Next", "Segoe UI", system-ui, sans-serif'
-} as const;
-
-const cockpitMonoStyle = {
-  fontFamily: '"IBM Plex Mono", "SFMono-Regular", "SF Mono", ui-monospace, monospace'
-} as const;
-
 export function MonitorDetailConsole({
   monitor,
   currentTab,
   refreshInterval,
   refreshCountdown,
   backHref,
+  helpHref,
   appHref,
   grafanaUrl,
   navigationContext,
-  signalHandoffLinks,
+  refreshTarget,
+  grafanaRefreshFallbackTab,
   onSelectTab,
   onSelectRefreshInterval,
   onRefresh,
@@ -77,7 +71,8 @@ export function MonitorDetailConsole({
   appHref?: string | null;
   grafanaUrl?: string | null;
   navigationContext?: NavigationContext;
-  signalHandoffLinks?: SignalHandoffLinks;
+  refreshTarget?: MonitorDetailConsoleTabKey;
+  grafanaRefreshFallbackTab?: Exclude<MonitorDetailConsoleTabKey, 'grafana'>;
   onSelectTab: (tab: MonitorDetailConsoleTabKey) => void;
   onSelectRefreshInterval: (value: number) => void;
   onRefresh: () => void;
@@ -93,14 +88,13 @@ export function MonitorDetailConsole({
     label: string,
     Icon: typeof Activity
   ) => (
-    <span
-      className="inline-flex items-center gap-2"
-      data-monitor-detail-tab-label-source="angular-title"
-      data-monitor-detail-tab-label={key}
+    <HzMonitorDetailTabLabel
+      tabKey={key}
+      icon={Icon}
+      data-monitor-detail-tab-label-owner="hertzbeat-ui-detail-tab-label"
     >
-      <Icon aria-hidden="true" className="h-3.5 w-3.5" data-monitor-detail-tab-icon={key} />
-      <span>{label}</span>
-    </span>
+      {label}
+    </HzMonitorDetailTabLabel>
   );
   const tabs = [
     { key: 'realtime', label: renderTabLabel('realtime', t('monitor.detail.realtime'), Activity) },
@@ -124,10 +118,20 @@ export function MonitorDetailConsole({
     }
   }
 
+  const selectedRefreshValue = String(refreshInterval);
+  const refreshOptions = REFRESH_OPTIONS.map(option => ({
+    value: String(option.value),
+    label: option.value > 0 ? t('monitor.detail.config-refresh', { time: option.value }) : t('monitor.detail.close-refresh')
+  }));
+  const selectedRefreshHasMenuOption = refreshOptions.some(option => option.value === selectedRefreshValue);
+  const effectiveRefreshOptions =
+    selectedRefreshHasMenuOption || refreshInterval <= 0
+      ? refreshOptions
+      : [{ value: selectedRefreshValue, label: t('monitor.detail.auto-refresh', { time: refreshInterval }) }, ...refreshOptions];
   const refreshCopy =
     refreshInterval > 0
-      ? `${t('monitor.detail.refresh-countdown')} ${refreshCountdown}s`
-      : t('monitor.detail.refresh-disabled');
+      ? t('monitor.detail.auto-refresh', { time: refreshCountdown })
+      : t('monitor.detail.close-refresh');
 
   const breadcrumbApp = navigationContext?.app || monitor.app || null;
   const breadcrumbAppTranslation = breadcrumbApp ? t(`monitor.app.${breadcrumbApp}`) : null;
@@ -136,172 +140,137 @@ export function MonitorDetailConsole({
       ? breadcrumbAppTranslation
       : breadcrumbApp;
 
-  const refreshToolbar = (
-    <div
-      className="flex flex-wrap items-center justify-end gap-1.5"
-      data-monitor-refresh-toolbar-variant="angular-tab-extra"
-      data-monitor-refresh-toolbar-position="tabbar-extra"
-      data-monitor-refresh-toolbar-density="angular-bordered-controls"
+  const detailTabExtra = (
+    <HzActionGroup
+      layout="end-wrap"
+      density="inline"
+      data-monitor-detail-tab-extra-owner="hertzbeat-ui-action-group"
+      data-monitor-detail-tab-extra-contract="angular-refresh-help"
     >
-      {signalHandoffLinks ? (
-        <div
-          className="flex min-w-0 flex-wrap items-center gap-1.5"
-          data-monitor-signal-handoff="compact-actions"
-          data-monitor-signal-handoff-source="context-only"
+      <HzMonitorRefreshToolbar
+        refreshLabel={refreshCopy}
+        refreshActionLabel={t('common.refresh')}
+        selectedRefresh={selectedRefreshValue}
+        refreshOptions={effectiveRefreshOptions}
+        refreshIcon={<RefreshCw aria-hidden="true" className="h-3.5 w-3.5" />}
+        onRefreshChange={value => onSelectRefreshInterval(Number(value))}
+        onRefresh={onRefresh}
+        data-monitor-refresh-toolbar-variant="compact-toolbar-extra"
+        data-monitor-refresh-toolbar-position="tabbar-extra"
+        data-monitor-refresh-toolbar-density="inline-quiet-controls"
+        data-hz-monitor-refresh-toolbar-layout="single-row-compact"
+        data-monitor-detail-refresh-options-contract="angular-config-refresh-copy"
+        data-monitor-detail-refresh-copy-contract="angular-deadline-label"
+        data-monitor-detail-default-refresh-contract={refreshInterval === 90 ? 'angular-deadline-90' : undefined}
+        data-monitor-detail-refresh-current-option={selectedRefreshHasMenuOption ? 'configured-option' : 'angular-default-deadline-current'}
+        data-monitor-detail-countdown-reset-contract="angular-deadline-reset"
+        data-monitor-detail-refresh-target={refreshTarget}
+        data-monitor-detail-grafana-refresh-target={currentTab === 'grafana' ? grafanaRefreshFallbackTab : undefined}
+      />
+      {helpHref ? (
+        <HzIconLink
+          href={helpHref}
+          label={t('common.button.help')}
+          target="_blank"
+          rel="noreferrer"
+          data-monitor-detail-help-action="angular-docs-help"
+          data-monitor-detail-help-owner="hertzbeat-ui-icon-link"
+          data-monitor-detail-help-target={helpHref}
         >
-          <ObservabilityControlChip
-            as={Link}
-            href={signalHandoffLinks.metricsHref}
-            size="compact"
-            title={t('otlp.metrics.title')}
-            className="h-7 gap-1.5 px-2"
-            data-monitor-signal-handoff-link="metrics"
-          >
-            <BarChart3 aria-hidden="true" className="h-3.5 w-3.5" />
-            <span>{t('otlp.metrics.title')}</span>
-          </ObservabilityControlChip>
-          <ObservabilityControlChip
-            as={Link}
-            href={signalHandoffLinks.logsHref}
-            size="compact"
-            title={t('menu.log.manage')}
-            className="h-7 gap-1.5 px-2"
-            data-monitor-signal-handoff-link="logs"
-          >
-            <FileText aria-hidden="true" className="h-3.5 w-3.5" />
-            <span>{t('menu.log.manage')}</span>
-          </ObservabilityControlChip>
-          <ObservabilityControlChip
-            as={Link}
-            href={signalHandoffLinks.tracesHref}
-            size="compact"
-            title={t('menu.trace.manage')}
-            className="h-7 gap-1.5 px-2"
-            data-monitor-signal-handoff-link="traces"
-          >
-            <GitBranch aria-hidden="true" className="h-3.5 w-3.5" />
-            <span>{t('menu.trace.manage')}</span>
-          </ObservabilityControlChip>
-        </div>
+          <HelpCircle aria-hidden="true" className="h-3.5 w-3.5" />
+        </HzIconLink>
       ) : null}
-      <div className="flex min-w-0 flex-wrap items-center gap-1.5">
-        <ObservabilityBadge
-          as="span"
-          size="compact"
-          tone="tertiary"
-          className="h-7 rounded-[2px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-2.5 py-1 text-[11px] tracking-[0.04em]"
-          style={cockpitMonoStyle}
-          data-monitor-refresh-badge-variant="bordered"
-        >
-          {refreshCopy}
-        </ObservabilityBadge>
-        <Select
-          aria-label={refreshCopy}
-          data-monitor-refresh-select="true"
-          data-monitor-refresh-select-density="bordered"
-          value={refreshInterval}
-          containerClassName="w-[82px]"
-          className="h-7 min-w-0 text-[11px] font-medium text-[var(--ops-text-secondary)] hover:text-[var(--ops-text-primary)]"
-          style={cockpitMonoStyle}
-          onChange={event => onSelectRefreshInterval(Number(event.target.value))}
-        >
-          {REFRESH_OPTIONS.map(option => (
-            <option key={option.value} value={option.value}>
-              {option.label}
-            </option>
-          ))}
-        </Select>
-      </div>
-      <ObservabilityControlChip
-        type="button"
-        size="compact"
-        aria-label={t('common.refresh')}
-        title={t('common.refresh')}
-        className="h-7 w-7 px-0"
-        data-monitor-refresh-action-density="bordered"
-        onClick={onRefresh}
-      >
-        <RefreshCw aria-hidden="true" className="h-3.5 w-3.5" />
-        <span className="sr-only">{t('common.refresh')}</span>
-      </ObservabilityControlChip>
-    </div>
+    </HzActionGroup>
   );
 
   return (
-    <div
-      className="monitor-detail-page-frame -mx-4 space-y-2 border-t border-[var(--ops-border-color)] px-3 py-3 text-[var(--ops-text-primary)] sm:-mx-6 sm:px-3"
+    <HzMonitorDetailConsoleShell
       data-monitor-console-layout="angular-workbench"
       data-monitor-console-tone="operator-sheet"
+      data-monitor-detail-console-shell-owner="hertzbeat-ui-detail-console-shell"
       data-monitor-detail-completion-guard="reported-angular-no-card-stack"
       data-monitor-detail-body-stack-state="single-workbench-table-rows"
       data-monitor-detail-completion-header="breadcrumb-only"
       data-monitor-first-viewport-rhythm="angular-tight"
-      style={cockpitSansStyle}
+      data-monitor-detail-history-catalog-load="angular-tab-click-lazy"
+      data-monitor-detail-history-tab-load="angular-click-load-metric-chart"
+      data-monitor-detail-realtime-tab-load="angular-click-load-real-time-metric"
+      data-monitor-detail-favorite-tab-load="angular-click-load-favorite-metrics"
+      data-monitor-detail-grafana-fallback-tab="angular-previous-data-tab"
     >
-      <nav
-        className="monitor-detail-workbench-breadcrumb flex flex-wrap items-center gap-2 border-b border-[var(--ops-border-color)] pb-2 text-[11px] text-[var(--ops-text-tertiary)]"
+      <HzMonitorBreadcrumb
         data-monitor-detail-header-mode="breadcrumb-only"
         data-monitor-detail-reference-source="apache-hertzbeat-master-monitor-detail"
+        data-monitor-detail-breadcrumb-owner="hertzbeat-ui-monitor-breadcrumb"
         aria-label={t('monitor.detail')}
       >
-        <Link href="/" className="transition hover:text-[var(--ops-text-primary)]">
+        <Link href="/">
           {t('menu.dashboard')}
         </Link>
-        <span className="text-[var(--ops-text-tertiary)]">/</span>
-        <Link href={backHref} className="transition hover:text-[var(--ops-text-primary)]">
+        <span>/</span>
+        <Link
+          href={backHref}
+          data-monitor-detail-list-return="angular-app-filter"
+          data-monitor-detail-list-return-target={backHref}
+        >
           {t('monitor.list')}
         </Link>
-        <span className="text-[var(--ops-text-tertiary)]">/</span>
-        <span className="text-[var(--ops-text-secondary)]">{t('monitor.detail')}</span>
+        <span>/</span>
+        <span>{t('monitor.detail')}</span>
         {breadcrumbApp ? (
           appHref ? (
-            <ObservabilityControlChip
-              as={Link}
+            <HzInlineContextMark
+              component={Link}
               href={appHref}
-              size="micro"
-              className="ml-1"
-              data-monitor-detail-app-chip="breadcrumb"
+              placement="breadcrumb"
+              data-monitor-detail-context-mark="breadcrumb"
+              data-monitor-detail-context-mark-owner="hertzbeat-ui-inline-context-mark"
+              data-monitor-detail-app-definition-link="angular-monitor-app"
+              data-monitor-detail-app-definition-target={appHref}
             >
               {breadcrumbAppLabel || breadcrumbApp}
-            </ObservabilityControlChip>
+            </HzInlineContextMark>
           ) : (
-            <ObservabilityControlChip as="span" size="micro" className="ml-1" data-monitor-detail-app-chip="breadcrumb">
+            <HzInlineContextMark
+              placement="breadcrumb"
+              data-monitor-detail-context-mark="breadcrumb"
+              data-monitor-detail-context-mark-owner="hertzbeat-ui-inline-context-mark"
+            >
               {breadcrumbAppLabel || breadcrumbApp}
-            </ObservabilityControlChip>
+            </HzInlineContextMark>
           )
         ) : null}
-      </nav>
+      </HzMonitorBreadcrumb>
 
-      <section
-        className="monitor-detail-workbench-layout overflow-visible bg-transparent"
+      <HzMonitorDetailWorkbenchFrame
         data-monitor-workbench-stage="angular-layout"
+        data-monitor-workbench-stage-owner="hertzbeat-ui-detail-workbench-frame"
         data-monitor-workbench-stage-chrome="angular-tabset-direct"
         data-monitor-workbench-stage-rhythm="direct-tab-body"
-      >
-        <div className="monitor-detail-workbench-tabs pb-2" data-monitor-detail-tabset-type="angular-card">
-          <ObservabilityTabStrip
+        tabs={
+          <HzMonitorDetailTabs
             items={tabs}
             selectedKey={currentTab}
             onSelect={onSelectTab}
             panelIdPrefix={panelIdPrefix}
-            tone="operator"
-            variant="card"
-            extra={refreshToolbar}
+            extra={detailTabExtra}
+            data-monitor-detail-tabs-owner="hertzbeat-ui-monitor-detail-tabs"
           />
-        </div>
+        }
+        tabsetProps={{
+          'data-monitor-detail-tabset-type': 'bottom-underline-switch'
+        } as React.HTMLAttributes<HTMLDivElement>}
+      >
 
-        <div
-          className="space-y-2"
-          data-monitor-console-tab-panel="true"
-          data-monitor-console-tab-panel-rhythm="angular-tight"
-          data-monitor-tab-body-surface="angular-tab-content-direct"
+        <HzMonitorDetailTabPanel
           id={`${panelIdPrefix}-panel-${currentTab}`}
-          role="tabpanel"
-          aria-labelledby={`${panelIdPrefix}-tab-${currentTab}`}
+          tabId={`${panelIdPrefix}-tab-${currentTab}`}
+          active
+          data-monitor-console-tab-panel-owner="hertzbeat-ui-detail-tab-panel"
         >
           {renderTabContent()}
-        </div>
-      </section>
-    </div>
+        </HzMonitorDetailTabPanel>
+      </HzMonitorDetailWorkbenchFrame>
+    </HzMonitorDetailConsoleShell>
   );
 }
diff --git a/web-next/components/monitor-detail/monitor-detail-sections.test.tsx b/web-next/components/monitor-detail/monitor-detail-sections.test.tsx
index 9900599569..031713847c 100644
--- a/web-next/components/monitor-detail/monitor-detail-sections.test.tsx
+++ b/web-next/components/monitor-detail/monitor-detail-sections.test.tsx
@@ -51,6 +51,25 @@ vi.mock('../workbench/workbench-page', () => ({
 const t = createTranslatorMock();
 
 describe('monitor detail sections', () => {
+  it('does not render a full-width realtime loading stage above metric cards', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-sections.tsx'), 'utf8');
+    const signalListSource = source.slice(
+      source.indexOf('function MetricSignalList'),
+      source.indexOf('function MonitorBasicSharedSurface')
+    );
+    const realtimeStart = source.indexOf('const realtimeNode = (');
+    const historyStart = source.indexOf('const historyNode = (', realtimeStart);
+    const realtimeSource = source.slice(realtimeStart, historyStart);
+
+    expect(realtimeSource).toContain('<MetricSignalList');
+    expect(signalListSource).toContain('data-monitor-detail-realtime-payload-errors="card-local"');
+    expect(realtimeSource).not.toContain('data-monitor-detail-state-owner="hertzbeat-ui-loading-state"');
+    expect(realtimeSource).not.toContain('metricPayloadLoading ?');
+    expect(realtimeSource).not.toContain('metricPayloadError ?');
+    expect(realtimeSource).not.toContain('data-monitor-detail-state-scope="realtime"');
+    expect(realtimeSource).not.toContain('<HzLoadingState');
+  });
+
   it('builds all five section outputs through the named consumer', async () => {
     const { MonitorDetailSections } = await import('./monitor-detail-sections');
     const sections = MonitorDetailSections({
@@ -69,6 +88,8 @@ describe('monitor detail sections', () => {
         { key: 'summary', title: 'summary', copy: '0', meta: 'fields' },
         { key: 'header', title: 'header', copy: '0', meta: 'fields' }
       ],
+      realtimeMetricTotalCount: 4,
+      realtimeMetricHasMore: true,
       metricCardPayloads: {
         summary: {
           loading: false,
@@ -88,7 +109,7 @@ describe('monitor detail sections', () => {
         }
       },
       metrics: [{ name: 'summary' }, { name: 'header' }] as any,
-      favoriteNames: [],
+      favoriteNames: ['summary'],
       favoriteJumpRows: [
         {
           key: 'realtime:summary',
@@ -111,6 +132,9 @@ describe('monitor detail sections', () => {
       ],
       selectedFavoriteKey: 'realtime:summary',
       favoriteSurfaceMode: 'realtime',
+      favoriteRealtimeMetricVisibleCount: 1,
+      favoriteRealtimeMetricTotalCount: 2,
+      favoriteRealtimeMetricHasMore: true,
       selectedMetricKey: 'summary',
       metricPayload: null,
       metricPayloadLoading: false,
@@ -129,6 +153,9 @@ describe('monitor detail sections', () => {
         { key: 'cpu:usage', title: 'cpu.usage', copy: '%', meta: 'history' },
         { key: 'memory:used', title: 'memory.used', copy: 'MB', meta: 'history' }
       ],
+      historyChartVisibleCount: 2,
+      historyChartTotalCount: 4,
+      historyChartHasMore: true,
       selectedHistoryMetricKey: 'cpu:usage',
       historySeriesSearch: '',
       historySeriesRows: [],
@@ -169,14 +196,18 @@ describe('monitor detail sections', () => {
       historyWindows: [{ value: '1h', label: '1h' }],
       historyModes: [{ value: false, label: 'Raw' }],
       historyFullscreen: false,
-      grafanaState: { enabled: false },
-      grafanaMessage: null,
+      favoriteHistoryChartVisibleCount: 1,
+      favoriteHistoryChartTotalCount: 2,
+      favoriteHistoryChartHasMore: true,
+      grafanaState: { enabled: true, url: 'https://grafana.example/d/monitor' },
+      grafanaMessage: 'Dashboard deleted',
       grafanaError: null,
       favoriteMessage: null,
       favoriteError: null,
       onMetricSearchChange: () => {},
       onSelectMetric: () => {},
       onToggleFavorite: () => {},
+      onLoadMoreRealtimeMetrics: () => {},
       onSelectMetricRow: () => {},
       onMetricModeChange: () => {},
       onMetricRefresh: () => {},
@@ -188,11 +219,15 @@ describe('monitor detail sections', () => {
       onSelectHistorySeries: () => {},
       onSelectHistoryPoint: () => {},
       onHistoryRefresh: () => {},
+      onFavoriteHistoryRefresh: () => {},
+      onLoadMoreHistoryCharts: () => {},
       onHistoryWindowChange: () => {},
       onHistoryModeChange: () => {},
       onHistoryFullscreenToggle: () => {},
       onSelectFavorite: () => {},
       onSetFavoriteSurfaceMode: () => {},
+      onLoadMoreFavoriteRealtimeMetrics: () => {},
+      onLoadMoreFavoriteHistoryCharts: () => {},
       onRemoveFavoriteToken: () => {},
       onDeleteGrafanaDashboard: () => {},
       t
@@ -218,27 +253,50 @@ describe('monitor detail sections', () => {
     expect(realtimeHtml).toContain('Monitoring Basic');
     expect(realtimeHtml).not.toContain('This card shows the base observation details and latest status for monitor instance 10.0.0.1.');
     expect(realtimeHtml).not.toContain('HertzBeat Collection');
-    expect(realtimeHtml).toContain('data-monitor-basic-edit-action="monitor-data-table"');
+    expect(realtimeHtml).toContain('data-hz-ui="monitor-basic-card"');
+    expect(realtimeHtml).toContain('data-monitor-basic-owner="hertzbeat-ui-basic-card"');
+    expect(realtimeHtml).toContain('data-monitor-basic-edit-action="hertzbeat-ui-icon-action"');
     expect(realtimeHtml).toContain('href="/monitors/42/edit?app=http"');
     expect(realtimeHtml).toContain('aria-label="Edit monitor"');
     expect(realtimeHtml).toContain('summary-card');
-    expect(realtimeHtml).toContain('data-monitor-detail-realtime-card-flow="angular-cards-list"');
+    expect(realtimeHtml).toContain('data-hz-ui="monitor-metric-card-grid"');
+    expect(realtimeHtml).toContain('data-monitor-detail-realtime-card-flow="shared-metric-card-grid"');
+    expect(realtimeHtml).not.toContain('data-monitor-detail-realtime-card-flow="angular-cards-list"');
+    expect(realtimeHtml).toContain('data-hz-ui="workbench-surface"');
+    expect(realtimeHtml).toContain('data-hz-workbench-surface-selected="false"');
     expect(realtimeHtml).toContain('data-monitor-detail-realtime-card-grid="basic-and-metrics"');
-    expect(realtimeHtml).toContain('data-monitor-detail-realtime-reference="apache-hertzbeat-master-cards-lists"');
-    expect(realtimeHtml).toContain('data-monitor-detail-realtime-card-height="angular-400px"');
-    expect(realtimeHtml).toContain('data-monitor-detail-realtime-card-chrome="angular-card-box"');
-    expect(realtimeHtml).toContain('data-monitor-basic-grid-item="angular-first-card"');
-    expect(realtimeHtml.indexOf('data-monitor-basic-grid-item="angular-first-card"')).toBeLessThan(
+    expect(realtimeHtml).toContain('data-monitor-detail-realtime-reference="hertzbeat-ui-monitor-card-grid"');
+    expect(realtimeHtml).toContain('data-monitor-detail-realtime-card-height="content-driven"');
+    expect(realtimeHtml).toContain('data-monitor-detail-realtime-card-chrome="hertzbeat-ui-card-grid"');
+    expect(realtimeHtml).toContain('data-hz-ui="monitor-incremental-load-footer"');
+    expect(realtimeHtml).toContain('data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"');
+    expect(realtimeHtml).toContain('data-monitor-detail-incremental-mode="angular-sentinel"');
+    expect(realtimeHtml).toContain('data-monitor-detail-incremental-scope="realtime"');
+    expect(realtimeHtml).toContain('data-hz-monitor-incremental-sentinel="true"');
+    expect(realtimeHtml).toContain('data-hz-monitor-incremental-action="load-more"');
+    expect(realtimeHtml).toContain('data-monitor-basic-grid-item="shared-first-card"');
+    expect(realtimeHtml.indexOf('data-monitor-basic-grid-item="shared-first-card"')).toBeLessThan(
       realtimeHtml.indexOf('data-monitor-detail-signal-row="summary"')
     );
     expect(realtimeHtml.indexOf('data-monitor-detail-signal-row="summary"')).toBeLessThan(
       realtimeHtml.indexOf('data-monitor-detail-signal-row="header"')
     );
-    expect(realtimeHtml).toContain('data-monitor-basic-stage-surface="monitor-data-table"');
-    expect(realtimeHtml).toContain('data-monitor-basic-card-chrome="angular-card-box"');
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-workbench-surface[^\"]*monitor-workbench-surface--plain/);
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-workbench-surface__header/);
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-workbench-card-title__title/);
+    expect(realtimeHtml).toContain('data-monitor-basic-stage-surface="hertzbeat-ui-basic-card"');
+    expect(realtimeHtml).toContain('data-monitor-basic-card-chrome="hertzbeat-ui-basic-card"');
+    expect(realtimeHtml).toContain('data-monitor-basic-card-tone="neutral-graphite"');
+    expect(realtimeHtml).toContain('bg-[var(--hz-ui-surface-graphite)]');
+    const basicCardStart = realtimeHtml.indexOf('data-monitor-basic-stage-surface="hertzbeat-ui-basic-card"');
+    const basicCardEnd = realtimeHtml.indexOf('data-monitor-detail-metric-card-owner="hertzbeat-ui-metric-card"', basicCardStart);
+    const basicCardHtml = realtimeHtml.slice(basicCardStart, basicCardEnd);
+    expect(basicCardHtml).not.toContain('bg-[var(--hz-ui-surface-raised)]');
+    expect(basicCardHtml).not.toContain('bg-[var(--hz-ui-active)]');
+    expect(realtimeHtml).not.toContain('data-monitor-basic-card-chrome="angular-card-box"');
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-workbench-surface[^\"]*monitor-workbench-surface--plain/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-card[^\"]*monitor-detail-card--overview/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-workbench-surface__edit/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-workbench-surface__header/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-workbench-card-title__copy/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-workbench-card-title__title/);
     expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-workbench-card-title__kicker/);
     expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-workbench-card-title__summary/);
     expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-workbench-card-title__badge/);
@@ -249,11 +307,18 @@ describe('monitor detail sections', () => {
     expect(realtimeHtml).not.toContain('data-monitor-realtime-action-band="monitor-data-table"');
     expect(realtimeHtml).not.toContain('data-monitor-realtime-action-group="metrics-card-extra"');
     expect(realtimeHtml).not.toContain('data-monitor-realtime-collect-time="true"');
-    expect(realtimeHtml).toContain('data-monitor-detail-tab-sequence="angular-tight"');
-    expect(realtimeHtml).toContain('data-monitor-detail-stage-rhythm="angular-tight"');
-    expect(realtimeHtml).toContain('data-monitor-detail-signal-list-rhythm="angular-tight"');
-    expect(realtimeHtml).toContain('data-monitor-detail-card-grid-rhythm="angular-tight"');
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-detail-tab-sequence[^\"]*monitor-detail-tab-sequence--angular-tight[^\"]*space-y-2/);
+    expect(realtimeHtml).toContain('data-hz-ui="monitor-detail-tab-sequence"');
+    expect(realtimeHtml).toContain('data-monitor-detail-tab-sequence-owner="hertzbeat-ui-detail-tab-sequence"');
+    expect(realtimeHtml).toContain('data-monitor-detail-tab-sequence="shared-tight"');
+    expect(realtimeHtml).not.toContain('data-monitor-detail-tab-sequence="angular-tight"');
+    const sharedTabSequenceClassMatch = realtimeHtml.match(/data-monitor-detail-tab-sequence-owner="hertzbeat-ui-detail-tab-sequence"[^>]*class="([^"]+)"/);
+    expect(sharedTabSequenceClassMatch?.[1]).not.toContain('monitor-detail-tab-sequence');
+    expect(realtimeHtml).toContain('data-hz-ui="monitor-detail-stage"');
+    expect(realtimeHtml).toContain('data-monitor-detail-stage-owner="hertzbeat-ui-detail-stage"');
+    expect(realtimeHtml).toContain('data-monitor-detail-stage-rhythm="shared-tight"');
+    expect(realtimeHtml).toContain('data-monitor-detail-signal-list-rhythm="shared-tight"');
+    expect(realtimeHtml).toContain('data-monitor-detail-card-grid-rhythm="shared-tight"');
+    expect(realtimeHtml).not.toContain('monitor-detail-tab-sequence--angular-tight');
     expect(realtimeHtml).not.toContain('data-monitor-detail-signal-summary-strip="monitor-data-table"');
     expect(realtimeHtml).not.toContain('data-monitor-detail-panel-summary-rhythm="angular-chip-row"');
     expect(realtimeHtml).not.toContain('data-monitor-detail-panel-summary-density="angular-pill-toolbar"');
@@ -275,27 +340,49 @@ describe('monitor detail sections', () => {
     expect(realtimeHtml).not.toContain('data-observability-panel-tone="operator-sheet"');
     expect(realtimeHtml).toContain('data-monitor-detail-signal-list="true"');
     expect(realtimeHtml).toContain('data-monitor-detail-signal-grid="monitor-data-table"');
-    expect(realtimeHtml).toContain('data-monitor-detail-signal-list-geometry="angular-two-column-metric-cards"');
-    expect(realtimeHtml).toContain('data-monitor-detail-signal-row-density="angular-metric-card"');
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-detail-card-grid[^\"]*monitor-detail-card-grid--realtime/);
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-detail-signal-card-grid[^\"]*grid[^\"]*grid-cols-1[^\"]*gap-2[^\"]*lg:grid-cols-2/);
+    expect(realtimeHtml).toContain('data-monitor-detail-signal-list-geometry="shared-two-column-metric-cards"');
+    expect(realtimeHtml).toContain('data-hz-ui="monitor-metric-card"');
+    expect(realtimeHtml).toContain('data-monitor-detail-metric-card-owner="hertzbeat-ui-metric-card"');
+    expect(realtimeHtml).toContain('data-monitor-detail-signal-row-density="shared-metric-card"');
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-card-grid/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-signal-card-grid/);
+    const sharedGridClassMatch = realtimeHtml.match(/data-monitor-detail-card-grid-rhythm="shared-tight"[^>]*class="([^"]+)"/);
+    expect(sharedGridClassMatch?.[1]).toEqual(expect.stringContaining('grid'));
+    expect(sharedGridClassMatch?.[1]).toEqual(expect.stringContaining('grid-cols-1'));
+    expect(sharedGridClassMatch?.[1]).toEqual(expect.stringContaining('gap-2'));
+    expect(sharedGridClassMatch?.[1]).toEqual(expect.stringContaining('lg:grid-cols-2'));
     expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-signal-card-stack/);
     expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-signal-table[^\"]*divide-y/);
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-detail-card[^\"]*monitor-detail-card--signal-flat/);
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-detail-card--signal-metric/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-card[^\"]*monitor-detail-card--signal-flat/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-card--signal-metric/);
     expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-card--signal-row/);
     expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-card--signal-card-row/);
     expect(realtimeHtml).not.toContain('monitor-realtime-data-table');
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*min-h-\[400px\][^\"]*grid[^\"]*content-start[^\"]*gap-3[^\"]*rounded-\[3px\][^\"]*border[^\"]*p-3/);
-    expect(realtimeHtml).toMatch(/class=\"[^\"]*monitor-detail-stage[^\"]*monitor-detail-stage--signals/);
+    const sharedMetricClassMatch = realtimeHtml.match(/data-monitor-detail-metric-card-tone="neutral-graphite" class="([^"]+)"/);
+    expect(sharedMetricClassMatch?.[1]).toEqual(expect.stringContaining('grid'));
+    expect(sharedMetricClassMatch?.[1]).toEqual(expect.stringContaining('min-h-[400px]'));
+    expect(sharedMetricClassMatch?.[1]).toEqual(expect.stringContaining('content-start'));
+    expect(sharedMetricClassMatch?.[1]).toEqual(expect.stringContaining('gap-3'));
+    expect(sharedMetricClassMatch?.[1]).toEqual(expect.stringContaining('rounded-[3px]'));
+    expect(sharedMetricClassMatch?.[1]).toEqual(expect.stringContaining('border'));
+    expect(sharedMetricClassMatch?.[1]).toEqual(expect.stringContaining('p-3'));
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-stage/);
+    expect(realtimeHtml).not.toMatch(/class=\"[^\"]*monitor-detail-stage--signals/);
     expect(realtimeHtml).toContain('data-monitor-detail-signal-row="summary"');
     expect(realtimeHtml).toContain('data-monitor-detail-signal-card="true"');
-    expect(realtimeHtml).toContain('data-monitor-detail-signal-card-chrome="angular-card-box"');
+    expect(realtimeHtml).toContain('data-monitor-detail-signal-card-chrome="hertzbeat-ui-metric-card"');
+    expect(realtimeHtml).toContain('data-monitor-detail-metric-card-tone="neutral-graphite"');
+    expect(realtimeHtml).toContain('bg-[var(--hz-ui-surface-graphite)]');
+    const metricCardStart = realtimeHtml.indexOf('data-monitor-detail-metric-card-owner="hertzbeat-ui-metric-card"');
+    const metricCardHtml = realtimeHtml.slice(metricCardStart);
+    expect(metricCardHtml).not.toContain('bg-[var(--hz-ui-surface-raised)]');
+    expect(metricCardHtml).not.toContain('bg-[var(--hz-ui-active-soft)]');
+    expect(realtimeHtml).not.toContain('data-selected=');
     expect(realtimeHtml).toContain('data-monitor-detail-signal-card-table="true"');
-    expect(realtimeHtml).toContain('data-monitor-detail-signal-card-body-density="angular-card-table"');
-    expect(realtimeHtml).toContain('data-monitor-detail-signal-selected-style="angular-neutral"');
-    expect(realtimeHtml).not.toContain('data-monitor-detail-signal-selected-style="left-rail"');
-    expect(realtimeHtml).toContain('data-monitor-detail-signal-card-table-row="metric-fields"');
+    expect(realtimeHtml).toContain('data-monitor-detail-signal-card-body-density="shared-metric-table"');
+    expect(realtimeHtml).toContain('data-monitor-detail-signal-selected-style="left-rail"');
+    expect(realtimeHtml).not.toContain('data-monitor-detail-signal-selected-style="angular-neutral"');
+    expect(realtimeHtml).toContain('data-hz-monitor-metric-table-row="metric-fields"');
     expect(realtimeHtml).toContain('data-monitor-detail-signal-row-title="true"');
     expect(realtimeHtml).not.toMatch(/>\s*\d+\s+(?:Fields|字段)\s*</);
     expect(realtimeHtml).not.toContain('data-monitor-detail-signal-card-table-row="source"');
@@ -304,9 +391,10 @@ describe('monitor detail sections', () => {
     expect(realtimeHtml).not.toContain('data-monitor-detail-signal-header-actions="plain-meta"');
     expect(realtimeHtml).not.toContain('data-monitor-detail-signal-source-badge="true"');
     expect(realtimeHtml).not.toContain('data-monitor-detail-signal-source-density="plain-meta"');
-    expect(realtimeHtml).not.toContain('data-monitor-detail-signal-row-action="favorite"');
-    expect(realtimeHtml).not.toContain('aria-label="Add favorite"');
-    expect(realtimeHtml).not.toContain('aria-label="Remove favorite"');
+    expect(realtimeHtml).toContain('data-monitor-detail-signal-row-action="favorite"');
+    expect(realtimeHtml).toContain('data-hz-ui="monitor-metric-favorite-action"');
+    expect(realtimeHtml).toContain('aria-label="Add favorite"');
+    expect(realtimeHtml).toContain('aria-label="Remove favorite"');
     expect(realtimeHtml).not.toContain('bg-[var(--ops-surface-elevated)]');
     expect(realtimeHtml).not.toContain('border-[var(--ops-primary)]');
     expect(realtimeHtml).not.toContain('shadow-[inset_2px_0_0_var(--ops-primary)]');
@@ -323,37 +411,79 @@ describe('monitor detail sections', () => {
     expect(realtimeHtml).not.toContain('Metric catalog');
     expect(realtimeHtml).not.toContain('Metric fields');
     expect((realtimeHtml.match(/HertzBeat Collection/g) ?? []).length).toBe(0);
-    expect(historyHtml).toContain('data-monitor-detail-history-layout="angular-chart-cards-only"');
-    expect(historyHtml).toContain('data-monitor-detail-history-reference="apache-hertzbeat-master-cards"');
+    expect(historyHtml).toContain('data-monitor-detail-history-layout="hertzbeat-ui-history-chart-grid"');
+    expect(historyHtml).toContain('data-monitor-detail-history-reference="hertzbeat-ui-history-chart-grid"');
     expect(historyHtml).not.toContain('data-monitor-detail-flat-stage="true"');
     expect(historyHtml).not.toContain('data-observability-panel-tone="operator-sheet"');
-    expect(historyHtml).toContain('data-monitor-history-chart-grid="angular-chart-cards"');
+    expect(historyHtml).toContain('data-hz-ui="monitor-history-chart-grid"');
+    expect(historyHtml).toContain('data-monitor-history-chart-grid="shared-history-chart-grid"');
+    expect(historyHtml).toContain('data-monitor-history-chart-grid-owner="hertzbeat-ui-history-chart-grid"');
     expect(historyHtml).toContain('data-monitor-history-chart-visual="shared-timeseries"');
     expect(historyHtml).toContain('data-monitor-history-axis-policy="sparse-readable"');
     expect(historyHtml).toContain('data-monitor-history-navigator="echarts-native-slider"');
-    expect(historyHtml).toContain('data-monitor-history-datazoom-state="local-observation"');
+    expect(historyHtml).toContain('data-monitor-history-datazoom-state="toolbar-feedback"');
     expect(historyHtml).toContain('data-monitor-history-datazoom-preserve="preserved"');
-    expect(historyHtml).toContain('data-monitor-history-time-toolbar="shared-time-context-control"');
-    expect(historyHtml).toContain('data-time-range-control="hertzbeat-shared"');
-    expect(historyHtml).toContain('data-time-range-control-state="applied"');
-    expect(historyHtml).toContain('data-monitor-history-time-range-select="true"');
+    expect(historyHtml).toContain('data-monitor-history-time-toolbar-owner="hertzbeat-ui-time-range-toolbar"');
+    expect(historyHtml).toContain('data-hz-ui="time-range-toolbar"');
+    expect(historyHtml).toContain('data-hz-time-range-toolbar-state="applied"');
+    expect(historyHtml).not.toContain('data-time-range-control="hertzbeat-shared"');
+    expect(historyHtml).toContain('data-hz-time-range-toolbar-time-entry="single-expression-picker"');
+    expect(historyHtml).toContain('data-hz-time-range-toolbar-preset-placement="picker-panel"');
+    expect(historyHtml).toContain('data-hz-time-range-toolbar-absolute="picker-panel"');
+    expect(historyHtml).toContain('data-hz-expression-time-range-picker-layout="expression-single-range"');
+    expect(historyHtml).not.toContain('data-monitor-history-time-range-select="true"');
     expect(historyHtml).toContain('data-monitor-history-time-range-option="last-1h"');
-    expect(historyHtml).toContain('data-monitor-history-mode-select="false"');
+    expect(historyHtml).toContain('data-monitor-history-mode-owner="hertzbeat-ui-tabs"');
+    expect(historyHtml).toContain('data-monitor-history-mode-active="raw"');
+    expect(historyHtml).toContain('data-hz-ui="tabs"');
+    expect(historyHtml).not.toContain('data-monitor-history-mode-option');
     expect(historyHtml).toContain('data-monitor-history-refresh-action="true"');
-    expect(historyHtml).toContain('data-monitor-history-card-chrome="angular-card-box"');
-    expect(historyHtml).toContain('data-monitor-history-card-height="angular-460px"');
+    expect(historyHtml).toContain('data-hz-ui="monitor-history-chart-card"');
+    expect(historyHtml).toContain('data-monitor-history-card-owner="hertzbeat-ui-history-chart-card"');
+    expect(historyHtml).toContain('data-monitor-history-card-source="hertzbeat-ui-history-chart"');
+    expect(historyHtml).toContain('data-monitor-history-card-chrome="hertzbeat-ui-history-chart-inline"');
+    expect(historyHtml).toContain('data-monitor-history-card-height="content-driven"');
+    expect(historyHtml).not.toContain('angular-chart-cards');
+    expect(historyHtml).not.toContain('angular-card-box');
+    expect(historyHtml).not.toContain('angular-460px');
+    expect(historyHtml).not.toContain('data-selected');
     expect(historyHtml).toContain('data-monitor-history-card="cpu:usage"');
     expect(historyHtml).toContain('data-monitor-history-card="memory:used"');
+    expect(historyHtml).toContain('data-hz-ui="monitor-incremental-load-footer"');
+    expect(historyHtml).toContain('data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"');
+    expect(historyHtml).toContain('data-monitor-detail-incremental-mode="angular-chart-sentinel"');
+    expect(historyHtml).toContain('data-monitor-detail-incremental-scope="history"');
+    expect(historyHtml).toContain('data-hz-monitor-incremental-visible="2"');
+    expect(historyHtml).toContain('data-hz-monitor-incremental-total="4"');
+    expect(historyHtml).toContain('data-hz-monitor-incremental-action="load-more"');
+    expect(historyHtml).toContain('data-monitor-history-card-action="favorite"');
+    expect(historyHtml).toContain('data-monitor-history-card-action-owner="hertzbeat-ui-favorite-action"');
+    expect(historyHtml).toContain('data-hz-ui="monitor-metric-favorite-action"');
     expect(historyHtml).not.toContain('monitor.detail.history-series.search.count');
     expect(historyHtml).not.toContain('monitor.detail.history-metric.search.count');
     expect(historyHtml).not.toContain('History store has not returned series yet.');
-    expect(historyHtml).not.toContain('history-panel');
+    expect(historyHtml).not.toContain('>history-panel<');
     expect(historyHtml).not.toContain('History catalog');
     expect(historyHtml).not.toContain('History series');
-    expect(favoritesHtml).toContain('data-monitor-detail-favorite-layout="angular-favorite-content"');
-    expect(favoritesHtml).toContain('data-monitor-detail-favorite-selector="angular-select-200"');
+    expect(favoritesHtml).toContain('data-hz-ui="monitor-favorite-surface"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-owner="hertzbeat-ui-favorite-surface"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-layout="hertzbeat-ui-favorite-surface"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-feedback-copy="angular-favorite-copy"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-set-semantics="angular-set"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-active-fallback="angular-subselector-sticky"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-history-reload-reset="angular-chart-reload"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-empty-contract="angular-subselector-empty"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-selector="hertzbeat-ui-select-menu"');
+    expect(favoritesHtml).toContain('data-hz-ui="select-menu"');
     expect(favoritesHtml).toContain('data-monitor-detail-favorite-mode="realtime"');
-    expect(favoritesHtml).toContain('data-monitor-detail-favorite-realtime="cards-lists"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-realtime-incremental="angular-favorite-sentinel"');
+    expect(favoritesHtml).toContain('data-hz-ui="monitor-favorite-pane"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-pane-owner="hertzbeat-ui-favorite-pane"');
+    expect(favoritesHtml).toContain('data-monitor-detail-favorite-realtime="shared-pane"');
+    expect(favoritesHtml).toContain('data-monitor-detail-incremental-mode="angular-favorite-sentinel"');
+    expect(favoritesHtml).toContain('data-monitor-detail-incremental-scope="favorite-realtime"');
+    expect(favoritesHtml).not.toContain('data-monitor-detail-favorite-realtime="cards-lists"');
+    expect(favoritesHtml).not.toContain('data-monitor-detail-favorite-history="cards"');
     expect(favoritesHtml).not.toContain('data-monitor-detail-flat-stage="true"');
     expect(favoritesHtml).not.toContain('data-observability-panel-tone="operator-sheet"');
     expect(favoritesHtml).not.toContain('realtime-panel');
@@ -361,17 +491,46 @@ describe('monitor detail sections', () => {
     expect(favoritesHtml).not.toContain('Favorite catalog');
     expect(grafanaHtml).toContain('Grafana');
     expect(grafanaHtml).toContain('data-monitor-detail-flat-stage="true"');
+    expect(grafanaHtml).toContain('data-hz-ui="monitor-detail-stage"');
+    expect(grafanaHtml).toContain('data-monitor-detail-stage-owner="hertzbeat-ui-detail-stage"');
+    expect(grafanaHtml).toContain('data-monitor-detail-grafana-layout-owner="hertzbeat-ui-detail-stage"');
+    expect(grafanaHtml).toContain('data-monitor-detail-grafana-action-contract="angular-edit-config-delete-dashboard"');
+    expect(grafanaHtml).toContain('data-monitor-detail-grafana-actions-contract="angular-dashboard-actions"');
+    expect(grafanaHtml).toContain('data-monitor-detail-grafana-config-owner="hertzbeat-ui-button-link"');
+    expect(grafanaHtml).toContain('data-monitor-detail-grafana-delete-action="delete-dashboard"');
+    expect(grafanaHtml).toContain('data-monitor-detail-grafana-delete-teardown="angular-hide-tab"');
     expect(grafanaHtml).not.toContain('data-observability-panel-tone="operator-sheet"');
   }, 30000);
 
-  it('routes favorites through the Angular favorite selector content instead of a preview workbench', () => {
+  it('routes favorites through the shared HertzBeat UI favorite surface instead of a preview workbench', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-sections.tsx'), 'utf8');
 
-    expect(source).toContain('data-monitor-detail-favorite-layout="angular-favorite-content"');
-    expect(source).toContain('data-monitor-detail-favorite-selector="angular-select-200"');
-    expect(source).toContain('data-monitor-detail-favorite-realtime="cards-lists"');
-    expect(source).toContain('data-monitor-detail-favorite-history="cards"');
-    expect(source).toContain('onSetFavoriteSurfaceMode(event.target.value as');
+    expect(source).toContain('HzMonitorFavoriteSurface');
+    expect(source).toContain('data-monitor-detail-favorite-owner="hertzbeat-ui-favorite-surface"');
+    expect(source).toContain('data-monitor-detail-favorite-layout="hertzbeat-ui-favorite-surface"');
+    expect(source).toContain('data-monitor-detail-favorite-feedback-copy="angular-favorite-copy"');
+    expect(source).toContain('data-monitor-detail-favorite-set-semantics="angular-set"');
+    expect(source).toContain('data-monitor-detail-favorite-active-fallback="angular-subselector-sticky"');
+    expect(source).toContain('data-monitor-detail-favorite-history-reload-reset="angular-chart-reload"');
+    expect(source).toContain('data-monitor-detail-favorite-empty-contract="angular-subselector-empty"');
+    expect(source).toContain('data-monitor-detail-favorite-empty-teardown="angular-empty-no-sentinel"');
+    expect(source).toContain('data-monitor-detail-favorite-realtime-incremental="angular-favorite-sentinel"');
+    expect(source).toContain("'data-monitor-detail-favorite-selector': 'hertzbeat-ui-select-menu'");
+    expect(source).toContain('HzMonitorFavoritePane');
+    expect(source).toContain('data-monitor-detail-favorite-realtime="shared-pane"');
+    expect(source).toContain('incrementalMode="angular-favorite-sentinel"');
+    expect(source).toContain('incrementalScope="favorite-realtime"');
+    expect(source).toContain('data-monitor-detail-favorite-history="shared-pane"');
+    expect(source).toContain('data-monitor-detail-favorite-history-source="favorite-history-chart-payloads"');
+    expect(source).toContain('data-monitor-detail-favorite-history-controls="angular-chart-toolbox"');
+    expect(source).toContain('showControls={true}');
+    expect(source).toContain('onRefresh={onFavoriteHistoryRefresh}');
+    expect(source).toContain('onToggleFavorite={onToggleHistoryFavorite}');
+    expect(source).not.toContain('data-monitor-detail-favorite-realtime="cards-lists"');
+    expect(source).not.toContain('data-monitor-detail-favorite-history="cards"');
+    expect(source).toContain("onSetFavoriteSurfaceMode(value as 'realtime' | 'history')");
+    expect(source).not.toContain("import { Select } from '../ui/select'");
+    expect(source).not.toContain('className="favorite-content');
     expect(source).not.toContain('<MonitorRealtimePanel');
     expect(source).not.toContain('<MonitorHistoryPanel');
     expect(source).not.toContain("title={t('monitor.detail.favorite.surface.title')}");
@@ -387,25 +546,28 @@ describe('monitor detail sections', () => {
   it('routes the basic monitor stage through monitor-data-table plain surface chrome', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-sections.tsx'), 'utf8');
     const basicSurfaceSource = source.slice(
-      source.indexOf('function MonitorBasicPlainSurface'),
+      source.indexOf('function MonitorBasicSharedSurface'),
       source.indexOf('export type MonitorSectionCompositionProps')
     );
 
-    expect(basicSurfaceSource).toContain('data-monitor-basic-stage-surface="monitor-data-table"');
-    expect(basicSurfaceSource).toContain('data-monitor-basic-grid-item="angular-first-card"');
-    expect(basicSurfaceSource).toContain('data-monitor-basic-card-chrome="angular-card-box"');
-    expect(basicSurfaceSource).toContain('min-h-[400px] min-w-0 grid content-start gap-3 rounded-[3px] border p-3');
-    expect(basicSurfaceSource).toContain('monitor-workbench-surface monitor-workbench-surface--plain');
-    expect(basicSurfaceSource).toContain('monitor-workbench-surface__header');
+    expect(source).not.toContain('function MonitorBasicPlainSurface');
+    expect(basicSurfaceSource).toContain('HzMonitorBasicCard');
+    expect(basicSurfaceSource).toContain('data-monitor-basic-owner="hertzbeat-ui-basic-card"');
+    expect(basicSurfaceSource).not.toContain('data-monitor-basic-stage-surface="monitor-data-table"');
+    expect(basicSurfaceSource).not.toContain('data-monitor-basic-grid-item="angular-first-card"');
+    expect(basicSurfaceSource).not.toContain('data-monitor-basic-card-chrome="angular-card-box"');
+    expect(basicSurfaceSource).not.toContain('min-h-[400px] min-w-0 grid content-start gap-3 rounded-[3px] border p-3');
+    expect(basicSurfaceSource).not.toContain('monitor-workbench-surface monitor-workbench-surface--plain');
+    expect(basicSurfaceSource).not.toContain('monitor-workbench-surface__header');
     expect(basicSurfaceSource).not.toContain('monitor-workbench-card-title__kicker');
     expect(basicSurfaceSource).not.toContain('monitor-workbench-card-title__summary');
     expect(basicSurfaceSource).not.toContain('monitor-workbench-card-title__badge');
     expect(basicSurfaceSource).not.toContain('data-monitor-basic-source-badge-density="plain-meta"');
-    expect(basicSurfaceSource).toContain('data-monitor-basic-edit-action="monitor-data-table"');
-    expect(basicSurfaceSource).toContain('data-monitor-basic-edit-action-density="plain-icon"');
+    expect(basicSurfaceSource).not.toContain('data-monitor-basic-edit-action="monitor-data-table"');
+    expect(basicSurfaceSource).not.toContain('data-monitor-basic-edit-action-density="plain-icon"');
     expect(basicSurfaceSource).toContain('editHref');
-    expect(basicSurfaceSource).toContain('Pencil');
-    expect(basicSurfaceSource).toContain('monitor-workbench-surface__edit inline-flex h-6 w-6 items-center justify-center border-0 bg-transparent');
+    expect(basicSurfaceSource).not.toContain('Pencil');
+    expect(basicSurfaceSource).not.toContain('monitor-workbench-surface__edit inline-flex h-6 w-6 items-center justify-center border-0 bg-transparent');
     expect(basicSurfaceSource).not.toContain('hover:border-[var(--ops-border-color)]');
     expect(basicSurfaceSource).not.toContain('monitor.detail.table.kicker.monitor');
     expect(basicSurfaceSource).not.toContain('monitor.detail.table.copy.monitor');
@@ -413,11 +575,11 @@ describe('monitor detail sections', () => {
     expect(source).not.toContain('description={t(\'monitor.detail.workbench.copy\'');
   });
 
-  it('keeps history as direct Angular chart cards instead of the old drilldown/catalog stack', () => {
+  it('keeps history as shared chart cards instead of the old drilldown/catalog stack', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-sections.tsx'), 'utf8');
 
-    expect(source).toContain('data-monitor-detail-history-layout="angular-chart-cards-only"');
-    expect(source).toContain('data-monitor-detail-history-reference="apache-hertzbeat-master-cards"');
+    expect(source).toContain('data-monitor-detail-history-layout="hertzbeat-ui-history-chart-grid"');
+    expect(source).toContain('data-monitor-detail-history-reference="hertzbeat-ui-history-chart-grid"');
     expect(source).toContain('showControls={true}');
     expect(source).toContain('historyWindow={historyWindow}');
     expect(source).toContain('historyWindows={historyWindows}');
@@ -426,6 +588,8 @@ describe('monitor detail sections', () => {
     expect(source).not.toContain("title={t('monitor.detail.section.history-catalog.title')}");
     expect(source).not.toContain("title={t('monitor.detail.history.series.title')}");
     expect(source).not.toContain('ObservabilitySearchInput');
+    expect(source).not.toContain('angular-chart-cards');
+    expect(source).not.toContain('apache-hertzbeat-master-cards');
   });
 
   it('keeps metric signal rows free of redundant source badges and summary strips', () => {
@@ -445,39 +609,55 @@ describe('monitor detail sections', () => {
     expect(signalListSource).not.toContain('monitor.detail.panel.summary.total');
     expect(signalListSource).not.toContain('monitor.detail.panel.summary.visible');
     expect(signalListSource).not.toContain('monitor.detail.panel.summary.source');
-    expect(signalListSource).toContain('data-monitor-detail-tab-sequence="angular-tight"');
-    expect(signalListSource).toContain('monitor-detail-tab-sequence monitor-detail-tab-sequence--angular-tight space-y-2');
-    expect(source).toContain('data-monitor-detail-stage-rhythm={rhythm === \'angular-tight\' ? \'angular-tight\' : undefined}');
-    expect(source).toContain('rhythm="angular-tight"');
-    expect(signalListSource).toContain('data-monitor-detail-signal-list-rhythm="angular-tight"');
-    expect(signalListSource).toContain('data-monitor-detail-signal-list-geometry="angular-two-column-metric-cards"');
-    expect(signalListSource).toContain('data-monitor-detail-realtime-card-flow="angular-cards-list"');
+    expect(source).toContain('HzMonitorDetailTabSequence');
+    expect(source).not.toContain('function MonitorRealtimeTightSequence');
+    expect(source).not.toContain('data-monitor-detail-tab-sequence="angular-tight"');
+    expect(source).not.toContain("className={cn('monitor-detail-tab-sequence");
+    expect(source).not.toContain('monitor-detail-tab-sequence monitor-detail-tab-sequence--angular-tight space-y-2');
+    expect(signalListSource).not.toContain('monitor-detail-tab-sequence--angular-tight');
+    expect(signalListSource).not.toContain('data-monitor-detail-tab-sequence="angular-tight"');
+    expect(source).toContain('HzMonitorDetailStage');
+    expect(source).not.toContain('function FlatStage');
+    expect(source).not.toContain('data-monitor-detail-stage-rhythm={rhythm === \'angular-tight\' ? \'angular-tight\' : undefined}');
+    expect(source).not.toContain('className="monitor-detail-stage monitor-detail-stage--signals');
+    expect(source).not.toContain('className="monitor-detail-stage--signals"');
+    expect(source).toContain('rhythm="tight"');
+    expect(signalListSource).toContain('HzMonitorDetailSignalList');
+    expect(signalListSource).toContain('data-monitor-detail-signal-list-owner="hertzbeat-ui-signal-list"');
+    expect(signalListSource).toContain('data-monitor-detail-signal-list-rhythm="shared-tight"');
+    expect(signalListSource).toContain('data-monitor-detail-signal-list-geometry="shared-two-column-metric-cards"');
+    expect(signalListSource).not.toContain('className="space-y-2 pt-0.5"');
+    expect(signalListSource).not.toContain('data-monitor-detail-signal-list-rhythm="angular-tight"');
+    expect(signalListSource).not.toContain('data-monitor-detail-signal-list-geometry="angular-two-column-metric-cards"');
+    expect(signalListSource).toContain('HzMonitorMetricCardGrid');
+    expect(signalListSource).toContain('data-monitor-detail-realtime-card-flow="shared-metric-card-grid"');
+    expect(signalListSource).not.toContain('data-monitor-detail-realtime-card-flow="angular-cards-list"');
     expect(signalListSource).toContain('data-monitor-detail-realtime-card-grid="basic-and-metrics"');
-    expect(signalListSource).toContain('data-monitor-detail-realtime-reference="apache-hertzbeat-master-cards-lists"');
-    expect(signalListSource).toContain('data-monitor-detail-realtime-card-height="angular-400px"');
-    expect(signalListSource).toContain('data-monitor-detail-realtime-card-chrome="angular-card-box"');
+    expect(signalListSource).toContain('data-monitor-detail-realtime-reference="hertzbeat-ui-monitor-card-grid"');
+    expect(signalListSource).toContain('data-monitor-detail-realtime-card-height="content-driven"');
+    expect(signalListSource).toContain('data-monitor-detail-realtime-card-chrome="hertzbeat-ui-card-grid"');
     expect(signalListSource).toContain('{leadingCard}');
-    expect(signalListSource).toContain('data-monitor-detail-card-grid-rhythm="angular-tight"');
+    expect(signalListSource).toContain('data-monitor-detail-card-grid-rhythm="shared-tight"');
     expect(signalListSource).toContain('data-monitor-detail-signal-grid="monitor-data-table"');
-    expect(signalListSource).toContain('monitor-detail-card-grid monitor-detail-card-grid--realtime');
-    expect(signalListSource).toContain('monitor-detail-signal-card-grid grid auto-rows-fr grid-cols-1 items-stretch gap-2 lg:grid-cols-2');
+    expect(signalListSource).not.toContain('monitor-detail-card-grid monitor-detail-card-grid--realtime');
+    expect(signalListSource).not.toContain('monitor-detail-signal-card-grid grid auto-rows-fr grid-cols-1 items-stretch gap-2 lg:grid-cols-2');
     expect(signalListSource).not.toContain('monitor-detail-signal-card-stack');
     expect(signalListSource).not.toContain('monitor-detail-signal-table divide-y divide-[var(--ops-border-color)] border-y border-[var(--ops-border-color)]');
-    expect(signalListSource).toContain('monitor-detail-card monitor-detail-card--signal-flat');
-    expect(signalListSource).toContain('monitor-detail-card--signal-metric');
+    expect(signalListSource).toContain('HzMonitorMetricCard');
+    expect(signalListSource).toContain('data-monitor-detail-metric-card-owner="hertzbeat-ui-metric-card"');
     expect(signalListSource).not.toContain('monitor-detail-card--signal-row');
     expect(signalListSource).not.toContain('monitor-detail-card--signal-card-row');
-    expect(signalListSource).toContain('data-monitor-detail-signal-row-density="angular-metric-card"');
-    expect(signalListSource).toContain('min-h-[400px] min-w-0 grid content-start gap-3 rounded-[3px] border p-3');
-    expect(signalListSource).toContain('data-monitor-detail-signal-row-title="true"');
+    expect(signalListSource).toContain('data-monitor-detail-signal-row-density="shared-metric-card"');
+    expect(signalListSource).not.toContain('data-monitor-detail-signal-row-density="angular-metric-card"');
     expect(signalListSource).toContain('data-monitor-detail-signal-card="true"');
-    expect(signalListSource).toContain('data-monitor-detail-signal-card-chrome="angular-card-box"');
-    expect(signalListSource).toContain('data-monitor-detail-signal-card-table="true"');
-    expect(signalListSource).toContain('data-monitor-detail-signal-card-body-density="angular-card-table"');
-    expect(signalListSource).toContain('data-monitor-detail-signal-selected-style="angular-neutral"');
-    expect(signalListSource).not.toContain('data-monitor-detail-signal-selected-style="left-rail"');
-    expect(signalListSource).toContain('data-monitor-detail-signal-card-table-row="metric-fields"');
-    expect(signalListSource).toContain('border-b border-[var(--ops-border-color)] pb-2');
+    expect(signalListSource).toContain('data-monitor-detail-signal-card-chrome="hertzbeat-ui-metric-card"');
+    expect(signalListSource).not.toContain('data-selected');
+    expect(signalListSource).not.toContain('monitor-workbench-metric-table');
+    expect(signalListSource).not.toContain('data-monitor-detail-signal-card-table="true"');
+    expect(signalListSource).not.toContain('data-monitor-detail-signal-card-body-density="angular-card-table"');
+    expect(signalListSource).toContain('data-monitor-detail-signal-selected-style="left-rail"');
+    expect(signalListSource).not.toContain('data-monitor-detail-signal-selected-style="angular-neutral"');
+    expect(signalListSource).not.toContain('data-monitor-detail-signal-card-table-row="metric-fields"');
     expect(signalListSource).not.toContain('line-clamp-2');
     expect(signalListSource).not.toContain('hover:bg-[var(--ops-surface-panel)]');
     expect(signalListSource).not.toContain('data-monitor-detail-signal-card-table-row="source"');
@@ -488,7 +668,8 @@ describe('monitor detail sections', () => {
     expect(signalListSource).not.toContain('data-monitor-detail-signal-source-badge="true"');
     expect(signalListSource).not.toContain('data-monitor-detail-signal-source-density="plain-meta"');
     expect(signalListSource).not.toContain('monitor-workbench-card-title__badge monitor-workbench-card-title__badge--plain px-0 py-0');
-    expect(signalListSource).not.toContain('data-monitor-detail-signal-row-action="favorite"');
+    expect(signalListSource).toContain('HzMonitorMetricFavoriteAction');
+    expect(signalListSource).toContain('data-monitor-detail-signal-row-action="favorite"');
     expect(signalListSource).not.toContain('<Star');
     expect(source).not.toContain("import { Star } from 'lucide-react';");
     expect(signalListSource).not.toContain('bg-[var(--ops-surface-elevated)]');
@@ -507,8 +688,37 @@ describe('monitor detail sections', () => {
   it('keeps the grafana iframe frame flat instead of adding another inset panel', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-detail-sections.tsx'), 'utf8');
 
+    expect(source).toContain('HzButton');
+    expect(source).toContain('HzButtonLink');
+    expect(source).toContain('HzActionGroup');
+    expect(source).toContain('HzMonitorEvidenceFrame');
+    expect(source).toContain('data-monitor-detail-grafana-layout-owner="hertzbeat-ui-detail-stage"');
+    expect(source).toContain('data-monitor-detail-grafana-action-contract="angular-edit-config-delete-dashboard"');
+    expect(source).not.toContain('<div className="grid gap-3" data-monitor-detail-grafana-layout="flat-shared-ui">');
+    expect(source).toContain('data-monitor-detail-grafana-status-owner="hertzbeat-ui-state-notice"');
+    expect(source).toContain('variant="embedded"');
+    expect(source).toContain('data-monitor-detail-grafana-frame-owner="hertzbeat-ui-evidence-frame"');
+    expect(source).toContain('variant="media"');
+    expect(source).not.toContain('className="overflow-hidden py-0"');
+    expect(source).not.toContain('className="h-[720px] w-full border-0"');
+    expect(source).toContain('data-monitor-detail-grafana-actions-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('data-monitor-detail-grafana-actions-contract="angular-dashboard-actions"');
+    expect(source).toContain('data-monitor-detail-grafana-config-owner="hertzbeat-ui-button-link"');
+    expect(source).toContain('data-monitor-detail-grafana-config-action="monitor-edit-grafana"');
+    expect(source).toContain('data-monitor-detail-grafana-delete-owner="hertzbeat-ui-button"');
+    expect(source).toContain('data-monitor-detail-grafana-delete-action="delete-dashboard"');
+    expect(source).toContain('data-monitor-detail-grafana-delete-teardown="angular-hide-tab"');
+    expect(source).toContain('data-monitor-detail-empty-scope="metric-search"');
+    expect(source).not.toContain("import { Button } from '../ui/button'");
+    expect(source).not.toContain("import { RowList } from '../workbench/workbench-page'");
+    expect(source).not.toContain('<RowList');
     expect(source).not.toContain('ObservabilityInsetPanel');
     expect(source).not.toContain('overflow-hidden rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)]');
+    expect(source).not.toContain('className="overflow-hidden border-y border-[var(--hz-ui-line-soft)]"');
+    expect(source).not.toContain('className="border-x-0"');
+    expect(source).not.toContain('className="border-x-0 border-y-0"');
+    expect(source).not.toContain('className="flex flex-wrap gap-2"');
+    expect(source).not.toContain('border-[var(--ops-border-color)]');
     expect(source).not.toContain("from '../workbench/primitives'");
   });
 
@@ -517,6 +727,8 @@ describe('monitor detail sections', () => {
 
     expect(source).not.toContain('ObservabilityPanelShell');
     expect(source).not.toContain('data-observability-panel-tone="operator-sheet"');
+    expect(source).not.toContain('function FlatStage');
+    expect(source).toContain('HzMonitorDetailStage');
     expect(source).not.toContain("<FlatStage title={t('monitor.detail.section.metrics.title')}>");
   });
 });
diff --git a/web-next/components/monitor-detail/monitor-detail-sections.tsx b/web-next/components/monitor-detail/monitor-detail-sections.tsx
index 7df7ba81c5..77d992c868 100644
--- a/web-next/components/monitor-detail/monitor-detail-sections.tsx
+++ b/web-next/components/monitor-detail/monitor-detail-sections.tsx
@@ -1,10 +1,25 @@
 import React from 'react';
-import { Pencil } from 'lucide-react';
+import {
+  HzActionGroup,
+  HzButton,
+  HzButtonLink,
+  HzEmptyState,
+  HzLoadingState,
+  HzMonitorBasicCard,
+  HzMonitorEvidenceFrame,
+  HzMonitorFavoritePane,
+  HzMonitorFavoriteSurface,
+  HzMonitorDetailStage,
+  HzMonitorDetailTabSequence,
+  HzMonitorDetailSignalList,
+  HzMonitorIncrementalLoadFooter,
+  HzMonitorMetricCard,
+  HzMonitorMetricCardGrid,
+  HzMonitorMetricFavoriteAction,
+  HzStateNotice
+} from '@hertzbeat/ui';
 import { MonitorHistoryChartGrid } from './monitor-history-chart-grid';
 import { MonitorSummaryCard } from './monitor-summary-card';
-import { Button } from '../ui/button';
-import { Select } from '../ui/select';
-import { RowList } from '../workbench/workbench-page';
 import type { MonitorHistoryMetricCatalogItem } from '../../lib/monitor-detail/controller';
 import { buildMonitorDetailSections, type MonitorDetailSectionsInput, type MonitorDetailSectionsOutput } from '../../lib/monitor-detail/detail-route-state';
 import { buildMonitorMetricTableMatrix, type MonitorFavoriteJumpRow } from '../../lib/monitor-detail/view-model';
@@ -20,76 +35,57 @@ type MetricCardPayloadState = {
   error: string | null;
 };
 
-function FlatStage({
-  title,
-  description,
-  children,
-  className = '',
-  header = 'visible',
-  rhythm = 'default'
-}: {
-  title: string;
-  description?: string;
-  children: React.ReactNode;
-  className?: string;
-  header?: 'visible' | 'hidden';
-  rhythm?: 'default' | 'angular-tight';
-}) {
-  const stageClassName =
-    rhythm === 'angular-tight'
-      ? `monitor-detail-stage space-y-2 pt-0 ${className}`
-      : `monitor-detail-stage space-y-3 border-t border-[var(--ops-border-color)] pt-3 ${className}`;
-
-  return (
-    <section
-      className={stageClassName}
-      data-monitor-detail-flat-stage="true"
-      data-monitor-detail-stage-header={header === 'hidden' ? 'hidden' : undefined}
-      data-monitor-detail-stage-rhythm={rhythm === 'angular-tight' ? 'angular-tight' : undefined}
-    >
-      {header === 'visible' ? (
-        <div>
-          <h3 className="text-[13px] font-semibold tracking-[0.02em] text-[var(--ops-text-primary)]">{title}</h3>
-          {description ? <p className="mt-1 text-[12px] leading-5 text-[var(--ops-text-secondary)]">{description}</p> : null}
-        </div>
-      ) : null}
-      {children}
-    </section>
-  );
-}
-
 function MetricSignalList({
   leadingCard,
   rows,
   selectedKey,
   payloads,
+  favoriteNames,
   onSelectMetric,
-  t
+  onToggleFavorite,
+  visibleTotal,
+  hasMore,
+  onLoadMore,
+  loadMoreLabel,
+  completeLabel,
+  incrementalMode = 'angular-sentinel',
+  incrementalScope = 'realtime',
+  t,
+  ...signalListProps
 }: {
   leadingCard?: React.ReactNode;
   rows: EvidenceRow[];
   selectedKey: string | null;
   payloads: Record<string, MetricCardPayloadState | undefined>;
+  favoriteNames: string[];
   onSelectMetric: (key: string) => void;
+  onToggleFavorite: (metricName: string) => Promise<void> | void;
+  visibleTotal?: number;
+  hasMore?: boolean;
+  onLoadMore?: () => void;
+  loadMoreLabel?: React.ReactNode;
+  completeLabel?: React.ReactNode;
+  incrementalMode?: string;
+  incrementalScope?: string;
   t: Translator;
-}) {
+} & React.HTMLAttributes<HTMLDivElement>) {
   return (
-    <div
-      className="monitor-detail-stage monitor-detail-stage--signals space-y-2 pt-0.5"
-      data-monitor-detail-signal-list="true"
+    <HzMonitorDetailSignalList
+      {...signalListProps}
+      data-monitor-detail-signal-list-owner="hertzbeat-ui-signal-list"
       data-monitor-detail-signal-grid="monitor-data-table"
-      data-monitor-detail-signal-list-rhythm="angular-tight"
-      data-monitor-detail-signal-list-geometry="angular-two-column-metric-cards"
+      data-monitor-detail-signal-list-rhythm="shared-tight"
+      data-monitor-detail-signal-list-geometry="shared-two-column-metric-cards"
+      data-monitor-detail-realtime-payload-errors="card-local"
     >
       {rows.length > 0 ? (
-        <div
-          className="monitor-detail-card-grid monitor-detail-card-grid--realtime monitor-detail-signal-card-grid grid auto-rows-fr grid-cols-1 items-stretch gap-2 lg:grid-cols-2"
-          data-monitor-detail-card-grid-rhythm="angular-tight"
-          data-monitor-detail-realtime-card-flow="angular-cards-list"
+        <HzMonitorMetricCardGrid
+          data-monitor-detail-card-grid-rhythm="shared-tight"
+          data-monitor-detail-realtime-card-flow="shared-metric-card-grid"
           data-monitor-detail-realtime-card-grid="basic-and-metrics"
-          data-monitor-detail-realtime-reference="apache-hertzbeat-master-cards-lists"
-          data-monitor-detail-realtime-card-height="angular-400px"
-          data-monitor-detail-realtime-card-chrome="angular-card-box"
+          data-monitor-detail-realtime-reference="hertzbeat-ui-monitor-card-grid"
+          data-monitor-detail-realtime-card-height="content-driven"
+          data-monitor-detail-realtime-card-chrome="hertzbeat-ui-card-grid"
         >
           {leadingCard}
           {rows.map(row => {
@@ -98,102 +94,71 @@ function MetricSignalList({
             const matrix = buildMonitorMetricTableMatrix(state?.payload, t);
             const columns = matrix.columns.slice(0, 3);
             const tableRows = matrix.rows.slice(0, 4);
-            const hasTable = columns.length > 0 && tableRows.length > 0;
+            const favorited = favoriteNames.includes(row.key);
             return (
-              <section
+              <HzMonitorMetricCard
                 key={row.key}
-                className={[
-                  'monitor-detail-card monitor-detail-card--signal-flat monitor-detail-card--signal-metric monitor-workbench-surface monitor-workbench-surface--plain min-h-[400px] min-w-0 grid content-start gap-3 rounded-[3px] border p-3 bg-[var(--ops-surface-raised)] transition-colors',
-                  'border-[var(--ops-border-color)]'
-                ].join(' ')}
+                title={row.title}
+                columns={columns}
+                rows={tableRows.map(tableRow => ({
+                  key: tableRow.key,
+                  label: tableRow.label,
+                  values: columns.map((_, index) => tableRow.values[index] ?? '-')
+                }))}
+                selected={selected}
+                loading={state?.loading}
+                error={state?.error}
+                loadingLabel={t('common.loading')}
+                emptyLabel={t('monitor.detail.metric.table.empty.copy')}
+                labelHeader={t('common.labels')}
+                actions={
+                  <HzMonitorMetricFavoriteAction
+                    active={favorited}
+                    label={favorited ? t('monitor.detail.favorite.remove') : t('monitor.detail.favorite.add')}
+                    onClick={event => {
+                      event.stopPropagation();
+                      void onToggleFavorite(row.key);
+                    }}
+                    data-monitor-detail-signal-row-action="favorite"
+                  />
+                }
+                onSelect={() => onSelectMetric(row.key)}
+                data-monitor-detail-metric-card-owner="hertzbeat-ui-metric-card"
                 data-monitor-detail-signal-card="true"
-                data-monitor-detail-signal-card-chrome="angular-card-box"
+                data-monitor-detail-signal-card-chrome="hertzbeat-ui-metric-card"
                 data-monitor-detail-signal-row={row.key}
-                data-monitor-detail-signal-row-density="angular-metric-card"
-                data-monitor-detail-signal-selected-style="angular-neutral"
-                data-selected={selected ? 'true' : 'false'}
-              >
-                <header className="monitor-workbench-surface__header flex items-start justify-between gap-3 border-b border-[var(--ops-border-color)] pb-2">
-                  <button
-                    type="button"
-                    className="monitor-workbench-card-title__copy min-w-0 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--ring)/0.2)]"
-                    data-monitor-detail-signal-row-title="true"
-                    onClick={() => onSelectMetric(row.key)}
-                  >
-                    <div className="monitor-workbench-card-title__title truncate text-[16px] font-semibold leading-6 text-[var(--ops-text-primary)]">
-                      {row.title}
-                    </div>
-                  </button>
-                  {state?.loading ? (
-                    <span className="flex-none text-[11px] text-[var(--ops-text-tertiary)]">{t('common.loading')}</span>
-                  ) : null}
-                </header>
-                <button
-                  type="button"
-                  className="monitor-workbench-metric-table grid w-full gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--ring)/0.2)]"
-                  data-monitor-detail-signal-card-table="true"
-                  data-monitor-detail-signal-card-body-density="angular-card-table"
-                  onClick={() => onSelectMetric(row.key)}
-                >
-                  {state?.error ? (
-                    <span className="border-y border-rose-400/20 bg-rose-400/10 px-3 py-3 text-sm text-rose-200">{state.error}</span>
-                  ) : hasTable ? (
-                    <span className="grid overflow-hidden border-y border-[var(--ops-border-color)]" data-monitor-detail-signal-card-table-row="metric-fields">
-                      <span
-                        className="grid gap-2 border-b border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-3 py-2 text-[11px] font-semibold text-[var(--ops-text-tertiary)]"
-                        style={{ gridTemplateColumns: `minmax(96px, 0.8fr) repeat(${columns.length}, minmax(64px, 1fr))` }}
-                      >
-                        <span>{t('common.labels')}</span>
-                        {columns.map(column => (
-                          <span key={column.key} className="truncate">
-                            {column.title}
-                          </span>
-                        ))}
-                      </span>
-                      {tableRows.map(tableRow => (
-                        <span
-                          key={tableRow.key}
-                          className="grid gap-2 border-b border-[var(--ops-border-color)] px-3 py-2 text-[12px] last:border-b-0"
-                          style={{ gridTemplateColumns: `minmax(96px, 0.8fr) repeat(${columns.length}, minmax(64px, 1fr))` }}
-                        >
-                          <span className="truncate text-[var(--ops-text-secondary)]">{tableRow.label}</span>
-                          {columns.map((column, index) => (
-                            <span key={column.key} className="truncate font-semibold text-[var(--ops-text-primary)]">
-                              {tableRow.values[index] ?? '-'}
-                            </span>
-                          ))}
-                        </span>
-                      ))}
-                    </span>
-                  ) : (
-                    <span className="border-y border-[var(--ops-border-color)] px-3 py-4 text-sm text-[var(--ops-text-secondary)]">
-                      {state?.loading ? t('common.loading') : t('monitor.detail.metric.table.empty.copy')}
-                    </span>
-                  )}
-                </button>
-              </section>
+                data-monitor-detail-signal-row-density="shared-metric-card"
+                data-monitor-detail-signal-selected-style="left-rail"
+              />
             );
           })}
-        </div>
+        </HzMonitorMetricCardGrid>
       ) : (
-        <RowList rows={[{ title: t('monitor.detail.metric.search.empty.title'), copy: t('monitor.detail.metric.search.empty.copy'), meta: '-' }]} />
+        <HzEmptyState
+          title={t('monitor.detail.metric.search.empty.title')}
+          description={t('monitor.detail.metric.search.empty.copy')}
+          data-monitor-detail-empty-owner="hertzbeat-ui-empty-state"
+          data-monitor-detail-empty-scope="metric-search"
+        />
       )}
-    </div>
-  );
-}
-
-function MonitorRealtimeTightSequence({ children }: { children: React.ReactNode }) {
-  return (
-    <div
-      className="monitor-detail-tab-sequence monitor-detail-tab-sequence--angular-tight space-y-2"
-      data-monitor-detail-tab-sequence="angular-tight"
-    >
-      {children}
-    </div>
+      {visibleTotal != null && visibleTotal > rows.length ? (
+        <HzMonitorIncrementalLoadFooter
+          visibleCount={rows.length}
+          totalCount={visibleTotal}
+          hasMore={hasMore}
+          loadMoreLabel={loadMoreLabel ?? t('common.view-more')}
+          completeLabel={completeLabel}
+          onLoadMore={onLoadMore}
+          data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"
+          data-monitor-detail-incremental-mode={incrementalMode}
+          data-monitor-detail-incremental-scope={incrementalScope}
+        />
+      ) : null}
+    </HzMonitorDetailSignalList>
   );
 }
 
-function MonitorBasicPlainSurface({
+function MonitorBasicSharedSurface({
   monitor,
   editHref,
   t
@@ -203,32 +168,14 @@ function MonitorBasicPlainSurface({
   t: Translator;
 }) {
   return (
-    <section
-      className="monitor-detail-card monitor-detail-card--overview monitor-detail-card--overview-flat monitor-workbench-surface monitor-workbench-surface--plain min-h-[400px] min-w-0 grid content-start gap-3 rounded-[3px] border p-3 border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)]"
-      data-monitor-basic-stage-surface="monitor-data-table"
-      data-monitor-basic-grid-item="angular-first-card"
-      data-monitor-basic-card-chrome="angular-card-box"
+    <HzMonitorBasicCard
+      heading={t('monitor.detail.basic')}
+      editHref={editHref}
+      editLabel={t('monitor.edit-monitor')}
+      data-monitor-basic-owner="hertzbeat-ui-basic-card"
     >
-      <header className="monitor-workbench-surface__header flex items-start justify-between gap-3 border-b border-[var(--ops-border-color)] pb-2">
-        <div className="monitor-workbench-card-title__copy min-w-0">
-          <div className="monitor-workbench-card-title__title text-[16px] font-semibold leading-6 text-[var(--ops-text-primary)]">
-            {t('monitor.detail.basic')}
-          </div>
-        </div>
-        <div className="monitor-workbench-surface__meta flex flex-none items-center gap-2">
-          <a
-            href={editHref}
-            aria-label={t('monitor.edit-monitor')}
-            className="monitor-workbench-surface__edit inline-flex h-6 w-6 items-center justify-center border-0 bg-transparent text-[var(--ops-text-tertiary)] transition-colors hover:text-[var(--ops-text-primary)]"
-            data-monitor-basic-edit-action="monitor-data-table"
-            data-monitor-basic-edit-action-density="plain-icon"
-          >
-            <Pencil aria-hidden="true" className="h-3.5 w-3.5" />
-          </a>
-        </div>
-      </header>
       <MonitorSummaryCard monitor={monitor} formatTime={formatTime} t={t} />
-    </section>
+    </HzMonitorBasicCard>
   );
 }
 
@@ -239,12 +186,17 @@ export type MonitorSectionCompositionProps = {
   selectedMetric: MonitorDetailMetric | null;
   metricSearch: string;
   metricRows: EvidenceRow[];
+  realtimeMetricTotalCount: number;
+  realtimeMetricHasMore: boolean;
   metricCardPayloads: Record<string, MetricCardPayloadState | undefined>;
   metrics: MonitorDetailMetric[];
   favoriteNames: string[];
   favoriteJumpRows: MonitorFavoriteJumpRow[];
   selectedFavoriteKey: string | null;
   favoriteSurfaceMode: 'realtime' | 'history';
+  favoriteRealtimeMetricVisibleCount: number;
+  favoriteRealtimeMetricTotalCount: number;
+  favoriteRealtimeMetricHasMore: boolean;
   selectedMetricKey: string | null;
   metricPayload: unknown;
   metricPayloadLoading: boolean;
@@ -257,6 +209,9 @@ export type MonitorSectionCompositionProps = {
   historyMetrics: MonitorHistoryMetricCatalogItem[];
   historyMetricSearch: string;
   historyRows: EvidenceRow[];
+  historyChartVisibleCount: number;
+  historyChartTotalCount: number;
+  historyChartHasMore: boolean;
   selectedHistoryMetricKey: string | null;
   historySeriesSearch: string;
   historySeriesRows: EvidenceRow[];
@@ -274,6 +229,9 @@ export type MonitorSectionCompositionProps = {
   historyWindows: Array<{ value: string; label: string }>;
   historyModes: Array<{ value: boolean; label: string }>;
   historyFullscreen: boolean;
+  favoriteHistoryChartVisibleCount: number;
+  favoriteHistoryChartTotalCount: number;
+  favoriteHistoryChartHasMore: boolean;
   grafanaState: GrafanaDashboard;
   grafanaMessage: string | null;
   grafanaError: string | null;
@@ -282,6 +240,7 @@ export type MonitorSectionCompositionProps = {
   onMetricSearchChange: (value: string) => void;
   onSelectMetric: (key: string) => void;
   onToggleFavorite: (metricName: string) => Promise<void> | void;
+  onLoadMoreRealtimeMetrics: () => void;
   onSelectMetricRow: (key: string) => void;
   onMetricModeChange: (mode: 'table' | 'detail') => void;
   onMetricRefresh: () => void;
@@ -293,6 +252,8 @@ export type MonitorSectionCompositionProps = {
   onSelectHistorySeries: (key: string) => void;
   onSelectHistoryPoint: (index: number) => void;
   onHistoryRefresh: () => void;
+  onFavoriteHistoryRefresh: () => void;
+  onLoadMoreHistoryCharts: () => void;
   onHistoryWindowChange: (value: string) => void;
   onHistoryTimeContextApply?: (context: TimeContext) => void;
   onApplyHistoryChartZoomTimeRange?: (context: TimeContext) => void;
@@ -300,6 +261,8 @@ export type MonitorSectionCompositionProps = {
   onHistoryFullscreenToggle: () => void;
   onSelectFavorite: (key: string) => void;
   onSetFavoriteSurfaceMode: (mode: 'realtime' | 'history') => void;
+  onLoadMoreFavoriteRealtimeMetrics: () => void;
+  onLoadMoreFavoriteHistoryCharts: () => void;
   onRemoveFavoriteToken: (token: string) => Promise<void> | void;
   onDeleteGrafanaDashboard: () => Promise<void> | void;
   t: Translator;
@@ -310,9 +273,15 @@ export function MonitorDetailSections(props: MonitorSectionCompositionProps): Mo
     monitor,
     editHref,
     metricRows,
+    realtimeMetricTotalCount,
+    realtimeMetricHasMore,
     metricCardPayloads,
+    favoriteNames,
     favoriteJumpRows,
     favoriteSurfaceMode,
+    favoriteRealtimeMetricVisibleCount,
+    favoriteRealtimeMetricTotalCount,
+    favoriteRealtimeMetricHasMore,
     selectedMetricKey,
     metricPayloadLoading,
     metricPayloadError,
@@ -320,6 +289,9 @@ export function MonitorDetailSections(props: MonitorSectionCompositionProps): Mo
     historyError,
     historyMetrics,
     historyRows,
+    historyChartVisibleCount,
+    historyChartTotalCount,
+    historyChartHasMore,
     selectedHistoryMetricKey,
     historyChartPayloads,
     historyChartLoadingKeys,
@@ -329,19 +301,29 @@ export function MonitorDetailSections(props: MonitorSectionCompositionProps): Mo
     historyTimeContext,
     historyWindows,
     historyModes,
+    favoriteHistoryChartVisibleCount,
+    favoriteHistoryChartTotalCount,
+    favoriteHistoryChartHasMore,
     grafanaState,
     grafanaMessage,
     grafanaError,
     favoriteMessage,
     favoriteError,
     onSelectMetric,
+    onToggleFavorite,
+    onLoadMoreRealtimeMetrics,
     onSelectHistoryMetric,
+    onToggleHistoryFavorite,
     onHistoryWindowChange,
     onHistoryTimeContextApply,
     onApplyHistoryChartZoomTimeRange,
     onHistoryModeChange,
     onHistoryRefresh,
+    onFavoriteHistoryRefresh,
+    onLoadMoreHistoryCharts,
     onSetFavoriteSurfaceMode,
+    onLoadMoreFavoriteRealtimeMetrics,
+    onLoadMoreFavoriteHistoryCharts,
     onDeleteGrafanaDashboard,
     t
   } = props;
@@ -352,8 +334,9 @@ export function MonitorDetailSections(props: MonitorSectionCompositionProps): Mo
   const historyChartItems = historyRows
     .map(row => historyMetrics.find(item => `${item.metrics}:${item.metric}` === row.key))
     .filter((item): item is MonitorHistoryMetricCatalogItem => Boolean(item))
-    .slice(0, 6);
-  const favoriteMetricRows = realtimeFavoriteRows.map(row => ({
+    .slice(0, historyChartVisibleCount);
+  const visibleRealtimeFavoriteRows = realtimeFavoriteRows.slice(0, favoriteRealtimeMetricVisibleCount);
+  const favoriteMetricRows = visibleRealtimeFavoriteRows.map(row => ({
     key: row.targetKey,
     title: row.title,
     copy: row.copy,
@@ -362,116 +345,186 @@ export function MonitorDetailSections(props: MonitorSectionCompositionProps): Mo
   const favoriteHistoryChartItems = historyFavoriteRows
     .map(row => historyMetricByKey.get(row.targetKey))
     .filter((item): item is MonitorHistoryMetricCatalogItem => Boolean(item))
-    .slice(0, 6);
+    .slice(0, favoriteHistoryChartVisibleCount);
+  const renderFavoriteFeedback = (scope: 'realtime' | 'history') => {
+    if (favoriteMessage) {
+      return (
+        <HzStateNotice
+          tone="success"
+          title={favoriteMessage}
+          variant="embedded"
+          data-monitor-detail-favorite-feedback="success"
+          data-monitor-detail-favorite-feedback-scope={scope}
+          data-monitor-detail-favorite-feedback-copy="angular-favorite-copy"
+        />
+      );
+    }
+    if (favoriteError) {
+      return (
+        <HzStateNotice
+          tone="critical"
+          title={favoriteError}
+          variant="embedded"
+          data-monitor-detail-favorite-feedback="error"
+          data-monitor-detail-favorite-feedback-scope={scope}
+          data-monitor-detail-favorite-feedback-copy="angular-favorite-copy"
+        />
+      );
+    }
+    return null;
+  };
 
   const contextNode = null;
 
   const realtimeNode = (
-    <MonitorRealtimeTightSequence>
-      <FlatStage
+    <HzMonitorDetailTabSequence data-monitor-detail-tab-sequence-owner="hertzbeat-ui-detail-tab-sequence">
+      <HzMonitorDetailStage
         title={t('monitor.detail.tab.realtime')}
-        className="monitor-detail-stage--signals"
         header="hidden"
-        rhythm="angular-tight"
+        rhythm="tight"
+        data-monitor-detail-stage-owner="hertzbeat-ui-detail-stage"
       >
-        {metricPayloadLoading ? (
-          <div className="text-sm text-[var(--ops-text-secondary)]">{t('common.loading')}</div>
-        ) : metricPayloadError ? (
-          <div className="text-sm text-rose-300">{metricPayloadError}</div>
-        ) : (
-          null
-        )}
+        {renderFavoriteFeedback('realtime')}
         <MetricSignalList
-          leadingCard={<MonitorBasicPlainSurface monitor={monitor} editHref={editHref} t={t} />}
+          leadingCard={<MonitorBasicSharedSurface monitor={monitor} editHref={editHref} t={t} />}
           rows={metricRows}
           selectedKey={selectedMetricKey}
           payloads={metricCardPayloads}
+          favoriteNames={favoriteNames}
           onSelectMetric={onSelectMetric}
+          onToggleFavorite={onToggleFavorite}
+          visibleTotal={realtimeMetricTotalCount}
+          hasMore={realtimeMetricHasMore}
+          onLoadMore={onLoadMoreRealtimeMetrics}
+          loadMoreLabel={t('common.view-more')}
+          data-monitor-detail-realtime-selection-reset="angular-table-reload"
           t={t}
         />
-      </FlatStage>
-    </MonitorRealtimeTightSequence>
+      </HzMonitorDetailStage>
+    </HzMonitorDetailTabSequence>
   );
 
   const historyNode = (
     <div
-      className="monitor-detail-history-cards"
-      data-monitor-detail-history-layout="angular-chart-cards-only"
-      data-monitor-detail-history-reference="apache-hertzbeat-master-cards"
+      className="monitor-detail-history-charts"
+      data-monitor-detail-history-layout="hertzbeat-ui-history-chart-grid"
+      data-monitor-detail-history-reference="hertzbeat-ui-history-chart-grid"
     >
+      {renderFavoriteFeedback('history')}
       {historyLoading ? (
-        <div className="rounded-[3px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-raised)] px-4 py-8 text-sm text-[var(--ops-text-secondary)]">
-          {t('common.loading')}
-        </div>
+        <HzLoadingState
+          title={t('common.loading')}
+          rows={4}
+          data-monitor-detail-state-owner="hertzbeat-ui-loading-state"
+          data-monitor-detail-state-scope="history"
+        />
       ) : historyError ? (
-        <div className="rounded-[3px] border border-rose-400/20 bg-rose-400/10 px-4 py-8 text-sm text-rose-200">
-          {historyError}
-        </div>
-      ) : (
-        <MonitorHistoryChartGrid
-          items={historyChartItems}
-          payloads={historyChartPayloads}
-          loadingKeys={historyChartLoadingKeys}
-          errors={historyChartErrors}
-          selectedKey={selectedHistoryMetricKey}
-          aggregated={historyInterval}
-          showControls={true}
-          historyWindow={historyWindow}
-          timeContext={historyTimeContext}
-          historyWindows={historyWindows}
-          historyModes={historyModes}
-          onHistoryWindowChange={onHistoryWindowChange}
-          onHistoryTimeContextApply={onHistoryTimeContextApply}
-          onApplyChartZoomTimeRange={onApplyHistoryChartZoomTimeRange}
-          onHistoryModeChange={onHistoryModeChange}
-          onRefresh={onHistoryRefresh}
-          onSelectMetric={onSelectHistoryMetric}
-          formatTime={formatTime}
-          t={t}
+        <HzStateNotice
+          tone="critical"
+          title={t('common.load-failed')}
+          description={historyError}
+          data-monitor-detail-state-owner="hertzbeat-ui-state-notice"
+          data-monitor-detail-state-scope="history"
         />
+      ) : (
+        <>
+          <MonitorHistoryChartGrid
+            items={historyChartItems}
+            payloads={historyChartPayloads}
+            loadingKeys={historyChartLoadingKeys}
+            errors={historyChartErrors}
+            selectedKey={selectedHistoryMetricKey}
+            aggregated={historyInterval}
+            showControls={true}
+            historyWindow={historyWindow}
+            timeContext={historyTimeContext}
+            historyWindows={historyWindows}
+            historyModes={historyModes}
+            onHistoryWindowChange={onHistoryWindowChange}
+            onHistoryTimeContextApply={onHistoryTimeContextApply}
+            onApplyChartZoomTimeRange={onApplyHistoryChartZoomTimeRange}
+            onHistoryModeChange={onHistoryModeChange}
+            onRefresh={onHistoryRefresh}
+            onSelectMetric={onSelectHistoryMetric}
+            favoriteNames={favoriteNames}
+            onToggleFavorite={onToggleHistoryFavorite}
+            formatTime={formatTime}
+            t={t}
+          />
+          {historyChartTotalCount > historyChartItems.length ? (
+            <HzMonitorIncrementalLoadFooter
+              visibleCount={historyChartItems.length}
+              totalCount={historyChartTotalCount}
+              hasMore={historyChartHasMore}
+              loadMoreLabel={t('common.view-more')}
+              onLoadMore={onLoadMoreHistoryCharts}
+              data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"
+              data-monitor-detail-incremental-mode="angular-chart-sentinel"
+              data-monitor-detail-incremental-scope="history"
+            />
+          ) : null}
+        </>
       )}
     </div>
   );
 
   const favoritesNode = (
-    <div
-      className="favorite-content space-y-3 rounded-[3px] border border-[var(--ops-border-color)] bg-transparent p-2"
-      data-monitor-detail-favorite-layout="angular-favorite-content"
+    <HzMonitorFavoriteSurface
+      value={favoriteSurfaceMode}
+      options={[
+        { value: 'realtime', label: t('monitor.detail.realtime') },
+        { value: 'history', label: t('monitor.detail.history') }
+      ]}
+      selectorLabel={t('monitor.detail.favorite')}
+      onValueChange={value => onSetFavoriteSurfaceMode(value as 'realtime' | 'history')}
+      message={favoriteMessage}
+      error={favoriteError}
+      data-monitor-detail-favorite-owner="hertzbeat-ui-favorite-surface"
+      data-monitor-detail-favorite-layout="hertzbeat-ui-favorite-surface"
       data-monitor-detail-favorite-mode={favoriteSurfaceMode}
+      data-monitor-detail-favorite-feedback-copy="angular-favorite-copy"
+      data-monitor-detail-favorite-set-semantics="angular-set"
+      data-monitor-detail-favorite-active-fallback="angular-subselector-sticky"
+      data-monitor-detail-favorite-history-reload-reset="angular-chart-reload"
+      data-monitor-detail-favorite-empty-contract="angular-subselector-empty"
+      data-monitor-detail-favorite-realtime-incremental="angular-favorite-sentinel"
+      selectorProps={{ 'data-monitor-detail-favorite-selector': 'hertzbeat-ui-select-menu' } as React.HTMLAttributes<HTMLDivElement>}
     >
-      <div className="favorite-selector" data-monitor-detail-favorite-selector="angular-select-200">
-        <Select
-          aria-label={t('monitor.detail.favorite')}
-          value={favoriteSurfaceMode}
-          containerClassName="w-[200px]"
-          onChange={event => onSetFavoriteSurfaceMode(event.target.value as 'realtime' | 'history')}
-        >
-          <option value="realtime">{t('monitor.detail.realtime')}</option>
-          <option value="history">{t('monitor.detail.history')}</option>
-        </Select>
-      </div>
-
       {favoriteSurfaceMode === 'realtime' ? (
         favoriteMetricRows.length > 0 ? (
-          <div data-monitor-detail-favorite-realtime="cards-lists">
+          <HzMonitorFavoritePane kind="realtime" data-monitor-detail-favorite-realtime="shared-pane">
             <MetricSignalList
               rows={favoriteMetricRows}
               selectedKey={selectedMetricKey}
               payloads={metricCardPayloads}
+              favoriteNames={favoriteNames}
               onSelectMetric={onSelectMetric}
+              onToggleFavorite={onToggleFavorite}
+              data-monitor-detail-realtime-selection-reset="angular-table-reload"
+              visibleTotal={favoriteRealtimeMetricTotalCount}
+              hasMore={favoriteRealtimeMetricHasMore}
+              onLoadMore={onLoadMoreFavoriteRealtimeMetrics}
+              loadMoreLabel={t('common.view-more')}
+              incrementalMode="angular-favorite-sentinel"
+              incrementalScope="favorite-realtime"
               t={t}
             />
-          </div>
+          </HzMonitorFavoritePane>
         ) : (
-          <div
-            className="rounded-[3px] border border-dashed border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-5 py-10 text-sm text-[var(--ops-text-secondary)]"
-            data-monitor-detail-favorite-empty="realtime"
-          >
-            {t('monitor.detail.favorite.surface.empty.realtime.title')}
-          </div>
+          <HzEmptyState
+            title={t('monitor.detail.favorite.surface.empty.realtime.title')}
+            data-monitor-detail-empty-owner="hertzbeat-ui-empty-state"
+            data-monitor-detail-empty-scope="favorite-realtime"
+            data-monitor-detail-favorite-empty-teardown="angular-empty-no-sentinel"
+          />
         )
       ) : favoriteHistoryChartItems.length > 0 ? (
-        <div data-monitor-detail-favorite-history="cards">
+        <HzMonitorFavoritePane
+          kind="history"
+          data-monitor-detail-favorite-history="shared-pane"
+          data-monitor-detail-favorite-history-source="favorite-history-chart-payloads"
+          data-monitor-detail-favorite-history-controls="angular-chart-toolbox"
+        >
           <MonitorHistoryChartGrid
             items={favoriteHistoryChartItems}
             payloads={historyChartPayloads}
@@ -479,59 +532,116 @@ export function MonitorDetailSections(props: MonitorSectionCompositionProps): Mo
             errors={historyChartErrors}
             selectedKey={selectedHistoryMetricKey}
             aggregated={historyInterval}
+            showControls={true}
             historyWindow={historyWindow}
             timeContext={historyTimeContext}
+            historyWindows={historyWindows}
+            historyModes={historyModes}
+            onHistoryWindowChange={onHistoryWindowChange}
+            onHistoryTimeContextApply={onHistoryTimeContextApply}
             onApplyChartZoomTimeRange={onApplyHistoryChartZoomTimeRange}
+            onHistoryModeChange={onHistoryModeChange}
+            onRefresh={onFavoriteHistoryRefresh}
             onSelectMetric={onSelectHistoryMetric}
+            favoriteNames={favoriteNames}
+            onToggleFavorite={onToggleHistoryFavorite}
             formatTime={formatTime}
             t={t}
           />
-        </div>
+          {favoriteHistoryChartTotalCount > favoriteHistoryChartItems.length ? (
+            <HzMonitorIncrementalLoadFooter
+              visibleCount={favoriteHistoryChartItems.length}
+              totalCount={favoriteHistoryChartTotalCount}
+              hasMore={favoriteHistoryChartHasMore}
+              loadMoreLabel={t('common.view-more')}
+              onLoadMore={onLoadMoreFavoriteHistoryCharts}
+              data-monitor-detail-incremental-owner="hertzbeat-ui-incremental-load-footer"
+              data-monitor-detail-incremental-mode="angular-favorite-chart-sentinel"
+              data-monitor-detail-incremental-scope="favorite-history"
+            />
+          ) : null}
+        </HzMonitorFavoritePane>
       ) : (
-        <div
-          className="rounded-[3px] border border-dashed border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-5 py-10 text-sm text-[var(--ops-text-secondary)]"
-          data-monitor-detail-favorite-empty="history"
-        >
-          {t('monitor.detail.favorite.surface.empty.history.title')}
-        </div>
+        <HzEmptyState
+          title={t('monitor.detail.favorite.surface.empty.history.title')}
+          data-monitor-detail-empty-owner="hertzbeat-ui-empty-state"
+          data-monitor-detail-empty-scope="favorite-history"
+          data-monitor-detail-favorite-empty-teardown="angular-empty-no-sentinel"
+        />
       )}
-
-      {favoriteMessage ? <div className="text-sm text-emerald-300">{favoriteMessage}</div> : null}
-      {favoriteError ? <div className="text-sm text-rose-300">{favoriteError}</div> : null}
-    </div>
+    </HzMonitorFavoriteSurface>
   );
 
   const grafanaNode = (
-    <div className="space-y-4">
-      <FlatStage
-        title={t('monitor.detail.section.grafana.title')}
-        description={t('monitor.detail.grafana.copy')}
-      >
-        <RowList
-          rows={[
-            {
-              title: t('monitor.detail.grafana.status'),
-              copy: grafanaState.enabled ? t('common.enabled') : t('common.disabled'),
-              meta: grafanaState.url || '-'
-            }
-          ]}
+    <HzMonitorDetailStage
+      title={t('monitor.detail.section.grafana.title')}
+      description={t('monitor.detail.grafana.copy')}
+      data-monitor-detail-stage-owner="hertzbeat-ui-detail-stage"
+      data-monitor-detail-grafana-layout-owner="hertzbeat-ui-detail-stage"
+      data-monitor-detail-grafana-action-contract="angular-edit-config-delete-dashboard"
+    >
+      <HzStateNotice
+        tone={grafanaState.enabled ? 'success' : 'neutral'}
+        title={t('monitor.detail.grafana.status')}
+        description={grafanaState.enabled ? t('common.enabled') : t('common.disabled')}
+        meta={grafanaState.url || '-'}
+        variant="embedded"
+        data-monitor-detail-grafana-status-owner="hertzbeat-ui-state-notice"
+      />
+      {grafanaState.enabled && grafanaState.url ? (
+        <>
+          <HzMonitorEvidenceFrame
+            variant="media"
+            data-monitor-detail-grafana-frame-owner="hertzbeat-ui-evidence-frame"
+          >
+            <iframe title="grafana-dashboard" src={grafanaState.url} />
+          </HzMonitorEvidenceFrame>
+          <HzActionGroup
+            density="inline"
+            data-monitor-detail-grafana-actions-owner="hertzbeat-ui-action-group"
+            data-monitor-detail-grafana-actions-contract="angular-dashboard-actions"
+          >
+            <HzButtonLink
+              href={editHref}
+              size="sm"
+              data-monitor-detail-grafana-config-owner="hertzbeat-ui-button-link"
+              data-monitor-detail-grafana-config-action="monitor-edit-grafana"
+            >
+              {t('monitor.detail.grafana.configure')}
+            </HzButtonLink>
+            <HzButton
+              intent="danger"
+              size="sm"
+              onClick={() => void onDeleteGrafanaDashboard()}
+              data-monitor-detail-grafana-delete-owner="hertzbeat-ui-button"
+              data-monitor-detail-grafana-delete-action="delete-dashboard"
+            >
+              {t('monitor.detail.delete-grafana')}
+            </HzButton>
+          </HzActionGroup>
+        </>
+      ) : null}
+      {grafanaMessage ? (
+        <HzStateNotice
+          tone="success"
+          title={grafanaMessage}
+          variant="embedded"
+          data-monitor-detail-state-owner="hertzbeat-ui-state-notice"
+          data-monitor-detail-state-scope="grafana-success"
+          data-monitor-detail-grafana-delete-teardown="angular-hide-tab"
         />
-        {grafanaState.enabled && grafanaState.url ? (
-          <>
-            <div className="overflow-hidden border-y border-[var(--ops-border-color)]">
-              <iframe title="grafana-dashboard" src={grafanaState.url} className="h-[720px] w-full border-0" />
-            </div>
-            <div className="flex flex-wrap gap-2">
-              <Button variant="subtle" onClick={() => void onDeleteGrafanaDashboard()}>
-                {t('monitor.detail.delete-grafana')}
-              </Button>
-            </div>
-          </>
-        ) : null}
-        {grafanaMessage ? <div className="text-sm text-emerald-300">{grafanaMessage}</div> : null}
-        {grafanaError ? <div className="text-sm text-rose-300">{grafanaError}</div> : null}
-      </FlatStage>
-    </div>
+      ) : null}
+      {grafanaError ? (
+        <HzStateNotice
+          tone="critical"
+          title={t('common.delete-failed')}
+          description={grafanaError}
+          variant="embedded"
+          data-monitor-detail-state-owner="hertzbeat-ui-state-notice"
+          data-monitor-detail-state-scope="grafana-error"
+        />
+      ) : null}
+    </HzMonitorDetailStage>
   );
 
   return buildMonitorDetailSections({
diff --git a/web-next/components/monitor-detail/monitor-history-chart-grid.test.tsx b/web-next/components/monitor-detail/monitor-history-chart-grid.test.tsx
index 0ee7fab009..b550b3a72a 100644
--- a/web-next/components/monitor-detail/monitor-history-chart-grid.test.tsx
+++ b/web-next/components/monitor-detail/monitor-history-chart-grid.test.tsx
@@ -1,4 +1,6 @@
 import React from 'react';
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it } from 'vitest';
 import { createTranslatorMock } from '../../test/i18n-test-helper';
@@ -8,7 +10,7 @@ const t = createTranslatorMock();
 const zhT = createTranslatorMock({ locale: 'zh-CN' });
 
 describe('monitor history chart grid', () => {
-  it('uses the shared TimeRangeControl for visible history time navigation', () => {
+  it('uses the shared time range toolbar for visible history time navigation', () => {
     const html = renderToStaticMarkup(
       <MonitorHistoryChartGrid
         items={[{ metrics: 'summary', metric: 'responseTime', unit: '' } as any]}
@@ -19,6 +21,7 @@ describe('monitor history chart grid', () => {
         aggregated={false}
         showControls
         historyWindow="1h"
+        timeContext={{ timeRange: 'last-1h', start: '1778985158146', end: '1778986674000' }}
         historyWindows={[
           { value: '30m', label: '30 minutes' },
           { value: '1h', label: '1 hour' }
@@ -36,12 +39,256 @@ describe('monitor history chart grid', () => {
       />
     );
 
-    expect(html).toContain('data-monitor-history-time-toolbar="shared-time-context-control"');
-    expect(html).toContain('data-time-range-control="hertzbeat-shared"');
-    expect(html).toContain('data-time-range-control-visual="cold-operator-toolbar"');
-    expect(html).toContain('data-monitor-history-time-range-select="true"');
+    expect(html).toContain('data-monitor-history-time-toolbar-owner="hertzbeat-ui-time-range-toolbar"');
+    expect(html).toContain('data-monitor-history-datazoom-feedback="time-toolbar"');
+    expect(html).toContain('data-monitor-history-refresh-contract="angular-first-page-reload"');
+    expect(html).toContain('data-hz-ui="time-range-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-owner="hertzbeat-ui-time-range-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-state="applied"');
+    expect(html).toContain('data-hz-time-range-toolbar-layout="compact-lined-controls"');
+    expect(html).toContain('data-hz-time-range-toolbar-density="operator-single-row-tight"');
+    expect(html).toContain('data-hz-time-range-toolbar-control-height="28"');
+    expect(html).toContain('data-hz-time-range-toolbar-action-mode="icon-text"');
+    expect(html).toContain('data-hz-time-range-toolbar-action="refresh"');
+    expect(html).toContain('data-hz-time-range-toolbar-action="reset"');
+    expect(html).toContain('data-hz-time-range-toolbar-action="apply"');
+    expect(html).toContain('Refresh now');
+    expect(html).toContain('Reset');
+    expect(html).toContain('Apply');
+    expect(html).toContain('data-hz-time-range-toolbar-time-entry="single-expression-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-preset-placement="picker-panel"');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute="picker-panel"');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute-layout="single-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-rail-layout="nowrap"');
+    expect(html).toContain('data-hz-time-range-toolbar-overflow="horizontal-scroll"');
+    expect(html).toContain('flex min-w-max flex-nowrap items-center gap-0.5');
+    expect(html).toContain('data-hz-ui="expression-time-range-picker"');
+    expect(html).toContain('data-hz-expression-time-range-picker-layout="expression-single-range"');
+    expect(html).toContain('data-hz-expression-time-range-trigger="single-range"');
+    expect(html).toContain('data-hz-expression-time-range-picker-trigger-width="compact"');
+    expect(html).toContain('max-w-[280px]');
+    expect(html).toContain('data-hz-expression-time-range-picker-panel-width="560"');
+    expect(html).not.toContain('max-w-[320px]');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute-control="expression-time-range-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute-input-mode="manual-text-with-picker"');
+    expect(html).toContain('2026-05-17 10:32:38');
+    expect(html).toContain('2026-05-17 10:57:54');
+    expect(html).toContain('2026-05-17 10:32:38 - 2026-05-17 10:57:54');
+    expect(html).not.toContain('type="datetime-local"');
+    expect(html).not.toContain('data-hz-time-range-toolbar-preset-width="compact"');
+    expect(html).not.toContain('data-monitor-history-time-range-select="true"');
+    expect(html).not.toContain('value="1778985158146"');
+    expect(html).not.toContain('value="1778986674000"');
+    expect(html).toContain('data-hz-time-range-toolbar-family="signal-handoff-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-action-family="signal-handoff-toolbar"');
+    expect(html).toContain('data-hz-time-range-toolbar-select-family="signal-handoff-toolbar"');
     expect(html).toContain('data-monitor-history-time-range-option="last-1h"');
     expect(html).toContain('data-monitor-history-refresh-action="true"');
+    expect(html).toContain('border-[var(--hz-ui-line-soft)] bg-transparent');
+    expect(html).not.toContain('border-y border-[var(--hz-ui-line-soft)] bg-[var(--hz-ui-surface)]');
+    expect(html).not.toContain('data-hz-time-range-toolbar-layout="stacked-operator-controls"');
+    expect(html).not.toContain('bg-[var(--hz-ui-active)]');
+    expect(html).not.toContain('min-w-[84px]');
+    expect(html).not.toContain('min-w-[104px]');
+    expect(html).not.toContain('data-time-range-control="hertzbeat-shared"');
+  });
+
+  it('keeps start and end controls visible on the first history render', () => {
+    const html = renderToStaticMarkup(
+      <MonitorHistoryChartGrid
+        items={[{ metrics: 'summary', metric: 'responseTime', unit: '' } as any]}
+        payloads={{}}
+        loadingKeys={[]}
+        errors={{}}
+        selectedKey="summary:responseTime"
+        aggregated={false}
+        showControls
+        historyWindow="1h"
+        historyWindows={[{ value: '1h', label: '1 hour' }]}
+        onRefresh={() => {}}
+        onSelectMetric={() => {}}
+        formatTime={value => `full-${value}`}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-hz-time-range-toolbar-absolute="picker-panel"');
+    expect(html).toContain('data-hz-time-range-toolbar-absolute-layout="single-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-time-entry="single-expression-picker"');
+    expect(html).toContain('data-hz-time-range-toolbar-rail-layout="nowrap"');
+    expect(html).toContain('data-hz-ui="expression-time-range-picker"');
+    expect(html).toContain('data-hz-expression-time-range-picker-layout="expression-single-range"');
+    expect(html).toContain('aria-label="Time range"');
+  });
+
+  it('localizes shared time toolbar refresh and timezone options', () => {
+    const html = renderToStaticMarkup(
+      <MonitorHistoryChartGrid
+        items={[{ metrics: 'summary', metric: 'responseTime', unit: '' } as any]}
+        payloads={{}}
+        loadingKeys={[]}
+        errors={{}}
+        selectedKey="summary:responseTime"
+        aggregated={false}
+        showControls
+        historyWindow="1h"
+        historyWindows={[{ value: '1h', label: zhT('monitor.detail.history.range.last-1h') }]}
+        onRefresh={() => {}}
+        onSelectMetric={() => {}}
+        formatTime={value => `full-${value}`}
+        t={zhT}
+      />
+    );
+
+    expect(html).toContain(zhT('time.range.refresh'));
+    expect(html).toContain(zhT('time.range.manual-refresh'));
+    expect(html).toContain(zhT('time.range.local-timezone'));
+    expect(html).not.toContain('Manual');
+    expect(html).not.toContain('Local</span>');
+  });
+
+  it('externalizes every monitor history time picker label through i18n', () => {
+    const source = readFileSync(join(__dirname, 'monitor-history-chart-grid.tsx'), 'utf8');
+
+    [
+      'time.range.relative',
+      'time.range.recent-ranges',
+      'time.range.custom-range',
+      'time.range.custom-name',
+      'time.range.save-custom-range',
+      'time.range.validation-valid',
+      'time.range.validation-invalid'
+    ].forEach(key => {
+      expect(source).toContain(`t('${key}')`);
+    });
+
+    [
+      'Relative time',
+      'Recent ranges',
+      'Custom range',
+      'Range name',
+      'Save range',
+      'Valid time expression',
+      'Invalid time expression'
+    ].forEach(copy => {
+      expect(source).not.toContain(copy);
+    });
+  });
+
+  it('passes canonical timezone values into the shared history time toolbar', () => {
+    const html = renderToStaticMarkup(
+      <MonitorHistoryChartGrid
+        items={[{ metrics: 'summary', metric: 'responseTime', unit: '' } as any]}
+        payloads={{}}
+        loadingKeys={[]}
+        errors={{}}
+        selectedKey="summary:responseTime"
+        aggregated={false}
+        showControls
+        historyWindow="1h"
+        timeContext={{ timeRange: 'last-1h', from: 'now-6h', to: 'now', timezone: 'Asia/Shanghai' }}
+        historyWindows={[{ value: '1h', label: '1 hour' }]}
+        onRefresh={() => {}}
+        onSelectMetric={() => {}}
+        formatTime={value => `full-${value}`}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-hz-time-range-toolbar-model="expression-from-to"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-mode="named"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-value="Asia/Shanghai"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-route-key="timezone"');
+    expect(html).toContain('Asia/Shanghai');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-control="shared-timezone-select"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-option="local"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-option-route-key="local"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-option="Asia/Shanghai"');
+    expect(html).toContain('data-hz-time-range-toolbar-timezone-option-route-key="timezone"');
+  });
+
+  it('passes canonical auto refresh seconds into the shared history time toolbar', () => {
+    const html = renderToStaticMarkup(
+      <MonitorHistoryChartGrid
+        items={[{ metrics: 'summary', metric: 'responseTime', unit: '' } as any]}
+        payloads={{}}
+        loadingKeys={[]}
+        errors={{}}
+        selectedKey="summary:responseTime"
+        aggregated={false}
+        showControls
+        historyWindow="1h"
+        timeContext={{ timeRange: 'last-1h', from: 'now-1h', to: 'now', refresh: '60', timezone: 'Asia/Shanghai' }}
+        historyWindows={[{ value: '1h', label: '1 hour' }]}
+        onRefresh={() => {}}
+        onSelectMetric={() => {}}
+        formatTime={value => `full-${value}`}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-hz-time-range-toolbar-refresh-mode="auto"');
+    expect(html).toContain('data-hz-time-range-toolbar-refresh-interval="60"');
+    expect(html).toContain('data-hz-time-range-toolbar-live-state="running"');
+    expect(html).not.toContain('data-hz-time-range-toolbar-refresh-interval="60s"');
+  });
+
+  it('renders live=false history routes as manual refresh even when stale refresh remains', () => {
+    const html = renderToStaticMarkup(
+      <MonitorHistoryChartGrid
+        items={[{ metrics: 'summary', metric: 'responseTime', unit: '' } as any]}
+        payloads={{}}
+        loadingKeys={[]}
+        errors={{}}
+        selectedKey="summary:responseTime"
+        aggregated={false}
+        showControls
+        historyWindow="1h"
+        timeContext={{ timeRange: 'last-1h', from: 'now-1h', to: 'now', refresh: '60', live: 'false' }}
+        historyWindows={[{ value: '1h', label: '1 hour' }]}
+        onRefresh={() => {}}
+        onSelectMetric={() => {}}
+        formatTime={value => `full-${value}`}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-hz-time-range-toolbar-refresh-mode="manual"');
+    expect(html).toContain('data-hz-time-range-toolbar-live-state="paused"');
+    expect(html).not.toContain('data-hz-time-range-toolbar-refresh-interval="60"');
+    expect(html).toContain('Manual');
+    expect(html).not.toContain('>1m</span>');
+  });
+
+  it('uses the shared tab primitive for history aggregation mode selection', () => {
+    const html = renderToStaticMarkup(
+      <MonitorHistoryChartGrid
+        items={[{ metrics: 'summary', metric: 'responseTime', unit: '' } as any]}
+        payloads={{}}
+        loadingKeys={[]}
+        errors={{}}
+        selectedKey="summary:responseTime"
+        aggregated
+        showControls
+        historyWindow="1h"
+        historyWindows={[{ value: '1h', label: '1 hour' }]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        onHistoryModeChange={() => {}}
+        onSelectMetric={() => {}}
+        formatTime={value => `full-${value}`}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-monitor-history-mode-owner="hertzbeat-ui-tabs"');
+    expect(html).toContain('data-monitor-history-mode-active="aggregated"');
+    expect(html).toContain('data-hz-ui="tabs"');
+    expect(html).toContain('Raw');
+    expect(html).toContain('Aggregated');
+    expect(html).not.toContain('data-monitor-history-mode-option');
   });
 
   it('uses the shared timeseries chart treatment without leaking raw history count keys', () => {
@@ -69,17 +316,82 @@ describe('monitor history chart grid', () => {
     );
 
     expect(html).toContain('data-monitor-history-chart-visual="shared-timeseries"');
+    expect(html).toContain('data-monitor-history-source="app-yml-metric-catalog"');
+    expect(html).toContain('data-monitor-history-template-source="monitor-yml"');
+    expect(html).toContain('data-monitor-history-panel="summary:responseTime"');
+    expect(html).toContain('data-monitor-history-panel-source="app-yml-metric-catalog"');
+    expect(html).toContain('data-monitor-history-panel-metric="summary.responseTime"');
+    expect(html).not.toContain('data-monitor-history-panel-unit');
+    expect(html).not.toContain('data-dashboard-template');
+    expect(html).not.toContain('data-dashboard-panel');
+    expect(html).toContain('data-hz-ui="monitor-history-chart-grid"');
+    expect(html).toContain('data-monitor-history-chart-grid-owner="hertzbeat-ui-history-chart-grid"');
+    expect(html).toContain('data-monitor-history-chart-grid="shared-history-chart-grid"');
+    expect(html).toContain('data-hz-ui="monitor-history-chart-card"');
+    expect(html).toContain('data-monitor-history-card-owner="hertzbeat-ui-history-chart-card"');
+    expect(html).toContain('data-monitor-history-card-source="hertzbeat-ui-history-chart"');
+    expect(html).toContain('data-monitor-history-card-chrome="hertzbeat-ui-history-chart-inline"');
+    expect(html).toContain('data-monitor-history-card-height="content-driven"');
+    expect(html).toContain('data-monitor-history-card-selected="true"');
+    expect(html).toContain('data-hz-ui="metric-time-series-panel"');
+    expect(html).toContain('data-hz-metric-timeseries-variant="inline"');
+    expect(html).toContain('data-hz-chart-surface-variant="inline"');
+    expect(html).toContain('data-monitor-history-chart-treatment="collector-latency-inline"');
+    expect(html).toContain('data-hz-chart-kind="metric-time-series-echarts"');
+    expect(html).toContain('data-hz-ui="chart-surface"');
+    expect(html).toContain('data-hz-chart-runtime="echarts"');
+    expect(html).toContain('data-hz-chart-surface-selected="true"');
     expect(html).toContain('data-monitor-history-axis-policy="sparse-readable"');
     expect(html).toContain('data-monitor-history-navigator="echarts-native-slider"');
-    expect(html).toContain('data-monitor-history-datazoom-state="local-observation"');
+    expect(html).toContain('data-monitor-history-datazoom-state="toolbar-feedback"');
+    expect(html).toContain('data-monitor-history-datazoom-feedback="time-toolbar"');
     expect(html).toContain('data-monitor-history-datazoom-preserve="preserved"');
-    expect(html).toContain('data-monitor-history-card-height="angular-460px"');
     expect(html).toContain('61 samples');
+    expect(html).not.toContain('angular-chart-cards');
+    expect(html).not.toContain('angular-card-box');
+    expect(html).not.toContain('angular-monitor-data-chart');
+    expect(html).not.toContain('angular-460px');
+    expect(html).not.toContain('data-selected');
+    expect(html).not.toContain('var(--ops-primary)');
     expect(html).not.toContain('monitor.detail.history-series.search.count');
     expect(html).not.toContain('monitor.detail.history-metric.search.count');
   });
 
-  it('keeps chart dataZoom local until the operator explicitly applies it as query time', () => {
+  it('renders history chart favorite actions through the shared monitor favorite icon', () => {
+    const html = renderToStaticMarkup(
+      <MonitorHistoryChartGrid
+        items={[{ metrics: 'summary', metric: 'responseTime', unit: 'ms' } as any]}
+        payloads={{
+          'summary:responseTime': {
+            values: {
+              origin: [
+                { time: 1_000, origin: '100' },
+                { time: 2_000, origin: '120' }
+              ]
+            }
+          }
+        }}
+        loadingKeys={[]}
+        errors={{}}
+        selectedKey="summary:responseTime"
+        aggregated={false}
+        favoriteNames={['summary.responseTime']}
+        onToggleFavorite={() => {}}
+        onSelectMetric={() => {}}
+        formatTime={value => `full-${value}`}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-monitor-history-card-action="favorite"');
+    expect(html).toContain('data-monitor-history-card-action-owner="hertzbeat-ui-favorite-action"');
+    expect(html).toContain('data-hz-ui="monitor-metric-favorite-action"');
+    expect(html).toContain('data-hz-monitor-favorite-active="true"');
+    expect(html).toContain('aria-label="Remove favorite"');
+    expect(html).not.toMatch(/>Remove favorite<\/button>/);
+  });
+
+  it('keeps chart dataZoom visible in the toolbar until the operator explicitly applies it as query time', () => {
     const html = renderToStaticMarkup(
       <MonitorHistoryChartGrid
         items={[{ metrics: 'summary', metric: 'responseTime', unit: 'ms' } as any]}
@@ -106,13 +418,109 @@ describe('monitor history chart grid', () => {
       />
     );
 
-    expect(html).toContain('data-monitor-history-datazoom-state="local-observation"');
+    expect(html).toContain('data-monitor-history-datazoom-state="toolbar-feedback"');
+    expect(html).toContain('data-monitor-history-datazoom-feedback="time-toolbar"');
+    expect(html).toContain('data-hz-echarts-datazoom-feedback="change-callback"');
+    expect(html).toContain('data-hz-echarts-datazoom-interaction="native-live-drag"');
+    expect(html).toContain('data-hz-echarts-datazoom-event="native-datazoom"');
+    expect(html).toContain('data-hz-echarts-datazoom-preserve="preserved"');
     expect(html).toContain('data-monitor-history-zoom-apply="local-to-query-time"');
     expect(html).toContain('data-monitor-history-zoom-apply-state="idle"');
     expect(html).toContain('disabled=""');
     expect(html).toContain('Apply as query time');
   });
 
+  it('keeps dataZoom feedback wired to the shared time toolbar source', () => {
+    const source = readFileSync(join(process.cwd(), 'components/monitor-detail/monitor-history-chart-grid.tsx'), 'utf8');
+
+    expect(source).toContain('HzTimeRangePreviewHandoff');
+    expect(source).toContain('previewApplyTimeContext');
+    expect(source).toContain('previewTimeContext');
+    expect(source).toContain("data-monitor-history-datazoom-preview-state={previewTimeContext ? 'active' : 'idle'}");
+    expect(source).toContain("previewSource={previewTimeContext ? 'chart-datazoom' : undefined}");
+    expect(source).toContain('data-monitor-history-datazoom-handoff-owner="hertzbeat-ui-time-range-preview-handoff"');
+    expect(source).toContain('source="chart-datazoom"');
+    expect(source).toContain('from={previewTimeContext.from}');
+    expect(source).toContain('to={previewTimeContext.to}');
+    expect(source).toContain('applyDisabled={!previewApplyTimeContext}');
+    expect(source).toContain('onApply={() => {');
+    expect(source).toContain('onApplyChartZoomTimeRange?.(previewApplyTimeContext);');
+    expect(source).toContain('setPreviewApplyTimeContext(null);');
+    expect(source).toContain('onReset={() => {');
+    expect(source).not.toContain("data-hz-time-range-toolbar-preview-source={previewTimeContext ? 'chart-datazoom' : undefined}");
+    expect(source).toContain('setPreviewTimeContext(nextContext)');
+    expect(source).toContain('setPreviewApplyTimeContext(nextApplyContext)');
+    expect(source).toContain('onDataZoomChange={nextZoom => {');
+    expect(source).toContain('handleChartZoomChange(key, nextZoom);');
+    expect(source).toContain('showAbsoluteFields');
+    expect(source).toContain('absoluteFieldsLayout="inline"');
+    expect(source).toContain('absoluteInputMode="datetime-local"');
+    expect(source).toContain('timeRangePickerMode="single"');
+    expect(source).toContain('railLayout="nowrap"');
+    expect(source).toContain('timeContext?.from');
+    expect(source).toContain('timeContext?.to');
+    expect(source).toContain('timeContext?.refresh');
+    expect(source).toContain('timeContext?.live');
+    expect(source).toContain('timeContext?.timezone');
+    expect(source).toContain('timeContext?.tz');
+    expect(source).toContain('data-monitor-history-datazoom-feedback="time-toolbar"');
+  });
+
+  it('does not keep an already-applied chart dataZoom range actionable after route synchronization', () => {
+    const source = readFileSync(join(process.cwd(), 'components/monitor-detail/monitor-history-chart-grid.tsx'), 'utf8');
+
+    expect(source).toContain('isSameTimeContextRange');
+    expect(source).toContain('!isSameTimeContextRange(zoomApplyContext, timeContext)');
+    expect(source).not.toContain('function isSameAppliedHistoryTimeContext(');
+  });
+
+  it('uses shared time window conversion for toolbar presets and dataZoom fallback ranges', () => {
+    const source = readFileSync(join(process.cwd(), 'components/monitor-detail/monitor-history-chart-grid.tsx'), 'utf8');
+
+    expect(source).toContain('timeRangeToTimeWindow');
+    expect(source).toContain('timeWindowToTimeRange');
+    expect(source).toContain('timeWindowToTimeRange(historyWindow || historyWindows[0]?.value)');
+    expect(source).toContain('timeRangeToTimeWindow(context.timeRange)');
+    expect(source).toContain('timeRangeToTimeWindow(resetContext.timeRange)');
+    expect(source).not.toContain('function historyWindowToTimeRange(');
+    expect(source).not.toContain('function timeRangeToHistoryWindow(');
+  });
+
+  it('clears local chart dataZoom drafts when the toolbar applies or resets query time', () => {
+    const source = readFileSync(join(process.cwd(), 'components/monitor-detail/monitor-history-chart-grid.tsx'), 'utf8');
+
+    expect(source).toContain('setZoomRangesByKey({});');
+    expect((source.match(/setZoomRangesByKey\(\{\}\);/g) || []).length).toBeGreaterThanOrEqual(3);
+  });
+
+  it('uses the metadata-preserving dataZoom context when applying chart zoom as query time', () => {
+    const source = readFileSync(join(process.cwd(), 'components/monitor-detail/monitor-history-chart-grid.tsx'), 'utf8');
+
+    expect(source).toContain('buildHistoryDataZoomApplyTimeContext');
+    expect(source).toContain('const zoomApplyContext = buildHistoryDataZoomApplyTimeContext(');
+    expect(source).toContain("'data-monitor-history-zoom-apply-url-model': 'expression-from-to'");
+    expect(source).toContain("'data-monitor-history-zoom-apply-from': zoomApplyContext?.from");
+    expect(source).toContain("'data-monitor-history-zoom-apply-to': zoomApplyContext?.to");
+    expect(source).toContain('const canApplyZoom = Boolean(');
+    expect(source).toContain('onApplyChartZoomTimeRange &&');
+    expect(source.indexOf('onApplyChartZoomTimeRange?.(zoomApplyContext);')).toBeLessThan(
+      source.indexOf('setZoomRangesByKey(current => ({ ...current, [key]: undefined }));')
+    );
+    expect(source).toContain('setPreviewTimeContext(null);');
+    expect(source).toContain('setZoomRangesByKey(current => ({ ...current, [key]: undefined }));');
+    expect(source).toContain('onApplyChartZoomTimeRange?.(zoomApplyContext);');
+    expect(source).not.toContain('onApplyChartZoomTimeRange?.(zoomContext);');
+  });
+
+  it('resets applied dataZoom query time through the shared time toolbar instead of only clearing the local draft', () => {
+    const source = readFileSync(join(process.cwd(), 'components/monitor-detail/monitor-history-chart-grid.tsx'), 'utf8');
+
+    expect(source).toContain('onReset={() => {');
+    expect(source).toContain('const resetContext = buildHistoryResetTimeContext(toolbarTimeContext, fallbackTimeRange);');
+    expect(source).toContain('onHistoryTimeContextApply?.(resetContext);');
+    expect(source).toContain('if (nextWindow) onHistoryWindowChange?.(nextWindow);');
+  });
+
   it('keeps zh-CN sample counts and empty history copy localized', () => {
     const chartHtml = renderToStaticMarkup(
       <MonitorHistoryChartGrid
diff --git a/web-next/components/monitor-detail/monitor-history-chart-grid.tsx b/web-next/components/monitor-detail/monitor-history-chart-grid.tsx
index 6130a9cd42..4f7a5368ec 100644
--- a/web-next/components/monitor-detail/monitor-history-chart-grid.tsx
+++ b/web-next/components/monitor-detail/monitor-history-chart-grid.tsx
@@ -1,10 +1,22 @@
 'use client';
 
 import React from 'react';
-import { EChartsPanel, type EChartsDataZoomRange } from '../observability/echarts-panel';
-import { buildTimeRangeControlLabels, TimeRangeControl } from '../observability/time-range-control';
-import { buildHistoryChartEChartsOption, buildHistoryDataZoomTimeContext } from '../../lib/monitor-detail/history-chart';
-import type { TimeContext } from '../../lib/time-context';
+import {
+  HzMonitorHistoryChartCard,
+  HzMonitorHistoryChartGrid,
+  HzMonitorMetricFavoriteAction,
+  HzSegmentedTabs,
+  HzTimeRangePreviewHandoff,
+  HzTimeRangeToolbar,
+  type HzEChartsDataZoomRange
+} from '@hertzbeat/ui';
+import {
+  buildHistoryChartEChartsOption,
+  buildHistoryDataZoomApplyTimeContext,
+  buildHistoryDataZoomPreviewTimeContext,
+  buildHistoryResetTimeContext
+} from '../../lib/monitor-detail/history-chart';
+import { isSameTimeContextRange, timeRangeToTimeWindow, timeWindowToTimeRange, type TimeContext } from '../../lib/time-context';
 import type { MonitorHistoryMetricCatalogItem } from '../../lib/monitor-detail/controller';
 import type { MonitorHistoryData, MonitorHistoryValue } from '../../lib/types';
 
@@ -18,6 +30,11 @@ function historyMetricTitle(item: MonitorHistoryMetricCatalogItem) {
   return `${item.metrics}.${item.metric}`;
 }
 
+function resolveHistoryFavoriteToken(item: MonitorHistoryMetricCatalogItem, favoriteNames: string[]) {
+  const fullPath = historyMetricTitle(item);
+  return favoriteNames.find(name => name === fullPath || name === item.metrics || name === item.metric) ?? null;
+}
+
 function pickPrimaryHistorySeries(payload: MonitorHistoryData | null | undefined): MonitorHistoryValue[] {
   const values = payload?.values ?? {};
   const preferred = values.origin || values.mean;
@@ -27,16 +44,106 @@ function pickPrimaryHistorySeries(payload: MonitorHistoryData | null | undefined
   return firstSeriesKey == null ? [] : values[firstSeriesKey] || [];
 }
 
-function historyWindowToTimeRange(value: string | undefined) {
-  if (!value) return undefined;
-  if (value.startsWith('last-')) return value.toLowerCase();
-  return `last-${value.toLowerCase()}`;
+function buildVisibleHistoryTimeContext(
+  items: MonitorHistoryMetricCatalogItem[],
+  payloads: Record<string, MonitorHistoryData | null | undefined>,
+  fallbackTimeRange?: string
+): TimeContext {
+  const times = items
+    .flatMap(item => pickPrimaryHistorySeries(payloads[historyMetricKey(item)]))
+    .map(value => Number(value.time))
+    .filter(value => Number.isFinite(value))
+    .sort((left, right) => left - right);
+
+  if (times.length < 2) return { timeRange: fallbackTimeRange };
+
+  return {
+    timeRange: fallbackTimeRange,
+    start: String(times[0]),
+    end: String(times[times.length - 1])
+  };
 }
 
-function timeRangeToHistoryWindow(value: string | undefined) {
-  if (!value) return undefined;
-  const normalized = value.replace(/^last-/, '');
-  return normalized.endsWith('w') ? normalized.replace('w', 'W') : normalized;
+function buildHistoryTimeToolbarLabels(t: Translator) {
+  return {
+    preset: t('monitor.detail.history.time-range'),
+    start: t('time.range.start'),
+    end: t('time.range.end'),
+    from: t('time.range.from'),
+    to: t('time.range.to'),
+    absoluteTitle: t('time.range.absolute-title'),
+    quickRanges: t('time.range.quick-ranges'),
+    relativeTitle: t('time.range.relative'),
+    recentRanges: t('time.range.recent-ranges'),
+    customRange: t('time.range.custom-range'),
+    customName: t('time.range.custom-name'),
+    saveCustomRange: t('time.range.save-custom-range'),
+    deleteCustomRange: t('time.range.delete-custom-range'),
+    validationValid: t('time.range.validation-valid'),
+    validationInvalid: t('time.range.validation-invalid'),
+    year: t('time.range.year'),
+    month: t('time.range.month'),
+    weekdays: [
+      t('common.week.1'),
+      t('common.week.2'),
+      t('common.week.3'),
+      t('common.week.4'),
+      t('common.week.5'),
+      t('common.week.6'),
+      t('common.week.7')
+    ],
+    months: [
+      t('common.month.1'),
+      t('common.month.2'),
+      t('common.month.3'),
+      t('common.month.4'),
+      t('common.month.5'),
+      t('common.month.6'),
+      t('common.month.7'),
+      t('common.month.8'),
+      t('common.month.9'),
+      t('common.month.10'),
+      t('common.month.11'),
+      t('common.month.12')
+    ],
+    date: t('time.range.date'),
+    hour: t('time.range.hour'),
+    minute: t('time.range.minute'),
+    second: t('time.range.second'),
+    previousMonth: t('time.range.previous-month'),
+    nextMonth: t('time.range.next-month'),
+    previousYears: t('time.range.previous-years'),
+    nextYears: t('time.range.next-years'),
+    decrease: t('time.range.decrease'),
+    increase: t('time.range.increase'),
+    clear: t('common.clear'),
+    absolutePlaceholder: t('time.range.unset'),
+    refresh: t('time.range.refresh'),
+    timezone: t('time.range.timezone'),
+    apply: t('time.range.apply'),
+    applyAria: t('time.range.apply-aria'),
+    refreshAction: t('time.range.refresh-action'),
+    reset: t('time.range.reset'),
+    resetAria: t('time.range.reset-aria')
+  };
+}
+
+function buildHistoryRefreshOptions(t: Translator) {
+  return [
+    { value: '', label: t('time.range.manual-refresh') },
+    { value: '10', label: '10s' },
+    { value: '30', label: '30s' },
+    { value: '60', label: '1m' },
+    { value: '300', label: '5m' }
+  ];
+}
+
+function buildHistoryTimezoneOptions(t: Translator) {
+  return [
+    { value: '', label: t('time.range.local-timezone') },
+    { value: 'Asia/Shanghai', label: 'Asia/Shanghai' },
+    { value: 'UTC', label: 'UTC' }
+  ];
 }
 
 export function MonitorHistoryChartGrid({
@@ -57,6 +164,8 @@ export function MonitorHistoryChartGrid({
   onHistoryModeChange,
   onRefresh,
   onSelectMetric,
+  favoriteNames = [],
+  onToggleFavorite,
   formatTime,
   t
 }: {
@@ -77,12 +186,16 @@ export function MonitorHistoryChartGrid({
   onHistoryModeChange?: (value: boolean) => void;
   onRefresh?: () => void;
   onSelectMetric: (key: string) => void;
+  favoriteNames?: string[];
+  onToggleFavorite?: (item: MonitorHistoryMetricCatalogItem) => Promise<void> | void;
   formatTime: (value?: number | string | null) => string;
   t: Translator;
 }) {
-  const [zoomRangesByKey, setZoomRangesByKey] = React.useState<Record<string, EChartsDataZoomRange | undefined>>({});
+  const [zoomRangesByKey, setZoomRangesByKey] = React.useState<Record<string, HzEChartsDataZoomRange | undefined>>({});
+  const [previewTimeContext, setPreviewTimeContext] = React.useState<TimeContext | null>(null);
+  const [previewApplyTimeContext, setPreviewApplyTimeContext] = React.useState<TimeContext | null>(null);
 
-  const handleChartZoomChange = React.useCallback((key: string, nextZoom: EChartsDataZoomRange) => {
+  const handleChartZoomChange = React.useCallback((key: string, nextZoom: HzEChartsDataZoomRange) => {
     setZoomRangesByKey(current => {
       const previous = current[key];
       if (
@@ -96,70 +209,136 @@ export function MonitorHistoryChartGrid({
       return { ...current, [key]: nextZoom };
     });
   }, []);
+
+  React.useEffect(() => {
+    setPreviewTimeContext(null);
+    setPreviewApplyTimeContext(null);
+    setZoomRangesByKey({});
+  }, [
+    aggregated,
+    historyWindow,
+    timeContext?.end,
+    timeContext?.from,
+    timeContext?.live,
+    timeContext?.refresh,
+    timeContext?.start,
+    timeContext?.timeRange,
+    timeContext?.tz,
+    timeContext?.timezone,
+    timeContext?.to
+  ]);
+
   if (items.length === 0) return null;
 
   const loadingSet = new Set(loadingKeys);
+  const fallbackTimeRange = timeWindowToTimeRange(historyWindow || historyWindows[0]?.value);
+  const visibleTimeContext = buildVisibleHistoryTimeContext(items, payloads, fallbackTimeRange);
+  const toolbarTimeContext = previewTimeContext || {
+    ...visibleTimeContext,
+    ...timeContext,
+    start: timeContext?.start || visibleTimeContext.start,
+    end: timeContext?.end || visibleTimeContext.end
+  };
 
   return (
     <div
       className="space-y-2"
-      data-monitor-history-chart-grid="angular-chart-cards"
       data-monitor-history-chart-visual="shared-timeseries"
       data-monitor-history-axis-policy="sparse-readable"
       data-monitor-history-navigator="echarts-native-slider"
-      data-monitor-history-datazoom-state="local-observation"
+      data-monitor-history-datazoom-state="toolbar-feedback"
+      data-monitor-history-datazoom-feedback="time-toolbar"
       data-monitor-history-datazoom-preserve="preserved"
+      data-monitor-history-selection-reset="angular-chart-reload"
+      data-monitor-history-source="app-yml-metric-catalog"
+      data-monitor-history-template-source="monitor-yml"
     >
       {showControls ? (
         <div
           className="space-y-2"
-          data-monitor-history-time-toolbar="shared-time-context-control"
+          data-monitor-history-time-toolbar="hertzbeat-ui-time-range-toolbar"
+          data-monitor-history-time-toolbar-owner="hertzbeat-ui-time-range-toolbar"
           data-monitor-history-time-toolbar-visual="cold-metrics-controls"
+          data-monitor-history-datazoom-feedback="time-toolbar"
+          data-monitor-history-datazoom-preview-state={previewTimeContext ? 'active' : 'idle'}
+          data-monitor-history-refresh-contract="angular-first-page-reload"
         >
-          <TimeRangeControl
-            value={timeContext || { timeRange: historyWindowToTimeRange(historyWindow || historyWindows[0]?.value) }}
+          <HzTimeRangeToolbar
+            value={toolbarTimeContext}
+            showAbsoluteFields
+            absoluteFieldsLayout="inline"
+            absoluteInputMode="datetime-local"
+            timeRangePickerMode="single"
+            railLayout="nowrap"
+            previewSource={previewTimeContext ? 'chart-datazoom' : undefined}
             presets={historyWindows.map(option => ({
-              value: historyWindowToTimeRange(option.value) || option.value,
+              value: timeWindowToTimeRange(option.value) || option.value,
               label: option.label
             }))}
-            labels={{
-              ...buildTimeRangeControlLabels(t),
-              preset: t('monitor.detail.history.time-range'),
-            }}
-            onApply={(context: TimeContext) => {
+            refreshOptions={buildHistoryRefreshOptions(t)}
+            timezoneOptions={buildHistoryTimezoneOptions(t)}
+            labels={buildHistoryTimeToolbarLabels(t)}
+            onApply={context => {
+              setPreviewTimeContext(null);
+              setPreviewApplyTimeContext(null);
+              setZoomRangesByKey({});
               onHistoryTimeContextApply?.(context);
-              const nextWindow = timeRangeToHistoryWindow(context.timeRange);
+              const nextWindow = timeRangeToTimeWindow(context.timeRange);
               if (nextWindow) onHistoryWindowChange?.(nextWindow);
             }}
             onRefresh={onRefresh}
-            presetSelectProps={{ 'data-monitor-history-time-range-select': 'true' } as React.ButtonHTMLAttributes<HTMLButtonElement>}
+            onReset={() => {
+              setPreviewTimeContext(null);
+              setPreviewApplyTimeContext(null);
+              setZoomRangesByKey({});
+              const resetContext = buildHistoryResetTimeContext(toolbarTimeContext, fallbackTimeRange);
+              onHistoryTimeContextApply?.(resetContext);
+              const nextWindow = timeRangeToTimeWindow(resetContext.timeRange);
+              if (nextWindow) onHistoryWindowChange?.(nextWindow);
+            }}
             presetOptionDataAttribute="data-monitor-history-time-range-option"
             refreshActionProps={{ 'data-monitor-history-refresh-action': 'true' } as React.ButtonHTMLAttributes<HTMLButtonElement>}
           />
+          {previewTimeContext ? (
+            <HzTimeRangePreviewHandoff
+              state="preview"
+              source="chart-datazoom"
+              from={previewTimeContext.from}
+              to={previewTimeContext.to}
+              applyLabel={t('monitor.detail.history.zoom.apply')}
+              resetLabel={t('time.range.reset')}
+              applyDisabled={!previewApplyTimeContext}
+              onApply={() => {
+                if (!previewApplyTimeContext) return;
+                onApplyChartZoomTimeRange?.(previewApplyTimeContext);
+                setPreviewTimeContext(null);
+                setPreviewApplyTimeContext(null);
+                setZoomRangesByKey({});
+              }}
+              onReset={() => {
+                setPreviewTimeContext(null);
+                setPreviewApplyTimeContext(null);
+                setZoomRangesByKey({});
+              }}
+              data-monitor-history-datazoom-handoff-owner="hertzbeat-ui-time-range-preview-handoff"
+            />
+          ) : null}
           {historyModes.length ? (
-            <div className="flex flex-wrap items-center gap-2">
-              {historyModes.map(option => (
-                <button
-                  key={String(option.value)}
-                  type="button"
-                  className={[
-                    'inline-flex h-8 items-center justify-center rounded-[3px] border px-3 text-[12px] font-semibold transition-colors',
-                    aggregated === option.value
-                      ? 'border-[var(--ops-primary)] bg-[var(--ops-surface-raised)] text-[var(--ops-text-primary)]'
-                      : 'border-[var(--ops-border-color)] bg-transparent text-[var(--ops-text-secondary)] hover:border-[var(--ops-border-strong)] hover:bg-[var(--ops-surface-panel)] hover:text-[var(--ops-text-primary)]'
-                  ].join(' ')}
-                  data-monitor-history-mode-select={String(option.value)}
-                  data-monitor-history-mode-option={String(option.value)}
-                  onClick={() => onHistoryModeChange?.(option.value)}
-                >
-                  {option.label}
-                </button>
-              ))}
+            <div
+              className="min-w-0"
+              data-monitor-history-mode-owner="hertzbeat-ui-tabs"
+              data-monitor-history-mode-active={aggregated ? 'aggregated' : 'raw'}
+            >
+              <HzSegmentedTabs
+                activeId={String(aggregated)}
+                items={historyModes.map(option => ({ id: String(option.value), label: option.label }))}
+                onSelect={value => onHistoryModeChange?.(value === 'true')}
+              />
             </div>
           ) : null}
         </div>
       ) : null}
-      <div className="grid gap-2 xl:grid-cols-2">
+      <HzMonitorHistoryChartGrid data-monitor-history-chart-grid-use="monitor-detail-page">
         {items.map(item => {
           const key = historyMetricKey(item);
           const title = historyMetricTitle(item);
@@ -167,8 +346,18 @@ export function MonitorHistoryChartGrid({
           const values = pickPrimaryHistorySeries(payloads[key]);
           const loading = loadingSet.has(key);
           const error = errors[key];
-          const zoomContext = buildHistoryDataZoomTimeContext(values, zoomRangesByKey[key], historyWindowToTimeRange(historyWindow));
-          const canApplyZoom = Boolean(zoomContext && onApplyChartZoomTimeRange);
+          const favorited = Boolean(resolveHistoryFavoriteToken(item, favoriteNames));
+          const zoomApplyContext = buildHistoryDataZoomApplyTimeContext(
+            values,
+            zoomRangesByKey[key],
+            timeContext,
+            timeWindowToTimeRange(historyWindow)
+          );
+          const canApplyZoom = Boolean(
+            zoomApplyContext &&
+            onApplyChartZoomTimeRange &&
+            !isSameTimeContextRange(zoomApplyContext, timeContext)
+          );
           const option = buildHistoryChartEChartsOption({
             values,
             formatTime,
@@ -177,85 +366,77 @@ export function MonitorHistoryChartGrid({
           });
 
           return (
-            <section
+            <HzMonitorHistoryChartCard
               key={key}
-              role="button"
-              tabIndex={0}
-              className={[
-                'monitor-detail-card monitor-detail-card--history-flat monitor-workbench-surface monitor-workbench-surface--plain min-h-[460px] min-w-0 space-y-3 rounded-[3px] border bg-[var(--ops-surface-raised)] p-3 transition-colors',
-                selected ? 'border-[var(--ops-primary)]' : 'border-[var(--ops-border-color)] hover:border-[var(--ops-border-strong)]'
-              ].join(' ')}
-              data-monitor-history-card={key}
-              data-monitor-history-card-source="angular-monitor-data-chart"
-              data-monitor-history-card-chrome="angular-card-box"
-              data-monitor-history-card-height="angular-460px"
-              data-selected={selected ? 'true' : 'false'}
-              onClick={() => onSelectMetric(key)}
-              onKeyDown={event => {
-                if (event.key !== 'Enter' && event.key !== ' ') return;
-                event.preventDefault();
-                onSelectMetric(key);
-              }}
-            >
-              <header className="monitor-workbench-surface__header flex items-start justify-between gap-3 border-b border-[var(--ops-border-color)] pb-2">
-                <div className="monitor-workbench-card-title__copy min-w-0">
-                  <div className="monitor-workbench-card-title__title truncate text-[16px] font-semibold leading-6 text-[var(--ops-text-primary)]">
-                    {title}
-                  </div>
-                </div>
-                <div className="flex flex-none items-center gap-2">
-                  {item.unit ? (
-                    <span className="text-[11px] font-semibold text-[var(--ops-text-tertiary)]">{item.unit}</span>
-                  ) : null}
-                  {onApplyChartZoomTimeRange ? (
-                    <button
-                      type="button"
-                      className="inline-flex h-7 items-center justify-center rounded-[3px] border border-[var(--ops-border-color)] bg-transparent px-2 text-[11px] font-semibold text-[var(--ops-text-secondary)] transition-colors enabled:hover:border-[var(--ops-border-strong)] enabled:hover:bg-[var(--ops-surface-panel)] enabled:hover:text-[var(--ops-text-primary)] disabled:cursor-not-allowed disabled:opacity-45"
-                      data-monitor-history-zoom-apply="local-to-query-time"
-                      data-monitor-history-zoom-apply-state={canApplyZoom ? 'ready' : 'idle'}
-                      disabled={!canApplyZoom}
-                      onClick={event => {
-                        event.stopPropagation();
-                        if (!zoomContext) return;
-                        onApplyChartZoomTimeRange(zoomContext);
-                      }}
-                    >
-                      {t('monitor.detail.history.zoom.apply')}
-                    </button>
-                  ) : null}
-                </div>
-              </header>
-
-              {loading ? (
-                <div className="border-y border-[var(--ops-border-color)] px-3 py-12 text-sm text-[var(--ops-text-secondary)]">
-                  {t('common.loading')}
-                </div>
-              ) : error ? (
-                <div className="border-y border-rose-400/20 bg-rose-400/10 px-3 py-12 text-sm text-rose-200">
-                  {error}
-                </div>
-              ) : values.length > 0 ? (
-                <EChartsPanel
-                  option={option}
-                  height={360}
-                  className="rounded-none border-x-0 border-y border-[var(--ops-border-color)] bg-transparent"
-                  tone="operator"
-                  preserveDataZoom
-                  onDataZoomChange={nextZoom => handleChartZoomChange(key, nextZoom)}
+              cardKey={key}
+              heading={title}
+              unit={item.unit || undefined}
+              selected={selected}
+              footer={values.length ? `${values.length} ${t('monitor.detail.history-series.search.count')}` : '-'}
+              option={values.length > 0 ? option : undefined}
+              height={360}
+              loading={loading}
+              loadingLabel={t('common.loading')}
+              error={error}
+              actions={onToggleFavorite ? (
+                <HzMonitorMetricFavoriteAction
+                  active={favorited}
+                  label={favorited ? t('monitor.detail.favorite.remove') : t('monitor.detail.favorite.add')}
+                  onClick={event => {
+                    event.stopPropagation();
+                    void onToggleFavorite(item);
+                  }}
+                  data-monitor-history-card-action="favorite"
+                  data-monitor-history-card-action-owner="hertzbeat-ui-favorite-action"
                 />
-              ) : (
-                <div className="border-y border-[var(--ops-border-color)] px-3 py-12 text-sm text-[var(--ops-text-secondary)]">
-                  <div className="font-medium text-[var(--ops-text-primary)]">{t('monitor.detail.history.blocker.title')}</div>
-                  <div className="mt-1">{t('monitor.detail.history.blocker.copy')}</div>
-                </div>
-              )}
-              <div className="text-[12px] text-[var(--ops-text-tertiary)]">
-                {values.length ? `${values.length} ${t('monitor.detail.history-series.search.count')}` : '-'}
-              </div>
-            </section>
+              ) : null}
+              emptyTitle={t('monitor.detail.history.blocker.title')}
+              emptyDescription={t('monitor.detail.history.blocker.copy')}
+              zoomActionLabel={onApplyChartZoomTimeRange ? t('monitor.detail.history.zoom.apply') : undefined}
+              zoomActionDisabled={!canApplyZoom}
+              zoomActionProps={{
+                'data-monitor-history-zoom-apply': 'local-to-query-time',
+                'data-monitor-history-zoom-apply-state': canApplyZoom ? 'ready' : 'idle',
+                'data-monitor-history-zoom-apply-url-model': 'expression-from-to',
+                'data-monitor-history-zoom-apply-from': zoomApplyContext?.from,
+                'data-monitor-history-zoom-apply-to': zoomApplyContext?.to
+              } as React.ButtonHTMLAttributes<HTMLButtonElement>}
+              onZoomAction={() => {
+                if (!zoomApplyContext) return;
+                onApplyChartZoomTimeRange?.(zoomApplyContext);
+                setPreviewTimeContext(null);
+                setPreviewApplyTimeContext(null);
+                setZoomRangesByKey(current => ({ ...current, [key]: undefined }));
+              }}
+              preserveDataZoom
+              surfaceProps={{
+                'data-monitor-history-panel': key,
+                'data-monitor-history-panel-source': 'app-yml-metric-catalog',
+                'data-monitor-history-panel-metric': title,
+                'data-monitor-history-panel-unit': item.unit || undefined
+              } as React.HTMLAttributes<HTMLElement>}
+              onDataZoomChange={nextZoom => {
+                handleChartZoomChange(key, nextZoom);
+                const nextContext = buildHistoryDataZoomPreviewTimeContext(
+                  values,
+                  nextZoom,
+                  timeContext,
+                  timeWindowToTimeRange(historyWindow)
+                );
+                const nextApplyContext = buildHistoryDataZoomApplyTimeContext(
+                  values,
+                  nextZoom,
+                  timeContext,
+                  timeWindowToTimeRange(historyWindow)
+                );
+                setPreviewTimeContext(nextContext);
+                setPreviewApplyTimeContext(nextApplyContext);
+              }}
+              onSelect={() => onSelectMetric(key)}
+            />
           );
         })}
-      </div>
+      </HzMonitorHistoryChartGrid>
     </div>
   );
 }
diff --git a/web-next/components/monitor-detail/monitor-history-panel.test.tsx b/web-next/components/monitor-detail/monitor-history-panel.test.tsx
index 73fa461333..879fd3c26c 100644
--- a/web-next/components/monitor-detail/monitor-history-panel.test.tsx
+++ b/web-next/components/monitor-detail/monitor-history-panel.test.tsx
@@ -80,12 +80,15 @@ describe('monitor history panel', () => {
     expect(html).toContain('Next series');
     expect(html).toContain('Previous point');
     expect(html).toContain('Next point');
-    expect(html).toContain('History chart');
-    expect(html).toContain('Selected series');
+    expect(html).not.toContain('History chart');
+    expect(html).not.toContain('Selected series');
     expect(html).not.toContain('Metric timeline canvas');
     expect(html).not.toContain('Series compare lane');
     expect(html).toMatch(/data-monitor-surface=\"history-stage\"/);
-    expect(html).toMatch(/data-monitor-surface-panel=\"series-compare\"/);
+    expect(html).toContain('data-monitor-history-stage-owner="hertzbeat-ui-detail-stage"');
+    expect(html).toContain('data-monitor-history-selection-reset="angular-chart-reload"');
+    expect(html).toContain('data-monitor-detail-stage-rhythm="shared-stack"');
+    expect(html).not.toMatch(/data-monitor-surface-panel=\"series-compare\"/);
   });
 
   it('disables series navigation at the current boundaries', () => {
@@ -223,6 +226,394 @@ describe('monitor history panel', () => {
     expect(html).toMatch(/<button[^>]*(data-compare-selected="true"[^>]*disabled=""|disabled=""[^>]*data-compare-selected="true")[^>]*>[\s\S]*host=db-2/);
     expect(html).toMatch(/<button[^>]*data-compare-row-selected="true"[^>]*>[\s\S]*host=db-2/);
     expect(html).toMatch(/<button[^>]*data-compare-row-selected="false"[^>]*>[\s\S]*host=db-1/);
+    expect(html).toContain('data-monitor-history-compare-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-hz-ui="data-meta-text"');
+  });
+
+  it('routes history selectable rows through shared HertzBeat UI ownership', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
+    const historyPayload = {
+      values: {
+        'host=db-1': [
+          { mean: '15', min: '10', max: '20', time: 1 },
+          { mean: '25', min: '22', max: '29', time: 2 }
+        ],
+        'host=db-2': [
+          { mean: '18', min: '12', max: '24', time: 1 },
+          { mean: '28', min: '24', max: '31', time: 2 }
+        ]
+      }
+    } as any;
+
+    const html = renderToStaticMarkup(
+      <MonitorHistoryPanel
+        payload={historyPayload}
+        selectedSeriesKey="host=db-2"
+        selectedPointIndex={1}
+        aggregated
+        historyWindow="1h"
+        historyWindows={[
+          { value: '30m', label: '30m' },
+          { value: '1h', label: '1h' }
+        ]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        visibleSeriesKeys={['host=db-1', 'host=db-2']}
+        expanded={false}
+        onSelectSeries={() => {}}
+        onSelectPoint={() => {}}
+        onRefresh={() => {}}
+        onSetHistoryWindow={() => {}}
+        onSetHistoryMode={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={value => `t${value}`}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('HzSelectableRows');
+    expect(source).toContain('data-monitor-history-selectable-owner="hertzbeat-ui-selectable-rows"');
+    expect(source).toContain('data-monitor-history-compare-owner="hertzbeat-ui-selectable-rows"');
+    expect(source).not.toContain('ObservabilitySelectableRows');
+    expect(source).not.toContain('ObservabilitySelectableRowsOrDetails');
+    expect(html).toContain('data-hz-ui="selectable-rows"');
+    expect(html).toContain('data-hz-selectable-row-owner="hertzbeat-ui-selectable-rows"');
+    expect(html).toContain('data-monitor-history-selectable-owner="hertzbeat-ui-selectable-rows"');
+    expect(html).toContain('data-monitor-history-compare-owner="hertzbeat-ui-selectable-rows"');
+    expect(html).toContain('data-hz-selectable-row-style="left-rail"');
+    expect(html).toMatch(/<button[^>]*data-series-selected="true"[^>]*>[\s\S]*host=db-2/);
+    expect(html).toMatch(/<button[^>]*data-compare-row-selected="true"[^>]*>[\s\S]*host=db-2/);
+  });
+
+  it('routes history buttons and toggle chips through shared HertzBeat UI controls', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
+    const historyPayload = {
+      values: {
+        'host=db-1': [
+          { mean: '15', min: '10', max: '20', time: 1 },
+          { mean: '25', min: '22', max: '29', time: 2 }
+        ],
+        'host=db-2': [
+          { mean: '18', min: '12', max: '24', time: 1 },
+          { mean: '28', min: '24', max: '31', time: 2 }
+        ]
+      }
+    } as any;
+
+    const html = renderToStaticMarkup(
+      <MonitorHistoryPanel
+        payload={historyPayload}
+        selectedSeriesKey="host=db-2"
+        selectedPointIndex={1}
+        aggregated
+        historyWindow="1h"
+        historyWindows={[
+          { value: '30m', label: '30m' },
+          { value: '1h', label: '1h' }
+        ]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        visibleSeriesKeys={['host=db-1', 'host=db-2']}
+        expanded={false}
+        onSelectSeries={() => {}}
+        onSelectPoint={() => {}}
+        onRefresh={() => {}}
+        onSetHistoryWindow={() => {}}
+        onSetHistoryMode={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={value => `t${value}`}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('HzButton');
+    expect(source).toContain('HzUnderlineToggle');
+    expect(source).toContain('data-monitor-history-control-owner="hertzbeat-ui-button"');
+    expect(source).toContain('data-monitor-history-toggle-owner="hertzbeat-ui-underline-toggle"');
+    expect(source).toContain('HzActionGroup');
+    expect(source).toContain('data-monitor-history-action-owner="hertzbeat-ui-action-group"');
+    expect(source).toContain('layout="split"');
+    expect(source).not.toContain('className="w-full justify-between"');
+    expect(source).not.toContain('className="flex flex-wrap gap-2"');
+    expect(source).not.toContain('className="flex flex-wrap items-center justify-between gap-2"');
+    expect(source).not.toContain('ObservabilityControlButton');
+    expect(source).not.toContain('ObservabilityChipToggle');
+    expect(html).toContain('data-hz-ui="button"');
+    expect(html).toContain('data-hz-ui="underline-toggle"');
+    expect(html).toContain('data-monitor-history-control-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-monitor-history-toggle-owner="hertzbeat-ui-underline-toggle"');
+    expect(html).toContain('data-monitor-history-action-owner="hertzbeat-ui-action-group"');
+    expect(html).toContain('data-hz-action-group-layout="split"');
+    expect(html).toContain('data-hz-control-height="28"');
+    expect(html).toContain('data-hz-control-edge="bottom-underline"');
+    expect(html).toMatch(/<button[^>]*data-chart-selected="true"[^>]*>[\s\S]*Mean/);
+    expect(html).toMatch(/<button[^>]*data-compare-selected="true"[^>]*>[\s\S]*host=db-2/);
+    expect(html).not.toContain('data-hz-ui="chip-toggle"');
+    expect(html).not.toContain('data-hz-chip-toggle-owner');
+  });
+
+  it('routes the history persistence blocker through shared HertzBeat UI feedback chrome', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
+    const emptyPayload = { values: { 'host=db-1': [] } } as any;
+
+    const html = renderToStaticMarkup(
+      <MonitorHistoryPanel
+        payload={emptyPayload}
+        selectedSeriesKey="host=db-1"
+        selectedPointIndex={null}
+        aggregated
+        historyWindow="1h"
+        historyWindows={[
+          { value: '30m', label: '30m' },
+          { value: '1h', label: '1h' }
+        ]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        visibleSeriesKeys={['host=db-1']}
+        expanded={false}
+        onSelectSeries={() => {}}
+        onSelectPoint={() => {}}
+        onRefresh={() => {}}
+        onSetHistoryWindow={() => {}}
+        onSetHistoryMode={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={value => `t${value}`}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('HzInlineFeedback');
+    expect(source).toContain('data-monitor-history-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).not.toContain('border-y border-amber-300/16 bg-amber-300/[0.06]');
+    expect(html).toContain('data-hz-ui="inline-feedback"');
+    expect(html).toContain('data-monitor-history-feedback-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('data-hz-feedback-tone="warning"');
+  });
+
+  it('routes history series and point navigation through the shared monitor row navigator', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
+    const historyPayload = {
+      values: {
+        'host=db-1': [
+          { mean: '15', min: '10', max: '20', time: 1 },
+          { mean: '25', min: '22', max: '29', time: 2 }
+        ],
+        'host=db-2': [
+          { mean: '18', min: '12', max: '24', time: 1 },
+          { mean: '28', min: '24', max: '31', time: 2 }
+        ]
+      }
+    } as any;
+
+    const html = renderToStaticMarkup(
+      <MonitorHistoryPanel
+        payload={historyPayload}
+        selectedSeriesKey="host=db-2"
+        selectedPointIndex={1}
+        aggregated
+        historyWindow="1h"
+        historyWindows={[
+          { value: '30m', label: '30m' },
+          { value: '1h', label: '1h' }
+        ]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        visibleSeriesKeys={['host=db-1', 'host=db-2']}
+        expanded={false}
+        onSelectSeries={() => {}}
+        onSelectPoint={() => {}}
+        onRefresh={() => {}}
+        onSetHistoryWindow={() => {}}
+        onSetHistoryMode={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={value => `t${value}`}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('HzMonitorRowNavigator');
+    expect(source).toContain('data-monitor-history-row-nav-owner="hertzbeat-ui-row-navigator"');
+    expect(source).not.toContain('border-y border-[var(--ops-border-color)] px-3 py-2 text-xs text-[var(--ops-text-secondary)]');
+    expect(html).toContain('data-hz-ui="monitor-row-navigator"');
+    expect(html).toContain('data-monitor-history-row-nav-owner="hertzbeat-ui-row-navigator"');
+    expect(html).toContain('data-monitor-row-nav-action="previous"');
+    expect(html).toContain('data-monitor-row-nav-action="next"');
+  });
+
+  it('routes history compare scope through the shared monitor control band', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
+    const historyPayload = {
+      values: {
+        'host=db-1': [
+          { mean: '15', min: '10', max: '20', time: 1 },
+          { mean: '25', min: '22', max: '29', time: 2 }
+        ],
+        'host=db-2': [
+          { mean: '18', min: '12', max: '24', time: 1 },
+          { mean: '28', min: '24', max: '31', time: 2 }
+        ]
+      }
+    } as any;
+
+    const html = renderToStaticMarkup(
+      <MonitorHistoryPanel
+        payload={historyPayload}
+        selectedSeriesKey="host=db-2"
+        selectedPointIndex={1}
+        aggregated
+        historyWindow="1h"
+        historyWindows={[
+          { value: '30m', label: '30m' },
+          { value: '1h', label: '1h' }
+        ]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        visibleSeriesKeys={['host=db-1', 'host=db-2']}
+        expanded={false}
+        onSelectSeries={() => {}}
+        onSelectPoint={() => {}}
+        onRefresh={() => {}}
+        onSetHistoryWindow={() => {}}
+        onSetHistoryMode={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={value => `t${value}`}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('HzMonitorControlBand');
+    expect(source).toContain('HzDataMetaText');
+    expect(source).toContain('data-monitor-history-compare-band-owner="hertzbeat-ui-control-band"');
+    expect(source).toContain('data-monitor-history-compare-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(source).not.toContain('className="space-y-2 border-y border-[var(--ops-border-color)] px-3 py-2" data-monitor-surface-panel="series-compare"');
+    expect(source).not.toContain('<span className="text-[var(--ops-text-tertiary)]">{row.copy}</span>');
+    expect(html).toContain('data-hz-ui="monitor-control-band"');
+    expect(html).toContain('data-monitor-control-band-owner="hertzbeat-ui-control-band"');
+    expect(html).toContain('data-monitor-history-compare-band-owner="hertzbeat-ui-control-band"');
+    expect(html).toContain('data-monitor-history-compare-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-compare-selected="true"');
+  });
+
+  it('routes history chart scope through the shared monitor control band', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
+    const historyPayload = {
+      values: {
+        'host=db-1': [
+          { mean: '15', min: '10', max: '20', time: 1 },
+          { mean: '25', min: '22', max: '29', time: 2 }
+        ]
+      }
+    } as any;
+
+    const html = renderToStaticMarkup(
+      <MonitorHistoryPanel
+        payload={historyPayload}
+        selectedSeriesKey="host=db-1"
+        selectedPointIndex={1}
+        aggregated
+        historyWindow="1h"
+        historyWindows={[
+          { value: '30m', label: '30m' },
+          { value: '1h', label: '1h' }
+        ]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        visibleSeriesKeys={['host=db-1']}
+        expanded={false}
+        onSelectSeries={() => {}}
+        onSelectPoint={() => {}}
+        onRefresh={() => {}}
+        onSetHistoryWindow={() => {}}
+        onSetHistoryMode={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={value => `t${value}`}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('data-monitor-history-chart-band-owner="hertzbeat-ui-control-band"');
+    expect(source).toContain('variant="embedded"');
+    expect(source).not.toContain('className="px-0"');
+    expect(source).toContain('HzEChartsPanel');
+    expect(source).toContain('HzMonitorEvidenceFrame');
+    expect(source).toContain('data-monitor-history-chart-frame-owner="hertzbeat-ui-evidence-frame"');
+    expect(source).toContain('data-monitor-history-chart-owner="hertzbeat-ui-echarts-panel"');
+    expect(source).not.toContain("import { EChartsPanel } from '../observability/echarts-panel'");
+    expect(source).not.toContain('className="rounded-none border-x-0 border-y border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)]"');
+    expect(source).not.toContain('className="space-y-2 border-y border-[var(--ops-border-color)] px-0 py-2"');
+    expect(source).not.toContain('className="space-y-3 border-y border-[var(--ops-border-color)] py-3"');
+    expect(html).toContain('data-hz-ui="monitor-control-band"');
+    expect(html).toContain('data-hz-ui="monitor-evidence-frame"');
+    expect(html).toContain('data-monitor-history-chart-frame-owner="hertzbeat-ui-evidence-frame"');
+    expect(html).toContain('data-hz-ui="echarts-panel"');
+    expect(html).toContain('data-monitor-history-chart-owner="hertzbeat-ui-echarts-panel"');
+    expect(html).toContain('data-monitor-history-chart-band-owner="hertzbeat-ui-control-band"');
+    expect(html).toContain('data-hz-control-band-variant="embedded"');
+    expect(html).toContain('data-chart-selected="true"');
+  });
+
+  it('routes history summary and selected-point detail rows through shared HertzBeat UI ownership', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
+    const historyPayload = {
+      values: {
+        'host=db-1': [
+          { mean: '15', min: '10', max: '20', time: 1 },
+          { mean: '25', min: '22', max: '29', time: 2 }
+        ]
+      }
+    } as any;
+
+    const html = renderToStaticMarkup(
+      <MonitorHistoryPanel
+        payload={historyPayload}
+        selectedSeriesKey="host=db-1"
+        selectedPointIndex={1}
+        aggregated
+        historyWindow="1h"
+        historyWindows={[
+          { value: '30m', label: '30m' },
+          { value: '1h', label: '1h' }
+        ]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        visibleSeriesKeys={['host=db-1']}
+        expanded={false}
+        onSelectSeries={() => {}}
+        onSelectPoint={() => {}}
+        onRefresh={() => {}}
+        onSetHistoryWindow={() => {}}
+        onSetHistoryMode={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={value => `t${value}`}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('HzDetailRows');
+    expect(source).toContain('data-monitor-history-summary-owner="hertzbeat-ui-detail-rows"');
+    expect(source).toContain('data-monitor-history-selected-point-owner="hertzbeat-ui-detail-rows"');
+    expect(source).not.toContain('ObservabilityDetailRows');
+    expect(html).toContain('data-hz-ui="detail-rows"');
+    expect(html).toContain('data-hz-detail-rows-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-hz-detail-rows-style="flat-evidence"');
+    expect(html).toContain('data-monitor-history-summary-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('data-monitor-history-selected-point-owner="hertzbeat-ui-detail-rows"');
+    expect(html).toContain('Latest value');
+    expect(html).toContain('selected point');
   });
 
   it('limits compare rows to the currently visible series keys', () => {
@@ -554,10 +945,49 @@ describe('monitor history panel', () => {
     expect(lastPointHtml).toMatch(/<button[^>]*disabled=""[^>]*>Next point<\/button>/);
   });
 
-  it('routes the fullscreen shell through the shared workbench fullscreen owner', () => {
+  it('routes the fullscreen shell through the shared monitor fullscreen frame owner', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-history-panel.tsx'), 'utf8');
 
-    expect(source).toContain('WorkbenchFullscreenShell');
-    expect(source).not.toContain('hb-scrollbar max-h-[92vh] w-full max-w-6xl overflow-auto rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] p-4 shadow-[0_24px_80px_rgba(0,0,0,0.45)]');
+    expect(source).toContain('HzMonitorFullscreenFrame');
+    expect(source).toContain('data-monitor-history-fullscreen-owner="hertzbeat-ui-fullscreen-frame"');
+    expect(source).not.toContain('WorkbenchFullscreenShell');
+    expect(source).not.toContain('className="fixed inset-0 z-50 flex items-center justify-center bg-[rgba(6,7,10,0.88)] p-4"');
+    expect(source).not.toContain('className="mb-3 flex items-center justify-between gap-3"');
+    expect(source).not.toContain('text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]');
+    expect(source).not.toContain('mt-1 text-lg font-semibold text-[var(--ops-text-primary)]');
+  });
+
+  it('renders expanded history through the shared fullscreen frame', () => {
+    const html = renderToStaticMarkup(
+      <MonitorHistoryPanel
+        payload={payload}
+        selectedSeriesKey="host=db-1"
+        selectedPointIndex={1}
+        aggregated
+        historyWindow="1h"
+        historyWindows={[
+          { value: '30m', label: '30m' },
+          { value: '1h', label: '1h' }
+        ]}
+        historyModes={[
+          { value: false, label: 'Raw' },
+          { value: true, label: 'Aggregated' }
+        ]}
+        visibleSeriesKeys={['host=db-1']}
+        expanded
+        onSelectSeries={() => {}}
+        onSelectPoint={() => {}}
+        onRefresh={() => {}}
+        onSetHistoryWindow={() => {}}
+        onSetHistoryMode={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={value => `t${value}`}
+        t={t}
+      />
+    );
+
+    expect(html).toContain('data-hz-ui="monitor-fullscreen-frame"');
+    expect(html).toContain('data-monitor-history-fullscreen-owner="hertzbeat-ui-fullscreen-frame"');
+    expect(html).toContain('Exit fullscreen');
   });
 });
diff --git a/web-next/components/monitor-detail/monitor-history-panel.tsx b/web-next/components/monitor-detail/monitor-history-panel.tsx
index 4e8e8daad5..e78d9c9d9f 100644
--- a/web-next/components/monitor-detail/monitor-history-panel.tsx
+++ b/web-next/components/monitor-detail/monitor-history-panel.tsx
@@ -1,14 +1,21 @@
 'use client';
 
 import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { EChartsPanel } from '../observability/echarts-panel';
 import {
-  ObservabilityChipToggle,
-  ObservabilityControlButton,
-  ObservabilityDetailRows,
-  ObservabilitySelectableRows,
-  ObservabilitySelectableRowsOrDetails
-} from '../observability';
+  HzActionGroup,
+  HzButton,
+  HzDataMetaText,
+  HzEChartsPanel,
+  HzInlineFeedback,
+  HzMonitorControlBand,
+  HzMonitorDetailStage,
+  HzMonitorEvidenceFrame,
+  HzMonitorFullscreenFrame,
+  HzMonitorRowNavigator,
+  HzUnderlineToggle,
+  HzDetailRows,
+  HzSelectableRows
+} from '@hertzbeat/ui';
 import { buildHistoryChartEChartsOption } from '../../lib/monitor-detail/history-chart';
 import {
   buildMonitorHistoryPointNavigation,
@@ -23,7 +30,6 @@ import {
 } from '../../lib/monitor-detail/view-model';
 import type { MonitorHistoryData } from '../../lib/types';
 import { MonitorStatGrid } from './monitor-panel-primitives';
-import { WorkbenchFullscreenShell } from '../workbench/primitives';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
 
@@ -41,13 +47,13 @@ function SelectablePointRows({
   if (rows.length === 0) return null;
 
   return (
-    <ObservabilitySelectableRows
+    <HzSelectableRows
       heading={t('monitor.detail.history-points.title')}
       rows={rows}
       selectedKey={selectedPointIndex == null ? null : String(selectedPointIndex)}
       selectionAttrName="data-selected"
       onSelect={key => onSelectPoint(Number(key))}
-      tone="operator"
+      data-monitor-history-selectable-owner="hertzbeat-ui-selectable-rows"
     />
   );
 }
@@ -67,13 +73,13 @@ function SelectableSeriesRows({
   if (visibleRows.length === 0) return null;
 
   return (
-    <ObservabilitySelectableRows
+    <HzSelectableRows
       heading={t('monitor.detail.history-series.samples.title')}
       rows={visibleRows}
       selectedKey={selectedSeriesKey}
       selectionAttrName="data-series-selected"
       onSelect={onSelectSeries}
-      tone="operator"
+      data-monitor-history-selectable-owner="hertzbeat-ui-selectable-rows"
     />
   );
 }
@@ -89,16 +95,23 @@ function SelectableCompareRows({
 }) {
   const visibleRows = rows.filter(row => row.key !== 'empty');
   if (visibleRows.length === 0) {
-    return <ObservabilityDetailRows tone="operator" rows={rows} />;
+    return (
+      <HzSelectableRows
+        rows={rows}
+        selectedKey={null}
+        selectable={false}
+        data-monitor-history-compare-owner="hertzbeat-ui-selectable-rows"
+      />
+    );
   }
 
   return (
-    <ObservabilitySelectableRowsOrDetails
+    <HzSelectableRows
       rows={visibleRows}
       selectedKey={selectedSeriesKey}
       selectionAttrName="data-compare-row-selected"
       onSelect={onSelectSeries}
-      tone="operator"
+      data-monitor-history-compare-owner="hertzbeat-ui-selectable-rows"
     />
   );
 }
@@ -214,72 +227,67 @@ function Surface({
   const latestValue = latestRaw == null || latestRaw === '' ? '-' : String(latestRaw);
 
   return (
-    <div className="space-y-3" data-monitor-surface="history-stage">
-      <div className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--ops-border-color)] pb-3">
-        <div>
-          <div className="text-[10px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{t('monitor.detail.history.stage.title')}</div>
-          <div className="mt-1 max-w-2xl text-sm text-[var(--ops-text-secondary)]">
-            {t('monitor.detail.history.stage.copy')}
-          </div>
-        </div>
-        <div className="border-l border-[var(--ops-border-color)] pl-3 text-right" data-monitor-surface-panel="series-compare">
-          <div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">
-            Selected series
-          </div>
-          <div className="mt-1 text-sm font-semibold text-[var(--ops-text-primary)]">
-            {activeSelectedSeriesKey || t('monitor.detail.history.compare.empty')}
-          </div>
-        </div>
-      </div>
-
+    <HzMonitorDetailStage
+      title={t('monitor.detail.tab.history')}
+      header="hidden"
+      rhythm="stack"
+      data-monitor-surface="history-stage"
+      data-monitor-history-stage-owner="hertzbeat-ui-detail-stage"
+      data-monitor-history-selection-reset="angular-chart-reload"
+    >
       {values.length > 0 ? (
-        <div className="space-y-3 border-y border-[var(--ops-border-color)] py-3">
+        <HzMonitorEvidenceFrame data-monitor-history-chart-frame-owner="hertzbeat-ui-evidence-frame">
           {aggregated && chartSeriesKeys.length > 1 ? (
-            <div className="space-y-2 border-y border-[var(--ops-border-color)] px-0 py-2">
-              <div className="flex flex-wrap items-center justify-between gap-2">
-                <div className="text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{t('monitor.detail.history-chart.scope.title')}</div>
-                <div className="flex flex-wrap gap-2">
-                  <ObservabilityControlButton
-                    tone="operator"
+            <HzMonitorControlBand
+              title={t('monitor.detail.history-chart.scope.title')}
+              actions={
+                <>
+                  <HzButton
+                    size="sm"
+                    intent="ghost"
+                    data-monitor-history-control-owner="hertzbeat-ui-button"
                     disabled={!canFocusPrimarySeries}
                     onClick={() => setActiveChartSeriesKeys(['mean'])}
                   >
                     {t('monitor.detail.history-chart.scope.primary-only')}
-                  </ObservabilityControlButton>
-                  <ObservabilityControlButton
-                    tone="operator"
+                  </HzButton>
+                  <HzButton
+                    size="sm"
+                    intent="ghost"
+                    data-monitor-history-control-owner="hertzbeat-ui-button"
                     disabled={!canShowAllChartStats}
                     onClick={() => setActiveChartSeriesKeys(chartSeriesKeys)}
                   >
                     {t('monitor.detail.history-chart.scope.all')}
-                  </ObservabilityControlButton>
-                </div>
-              </div>
-              <div className="flex flex-wrap gap-2">
-                {chartSeriesKeys.map(key => {
-                  const selected = activeChartSeriesKeys.includes(key);
-                  const title = key === 'origin' ? 'Value' : key.charAt(0).toUpperCase() + key.slice(1);
-                  return (
-                    <ObservabilityChipToggle
-                      key={key}
-                      tone="operator"
-                      selected={selected}
-                      selectionAttrName="data-chart-selected"
-                      onClick={() => setActiveChartSeriesKeys(currentKeys => toggleMonitorHistoryVisibleSeriesKey(currentKeys, key, chartSeriesKeys))}
-                    >
-                      <span>{title}</span>
-                    </ObservabilityChipToggle>
-                  );
-                })}
-              </div>
-            </div>
+                  </HzButton>
+                </>
+              }
+              variant="embedded"
+              data-monitor-history-chart-band-owner="hertzbeat-ui-control-band"
+            >
+              {chartSeriesKeys.map(key => {
+                const selected = activeChartSeriesKeys.includes(key);
+                const title = key === 'origin' ? 'Value' : key.charAt(0).toUpperCase() + key.slice(1);
+                return (
+                  <HzUnderlineToggle
+                    key={key}
+                    selected={selected}
+                    selectionAttrName="data-chart-selected"
+                    data-monitor-history-toggle-owner="hertzbeat-ui-underline-toggle"
+                    onClick={() => setActiveChartSeriesKeys(currentKeys => toggleMonitorHistoryVisibleSeriesKey(currentKeys, key, chartSeriesKeys))}
+                  >
+                    <span>{title}</span>
+                  </HzUnderlineToggle>
+                );
+              })}
+            </HzMonitorControlBand>
           ) : null}
-          <EChartsPanel
+          <HzEChartsPanel
             option={historyChartOption}
             height={260}
-            className="rounded-none border-x-0 border-y border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)]"
             onChartClick={index => onSelectPoint(index)}
             tone="operator"
+            data-monitor-history-chart-owner="hertzbeat-ui-echarts-panel"
           />
           <MonitorStatGrid
             items={[
@@ -288,170 +296,175 @@ function Surface({
               { label: t('monitor.detail.history.stats.range'), value: rangeValue }
             ]}
           />
-        </div>
+        </HzMonitorEvidenceFrame>
       ) : null}
-      <div className="flex flex-wrap items-center justify-between gap-2">
-        <div className="flex flex-wrap gap-2">
+      <HzActionGroup
+        density="inline"
+        layout="split"
+        data-monitor-history-action-owner="hertzbeat-ui-action-group"
+        data-monitor-history-action="history-toolbar"
+      >
+        <HzActionGroup
+          density="inline"
+          data-monitor-history-action-owner="hertzbeat-ui-action-group"
+          data-monitor-history-action="history-range-mode"
+        >
           {historyWindows.map(option => (
-            <ObservabilityChipToggle
+            <HzUnderlineToggle
               key={option.value}
-              tone="operator"
               selected={historyWindow === option.value}
+              data-monitor-history-toggle-owner="hertzbeat-ui-underline-toggle"
               onClick={() => onSetHistoryWindow(option.value)}
             >
               {option.label}
-            </ObservabilityChipToggle>
+            </HzUnderlineToggle>
           ))}
           {historyModes.map(option => (
-            <ObservabilityChipToggle
+            <HzUnderlineToggle
               key={option.label}
-              tone="operator"
               selected={aggregated === option.value}
+              data-monitor-history-toggle-owner="hertzbeat-ui-underline-toggle"
               onClick={() => onSetHistoryMode(option.value)}
             >
               {option.label}
-            </ObservabilityChipToggle>
+            </HzUnderlineToggle>
           ))}
-        </div>
-        <div className="flex flex-wrap gap-2">
+        </HzActionGroup>
+        <HzActionGroup
+          density="inline"
+          data-monitor-history-action-owner="hertzbeat-ui-action-group"
+          data-monitor-history-action="history-actions"
+        >
           {values.length > 0 ? (
-            <ObservabilityControlButton tone="operator" onClick={() => onSelectPoint(values.length - 1)}>
+            <HzButton size="sm" intent="ghost" data-monitor-history-control-owner="hertzbeat-ui-button" onClick={() => onSelectPoint(values.length - 1)}>
               {t('monitor.detail.history.latest-point')}
-            </ObservabilityControlButton>
+            </HzButton>
           ) : null}
-          <ObservabilityControlButton tone="operator" onClick={onRefresh}>{t('common.refresh')}</ObservabilityControlButton>
+          <HzButton size="sm" intent="ghost" data-monitor-history-control-owner="hertzbeat-ui-button" onClick={onRefresh}>{t('common.refresh')}</HzButton>
           {showExpandControl ? (
-            <ObservabilityControlButton tone="operator" onClick={onToggleExpanded}>
+            <HzButton size="sm" intent="ghost" data-monitor-history-control-owner="hertzbeat-ui-button" onClick={onToggleExpanded}>
               {t('monitor.detail.history.fullscreen.enter')}
-            </ObservabilityControlButton>
+            </HzButton>
           ) : null}
-        </div>
-      </div>
+        </HzActionGroup>
+      </HzActionGroup>
       {payload && !hasPersistedSeriesData ? (
-        <div className="border-y border-amber-300/16 bg-amber-300/[0.06] px-3 py-2 text-sm text-amber-50/92">
-          <div className="font-medium">{t('monitor.detail.history.blocker.title')}</div>
-          <div className="mt-1 text-amber-100/80">
-            {t('monitor.detail.history.blocker.copy')}
-          </div>
-        </div>
+        <HzInlineFeedback
+          tone="warning"
+          title={t('monitor.detail.history.blocker.title')}
+          description={t('monitor.detail.history.blocker.copy')}
+          data-monitor-history-feedback-owner="hertzbeat-ui-inline-feedback"
+        />
       ) : null}
       {seriesNavigation.total > 0 ? (
-        <div className="flex flex-wrap items-center justify-between gap-2 border-y border-[var(--ops-border-color)] px-3 py-2 text-xs text-[var(--ops-text-secondary)]">
-          <div>
-            {seriesNavigation.selectedIndex != null
+        <HzMonitorRowNavigator
+          label={
+            seriesNavigation.selectedIndex != null
               ? `${t('monitor.detail.history-series.label')} ${seriesNavigation.selectedIndex + 1} / ${seriesNavigation.total}${
                   seriesNavigation.selectedLabel ? ` · ${seriesNavigation.selectedLabel}` : ''
                 }`
-              : t('monitor.detail.history-series.empty.copy')}
-          </div>
-          <div className="flex flex-wrap gap-2">
-            <ObservabilityControlButton
-              tone="operator"
-              disabled={!seriesNavigation.canPrevious}
-              onClick={() => {
-                if (seriesNavigation.selectedIndex == null || !seriesNavigation.canPrevious) return;
-                onSelectSeries(seriesKeys[seriesNavigation.selectedIndex - 1] || selectedSeriesKey || '');
-              }}
-            >
-              {t('common.previous-series')}
-            </ObservabilityControlButton>
-            <ObservabilityControlButton
-              tone="operator"
-              disabled={!seriesNavigation.canNext}
-              onClick={() => {
-                if (seriesNavigation.selectedIndex == null || !seriesNavigation.canNext) return;
-                onSelectSeries(seriesKeys[seriesNavigation.selectedIndex + 1] || selectedSeriesKey || '');
-              }}
-            >
-              {t('common.next-series')}
-            </ObservabilityControlButton>
-          </div>
-        </div>
+              : t('monitor.detail.history-series.empty.copy')
+          }
+          previousLabel={t('common.previous-series')}
+          nextLabel={t('common.next-series')}
+          canPrevious={seriesNavigation.canPrevious}
+          canNext={seriesNavigation.canNext}
+          onPrevious={() => {
+            if (seriesNavigation.selectedIndex == null || !seriesNavigation.canPrevious) return;
+            onSelectSeries(seriesKeys[seriesNavigation.selectedIndex - 1] || selectedSeriesKey || '');
+          }}
+          onNext={() => {
+            if (seriesNavigation.selectedIndex == null || !seriesNavigation.canNext) return;
+            onSelectSeries(seriesKeys[seriesNavigation.selectedIndex + 1] || selectedSeriesKey || '');
+          }}
+          data-monitor-history-row-nav-owner="hertzbeat-ui-row-navigator"
+        />
       ) : null}
       {pointNavigation.total > 0 ? (
-        <div className="flex flex-wrap items-center justify-between gap-2 border-y border-[var(--ops-border-color)] px-3 py-2 text-xs text-[var(--ops-text-secondary)]">
-          <div>
-            {pointNavigation.selectedIndex != null
+        <HzMonitorRowNavigator
+          label={
+            pointNavigation.selectedIndex != null
               ? `${t('monitor.detail.history-point.label')} ${pointNavigation.selectedIndex + 1} / ${pointNavigation.total}${
                   pointNavigation.selectedLabel ? ` · ${pointNavigation.selectedLabel}` : ''
                 }`
-              : t('monitor.detail.history-selected.empty.copy')}
-          </div>
-          <div className="flex flex-wrap gap-2">
-            <ObservabilityControlButton
-              tone="operator"
-              disabled={!pointNavigation.canPrevious}
-              onClick={() => {
-                if (pointNavigation.selectedIndex == null || !pointNavigation.canPrevious) return;
-                onSelectPoint(pointNavigation.selectedIndex - 1);
-              }}
-            >
-              {t('common.previous-point')}
-            </ObservabilityControlButton>
-            <ObservabilityControlButton
-              tone="operator"
-              disabled={!pointNavigation.canNext}
-              onClick={() => {
-                if (pointNavigation.selectedIndex == null || !pointNavigation.canNext) return;
-                onSelectPoint(pointNavigation.selectedIndex + 1);
-              }}
-            >
-              {t('common.next-point')}
-            </ObservabilityControlButton>
-          </div>
-        </div>
+              : t('monitor.detail.history-selected.empty.copy')
+          }
+          previousLabel={t('common.previous-point')}
+          nextLabel={t('common.next-point')}
+          canPrevious={pointNavigation.canPrevious}
+          canNext={pointNavigation.canNext}
+          onPrevious={() => {
+            if (pointNavigation.selectedIndex == null || !pointNavigation.canPrevious) return;
+            onSelectPoint(pointNavigation.selectedIndex - 1);
+          }}
+          onNext={() => {
+            if (pointNavigation.selectedIndex == null || !pointNavigation.canNext) return;
+            onSelectPoint(pointNavigation.selectedIndex + 1);
+          }}
+          data-monitor-history-row-nav-owner="hertzbeat-ui-row-navigator"
+        />
       ) : null}
       <SelectableSeriesRows rows={seriesRows} selectedSeriesKey={activeSelectedSeriesKey} onSelectSeries={onSelectSeries} t={t} />
-      <ObservabilityDetailRows tone="operator" rows={summaryRows} />
+      <HzDetailRows rows={summaryRows} data-monitor-history-summary-owner="hertzbeat-ui-detail-rows" />
       <SelectablePointRows rows={pointRows.slice(0, 5)} selectedPointIndex={selectedPointIndex} onSelectPoint={onSelectPoint} t={t} />
-      <ObservabilityDetailRows tone="operator" rows={selectedPointRows} />
+      <HzDetailRows rows={selectedPointRows} data-monitor-history-selected-point-owner="hertzbeat-ui-detail-rows" />
       {availableSeriesKeys.length > 1 ? (
-        <div className="space-y-2 border-y border-[var(--ops-border-color)] px-3 py-2" data-monitor-surface-panel="series-compare">
-          <div className="flex flex-wrap items-center justify-between gap-2">
-            <div className="text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{t('monitor.detail.history-compare.scope.title')}</div>
-            <div className="flex flex-wrap gap-2">
-              <ObservabilityControlButton
-                tone="operator"
+        <HzMonitorControlBand
+          title={t('monitor.detail.history-compare.scope.title')}
+          actions={
+            <>
+              <HzButton
+                size="sm"
+                intent="ghost"
+                data-monitor-history-control-owner="hertzbeat-ui-button"
                 disabled={!activeSelectedSeriesKey}
                 onClick={() => activeSelectedSeriesKey && setActiveCompareSeriesKeys([activeSelectedSeriesKey])}
               >
                 {t('monitor.detail.history-compare.scope.selected-only')}
-              </ObservabilityControlButton>
-              <ObservabilityControlButton tone="operator" onClick={() => setActiveCompareSeriesKeys(availableSeriesKeys)}>
+              </HzButton>
+              <HzButton
+                size="sm"
+                intent="ghost"
+                data-monitor-history-control-owner="hertzbeat-ui-button"
+                onClick={() => setActiveCompareSeriesKeys(availableSeriesKeys)}
+              >
                 {t('monitor.detail.history-compare.scope.all')}
-              </ObservabilityControlButton>
-            </div>
-          </div>
-          <div className="flex flex-wrap gap-2">
-            {seriesRows
-              .filter(row => row.key !== 'empty' && availableSeriesKeys.includes(row.key))
-              .map(row => {
-                const selected = activeCompareSeriesKeys.includes(row.key);
-                const locked = activeSelectedSeriesKey === row.key;
-                return (
-                  <ObservabilityChipToggle
-                    key={row.key}
-                    tone="operator"
-                    selected={selected}
-                    disabled={locked}
-                    selectionAttrName="data-compare-selected"
-                    onClick={() =>
-                      !locked &&
-                      setActiveCompareSeriesKeys(currentKeys =>
-                        toggleMonitorHistoryVisibleSeriesKey(currentKeys, row.key, availableSeriesKeys)
-                      )
-                    }
+              </HzButton>
+            </>
+          }
+          data-monitor-history-compare-band-owner="hertzbeat-ui-control-band"
+        >
+          {seriesRows
+            .filter(row => row.key !== 'empty' && availableSeriesKeys.includes(row.key))
+            .map(row => {
+              const selected = activeCompareSeriesKeys.includes(row.key);
+              const locked = activeSelectedSeriesKey === row.key;
+              return (
+                <HzUnderlineToggle
+                  key={row.key}
+                  selected={selected}
+                  disabled={locked}
+                  selectionAttrName="data-compare-selected"
+                  data-monitor-history-toggle-owner="hertzbeat-ui-underline-toggle"
+                  onClick={() =>
+                    !locked &&
+                    setActiveCompareSeriesKeys(currentKeys => toggleMonitorHistoryVisibleSeriesKey(currentKeys, row.key, availableSeriesKeys))
+                  }
+                >
+                  <span>{row.title}</span>
+                  <HzDataMetaText
+                    className="normal-case tracking-normal"
+                    data-monitor-history-compare-meta-owner="hertzbeat-ui-data-meta-text"
                   >
-                    <span>{row.title}</span>
-                    <span className="text-[var(--ops-text-tertiary)]">{row.copy}</span>
-                  </ObservabilityChipToggle>
-                );
-              })}
-          </div>
-        </div>
+                    {row.copy}
+                  </HzDataMetaText>
+                </HzUnderlineToggle>
+              );
+            })}
+        </HzMonitorControlBand>
       ) : null}
       <SelectableCompareRows rows={compareRows} selectedSeriesKey={activeSelectedSeriesKey} onSelectSeries={onSelectSeries} />
-    </div>
+    </HzMonitorDetailStage>
   );
 }
 
@@ -536,48 +549,35 @@ export function MonitorHistoryPanel({
         t={t}
       />
       {expanded ? (
-        <div
-          className="fixed inset-0 z-50 flex items-center justify-center bg-[rgba(6,7,10,0.88)] p-4"
+        <HzMonitorFullscreenFrame
+          ref={dialogRef}
           data-expanded="true"
-          role="dialog"
-          aria-modal="true"
-          aria-label={t('monitor.detail.history.fullscreen.title')}
+          title={t('monitor.detail.history.fullscreen.title')}
+          kicker={t('monitor.detail.history.fullscreen.kicker')}
+          closeLabel={t('monitor.detail.history.fullscreen.exit')}
+          onClose={onToggleExpanded}
+          data-monitor-history-fullscreen-owner="hertzbeat-ui-fullscreen-frame"
         >
-          <WorkbenchFullscreenShell
-            ref={dialogRef}
-            tabIndex={-1}
-            className="hb-scrollbar max-h-[92vh] w-full max-w-6xl overflow-auto"
-          >
-            <div className="mb-3 flex items-center justify-between gap-3">
-              <div>
-                <div className="text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{t('monitor.detail.history.fullscreen.kicker')}</div>
-                <div className="mt-1 text-lg font-semibold text-[var(--ops-text-primary)]">{t('monitor.detail.history.fullscreen.title')}</div>
-              </div>
-              <ObservabilityControlButton tone="operator" onClick={onToggleExpanded}>
-                {t('monitor.detail.history.fullscreen.exit')}
-              </ObservabilityControlButton>
-            </div>
-            <Surface
-              payload={payload}
-              selectedSeriesKey={selectedSeriesKey}
-              selectedPointIndex={selectedPointIndex}
-              aggregated={aggregated}
-              historyWindow={historyWindow}
-              historyWindows={historyWindows}
-              historyModes={historyModes}
-              visibleSeriesKeys={visibleSeriesKeys}
-              showExpandControl={false}
-              onSelectSeries={onSelectSeries}
-              onSelectPoint={onSelectPoint}
-              onRefresh={onRefresh}
-              onSetHistoryWindow={onSetHistoryWindow}
-              onSetHistoryMode={onSetHistoryMode}
-              onToggleExpanded={onToggleExpanded}
-              formatTime={formatTime}
-              t={t}
-            />
-          </WorkbenchFullscreenShell>
-        </div>
+          <Surface
+            payload={payload}
+            selectedSeriesKey={selectedSeriesKey}
+            selectedPointIndex={selectedPointIndex}
+            aggregated={aggregated}
+            historyWindow={historyWindow}
+            historyWindows={historyWindows}
+            historyModes={historyModes}
+            visibleSeriesKeys={visibleSeriesKeys}
+            showExpandControl={false}
+            onSelectSeries={onSelectSeries}
+            onSelectPoint={onSelectPoint}
+            onRefresh={onRefresh}
+            onSetHistoryWindow={onSetHistoryWindow}
+            onSetHistoryMode={onSetHistoryMode}
+            onToggleExpanded={onToggleExpanded}
+            formatTime={formatTime}
+            t={t}
+          />
+        </HzMonitorFullscreenFrame>
       ) : null}
     </>
   );
diff --git a/web-next/components/monitor-detail/monitor-metric-table.test.tsx b/web-next/components/monitor-detail/monitor-metric-table.test.tsx
index 2ee52162e3..74d59066a8 100644
--- a/web-next/components/monitor-detail/monitor-metric-table.test.tsx
+++ b/web-next/components/monitor-detail/monitor-metric-table.test.tsx
@@ -27,7 +27,14 @@ describe('monitor metric table', () => {
     expect(html).toContain('%');
     expect(html).toContain('NULL');
     expect(html).toContain('host=db-2');
-    expect(html).toContain('data-selected="true"');
+    expect(html).toContain('data-monitor-metric-table-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-monitor-metric-unit-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-hz-data-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).toContain('data-hz-row-selected="true"');
+    expect(html).not.toContain('data-selected="true"');
+    expect(html).not.toContain('var(--ops-border-color)');
+    expect(html).not.toContain('var(--ops-surface-panel)');
   });
 
   it('renders focused-row detail mode', () => {
@@ -53,5 +60,23 @@ describe('monitor metric table', () => {
     expect(html).toContain('64');
     expect(html).toContain('idle');
     expect(html).toContain('36');
+    expect(html).toContain('data-monitor-metric-detail-owner="hertzbeat-ui-data-table"');
+    expect(html).toContain('data-monitor-metric-row-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-hz-data-meta-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-hz-ui="data-table"');
+    expect(html).not.toContain('ObservabilityDetailRows');
+    expect(html).not.toContain('var(--ops-border-color)');
+  });
+
+  it('does not keep monitor metric table meta text as page-local utility classes', () => {
+    const source = require('node:fs').readFileSync(require('node:path').resolve(__dirname, 'monitor-metric-table.tsx'), 'utf8');
+
+    expect(source).toContain('HzDataMetaText');
+    expect(source).toContain('spacing="inline"');
+    expect(source).toContain('spacing="compact"');
+    expect(source).not.toContain('ml-2 text-[10px] uppercase tracking-[0.12em] text-[#727b8c]');
+    expect(source).not.toContain('ml-1 text-[10px] tracking-normal text-[#727b8c]');
+    expect(source).not.toContain('className="ml-2"');
+    expect(source).not.toContain('className="ml-1"');
   });
 });
diff --git a/web-next/components/monitor-detail/monitor-metric-table.tsx b/web-next/components/monitor-detail/monitor-metric-table.tsx
index f8bcce02f9..65af494547 100644
--- a/web-next/components/monitor-detail/monitor-metric-table.tsx
+++ b/web-next/components/monitor-detail/monitor-metric-table.tsx
@@ -1,12 +1,18 @@
 'use client';
 
 import React from 'react';
-import type { ColumnDef } from '@tanstack/react-table';
-import { DataTable, ObservabilityDetailRows } from '../observability';
+import { HzDataMetaText, HzDataTable } from '@hertzbeat/ui';
 import { buildMonitorMetricSelectedRowTableRows, buildMonitorMetricTableMatrix, type MonitorMetricTableMatrix } from '../../lib/monitor-detail/view-model';
 import type { MonitorRealtimeMetricData } from '../../lib/types';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
+type MetricMatrixRow = MonitorMetricTableMatrix['rows'][number];
+type MetricDetailRow = {
+  key: string;
+  title: string;
+  copy: string;
+  meta: string;
+};
 
 export function MonitorMetricTable({
   payload,
@@ -30,55 +36,86 @@ export function MonitorMetricTable({
     const rowIndex = selectedRowKey == null ? null : Number(selectedRowKey);
     const selectedRow = rowIndex == null ? null : resolvedMatrix.rows.find(row => row.key === selectedRowKey) ?? null;
     const detailRows = buildMonitorMetricSelectedRowTableRows(payload, rowIndex, t);
+    const rows: MetricDetailRow[] = [
+      {
+        key: 'selected-row',
+        title: t('monitor.detail.metric.row-label'),
+        copy: selectedRow?.label || t('monitor.detail.metric.table.empty.title'),
+        meta: ''
+      },
+      ...detailRows.map((row, index) => ({
+        key: `${row.title}-${index}`,
+        title: row.title,
+        copy: row.copy,
+        meta: row.meta
+      }))
+    ];
 
     return (
-      <div className="space-y-3 border-y border-[var(--ops-border-color)] py-3">
-        <ObservabilityDetailRows
-          tone="operator"
-          rows={[
+      <div className="min-w-0" data-monitor-metric-detail-owner="hertzbeat-ui-data-table">
+        <HzDataTable
+          columns={[
             {
-              title: t('monitor.detail.metric.row-label'),
-              copy: selectedRow?.label || t('monitor.detail.metric.table.empty.title')
+              key: 'title',
+              header: t('monitor.detail.metric.row-label'),
+              width: '180px',
+              render: row => <span className="font-semibold">{row.title}</span>
             },
-            ...detailRows
+            {
+              key: 'copy',
+              header: t('monitor.detail.metric.payload.meta'),
+              render: row => (
+                <span>
+                  <span>{row.copy}</span>
+                  {row.meta ? (
+                    <HzDataMetaText spacing="inline" data-monitor-metric-row-meta-owner="hertzbeat-ui-data-meta-text">
+                      {row.meta}
+                    </HzDataMetaText>
+                  ) : null}
+                </span>
+              )
+            }
           ]}
+          rows={rows}
+          getRowKey={row => row.key}
+          emptyLabel={t('monitor.detail.metric.table.empty.copy')}
         />
       </div>
     );
   }
 
-  const columns: ColumnDef<(typeof resolvedMatrix.rows)[number], unknown>[] = [
+  const columns = [
     {
-      id: 'label',
+      key: 'label',
       header: t('monitor.detail.metric.row-label'),
-      cell: ({ row }) => <span className="font-medium text-[var(--ops-text-primary)]">{row.original.label}</span>
+      render: (row: MetricMatrixRow) => <span className="font-medium">{row.label}</span>
     },
     ...resolvedMatrix.columns.map((column, index) => ({
-      id: column.key,
-      header: () => (
-        <div>
-          <div>{column.title}</div>
-          {column.unit ? <div className="mt-1 text-[10px] tracking-normal text-[var(--ops-text-tertiary)]">{column.unit}</div> : null}
-        </div>
+      key: column.key,
+      header: (
+        <span>
+          <span>{column.title}</span>
+          {column.unit ? (
+            <HzDataMetaText variant="unit" spacing="compact" data-monitor-metric-unit-owner="hertzbeat-ui-data-meta-text">
+              {column.unit}
+            </HzDataMetaText>
+          ) : null}
+        </span>
       ),
-      cell: ({ row }: { row: { original: (typeof resolvedMatrix.rows)[number] } }) => (
-        <span className="text-[var(--ops-text-secondary)]">{row.original.values[index]}</span>
-      )
+      render: (row: MetricMatrixRow) => <span>{row.values[index]}</span>
     }))
   ];
 
   return (
-    <DataTable
-      columns={columns}
-      data={resolvedMatrix.rows}
-      emptyState={t('monitor.detail.metric.table.empty.copy')}
-      tableOptions={{ getRowId: row => row.key }}
-      rowClassName={row => (row.original.key === selectedRowKey ? 'bg-[var(--ops-surface-raised)] shadow-[inset_2px_0_0_var(--ops-primary)]' : undefined)}
-      rowAttributes={row => ({
-        'data-selected': row.original.key === selectedRowKey ? 'true' : 'false'
-      })}
-      onRowClick={row => onSelect(row.original.key)}
-      className="rounded-none border-x-0 border-y border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)]"
-    />
+    <div className="min-w-0" data-monitor-metric-table-owner="hertzbeat-ui-data-table">
+      <HzDataTable
+        columns={columns}
+        rows={resolvedMatrix.rows}
+        emptyLabel={t('monitor.detail.metric.table.empty.copy')}
+        getRowKey={row => row.key}
+        selectedRowKey={selectedRowKey ?? undefined}
+        onRowClick={row => onSelect(row.key)}
+      />
+    </div>
   );
 }
diff --git a/web-next/components/monitor-detail/monitor-panel-primitives.test.tsx b/web-next/components/monitor-detail/monitor-panel-primitives.test.tsx
index 05aec9881b..746bc6d6b6 100644
--- a/web-next/components/monitor-detail/monitor-panel-primitives.test.tsx
+++ b/web-next/components/monitor-detail/monitor-panel-primitives.test.tsx
@@ -1,6 +1,8 @@
 import React from 'react';
 import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
 import { MonitorSignalBars, MonitorStatGrid } from './monitor-panel-primitives';
 
 describe('monitor panel primitives', () => {
@@ -21,6 +23,8 @@ describe('monitor panel primitives', () => {
     expect(html).toContain('+10');
     expect(html).toContain('Range');
     expect(html).toContain('10 - 29');
+    expect(html).toContain('data-hz-ui="monitor-stat-grid"');
+    expect(html).toContain('data-monitor-stat-grid-owner="hertzbeat-ui-monitor-stat-grid"');
   });
 
   it('renders reusable signal bars', () => {
@@ -39,5 +43,17 @@ describe('monitor panel primitives', () => {
     expect(html).toContain('28 %');
     expect(html).toContain('width:72%');
     expect(html).toContain('width:28%');
+    expect(html).toContain('data-hz-ui="monitor-signal-bars"');
+    expect(html).toContain('data-monitor-signal-bars-owner="hertzbeat-ui-monitor-signal-bars"');
+  });
+
+  it('does not bridge monitor panel primitives through legacy observability chrome', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-panel-primitives.tsx'), 'utf8');
+
+    expect(source).toContain('@hertzbeat/ui');
+    expect(source).not.toContain('../observability/stat-grid');
+    expect(source).not.toContain('../observability/signal-bars');
+    expect(source).not.toContain('ObservabilityStatGrid');
+    expect(source).not.toContain('ObservabilitySignalBars');
   });
 });
diff --git a/web-next/components/monitor-detail/monitor-panel-primitives.tsx b/web-next/components/monitor-detail/monitor-panel-primitives.tsx
index cb93c54049..47a2b8c042 100644
--- a/web-next/components/monitor-detail/monitor-panel-primitives.tsx
+++ b/web-next/components/monitor-detail/monitor-panel-primitives.tsx
@@ -1,18 +1,18 @@
 'use client';
 
 import React from 'react';
-import { ObservabilityStatGrid, type ObservabilityStatGridItem } from '../observability/stat-grid';
+import { HzMonitorSignalBars, HzMonitorStatGrid, type HzMonitorSignalBarItem, type HzMonitorStatGridItem } from '@hertzbeat/ui';
 
-export type { ObservabilitySignalBarItem as MonitorSignalBarItem } from '../observability/signal-bars';
-export type MonitorStatItem = ObservabilityStatGridItem;
-export { ObservabilitySignalBars as MonitorSignalBars } from '../observability/signal-bars';
+export type MonitorSignalBarItem = HzMonitorSignalBarItem;
+export type MonitorStatItem = HzMonitorStatGridItem;
+export const MonitorSignalBars = HzMonitorSignalBars;
 
 export function MonitorStatGrid({
   items,
   columns = 3
 }: {
-  items: ObservabilityStatGridItem[];
+  items: HzMonitorStatGridItem[];
   columns?: 2 | 3 | 4;
 }) {
-  return <ObservabilityStatGrid items={items} columns={columns} tone="operator" />;
+  return <HzMonitorStatGrid items={items} columns={columns} />;
 }
diff --git a/web-next/components/monitor-detail/monitor-realtime-panel.test.tsx b/web-next/components/monitor-detail/monitor-realtime-panel.test.tsx
index 613b4dd1b7..ff1c8fe631 100644
--- a/web-next/components/monitor-detail/monitor-realtime-panel.test.tsx
+++ b/web-next/components/monitor-detail/monitor-realtime-panel.test.tsx
@@ -53,11 +53,17 @@ describe('monitor realtime panel', () => {
     expect(html).toContain('28');
     expect(html).toContain('Previous');
     expect(html).toContain('Next');
-    expect(html).toContain('Selected row');
     expect(html).not.toContain('Live sample matrix');
     expect(html).not.toContain('Row inspector');
+    expect(html).not.toContain('Realtime stage');
+    expect(html).not.toContain('Inspect the latest metric rows and row details.');
     expect(html).toMatch(/data-monitor-surface=\"realtime-stage\"/);
-    expect(html).toMatch(/data-monitor-surface-panel=\"row-inspector\"/);
+    expect(html).toContain('data-monitor-realtime-selection-reset="angular-table-reload"');
+    expect(html).toContain('data-monitor-realtime-toolbar-owner="hertzbeat-ui-realtime-toolbar"');
+    expect(html).toContain('data-monitor-realtime-inspector-owner="hertzbeat-ui-realtime-inspector"');
+    expect(html).toContain('data-monitor-realtime-inspector-variant="details"');
+    expect(html).not.toContain('data-monitor-realtime-inspector-variant="summary"');
+    expect(html).not.toMatch(/data-monitor-surface-panel=\"row-inspector\"/);
   });
 
   it('renders a compact inline surface without expanding row search and inspector details', () => {
@@ -79,17 +85,19 @@ describe('monitor realtime panel', () => {
 
     expect(html).toMatch(/data-monitor-surface=\"realtime-stage\"/);
     expect(html).toMatch(/data-monitor-surface-compact=\"true\"/);
-    expect(html).toContain('data-monitor-surface-compact-layout="cardless-table"');
-    expect(html).toContain('data-monitor-realtime-wrapper="monitor-data-table"');
-    expect(html).toContain('data-monitor-realtime-action-band="monitor-data-table"');
-    expect(html).toContain('data-monitor-realtime-action-group="metrics-card-extra"');
-    expect(html).toContain('data-monitor-realtime-action-density="angular-plain-meta"');
-    expect(html).toContain('data-monitor-realtime-expand-action-density="plain-link"');
-    expect(html).toContain('data-observability-control-button-variant="plain"');
+    expect(html).toContain('data-monitor-surface-owner="hertzbeat-ui-detail-stage"');
+    expect(html).toContain('data-monitor-surface-compact-layout="flat-inline"');
+    expect(html).toContain('data-monitor-realtime-wrapper="hertzbeat-ui-detail-stage"');
+    expect(html).toContain('data-monitor-realtime-toolbar-owner="hertzbeat-ui-realtime-toolbar"');
+    expect(html).toContain('data-monitor-realtime-action-band="shared-monitor-data-table"');
+    expect(html).toContain('data-monitor-realtime-action-group="shared-metric-extra"');
+    expect(html).toContain('data-monitor-realtime-action-density="hertzbeat-ui-compact-actions"');
+    expect(html).toContain('data-monitor-realtime-expand-action-density="hertzbeat-ui-link-action"');
     expect(html).toContain('data-monitor-realtime-collect-time="true"');
-    expect(html).toMatch(/class=\"[^\"]*monitor-workbench-surface[^\"]*monitor-workbench-surface--plain[^\"]*monitor-realtime-data-table/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-realtime-data-table[^\"]*space-y-1[^\"]*border-t[^\"]*pt-1/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-workbench-surface__header/);
+    expect(html).toContain('data-hz-ui="monitor-detail-stage"');
+    expect(html).not.toMatch(/class=\"[^\"]*monitor-workbench-surface[^\"]*monitor-workbench-surface--plain[^\"]*monitor-realtime-data-table/);
+    expect(html).not.toMatch(/class=\"[^\"]*monitor-realtime-data-table[^\"]*space-y-1[^\"]*border-t[^\"]*pt-1/);
+    expect(html).not.toMatch(/class=\"[^\"]*monitor-workbench-surface__header/);
     expect(html).toContain('Collect time');
     expect(html).toContain('Fullscreen');
     expect(html).not.toMatch(/>Refresh<\/button>/);
@@ -102,6 +110,7 @@ describe('monitor realtime panel', () => {
     expect(html).not.toContain('Active row');
     expect(html).not.toContain('host=db-1');
     expect(html).not.toMatch(/data-monitor-surface-panel=\"row-inspector\"/);
+    expect(html).not.toContain('data-monitor-realtime-inspector-owner="hertzbeat-ui-realtime-inspector"');
   });
 
   it('disables row navigation at the current boundaries', () => {
@@ -137,6 +146,9 @@ describe('monitor realtime panel', () => {
 
     expect(firstRowHtml).toMatch(/<button[^>]*disabled=""[^>]*>Previous<\/button>/);
     expect(firstRowHtml).not.toMatch(/<button[^>]*disabled=""[^>]*>Next<\/button>/);
+    expect(firstRowHtml).toContain('data-monitor-realtime-row-nav-owner="hertzbeat-ui-row-navigator"');
+    expect(firstRowHtml).toContain('data-monitor-realtime-row-nav-action="previous"');
+    expect(firstRowHtml).toContain('data-monitor-realtime-row-nav-action="next"');
     expect(lastRowHtml).not.toMatch(/<button[^>]*disabled=""[^>]*>Previous<\/button>/);
     expect(lastRowHtml).toMatch(/<button[^>]*disabled=""[^>]*>Next<\/button>/);
   });
@@ -162,33 +174,181 @@ describe('monitor realtime panel', () => {
     expect(html).toContain('role="dialog"');
     expect(html).toContain('aria-modal="true"');
     expect(html).toContain('Exit fullscreen');
-    expect(html).toContain('Selected row');
     expect(html).toContain('host=db-2');
     expect(html).toContain('Row 2 / 2');
     expect(modalHtml).not.toMatch(/>Fullscreen<\/button>/);
   });
 
-  it('routes the fullscreen shell through the shared workbench fullscreen owner', () => {
+  it('routes the fullscreen shell through the shared monitor fullscreen frame owner', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
+
+    expect(source).toContain('HzMonitorFullscreenFrame');
+    expect(source).toContain('data-monitor-realtime-fullscreen-owner="hertzbeat-ui-fullscreen-frame"');
+    expect(source).not.toContain('WorkbenchFullscreenShell');
+    expect(source).not.toContain('className="fixed inset-0 z-50 flex items-center justify-center bg-[rgba(6,7,10,0.88)] p-4"');
+    expect(source).not.toContain('className="mb-3 flex items-center justify-between gap-3"');
+    expect(source).not.toContain('text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]');
+    expect(source).not.toContain('mt-1 text-lg font-semibold text-[var(--ops-text-primary)]');
+  });
+
+  it('keeps the realtime row inspector owned by the shared UI primitive', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
+
+    expect(source).toContain('HzMonitorRealtimeInspector');
+    expect(source).toContain('data-monitor-realtime-inspector-owner="hertzbeat-ui-realtime-inspector"');
+    expect(source).not.toContain('data-monitor-surface-panel="row-inspector"');
+    expect(source).not.toContain('border-l border-[var(--ops-border-color)] pl-3 text-right');
+    expect(source).not.toContain('MonitorStatGrid');
+    expect(source).not.toContain('ObservabilityDetailRows');
+  });
+
+  it('keeps realtime row navigation owned by the shared UI primitive', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
 
-    expect(source).toContain('WorkbenchFullscreenShell');
-    expect(source).not.toContain('hb-scrollbar max-h-[92vh] w-full max-w-6xl overflow-auto rounded-[6px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] p-4 shadow-[0_24px_80px_rgba(0,0,0,0.45)]');
+    expect(source).toContain('HzMonitorRealtimeRowNavigator');
+    expect(source).toContain('data-monitor-realtime-row-nav-owner="hertzbeat-ui-row-navigator"');
+    expect(source).not.toContain('border-y border-[var(--ops-border-color)] px-3 py-2 text-xs text-[var(--ops-text-secondary)]');
+    expect(source).not.toContain('disabled={!rowNavigation.canPrevious}');
+    expect(source).not.toContain('disabled={!rowNavigation.canNext}');
+  });
+
+  it('routes realtime empty row feedback through the shared inline feedback primitive', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
+    const emptyPayload = { ...payload, valueRows: [] } as any;
+    const html = renderToStaticMarkup(
+      <MonitorRealtimePanel
+        payload={emptyPayload}
+        selectedRowKey={null}
+        mode="table"
+        expanded={false}
+        onSelect={() => {}}
+        onModeChange={() => {}}
+        onRefresh={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={() => '2026-04-11 10:00:00'}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('HzInlineFeedback');
+    expect(source).toContain('data-monitor-realtime-empty-owner="hertzbeat-ui-inline-feedback"');
+    expect(source).not.toContain('className="border-y border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-3 py-2 text-sm text-[var(--ops-text-secondary)]"');
+    expect(html).toContain('data-hz-ui="inline-feedback"');
+    expect(html).toContain('data-monitor-realtime-empty-owner="hertzbeat-ui-inline-feedback"');
+    expect(html).toContain('No rows matched the search.');
+  });
+
+  it('does not wrap the shared realtime metric table in page-local border chrome', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorRealtimePanel
+        payload={payload}
+        selectedRowKey="0"
+        mode="table"
+        expanded={false}
+        onSelect={() => {}}
+        onModeChange={() => {}}
+        onRefresh={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={() => '2026-04-11 10:00:00'}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('MonitorMetricTable');
+    expect(source).not.toContain('className="border-y border-[var(--ops-border-color)] py-2"');
+    expect(html).toContain('data-monitor-metric-table-owner="hertzbeat-ui-data-table"');
+  });
+
+  it('does not render the old page-local realtime stage header chrome', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorRealtimePanel
+        payload={payload}
+        selectedRowKey="0"
+        mode="table"
+        expanded={false}
+        onSelect={() => {}}
+        onModeChange={() => {}}
+        onRefresh={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={() => '2026-04-11 10:00:00'}
+        t={t}
+      />
+    );
+
+    expect(source).not.toContain('border-b border-[var(--ops-border-color)] pb-3');
+    expect(source).not.toContain("t('monitor.detail.metric.stage.title')");
+    expect(source).not.toContain("t('monitor.detail.metric.stage.copy')");
+    expect(html).not.toContain('Realtime stage');
+    expect(html).not.toContain('Inspect the latest metric rows and row details.');
+    expect(html).not.toContain('data-monitor-realtime-inspector-variant="summary"');
   });
 
   it('keeps compact realtime actions on the monitor-data-table action band', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
 
-    expect(source).toContain('monitor-workbench-surface monitor-workbench-surface--plain monitor-realtime-data-table');
-    expect(source).toContain('data-monitor-realtime-wrapper={compact ? \'monitor-data-table\' : undefined}');
-    expect(source).toContain('data-monitor-realtime-action-band={compact ? \'monitor-data-table\' : undefined}');
-    expect(source).toContain('data-monitor-realtime-action-group={compact ? \'metrics-card-extra\' : undefined}');
-    expect(source).toContain('data-monitor-realtime-action-density={compact ? \'angular-plain-meta\' : undefined}');
-    expect(source).toContain('data-monitor-realtime-expand-action-density={compact ? \'plain-link\' : undefined}');
-    expect(source).toContain('data-monitor-realtime-collect-time={compact ? \'true\' : undefined}');
-    expect(source).toContain("variant={compact ? 'plain' : 'default'}");
-    expect(source).toContain("className={compact ? 'h-6 px-0 py-0 text-[var(--ops-text-tertiary)]' : undefined}");
-    expect(source).toContain('monitor-realtime-data-table space-y-1 border-t border-[var(--ops-border-color)] pt-1');
+    expect(source).toContain('HzMonitorDetailStage');
+    expect(source).toContain('data-monitor-surface-owner="hertzbeat-ui-detail-stage"');
+    expect(source).toContain('data-monitor-realtime-wrapper={compact ? \'hertzbeat-ui-detail-stage\' : undefined}');
+    expect(source).toContain('HzMonitorRealtimeToolbar');
+    expect(source).toContain('data-monitor-realtime-toolbar-owner="hertzbeat-ui-realtime-toolbar"');
+    expect(source).not.toContain('monitor-workbench-surface monitor-workbench-surface--plain monitor-realtime-data-table');
+    expect(source).not.toContain('data-monitor-realtime-action-band={compact ? \'monitor-data-table\' : undefined}');
+    expect(source).not.toContain('data-monitor-realtime-action-group={compact ? \'metrics-card-extra\' : undefined}');
+    expect(source).not.toContain('data-monitor-realtime-action-density={compact ? \'angular-plain-meta\' : undefined}');
+    expect(source).not.toContain('data-monitor-realtime-expand-action-density={compact ? \'plain-link\' : undefined}');
+    expect(source).not.toContain("variant={compact ? 'plain' : 'default'}");
+    expect(source).not.toContain("className={compact ? 'h-6 px-0 py-0 text-[var(--ops-text-tertiary)]' : undefined}");
+    expect(source).not.toContain('monitor-realtime-data-table space-y-1 border-t border-[var(--ops-border-color)] pt-1');
     expect(source).not.toContain('monitor-realtime-data-table space-y-2 border-t border-[var(--ops-border-color)] pt-2');
-    expect(source).toContain('!compact ? (');
+  });
+
+  it('keeps realtime search and fullscreen close controls owned by shared HertzBeat UI primitives', () => {
+    const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-realtime-panel.tsx'), 'utf8');
+    const html = renderToStaticMarkup(
+      <MonitorRealtimePanel
+        payload={payload}
+        selectedRowKey="0"
+        mode="table"
+        expanded
+        onSelect={() => {}}
+        onModeChange={() => {}}
+        onRefresh={() => {}}
+        onToggleExpanded={() => {}}
+        formatTime={() => '2026-04-11 10:00:00'}
+        t={t}
+      />
+    );
+
+    expect(source).toContain('HzInput');
+    expect(source).toContain('HzDataMetaText');
+    expect(source).toContain('HzControlStack');
+    expect(source).toContain('HzButton');
+    expect(source).toContain('HzMonitorFullscreenFrame');
+    expect(source).toContain('data-monitor-realtime-search-stack-owner="hertzbeat-ui-control-stack"');
+    expect(source).toContain('data-monitor-realtime-search-owner="hertzbeat-ui-input"');
+    expect(source).toContain('data-monitor-realtime-search-count-owner="hertzbeat-ui-data-meta-text"');
+    expect(source).toContain('display="block"');
+    expect(source).toContain('casing="plain"');
+    expect(source).toContain('data-monitor-realtime-fullscreen-owner="hertzbeat-ui-fullscreen-frame"');
+    expect(source).toContain("'data-monitor-realtime-fullscreen-close-owner': 'hertzbeat-ui-button'");
+    expect(source).not.toContain('className="block normal-case tracking-normal"');
+    expect(source).not.toContain('text-xs text-[var(--ops-text-tertiary)]');
+    expect(source).not.toContain('className="space-y-2"');
+    expect(source).not.toContain('ObservabilitySearchInput');
+    expect(source).not.toContain('ObservabilityControlButton');
+    expect(html).toContain('data-hz-ui="input"');
+    expect(html).toContain('data-hz-ui="control-stack"');
+    expect(html).toContain('data-hz-ui="data-meta-text"');
+    expect(html).toContain('data-hz-ui="button"');
+    expect(html).toContain('data-hz-ui="monitor-fullscreen-frame"');
+    expect(html).toContain('data-monitor-realtime-search-owner="hertzbeat-ui-input"');
+    expect(html).toContain('data-monitor-realtime-search-stack-owner="hertzbeat-ui-control-stack"');
+    expect(html).toContain('data-monitor-realtime-search-count-owner="hertzbeat-ui-data-meta-text"');
+    expect(html).toContain('data-monitor-realtime-fullscreen-owner="hertzbeat-ui-fullscreen-frame"');
+    expect(html).toContain('data-monitor-realtime-fullscreen-close-owner="hertzbeat-ui-button"');
+    expect(html).toContain('data-hz-control-height="32"');
+    expect(html).toContain('data-hz-control-edge="lined"');
   });
 });
diff --git a/web-next/components/monitor-detail/monitor-realtime-panel.tsx b/web-next/components/monitor-detail/monitor-realtime-panel.tsx
index 12ac9568b4..d3c48eb352 100644
--- a/web-next/components/monitor-detail/monitor-realtime-panel.tsx
+++ b/web-next/components/monitor-detail/monitor-realtime-panel.tsx
@@ -1,7 +1,18 @@
 'use client';
 
 import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { ObservabilityControlButton, ObservabilityDetailRows, ObservabilitySearchInput } from '../observability';
+import {
+  HzButton,
+  HzControlStack,
+  HzDataMetaText,
+  HzInlineFeedback,
+  HzInput,
+  HzMonitorDetailStage,
+  HzMonitorFullscreenFrame,
+  HzMonitorRealtimeInspector,
+  HzMonitorRealtimeRowNavigator,
+  HzMonitorRealtimeToolbar
+} from '@hertzbeat/ui';
 import {
   buildMonitorMetricRealtimeFactRows,
   buildMonitorMetricSelectedRowRows,
@@ -10,8 +21,6 @@ import {
 } from '../../lib/monitor-detail/view-model';
 import type { MonitorRealtimeMetricData } from '../../lib/types';
 import { MonitorMetricTable } from './monitor-metric-table';
-import { MonitorStatGrid } from './monitor-panel-primitives';
-import { WorkbenchFullscreenShell } from '../workbench/primitives';
 
 type Translator = (key: string, params?: Record<string, string | number | null | undefined>) => string;
 
@@ -75,166 +84,116 @@ function Surface({
     canPrevious: selectedVisibleIndex != null && selectedVisibleIndex > 0,
     canNext: selectedVisibleIndex != null && selectedVisibleIndex < filteredMatrix.rows.length - 1
   };
-  const surfaceClassName = compact
-    ? 'monitor-workbench-surface monitor-workbench-surface--plain monitor-realtime-data-table space-y-1 border-t border-[var(--ops-border-color)] pt-1'
-    : 'space-y-3';
-  const toolbarClassName = compact
-    ? 'monitor-workbench-surface__header flex flex-wrap items-center justify-between gap-2 border-b border-[var(--ops-border-color)] pb-2'
-    : 'flex flex-wrap items-center justify-between gap-2';
-
   return (
-    <div
-      className={surfaceClassName}
+    <HzMonitorDetailStage
+      title={t('monitor.detail.tab.realtime')}
+      header="hidden"
+      rhythm="tight"
       data-monitor-surface="realtime-stage"
+      data-monitor-surface-owner="hertzbeat-ui-detail-stage"
       data-monitor-surface-compact={compact ? 'true' : undefined}
-      data-monitor-surface-compact-layout={compact ? 'cardless-table' : undefined}
-      data-monitor-realtime-wrapper={compact ? 'monitor-data-table' : undefined}
+      data-monitor-surface-compact-layout={compact ? 'flat-inline' : undefined}
+      data-monitor-realtime-wrapper={compact ? 'hertzbeat-ui-detail-stage' : undefined}
+      data-monitor-realtime-selection-reset="angular-table-reload"
     >
-      {compact ? null : (
-        <div className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--ops-border-color)] pb-3">
-          <div>
-            <div className="text-[10px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{t('monitor.detail.metric.stage.title')}</div>
-            <div className="mt-1 max-w-2xl text-sm text-[var(--ops-text-secondary)]">
-              {t('monitor.detail.metric.stage.copy')}
-            </div>
-          </div>
-          <div
-            className="border-l border-[var(--ops-border-color)] pl-3 text-right"
-            data-monitor-surface-panel="row-inspector"
-          >
-            <div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{t('monitor.detail.metric.selection.title')}</div>
-            <div className="mt-1 text-sm font-semibold text-[var(--ops-text-primary)]">
-              {selectedRow ? selectedRow.label : t('monitor.detail.metric.inspector.empty')}
-            </div>
-          </div>
-        </div>
-      )}
-
-      <div
-        className={toolbarClassName}
+      <HzMonitorRealtimeToolbar
+        facts={visibleFactRows}
+        compact={compact}
+        selectedMode={mode}
+        modeOptions={
+          compact
+            ? []
+            : [
+                { value: 'table', label: t('monitor.detail.metric.mode.table') },
+                { value: 'detail', label: t('monitor.detail.metric.mode.detail') }
+              ]
+        }
+        onModeChange={value => onModeChange(value === 'detail' ? 'detail' : 'table')}
+        refreshLabel={compact ? undefined : t('common.refresh')}
+        onRefresh={onRefresh}
+        expandLabel={showExpandControl ? t('monitor.detail.metric.fullscreen.enter') : undefined}
+        onExpand={onToggleExpanded}
+        showExpand={showExpandControl}
         data-monitor-surface-compact-toolbar={compact ? 'true' : undefined}
-        data-monitor-realtime-action-band={compact ? 'monitor-data-table' : undefined}
-      >
-        <div className="flex flex-wrap gap-2" data-monitor-realtime-collect-time={compact ? 'true' : undefined}>
-          {visibleFactRows.map(row => (
-            <div key={row.title} className="border-b border-[var(--ops-border-color)] px-0 py-1.5 text-xs text-[var(--ops-text-secondary)]">
-              <span className="text-[var(--ops-text-tertiary)]">{row.title}</span>
-              {' · '}
-              <span className="text-[var(--ops-text-primary)]">{row.copy}</span>
-            </div>
-          ))}
-        </div>
-        <div
-          className="flex flex-wrap gap-2"
-          data-monitor-realtime-action-group={compact ? 'metrics-card-extra' : undefined}
-          data-monitor-realtime-action-density={compact ? 'angular-plain-meta' : undefined}
-        >
-          {compact ? null : (
-            <>
-              <ObservabilityControlButton tone="operator" selected={mode === 'table'} onClick={() => onModeChange('table')}>
-                {t('monitor.detail.metric.mode.table')}
-              </ObservabilityControlButton>
-              <ObservabilityControlButton tone="operator" selected={mode === 'detail'} onClick={() => onModeChange('detail')}>
-                {t('monitor.detail.metric.mode.detail')}
-              </ObservabilityControlButton>
-            </>
-          )}
-          {!compact ? (
-            <ObservabilityControlButton tone="operator" onClick={onRefresh}>{t('common.refresh')}</ObservabilityControlButton>
-          ) : null}
-          {showExpandControl ? (
-            <ObservabilityControlButton
-              tone="operator"
-              variant={compact ? 'plain' : 'default'}
-              className={compact ? 'h-6 px-0 py-0 text-[var(--ops-text-tertiary)]' : undefined}
-              data-monitor-realtime-expand-action-density={compact ? 'plain-link' : undefined}
-              onClick={onToggleExpanded}
-            >
-              {t('monitor.detail.metric.fullscreen.enter')}
-            </ObservabilityControlButton>
-          ) : null}
-        </div>
-      </div>
+        data-monitor-realtime-toolbar-owner="hertzbeat-ui-realtime-toolbar"
+      />
 
       {compact ? null : (
-        <div className="border-y border-[var(--ops-border-color)] py-2">
-          <MonitorMetricTable
-            payload={payload}
-            matrix={filteredMatrix}
-            selectedRowKey={selectedRowKey}
-            onSelect={onSelect}
-            mode={mode}
-            t={t}
-          />
-        </div>
+        <MonitorMetricTable
+          payload={payload}
+          matrix={filteredMatrix}
+          selectedRowKey={selectedRowKey}
+          onSelect={onSelect}
+          mode={mode}
+          t={t}
+        />
       )}
       {!compact && selectedRow ? (
-        <div className="space-y-3 border-y border-[var(--ops-border-color)] py-3" data-monitor-surface-panel="row-inspector">
-          <MonitorStatGrid
-            items={[
-              { label: t('monitor.detail.metric.stats.active-row'), value: selectedRow.label },
-              { label: t('monitor.detail.metric.stats.fields'), value: String(selectedFieldCount) },
-              { label: t('monitor.detail.metric.stats.labels'), value: String(selectedLabelCount) }
-            ]}
-          />
-          <ObservabilityDetailRows tone="operator" rows={selectedRowRows} />
-        </div>
+        <HzMonitorRealtimeInspector
+          variant="details"
+          label={t('monitor.detail.metric.stats.active-row')}
+          value={selectedRow.label}
+          stats={[
+            { label: t('monitor.detail.metric.stats.fields'), value: String(selectedFieldCount) },
+            { label: t('monitor.detail.metric.stats.labels'), value: String(selectedLabelCount) }
+          ]}
+          rows={selectedRowRows.map(row => ({ label: row.title, value: row.copy, meta: row.meta && row.meta !== '-' ? row.meta : undefined }))}
+          data-monitor-realtime-inspector-owner="hertzbeat-ui-realtime-inspector"
+        />
       ) : null}
 
       {compact ? null : (
-        <div className="space-y-2">
-          <ObservabilitySearchInput
-            tone="operator"
+        <HzControlStack data-monitor-realtime-search-stack-owner="hertzbeat-ui-control-stack">
+          <HzInput
             value={rowSearch}
-            onChange={setRowSearch}
+            onChange={event => setRowSearch(event.target.value)}
             placeholder={t('monitor.detail.metric.rows.search.placeholder')}
+            aria-label={t('monitor.detail.metric.rows.search.placeholder')}
+            data-monitor-realtime-search-owner="hertzbeat-ui-input"
           />
-          <div className="text-xs text-[var(--ops-text-tertiary)]">
+          <HzDataMetaText
+            display="block"
+            casing="plain"
+            data-monitor-realtime-search-count-owner="hertzbeat-ui-data-meta-text"
+          >
             {`${filteredMatrix.rows.length} / ${buildMonitorMetricTableMatrix(payload, t).rows.length} ${t('monitor.detail.metric.rows.search.count')}`}
-          </div>
-        </div>
+          </HzDataMetaText>
+        </HzControlStack>
       )}
 
       {filteredMatrix.rows.length === 0 ? (
-        <div className="border-y border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-3 py-2 text-sm text-[var(--ops-text-secondary)]">
-          {t('monitor.detail.metric.rows.search.empty.copy')}
-        </div>
+        <HzInlineFeedback
+          tone="neutral"
+          title={t('monitor.detail.metric.rows.search.empty.copy')}
+          data-monitor-realtime-empty-owner="hertzbeat-ui-inline-feedback"
+        />
       ) : null}
 
       {!compact && rowNavigation.total > 0 && selectedVisibleIndex != null ? (
-        <div className="flex flex-wrap items-center justify-between gap-2 border-y border-[var(--ops-border-color)] px-3 py-2 text-xs text-[var(--ops-text-secondary)]">
-          <div>
-            {rowNavigation.selectedIndex != null
+        <HzMonitorRealtimeRowNavigator
+          label={
+            rowNavigation.selectedIndex != null
               ? `${t('monitor.detail.metric.row-label')} ${rowNavigation.selectedIndex + 1} / ${rowNavigation.total}${
                   rowNavigation.selectedLabel ? ` · ${rowNavigation.selectedLabel}` : ''
                 }`
-              : t('monitor.detail.metric.table.empty.copy')}
-          </div>
-          <div className="flex flex-wrap gap-2">
-            <ObservabilityControlButton
-              tone="operator"
-              disabled={!rowNavigation.canPrevious}
-              onClick={() =>
-                rowNavigation.selectedIndex != null &&
-                onSelect(filteredMatrix.rows[Math.max(0, selectedVisibleIndex - 1)]?.key || selectedRowKey || '')
-              }
-            >
-              {t('common.previous')}
-            </ObservabilityControlButton>
-            <ObservabilityControlButton
-              tone="operator"
-              disabled={!rowNavigation.canNext}
-              onClick={() =>
-                rowNavigation.selectedIndex != null &&
-                onSelect(filteredMatrix.rows[Math.min(filteredMatrix.rows.length - 1, selectedVisibleIndex + 1)]?.key || selectedRowKey || '')
-              }
-            >
-              {t('common.next')}
-            </ObservabilityControlButton>
-          </div>
-        </div>
+              : t('monitor.detail.metric.table.empty.copy')
+          }
+          previousLabel={t('common.previous')}
+          nextLabel={t('common.next')}
+          canPrevious={rowNavigation.canPrevious}
+          canNext={rowNavigation.canNext}
+          onPrevious={() =>
+            rowNavigation.selectedIndex != null &&
+            onSelect(filteredMatrix.rows[Math.max(0, selectedVisibleIndex - 1)]?.key || selectedRowKey || '')
+          }
+          onNext={() =>
+            rowNavigation.selectedIndex != null &&
+            onSelect(filteredMatrix.rows[Math.min(filteredMatrix.rows.length - 1, selectedVisibleIndex + 1)]?.key || selectedRowKey || '')
+          }
+          data-monitor-realtime-row-nav-owner="hertzbeat-ui-row-navigator"
+        />
       ) : null}
-    </div>
+    </HzMonitorDetailStage>
   );
 }
 
@@ -300,41 +259,31 @@ export function MonitorRealtimePanel({
         t={t}
       />
       {expanded ? (
-        <div
-          className="fixed inset-0 z-50 flex items-center justify-center bg-[rgba(6,7,10,0.88)] p-4"
+        <HzMonitorFullscreenFrame
+          ref={dialogRef}
           data-expanded="true"
-          role="dialog"
-          aria-modal="true"
-          aria-label={t('monitor.detail.metric.fullscreen.title')}
+          title={t('monitor.detail.metric.fullscreen.title')}
+          kicker={t('monitor.detail.metric.fullscreen.kicker')}
+          closeLabel={t('monitor.detail.metric.fullscreen.exit')}
+          onClose={onToggleExpanded}
+          closeButtonProps={{
+            'data-monitor-realtime-fullscreen-close-owner': 'hertzbeat-ui-button'
+          } as React.ButtonHTMLAttributes<HTMLButtonElement>}
+          data-monitor-realtime-fullscreen-owner="hertzbeat-ui-fullscreen-frame"
         >
-          <WorkbenchFullscreenShell
-            ref={dialogRef}
-            tabIndex={-1}
-            className="hb-scrollbar max-h-[92vh] w-full max-w-6xl overflow-auto"
-          >
-            <div className="mb-3 flex items-center justify-between gap-3">
-              <div>
-                <div className="text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{t('monitor.detail.metric.fullscreen.kicker')}</div>
-                <div className="mt-1 text-lg font-semibold text-[var(--ops-text-primary)]">{t('monitor.detail.metric.fullscreen.title')}</div>
-              </div>
-              <ObservabilityControlButton tone="operator" onClick={onToggleExpanded}>
-                {t('monitor.detail.metric.fullscreen.exit')}
-              </ObservabilityControlButton>
-            </div>
-            <Surface
-              payload={payload}
-              selectedRowKey={selectedRowKey}
-              mode={mode}
-              showExpandControl={false}
-              onSelect={onSelect}
-              onModeChange={onModeChange}
-              onRefresh={onRefresh}
-              onToggleExpanded={onToggleExpanded}
-              formatTime={formatTime}
-              t={t}
-            />
-          </WorkbenchFullscreenShell>
-        </div>
+          <Surface
+            payload={payload}
+            selectedRowKey={selectedRowKey}
+            mode={mode}
+            showExpandControl={false}
+            onSelect={onSelect}
+            onModeChange={onModeChange}
+            onRefresh={onRefresh}
+            onToggleExpanded={onToggleExpanded}
+            formatTime={formatTime}
+            t={t}
+          />
+        </HzMonitorFullscreenFrame>
       ) : null}
     </>
   );
diff --git a/web-next/components/monitor-detail/monitor-summary-card.test.tsx b/web-next/components/monitor-detail/monitor-summary-card.test.tsx
index 2908ebb79f..939aba9614 100644
--- a/web-next/components/monitor-detail/monitor-summary-card.test.tsx
+++ b/web-next/components/monitor-detail/monitor-summary-card.test.tsx
@@ -41,26 +41,39 @@ describe('monitor summary card', () => {
     expect(html).toContain('env');
     expect(html).toContain('owner');
     expect(html).toContain('2026-04-10 18:00:00');
-    expect(html).toMatch(/class=\"[^\"]*monitor-detail-container/);
-    expect(html).toContain('data-monitor-basic-density="angular-cardless"');
-    expect(html).toContain('data-monitor-basic-content-inset="angular-card-padding"');
-    expect(html).toMatch(/class=\"[^\"]*monitor-basic-info/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-basic-info[^\"]*px-2/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-info-header/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-name/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-basic-facts/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-basic-meta/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-basic-meta__row/);
-    expect(html).toMatch(/class=\"[^\"]*monitor-basic-meta__row--stacked/);
-    expect(html.indexOf('monitor-basic-meta')).toBeLessThan(html.indexOf('region'));
+    expect(html).toContain('data-hz-ui="monitor-basic-summary"');
+    expect(html).toContain('data-monitor-basic-summary-owner="hertzbeat-ui-basic-summary"');
+    expect(html).toContain('data-monitor-basic-density="shared-basic-summary"');
+    expect(html).toContain('data-monitor-basic-content-inset="hertzbeat-ui-basic-summary"');
+    expect(html).toContain('data-monitor-basic-facts-density="hertzbeat-ui-fact-grid"');
+    expect(html).toContain('data-monitor-basic-meta-density="hertzbeat-ui-rows"');
+    expect(html).toContain('data-monitor-basic-token-kind="label"');
+    expect(html).toContain('data-monitor-basic-token-kind="annotation"');
+    expect(html).not.toContain('data-monitor-basic-density="angular-cardless"');
+    expect(html).not.toContain('data-monitor-basic-content-inset="angular-card-padding"');
+    expect(html).not.toContain('data-monitor-basic-facts-density="angular-compact"');
+    expect(html).not.toContain('data-monitor-basic-meta-density="angular-rows"');
+    expect(html).not.toContain('monitor-basic-meta__row');
+    expect(html).not.toContain('var(--ops-border-color)');
+    expect(html).not.toContain('var(--ops-text-primary)');
+    expect(html).not.toContain('var(--ops-text-tertiary)');
+    expect(html.indexOf('data-monitor-basic-meta-density="hertzbeat-ui-rows"')).toBeLessThan(html.indexOf('region'));
   });
 
-  it('routes label chips through the observability badge owner', () => {
+  it('routes summary rows through @hertzbeat/ui instead of old observability primitives', () => {
     const source = readFileSync(resolve(process.cwd(), 'components/monitor-detail/monitor-summary-card.tsx'), 'utf8');
 
-    expect(source).toContain('ObservabilityBadge');
+    expect(source).toContain('HzMonitorBasicSummary');
+    expect(source).toContain("from '@hertzbeat/ui'");
     expect(source).toContain("t('monitor.status.up')");
     expect(source).not.toContain("t('monitor.detail.status.up')");
+    expect(source).not.toContain('ObservabilityBadge');
+    expect(source).not.toContain('ObservabilityDetailRows');
+    expect(source).not.toContain('ObservabilityStatGrid');
+    expect(source).not.toContain('ObservabilityStatusBadge');
+    expect(source).not.toContain('data-monitor-basic-density="angular-cardless"');
+    expect(source).not.toContain('data-monitor-basic-content-inset="angular-card-padding"');
+    expect(source).not.toContain('data-monitor-basic-meta-density="angular-rows"');
     expect(source).not.toContain('inline-flex rounded-[2px] border border-[var(--ops-border-color)] bg-[var(--ops-surface-panel)] px-2.5 py-1 text-xs text-[var(--ops-text-secondary)]');
     expect(source).not.toContain("from '../workbench/primitives'");
   });
diff --git a/web-next/components/monitor-detail/monitor-summary-card.tsx b/web-next/components/monitor-detail/monitor-summary-card.tsx
index d044673a5f..4777aaabdd 100644
--- a/web-next/components/monitor-detail/monitor-summary-card.tsx
+++ b/web-next/components/monitor-detail/monitor-summary-card.tsx
@@ -1,7 +1,7 @@
 'use client';
 
 import React from 'react';
-import { ObservabilityBadge, ObservabilityDetailRows, ObservabilityStatGrid, ObservabilityStatusBadge } from '../observability';
+import { HzMonitorBasicSummary, type HzStatusTone } from '@hertzbeat/ui';
 import { buildMonitorLabelRows, buildMonitorSummaryMetaRows, buildMonitorSummaryStats } from '../../lib/monitor-detail/view-model';
 import type { Monitor } from '../../lib/types';
 
@@ -26,59 +26,20 @@ export function MonitorSummaryCard({
       : monitor.status === 1
         ? t('monitor.status.up')
         : t('monitor.status.paused');
+  const statusTone: HzStatusTone =
+    monitor.status === 2 ? 'critical' : monitor.status === 1 ? 'success' : 'neutral';
 
   return (
-    <div className="monitor-detail-container space-y-3" data-monitor-basic-density="angular-cardless">
-      <div className="monitor-basic-info space-y-3 px-2 sm:px-3" data-monitor-basic-content-inset="angular-card-padding">
-        <div className="monitor-info-header flex items-center gap-2">
-          <div className="monitor-name min-w-0 flex-1 truncate text-[16px] font-semibold leading-6 text-[var(--ops-text-primary)]">
-            {monitor.name || t('monitor.detail.title-fallback')}
-          </div>
-          <ObservabilityStatusBadge
-            label={statusLabel}
-            tone={monitor.status === 2 ? 'danger' : monitor.status === 1 ? 'success' : 'default'}
-          />
-        </div>
-
-        <div className="monitor-basic-facts" data-monitor-basic-facts-density="angular-compact">
-          <ObservabilityStatGrid items={stats} columns={4} tone="operator" />
-        </div>
-
-        <div className="monitor-basic-meta border-t border-[var(--ops-border-color)]" data-monitor-basic-meta-density="angular-rows">
-          {metaRows.map(row => (
-            <div
-              key={`${row.label}-${row.value}`}
-              className="monitor-basic-meta__row grid gap-2 border-b border-[var(--ops-border-color)] py-2.5 sm:grid-cols-[160px_minmax(0,1fr)] sm:items-start"
-            >
-              <span className="text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{row.label}</span>
-              <strong className="text-sm font-medium leading-6 text-[var(--ops-text-primary)] sm:text-right">{row.value}</strong>
-            </div>
-          ))}
-          {labelRows.length > 0 ? (
-            <div className="monitor-basic-meta__row monitor-basic-meta__row--stacked grid gap-2 border-b border-[var(--ops-border-color)] py-2.5 sm:grid-cols-[160px_minmax(0,1fr)] sm:items-start">
-              <span className="text-[11px] uppercase tracking-[0.16em] text-[var(--ops-text-tertiary)]">{t('label')}</span>
-              <div className="flex flex-wrap gap-1.5 sm:justify-end">
-                {labelRows.map(row => (
-                  <ObservabilityBadge
-                    as="span"
-                    key={`${row.title}-${row.copy}`}
-                    size="token"
-                  >
-                    {row.title}: {row.copy}
-                  </ObservabilityBadge>
-                ))}
-              </div>
-            </div>
-          ) : null}
-        </div>
-      </div>
-
-      {annotationRows.length > 0 ? (
-        <div className="monitor-annotations space-y-2">
-          <div className="text-[11px] uppercase tracking-[0.18em] text-[var(--ops-text-tertiary)]">{t('annotation')}</div>
-          <ObservabilityDetailRows tone="operator" rows={annotationRows.map(row => ({ title: row.title, copy: row.copy }))} />
-        </div>
-      ) : null}
-    </div>
+    <HzMonitorBasicSummary
+      name={monitor.name || t('monitor.detail.title-fallback')}
+      statusLabel={statusLabel}
+      statusTone={statusTone}
+      facts={stats}
+      metaRows={metaRows}
+      labels={labelRows.map(row => ({ label: row.title, value: row.copy }))}
+      annotations={annotationRows.map(row => ({ label: row.title, value: row.copy }))}
+      labelHeading={t('label')}
+      annotationHeading={t('annotation')}
+    />
   );
 }
diff --git a/web-next/lib/monitor-detail/controller.test.ts b/web-next/lib/monitor-detail/controller.test.ts
index e149eb63a6..83fbd22ee0 100644
--- a/web-next/lib/monitor-detail/controller.test.ts
+++ b/web-next/lib/monitor-detail/controller.test.ts
@@ -1,18 +1,27 @@
 import { describe, expect, it, vi } from 'vitest';
 import {
   addMonitorFavorite,
+  addMonitorFavoriteFromFacade,
   buildMonitorDetailUrl,
   buildMonitorFavoriteUrl,
   buildMonitorGrafanaUrl,
+  buildMonitorHistoryCatalogUrl,
   buildMonitorHistoryMetricDataUrl,
   buildMonitorRealtimeMetricUrl,
   deleteMonitorGrafanaDashboard,
+  deleteMonitorGrafanaDashboardFromFacade,
   loadMonitorDetail,
   loadMonitorDetailBundle,
+  loadMonitorDetailBundleFromFacade,
   loadMonitorFavoriteMetrics,
+  loadMonitorHistoryMetricCatalog,
+  loadMonitorHistoryMetricCatalogFromFacade,
   loadMonitorHistoryMetricData,
+  loadMonitorHistoryMetricDataFromFacade,
   loadMonitorRealtimeMetricData,
-  removeMonitorFavorite
+  loadMonitorRealtimeMetricDataFromFacade,
+  removeMonitorFavorite,
+  removeMonitorFavoriteFromFacade
 } from './controller';
 
 describe('monitor detail controller', () => {
@@ -76,6 +85,46 @@ describe('monitor detail controller', () => {
     });
   });
 
+  it('loads monitor detail bundle through domain facade readers', async () => {
+    const readMonitorDetail = vi.fn().mockResolvedValue({
+      monitor: { id: 42, name: 'mysql-prod' },
+      params: [{ field: 'host', paramValue: '10.0.0.1' }],
+      metrics: [{ name: 'cpu' }]
+    });
+    const readGrafanaDashboard = vi.fn().mockResolvedValue(null);
+    const readFavoriteMetrics = vi.fn().mockResolvedValue(['cpu']);
+
+    await expect(
+      loadMonitorDetailBundleFromFacade(readMonitorDetail as any, readGrafanaDashboard as any, readFavoriteMetrics as any, 42)
+    ).resolves.toEqual({
+      monitor: { id: 42, name: 'mysql-prod' },
+      params: [{ field: 'host', paramValue: '10.0.0.1' }],
+      metrics: [{ name: 'cpu' }],
+      favoriteMetrics: ['cpu'],
+      grafana: { enabled: false }
+    });
+
+    expect(readMonitorDetail).toHaveBeenCalledWith(42);
+    expect(readGrafanaDashboard).toHaveBeenCalledWith(42);
+    expect(readFavoriteMetrics).toHaveBeenCalledWith(42);
+  });
+
+  it('degrades optional facade grafana and favorites failures instead of blocking detail bundle', async () => {
+    const readMonitorDetail = vi.fn().mockResolvedValue({ monitor: { id: 42 }, params: [], metrics: [] });
+    const readGrafanaDashboard = vi.fn().mockRejectedValue(new Error('grafana unavailable'));
+    const readFavoriteMetrics = vi.fn().mockRejectedValue(new Error('favorites unavailable'));
+
+    await expect(
+      loadMonitorDetailBundleFromFacade(readMonitorDetail as any, readGrafanaDashboard as any, readFavoriteMetrics as any, 42)
+    ).resolves.toEqual({
+      monitor: { id: 42 },
+      params: [],
+      metrics: [],
+      favoriteMetrics: [],
+      grafana: { enabled: false }
+    });
+  });
+
   it('deletes grafana dashboard by monitor id', async () => {
     const apiDelete = vi.fn().mockResolvedValue(undefined);
     await deleteMonitorGrafanaDashboard(apiDelete as any, '42');
@@ -96,6 +145,65 @@ describe('monitor detail controller', () => {
     expect(apiDelete).toHaveBeenCalledWith('/metrics/favorite/42/cpu.usage');
   });
 
+  it('runs favorite and grafana mutations through facade writers', async () => {
+    const addFavorite = vi.fn().mockResolvedValue(undefined);
+    const removeFavorite = vi.fn().mockResolvedValue(undefined);
+    const deleteGrafanaDashboard = vi.fn().mockResolvedValue(undefined);
+
+    await addMonitorFavoriteFromFacade(addFavorite, 42, 'cpu.usage');
+    await removeMonitorFavoriteFromFacade(removeFavorite, 42, 'cpu.usage');
+    await deleteMonitorGrafanaDashboardFromFacade(deleteGrafanaDashboard, 42);
+
+    expect(addFavorite).toHaveBeenCalledWith(42, 'cpu.usage');
+    expect(removeFavorite).toHaveBeenCalledWith(42, 'cpu.usage');
+    expect(deleteGrafanaDashboard).toHaveBeenCalledWith(42);
+  });
+
+  it('builds history catalog urls and extracts visible numeric fields', async () => {
+    const apiGet = vi.fn()
+      .mockResolvedValueOnce({ ok: true })
+      .mockResolvedValueOnce({
+        metrics: [
+          {
+            name: 'cpu',
+            visible: true,
+            fields: [
+              { type: 0, field: 'usage', unit: '%' },
+              { type: 1, field: 'host' }
+            ]
+          },
+          { name: 'hidden', visible: false, fields: [{ type: 0, field: 'dropped' }] }
+        ]
+      });
+    const monitor = { id: 42, app: 'prometheus', scrape: 'prometheus', name: 'node-exporter' } as any;
+
+    expect(buildMonitorHistoryCatalogUrl({ id: 1, app: 'push', scrape: 'push' } as any)).toBe('/apps/1/pushdefine');
+    expect(buildMonitorHistoryCatalogUrl(monitor)).toBe('/apps/42/define/dynamic');
+    expect(buildMonitorHistoryCatalogUrl({ id: 2, app: 'mysql', scrape: 'static' } as any)).toBe('/apps/mysql/define');
+
+    await expect(loadMonitorHistoryMetricCatalog(apiGet as any, monitor)).resolves.toEqual([
+      { metrics: 'cpu', metric: 'usage', unit: '%' }
+    ]);
+
+    expect(apiGet).toHaveBeenNthCalledWith(1, '/warehouse/storage/status');
+    expect(apiGet).toHaveBeenNthCalledWith(2, '/apps/42/define/dynamic');
+  });
+
+  it('loads history catalog definitions through domain facade readers', async () => {
+    const readWarehouseStorageStatus = vi.fn().mockResolvedValue({ available: true });
+    const readHistoryCatalogDefine = vi.fn().mockResolvedValue({
+      metrics: [{ name: 'availability', fields: [{ type: 0, field: 'responseTime', unit: 'ms' }] }]
+    });
+    const monitor = { id: 42, app: 'website', scrape: 'static' } as any;
+
+    await expect(
+      loadMonitorHistoryMetricCatalogFromFacade(readWarehouseStorageStatus as any, readHistoryCatalogDefine as any, monitor)
+    ).resolves.toEqual([{ metrics: 'availability', metric: 'responseTime', unit: 'ms' }]);
+
+    expect(readWarehouseStorageStatus).toHaveBeenCalledWith();
+    expect(readHistoryCatalogDefine).toHaveBeenCalledWith(monitor);
+  });
+
   it('builds realtime metric url and loads metric payload', async () => {
     const apiGet = vi.fn().mockResolvedValue({ status: 'ok' });
     expect(buildMonitorRealtimeMetricUrl('42', 'cpu')).toBe('/monitor/42/metrics/cpu');
@@ -103,6 +211,16 @@ describe('monitor detail controller', () => {
     expect(apiGet).toHaveBeenCalledWith('/monitor/42/metrics/cpu');
   });
 
+  it('loads realtime metric payloads through the domain facade reader', async () => {
+    const readRealtimeMetric = vi.fn().mockResolvedValue({ rows: [{ key: 'usage' }] });
+
+    await expect(loadMonitorRealtimeMetricDataFromFacade(readRealtimeMetric as any, 42, 'cpu')).resolves.toEqual({
+      rows: [{ key: 'usage' }]
+    });
+
+    expect(readRealtimeMetric).toHaveBeenCalledWith(42, 'cpu');
+  });
+
   it('builds history metric url and loads history payload', async () => {
     const monitor = { id: 42, instance: '127.0.0.1:8080', app: 'prometheus', name: 'node-exporter' } as any;
     const item = { metrics: 'cpu', metric: 'usage' };
@@ -121,6 +239,19 @@ describe('monitor detail controller', () => {
     );
   });
 
+  it('loads history metric payloads through the domain facade reader', async () => {
+    const monitor = { id: 42, instance: '127.0.0.1:8080', app: 'website', name: 'site' } as any;
+    const item = { metrics: 'availability', metric: 'responseTime' };
+    const query = { history: '1h', interval: true };
+    const readHistoryMetric = vi.fn().mockResolvedValue({ values: { responseTime: [{ origin: '120' }] } });
+
+    await expect(loadMonitorHistoryMetricDataFromFacade(readHistoryMetric as any, monitor, item, query)).resolves.toEqual({
+      values: { responseTime: [{ origin: '120' }] }
+    });
+
+    expect(readHistoryMetric).toHaveBeenCalledWith(monitor, item, query);
+  });
+
   it('supports custom history query when building history url', () => {
     const monitor = { id: 7, instance: 'db.internal:5432', app: 'postgresql', name: 'pg-prod' } as any;
     const item = { metrics: 'performance', metric: 'qps' };
diff --git a/web-next/lib/monitor-detail/controller.ts b/web-next/lib/monitor-detail/controller.ts
index f20d7bcf66..4f971194d0 100644
--- a/web-next/lib/monitor-detail/controller.ts
+++ b/web-next/lib/monitor-detail/controller.ts
@@ -4,11 +4,22 @@ import { readEpochMillisTimeParam, readStepParam } from '@/lib/time-context';
 type ApiGetter = <T>(url: string) => Promise<T>;
 type ApiPost = <T>(url: string, payload: unknown) => Promise<T>;
 type ApiDelete = <T>(url: string) => Promise<T>;
-type MonitorDetailResponse = {
+type MonitorRealtimeMetricReader = <T = unknown>(monitorId: string | number, metricName: string) => Promise<T>;
+type MonitorHistoryMetricReader = (
+  monitor: Monitor,
+  item: MonitorHistoryMetricCatalogItem,
+  query?: MonitorHistoryQuery
+) => Promise<MonitorHistoryData>;
+type MonitorHistoryStorageStatusReader = () => Promise<unknown>;
+type MonitorHistoryCatalogDefineReader = (monitor: Monitor) => Promise<MonitorHistoryCatalogDefineResponse>;
+export type MonitorDetailResponse = {
   monitor: Monitor;
   params?: Param[];
   metrics?: MonitorDetailMetric[];
 };
+type MonitorDetailReader = (monitorId: string | number) => Promise<MonitorDetailResponse>;
+type MonitorGrafanaReader = <T = GrafanaDashboard | null>(monitorId: string | number) => Promise<T>;
+type MonitorFavoriteMetricsReader = (monitorId: string | number) => Promise<string[]>;
 
 export type MonitorDetailBundle = {
   monitor: Monitor;
@@ -52,6 +63,10 @@ export async function deleteMonitorGrafanaDashboard(apiDelete: ApiDelete, monito
   return apiDelete<void>(buildMonitorGrafanaUrl(monitorId));
 }
 
+export async function deleteMonitorGrafanaDashboardFromFacade(deleteGrafanaDashboard: (monitorId: string | number) => Promise<unknown>, monitorId: string | number) {
+  return deleteGrafanaDashboard(monitorId);
+}
+
 export function buildMonitorFavoriteUrl(monitorId: string, metricName?: string) {
   return metricName == null
     ? `/metrics/favorite/${monitorId}`
@@ -81,11 +96,11 @@ type AppDefineMetric = {
   visible?: boolean;
 };
 
-type AppDefineResponse = {
+export type MonitorHistoryCatalogDefineResponse = {
   metrics?: AppDefineMetric[];
 };
 
-export function extractHistoryMetricCatalog(define: AppDefineResponse): MonitorHistoryMetricCatalogItem[] {
+export function extractHistoryMetricCatalog(define: MonitorHistoryCatalogDefineResponse): MonitorHistoryMetricCatalogItem[] {
   const rows: MonitorHistoryMetricCatalogItem[] = [];
   for (const metric of define.metrics || []) {
     if (metric.visible === false) continue;
@@ -104,7 +119,17 @@ export function extractHistoryMetricCatalog(define: AppDefineResponse): MonitorH
 
 export async function loadMonitorHistoryMetricCatalog(apiGet: ApiGetter, monitor: Monitor) {
   await apiGet<unknown>('/warehouse/storage/status');
-  const define = await apiGet<AppDefineResponse>(buildMonitorHistoryCatalogUrl(monitor));
+  const define = await apiGet<MonitorHistoryCatalogDefineResponse>(buildMonitorHistoryCatalogUrl(monitor));
+  return extractHistoryMetricCatalog(define);
+}
+
+export async function loadMonitorHistoryMetricCatalogFromFacade(
+  readWarehouseStorageStatus: MonitorHistoryStorageStatusReader,
+  readHistoryCatalogDefine: MonitorHistoryCatalogDefineReader,
+  monitor: Monitor
+) {
+  await readWarehouseStorageStatus();
+  const define = await readHistoryCatalogDefine(monitor);
   return extractHistoryMetricCatalog(define);
 }
 
@@ -144,6 +169,15 @@ export async function loadMonitorHistoryMetricData(
   return apiGet<MonitorHistoryData>(buildMonitorHistoryMetricDataUrl(monitor, item, query));
 }
 
+export async function loadMonitorHistoryMetricDataFromFacade(
+  readHistoryMetric: MonitorHistoryMetricReader,
+  monitor: Monitor,
+  item: MonitorHistoryMetricCatalogItem,
+  query?: MonitorHistoryQuery
+) {
+  return readHistoryMetric(monitor, item, query);
+}
+
 export function buildMonitorRealtimeMetricUrl(monitorId: string, metricName: string) {
   return `/monitor/${monitorId}/metrics/${encodeURIComponent(metricName)}`;
 }
@@ -156,14 +190,30 @@ export async function addMonitorFavorite(apiPost: ApiPost, monitorId: string, me
   return apiPost<void>(buildMonitorFavoriteUrl(monitorId, metricName), null);
 }
 
+export async function addMonitorFavoriteFromFacade(addFavorite: (monitorId: string | number, metricName: string) => Promise<unknown>, monitorId: string | number, metricName: string) {
+  return addFavorite(monitorId, metricName);
+}
+
 export async function removeMonitorFavorite(apiDelete: ApiDelete, monitorId: string, metricName: string) {
   return apiDelete<void>(buildMonitorFavoriteUrl(monitorId, metricName));
 }
 
+export async function removeMonitorFavoriteFromFacade(removeFavorite: (monitorId: string | number, metricName: string) => Promise<unknown>, monitorId: string | number, metricName: string) {
+  return removeFavorite(monitorId, metricName);
+}
+
 export async function loadMonitorRealtimeMetricData(apiGet: ApiGetter, monitorId: string, metricName: string) {
   return apiGet<unknown>(buildMonitorRealtimeMetricUrl(monitorId, metricName));
 }
 
+export async function loadMonitorRealtimeMetricDataFromFacade(
+  readRealtimeMetric: MonitorRealtimeMetricReader,
+  monitorId: string | number,
+  metricName: string
+) {
+  return readRealtimeMetric<unknown>(monitorId, metricName);
+}
+
 export async function loadMonitorDetailBundle(apiGet: ApiGetter, monitorId: string) {
   const [detail, grafana, favoriteMetrics] = await Promise.all([
     apiGet<MonitorDetailResponse>(buildMonitorDetailUrl(monitorId)),
@@ -181,3 +231,26 @@ export async function loadMonitorDetailBundle(apiGet: ApiGetter, monitorId: stri
     grafana: normalizeGrafanaDashboard(grafana)
   } satisfies MonitorDetailBundle;
 }
+
+export async function loadMonitorDetailBundleFromFacade(
+  readMonitorDetail: MonitorDetailReader,
+  readGrafanaDashboard: MonitorGrafanaReader,
+  readFavoriteMetrics: MonitorFavoriteMetricsReader,
+  monitorId: string | number
+) {
+  const [detail, grafana, favoriteMetrics] = await Promise.all([
+    readMonitorDetail(monitorId),
+    readGrafanaDashboard<GrafanaDashboard | null>(monitorId)
+      .then(result => normalizeGrafanaDashboard(result))
+      .catch(() => ({ enabled: false } satisfies GrafanaDashboard)),
+    readFavoriteMetrics(monitorId).catch(() => [])
+  ]);
+
+  return {
+    monitor: detail.monitor,
+    params: detail.params || [],
+    metrics: detail.metrics || [],
+    favoriteMetrics: favoriteMetrics || [],
+    grafana: normalizeGrafanaDashboard(grafana)
+  } satisfies MonitorDetailBundle;
+}
diff --git a/web-next/lib/monitor-detail/detail-route-state.test.ts b/web-next/lib/monitor-detail/detail-route-state.test.ts
index 9975ccb04e..b75a01397d 100644
--- a/web-next/lib/monitor-detail/detail-route-state.test.ts
+++ b/web-next/lib/monitor-detail/detail-route-state.test.ts
@@ -47,6 +47,28 @@ describe('monitor detail route state', () => {
     });
   });
 
+  it('reads monitor detail expression time context for shared signal handoff links', () => {
+    const params = new URLSearchParams(
+      'app=mysql&entityId=42&entityName=orders-db&returnTo=%2Fmonitors&from=now-6h&to=now&refresh=30&live=true&timezone=Asia%2FShanghai'
+    );
+
+    expect(readMonitorDetailNavigationContext(params)).toEqual({
+      app: 'mysql',
+      labels: null,
+      pageIndex: null,
+      pageSize: null,
+      entityId: '42',
+      entityName: 'orders-db',
+      returnTo: '/monitors',
+      from: 'now-6h',
+      to: 'now',
+      refresh: '30',
+      live: 'true',
+      tz: 'Asia/Shanghai',
+      timezone: 'Asia/Shanghai'
+    });
+  });
+
   it('builds traditional-monitor signal handoff links with time and monitor context only', () => {
     const links = buildMonitorSignalHandoffLinks(
       {
@@ -93,11 +115,56 @@ describe('monitor detail route state', () => {
     expect(metricsUrl.searchParams.has('status')).toBe(false);
   });
 
+  it('builds monitor signal handoff links with canonical expression time context', () => {
+    const links = buildMonitorSignalHandoffLinks(
+      {
+        id: 640360126405888,
+        name: 'mysql-prod',
+        app: 'mysql',
+        instance: '127.0.0.1:3306'
+      } as any,
+      {
+        app: 'mysql',
+        entityId: 'orders-db',
+        entityName: 'Orders DB',
+        timeRange: 'last-1h',
+        from: 'now-6h',
+        to: 'now',
+        start: '1712730000000',
+        end: '1712733600000',
+        refresh: '30',
+        live: 'true',
+        tz: 'Asia/Shanghai',
+        timezone: 'Asia/Shanghai'
+      },
+      '/monitors/640360126405888?app=mysql&from=now-6h&to=now&timezone=Asia%2FShanghai&returnLabel=Monitor'
+    );
+
+    const metricsUrl = new URL(links.metricsHref, 'http://hertzbeat.local');
+    expect(metricsUrl.searchParams.get('from')).toBe('now-6h');
+    expect(metricsUrl.searchParams.get('to')).toBe('now');
+    expect(metricsUrl.searchParams.get('refresh')).toBe('30');
+    expect(metricsUrl.searchParams.get('live')).toBe('true');
+    expect(metricsUrl.searchParams.get('timezone')).toBe('Asia/Shanghai');
+    expect(metricsUrl.searchParams.get('source')).toBe('monitor');
+    expect(metricsUrl.searchParams.get('monitorId')).toBe('640360126405888');
+    expect(metricsUrl.searchParams.get('monitorApp')).toBe('mysql');
+    expect(metricsUrl.searchParams.get('returnTo')).toBe(
+      '/monitors/640360126405888?app=mysql&from=now-6h&to=now&timezone=Asia%2FShanghai'
+    );
+    expect(metricsUrl.searchParams.has('timeRange')).toBe(false);
+    expect(metricsUrl.searchParams.has('start')).toBe(false);
+    expect(metricsUrl.searchParams.has('end')).toBe(false);
+    expect(metricsUrl.searchParams.has('tz')).toBe(false);
+  });
+
   it('resolves tab-scoped refresh targets', () => {
     expect(resolveMonitorDetailRefreshTarget('realtime')).toBe('realtime');
     expect(resolveMonitorDetailRefreshTarget('history')).toBe('history');
     expect(resolveMonitorDetailRefreshTarget('favorites')).toBe('favorites');
-    expect(resolveMonitorDetailRefreshTarget('grafana')).toBe('grafana');
+    expect(resolveMonitorDetailRefreshTarget('grafana')).toBe('realtime');
+    expect(resolveMonitorDetailRefreshTarget('grafana', 'history')).toBe('history');
+    expect(resolveMonitorDetailRefreshTarget('grafana', 'favorites')).toBe('favorites');
   });
 
   it('resolves favorite jump targets from the favorite row kind', () => {
@@ -154,6 +221,36 @@ describe('monitor detail route state', () => {
     expect(resolveFavoriteSurfaceMode('history', favoriteRows as any, 'realtime:summary')).toBe('realtime');
   });
 
+  it('keeps favorite removal fallback scoped to the Angular subselector', () => {
+    const favoriteRows = [
+      {
+        key: 'realtime:summary',
+        title: 'summary',
+        copy: 'Favorite realtime metric',
+        meta: 'realtime',
+        targetKey: 'summary',
+        targetKind: 'realtime',
+        favoriteToken: 'summary'
+      },
+      {
+        key: 'history:latency',
+        title: 'latency',
+        copy: 'Favorite history metric',
+        meta: 'history',
+        targetKey: 'website:latency',
+        targetKind: 'history',
+        favoriteToken: 'website.latency'
+      }
+    ] as const;
+
+    expect(resolveActiveFavoriteRow(favoriteRows as any, 'history:removed', 'history')?.key).toBe('history:latency');
+    expect(resolveActiveFavoriteRow(favoriteRows as any, 'history:removed', 'realtime')?.key).toBe('realtime:summary');
+    expect(resolveActiveFavoriteRow(favoriteRows as any, 'history:removed', 'history')?.targetKind).toBe('history');
+    expect(resolveActiveFavoriteRow([favoriteRows[0]] as any, 'history:removed', 'history')).toBeNull();
+    expect(resolveFavoriteSurfaceMode('history', [favoriteRows[0]] as any, 'history:removed')).toBe('history');
+    expect(resolveFavoriteSurfaceMode('realtime', [favoriteRows[1]] as any, null)).toBe('realtime');
+  });
+
   it('falls back from grafana tab only when grafana is disabled', () => {
     expect(shouldFallbackFromGrafanaTab(false, 'grafana')).toBe(true);
     expect(shouldFallbackFromGrafanaTab(true, 'grafana')).toBe(false);
diff --git a/web-next/lib/monitor-detail/detail-route-state.ts b/web-next/lib/monitor-detail/detail-route-state.ts
index ad3988d421..a38751bb6c 100644
--- a/web-next/lib/monitor-detail/detail-route-state.ts
+++ b/web-next/lib/monitor-detail/detail-route-state.ts
@@ -18,11 +18,14 @@ export type MonitorDetailNavigationContext = {
   entityName: string | null;
   returnTo: string | null;
   timeRange?: string;
+  from?: string;
+  to?: string;
   start?: string;
   end?: string;
   refresh?: string;
   live?: string;
   tz?: string;
+  timezone?: string;
 };
 
 export type MonitorSignalHandoffLinks = {
@@ -32,6 +35,7 @@ export type MonitorSignalHandoffLinks = {
 };
 
 export type MonitorDetailRefreshTarget = 'realtime' | 'history' | 'favorites' | 'grafana';
+export type MonitorDetailRefreshableTab = Exclude<MonitorDetailConsoleTabKey, 'grafana'>;
 
 export type MonitorDetailSectionsInput = {
   contextNode: ReactNode;
@@ -61,11 +65,14 @@ export function readMonitorDetailNavigationContext(searchParams: URLSearchParams
     entityName: searchParams.get('entityName'),
     returnTo: stripReturnLabelFromHref(searchParams.get('returnTo')) || null,
     ...(signalContext.timeRange ? { timeRange: signalContext.timeRange } : {}),
+    ...(signalContext.from ? { from: signalContext.from } : {}),
+    ...(signalContext.to ? { to: signalContext.to } : {}),
     ...(signalContext.start ? { start: signalContext.start } : {}),
     ...(signalContext.end ? { end: signalContext.end } : {}),
     ...(signalContext.refresh ? { refresh: signalContext.refresh } : {}),
     ...(signalContext.live ? { live: signalContext.live } : {}),
-    ...(signalContext.tz ? { tz: signalContext.tz } : {})
+    ...(signalContext.tz ? { tz: signalContext.tz } : {}),
+    ...(signalContext.timezone ? { timezone: signalContext.timezone } : {})
   };
 }
 
@@ -85,11 +92,14 @@ export function buildMonitorSignalHandoffLinks(
   const returnTo = stripReturnLabelFromHref(currentHref || (monitorId ? `/monitors/${monitorId}` : '/monitors'));
   const signalContext: SignalRouteContext = {
     timeRange: context.timeRange,
+    from: context.from,
+    to: context.to,
     start: context.start,
     end: context.end,
     refresh: context.refresh,
     live: context.live,
     tz: context.tz,
+    timezone: context.timezone,
     entityId: context.entityId || undefined,
     entityName: context.entityName || undefined,
     returnTo,
@@ -107,10 +117,13 @@ export function buildMonitorSignalHandoffLinks(
   };
 }
 
-export function resolveMonitorDetailRefreshTarget(currentTab: MonitorDetailConsoleTabKey): MonitorDetailRefreshTarget {
+export function resolveMonitorDetailRefreshTarget(
+  currentTab: MonitorDetailConsoleTabKey,
+  previousDataTab: MonitorDetailRefreshableTab = 'realtime'
+): MonitorDetailRefreshTarget {
   if (currentTab === 'history') return 'history';
   if (currentTab === 'favorites') return 'favorites';
-  if (currentTab === 'grafana') return 'grafana';
+  if (currentTab === 'grafana') return previousDataTab;
   return 'realtime';
 }
 
@@ -122,9 +135,15 @@ export function resolveFavoriteJumpTarget(row: MonitorFavoriteJumpRow): { tab: '
 
 export function resolveActiveFavoriteRow(
   favoriteRows: MonitorFavoriteJumpRow[],
-  selectedFavoriteKey: string | null
+  selectedFavoriteKey: string | null,
+  preferredMode?: 'realtime' | 'history'
 ): MonitorFavoriteJumpRow | null {
-  return favoriteRows.find(row => row.key === selectedFavoriteKey) ?? favoriteRows[0] ?? null;
+  const selectedRow = favoriteRows.find(row => row.key === selectedFavoriteKey);
+  if (selectedRow) return selectedRow;
+  if (preferredMode) {
+    return favoriteRows.find(row => row.targetKind === preferredMode) ?? null;
+  }
+  return favoriteRows[0] ?? null;
 }
 
 export function resolveFavoriteSurfaceMode(
@@ -132,19 +151,12 @@ export function resolveFavoriteSurfaceMode(
   favoriteRows: MonitorFavoriteJumpRow[],
   selectedFavoriteKey: string | null
 ): 'realtime' | 'history' {
-  const activeRow = resolveActiveFavoriteRow(favoriteRows, selectedFavoriteKey);
+  const activeRow = favoriteRows.find(row => row.key === selectedFavoriteKey);
   if (activeRow) {
     return activeRow.targetKind;
   }
 
-  const hasRealtimeFavorites = favoriteRows.some(row => row.targetKind === 'realtime');
-  const hasHistoryFavorites = favoriteRows.some(row => row.targetKind === 'history');
-
-  if (currentMode === 'realtime' && hasRealtimeFavorites) return currentMode;
-  if (currentMode === 'history' && hasHistoryFavorites) return currentMode;
-  if (hasRealtimeFavorites) return 'realtime';
-  if (hasHistoryFavorites) return 'history';
-  return 'realtime';
+  return currentMode;
 }
 
 export function shouldFallbackFromGrafanaTab(grafanaEnabled: boolean, currentTab: MonitorDetailConsoleTabKey): boolean {
diff --git a/web-next/lib/monitor-detail/history-chart.test.ts b/web-next/lib/monitor-detail/history-chart.test.ts
index b600758f2b..68eacdb072 100644
--- a/web-next/lib/monitor-detail/history-chart.test.ts
+++ b/web-next/lib/monitor-detail/history-chart.test.ts
@@ -5,7 +5,10 @@ import {
   buildHistoryChartPoints,
   buildHistoryChartPolyline,
   buildHistoryChartSeries,
+  buildHistoryDataZoomApplyTimeContext,
+  buildHistoryDataZoomPreviewTimeContext,
   buildHistoryDataZoomTimeContext,
+  buildHistoryResetTimeContext,
   findNearestHistoryChartPointIndex,
   buildHistoryChartSvgDataUrl,
   buildHistoryChartSvgDocument
@@ -238,11 +241,14 @@ describe('monitor history chart helpers', () => {
   });
 
   it('resolves a local dataZoom percentage range into an explicit query time window', () => {
+    const first = new Date(2026, 4, 17, 15, 0, 0).getTime();
+    const middle = new Date(2026, 4, 17, 16, 0, 0).getTime();
+    const last = new Date(2026, 4, 17, 17, 0, 0).getTime();
     const context = buildHistoryDataZoomTimeContext(
       [
-        { origin: '10', time: 1_000 },
-        { origin: '20', time: 2_000 },
-        { origin: '30', time: 3_000 }
+        { origin: '10', time: first },
+        { origin: '20', time: middle },
+        { origin: '30', time: last }
       ] as any,
       { start: 25, end: 75 },
       'last-1h'
@@ -250,26 +256,156 @@ describe('monitor history chart helpers', () => {
 
     expect(context).toEqual({
       timeRange: 'last-1h',
-      start: '1500',
-      end: '2500'
+      from: '2026-05-17 15:30:00',
+      to: '2026-05-17 16:30:00'
     });
   });
 
   it('prefers concrete dataZoom startValue and endValue when applying chart zoom to query time', () => {
+    const first = new Date(2026, 4, 17, 15, 0, 0).getTime();
+    const middle = new Date(2026, 4, 17, 16, 0, 0).getTime();
+    const last = new Date(2026, 4, 17, 17, 0, 0).getTime();
     const context = buildHistoryDataZoomTimeContext(
       [
-        { origin: '10', time: 1_000 },
-        { origin: '20', time: 2_000 },
-        { origin: '30', time: 3_000 }
+        { origin: '10', time: first },
+        { origin: '20', time: middle },
+        { origin: '30', time: last }
+      ] as any,
+      { start: 0, end: 100, startValue: first + 15 * 60 * 1000, endValue: last - 15 * 60 * 1000 },
+      'last-1h'
+    );
+
+    expect(context).toEqual({
+      timeRange: 'last-1h',
+      from: '2026-05-17 15:15:00',
+      to: '2026-05-17 16:45:00'
+    });
+  });
+
+  it('keeps refresh live and timezone controls attached when dataZoom previews the toolbar range', () => {
+    const first = new Date(2026, 4, 17, 15, 0, 0).getTime();
+    const middle = new Date(2026, 4, 17, 16, 0, 0).getTime();
+    const last = new Date(2026, 4, 17, 17, 0, 0).getTime();
+    const context = buildHistoryDataZoomPreviewTimeContext(
+      [
+        { origin: '10', time: first },
+        { origin: '20', time: middle },
+        { origin: '30', time: last }
+      ] as any,
+      { start: 25, end: 75 },
+      {
+        timeRange: 'last-1h',
+        from: 'now-1h',
+        to: 'now',
+        start: String(first),
+        end: String(last),
+        refresh: '30',
+        live: 'true',
+        tz: 'Asia/Shanghai',
+        timezone: 'Asia/Shanghai'
+      },
+      'last-1h'
+    );
+
+    expect(context).toEqual({
+      timeRange: 'last-1h',
+      from: '2026-05-17 15:30:00',
+      to: '2026-05-17 16:30:00',
+      refresh: '30',
+      live: 'true',
+      tz: 'Asia/Shanghai',
+      timezone: 'Asia/Shanghai'
+    });
+  });
+
+  it('applies chart dataZoom as a readable expression query without carrying the relative preset', () => {
+    const first = new Date(2026, 4, 17, 15, 0, 0).getTime();
+    const middle = new Date(2026, 4, 17, 16, 0, 0).getTime();
+    const last = new Date(2026, 4, 17, 17, 0, 0).getTime();
+    const context = buildHistoryDataZoomApplyTimeContext(
+      [
+        { origin: '10', time: first },
+        { origin: '20', time: middle },
+        { origin: '30', time: last }
       ] as any,
-      { start: 0, end: 100, startValue: 1_200, endValue: 2_600 },
+      { start: 25, end: 75 },
+      {
+        timeRange: 'last-1h',
+        from: 'now-1h',
+        to: 'now',
+        start: String(first),
+        end: String(last),
+        refresh: '30',
+        live: 'true',
+        timezone: 'Asia/Shanghai'
+      },
       'last-1h'
     );
 
     expect(context).toEqual({
+      from: '2026-05-17 15:30:00',
+      to: '2026-05-17 16:30:00',
+      refresh: '30',
+      live: 'true',
+      tz: 'Asia/Shanghai',
+      timezone: 'Asia/Shanghai'
+    });
+  });
+
+  it('keeps manual refresh canonical when dataZoom previews inherit a stale interval', () => {
+    const first = new Date(2026, 4, 17, 15, 0, 0).getTime();
+    const middle = new Date(2026, 4, 17, 16, 0, 0).getTime();
+    const last = new Date(2026, 4, 17, 17, 0, 0).getTime();
+    const context = buildHistoryDataZoomPreviewTimeContext(
+      [
+        { origin: '10', time: first },
+        { origin: '20', time: middle },
+        { origin: '30', time: last }
+      ] as any,
+      { start: 25, end: 75 },
+      {
+        timeRange: 'last-1h',
+        from: 'now-1h',
+        to: 'now',
+        refresh: '60',
+        live: 'false',
+        timezone: 'Asia/Shanghai'
+      },
+      'last-1h'
+    );
+
+    expect(context).toEqual({
+      timeRange: 'last-1h',
+      from: '2026-05-17 15:30:00',
+      to: '2026-05-17 16:30:00',
+      live: 'false',
+      tz: 'Asia/Shanghai',
+      timezone: 'Asia/Shanghai'
+    });
+  });
+
+  it('resets explicit chart zoom windows back to the quick range while preserving toolbar controls', () => {
+    expect(
+      buildHistoryResetTimeContext(
+        {
+          timeRange: 'last-1h',
+          from: '2026-05-17 15:30:00',
+          to: '2026-05-17 16:30:00',
+          start: '1779003000000',
+          end: '1779006600000',
+          refresh: '30',
+          live: 'true',
+          tz: 'Asia/Shanghai',
+          timezone: 'Asia/Shanghai'
+        },
+        'last-1h'
+      )
+    ).toEqual({
       timeRange: 'last-1h',
-      start: '1200',
-      end: '2600'
+      refresh: '30',
+      live: 'true',
+      tz: 'Asia/Shanghai',
+      timezone: 'Asia/Shanghai'
     });
   });
 
diff --git a/web-next/lib/monitor-detail/history-chart.ts b/web-next/lib/monitor-detail/history-chart.ts
index 6965de362e..deecb7645c 100644
--- a/web-next/lib/monitor-detail/history-chart.ts
+++ b/web-next/lib/monitor-detail/history-chart.ts
@@ -1,5 +1,5 @@
 import { graphic, type EChartsOption } from 'echarts';
-import { buildChartDataZoomTimeContext, type ChartDataZoomRange, type TimeContext } from '@/lib/time-context';
+import { buildChartDataZoomTimeContext, sanitizeTimeContext, type ChartDataZoomRange, type TimeContext } from '@/lib/time-context';
 import type { MonitorHistoryValue } from '@/lib/types';
 
 export type HistoryChartPoint = {
@@ -194,6 +194,59 @@ export function buildHistoryDataZoomTimeContext(
   return buildChartDataZoomTimeContext(values.map(value => value.time as number | string | null | undefined), zoomRange, fallbackTimeRange);
 }
 
+export function buildHistoryDataZoomPreviewTimeContext(
+  values: MonitorHistoryValue[],
+  zoomRange: HistoryDataZoomRange | null | undefined,
+  currentTimeContext: TimeContext | undefined,
+  fallbackTimeRange?: string
+): TimeContext | null {
+  const zoomContext = buildHistoryDataZoomTimeContext(values, zoomRange, fallbackTimeRange);
+  if (!zoomContext) return null;
+
+  const timezone = currentTimeContext?.timezone || currentTimeContext?.tz;
+  return sanitizeTimeContext({
+    ...zoomContext,
+    refresh: currentTimeContext?.refresh,
+    live: currentTimeContext?.live,
+    tz: currentTimeContext?.tz || timezone,
+    timezone
+  });
+}
+
+export function buildHistoryDataZoomApplyTimeContext(
+  values: MonitorHistoryValue[],
+  zoomRange: HistoryDataZoomRange | null | undefined,
+  currentTimeContext: TimeContext | undefined,
+  fallbackTimeRange?: string
+): TimeContext | null {
+  const zoomContext = buildHistoryDataZoomTimeContext(values, zoomRange, fallbackTimeRange);
+  if (!zoomContext?.from || !zoomContext.to) return null;
+
+  const timezone = currentTimeContext?.timezone || currentTimeContext?.tz;
+  return sanitizeTimeContext({
+    from: zoomContext.from,
+    to: zoomContext.to,
+    refresh: currentTimeContext?.refresh,
+    live: currentTimeContext?.live,
+    tz: currentTimeContext?.tz || timezone,
+    timezone
+  });
+}
+
+export function buildHistoryResetTimeContext(
+  currentTimeContext: TimeContext | undefined,
+  fallbackTimeRange?: string
+): TimeContext {
+  const timezone = currentTimeContext?.timezone || currentTimeContext?.tz;
+  return sanitizeTimeContext({
+    timeRange: fallbackTimeRange || currentTimeContext?.timeRange,
+    refresh: currentTimeContext?.refresh,
+    live: currentTimeContext?.live,
+    tz: currentTimeContext?.tz || timezone,
+    timezone
+  });
+}
+
 export function buildHistoryChartEChartsOption({
   values,
   formatTime,
diff --git a/web-next/lib/monitor-detail/view-model.test.ts b/web-next/lib/monitor-detail/view-model.test.ts
index aae229873b..b1d5dadd9f 100644
--- a/web-next/lib/monitor-detail/view-model.test.ts
+++ b/web-next/lib/monitor-detail/view-model.test.ts
@@ -31,6 +31,7 @@ import {
   buildMonitorMetricTableMatrix,
   buildMonitorMetricTableEvidenceRows,
   buildMonitorMetricTableRows,
+  normalizeMonitorFavoriteNames,
   resolveMonitorMetricTableMode,
   buildMonitorNextRows,
   buildMonitorParamRows,
@@ -518,6 +519,18 @@ describe('monitor detail view model', () => {
     ]);
   });
 
+  it('keeps monitor favorites aligned to Angular Set semantics', () => {
+    expect(normalizeMonitorFavoriteNames(['cpu', 'cpu', '', undefined, 'cpu.usage', 'cpu.usage'])).toEqual(['cpu', 'cpu.usage']);
+    expect(
+      buildMonitorFavoriteJumpRows(
+        ['cpu', 'cpu', 'cpu.usage', 'cpu.usage'],
+        [{ name: 'cpu', fields: [{ field: 'usage' }] }] as any,
+        [{ metrics: 'cpu', metric: 'usage', unit: '%' }] as any,
+        t
+      ).map(row => row.key)
+    ).toEqual(['realtime:cpu', 'history:cpu.usage']);
+  });
+
   it('filters history metric catalog and history series rows by search text', () => {
     expect(
       filterMonitorHistoryMetricCatalog(
diff --git a/web-next/lib/monitor-detail/view-model.ts b/web-next/lib/monitor-detail/view-model.ts
index 400f4b62fd..2f0bf1e350 100644
--- a/web-next/lib/monitor-detail/view-model.ts
+++ b/web-next/lib/monitor-detail/view-model.ts
@@ -33,6 +33,10 @@ export type MonitorMetricTableMatrix = {
   }>;
 };
 
+export function normalizeMonitorFavoriteNames(favoriteNames: Array<string | null | undefined>): string[] {
+  return Array.from(new Set(favoriteNames.filter((name): name is string => Boolean(name))));
+}
+
 export function filterMonitorMetricTableMatrix(matrix: MonitorMetricTableMatrix, query: string): MonitorMetricTableMatrix {
   const normalizedQuery = normalizeMonitorDetailQuery(query);
   if (!normalizedQuery) return matrix;
@@ -799,9 +803,10 @@ export function buildMonitorFavoriteJumpRows(
   t: Translator
 ): MonitorFavoriteJumpRow[] {
   const rows: MonitorFavoriteJumpRow[] = [];
+  const favoriteNameSet = new Set(normalizeMonitorFavoriteNames(favoriteNames));
 
   for (const metric of metrics) {
-    if (favoriteNames.includes(metric.name)) {
+    if (favoriteNameSet.has(metric.name)) {
       rows.push({
         key: `realtime:${metric.name}`,
         title: metric.name,
@@ -816,7 +821,7 @@ export function buildMonitorFavoriteJumpRows(
 
   for (const item of historyMetrics) {
     const fullPath = `${item.metrics}.${item.metric}`;
-    if (favoriteNames.includes(fullPath) || favoriteNames.includes(item.metrics) || favoriteNames.includes(item.metric)) {
+    if (favoriteNameSet.has(fullPath) || favoriteNameSet.has(item.metrics) || favoriteNameSet.has(item.metric)) {
       rows.push({
         key: `history:${fullPath}`,
         title: fullPath,
@@ -824,9 +829,9 @@ export function buildMonitorFavoriteJumpRows(
         meta: t('monitor.detail.favorite.history.meta'),
         targetKey: `${item.metrics}:${item.metric}`,
         targetKind: 'history',
-        favoriteToken: favoriteNames.includes(fullPath)
+        favoriteToken: favoriteNameSet.has(fullPath)
           ? fullPath
-          : favoriteNames.includes(item.metrics)
+          : favoriteNameSet.has(item.metrics)
             ? item.metrics
             : item.metric
       });


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 21/50: feat(web-next): close M10 route cutover audit

Posted by zh...@apache.org.
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 <zq...@dromara.org>
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: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 45/50: fix(web-next): keep compact topology graphs at 1x

Posted by zh...@apache.org.
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 6afe1a7f0e151404b083c1ba03c4127970534b50
Author: Logic <zq...@dromara.org>
AuthorDate: Sun May 31 10:26:42 2026 +0800

    fix(web-next): keep compact topology graphs at 1x
---
 .../packages/hertzbeat-ui/src/topology-g6.test.tsx | 20 ++++++++-
 web-next/packages/hertzbeat-ui/src/topology-g6.tsx | 48 +++++++++++++++++++---
 2 files changed, 61 insertions(+), 7 deletions(-)

diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
index 47bd846a98..b4f71a5d5f 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx
@@ -6,6 +6,7 @@ import {
   buildHzTopologyG6Graph,
   buildHzTopologyG6FilterScope,
   buildHzTopologyG6GroupSummary,
+  buildHzTopologyG6InitialFitStrategy,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6NeighborFocus,
   buildHzTopologyG6RenderWindow,
@@ -363,6 +364,9 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     const source = topologyG6Source;
     const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={graph} />);
 
+    expect(source).toContain('buildHzTopologyG6InitialFitStrategy');
+    expect(source).toContain('initialFitStrategy === "center-only"');
+    expect(source).toContain('centerOnlyG6Viewport');
     expect(source).toContain('scheduleInitialFitView');
     expect(source).toContain('fitAndCenterG6Viewport');
     expect(source).toContain("fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, false)");
@@ -374,6 +378,8 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(html).toContain('data-hz-topology-g6-auto-fit-max-zoom="1"');
     expect(html).toContain('data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"');
     expect(html).toContain('data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range"');
+    expect(html).toContain('data-hz-topology-g6-initial-fit-strategy="center-only"');
+    expect(html).toContain('data-hz-topology-g6-initial-fit-strategy-owner="hertzbeat-ui-g6-initial-fit-strategy"');
     expect(html).toContain('data-hz-topology-g6-operator-zoom-bounds="0.18-2.2"');
     expect(html).toContain('data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"');
     expect(html).toContain('data-hz-topology-g6-fit-mode="overflow-only-center"');
@@ -384,6 +390,18 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(source).toContain('runtimeGraph.setZoomRange?.([HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_MAX_ZOOM])');
   });
 
+  it('keeps compact service graphs at readable one-to-one scale instead of fitting them to fill wide canvases', () => {
+    const compactGraph = buildHzTopologyG6ScaleFixture(7);
+    const denseGraph = buildHzTopologyG6ScaleFixture(50);
+    const compactHtml = renderToStaticMarkup(<HzTopologyG6Canvas graph={compactGraph} />);
+    const denseHtml = renderToStaticMarkup(<HzTopologyG6Canvas graph={denseGraph} />);
+
+    expect(buildHzTopologyG6InitialFitStrategy(compactGraph)).toBe('center-only');
+    expect(buildHzTopologyG6InitialFitStrategy(denseGraph)).toBe('overflow-fit');
+    expect(compactHtml).toContain('data-hz-topology-g6-initial-fit-strategy="center-only"');
+    expect(denseHtml).toContain('data-hz-topology-g6-initial-fit-strategy="overflow-fit"');
+  });
+
   it('centers the shared G6 canvas after fit and reset view actions', () => {
     const source = topologyG6Source;
     const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={buildHzTopologyG6ScaleFixture(8)} />);
@@ -1121,7 +1139,7 @@ describe('@hertzbeat/ui topology G6 canvas', () => {
     expect(source).toContain('initialFitTimerRef.current');
     expect(source).toContain('cancelPendingInitialFitView();');
     expect(source).toContain('if (source === "initial-fit" && hasUserViewportInteractedRef.current) return;');
-    expect(source).toContain('scheduleInitialFitView(runtimeGraph, () => !hasUserViewportInteractedRef.current)');
+    expect(source).toContain('scheduleInitialFitView(runtimeGraph, initialFitStrategy, () => !hasUserViewportInteractedRef.current)');
     expect(source).toContain('publishViewportTelemetryAfterViewportAction("wheel"');
     expect(source).toContain('publishViewportTelemetry("pointer-pan")');
     expect(source).toContain('publishViewportTelemetry("redraw-restore")');
diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
index 4374eb251d..86e954234a 100644
--- a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
+++ b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx
@@ -142,6 +142,8 @@ export type HzTopologyG6LargeGraphStrategy = {
   visibleNodeBudget: number;
 };
 
+export type HzTopologyG6InitialFitStrategy = 'center-only' | 'overflow-fit';
+
 export type HzTopologyG6RenderWindow = {
   graph: HzTopologyG6GraphInput;
   mode: 'direct' | 'windowed';
@@ -722,6 +724,12 @@ export function buildHzTopologyG6LargeGraphStrategy(input: HzTopologyG6GraphInpu
   };
 }
 
+export function buildHzTopologyG6InitialFitStrategy(input: HzTopologyG6GraphInput): HzTopologyG6InitialFitStrategy {
+  const nodeCount = input.nodes.length;
+  const edgeCount = input.edges.length;
+  return nodeCount > 0 && nodeCount <= 12 && edgeCount <= 18 ? 'center-only' : 'overflow-fit';
+}
+
 export function buildHzTopologyG6RenderWindow(
   input: HzTopologyG6GraphInput,
   strategy: HzTopologyG6LargeGraphStrategy = buildHzTopologyG6LargeGraphStrategy(input),
@@ -1150,6 +1158,15 @@ async function fitAndCenterG6Viewport(
   });
 }
 
+async function centerOnlyG6Viewport(
+  runtimeGraph: G6GraphRuntime | null | undefined,
+  animation: Record<string, unknown> | boolean
+) {
+  if (!runtimeGraph) return;
+  await runtimeGraph.zoomTo?.(HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM, animation);
+  await runtimeGraph.fitCenter?.(animation);
+}
+
 async function withG6AutoFitZoomRange(runtimeGraph: G6GraphRuntime, action: () => Promise<void>) {
   runtimeGraph.setZoomRange?.([HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM]);
   try {
@@ -1159,9 +1176,17 @@ async function withG6AutoFitZoomRange(runtimeGraph: G6GraphRuntime, action: () =
   }
 }
 
-function scheduleInitialFitView(runtimeGraph: G6GraphRuntime, shouldRun: () => boolean) {
+function scheduleInitialFitView(
+  runtimeGraph: G6GraphRuntime,
+  initialFitStrategy: HzTopologyG6InitialFitStrategy,
+  shouldRun: () => boolean
+) {
   return window.setTimeout(() => {
     if (!shouldRun()) return;
+    if (initialFitStrategy === "center-only") {
+      void centerOnlyG6Viewport(runtimeGraph, { duration: 120 });
+      return;
+    }
     void fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, { duration: 120 });
   }, 180);
 }
@@ -1398,6 +1423,7 @@ export function HzTopologyG6Canvas({
     () => buildHzTopologyG6RenderWindow(canvasGraph, largeGraphStrategy, { priorityNodeIds: renderWindowPriorityNodeIds }),
     [canvasGraph, largeGraphStrategy, renderWindowPriorityNodeIds]
   );
+  const initialFitStrategy = React.useMemo(() => buildHzTopologyG6InitialFitStrategy(renderWindow.graph), [renderWindow.graph]);
   const g6Graph = React.useMemo(() => buildHzTopologyG6Graph(renderWindow.graph), [renderWindow.graph]);
   const g6RenderKey = React.useMemo(() => buildHzTopologyG6RenderKey(g6Graph), [g6Graph]);
   latestG6GraphRef.current = g6Graph;
@@ -1614,10 +1640,14 @@ export function HzTopologyG6Canvas({
           clearSharedHover();
         });
         await runtimeGraph.render();
-        await fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, false);
+        if (initialFitStrategy === "center-only") {
+          await centerOnlyG6Viewport(runtimeGraph, false);
+        } else {
+          await fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, false);
+        }
         lastFitStructureKeyRef.current = graphStructureKey;
         lastDrawGraphKeyRef.current = latestG6RenderKeyRef.current;
-        initialFitTimerRef.current = scheduleInitialFitView(runtimeGraph, () => !hasUserViewportInteractedRef.current);
+        initialFitTimerRef.current = scheduleInitialFitView(runtimeGraph, initialFitStrategy, () => !hasUserViewportInteractedRef.current);
         publishViewportTelemetry("initial-fit");
         if (!disposed) setRenderState('ready');
         handleG6WheelViewportControl = event => {
@@ -1667,7 +1697,7 @@ export function HzTopologyG6Canvas({
     // The mount lifecycle intentionally excludes g6Graph so hover/selection style redraws do not destroy and refit the operator viewport.
     // Graph data updates flow through the setData/draw effect below.
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [cancelPendingInitialFitView, clearSharedHover, graphStructureKey, handleG6EdgeSelect, handleG6NodeFocus, handleG6NodeSelect, height, layout, markUserViewportInteraction, publishViewportTelemetry]);
+  }, [cancelPendingInitialFitView, clearSharedHover, graphStructureKey, handleG6EdgeSelect, handleG6NodeFocus, handleG6NodeSelect, height, initialFitStrategy, layout, markUserViewportInteraction, publishViewportTelemetry]);
 
   React.useEffect(() => {
     const runtimeGraph = graphRef.current;
@@ -1681,7 +1711,11 @@ export function HzTopologyG6Canvas({
       if (shouldFitAfterDataChange) {
         lastFitStructureKeyRef.current = graphStructureKey;
         lastDrawGraphKeyRef.current = g6RenderKey;
-        void fitAndCenterG6Viewport(runtimeGraph, { when: "overflow" }, false);
+        if (initialFitStrategy === "center-only") {
+          void centerOnlyG6Viewport(runtimeGraph, false);
+        } else {
+          void fitAndCenterG6Viewport(runtimeGraph, { when: "overflow" }, false);
+        }
       } else {
         await restoreG6ViewportSnapshot(runtimeGraph, snapshot);
         publishViewportTelemetry("redraw-restore");
@@ -1691,7 +1725,7 @@ export function HzTopologyG6Canvas({
     }).catch(error => {
       console.warn('HzTopologyG6Canvas failed to update AntV G6 data.', error);
     });
-  }, [g6Graph, g6RenderKey, graphStructureKey, publishViewportTelemetry, renderState]);
+  }, [g6Graph, g6RenderKey, graphStructureKey, initialFitStrategy, publishViewportTelemetry, renderState]);
 
   React.useEffect(() => {
     const runtimeGraph = graphRef.current;
@@ -1854,6 +1888,8 @@ export function HzTopologyG6Canvas({
       data-hz-topology-g6-auto-fit-max-zoom={HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM}
       data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"
       data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range"
+      data-hz-topology-g6-initial-fit-strategy={initialFitStrategy}
+      data-hz-topology-g6-initial-fit-strategy-owner="hertzbeat-ui-g6-initial-fit-strategy"
       data-hz-topology-g6-operator-zoom-bounds={`${HZ_TOPOLOGY_G6_MIN_ZOOM}-${HZ_TOPOLOGY_G6_MAX_ZOOM}`}
       data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"
       data-hz-topology-g6-fit-mode="overflow-only-center"


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org


(hertzbeat) 26/50: fix(remoting): use background task executor

Posted by zh...@apache.org.
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 778011d21990cc2f1f8031b565a43b94aee7c036
Author: Logic <zq...@dromara.org>
AuthorDate: Fri May 29 09:51:45 2026 +0800

    fix(remoting): use background task executor
    
    Validation:
    
    - ./mvnw -pl hertzbeat-remoting -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests clean compile
    
    - git diff --cached --check
    
    - cached trailing-whitespace scan
---
 .../org/apache/hertzbeat/remoting/netty/NettyRemotingServer.java  | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/hertzbeat-remoting/src/main/java/org/apache/hertzbeat/remoting/netty/NettyRemotingServer.java b/hertzbeat-remoting/src/main/java/org/apache/hertzbeat/remoting/netty/NettyRemotingServer.java
index 48062fadb9..814977c98f 100644
--- a/hertzbeat-remoting/src/main/java/org/apache/hertzbeat/remoting/netty/NettyRemotingServer.java
+++ b/hertzbeat-remoting/src/main/java/org/apache/hertzbeat/remoting/netty/NettyRemotingServer.java
@@ -44,13 +44,13 @@ import io.netty.handler.timeout.IdleStateHandler;
 import java.util.List;
 import java.util.concurrent.ThreadFactory;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor;
 import org.apache.hertzbeat.common.entity.message.ClusterMsg;
-import org.apache.hertzbeat.common.support.CommonThreadPool;
 import org.apache.hertzbeat.remoting.RemotingServer;
 import org.apache.hertzbeat.remoting.event.NettyEventListener;
 
 /**
- * Derived from Apache Rocketmq org.apache.rocketmq.remoting.netty.NettyRemotingServer 
+ * Derived from Apache Rocketmq org.apache.rocketmq.remoting.netty.NettyRemotingServer
  * netty server
  * @see <a href="https://github.com/apache/rocketmq/blob/develop/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java">NettyRemotingServer</a>
  */
@@ -59,7 +59,7 @@ public class NettyRemotingServer extends NettyRemotingAbstract implements Remoti
 
     private final NettyServerConfig nettyServerConfig;
 
-    private final CommonThreadPool threadPool;
+    private final BackgroundTaskExecutor threadPool;
 
     private EventLoopGroup bossGroup;
 
@@ -69,7 +69,7 @@ public class NettyRemotingServer extends NettyRemotingAbstract implements Remoti
 
     public NettyRemotingServer(final NettyServerConfig nettyServerConfig,
                                final NettyEventListener nettyEventListener,
-                               final CommonThreadPool threadPool) {
+                               final BackgroundTaskExecutor threadPool) {
         super(nettyEventListener);
         this.nettyServerConfig = nettyServerConfig;
         this.threadPool = threadPool;


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org